Reintroduce component index support for Jakarta annotations

Spring Framework 6.0 GA introduced a regression in the component index
support for Jakarta annotations such as @Named and @ManagedBean.

Prior to this commit, @Named and @ManagedBean components were
registered in the component index at build time; however, component
scanning failed to find those component at run time.

This commit updates ClassPathScanningCandidateComponentProvider so that
`jakarta.*` annotation types are once again supported for component
scanning via the component index at run time.

Closes gh-29641
This commit is contained in:
Sam Brannen 2022-12-06 16:18:13 -05:00
parent e124e802a3
commit f4bc9ffb98
7 changed files with 196 additions and 81 deletions

View File

@ -79,6 +79,7 @@ import org.springframework.util.ClassUtils;
* @author Ramnivas Laddad
* @author Chris Beams
* @author Stephane Nicoll
* @author Sam Brannen
* @since 2.5
* @see org.springframework.core.type.classreading.MetadataReaderFactory
* @see org.springframework.core.type.AnnotationMetadata
@ -343,7 +344,7 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
if (filter instanceof AnnotationTypeFilter annotationTypeFilter) {
Class<? extends Annotation> annotationType = annotationTypeFilter.getAnnotationType();
return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) ||
annotationType.getName().startsWith("javax."));
annotationType.getName().startsWith("jakarta."));
}
if (filter instanceof AssignableTypeFilter assignableTypeFilter) {
Class<?> target = assignableTypeFilter.getTargetType();

View File

@ -0,0 +1,24 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.indexed;
/**
* @author Sam Brannen
*/
@jakarta.annotation.ManagedBean
public class IndexedJakartaManagedBeanComponent {
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.indexed;
/**
* @author Sam Brannen
*/
@jakarta.inject.Named("myIndexedJakartaNamedComponent")
public class IndexedJakartaNamedComponent {
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.scannable;
/**
* @author Sam Brannen
*/
@jakarta.annotation.ManagedBean
public class JakartaManagedBeanComponent {
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.scannable;
/**
* @author Sam Brannen
*/
@jakarta.inject.Named("myJakartaNamedComponent")
public class JakartaNamedComponent {
}

View File

@ -18,10 +18,17 @@ package org.springframework.context.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import example.gh24375.AnnotatedComponent;
import example.indexed.IndexedJakartaManagedBeanComponent;
import example.indexed.IndexedJakartaNamedComponent;
import example.profilescan.DevComponent;
import example.profilescan.ProfileAnnotatedComponent;
import example.profilescan.ProfileMetaAnnotatedComponent;
@ -31,6 +38,8 @@ import example.scannable.DefaultNamedComponent;
import example.scannable.FooDao;
import example.scannable.FooService;
import example.scannable.FooServiceImpl;
import example.scannable.JakartaManagedBeanComponent;
import example.scannable.JakartaNamedComponent;
import example.scannable.MessageBean;
import example.scannable.NamedComponent;
import example.scannable.NamedStubDao;
@ -58,10 +67,13 @@ import org.springframework.stereotype.Service;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link ClassPathScanningCandidateComponentProvider}.
*
* @author Mark Fisher
* @author Juergen Hoeller
* @author Chris Beams
* @author Stephane Nicoll
* @author Sam Brannen
*/
class ClassPathScanningCandidateComponentProviderTests {
@ -79,27 +91,53 @@ class ClassPathScanningCandidateComponentProviderTests {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true);
provider.setResourceLoader(new DefaultResourceLoader(
CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader())));
testDefault(provider);
testDefault(provider, true, false);
}
@Test
void defaultsWithIndex() {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true);
provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER));
testDefault(provider);
testDefault(provider, "example", true, true);
}
private void testDefault(ClassPathScanningCandidateComponentProvider provider) {
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(containsBeanClass(candidates, DefaultNamedComponent.class)).isTrue();
assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue();
assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isTrue();
assertThat(containsBeanClass(candidates, StubFooDao.class)).isTrue();
assertThat(containsBeanClass(candidates, NamedStubDao.class)).isTrue();
assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue();
assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue();
assertThat(candidates).hasSize(7);
assertBeanDefinitionType(candidates);
private static final Set<Class<?>> springComponents = Set.of(
DefaultNamedComponent.class,
NamedComponent.class,
FooServiceImpl.class,
StubFooDao.class,
NamedStubDao.class,
ServiceInvocationCounter.class,
BarComponent.class
);
private static final Set<Class<?>> scannedJakartaComponents = Set.of(
JakartaNamedComponent.class,
JakartaManagedBeanComponent.class
);
private static final Set<Class<?>> indexedJakartaComponents = Set.of(
IndexedJakartaNamedComponent.class,
IndexedJakartaManagedBeanComponent.class
);
private void testDefault(ClassPathScanningCandidateComponentProvider provider, boolean includeScannedJakartaComponents, boolean includeIndexedJakartaComponents) {
testDefault(provider, TEST_BASE_PACKAGE, includeScannedJakartaComponents, includeIndexedJakartaComponents);
}
private void testDefault(ClassPathScanningCandidateComponentProvider provider, String basePackage, boolean includeScannedJakartaComponents, boolean includeIndexedJakartaComponents) {
Set<Class<?>> expectedTypes = new HashSet<>(springComponents);
if (includeScannedJakartaComponents) {
expectedTypes.addAll(scannedJakartaComponents);
}
if (includeIndexedJakartaComponents) {
expectedTypes.addAll(indexedJakartaComponents);
}
Set<BeanDefinition> candidates = provider.findCandidateComponents(basePackage);
assertScannedBeanDefinitions(candidates);
assertBeanTypes(candidates, expectedTypes);
}
@Test
@ -119,9 +157,8 @@ class ClassPathScanningCandidateComponentProviderTests {
private void testAntStyle(ClassPathScanningCandidateComponentProvider provider) {
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE + ".**.sub");
assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue();
assertThat(candidates).hasSize(1);
assertBeanDefinitionType(candidates);
assertScannedBeanDefinitions(candidates);
assertBeanTypes(candidates, BarComponent.class);
}
@Test
@ -148,7 +185,7 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addIncludeFilter(new AnnotationTypeFilter(Component.class));
provider.resetFilters(true);
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertBeanDefinitionType(candidates);
assertScannedBeanDefinitions(candidates);
}
@Test
@ -168,7 +205,7 @@ class ClassPathScanningCandidateComponentProviderTests {
private void testCustomAnnotationTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider) {
provider.addIncludeFilter(new AnnotationTypeFilter(Component.class));
testDefault(provider);
testDefault(provider, false, false);
}
@Test
@ -189,12 +226,9 @@ class ClassPathScanningCandidateComponentProviderTests {
private void testCustomAssignableTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider) {
provider.addIncludeFilter(new AssignableTypeFilter(FooService.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertScannedBeanDefinitions(candidates);
// Interfaces/Abstract class are filtered out automatically.
assertThat(containsBeanClass(candidates, AutowiredQualifierFooService.class)).isTrue();
assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isTrue();
assertThat(containsBeanClass(candidates, ScopedProxyTestBean.class)).isTrue();
assertThat(candidates).hasSize(3);
assertBeanDefinitionType(candidates);
assertBeanTypes(candidates, AutowiredQualifierFooService.class, FooServiceImpl.class, ScopedProxyTestBean.class);
}
@Test
@ -217,24 +251,20 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addExcludeFilter(new AnnotationTypeFilter(Service.class));
provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue();
assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue();
assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue();
assertThat(candidates).hasSize(3);
assertBeanDefinitionType(candidates);
assertScannedBeanDefinitions(candidates);
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
}
@Test
void customSupportIncludeFilterWithNonIndexedTypeUseScan() {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER));
// This annotation type is not directly annotated with Indexed so we can use
// the index to find candidates
// This annotation type is not directly annotated with @Indexed so we can use
// the index to find candidates.
provider.addIncludeFilter(new AnnotationTypeFilter(CustomStereotype.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(containsBeanClass(candidates, DefaultNamedComponent.class)).isTrue();
assertThat(candidates).hasSize(1);
assertBeanDefinitionType(candidates);
assertScannedBeanDefinitions(candidates);
assertBeanTypes(candidates, DefaultNamedComponent.class);
}
@Test
@ -243,9 +273,8 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER));
provider.addIncludeFilter(new AssignableTypeFilter(FooDao.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(containsBeanClass(candidates, StubFooDao.class)).isTrue();
assertThat(candidates).hasSize(1);
assertBeanDefinitionType(candidates);
assertScannedBeanDefinitions(candidates);
assertBeanTypes(candidates, StubFooDao.class);
}
@Test
@ -267,12 +296,9 @@ class ClassPathScanningCandidateComponentProviderTests {
private void testExclude(ClassPathScanningCandidateComponentProvider provider) {
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isTrue();
assertThat(containsBeanClass(candidates, StubFooDao.class)).isTrue();
assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue();
assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue();
assertThat(candidates).hasSize(4);
assertBeanDefinitionType(candidates);
assertScannedBeanDefinitions(candidates);
assertBeanTypes(candidates, FooServiceImpl.class, StubFooDao.class, ServiceInvocationCounter.class,
BarComponent.class, JakartaManagedBeanComponent.class);
}
@Test
@ -290,13 +316,7 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addExcludeFilter(new AnnotationTypeFilter(Service.class));
provider.addExcludeFilter(new AnnotationTypeFilter(Controller.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(candidates).hasSize(3);
assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue();
assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue();
assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue();
assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isFalse();
assertThat(containsBeanClass(candidates, StubFooDao.class)).isFalse();
assertThat(containsBeanClass(candidates, NamedStubDao.class)).isFalse();
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
}
@Test
@ -304,8 +324,7 @@ class ClassPathScanningCandidateComponentProviderTests {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(Aspect.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(candidates).hasSize(1);
assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue();
assertBeanTypes(candidates, ServiceInvocationCounter.class);
}
@Test
@ -313,8 +332,7 @@ class ClassPathScanningCandidateComponentProviderTests {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(FooDao.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(candidates).hasSize(1);
assertThat(containsBeanClass(candidates, StubFooDao.class)).isTrue();
assertBeanTypes(candidates, StubFooDao.class);
}
@Test
@ -322,8 +340,7 @@ class ClassPathScanningCandidateComponentProviderTests {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(MessageBean.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(candidates).hasSize(1);
assertThat(containsBeanClass(candidates, MessageBean.class)).isTrue();
assertBeanTypes(candidates, MessageBean.class);
}
@Test
@ -332,11 +349,8 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addIncludeFilter(new AnnotationTypeFilter(Component.class));
provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(candidates).hasSize(7);
assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue();
assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue();
assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isTrue();
assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue();
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, FooServiceImpl.class,
BarComponent.class, DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class);
}
@Test
@ -346,18 +360,15 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class));
provider.addExcludeFilter(new AssignableTypeFilter(FooService.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertThat(candidates).hasSize(6);
assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue();
assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue();
assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue();
assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isFalse();
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class,
DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class);
}
@Test
void withNullEnvironment() {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true);
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_PROFILE_PACKAGE);
assertThat(containsBeanClass(candidates, ProfileAnnotatedComponent.class)).isFalse();
assertThat(candidates).isEmpty();
}
@Test
@ -367,7 +378,7 @@ class ClassPathScanningCandidateComponentProviderTests {
env.setActiveProfiles("other");
provider.setEnvironment(env);
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_PROFILE_PACKAGE);
assertThat(containsBeanClass(candidates, ProfileAnnotatedComponent.class)).isFalse();
assertThat(candidates).isEmpty();
}
@Test
@ -377,7 +388,7 @@ class ClassPathScanningCandidateComponentProviderTests {
env.setActiveProfiles(ProfileAnnotatedComponent.PROFILE_NAME);
provider.setEnvironment(env);
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_PROFILE_PACKAGE);
assertThat(containsBeanClass(candidates, ProfileAnnotatedComponent.class)).isTrue();
assertBeanTypes(candidates, ProfileAnnotatedComponent.class);
}
@Test
@ -515,19 +526,22 @@ class ClassPathScanningCandidateComponentProviderTests {
}
private boolean containsBeanClass(Set<BeanDefinition> candidates, Class<?> beanClass) {
for (BeanDefinition candidate : candidates) {
if (beanClass.getName().equals(candidate.getBeanClassName())) {
return true;
}
}
return false;
private static void assertBeanTypes(Set<BeanDefinition> candidates, Class<?>... expectedTypes) {
assertBeanTypes(candidates, Arrays.stream(expectedTypes));
}
private void assertBeanDefinitionType(Set<BeanDefinition> candidates) {
candidates.forEach(c ->
assertThat(c).isInstanceOf(ScannedGenericBeanDefinition.class)
);
private static void assertBeanTypes(Set<BeanDefinition> candidates, Collection<Class<?>> expectedTypes) {
assertBeanTypes(candidates, expectedTypes.stream());
}
private static void assertBeanTypes(Set<BeanDefinition> candidates, Stream<Class<?>> expectedTypes) {
List<String> actualTypeNames = candidates.stream().map(BeanDefinition::getBeanClassName).distinct().sorted().toList();
List<String> expectedTypeNames = expectedTypes.map(Class::getName).distinct().sorted().toList();
assertThat(actualTypeNames).containsExactlyElementsOf(expectedTypeNames);
}
private static void assertScannedBeanDefinitions(Set<BeanDefinition> candidates) {
candidates.forEach(type -> assertThat(type).isInstanceOf(ScannedGenericBeanDefinition.class));
}

View File

@ -8,3 +8,7 @@ example.scannable.StubFooDao=org.springframework.stereotype.Component
example.scannable.NamedStubDao=org.springframework.stereotype.Component
example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component
example.scannable.sub.BarComponent=org.springframework.stereotype.Component
example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean
example.scannable.JakartaNamedComponent=jakarta.inject.Named
example.indexed.IndexedJakartaManagedBeanComponent=jakarta.annotation.ManagedBean
example.indexed.IndexedJakartaNamedComponent=jakarta.inject.Named