diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index 3ce408c9928..e24b75d4518 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -55,7 +55,8 @@ import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -235,7 +236,7 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean Constructor requiredConstructor = null; Constructor defaultConstructor = null; for (Constructor candidate : rawCandidates) { - Annotation annotation = findAutowiredAnnotation(candidate); + AnnotationAttributes annotation = findAutowiredAnnotation(candidate); if (annotation != null) { if (requiredConstructor != null) { throw new BeanCreationException("Invalid autowire-marked constructor: " + candidate + @@ -333,7 +334,7 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean do { LinkedList currElements = new LinkedList(); for (Field field : targetClass.getDeclaredFields()) { - Annotation annotation = findAutowiredAnnotation(field); + AnnotationAttributes annotation = findAutowiredAnnotation(field); if (annotation != null) { if (Modifier.isStatic(field.getModifiers())) { if (logger.isWarnEnabled()) { @@ -347,7 +348,7 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean } for (Method method : targetClass.getDeclaredMethods()) { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); - Annotation annotation = BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod) ? + AnnotationAttributes annotation = BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod) ? findAutowiredAnnotation(bridgedMethod) : findAutowiredAnnotation(method); if (annotation != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { @@ -374,9 +375,9 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean return new InjectionMetadata(clazz, elements); } - private Annotation findAutowiredAnnotation(AccessibleObject ao) { + private AnnotationAttributes findAutowiredAnnotation(AccessibleObject ao) { for (Class type : this.autowiredAnnotationTypes) { - Annotation annotation = AnnotationUtils.getAnnotation(ao, type); + AnnotationAttributes annotation = AnnotatedElementUtils.getAnnotationAttributes(ao, type.getName()); if (annotation != null) { return annotation; } @@ -384,6 +385,19 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean return null; } + /** + * Determine if the annotated field or method requires its dependency. + *

A 'required' dependency means that autowiring should fail when no beans + * are found. Otherwise, the autowiring process will simply bypass the field + * or method when no beans are found. + * @param annotation the Autowired annotation + * @return whether the annotation indicates that a dependency is required + */ + protected boolean determineRequiredStatus(AnnotationAttributes annotation) { + return (!annotation.containsKey(this.requiredParameterName) || + this.requiredParameterValue == annotation.getBoolean(this.requiredParameterName)); + } + /** * Obtain all beans of the given type as autowire candidates. * @param type the type of the bean @@ -398,31 +412,6 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean return BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory, type); } - /** - * Determine if the annotated field or method requires its dependency. - *

A 'required' dependency means that autowiring should fail when no beans - * are found. Otherwise, the autowiring process will simply bypass the field - * or method when no beans are found. - * @param annotation the Autowired annotation - * @return whether the annotation indicates that a dependency is required - */ - protected boolean determineRequiredStatus(Annotation annotation) { - try { - Method method = ReflectionUtils.findMethod(annotation.annotationType(), this.requiredParameterName); - if (method == null) { - // annotations like @Inject and @Value don't have a method (attribute) named "required" - // -> default to required status - return true; - } - return (this.requiredParameterValue == (Boolean) ReflectionUtils.invokeMethod(method, annotation)); - } - catch (Exception ex) { - // an exception was thrown during reflective invocation of the required attribute - // -> default to required status - return true; - } - } - /** * Register the specified bean as dependent on the autowired beans. */ diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java index 16565d9b1d7..eccfe7a6f26 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java @@ -16,12 +16,11 @@ package org.springframework.context.annotation; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Retention; import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; @@ -29,6 +28,8 @@ import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; +import static org.junit.Assert.*; + /** * @author Rick Evans * @author Chris Beams @@ -83,6 +84,15 @@ public final class AnnotationScopeMetadataResolverTests { assertEquals(ScopedProxyMode.NO, scopeMetadata.getScopedProxyMode()); } + @Test + public void testCustomRequestScopeWithAttribute() { + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithCustomRequestScopeWithAttribute.class); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); + assertNotNull("resolveScopeMetadata(..) must *never* return null.", scopeMetadata); + assertEquals("request", scopeMetadata.getScopeName()); + assertEquals(ScopedProxyMode.TARGET_CLASS, scopeMetadata.getScopedProxyMode()); + } + @Test(expected=IllegalArgumentException.class) public void testCtorWithNullScopedProxyMode() { new AnnotationScopeMetadataResolver(null); @@ -98,17 +108,21 @@ public final class AnnotationScopeMetadataResolverTests { private static final class AnnotatedWithSingletonScope { } - @Scope("prototype") private static final class AnnotatedWithPrototypeScope { } - @Scope(value="request", proxyMode = ScopedProxyMode.TARGET_CLASS) private static final class AnnotatedWithScopedProxy { } + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @Scope("request") + public @interface CustomRequestScope { + } + @CustomRequestScope private static final class AnnotatedWithCustomRequestScope { } @@ -117,8 +131,13 @@ public final class AnnotationScopeMetadataResolverTests { @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Scope("request") - public @interface CustomRequestScope { + public @interface CustomRequestScopeWithAttribute { + ScopedProxyMode proxyMode(); + } + + @CustomRequestScopeWithAttribute(proxyMode = ScopedProxyMode.TARGET_CLASS) + private static final class AnnotatedWithCustomRequestScopeWithAttribute { } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index ba336cdb8f2..7840a222fce 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -20,6 +20,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import org.junit.Test; + +import org.springframework.tests.sample.beans.NestedTestBean; import org.springframework.tests.sample.beans.TestBean; import org.springframework.beans.factory.annotation.Autowired; @@ -60,9 +62,19 @@ public class BeanMethodQualificationTests { assertThat(pojo.testBean.getName(), equalTo("interesting")); } + @Test + public void testCustomWithAttributeOverride() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(CustomConfigWithAttributeOverride.class, CustomPojo.class); + assertFalse(ctx.getBeanFactory().containsSingleton("testBeanX")); + CustomPojo pojo = ctx.getBean(CustomPojo.class); + assertThat(pojo.testBean.getName(), equalTo("interesting")); + } + @Configuration static class StandardConfig { + @Bean @Lazy @Qualifier("interesting") public TestBean testBean1() { return new TestBean("interesting"); @@ -76,11 +88,13 @@ public class BeanMethodQualificationTests { @Component @Lazy static class StandardPojo { + @Autowired @Qualifier("interesting") TestBean testBean; } @Configuration static class CustomConfig { + @InterestingBean public TestBean testBean1() { return new TestBean("interesting"); @@ -92,9 +106,26 @@ public class BeanMethodQualificationTests { } } + @Configuration + static class CustomConfigWithAttributeOverride { + + @InterestingBeanWithName(name="testBeanX") + public TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Qualifier("boring") + public TestBean testBean2() { + return new TestBean("boring"); + } + } + @InterestingPojo static class CustomPojo { + @InterestingNeed TestBean testBean; + + @InterestingNeedWithRequiredOverride(required=false) NestedTestBean nestedTestBean; } @Bean @Lazy @Qualifier("interesting") @@ -102,11 +133,25 @@ public class BeanMethodQualificationTests { public @interface InterestingBean { } + @Bean @Lazy @Qualifier("interesting") + @Retention(RetentionPolicy.RUNTIME) + public @interface InterestingBeanWithName { + + String name(); + } + @Autowired @Qualifier("interesting") @Retention(RetentionPolicy.RUNTIME) public @interface InterestingNeed { } + @Autowired @Qualifier("interesting") + @Retention(RetentionPolicy.RUNTIME) + public @interface InterestingNeedWithRequiredOverride { + + boolean required(); + } + @Component @Lazy @Retention(RetentionPolicy.RUNTIME) public @interface InterestingPojo { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java new file mode 100644 index 00000000000..da50710602e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Utility class used to collect all annotation values including those declared on meta-annotations. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 4.0 + */ +public class AnnotatedElementUtils { + + public static Set getMetaAnnotationTypes(AnnotatedElement element, String annotationType) { + final Set types = new LinkedHashSet(); + process(element, annotationType, new Processor() { + @Override + public Object process(Annotation annotation, int depth) { + if (depth > 0) { + types.add(annotation.annotationType().getName()); + } + return null; + } + @Override + public void postProcess(Annotation annotation, Object result) { + } + }); + return (types.isEmpty() ? null : types); + } + + public static boolean hasMetaAnnotationTypes(AnnotatedElement element, String annotationType) { + return Boolean.TRUE.equals(process(element, annotationType, new Processor() { + @Override + public Boolean process(Annotation annotation, int depth) { + if (depth > 0) { + return true; + } + return null; + } + @Override + public void postProcess(Annotation annotation, Boolean result) { + } + })); + } + + public static boolean isAnnotated(AnnotatedElement element, String annotationType) { + return Boolean.TRUE.equals(process(element, annotationType, new Processor() { + @Override + public Boolean process(Annotation annotation, int depth) { + return true; + } + @Override + public void postProcess(Annotation annotation, Boolean result) { + } + })); + } + + public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationType) { + return getAnnotationAttributes(element, annotationType, false, false); + } + + public static AnnotationAttributes getAnnotationAttributes( + AnnotatedElement element, String annotationType, + final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { + + return process(element, annotationType, new Processor() { + @Override + public AnnotationAttributes process(Annotation annotation, int depth) { + return AnnotationUtils.getAnnotationAttributes(annotation, classValuesAsString, nestedAnnotationsAsMap); + } + @Override + public void postProcess(Annotation annotation, AnnotationAttributes result) { + for (String key : result.keySet()) { + if (!"value".equals(key)) { + Object value = AnnotationUtils.getValue(annotation, key); + if (value != null) { + result.put(key, value); + } + } + } + } + }); + } + + public static MultiValueMap getAllAnnotationAttributes( + AnnotatedElement element, final String annotationType, + final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { + + final MultiValueMap attributes = new LinkedMultiValueMap(); + process(element, annotationType, new Processor() { + @Override + public Void process(Annotation annotation, int depth) { + if (annotation.annotationType().getName().equals(annotationType)) { + for (Map.Entry entry : + AnnotationUtils.getAnnotationAttributes( + annotation, classValuesAsString, nestedAnnotationsAsMap).entrySet()) { + attributes.add(entry.getKey(), entry.getValue()); + } + } + return null; + } + @Override + public void postProcess(Annotation annotation, Void result) { + for (String key : attributes.keySet()) { + if (!"value".equals(key)) { + Object value = AnnotationUtils.getValue(annotation, key); + if (value != null) { + attributes.add(key, value); + } + } + } + } + }); + return (attributes.isEmpty() ? null : attributes); + } + + /** + * Process all annotations of the specified annotation type and recursively all + * meta-annotations on the specified type. + * @param element the annotated element + * @param annotationType the annotation type to find. Only items of the specified + * type or meta-annotations of the specified type will be processed. + * @param processor the processor + * @return the result of the processor + */ + private static T process(AnnotatedElement element, String annotationType, Processor processor) { + return doProcess(element, annotationType, processor, new HashSet(), 0); + } + + private static T doProcess(AnnotatedElement element, String annotationType, + Processor processor, Set visited, int depth) { + + if (visited.add(element)) { + for (Annotation annotation : element.getAnnotations()) { + if (annotation.annotationType().getName().equals(annotationType) || depth > 0) { + T result = processor.process(annotation, depth); + if (result != null) { + return result; + } + result = doProcess(annotation.annotationType(), annotationType, processor, visited, depth + 1); + if (result != null) { + processor.postProcess(annotation, result); + return result; + } + } + } + for (Annotation annotation : element.getAnnotations()) { + T result = doProcess(annotation.annotationType(), annotationType, processor, visited, depth); + if (result != null) { + processor.postProcess(annotation, result); + return result; + } + } + } + return null; + } + + + /** + * Callback interface used to process an annotation. + * @param the result type + */ + private static interface Processor { + + /** + * Called to process the annotation. + * @param annotation the annotation to process + * @param depth the depth of the annotation relative to the initial match. + * For example, a matched annotation will have a depth of 0, a meta-annotation + * 1 and a meta-meta-annotation 2 + * @return the result of the processing or {@code null} to continue + */ + T process(Annotation annotation, int depth); + + void postProcess(Annotation annotation, T result); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java index d18ad519fb8..090621d8036 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -16,8 +16,7 @@ package org.springframework.core.annotation; -import static java.lang.String.format; - +import java.lang.reflect.Array; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; @@ -27,10 +26,9 @@ import org.springframework.util.StringUtils; /** * {@link LinkedHashMap} subclass representing annotation attribute key/value pairs - * as read by Spring's reflection- or ASM-based {@link - * org.springframework.core.type.AnnotationMetadata AnnotationMetadata} implementations. - * Provides 'pseudo-reification' to avoid noisy Map generics in the calling code as well - * as convenience methods for looking up annotation attributes in a type-safe fashion. + * as read by Spring's reflection- or ASM-based {@link org.springframework.core.type.AnnotationMetadata} + * implementations. Provides 'pseudo-reification' to avoid noisy Map generics in the calling code + * as well as convenience methods for looking up annotation attributes in a type-safe fashion. * * @author Chris Beams * @since 3.1.1 @@ -63,24 +61,6 @@ public class AnnotationAttributes extends LinkedHashMap { super(map); } - /** - * Return an {@link AnnotationAttributes} instance based on the given map; if the map - * is already an {@code AnnotationAttributes} instance, it is casted and returned - * immediately without creating any new instance; otherwise create a new instance by - * wrapping the map with the {@link #AnnotationAttributes(Map)} constructor. - * @param map original source of annotation attribute key/value pairs - */ - public static AnnotationAttributes fromMap(Map map) { - if (map == null) { - return null; - } - - if (map instanceof AnnotationAttributes) { - return (AnnotationAttributes) map; - } - - return new AnnotationAttributes(map); - } public String getString(String attributeName) { return doGet(attributeName, String.class); @@ -96,7 +76,7 @@ public class AnnotationAttributes extends LinkedHashMap { @SuppressWarnings("unchecked") public N getNumber(String attributeName) { - return (N) doGet(attributeName, Integer.class); + return (N) doGet(attributeName, Number.class); } @SuppressWarnings("unchecked") @@ -124,11 +104,20 @@ public class AnnotationAttributes extends LinkedHashMap { @SuppressWarnings("unchecked") private T doGet(String attributeName, Class expectedType) { Assert.hasText(attributeName, "attributeName must not be null or empty"); - Object value = this.get(attributeName); - Assert.notNull(value, format("Attribute '%s' not found", attributeName)); - Assert.isAssignable(expectedType, value.getClass(), - format("Attribute '%s' is of type [%s], but [%s] was expected. Cause: ", + Object value = get(attributeName); + Assert.notNull(value, String.format("Attribute '%s' not found", attributeName)); + if (!expectedType.isInstance(value)) { + if (expectedType.isArray() && expectedType.getComponentType().isInstance(value)) { + Object arrayValue = Array.newInstance(expectedType.getComponentType(), 1); + Array.set(arrayValue, 0, value); + value = arrayValue; + } + else { + throw new IllegalArgumentException( + String.format("Attribute '%s' is of type [%s], but [%s] was expected. Cause: ", attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName())); + } + } return (T) value; } @@ -156,4 +145,23 @@ public class AnnotationAttributes extends LinkedHashMap { } return String.valueOf(value); } + + + /** + * Return an {@link AnnotationAttributes} instance based on the given map; if the map + * is already an {@code AnnotationAttributes} instance, it is casted and returned + * immediately without creating any new instance; otherwise create a new instance by + * wrapping the map with the {@link #AnnotationAttributes(Map)} constructor. + * @param map original source of annotation attribute key/value pairs + */ + public static AnnotationAttributes fromMap(Map map) { + if (map == null) { + return null; + } + if (map instanceof AnnotationAttributes) { + return (AnnotationAttributes) map; + } + return new AnnotationAttributes(map); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/type/AnnotatedElementUtils.java deleted file mode 100644 index 342089b6257..00000000000 --- a/spring-core/src/main/java/org/springframework/core/type/AnnotatedElementUtils.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2002-2013 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 - * - * http://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.core.type; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -/** - * Internal utility class used to collect all annotation values including those declared - * on meta-annotations. - * - * @author Phillip Webb - * @since 4.0 - */ -class AnnotatedElementUtils { - - public static Set getMetaAnnotationTypes(AnnotatedElement element, - String annotationType) { - final Set types = new LinkedHashSet(); - process(element, annotationType, new Processor() { - @Override - public Object process(Annotation annotation, int depth) { - if (depth > 0) { - types.add(annotation.annotationType().getName()); - } - return null; - } - }); - return (types.size() == 0 ? null : types); - } - - public static boolean hasMetaAnnotationTypes(AnnotatedElement element, - String annotationType) { - return Boolean.TRUE.equals( - process(element, annotationType, new Processor() { - @Override - public Boolean process(Annotation annotation, int depth) { - if (depth > 0) { - return true; - } - return null; - } - })); - } - - public static boolean isAnnotated(AnnotatedElement element, String annotationType) { - return Boolean.TRUE.equals( - process(element, annotationType, new Processor() { - @Override - public Boolean process(Annotation annotation, int depth) { - return true; - } - })); - } - - public static Map getAnnotationAttributes(AnnotatedElement element, - String annotationType, final boolean classValuesAsString, - final boolean nestedAnnotationsAsMap) { - return process(element, annotationType, new Processor>() { - @Override - public Map process(Annotation annotation, int depth) { - return AnnotationUtils.getAnnotationAttributes(annotation, - classValuesAsString, nestedAnnotationsAsMap); - } - }); - } - - public static MultiValueMap getAllAnnotationAttributes( - AnnotatedElement element, final String annotationType, - final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { - final MultiValueMap attributes = new LinkedMultiValueMap(); - process(element, annotationType, new Processor() { - @Override - public Object process(Annotation annotation, int depth) { - if (annotation.annotationType().getName().equals(annotationType)) { - for (Map.Entry entry : AnnotationUtils.getAnnotationAttributes( - annotation, classValuesAsString, nestedAnnotationsAsMap).entrySet()) { - attributes.add(entry.getKey(), entry.getValue()); - } - } - return null; - } - }); - return (attributes.size() == 0 ? null : attributes); - } - - /** - * Process all annotations of the specified annotation type and recursively all - * meta-annotations on the specified type. - * @param element the annotated element - * @param annotationType the annotation type to find. Only items of the specified type - * or meta-annotations of the specified type will be processed - * @param processor the processor - * @return the result of the processor - */ - private static T process(AnnotatedElement element, String annotationType, - Processor processor) { - return recursivelyProcess(element, annotationType, processor, - new HashSet(), 0); - } - - private static T recursivelyProcess(AnnotatedElement element, - String annotationType, Processor processor, Set visited, - int depth) { - T result = null; - if (visited.add(element)) { - for (Annotation annotation : element.getAnnotations()) { - if (annotation.annotationType().getName().equals(annotationType) || depth > 0) { - result = result != null ? result : - processor.process(annotation, depth); - result = result != null ? result : - recursivelyProcess(annotation.annotationType(), annotationType, - processor, visited, depth + 1); - } - } - for (Annotation annotation : element.getAnnotations()) { - result = result != null ? result : - recursivelyProcess(annotation.annotationType(), annotationType, - processor, visited, depth); - } - - } - return result; - } - - /** - * Callback interface used to process an annotation. - * @param the result type - */ - private static interface Processor { - - /** - * Called to process the annotation. - * @param annotation the annotation to process - * @param depth the depth of the annotation relative to the initial match. For - * example a matched annotation will have a depth of 0, a meta-annotation 1 - * and a meta-meta-annotation 2 - * @return the result of the processing or {@code null} to continue - */ - T process(Annotation annotation, int depth); - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java index a1e131804d3..954c12721d4 100644 --- a/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java @@ -22,7 +22,7 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; -import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.MultiValueMap; /** @@ -52,11 +52,12 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements /** * Create a new {@link StandardAnnotationMetadata} wrapper for the given Class, * providing the option to return any nested annotations or annotation arrays in the - * form of {@link AnnotationAttributes} instead of actual {@link Annotation} instances. + * form of {@link org.springframework.core.annotation.AnnotationAttributes} instead + * of actual {@link Annotation} instances. * @param introspectedClass the Class to instrospect * @param nestedAnnotationsAsMap return nested annotations and annotation arrays as - * {@link AnnotationAttributes} for compatibility with ASM-based - * {@link AnnotationMetadata} implementations + * {@link org.springframework.core.annotation.AnnotationAttributes} for compatibility + * with ASM-based {@link AnnotationMetadata} implementations * @since 3.1.1 */ public StandardAnnotationMetadata(Class introspectedClass, boolean nestedAnnotationsAsMap) { @@ -107,8 +108,7 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements } @Override - public Map getAnnotationAttributes(String annotationType, - boolean classValuesAsString) { + public Map getAnnotationAttributes(String annotationType, boolean classValuesAsString) { return AnnotatedElementUtils.getAnnotationAttributes(getIntrospectedClass(), annotationType, classValuesAsString, this.nestedAnnotationsAsMap); } @@ -119,8 +119,7 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements } @Override - public MultiValueMap getAllAnnotationAttributes( - String annotationType, boolean classValuesAsString) { + public MultiValueMap getAllAnnotationAttributes(String annotationType, boolean classValuesAsString) { return AnnotatedElementUtils.getAllAnnotationAttributes(getIntrospectedClass(), annotationType, classValuesAsString, this.nestedAnnotationsAsMap); } @@ -141,7 +140,6 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements Method[] methods = getIntrospectedClass().getDeclaredMethods(); Set annotatedMethods = new LinkedHashSet(); for (Method method : methods) { - // TODO: on OpenJDK 8 b99, bridge methods seem to be discovered as annotated as well... if (AnnotatedElementUtils.isAnnotated(method, annotationType)) { annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap)); } diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java index 5b9d66ffdae..6899ed7bf96 100644 --- a/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Map; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; @@ -49,9 +50,14 @@ public class StandardMethodMetadata implements MethodMetadata { } /** - * Create a new StandardMethodMetadata wrapper for the given Method. + * Create a new StandardMethodMetadata wrapper for the given Method, + * providing the option to return any nested annotations or annotation arrays in the + * form of {@link org.springframework.core.annotation.AnnotationAttributes} instead + * of actual {@link java.lang.annotation.Annotation} instances. * @param introspectedMethod the Method to introspect - * @param nestedAnnotationsAsMap + * @param nestedAnnotationsAsMap return nested annotations and annotation arrays as + * {@link org.springframework.core.annotation.AnnotationAttributes} for compatibility + * with ASM-based {@link AnnotationMetadata} implementations * @since 3.1.1 */ public StandardMethodMetadata(Method introspectedMethod, boolean nestedAnnotationsAsMap) { @@ -115,8 +121,7 @@ public class StandardMethodMetadata implements MethodMetadata { } @Override - public MultiValueMap getAllAnnotationAttributes( - String annotationType, boolean classValuesAsString) { + public MultiValueMap getAllAnnotationAttributes(String annotationType, boolean classValuesAsString) { return AnnotatedElementUtils.getAllAnnotationAttributes(this.introspectedMethod, annotationType, classValuesAsString, this.nestedAnnotationsAsMap); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/JtaTransactionAnnotationParser.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/JtaTransactionAnnotationParser.java index f81ef2fac66..01b5909912c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/JtaTransactionAnnotationParser.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/JtaTransactionAnnotationParser.java @@ -20,7 +20,9 @@ import java.io.Serializable; import java.lang.reflect.AnnotatedElement; import java.util.ArrayList; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.transaction.interceptor.NoRollbackRuleAttribute; import org.springframework.transaction.interceptor.RollbackRuleAttribute; import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute; @@ -37,7 +39,7 @@ public class JtaTransactionAnnotationParser implements TransactionAnnotationPars @Override public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) { - javax.transaction.Transactional ann = AnnotationUtils.getAnnotation(ae, javax.transaction.Transactional.class); + AnnotationAttributes ann = AnnotatedElementUtils.getAnnotationAttributes(ae, javax.transaction.Transactional.class.getName()); if (ann != null) { return parseTransactionAnnotation(ann); } @@ -47,15 +49,20 @@ public class JtaTransactionAnnotationParser implements TransactionAnnotationPars } public TransactionAttribute parseTransactionAnnotation(javax.transaction.Transactional ann) { + return parseTransactionAnnotation(AnnotationUtils.getAnnotationAttributes(ann, false, false)); + } + + protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); - rbta.setPropagationBehaviorName(RuleBasedTransactionAttribute.PREFIX_PROPAGATION + ann.value().toString()); + rbta.setPropagationBehaviorName( + RuleBasedTransactionAttribute.PREFIX_PROPAGATION + attributes.getEnum("value").toString()); ArrayList rollBackRules = new ArrayList(); - Class[] rbf = ann.rollbackOn(); + Class[] rbf = attributes.getClassArray("rollbackOn"); for (Class rbRule : rbf) { RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); rollBackRules.add(rule); } - Class[] nrbf = ann.dontRollbackOn(); + Class[] nrbf = attributes.getClassArray("dontRollbackOn"); for (Class rbRule : nrbf) { NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); rollBackRules.add(rule); diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/SpringTransactionAnnotationParser.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/SpringTransactionAnnotationParser.java index 514548d9ce9..f9d45cc2940 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/SpringTransactionAnnotationParser.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/SpringTransactionAnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -20,7 +20,9 @@ import java.io.Serializable; import java.lang.reflect.AnnotatedElement; import java.util.ArrayList; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.transaction.interceptor.NoRollbackRuleAttribute; import org.springframework.transaction.interceptor.RollbackRuleAttribute; import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute; @@ -37,7 +39,7 @@ public class SpringTransactionAnnotationParser implements TransactionAnnotationP @Override public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) { - Transactional ann = AnnotationUtils.getAnnotation(ae, Transactional.class); + AnnotationAttributes ann = AnnotatedElementUtils.getAnnotationAttributes(ae, Transactional.class.getName()); if (ann != null) { return parseTransactionAnnotation(ann); } @@ -47,29 +49,35 @@ public class SpringTransactionAnnotationParser implements TransactionAnnotationP } public TransactionAttribute parseTransactionAnnotation(Transactional ann) { + return parseTransactionAnnotation(AnnotationUtils.getAnnotationAttributes(ann, false, false)); + } + + protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); - rbta.setPropagationBehavior(ann.propagation().value()); - rbta.setIsolationLevel(ann.isolation().value()); - rbta.setTimeout(ann.timeout()); - rbta.setReadOnly(ann.readOnly()); - rbta.setQualifier(ann.value()); + Propagation propagation = attributes.getEnum("propagation"); + rbta.setPropagationBehavior(propagation.value()); + Isolation isolation = attributes.getEnum("isolation"); + rbta.setIsolationLevel(isolation.value()); + rbta.setTimeout(attributes.getNumber("timeout").intValue()); + rbta.setReadOnly(attributes.getBoolean("readOnly")); + rbta.setQualifier(attributes.getString("value")); ArrayList rollBackRules = new ArrayList(); - Class[] rbf = ann.rollbackFor(); + Class[] rbf = attributes.getClassArray("rollbackFor"); for (Class rbRule : rbf) { RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); rollBackRules.add(rule); } - String[] rbfc = ann.rollbackForClassName(); + String[] rbfc = attributes.getStringArray("rollbackForClassName"); for (String rbRule : rbfc) { RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); rollBackRules.add(rule); } - Class[] nrbf = ann.noRollbackFor(); + Class[] nrbf = attributes.getClassArray("noRollbackFor"); for (Class rbRule : nrbf) { NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); rollBackRules.add(rule); } - String[] nrbfc = ann.noRollbackForClassName(); + String[] nrbfc = attributes.getStringArray("noRollbackForClassName"); for (String rbRule : nrbfc) { NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); rollBackRules.add(rule); diff --git a/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSourceTests.java b/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSourceTests.java index 837b90a4d2a..b8c6840e52e 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSourceTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSourceTests.java @@ -214,7 +214,7 @@ public class AnnotationTransactionAttributeSourceTests { Method method = TestBean6.class.getMethod("getAge", (Class[]) null); AnnotationTransactionAttributeSource atas = new AnnotationTransactionAttributeSource(); - TransactionAttribute actual = atas.getTransactionAttribute(method, TestBean5.class); + TransactionAttribute actual = atas.getTransactionAttribute(method, TestBean6.class); RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); rbta.getRollbackRules().add(new RollbackRuleAttribute(Exception.class)); @@ -222,6 +222,36 @@ public class AnnotationTransactionAttributeSourceTests { assertEquals(rbta.getRollbackRules(), ((RuleBasedTransactionAttribute) actual).getRollbackRules()); } + @Test + public void testCustomClassAttributeWithReadOnlyOverrideDetected() throws Exception { + Method method = TestBean7.class.getMethod("getAge", (Class[]) null); + + AnnotationTransactionAttributeSource atas = new AnnotationTransactionAttributeSource(); + TransactionAttribute actual = atas.getTransactionAttribute(method, TestBean7.class); + + RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); + rbta.getRollbackRules().add(new RollbackRuleAttribute(Exception.class)); + rbta.getRollbackRules().add(new NoRollbackRuleAttribute(IOException.class)); + assertEquals(rbta.getRollbackRules(), ((RuleBasedTransactionAttribute) actual).getRollbackRules()); + + assertTrue(actual.isReadOnly()); + } + + @Test + public void testCustomMethodAttributeWithReadOnlyOverrideDetected() throws Exception { + Method method = TestBean8.class.getMethod("getAge", (Class[]) null); + + AnnotationTransactionAttributeSource atas = new AnnotationTransactionAttributeSource(); + TransactionAttribute actual = atas.getTransactionAttribute(method, TestBean8.class); + + RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); + rbta.getRollbackRules().add(new RollbackRuleAttribute(Exception.class)); + rbta.getRollbackRules().add(new NoRollbackRuleAttribute(IOException.class)); + assertEquals(rbta.getRollbackRules(), ((RuleBasedTransactionAttribute) actual).getRollbackRules()); + + assertTrue(actual.isReadOnly()); + } + @Test public void testTransactionAttributeDeclaredOnClassMethodWithEjb3() throws Exception { Method getAgeMethod = ITestBean.class.getMethod("getAge", (Class[]) null); @@ -543,6 +573,33 @@ public class AnnotationTransactionAttributeSourceTests { } + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @Transactional(rollbackFor=Exception.class, noRollbackFor={IOException.class}) + public @interface TxWithAttribute { + + boolean readOnly(); + } + + + @TxWithAttribute(readOnly=true) + public static class TestBean7 { + + public int getAge() { + return 10; + } + } + + + public static class TestBean8 { + + @TxWithAttribute(readOnly=true) + public int getAge() { + return 10; + } + } + + public static interface Foo { void doSomething(T theArgument);