diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java index 2c9c9229eb..607fe64784 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java @@ -76,6 +76,7 @@ import org.springframework.util.StringUtils; * @author Jakub Kubrynski * @author Stephane Nicoll * @author Andy Wilkinson + * @author Uladzislau Seuruk * @see ConditionalOnBean * @see ConditionalOnMissingBean * @see ConditionalOnSingleCandidate @@ -205,13 +206,13 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat ConfigurableListableBeanFactory beanFactory = getSearchBeanFactory(spec); ClassLoader classLoader = spec.getContext().getClassLoader(); boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT; - Set> parameterizedContainers = spec.getParameterizedContainers(); + Set parameterizedContainers = spec.getParameterizedContainers(); MatchResult result = new MatchResult(); - Set beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy, + Set beansIgnoredByType = getNamesOfBeansIgnoredByType(beanFactory, considerHierarchy, spec.getIgnoredTypes(), parameterizedContainers); - for (String type : spec.getTypes()) { - Map typeMatchedDefinitions = getBeanDefinitionsForType(classLoader, - considerHierarchy, beanFactory, type, parameterizedContainers); + for (ResolvableType type : spec.getTypes()) { + Map typeMatchedDefinitions = getBeanDefinitionsForType(beanFactory, + considerHierarchy, type, parameterizedContainers); Set typeMatchedNames = matchedNamesFrom(typeMatchedDefinitions, (name, definition) -> !ScopedProxyUtils.isScopedTarget(name) && isCandidate(beanFactory, name, definition, beansIgnoredByType)); @@ -296,42 +297,31 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat return true; } - private Set getNamesOfBeansIgnoredByType(ClassLoader classLoader, ListableBeanFactory beanFactory, - boolean considerHierarchy, Set ignoredTypes, Set> parameterizedContainers) { + private Set getNamesOfBeansIgnoredByType(ListableBeanFactory beanFactory, boolean considerHierarchy, + Set ignoredTypes, Set parameterizedContainers) { Set result = null; - for (String ignoredType : ignoredTypes) { - Collection ignoredNames = getBeanDefinitionsForType(classLoader, considerHierarchy, beanFactory, - ignoredType, parameterizedContainers) + for (ResolvableType ignoredType : ignoredTypes) { + Collection ignoredNames = getBeanDefinitionsForType(beanFactory, considerHierarchy, ignoredType, + parameterizedContainers) .keySet(); result = addAll(result, ignoredNames); } return (result != null) ? result : Collections.emptySet(); } - private Map getBeanDefinitionsForType(ClassLoader classLoader, boolean considerHierarchy, - ListableBeanFactory beanFactory, String type, Set> parameterizedContainers) throws LinkageError { - try { - return getBeanDefinitionsForType(beanFactory, considerHierarchy, resolve(type, classLoader), - parameterizedContainers); - } - catch (ClassNotFoundException | NoClassDefFoundError ex) { - return Collections.emptyMap(); - } - } - private Map getBeanDefinitionsForType(ListableBeanFactory beanFactory, - boolean considerHierarchy, Class type, Set> parameterizedContainers) { + boolean considerHierarchy, ResolvableType type, Set parameterizedContainers) { Map result = collectBeanDefinitionsForType(beanFactory, considerHierarchy, type, parameterizedContainers, null); return (result != null) ? result : Collections.emptyMap(); } private Map collectBeanDefinitionsForType(ListableBeanFactory beanFactory, - boolean considerHierarchy, Class type, Set> parameterizedContainers, + boolean considerHierarchy, ResolvableType type, Set parameterizedContainers, Map result) { result = putAll(result, beanFactory.getBeanNamesForType(type, true, false), beanFactory); - for (Class container : parameterizedContainers) { - ResolvableType generic = ResolvableType.forClassWithGenerics(container, type); + for (ResolvableType parameterizedContainer : parameterizedContainers) { + ResolvableType generic = ResolvableType.forClassWithGenerics(parameterizedContainer.resolve(), type); result = putAll(result, beanFactory.getBeanNamesForType(generic, true, false), beanFactory); } if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory) { @@ -550,13 +540,13 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat private final Set names; - private final Set types; + private final Set types; private final Set annotations; - private final Set ignoredTypes; + private final Set ignoredTypes; - private final Set> parameterizedContainers; + private final Set parameterizedContainers; private final SearchStrategy strategy; @@ -570,10 +560,10 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat this.annotationType = annotationType; this.names = extract(attributes, "name"); this.annotations = extract(attributes, "annotation"); - this.ignoredTypes = extract(attributes, "ignored", "ignoredType"); + this.ignoredTypes = resolveWhenPossible(extract(attributes, "ignored", "ignoredType")); this.parameterizedContainers = resolveWhenPossible(extract(attributes, "parameterizedContainer")); this.strategy = annotation.getValue("search", SearchStrategy.class).orElse(null); - Set types = extractTypes(attributes); + Set types = resolveWhenPossible(extractTypes(attributes)); BeanTypeDeductionException deductionException = null; if (types.isEmpty() && this.names.isEmpty() && this.annotations.isEmpty()) { try { @@ -614,17 +604,18 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat Collections.addAll(result, additional); } - private Set> resolveWhenPossible(Set classNames) { + private Set resolveWhenPossible(Set classNames) { if (classNames.isEmpty()) { return Collections.emptySet(); } - Set> resolved = new LinkedHashSet<>(classNames.size()); + Set resolved = new LinkedHashSet<>(classNames.size()); for (String className : classNames) { try { - resolved.add(resolve(className, this.context.getClassLoader())); + Class type = resolve(className, this.context.getClassLoader()); + resolved.add(ResolvableType.forRawClass(type)); } catch (ClassNotFoundException | NoClassDefFoundError ex) { - // Ignore + resolved.add(ResolvableType.NONE); } } return resolved; @@ -653,48 +644,44 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat return "@" + ClassUtils.getShortName(this.annotationType); } - private Set deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) { + private Set deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) { if (metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName())) { return deducedBeanTypeForBeanMethod(context, (MethodMetadata) metadata); } return Collections.emptySet(); } - private Set deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) { + private Set deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) { try { - Class returnType = getReturnType(context, metadata); - return Collections.singleton(returnType.getName()); + return Set.of(getReturnType(context, metadata)); } catch (Throwable ex) { throw new BeanTypeDeductionException(metadata.getDeclaringClassName(), metadata.getMethodName(), ex); } } - private Class getReturnType(ConditionContext context, MethodMetadata metadata) + private ResolvableType getReturnType(ConditionContext context, MethodMetadata metadata) throws ClassNotFoundException, LinkageError { // Safe to load at this point since we are in the REGISTER_BEAN phase ClassLoader classLoader = context.getClassLoader(); - Class returnType = resolve(metadata.getReturnTypeName(), classLoader); - if (isParameterizedContainer(returnType)) { - returnType = getReturnTypeGeneric(metadata, classLoader); + ResolvableType returnType = getMethodReturnType(metadata, classLoader); + if (isParameterizedContainer(returnType.resolve())) { + returnType = returnType.getGeneric(); } return returnType; } private boolean isParameterizedContainer(Class type) { - for (Class parameterizedContainer : this.parameterizedContainers) { - if (parameterizedContainer.isAssignableFrom(type)) { - return true; - } - } - return false; + return (type != null) && this.parameterizedContainers.stream() + .map(ResolvableType::resolve) + .anyMatch((container) -> container != null && container.isAssignableFrom(type)); } - private Class getReturnTypeGeneric(MethodMetadata metadata, ClassLoader classLoader) + private ResolvableType getMethodReturnType(MethodMetadata metadata, ClassLoader classLoader) throws ClassNotFoundException, LinkageError { Class declaringClass = resolve(metadata.getDeclaringClassName(), classLoader); Method beanMethod = findBeanMethod(declaringClass, metadata.getMethodName()); - return ResolvableType.forMethodReturnType(beanMethod).resolveGeneric(); + return ResolvableType.forMethodReturnType(beanMethod); } private Method findBeanMethod(Class declaringClass, String methodName) { @@ -720,6 +707,10 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat return (this.strategy != null) ? this.strategy : SearchStrategy.ALL; } + Set getTypes() { + return this.types; + } + private ConditionContext getContext() { return this.context; } @@ -728,19 +719,15 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat return this.names; } - protected Set getTypes() { - return this.types; - } - private Set getAnnotations() { return this.annotations; } - private Set getIgnoredTypes() { + private Set getIgnoredTypes() { return this.ignoredTypes; } - private Set> getParameterizedContainers() { + private Set getParameterizedContainers() { return this.parameterizedContainers; } @@ -847,13 +834,13 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat this.unmatchedAnnotations.add(annotation); } - private void recordMatchedType(String type, Collection matchingNames) { - this.matchedTypes.put(type, matchingNames); + private void recordMatchedType(ResolvableType type, Collection matchingNames) { + this.matchedTypes.put(type.toString(), matchingNames); this.namesOfAllMatches.addAll(matchingNames); } - private void recordUnmatchedType(String type) { - this.unmatchedTypes.add(type); + private void recordUnmatchedType(ResolvableType type) { + this.unmatchedTypes.add(type.toString()); } boolean isAllMatched() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanGenericReturnTypeTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanGenericReturnTypeTests.java new file mode 100644 index 0000000000..ba3c649cac --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanGenericReturnTypeTests.java @@ -0,0 +1,209 @@ +/* + * Copyright 2012-2025 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 org.springframework.boot.autoconfigure.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnBean @ConditionalOnBean} with generic bean return type. + * + * @author Uladzislau Seuruk + */ +class ConditionalOnBeanGenericReturnTypeTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void genericWhenTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, GenericWithStringTypeArgumentsConfig.class, + GenericWithIntegerTypeArgumentsConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean", "genericStringTypeArgumentsExampleBean"))); + } + + @Test + void genericWhenTypeArgumentWithValueMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringConfig.class, TypeArgumentsConditionWithValueConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("genericStringExampleBean", "genericStringWithValueExampleBean"))); + } + + @Test + void genericWithValueWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, TypeArgumentsConditionWithValueConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean", "genericStringWithValueExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenTypeArgumentNotMatches() { + this.contextRunner + .withUserConfiguration(GenericWithIntegerConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("genericIntegerExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies( + exampleBeanRequirement("genericStringExampleBean", "parameterizedContainerGenericExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean", "parameterizedContainerGenericExampleBean"))); + } + + private Consumer exampleBeanRequirement(String... names) { + return (context) -> { + String[] beans = context.getBeanNamesForType(GenericExampleBean.class); + String[] containers = context.getBeanNamesForType(TestParameterizedContainer.class); + assertThat(StringUtils.concatenateStringArrays(beans, containers)).containsOnly(names); + }; + } + + @Configuration(proxyBeanMethods = false) + static class ParameterizedWithCustomConfig { + + @Bean + CustomExampleBean customExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithStringConfig { + + @Bean + GenericExampleBean genericStringExampleBean() { + return new GenericExampleBean<>("genericStringExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithStringTypeArgumentsConfig { + + @Bean + @ConditionalOnBean + GenericExampleBean genericStringTypeArgumentsExampleBean() { + return new GenericExampleBean<>("genericStringTypeArgumentsExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithIntegerConfig { + + @Bean + GenericExampleBean genericIntegerExampleBean() { + return new GenericExampleBean<>(1_000); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithIntegerTypeArgumentsConfig { + + @Bean + @ConditionalOnBean + GenericExampleBean genericIntegerTypeArgumentsExampleBean() { + return new GenericExampleBean<>(1_000); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TypeArgumentsConditionWithValueConfig { + + @Bean + @ConditionalOnBean(GenericExampleBean.class) + GenericExampleBean genericStringWithValueExampleBean() { + return new GenericExampleBean<>("genericStringWithValueExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TypeArgumentsConditionWithParameterizedContainerConfig { + + @Bean + @ConditionalOnBean(parameterizedContainer = TestParameterizedContainer.class) + TestParameterizedContainer> parameterizedContainerGenericExampleBean() { + return new TestParameterizedContainer<>(); + } + + } + + @TestAnnotation + static class GenericExampleBean { + + private final T value; + + GenericExampleBean(T value) { + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(this.value); + } + + } + + static class CustomExampleBean extends GenericExampleBean { + + CustomExampleBean() { + super("custom subclass"); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface TestAnnotation { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanGenericReturnTypeTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanGenericReturnTypeTests.java new file mode 100644 index 0000000000..3f60bd24e3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanGenericReturnTypeTests.java @@ -0,0 +1,204 @@ +/* + * Copyright 2012-2025 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 org.springframework.boot.autoconfigure.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnMissingBean @ConditionalOnMissingBean} with generic bean + * return type. + * + * @author Uladzislau Seuruk + */ +@SuppressWarnings("resource") +class ConditionalOnMissingBeanGenericReturnTypeTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void genericWhenTypeArgumentNotMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringTypeArgumentsConfig.class, + GenericWithIntegerTypeArgumentsConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("genericStringExampleBean", "genericIntegerExampleBean"))); + } + + @Test + void genericWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, GenericWithStringTypeArgumentsConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + void genericWhenSubclassTypeArgumentNotMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, GenericWithIntegerTypeArgumentsConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean", "genericIntegerExampleBean"))); + } + + @Test + void genericWhenTypeArgumentWithValueMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringTypeArgumentsConfig.class, + TypeArgumentsConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("genericStringExampleBean"))); + } + + @Test + void genericWithValueWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, TypeArgumentsConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenTypeArgumentNotMatches() { + this.contextRunner + .withUserConfiguration(GenericWithIntegerTypeArgumentsConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies( + exampleBeanRequirement("genericIntegerExampleBean", "parameterizedContainerGenericExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringTypeArgumentsConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("genericStringExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("customExampleBean"))); + } + + private Consumer exampleBeanRequirement(String... names) { + return (context) -> { + String[] beans = context.getBeanNamesForType(GenericExampleBean.class); + String[] containers = context.getBeanNamesForType(TestParameterizedContainer.class); + assertThat(StringUtils.concatenateStringArrays(beans, containers)).containsOnly(names); + }; + } + + @Configuration(proxyBeanMethods = false) + static class ParameterizedWithCustomConfig { + + @Bean + CustomExampleBean customExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithStringTypeArgumentsConfig { + + @Bean + @ConditionalOnMissingBean + GenericExampleBean genericStringExampleBean() { + return new GenericExampleBean<>("genericStringExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithIntegerTypeArgumentsConfig { + + @Bean + @ConditionalOnMissingBean + GenericExampleBean genericIntegerExampleBean() { + return new GenericExampleBean<>(1_000); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TypeArgumentsConditionWithValueConfig { + + @Bean + @ConditionalOnMissingBean(GenericExampleBean.class) + GenericExampleBean genericStringWithValueExampleBean() { + return new GenericExampleBean<>("genericStringWithValueExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TypeArgumentsConditionWithParameterizedContainerConfig { + + @Bean + @ConditionalOnMissingBean(parameterizedContainer = TestParameterizedContainer.class) + TestParameterizedContainer> parameterizedContainerGenericExampleBean() { + return new TestParameterizedContainer<>(); + } + + } + + @TestAnnotation + static class GenericExampleBean { + + private final T value; + + GenericExampleBean(T value) { + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(this.value); + } + + } + + static class CustomExampleBean extends GenericExampleBean { + + CustomExampleBean() { + super("custom subclass"); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface TestAnnotation { + + } + +}