If the source is an annotation that is not synthesizable, it + * will be returned unmodified. + *
Consult the documentation for {@link MergedAnnotation#synthesize()}
+ * for an explanation of what is considered synthesizable.
*/
- protected abstract A createSynthesized();
+ protected abstract A createSynthesizedAnnotation();
}
diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java
index ed60e365c3d..68422f7ec0f 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * 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.
@@ -19,7 +19,6 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.reflect.AnnotatedElement;
-import java.lang.reflect.Proxy;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
@@ -481,12 +480,15 @@ public interface MergedAnnotation {
/**
* Create a type-safe synthesized version of this merged annotation that can
* be used directly in code.
- * The result is synthesized using a JDK {@link Proxy} and as a result may
- * incur a computational cost when first invoked.
+ * The result is synthesized using a JDK {@link java.lang.reflect.Proxy Proxy}
+ * and as a result may incur a computational cost when first invoked.
+ * If this merged annotation was created {@linkplain #of(AnnotatedElement, Class, Map)
+ * from} a map of annotation attributes or default attribute values, those
+ * attributes will always be synthesized into an annotation instance.
* If this merged annotation was created {@linkplain #from(Annotation) from}
* an annotation instance, that annotation will be returned unmodified if it is
* not synthesizable. An annotation is considered synthesizable if
- * one of the following is true.
+ * it has not already been synthesized and one of the following is true.
* The result is synthesized using a JDK {@link Proxy} and as a result may
- * incur a computational cost when first invoked.
+ * The result is synthesized using a JDK {@link java.lang.reflect.Proxy Proxy}
+ * and as a result may incur a computational cost when first invoked.
* Consult the documentation for {@link #synthesize()} for an explanation
* of what is considered synthesizable.
* @param condition the test to determine if the annotation can be synthesized
diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java
index caa764caf11..0c7c9abad61 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java
@@ -162,7 +162,7 @@ final class MissingMergedAnnotation extends AbstractMerged
}
@Override
- protected A createSynthesized() {
+ protected A createSynthesizedAnnotation() {
throw new NoSuchElementException("Unable to synthesize missing annotation");
}
diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java
index 575214ab746..f3218312be8 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -321,19 +321,37 @@ final class TypeMappedAnnotation extends AbstractMergedAnn
@Override
@SuppressWarnings("unchecked")
- protected A createSynthesized() {
- if (getType().isInstance(this.rootAttributes) && !isSynthesizable()) {
+ protected A createSynthesizedAnnotation() {
+ // Check root annotation
+ if (isTargetAnnotation(this.rootAttributes) && isNotSynthesizable((Annotation) this.rootAttributes)) {
return (A) this.rootAttributes;
}
+ // Check meta-annotation
+ else if (isTargetAnnotation(this.mapping.getAnnotation()) && isNotSynthesizable(this.mapping.getAnnotation())) {
+ return (A) this.mapping.getAnnotation();
+ }
return SynthesizedMergedAnnotationInvocationHandler.createProxy(this, getType());
}
- private boolean isSynthesizable() {
- // Already synthesized?
- if (this.rootAttributes instanceof SynthesizedAnnotation) {
- return false;
- }
- return this.mapping.isSynthesizable();
+ /**
+ * Determine if the supplied object is an annotation of the required
+ * {@linkplain #getType() type}.
+ * @param obj the object to check
+ * @since 5.3.22
+ */
+ private boolean isTargetAnnotation(@Nullable Object obj) {
+ return getType().isInstance(obj);
+ }
+
+ /**
+ * Determine if the supplied annotation has already been synthesized or if the
+ * mapped annotation is not {@linkplain AnnotationTypeMapping#isSynthesizable()
+ * synthesizable} in general.
+ * @param annotation the annotation to check
+ * @since 5.3.22
+ */
+ private boolean isNotSynthesizable(Annotation annotation) {
+ return (annotation instanceof SynthesizedAnnotation || !this.mapping.isSynthesizable());
}
@Override
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
index cb3a3b76185..ec78683b47a 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
@@ -1539,6 +1539,17 @@ class MergedAnnotationsTests {
assertThat(generatedValue).isSameAs(synthesizedGeneratedValue);
}
+ @Test
+ void synthesizeShouldNotSynthesizeNonsynthesizableAnnotationsWhenUsingMergedAnnotationsFromApi() {
+ MergedAnnotations mergedAnnotations = MergedAnnotations.from(SecurityConfig.class);
+
+ EnableWebSecurity enableWebSecurity = mergedAnnotations.get(EnableWebSecurity.class).synthesize();
+ assertThat(enableWebSecurity).isNotInstanceOf(SynthesizedAnnotation.class);
+
+ EnableGlobalAuthentication enableGlobalAuthentication = mergedAnnotations.get(EnableGlobalAuthentication.class).synthesize();
+ assertThat(enableGlobalAuthentication).isNotInstanceOf(SynthesizedAnnotation.class);
+ }
+
/**
* If an attempt is made to synthesize an annotation from an annotation instance
* that has already been synthesized, the original synthesized annotation should
@@ -3216,6 +3227,25 @@ class MergedAnnotationsTests {
return 42L;
}
+ /**
+ * Mimics org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication
+ */
+ @Retention(RUNTIME)
+ @interface EnableGlobalAuthentication {
+ }
+
+ /**
+ * Mimics org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+ */
+ @Retention(RUNTIME)
+ @EnableGlobalAuthentication
+ @interface EnableWebSecurity {
+ }
+
+ @EnableWebSecurity
+ static class SecurityConfig {
+ }
+
@Retention(RetentionPolicy.RUNTIME)
@interface TestConfiguration {
diff --git a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java
index 14d45f1b3b3..db3297c5fc8 100644
--- a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java
@@ -61,8 +61,8 @@ class BootstrapUtilsTests {
@Test
void resolveTestContextBootstrapperWithEmptyBootstrapWithAnnotation() {
BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(EmptyBootstrapWithAnnotationClass.class, delegate);
- assertThatIllegalStateException().isThrownBy(() ->
- resolveTestContextBootstrapper(bootstrapContext))
+ assertThatIllegalStateException()
+ .isThrownBy(() -> resolveTestContextBootstrapper(bootstrapContext))
.withMessageContaining("Specify @BootstrapWith's 'value' attribute");
}
@@ -70,11 +70,11 @@ class BootstrapUtilsTests {
void resolveTestContextBootstrapperWithDoubleMetaBootstrapWithAnnotations() {
BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(
DoubleMetaAnnotatedBootstrapWithAnnotationClass.class, delegate);
- assertThatIllegalStateException().isThrownBy(() ->
- resolveTestContextBootstrapper(bootstrapContext))
+ assertThatIllegalStateException()
+ .isThrownBy(() -> resolveTestContextBootstrapper(bootstrapContext))
.withMessageContaining("Configuration error: found multiple declarations of @BootstrapWith")
- .withMessageContaining(FooBootstrapper.class.getCanonicalName())
- .withMessageContaining(BarBootstrapper.class.getCanonicalName());
+ .withMessageContaining(FooBootstrapper.class.getSimpleName())
+ .withMessageContaining(BarBootstrapper.class.getSimpleName());
}
@Test
*