Discover test config on enclosing classes for nested test classes

Prior to this commit (and since Spring Framework 5.0), Spring's
integration with JUnit Jupiter supported detection of test
configuration (e.g., @ContextConfiguration, etc.) on @Nested classes.
However, if a @Nested class did not declare its own test configuration,
Spring would not find the configuration from the enclosing class. This
is in contrast to Spring's support for automatic inheritance of test
configuration from superclasses. The only workaround was to
copy-n-paste the entire annotation configuration from enclosing classes
to nested tests classes, which is cumbersome and error prone.

This commit introduces a new @NestedTestConfiguration annotation that
allows one to choose the EnclosingConfiguration mode that Spring should
use when searching for test configuration on a @Nested test class.
Currently, the options are INHERIT or OVERRIDE, where the current
default is OVERRIDE. Note, however, that the default mode will be
changed to INHERIT in a subsequent commit. In addition, support will be
added to configure the global default mode via the SpringProperties
mechanism in order to allow development teams to revert to the behavior
prior to Spring Framework 5.3.

As of this commit, inheritance of the following annotations is honored
when the EnclosingConfiguration mode is INHERIT.

- @ContextConfiguration / @ContextHierarchy
- @ActiveProfiles
- @TestPropertySource / @TestPropertySources
- @WebAppConfiguration
- @TestConstructor
- @BootstrapWith
- @TestExecutionListeners
- @DirtiesContext
- @Transactional
- @Rollback / @Commit

This commit does NOT include support for inheriting the following
annotations on enclosing classes.

- @Sql / @SqlConfig / @SqlGroup

In order to implement this feature, the search algorithms in
MetaAnnotationUtils (and various other spring-test internals) have been
enhanced to detect when annotations should be looked up on enclosing
classes. Other parts of the ecosystem may find the new
searchEnclosingClass() method in MetaAnnotationUtils useful to provide
similar support.

As a side effect of the changes in this commit, validation of user
configuration in repeated @TestPropertySource declarations has been
removed, but this may be reintroduced at a later date.

Closes gh-19930
This commit is contained in:
Sam Brannen
2019-05-20 18:59:07 +02:00
committed by Sam Brannen
parent b9f7b0d955
commit 6641dbc852
30 changed files with 3185 additions and 949 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -16,16 +16,18 @@
package org.springframework.test.context;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.lang.Nullable;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.ClassUtils;
/**
@@ -56,6 +58,8 @@ abstract class BootstrapUtils {
private static final String WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME =
"org.springframework.test.context.web.WebAppConfiguration";
private static final Class<? extends Annotation> webAppConfigurationClass = loadWebAppConfigurationClass();
private static final Log logger = LogFactory.getLog(BootstrapUtils.class);
@@ -149,7 +153,14 @@ abstract class BootstrapUtils {
@Nullable
private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
Set<BootstrapWith> annotations = new LinkedHashSet<>();
AnnotationDescriptor<BootstrapWith> descriptor =
MetaAnnotationUtils.findAnnotationDescriptor(testClass, BootstrapWith.class);
while (descriptor != null) {
annotations.addAll(descriptor.findAllLocalMergedAnnotations());
descriptor = descriptor.next();
}
if (annotations.isEmpty()) {
return null;
}
@@ -169,13 +180,22 @@ abstract class BootstrapUtils {
}
private static Class<?> resolveDefaultTestContextBootstrapper(Class<?> testClass) throws Exception {
ClassLoader classLoader = BootstrapUtils.class.getClassLoader();
AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(testClass,
WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false);
if (attributes != null) {
return ClassUtils.forName(DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
boolean webApp = (MetaAnnotationUtils.findMergedAnnotation(testClass, webAppConfigurationClass) != null);
String bootstrapperClassName = (webApp ? DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME :
DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME);
return ClassUtils.forName(bootstrapperClassName, BootstrapUtils.class.getClassLoader());
}
@SuppressWarnings("unchecked")
private static Class<? extends Annotation> loadWebAppConfigurationClass() {
try {
return (Class<? extends Annotation>) ClassUtils.forName(WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME,
BootstrapUtils.class.getClassLoader());
}
catch (ClassNotFoundException | LinkageError ex) {
throw new IllegalStateException(
"Failed to load class for @" + WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, ex);
}
return ClassUtils.forName(DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2002-2020 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.test.context;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@code @NestedTestConfiguration} is a type-level annotation that is used to
* configure how Spring test configuration annotations are processed within
* enclosing class hierarchies (i.e., for <em>inner</em> test classes).
*
* <p>If {@code @NestedTestConfiguration} is not <em>present</em> or
* <em>meta-present</em> on a test class, configuration from the test class will
* not propagate to inner test classes (see {@link EnclosingConfiguration#OVERRIDE}).
* Consequently, inner test classes will have to declare their own Spring test
* configuration annotations. If you wish for an inner test class to inherit
* configuration from its enclosing class, annotate either the inner test class
* or the enclosing class with
* {@code @NestedTestConfiguration(EnclosingConfiguration.INHERIT)}. Note that
* a {@code @NestedTestConfiguration(...)} declaration is inherited within the
* superclass hierarchy as well as within the enclosing class hierarchy. Thus,
* there is no need to redeclare the annotation unless you wish to switch the
* mode.
*
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em>.
*
* <p>As of Spring Framework 5.3, the use of this annotation typically only makes
* sense in conjunction with {@link org.junit.jupiter.api.Nested @Nested} test
* classes in JUnit Jupiter.
*
* @author Sam Brannen
* @since 5.3
* @see EnclosingConfiguration#INHERIT
* @see EnclosingConfiguration#OVERRIDE
* @see ContextConfiguration @ContextConfiguration
* @see ContextHierarchy @ContextHierarchy
* @see ActiveProfiles @ActiveProfiles
* @see TestPropertySource @TestPropertySource
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface NestedTestConfiguration {
/**
* Configures the {@link EnclosingConfiguration} mode.
*/
EnclosingConfiguration value();
/**
* Enumeration of <em>modes</em> that dictate how test configuration from
* enclosing classes is processed for inner test classes.
*/
enum EnclosingConfiguration {
/**
* Indicates that test configuration for an inner test class should be
* <em>inherited</em> from its {@linkplain Class#getEnclosingClass()
* enclosing class}, as if the enclosing class were a superclass.
*/
INHERIT,
/**
* Indicates that test configuration for an inner test class should
* <em>override</em> configuration from its
* {@linkplain Class#getEnclosingClass() enclosing class}.
*/
OVERRIDE
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2020 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.
@@ -29,6 +29,7 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.annotation.DirtiesContext.MethodMode;
import org.springframework.test.context.TestContext;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.util.Assert;
/**
@@ -96,7 +97,7 @@ public abstract class AbstractDirtiesContextTestExecutionListener extends Abstra
Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
DirtiesContext methodAnn = AnnotatedElementUtils.findMergedAnnotation(testMethod, DirtiesContext.class);
DirtiesContext classAnn = AnnotatedElementUtils.findMergedAnnotation(testClass, DirtiesContext.class);
DirtiesContext classAnn = MetaAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class);
boolean methodAnnotated = (methodAnn != null);
boolean classAnnotated = (classAnn != null);
MethodMode methodMode = (methodAnnotated ? methodAnn.methodMode() : null);
@@ -133,7 +134,7 @@ public abstract class AbstractDirtiesContextTestExecutionListener extends Abstra
Class<?> testClass = testContext.getTestClass();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
DirtiesContext dirtiesContext = AnnotatedElementUtils.findMergedAnnotation(testClass, DirtiesContext.class);
DirtiesContext dirtiesContext = MetaAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class);
boolean classAnnotated = (dirtiesContext != null);
ClassMode classMode = (classAnnotated ? dirtiesContext.classMode() : null);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -31,7 +31,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Nullable;
import org.springframework.test.context.BootstrapContext;
@@ -139,13 +138,11 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
}
boolean inheritListeners = testExecutionListeners.inheritListeners();
AnnotationDescriptor<TestExecutionListeners> superDescriptor =
MetaAnnotationUtils.findAnnotationDescriptor(
descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
AnnotationDescriptor<TestExecutionListeners> parentDescriptor = descriptor.next();
// If there are no listeners to inherit, we might need to merge the
// locally declared listeners with the defaults.
if ((!inheritListeners || superDescriptor == null) &&
if ((!inheritListeners || parentDescriptor == null) &&
testExecutionListeners.mergeMode() == MergeMode.MERGE_WITH_DEFAULTS) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Merging default listeners with listeners configured via " +
@@ -157,7 +154,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners()));
descriptor = (inheritListeners ? superDescriptor : null);
descriptor = (inheritListeners ? parentDescriptor : null);
}
}
@@ -265,7 +262,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
}
if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) {
if (MetaAnnotationUtils.findAnnotationDescriptor(testClass, ContextHierarchy.class) != null) {
Map<String, List<ContextConfigurationAttributes>> hierarchyMap =
ContextLoaderUtils.buildContextHierarchyMap(testClass);
MergedContextConfiguration parentConfig = null;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 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.
@@ -28,12 +28,13 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ActiveProfilesResolver;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptor;
/**
* Utility methods for working with {@link ActiveProfiles @ActiveProfiles} and
* {@link ActiveProfilesResolver ActiveProfilesResolvers}.
@@ -70,25 +71,21 @@ abstract class ActiveProfilesUtils {
static String[] resolveActiveProfiles(Class<?> testClass) {
Assert.notNull(testClass, "Class must not be null");
final List<String[]> profileArrays = new ArrayList<>();
Class<ActiveProfiles> annotationType = ActiveProfiles.class;
AnnotationDescriptor<ActiveProfiles> descriptor =
MetaAnnotationUtils.findAnnotationDescriptor(testClass, annotationType);
List<String[]> profileArrays = new ArrayList<>();
AnnotationDescriptor<ActiveProfiles> descriptor = findAnnotationDescriptor(testClass, ActiveProfiles.class);
if (descriptor == null && logger.isDebugEnabled()) {
logger.debug(String.format(
"Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
annotationType.getName(), testClass.getName()));
ActiveProfiles.class.getName(), testClass.getName()));
}
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getRootDeclaringClass();
Class<?> declaringClass = descriptor.getDeclaringClass();
ActiveProfiles annotation = descriptor.synthesizeAnnotation();
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s]",
annotation, declaringClass.getName()));
annotation, descriptor.getDeclaringClass().getName()));
}
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
@@ -112,14 +109,13 @@ abstract class ActiveProfilesUtils {
profileArrays.add(profiles);
}
descriptor = (annotation.inheritProfiles() ? MetaAnnotationUtils.findAnnotationDescriptor(
rootDeclaringClass.getSuperclass(), annotationType) : null);
descriptor = (annotation.inheritProfiles() ? descriptor.next() : null);
}
// Reverse the list so that we can traverse "down" the hierarchy.
Collections.reverse(profileArrays);
final Set<String> activeProfiles = new LinkedHashSet<>();
Set<String> activeProfiles = new LinkedHashSet<>();
for (String[] profiles : profileArrays) {
for (String profile : profiles) {
if (StringUtils.hasText(profile)) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -150,8 +150,8 @@ abstract class ContextLoaderUtils {
}
hierarchyAttributes.add(0, configAttributesList);
desc = findAnnotationDescriptorForTypes(
rootDeclaringClass.getSuperclass(), contextConfigType, contextHierarchyType);
desc = desc.next();
}
return hierarchyAttributes;
@@ -182,7 +182,7 @@ abstract class ContextLoaderUtils {
* @see #resolveContextHierarchyAttributes(Class)
*/
static Map<String, List<ContextConfigurationAttributes>> buildContextHierarchyMap(Class<?> testClass) {
final Map<String, List<ContextConfigurationAttributes>> map = new LinkedHashMap<>();
Map<String, List<ContextConfigurationAttributes>> map = new LinkedHashMap<>();
int hierarchyLevel = 1;
for (List<ContextConfigurationAttributes> configAttributesList : resolveContextHierarchyAttributes(testClass)) {
@@ -237,30 +237,34 @@ abstract class ContextLoaderUtils {
static List<ContextConfigurationAttributes> resolveContextConfigurationAttributes(Class<?> testClass) {
Assert.notNull(testClass, "Class must not be null");
List<ContextConfigurationAttributes> attributesList = new ArrayList<>();
Class<ContextConfiguration> annotationType = ContextConfiguration.class;
AnnotationDescriptor<ContextConfiguration> descriptor = findAnnotationDescriptor(testClass, annotationType);
Assert.notNull(descriptor, () -> String.format(
"Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
annotationType.getName(), testClass.getName()));
while (descriptor != null) {
convertContextConfigToConfigAttributesAndAddToList(descriptor.synthesizeAnnotation(),
descriptor.getRootDeclaringClass(), attributesList);
descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
}
List<ContextConfigurationAttributes> attributesList = new ArrayList<>();
resolveContextConfigurationAttributes(attributesList, descriptor);
return attributesList;
}
private static void resolveContextConfigurationAttributes(List<ContextConfigurationAttributes> attributesList,
AnnotationDescriptor<ContextConfiguration> descriptor) {
if (descriptor != null) {
convertContextConfigToConfigAttributesAndAddToList(descriptor.synthesizeAnnotation(),
descriptor.getRootDeclaringClass(), attributesList);
resolveContextConfigurationAttributes(attributesList, descriptor.next());
}
}
/**
* Convenience method for creating a {@link ContextConfigurationAttributes}
* instance from the supplied {@link ContextConfiguration} annotation and
* declaring class and then adding the attributes to the supplied list.
*/
private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration,
Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) {
Class<?> declaringClass, List<ContextConfigurationAttributes> attributesList) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].",

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2020 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.
@@ -16,6 +16,10 @@
package org.springframework.test.context.support;
import java.util.Arrays;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.Assert;
@@ -76,4 +80,55 @@ class MergedTestPropertySources {
return this.properties;
}
/**
* Determine if the supplied object is equal to this {@code MergedTestPropertySources}
* instance by comparing both object's {@linkplain #getLocations() locations}
* and {@linkplain #getProperties() properties}.
* @since 5.3
*/
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || other.getClass() != getClass()) {
return false;
}
MergedTestPropertySources that = (MergedTestPropertySources) other;
if (!Arrays.equals(this.locations, that.locations)) {
return false;
}
if (!Arrays.equals(this.properties, that.properties)) {
return false;
}
return true;
}
/**
* Generate a unique hash code for all properties of this
* {@code MergedTestPropertySources} instance.
* @since 5.3
*/
@Override
public int hashCode() {
int result = Arrays.hashCode(this.locations);
result = 31 * result + Arrays.hashCode(this.properties);
return result;
}
/**
* Provide a String representation of this {@code MergedTestPropertySources}
* instance.
* @since 5.3
*/
@Override
public String toString() {
return new ToStringCreator(this)
.append("locations", Arrays.toString(this.locations))
.append("properties", Arrays.toString(this.properties))
.toString();
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.test.context.TestConstructor;
import org.springframework.test.context.TestConstructor.AutowireMode;
import org.springframework.test.util.MetaAnnotationUtils;
/**
* Utility methods for working with {@link TestConstructor @TestConstructor}.
@@ -133,7 +134,7 @@ public abstract class TestConstructorUtils {
AutowireMode autowireMode = null;
// Is the test class annotated with @TestConstructor?
TestConstructor testConstructor = AnnotatedElementUtils.findMergedAnnotation(testClass, TestConstructor.class);
TestConstructor testConstructor = MetaAnnotationUtils.findMergedAnnotation(testClass, TestConstructor.class);
if (testConstructor != null) {
autowireMode = testConstructor.autowireMode();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -17,7 +17,7 @@
package org.springframework.test.context.support;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
@@ -25,7 +25,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.log.LogMessage;
import org.springframework.core.style.ToStringCreator;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.Assert;
@@ -52,12 +51,8 @@ class TestPropertySourceAttributes {
private static final Log logger = LogFactory.getLog(TestPropertySourceAttributes.class);
private final int aggregateIndex;
private final Class<?> declaringClass;
private final MergedAnnotation<?> rootAnnotation;
private final List<String> locations = new ArrayList<>();
private final boolean inheritLocations;
@@ -68,96 +63,24 @@ class TestPropertySourceAttributes {
TestPropertySourceAttributes(MergedAnnotation<TestPropertySource> annotation) {
this.aggregateIndex = annotation.getAggregateIndex();
this.declaringClass = declaringClass(annotation);
this.rootAnnotation = annotation.getRoot();
this.inheritLocations = annotation.getBoolean("inheritLocations");
this.inheritProperties = annotation.getBoolean("inheritProperties");
mergePropertiesAndLocations(annotation);
}
/**
* Determine if the annotation represented by this
* {@code TestPropertySourceAttributes} instance can be merged with the
* supplied {@code annotation}.
* <p>This method effectively checks that two annotations are declared at
* the same level in the type hierarchy (i.e., have the same
* {@linkplain MergedAnnotation#getAggregateIndex() aggregate index}).
* @since 5.2
* @see #mergeWith(MergedAnnotation)
*/
boolean canMergeWith(MergedAnnotation<TestPropertySource> annotation) {
return annotation.getAggregateIndex() == this.aggregateIndex;
}
/**
* Merge this {@code TestPropertySourceAttributes} instance with the
* supplied {@code annotation}, asserting that the two sets of test property
* source attributes have identical values for the
* {@link TestPropertySource#inheritLocations} and
* {@link TestPropertySource#inheritProperties} flags and that the two
* underlying annotations were declared on the same class.
* <p>This method should only be invoked if {@link #canMergeWith(MergedAnnotation)}
* returns {@code true}.
* @since 5.2
* @see #canMergeWith(MergedAnnotation)
*/
void mergeWith(MergedAnnotation<TestPropertySource> annotation) {
Class<?> source = declaringClass(annotation);
Assert.state(source == this.declaringClass,
() -> "Detected @TestPropertySource declarations within an aggregate index "
+ "with different sources: " + this.declaringClass.getName() + " and "
+ source.getName());
logger.trace(LogMessage.format("Retrieved %s for declaring class [%s].",
annotation, this.declaringClass.getName()));
assertSameBooleanAttribute(this.inheritLocations, annotation, "inheritLocations");
assertSameBooleanAttribute(this.inheritProperties, annotation, "inheritProperties");
mergePropertiesAndLocations(annotation);
}
private void assertSameBooleanAttribute(boolean expected, MergedAnnotation<TestPropertySource> annotation,
String attribute) {
Assert.isTrue(expected == annotation.getBoolean(attribute), () -> String.format(
"@%s on %s and @%s on %s must declare the same value for '%s' as other " +
"directly present or meta-present @TestPropertySource annotations",
this.rootAnnotation.getType().getSimpleName(), this.declaringClass.getSimpleName(),
annotation.getRoot().getType().getSimpleName(), declaringClass(annotation).getSimpleName(),
attribute));
}
private void mergePropertiesAndLocations(MergedAnnotation<TestPropertySource> annotation) {
String[] locations = annotation.getStringArray("locations");
String[] properties = annotation.getStringArray("properties");
// If the meta-distance is positive, that means the annotation is
// meta-present and should therefore have lower priority than directly
// present annotations (i.e., it should be prepended to the list instead
// of appended). This follows the rule of last-one-wins for overriding
// properties.
boolean prepend = annotation.getDistance() > 0;
if (ObjectUtils.isEmpty(locations) && ObjectUtils.isEmpty(properties)) {
addAll(prepend, this.locations, detectDefaultPropertiesFile(annotation));
Collections.addAll(this.locations, detectDefaultPropertiesFile(annotation));
}
else {
addAll(prepend, this.locations, locations);
addAll(prepend, this.properties, properties);
Collections.addAll(this.locations, locations);
Collections.addAll(this.properties, properties);
}
}
/**
* Add all of the supplied elements to the provided list, honoring the
* {@code prepend} flag.
* <p>If the {@code prepend} flag is {@code false}, the elements will appended
* to the list.
* @param prepend whether the elements should be prepended to the list
* @param list the list to which to add the elements
* @param elements the elements to add to the list
*/
private void addAll(boolean prepend, List<String> list, String... elements) {
list.addAll((prepend ? 0 : list.size()), Arrays.asList(elements));
}
private String detectDefaultPropertiesFile(MergedAnnotation<TestPropertySource> annotation) {
Class<?> testClass = declaringClass(annotation);
String resourcePath = ClassUtils.convertClassNameToResourcePath(testClass.getName()) + ".properties";

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -18,12 +18,15 @@ package org.springframework.test.context.support;
import java.io.IOException;
import java.io.StringReader;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -42,6 +45,7 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePropertySource;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.util.TestContextResourceUtils;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -71,42 +75,22 @@ public abstract class TestPropertySourceUtils {
static MergedTestPropertySources buildMergedTestPropertySources(Class<?> testClass) {
MergedAnnotations mergedAnnotations = MergedAnnotations.from(testClass, SearchStrategy.TYPE_HIERARCHY);
return (mergedAnnotations.isPresent(TestPropertySource.class) ? mergeTestPropertySources(mergedAnnotations) :
MergedTestPropertySources.empty());
return mergeTestPropertySources(findRepeatableAnnotations(testClass, TestPropertySource.class));
}
private static MergedTestPropertySources mergeTestPropertySources(MergedAnnotations mergedAnnotations) {
List<TestPropertySourceAttributes> attributesList = resolveTestPropertySourceAttributes(mergedAnnotations);
private static MergedTestPropertySources mergeTestPropertySources(
List<MergedAnnotation<TestPropertySource>> mergedAnnotations) {
if (mergedAnnotations.isEmpty()) {
return MergedTestPropertySources.empty();
}
List<TestPropertySourceAttributes> attributesList = mergedAnnotations.stream()
.map(TestPropertySourceAttributes::new)
.collect(Collectors.toList());
return new MergedTestPropertySources(mergeLocations(attributesList), mergeProperties(attributesList));
}
private static List<TestPropertySourceAttributes> resolveTestPropertySourceAttributes(
MergedAnnotations mergedAnnotations) {
List<TestPropertySourceAttributes> attributesList = new ArrayList<>();
mergedAnnotations.stream(TestPropertySource.class)
.forEach(annotation -> addOrMergeTestPropertySourceAttributes(attributesList, annotation));
return attributesList;
}
private static void addOrMergeTestPropertySourceAttributes(List<TestPropertySourceAttributes> attributesList,
MergedAnnotation<TestPropertySource> current) {
if (attributesList.isEmpty()) {
attributesList.add(new TestPropertySourceAttributes(current));
}
else {
TestPropertySourceAttributes previous = attributesList.get(attributesList.size() - 1);
if (previous.canMergeWith(current)) {
previous.mergeWith(current);
}
else {
attributesList.add(new TestPropertySourceAttributes(current));
}
}
}
private static String[] mergeLocations(List<TestPropertySourceAttributes> attributesList) {
List<String> locations = new ArrayList<>();
for (TestPropertySourceAttributes attrs : attributesList) {
@@ -292,4 +276,53 @@ public abstract class TestPropertySourceUtils {
return map;
}
private static <T extends Annotation> List<MergedAnnotation<T>> findRepeatableAnnotations(
Class<?> clazz, Class<T> annotationType) {
List<List<MergedAnnotation<T>>> listOfLists = new ArrayList<>();
findRepeatableAnnotations(clazz, annotationType, listOfLists, new int[] {0});
return listOfLists.stream().flatMap(List::stream).collect(Collectors.toList());
}
private static <T extends Annotation> void findRepeatableAnnotations(
Class<?> clazz, Class<T> annotationType, List<List<MergedAnnotation<T>>> listOfLists, int[] aggregateIndex) {
MergedAnnotations.from(clazz, SearchStrategy.DIRECT)
.stream(annotationType)
.sorted(highMetaDistancesFirst())
.forEach(annotation -> {
List<MergedAnnotation<T>> current = null;
if (listOfLists.size() < aggregateIndex[0] + 1) {
current = new ArrayList<>();
listOfLists.add(current);
}
else {
current = listOfLists.get(aggregateIndex[0]);
}
current.add(0, annotation);
});
aggregateIndex[0]++;
// Declared on an interface?
for (Class<?> ifc : clazz.getInterfaces()) {
findRepeatableAnnotations(ifc, annotationType, listOfLists, aggregateIndex);
}
// Declared on a superclass?
Class<?> superclass = clazz.getSuperclass();
if (superclass != null & superclass != Object.class) {
findRepeatableAnnotations(superclass, annotationType, listOfLists, aggregateIndex);
}
// Declared on an enclosing class of an inner class?
if (MetaAnnotationUtils.searchEnclosingClass(clazz)) {
findRepeatableAnnotations(clazz.getEnclosingClass(), annotationType, listOfLists, aggregateIndex);
}
}
private static <A extends Annotation> Comparator<MergedAnnotation<A>> highMetaDistancesFirst() {
return Comparator.<MergedAnnotation<A>> comparingInt(MergedAnnotation::getDistance).reversed();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -36,6 +36,7 @@ import org.springframework.test.annotation.Commit;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
@@ -148,7 +149,27 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class);
// Do not require @Transactional test methods to be public.
protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false);
@SuppressWarnings("serial")
protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false) {
@Override
protected TransactionAttribute findTransactionAttribute(Class<?> clazz) {
// @Transactional present in inheritance hierarchy?
TransactionAttribute result = super.findTransactionAttribute(clazz);
if (result != null) {
return result;
}
// @Transactional present in enclosing class hierarchy?
return findTransactionAttributeInEnclosingClassHierarchy(clazz);
}
private TransactionAttribute findTransactionAttributeInEnclosingClassHierarchy(Class<?> clazz) {
if (MetaAnnotationUtils.searchEnclosingClass(clazz)) {
return findTransactionAttribute(clazz.getEnclosingClass());
}
return null;
}
};
/**
@@ -376,7 +397,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
*/
protected final boolean isDefaultRollback(TestContext testContext) throws Exception {
Class<?> testClass = testContext.getTestClass();
Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class);
Rollback rollback = MetaAnnotationUtils.findMergedAnnotation(testClass, Rollback.class);
boolean rollbackPresent = (rollback != null);
if (rollbackPresent) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2020 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.
@@ -16,11 +16,11 @@
package org.springframework.test.context.web;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextBootstrapper;
import org.springframework.test.context.support.DefaultTestContextBootstrapper;
import org.springframework.test.util.MetaAnnotationUtils;
/**
* Web-specific implementation of the {@link TestContextBootstrapper} SPI.
@@ -45,7 +45,7 @@ public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper {
*/
@Override
protected Class<? extends ContextLoader> getDefaultContextLoaderClass(Class<?> testClass) {
if (AnnotatedElementUtils.hasAnnotation(testClass, WebAppConfiguration.class)) {
if (getWebAppConfiguration(testClass) != null) {
return WebDelegatingSmartContextLoader.class;
}
else {
@@ -61,8 +61,7 @@ public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper {
*/
@Override
protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) {
WebAppConfiguration webAppConfiguration =
AnnotatedElementUtils.findMergedAnnotation(mergedConfig.getTestClass(), WebAppConfiguration.class);
WebAppConfiguration webAppConfiguration = getWebAppConfiguration(mergedConfig.getTestClass());
if (webAppConfiguration != null) {
return new WebMergedContextConfiguration(mergedConfig, webAppConfiguration.value());
}
@@ -71,4 +70,8 @@ public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper {
}
}
private static WebAppConfiguration getWebAppConfiguration(Class<?> testClass) {
return MetaAnnotationUtils.findMergedAnnotation(testClass, WebAppConfiguration.class);
}
}

View File

@@ -23,15 +23,27 @@ import java.util.Set;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationCollectors;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.NestedTestConfiguration;
import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.ObjectUtils;
/**
* {@code MetaAnnotationUtils} is a collection of utility methods that complements
* the standard support already available in {@link AnnotationUtils}.
*
* <p>Mainly for internal use within the framework.
*
* <p>Whereas {@code AnnotationUtils} provides utilities for <em>getting</em> or
* <em>finding</em> an annotation, {@code MetaAnnotationUtils} goes a step further
* by providing support for determining the <em>root class</em> on which an
@@ -56,6 +68,34 @@ import org.springframework.util.ObjectUtils;
*/
public abstract class MetaAnnotationUtils {
private static final ConcurrentLruCache<Class<?>, SearchStrategy> cachedSearchStrategies =
new ConcurrentLruCache<>(32, MetaAnnotationUtils::lookUpSearchStrategy);
/**
* Find the first annotation of the specified {@code annotationType} within
* the annotation hierarchy <em>above</em> the supplied class, merge that
* annotation's attributes with <em>matching</em> attributes from annotations
* in lower levels of the annotation hierarchy, and synthesize the result back
* into an annotation of the specified {@code annotationType}.
* <p>In the context of this method, the term "above" means within the
* {@linkplain Class#getSuperclass() superclass} hierarchy or within the
* {@linkplain Class#getEnclosingClass() enclosing class} hierarchy of the
* supplied class. The enclosing class hierarchy will only be searched if
* appropriate.
* @param clazz the class to look for annotations on
* @param annotationType the type of annotation to look for
* @return the merged, synthesized {@code Annotation}, or {@code null} if not found
* @since 5.3
* @see AnnotatedElementUtils#findMergedAnnotation(java.lang.reflect.AnnotatedElement, Class)
* @see #findAnnotationDescriptor(Class, Class)
*/
@Nullable
public static <T extends Annotation> T findMergedAnnotation(Class<?> clazz, Class<T> annotationType) {
AnnotationDescriptor<T> descriptor = findAnnotationDescriptor(clazz, annotationType);
return (descriptor != null ? descriptor.synthesizeAnnotation() : null);
}
/**
* Find the {@link AnnotationDescriptor} for the supplied {@code annotationType}
* on the supplied {@link Class}, traversing its annotations, interfaces, and
@@ -123,7 +163,7 @@ public abstract class MetaAnnotationUtils {
}
}
// Declared on interface?
// Declared on an interface?
for (Class<?> ifc : clazz.getInterfaces()) {
AnnotationDescriptor<T> descriptor = findAnnotationDescriptor(ifc, visited, annotationType);
if (descriptor != null) {
@@ -133,7 +173,21 @@ public abstract class MetaAnnotationUtils {
}
// Declared on a superclass?
return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType);
AnnotationDescriptor<T> descriptor =
findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType);
if (descriptor != null) {
return descriptor;
}
// Declared on an enclosing class of an inner class?
if (searchEnclosingClass(clazz)) {
descriptor = findAnnotationDescriptor(clazz.getEnclosingClass(), visited, annotationType);
if (descriptor != null) {
return descriptor;
}
}
return null;
}
/**
@@ -196,7 +250,7 @@ public abstract class MetaAnnotationUtils {
// Declared locally?
for (Class<? extends Annotation> annotationType : annotationTypes) {
if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, clazz)) {
return new UntypedAnnotationDescriptor(clazz, clazz.getAnnotation(annotationType));
return new UntypedAnnotationDescriptor(clazz, clazz.getAnnotation(annotationType), annotationTypes);
}
}
@@ -207,22 +261,75 @@ public abstract class MetaAnnotationUtils {
composedAnnotation.annotationType(), visited, annotationTypes);
if (descriptor != null) {
return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(),
composedAnnotation, descriptor.getAnnotation());
composedAnnotation, descriptor.getAnnotation(), annotationTypes);
}
}
}
// Declared on interface?
// Declared on an interface?
for (Class<?> ifc : clazz.getInterfaces()) {
UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(ifc, visited, annotationTypes);
if (descriptor != null) {
return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(),
descriptor.getComposedAnnotation(), descriptor.getAnnotation());
descriptor.getComposedAnnotation(), descriptor.getAnnotation(), annotationTypes);
}
}
// Declared on a superclass?
return findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes);
UntypedAnnotationDescriptor descriptor =
findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes);
if (descriptor != null) {
return descriptor;
}
// Declared on an enclosing class of an inner class?
if (searchEnclosingClass(clazz)) {
descriptor = findAnnotationDescriptorForTypes(clazz.getEnclosingClass(), visited, annotationTypes);
if (descriptor != null) {
return descriptor;
}
}
return null;
}
/**
* Determine if annotations on the enclosing class of the supplied class
* should be searched by algorithms in {@link MetaAnnotationUtils}.
* @param clazz the class whose enclosing class should potentially be searched
* @return {@code true} if the supplied class is an inner class whose enclosing
* class should be searched
* @since 5.3
* @see ClassUtils#isInnerClass(Class)
* @see #getSearchStrategy(Class)
*/
public static boolean searchEnclosingClass(Class<?> clazz) {
return (ClassUtils.isInnerClass(clazz) &&
getSearchStrategy(clazz) == SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES);
}
/**
* Get the {@link SearchStrategy} for the supplied class.
* @param clazz the class for which the search strategy should be resolved
* @return the resolved search strategy
* @since 5.3
*/
private static SearchStrategy getSearchStrategy(Class<?> clazz) {
return cachedSearchStrategies.get(clazz);
}
private static SearchStrategy lookUpSearchStrategy(Class<?> clazz) {
EnclosingConfiguration enclosingConfiguration =
MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
.stream(NestedTestConfiguration.class)
.map(mergedAnnotation -> mergedAnnotation.getEnum("value", EnclosingConfiguration.class))
.findFirst()
.orElse(EnclosingConfiguration.OVERRIDE);
// TODO Switch the default EnclosingConfiguration mode to INHERIT.
// TODO Make the default EnclosingConfiguration mode globally configurable via SpringProperties.
return (enclosingConfiguration == EnclosingConfiguration.INHERIT ?
SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES :
SearchStrategy.TYPE_HIERARCHY);
}
private static void assertNonEmptyAnnotationTypeArray(Class<?>[] annotationTypes, String message) {
@@ -358,6 +465,54 @@ public abstract class MetaAnnotationUtils {
return (this.composedAnnotation != null ? this.composedAnnotation.annotationType() : null);
}
/**
* Find the next {@link AnnotationDescriptor} for the specified
* {@linkplain #getAnnotationType() annotation type} in the hierarchy
* above the {@linkplain #getRootDeclaringClass() root declaring class}
* of this descriptor.
* <p>If a corresponding annotation is found in the superclass hierarchy
* of the root declaring class, that will be returned. Otherwise, an
* attempt will be made to find a corresponding annotation in the
* {@linkplain Class#getEnclosingClass() enclosing class} hierarchy of
* the root declaring class if
* {@linkplain MetaAnnotationUtils#searchEnclosingClass appropriate}.
* @return the next corresponding annotation descriptor if the annotation
* was found; otherwise {@code null}
* @since 5.3
*/
@Nullable
@SuppressWarnings("unchecked")
public AnnotationDescriptor<T> next() {
Class<T> annotationType = (Class<T>) getAnnotationType();
// Declared on a superclass?
AnnotationDescriptor<T> descriptor =
findAnnotationDescriptor(getRootDeclaringClass().getSuperclass(), annotationType);
// Declared on an enclosing class of an inner class?
if (descriptor == null && searchEnclosingClass(getRootDeclaringClass())) {
descriptor = findAnnotationDescriptor(getRootDeclaringClass().getEnclosingClass(), annotationType);
}
return descriptor;
}
/**
* Find <strong>all</strong> annotations of the specified
* {@linkplain #getAnnotationType() annotation type} that are present or
* meta-present on the {@linkplain #getRootDeclaringClass() root declaring
* class} of this descriptor.
* @return the set of all merged, synthesized {@code Annotations} found,
* or an empty set if none were found
* @since 5.3
*/
@SuppressWarnings("unchecked")
public Set<T> findAllLocalMergedAnnotations() {
Class<T> annotationType = (Class<T>) getAnnotationType();
SearchStrategy searchStrategy = getSearchStrategy(getRootDeclaringClass());
return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none())
.stream(annotationType)
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
.collect(MergedAnnotationCollectors.toAnnotationSet());
}
/**
* Provide a textual representation of this {@code AnnotationDescriptor}.
*/
@@ -380,20 +535,48 @@ public abstract class MetaAnnotationUtils {
*/
public static class UntypedAnnotationDescriptor extends AnnotationDescriptor<Annotation> {
@Nullable
private final Class<? extends Annotation>[] annotationTypes;
/**
* Create a new {@plain UntypedAnnotationDescriptor}.
* @deprecated As of Spring Framework 5.3, in favor of
* {@link UntypedAnnotationDescriptor#UntypedAnnotationDescriptor(Class, Annotation, Class[])}
*/
@Deprecated
public UntypedAnnotationDescriptor(Class<?> rootDeclaringClass, Annotation annotation) {
this(rootDeclaringClass, rootDeclaringClass, null, annotation);
this(rootDeclaringClass, annotation, null);
}
public UntypedAnnotationDescriptor(Class<?> rootDeclaringClass, Annotation annotation,
@Nullable Class<? extends Annotation>[] annotationTypes) {
this(rootDeclaringClass, rootDeclaringClass, null, annotation, annotationTypes);
}
/**
* Create a new {@plain UntypedAnnotationDescriptor}.
* @deprecated As of Spring Framework 5.3, in favor of
* {@link UntypedAnnotationDescriptor#UntypedAnnotationDescriptor(Class, Class, Annotation, Annotation, Class[])}
*/
@Deprecated
public UntypedAnnotationDescriptor(Class<?> rootDeclaringClass, Class<?> declaringClass,
@Nullable Annotation composedAnnotation, Annotation annotation) {
this(rootDeclaringClass, declaringClass, composedAnnotation, annotation, null);
}
public UntypedAnnotationDescriptor(Class<?> rootDeclaringClass, Class<?> declaringClass,
@Nullable Annotation composedAnnotation, Annotation annotation,
@Nullable Class<? extends Annotation>[] annotationTypes) {
super(rootDeclaringClass, declaringClass, composedAnnotation, annotation);
this.annotationTypes = annotationTypes;
}
/**
* Throws an {@link UnsupportedOperationException} since the type of annotation
* represented by the {@link #getAnnotationAttributes AnnotationAttributes} in
* an {@code UntypedAnnotationDescriptor} is unknown.
* represented by an {@code UntypedAnnotationDescriptor} is unknown.
* @since 4.2
*/
@Override
@@ -401,6 +584,52 @@ public abstract class MetaAnnotationUtils {
throw new UnsupportedOperationException(
"synthesizeAnnotation() is unsupported in UntypedAnnotationDescriptor");
}
/**
* Find the next {@link UntypedAnnotationDescriptor} for the specified
* annotation types in the hierarchy above the
* {@linkplain #getRootDeclaringClass() root declaring class} of this
* descriptor.
* <p>If one of the corresponding annotations is found in the superclass
* hierarchy of the root declaring class, that will be returned. Otherwise,
* an attempt will be made to find a corresponding annotation in the
* {@linkplain Class#getEnclosingClass() enclosing class} hierarchy of
* the root declaring class if
* {@linkplain MetaAnnotationUtils#searchEnclosingClass appropriate}.
* @return the next corresponding annotation descriptor if one of the
* annotations was found; otherwise {@code null}
* @since 5.3
* @see AnnotationDescriptor#next()
*/
@Override
@Nullable
public UntypedAnnotationDescriptor next() {
if (ObjectUtils.isEmpty(this.annotationTypes)) {
throw new UnsupportedOperationException(
"next() is unsupported if UntypedAnnotationDescriptor is instantiated without 'annotationTypes'");
}
// Declared on a superclass?
UntypedAnnotationDescriptor descriptor =
findAnnotationDescriptorForTypes(getRootDeclaringClass().getSuperclass(), this.annotationTypes);
// Declared on an enclosing class of an inner class?
if (descriptor == null && searchEnclosingClass(getRootDeclaringClass())) {
descriptor = findAnnotationDescriptorForTypes(getRootDeclaringClass().getEnclosingClass(), this.annotationTypes);
}
return descriptor;
}
/**
* Throws an {@link UnsupportedOperationException} since the type of annotation
* represented by an {@code UntypedAnnotationDescriptor} is unknown.
* @since 5.3
*/
@Override
public Set<Annotation> findAllLocalMergedAnnotations() {
throw new UnsupportedOperationException(
"findAllLocalMergedAnnotations() is unsupported in UntypedAnnotationDescriptor");
}
}
}