diff --git a/org.springframework.aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java b/org.springframework.aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java index ae9888dca4d..c3985b8934e 100644 --- a/org.springframework.aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java +++ b/org.springframework.aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java @@ -16,12 +16,12 @@ package org.springframework.aop.config; -import org.w3c.dom.Element; - import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Element; /** * Utility class for handling registration of auto-proxy creators used internally @@ -52,6 +52,11 @@ public abstract class AopNamespaceUtils { private static final String EXPOSE_PROXY_ATTRIBUTE = "expose-proxy"; + /** + * @deprecated since Spring 3.1 in favor of + * {@link #registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry, ComponentRegistrar, Object, Boolean, Boolean)} + */ + @Deprecated public static void registerAutoProxyCreatorIfNecessary( ParserContext parserContext, Element sourceElement) { @@ -61,6 +66,20 @@ public abstract class AopNamespaceUtils { registerComponentIfNecessary(beanDefinition, parserContext); } + public static void registerAutoProxyCreatorIfNecessary( + BeanDefinitionRegistry registry, ComponentRegistrar parserContext, Object source, Boolean proxyTargetClass, Boolean exposeProxy) { + + BeanDefinition beanDefinition = + AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry, source); + useClassProxyingIfNecessary(registry, proxyTargetClass, exposeProxy); + registerComponentIfNecessary(beanDefinition, parserContext); + } + + public static void registerAutoProxyCreatorIfNecessary( + BeanDefinitionRegistry registry, ComponentRegistrar parserContext, Object source, Boolean proxyTargetClass) { + registerAutoProxyCreatorIfNecessary(registry, parserContext, source, proxyTargetClass, false); + } + public static void registerAspectJAutoProxyCreatorIfNecessary( ParserContext parserContext, Element sourceElement) { @@ -101,6 +120,12 @@ public abstract class AopNamespaceUtils { } + /** + * @deprecated since Spring 3.1 in favor of + * {@link #useClassProxyingIfNecessary(BeanDefinitionRegistry, Boolean, Boolean)} + * which does not require a parameter of type org.w3c.dom.Element + */ + @Deprecated private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, Element sourceElement) { if (sourceElement != null) { boolean proxyTargetClass = Boolean.valueOf(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE)); @@ -114,11 +139,20 @@ public abstract class AopNamespaceUtils { } } - private static void registerComponentIfNecessary(BeanDefinition beanDefinition, ParserContext parserContext) { + private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, Boolean proxyTargetClass, Boolean exposeProxy) { + if (proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + if (exposeProxy) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } + + private static void registerComponentIfNecessary(BeanDefinition beanDefinition, ComponentRegistrar componentRegistrar) { if (beanDefinition != null) { BeanComponentDefinition componentDefinition = new BeanComponentDefinition(beanDefinition, AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); - parserContext.registerComponent(componentDefinition); + componentRegistrar.registerComponent(componentDefinition); } } diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/BeanUtils.java b/org.springframework.beans/src/main/java/org/springframework/beans/BeanUtils.java index 7c5ba8295ab..dcbe5a54e51 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -86,7 +86,7 @@ public abstract class BeanUtils { } /** - * Convenience method to instantiate a class using its no-arg constructor. + * Instantiate a class using its no-arg constructor. * As this method doesn't try to load classes by name, it should avoid * class-loading issues. *

Note that this method tries to set the constructor accessible @@ -108,6 +108,27 @@ public abstract class BeanUtils { } } + /** + * Instantiate a class using its no-arg constructor and return the new instance + * as the the specified assignable type. + *

Useful in cases where + * the type of the class to instantiate (clazz) is not available, but the type + * desired (assignableTo) is known. + *

As this method doesn't try to load classes by name, it should avoid + * class-loading issues. + *

Note that this method tries to set the constructor accessible + * if given a non-accessible (that is, non-public) constructor. + * @param clazz class to instantiate + * @param assignableTo type that clazz must be assignableTo + * @return the new instance + * @throws BeanInstantiationException if the bean cannot be instantiated + */ + @SuppressWarnings("unchecked") + public static T instantiateClass(Class clazz, Class assignableTo) throws BeanInstantiationException { + Assert.isAssignable(assignableTo, clazz); + return (T)instantiateClass(clazz); + } + /** * Convenience method to instantiate a class using the given constructor. * As this method doesn't try to load classes by name, it should avoid diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionRegistrar.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionRegistrar.java new file mode 100644 index 00000000000..9184b9c0a3b --- /dev/null +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionRegistrar.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2011 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.beans.factory.parsing; + +import org.springframework.beans.factory.config.BeanDefinition; + +public interface BeanDefinitionRegistrar { + + String registerWithGeneratedName(BeanDefinition beanDefinition); + +} diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ComponentRegistrar.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ComponentRegistrar.java new file mode 100644 index 00000000000..94eb6541b62 --- /dev/null +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ComponentRegistrar.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2011 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.beans.factory.parsing; + +public interface ComponentRegistrar extends BeanDefinitionRegistrar { + + void registerBeanComponent(BeanComponentDefinition component); + + void registerComponent(ComponentDefinition component); +} diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java index 64689f6486c..39c9741a971 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java @@ -30,12 +30,13 @@ public class ReaderContext { private final Resource resource; - private final ProblemReporter problemReporter; - private final ReaderEventListener eventListener; private final SourceExtractor sourceExtractor; + // TODO SPR-7420: review exposing problem reporter + protected final ProblemReporter problemReporter; + public ReaderContext(Resource resource, ProblemReporter problemReporter, ReaderEventListener eventListener, SourceExtractor sourceExtractor) { diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/SimpleProblemCollector.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/SimpleProblemCollector.java new file mode 100644 index 00000000000..2f90f761fae --- /dev/null +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/SimpleProblemCollector.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2011 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.beans.factory.parsing; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.io.DescriptiveResource; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public class SimpleProblemCollector { + + private Location location = null; + private List errors = new ArrayList(); + + public SimpleProblemCollector(Object location) { + if (location != null) { + this.location = new Location(new DescriptiveResource(location.toString())); + } + } + + public void error(String message) { + this.errors.add(new Problem(message, this.location)); + } + + public void error(String message, Throwable cause) { + this.errors.add(new Problem(message, this.location, null, cause)); + } + + public void reportProblems(ProblemReporter reporter) { + for (Problem error : errors) { + reporter.error(error); + } + } + + public boolean hasErrors() { + return this.errors.size() > 0; + } + +} diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java index 478697fffcf..3b338d5b27b 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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. diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java index 1d80a290cfc..26603663fd3 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java @@ -166,7 +166,7 @@ public class BeanDefinitionReaderUtils { * for the given bean definition or the definition cannot be registered */ public static String registerWithGeneratedName( - AbstractBeanDefinition definition, BeanDefinitionRegistry registry) + BeanDefinition definition, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { String generatedName = generateBeanName(definition, registry, false); diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java index 93ecbb44bb5..2f352d9dddf 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -21,9 +21,8 @@ import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; /** - * Interface used by the - * {@link org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader} to - * handle custom, top-level (directly under <beans>) tags. + * Interface used by the {@link DefaultBeanDefinitionDocumentReader} to handle custom, + * top-level (directly under {@code }) tags. * *

Implementations are free to turn the metadata in the custom tag into as many * {@link BeanDefinition BeanDefinitions} as required. @@ -31,11 +30,19 @@ import org.springframework.beans.factory.config.BeanDefinition; *

The parser locates a {@link BeanDefinitionParser} from the associated * {@link NamespaceHandler} for the namespace in which the custom tag resides. * + *

Implementations are encouraged to decouple XML parsing from bean registration by + * parsing element(s) into a {@link org.springframework.context.FeatureSpecification + * FeatureSpecification} object and subsequently executing that specification. + * Doing so allows for maximum reuse between XML-based and annotation-based + * configuration options. + * * @author Rob Harrop * @since 2.0 * @see NamespaceHandler - * @see org.springframework.beans.factory.xml.BeanDefinitionDecorator * @see AbstractBeanDefinitionParser + * @see org.springframework.beans.factory.xml.BeanDefinitionDecorator + * @see org.springframework.context.FeatureSpecification + * @see org.springframework.context.AbstractSpecificationExecutor */ public interface BeanDefinitionParser { diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java index c36314c0d14..78ad74e2f2c 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java @@ -21,6 +21,7 @@ import java.util.Stack; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.ComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -36,7 +37,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; * @see XmlReaderContext * @see BeanDefinitionParserDelegate */ -public final class ParserContext { +public final class ParserContext implements ComponentRegistrar { private final XmlReaderContext readerContext; @@ -121,4 +122,8 @@ public final class ParserContext { registerComponent(component); } + public String registerWithGeneratedName(BeanDefinition beanDefinition) { + return this.readerContext.registerWithGeneratedName(beanDefinition); + } + } diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java index cbb5a0d5040..8dd361ce399 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java @@ -83,4 +83,9 @@ public class XmlReaderContext extends ReaderContext { return generatedName; } + // TODO SPR-7420: review exposing problem reporter + public ProblemReporter getProblemReporter() { + return this.problemReporter; + } + } diff --git a/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index eef452f0ee9..f65f0ae2927 100644 --- a/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,15 @@ package org.springframework.beans.factory; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.lang.reflect.Field; import java.net.MalformedURLException; import java.security.AccessControlContext; @@ -31,20 +40,13 @@ import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; + import javax.security.auth.Subject; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import static org.junit.Assert.*; import org.junit.Ignore; import org.junit.Test; -import test.beans.DerivedTestBean; -import test.beans.DummyFactory; -import test.beans.ITestBean; -import test.beans.LifecycleBean; -import test.beans.NestedTestBean; -import test.beans.TestBean; - import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.NotWritablePropertyException; @@ -75,12 +77,19 @@ import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.util.StopWatch; +import test.beans.DerivedTestBean; +import test.beans.DummyFactory; +import test.beans.ITestBean; +import test.beans.LifecycleBean; +import test.beans.NestedTestBean; +import test.beans.TestBean; + /** * Tests properties population and autowire behavior. * @@ -849,7 +858,7 @@ public class DefaultListableBeanFactoryTests { @Test public void testCustomConverter() { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); - GenericConversionService conversionService = (GenericConversionService) ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new Converter() { public Float convert(String source) { try { diff --git a/org.springframework.context/.springBeans b/org.springframework.context/.springBeans index 84849427f41..c36c1e5de08 100644 --- a/org.springframework.context/.springBeans +++ b/org.springframework.context/.springBeans @@ -11,6 +11,7 @@ src/test/java/org/springframework/context/annotation/configuration/SecondLevelSubConfig-context.xml src/test/java/org/springframework/context/annotation/configuration/ImportXmlWithAopNamespace-context.xml src/test/java/org/springframework/context/annotation/Spr6602Tests-context.xml + src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java index fd8b5dc25b8..334cc233f12 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -141,5 +141,4 @@ public class AnnotatedBeanDefinitionReader { definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); } - } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java index 1c201527b94..62f07731908 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -37,10 +37,9 @@ import org.springframework.util.Assert; */ public class AnnotationScopeMetadataResolver implements ScopeMetadataResolver { - private Class scopeAnnotationType = Scope.class; - - private final ScopedProxyMode defaultProxyMode; + protected Class scopeAnnotationType = Scope.class; + private final ScopedProxyMode defaultProxyMode; /** * Create a new instance of the AnnotationScopeMetadataResolver class. diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java new file mode 100644 index 00000000000..f44d7b50518 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import java.lang.reflect.Method; + +import org.springframework.core.annotation.AnnotationUtils; + +/** + * Utilities for processing {@link Bean}-annotated methods. + * + * @author Chris Beams + * @since 3.1 + */ +class BeanAnnotationHelper { + + /** + * Return whether the given method is annotated directly or indirectly with @Bean. + */ + public static boolean isBeanAnnotated(Method method) { + return AnnotationUtils.findAnnotation(method, Bean.class) != null; + } + + public static String determineBeanNameFor(Method beanMethod) { + // by default the bean name is the name of the @Bean-annotated method + String beanName = beanMethod.getName(); + + // check to see if the user has explicitly set the bean name + Bean bean = AnnotationUtils.findAnnotation(beanMethod, Bean.class); + if (bean != null && bean.name().length > 0) { + beanName = bean.name()[0]; + } + + return beanName; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java index 4f780ef343b..9488edf117c 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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. @@ -208,9 +208,10 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo *

This method does not register an annotation config processor * but rather leaves this up to the caller. * @param basePackages the packages to check for annotated classes - * @return number of beans registered + * @return set of beans registered if any for tooling registration purposes (never {@code null}) */ protected Set doScan(String... basePackages) { + Assert.notEmpty(basePackages, "At least one base package must be specified"); Set beanDefinitions = new LinkedHashSet(); for (String basePackage : basePackages) { Set candidates = findCandidateComponents(basePackage); diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index 9ea48239b14..459d7b271cf 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -69,8 +69,7 @@ import org.springframework.util.ClassUtils; */ public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware { - private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; - + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; protected final Log logger = LogFactory.getLog(getClass()); diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScan.java index 043a485ea3c..c45ca50401f 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScan.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,59 +16,137 @@ package org.springframework.context.annotation; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.beans.factory.support.BeanNameGenerator; - +import org.springframework.core.type.filter.TypeFilter; /** - * Configures component scanning directives for use with {@link Configuration} - * classes. Provides support parallel with Spring XML's - * {@code } element. + * Configures component scanning directives for use with {@link Configuration @Configuration} + * classes. Provides support parallel with Spring XML's {@code } + * element. * - * TODO SPR-7508: complete documentation. + *

One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias {@link #value()} + * must be specified. + * + *

Note that the {@code } element has an {@code annotation-config} + * attribute, however this annotation does not. This is because in almost all cases when + * using {@code @ComponentScan}, default annotation config processing (e.g. + * processing {@code @Autowired} and friends) is assumed. Furthermore, when using + * {@link AnnotationConfigApplicationContext}, annotation config processors are always + * registered, meaning that any attempt to disable them at the {@code @ComponentScan} level + * would be ignored. * * @author Chris Beams * @since 3.1 - * @see FilterType - * @see Configuration */ +@Documented +@FeatureAnnotation(parser=ComponentScanAnnotationParser.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ComponentScan { - /** base packages to scan */ + /** + * Alias for the {@link #basePackages()} attribute. + * Allows for more concise annotation declarations e.g.: + * {@code @ComponentScan("org.my.pkg")} instead of + * {@code @ComponentScan(basePackages="org.my.pkg")}. + */ String[] value() default {}; - Class[] packageOf() default Void.class; + /** + * Base packages to scan for annotated components. + *

{@link #value()} is an alias for (and mutually exclusive with) this attribute. + *

Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + */ + String[] basePackages() default {}; + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages + * to scan for annotated components. The package of each class specified will be scanned. + *

Consider creating a special no-op marker class or interface in each package + * that serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + + /** + * The {@link BeanNameGenerator} class to be used for naming detected components + * within the Spring container. + */ Class nameGenerator() default AnnotationBeanNameGenerator.class; + /** + * The {@link ScopeMetadataResolver} to be used for resolving the scope of detected components. + */ Class scopeResolver() default AnnotationScopeMetadataResolver.class; - String resourcePattern() default "**/*.class"; - + /** + * Indicates whether proxies should be generated for detected components, which may be + * necessary when using scopes in a proxy-style fashion. + *

The default is defer to the default behavior of the component scanner used to + * execute the actual scan. + * @see ClassPathBeanDefinitionScanner#setScopedProxyMode(ScopedProxyMode) + */ ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT; + /** + * Controls the class files eligible for component detection. + *

Consider use of {@link #includeFilters()} and {@link #excludeFilters()} + * for a more flexible approach. + */ + String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN; + + /** + * Indicates whether automatic detection of classes annotated with {@code @Component} + * {@code @Repository}, {@code @Service}, or {@code @Controller} should be enabled. + */ boolean useDefaultFilters() default true; - IncludeFilter[] includeFilters() default {}; + /** + * Specifies which types are eligible for component scanning. + *

Further narrows the set of candidate components from everything in + * {@link #basePackages()} to everything in the base packages that matches + * the given filter or filters. + * @see #resourcePattern() + */ + Filter[] includeFilters() default {}; - ExcludeFilter[] excludeFilters() default {}; + /** + * Specifies which types are not eligible for component scanning. + * @see #resourcePattern() + */ + Filter[] excludeFilters() default {}; + /** + * Declares the type filter to be used as an {@linkplain ComponentScan#includeFilters() + * include filter} or {@linkplain ComponentScan#includeFilters() exclude filter}. + */ @Retention(RetentionPolicy.SOURCE) - @interface IncludeFilter { + @interface Filter { + /** + * The type of filter to use. + *

Note that the filter types available are limited to those that may + * be expressed as a {@code Class} in the {@link #value()} attribute. This is + * in contrast to {@code }, which allows for + * expression-based (i.e., string-based) filters such as AspectJ pointcuts. + * These filter types are intentionally not supported here, and not available + * in the {@link FilterType} enum. + * @see FilterType + */ FilterType type() default FilterType.ANNOTATION; - Class value(); - } - @Retention(RetentionPolicy.SOURCE) - @interface ExcludeFilter { - FilterType type() default FilterType.ANNOTATION; + /** + * The class to use as the filter. In the case of {@link FilterType#ANNOTATION}, + * the class will be the annotation itself. In the case of + * {@link FilterType#ASSIGNABLE_TYPE}, the class will be the type that detected + * components should be assignable to. And in the case of {@link FilterType#CUSTOM}, + * the class will be an implementation of {@link TypeFilter}. + */ Class value(); } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java new file mode 100644 index 00000000000..47c1abedaa9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import java.util.Map; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link FeatureAnnotationParser} implementation that reads attributes from a + * {@link ComponentScan @ComponentScan} annotation into a {@link ComponentScanSpec} + * which can in turn be executed by {@link ComponentScanExecutor}. + * {@link ComponentScanBeanDefinitionParser} serves the same role for + * the {@code } XML element. + * + *

Note that {@link ComponentScanSpec} objects may be directly + * instantiated and returned from {@link Feature @Feature} methods as an + * alternative to using the {@link ComponentScan @ComponentScan} annotation. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScan + * @see ComponentScanSpec + * @see ComponentScanExecutor + * @see ComponentScanBeanDefinitionParser + * @see ConfigurationClassBeanDefinitionReader + */ +final class ComponentScanAnnotationParser implements FeatureAnnotationParser { + + /** + * Create and return a new {@link ComponentScanSpec} from the given + * {@link ComponentScan} annotation metadata. + * @throws IllegalArgumentException if ComponentScan attributes are not present in metadata + */ + public ComponentScanSpec process(AnnotationMetadata metadata) { + Map attribs = metadata.getAnnotationAttributes(ComponentScan.class.getName(), true); + Assert.notNull(attribs, String.format("@ComponentScan annotation not found " + + "while parsing metadata for class [%s].", metadata.getClassName())); + + ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + ComponentScanSpec spec = new ComponentScanSpec(); + + for (String pkg : (String[])attribs.get("value")) { + spec.addBasePackage(pkg); + } + for (String pkg : (String[])attribs.get("basePackages")) { + spec.addBasePackage(pkg); + } + for (String className : (String[])attribs.get("basePackageClasses")) { + spec.addBasePackage(className.substring(0, className.lastIndexOf('.'))); + } + + String resolverAttribute = "scopeResolver"; + if (!((String)attribs.get(resolverAttribute)).equals(((Class)AnnotationUtils.getDefaultValue(ComponentScan.class, resolverAttribute)).getName())) { + spec.scopeMetadataResolver((String)attribs.get(resolverAttribute), classLoader); + } + String scopedProxyAttribute = "scopedProxy"; + ScopedProxyMode scopedProxyMode = (ScopedProxyMode) attribs.get(scopedProxyAttribute); + if (scopedProxyMode != ((ScopedProxyMode)AnnotationUtils.getDefaultValue(ComponentScan.class, scopedProxyAttribute))) { + spec.scopedProxyMode(scopedProxyMode); + } + + for (Filter filter : (Filter[]) attribs.get("includeFilters")) { + spec.addIncludeFilter(filter.type().toString(), filter.value().getName(), classLoader); + } + for (Filter filter : (Filter[]) attribs.get("excludeFilters")) { + spec.addExcludeFilter(filter.type().toString(), filter.value().getName(), classLoader); + } + + spec.resourcePattern((String)attribs.get("resourcePattern")) + .useDefaultFilters((Boolean)attribs.get("useDefaultFilters")) + .beanNameGenerator((String)attribs.get("nameGenerator"), classLoader) + .source(metadata.getClassName()) + .sourceName(metadata.getClassName()); + + return spec; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java index ee1a4920224..bb56e713e73 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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,264 +16,80 @@ package org.springframework.context.annotation; -import java.lang.annotation.Annotation; -import java.util.Set; -import java.util.regex.Pattern; - +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.context.config.ExecutorContext; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import org.springframework.beans.BeanUtils; -import org.springframework.beans.FatalBeanException; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.parsing.CompositeComponentDefinition; -import org.springframework.beans.factory.support.BeanNameGenerator; -import org.springframework.beans.factory.xml.BeanDefinitionParser; -import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.beans.factory.xml.XmlReaderContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.type.filter.AnnotationTypeFilter; -import org.springframework.core.type.filter.AspectJTypeFilter; -import org.springframework.core.type.filter.AssignableTypeFilter; -import org.springframework.core.type.filter.RegexPatternTypeFilter; -import org.springframework.core.type.filter.TypeFilter; -import org.springframework.util.StringUtils; - /** - * Parser for the <context:component-scan/> element. - * + * Parser for the {@code } element. Parsed metadata is + * used to populate and execute a {@link ComponentScanSpec} instance. + * * @author Mark Fisher * @author Ramnivas Laddad * @author Juergen Hoeller + * @author Chris Beams * @since 2.5 + * @see ComponentScan + * @see ComponentScanSpec + * @see ComponentScanExecutor */ public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser { - private static final String BASE_PACKAGE_ATTRIBUTE = "base-package"; - - private static final String RESOURCE_PATTERN_ATTRIBUTE = "resource-pattern"; - - private static final String USE_DEFAULT_FILTERS_ATTRIBUTE = "use-default-filters"; - - private static final String ANNOTATION_CONFIG_ATTRIBUTE = "annotation-config"; - - private static final String NAME_GENERATOR_ATTRIBUTE = "name-generator"; - - private static final String SCOPE_RESOLVER_ATTRIBUTE = "scope-resolver"; - - private static final String SCOPED_PROXY_ATTRIBUTE = "scoped-proxy"; - - private static final String EXCLUDE_FILTER_ELEMENT = "exclude-filter"; - - private static final String INCLUDE_FILTER_ELEMENT = "include-filter"; - - private static final String FILTER_TYPE_ATTRIBUTE = "type"; - - private static final String FILTER_EXPRESSION_ATTRIBUTE = "expression"; - - public BeanDefinition parse(Element element, ParserContext parserContext) { - String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(BASE_PACKAGE_ATTRIBUTE), - ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); - - // Actually scan for bean definitions and register them. - ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element); - Set beanDefinitions = scanner.doScan(basePackages); - registerComponents(parserContext.getReaderContext(), beanDefinitions, element); - - return null; - } - - protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) { XmlReaderContext readerContext = parserContext.getReaderContext(); + ClassLoader classLoader = readerContext.getResourceLoader().getClassLoader(); - boolean useDefaultFilters = true; - if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) { - useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)); - } - - // Delegate bean definition registration to scanner class. - ClassPathBeanDefinitionScanner scanner = createScanner(readerContext, useDefaultFilters); - scanner.setResourceLoader(readerContext.getResourceLoader()); - scanner.setEnvironment(parserContext.getDelegate().getEnvironment()); - scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults()); - scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns()); - - if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) { - scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE)); - } - - try { - parseBeanNameGenerator(element, scanner); - } - catch (Exception ex) { - readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); - } - - try { - parseScope(element, scanner); - } - catch (Exception ex) { - readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); - } - - parseTypeFilters(element, scanner, readerContext, parserContext); - - return scanner; - } - - protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) { - return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters); - } - - protected void registerComponents( - XmlReaderContext readerContext, Set beanDefinitions, Element element) { - - Object source = readerContext.extractSource(element); - CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), source); - - for (BeanDefinitionHolder beanDefHolder : beanDefinitions) { - compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder)); - } - - // Register annotation config processors, if necessary. - boolean annotationConfig = true; - if (element.hasAttribute(ANNOTATION_CONFIG_ATTRIBUTE)) { - annotationConfig = Boolean.valueOf(element.getAttribute(ANNOTATION_CONFIG_ATTRIBUTE)); - } - if (annotationConfig) { - Set processorDefinitions = - AnnotationConfigUtils.registerAnnotationConfigProcessors(readerContext.getRegistry(), source); - for (BeanDefinitionHolder processorDefinition : processorDefinitions) { - compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition)); - } - } - - readerContext.fireComponentRegistered(compositeDef); - } - - protected void parseBeanNameGenerator(Element element, ClassPathBeanDefinitionScanner scanner) { - if (element.hasAttribute(NAME_GENERATOR_ATTRIBUTE)) { - BeanNameGenerator beanNameGenerator = (BeanNameGenerator) instantiateUserDefinedStrategy( - element.getAttribute(NAME_GENERATOR_ATTRIBUTE), BeanNameGenerator.class, - scanner.getResourceLoader().getClassLoader()); - scanner.setBeanNameGenerator(beanNameGenerator); - } - } - - protected void parseScope(Element element, ClassPathBeanDefinitionScanner scanner) { - // Register ScopeMetadataResolver if class name provided. - if (element.hasAttribute(SCOPE_RESOLVER_ATTRIBUTE)) { - if (element.hasAttribute(SCOPED_PROXY_ATTRIBUTE)) { - throw new IllegalArgumentException( - "Cannot define both 'scope-resolver' and 'scoped-proxy' on tag"); - } - ScopeMetadataResolver scopeMetadataResolver = (ScopeMetadataResolver) instantiateUserDefinedStrategy( - element.getAttribute(SCOPE_RESOLVER_ATTRIBUTE), ScopeMetadataResolver.class, - scanner.getResourceLoader().getClassLoader()); - scanner.setScopeMetadataResolver(scopeMetadataResolver); - } - - if (element.hasAttribute(SCOPED_PROXY_ATTRIBUTE)) { - String mode = element.getAttribute(SCOPED_PROXY_ATTRIBUTE); - if ("targetClass".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.TARGET_CLASS); - } - else if ("interfaces".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.INTERFACES); - } - else if ("no".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.NO); - } - else { - throw new IllegalArgumentException("scoped-proxy only supports 'no', 'interfaces' and 'targetClass'"); - } - } - } - - protected void parseTypeFilters( - Element element, ClassPathBeanDefinitionScanner scanner, XmlReaderContext readerContext, ParserContext parserContext) { + ComponentScanSpec spec = + ComponentScanSpec.forDelimitedPackages(element.getAttribute("base-package")) + .includeAnnotationConfig(element.getAttribute("annotation-config")) + .useDefaultFilters(element.getAttribute("use-default-filters")) + .resourcePattern(element.getAttribute("resource-pattern")) + .beanNameGenerator(element.getAttribute("name-generator"), classLoader) + .scopeMetadataResolver(element.getAttribute("scope-resolver"), classLoader) + .scopedProxyMode(element.getAttribute("scoped-proxy")); // Parse exclude and include filter elements. - ClassLoader classLoader = scanner.getResourceLoader().getClassLoader(); NodeList nodeList = element.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { String localName = parserContext.getDelegate().getLocalName(node); - try { - if (INCLUDE_FILTER_ELEMENT.equals(localName)) { - TypeFilter typeFilter = createTypeFilter((Element) node, classLoader); - scanner.addIncludeFilter(typeFilter); - } - else if (EXCLUDE_FILTER_ELEMENT.equals(localName)) { - TypeFilter typeFilter = createTypeFilter((Element) node, classLoader); - scanner.addExcludeFilter(typeFilter); - } + String filterType = ((Element)node).getAttribute("type"); + String expression = ((Element)node).getAttribute("expression"); + if ("include-filter".equals(localName)) { + spec.addIncludeFilter(filterType, expression, classLoader); } - catch (Exception ex) { - readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); + else if ("exclude-filter".equals(localName)) { + spec.addExcludeFilter(filterType, expression, classLoader); } } } + + spec.beanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults()) + .autowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns()) + .source(readerContext.extractSource(element)) + .sourceName(element.getTagName()) + .execute(createExecutorContext(parserContext)); + return null; } - @SuppressWarnings("unchecked") - protected TypeFilter createTypeFilter(Element element, ClassLoader classLoader) { - String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE); - String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE); - try { - if ("annotation".equals(filterType)) { - return new AnnotationTypeFilter((Class) classLoader.loadClass(expression)); - } - else if ("assignable".equals(filterType)) { - return new AssignableTypeFilter(classLoader.loadClass(expression)); - } - else if ("aspectj".equals(filterType)) { - return new AspectJTypeFilter(expression, classLoader); - } - else if ("regex".equals(filterType)) { - return new RegexPatternTypeFilter(Pattern.compile(expression)); - } - else if ("custom".equals(filterType)) { - Class filterClass = classLoader.loadClass(expression); - if (!TypeFilter.class.isAssignableFrom(filterClass)) { - throw new IllegalArgumentException( - "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression); - } - return (TypeFilter) BeanUtils.instantiateClass(filterClass); - } - else { - throw new IllegalArgumentException("Unsupported filter type: " + filterType); - } - } - catch (ClassNotFoundException ex) { - throw new FatalBeanException("Type filter class not found: " + expression, ex); - } - } - @SuppressWarnings("unchecked") - private Object instantiateUserDefinedStrategy(String className, Class strategyType, ClassLoader classLoader) { - Object result = null; - try { - result = classLoader.loadClass(className).newInstance(); - } - catch (ClassNotFoundException ex) { - throw new IllegalArgumentException("Class [" + className + "] for strategy [" + - strategyType.getName() + "] not found", ex); - } - catch (Exception ex) { - throw new IllegalArgumentException("Unable to instantiate class [" + className + "] for strategy [" + - strategyType.getName() + "]. A zero-argument constructor is required", ex); - } - - if (!strategyType.isAssignableFrom(result.getClass())) { - throw new IllegalArgumentException("Provided class name must be an implementation of " + strategyType); - } - return result; + // Adapt the given ParserContext instance into an ExecutorContext. + // TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter utility + // or otherwise unify these two types + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setResourceLoader(parserContext.getReaderContext().getResourceLoader()); + executorContext.setEnvironment(parserContext.getDelegate().getEnvironment()); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanExecutor.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanExecutor.java new file mode 100644 index 00000000000..ac146d176f2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanExecutor.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import java.util.Set; + +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.filter.TypeFilter; + +/** + * Executes the {@link ComponentScanSpec} feature specification. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScanSpec + * @see ComponentScanBeanDefinitionParser + * @see ComponentScan + */ +final class ComponentScanExecutor extends AbstractSpecificationExecutor { + + /** + * Configure a {@link ClassPathBeanDefinitionScanner} based on the content of + * the given specification and perform actual scanning and bean definition + * registration. + */ + protected void doExecute(ComponentScanSpec spec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ResourceLoader resourceLoader = executorContext.getResourceLoader(); + Environment environment = executorContext.getEnvironment(); + + ClassPathBeanDefinitionScanner scanner = spec.useDefaultFilters() == null ? + new ClassPathBeanDefinitionScanner(registry) : + new ClassPathBeanDefinitionScanner(registry, spec.useDefaultFilters()); + + scanner.setResourceLoader(resourceLoader); + scanner.setEnvironment(environment); + + if (spec.beanDefinitionDefaults() != null) { + scanner.setBeanDefinitionDefaults(spec.beanDefinitionDefaults()); + } + if (spec.autowireCandidatePatterns() != null) { + scanner.setAutowireCandidatePatterns(spec.autowireCandidatePatterns()); + } + + if (spec.resourcePattern() != null) { + scanner.setResourcePattern(spec.resourcePattern()); + } + if (spec.beanNameGenerator() != null) { + scanner.setBeanNameGenerator(spec.beanNameGenerator()); + } + if (spec.includeAnnotationConfig() != null) { + scanner.setIncludeAnnotationConfig(spec.includeAnnotationConfig()); + } + if (spec.scopeMetadataResolver() != null) { + scanner.setScopeMetadataResolver(spec.scopeMetadataResolver()); + } + if (spec.scopedProxyMode() != null) { + scanner.setScopedProxyMode(spec.scopedProxyMode()); + } + for (TypeFilter filter : spec.includeFilters()) { + scanner.addIncludeFilter(filter); + } + for (TypeFilter filter : spec.excludeFilters()) { + scanner.addExcludeFilter(filter); + } + + Set scannedBeans = scanner.doScan(spec.basePackages()); + + Object source = spec.source(); + String sourceName = spec.sourceName(); + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(sourceName, source); + + for (BeanDefinitionHolder beanDefHolder : scannedBeans) { + compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder)); + } + + // Register annotation config processors, if necessary. + if ((spec.includeAnnotationConfig() != null) && spec.includeAnnotationConfig()) { + Set processorDefinitions = + AnnotationConfigUtils.registerAnnotationConfigProcessors(registry, source); + for (BeanDefinitionHolder processorDefinition : processorDefinitions) { + compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition)); + } + } + + executorContext.getRegistrar().registerComponent(compositeDef); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanSpec.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanSpec.java new file mode 100644 index 00000000000..6a85d98b680 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanSpec.java @@ -0,0 +1,460 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.beans.factory.support.BeanDefinitionDefaults; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AspectJTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Specifies the configuration of Spring's component-scanning feature. + * May be used directly within a {@link Feature @Feature} method, or indirectly + * through the {@link ComponentScan @ComponentScan} annotation. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScan + * @see ComponentScanAnnotationParser + * @see ComponentScanBeanDefinitionParser + * @see ComponentScanExecutor + */ +public final class ComponentScanSpec extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = ComponentScanExecutor.class; + + private Boolean includeAnnotationConfig = null; + private String resourcePattern = null; + private List basePackages = new ArrayList(); + private Object beanNameGenerator = null; + private Object scopeMetadataResolver = null; + private Object scopedProxyMode = null; + private Boolean useDefaultFilters = null; + private List includeFilters = new ArrayList(); + private List excludeFilters = new ArrayList(); + + private BeanDefinitionDefaults beanDefinitionDefaults; + private String[] autowireCandidatePatterns; + + private ClassLoader classLoader; + + /** + * Package-visible constructor for use by {@link ComponentScanBeanDefinitionParser}. + * End users should always call String... or Class... constructors to specify + * base packages. + * + * @see #validate() + */ + ComponentScanSpec() { + super(EXECUTOR_TYPE); + } + + /** + * + * @param basePackages + * @see #forDelimitedPackages(String) + */ + public ComponentScanSpec(String... basePackages) { + this(); + Assert.notEmpty(basePackages, "At least one base package must be specified"); + for (String basePackage : basePackages) { + addBasePackage(basePackage); + } + } + + public ComponentScanSpec(Class... basePackageClasses) { + this(packagesFor(basePackageClasses)); + } + + + public ComponentScanSpec includeAnnotationConfig(Boolean includeAnnotationConfig) { + this.includeAnnotationConfig = includeAnnotationConfig; + return this; + } + + ComponentScanSpec includeAnnotationConfig(String includeAnnotationConfig) { + if (StringUtils.hasText(includeAnnotationConfig)) { + this.includeAnnotationConfig = Boolean.valueOf(includeAnnotationConfig); + } + return this; + } + + Boolean includeAnnotationConfig() { + return this.includeAnnotationConfig; + } + + public ComponentScanSpec resourcePattern(String resourcePattern) { + if (StringUtils.hasText(resourcePattern)) { + this.resourcePattern = resourcePattern; + } + return this; + } + + String resourcePattern() { + return resourcePattern; + } + + ComponentScanSpec addBasePackage(String basePackage) { + if (StringUtils.hasText(basePackage)) { + this.basePackages.add(basePackage); + } + return this; + } + + /** + * Return the set of base packages specified, never {@code null}, never empty + * post-validation. + * @see #doValidate(SimpleProblemReporter) + */ + String[] basePackages() { + return this.basePackages.toArray(new String[this.basePackages.size()]); + } + + public ComponentScanSpec beanNameGenerator(BeanNameGenerator beanNameGenerator) { + this.beanNameGenerator = beanNameGenerator; + return this; + } + + /** + * Set the class name of the BeanNameGenerator to be used and the ClassLoader + * to load it. + */ + ComponentScanSpec beanNameGenerator(String beanNameGenerator, ClassLoader classLoader) { + setClassLoader(classLoader); + if (StringUtils.hasText(beanNameGenerator)) { + this.beanNameGenerator = beanNameGenerator; + } + return this; + } + + BeanNameGenerator beanNameGenerator() { + return nullSafeTypedObject(this.beanNameGenerator, BeanNameGenerator.class); + } + + public ComponentScanSpec scopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) { + this.scopeMetadataResolver = scopeMetadataResolver; + return this; + } + + ComponentScanSpec scopeMetadataResolver(String scopeMetadataResolver, ClassLoader classLoader) { + setClassLoader(classLoader); + if (StringUtils.hasText(scopeMetadataResolver)) { + this.scopeMetadataResolver = scopeMetadataResolver; + } + return this; + } + + ScopeMetadataResolver scopeMetadataResolver() { + return nullSafeTypedObject(this.scopeMetadataResolver, ScopeMetadataResolver.class); + } + + public ComponentScanSpec scopedProxyMode(ScopedProxyMode scopedProxyMode) { + this.scopedProxyMode = scopedProxyMode; + return this; + } + + ComponentScanSpec scopedProxyMode(String scopedProxyMode) { + if (StringUtils.hasText(scopedProxyMode)) { + this.scopedProxyMode = scopedProxyMode; + } + return this; + } + + ScopedProxyMode scopedProxyMode() { + return nullSafeTypedObject(this.scopedProxyMode, ScopedProxyMode.class); + } + + public ComponentScanSpec useDefaultFilters(Boolean useDefaultFilters) { + this.useDefaultFilters = useDefaultFilters; + return this; + } + + ComponentScanSpec useDefaultFilters(String useDefaultFilters) { + if (StringUtils.hasText(useDefaultFilters)) { + this.useDefaultFilters = Boolean.valueOf(useDefaultFilters); + } + return this; + } + + Boolean useDefaultFilters() { + return this.useDefaultFilters; + } + + public ComponentScanSpec includeFilters(TypeFilter... includeFilters) { + this.includeFilters.clear(); + for (TypeFilter filter : includeFilters) { + addIncludeFilter(filter); + } + return this; + } + + ComponentScanSpec addIncludeFilter(TypeFilter includeFilter) { + Assert.notNull(includeFilter, "includeFilter must not be null"); + this.includeFilters.add(includeFilter); + return this; + } + + ComponentScanSpec addIncludeFilter(String filterType, String expression, ClassLoader classLoader) { + this.includeFilters.add(new FilterTypeDescriptor(filterType, expression, classLoader)); + return this; + } + + TypeFilter[] includeFilters() { + return this.includeFilters.toArray(new TypeFilter[this.includeFilters.size()]); + } + + public ComponentScanSpec excludeFilters(TypeFilter... excludeFilters) { + this.excludeFilters.clear(); + for (TypeFilter filter : excludeFilters) { + addExcludeFilter(filter); + } + return this; + } + + ComponentScanSpec addExcludeFilter(TypeFilter excludeFilter) { + Assert.notNull(excludeFilter, "excludeFilter must not be null"); + this.excludeFilters.add(excludeFilter); + return this; + } + + ComponentScanSpec addExcludeFilter(String filterType, String expression, ClassLoader classLoader) { + this.excludeFilters.add(new FilterTypeDescriptor(filterType, expression, classLoader)); + return this; + } + + TypeFilter[] excludeFilters() { + return this.excludeFilters.toArray(new TypeFilter[this.excludeFilters.size()]); + } + + ComponentScanSpec beanDefinitionDefaults(BeanDefinitionDefaults beanDefinitionDefaults) { + this.beanDefinitionDefaults = beanDefinitionDefaults; + return this; + } + + BeanDefinitionDefaults beanDefinitionDefaults() { + return this.beanDefinitionDefaults; + } + + ComponentScanSpec autowireCandidatePatterns(String[] autowireCandidatePatterns) { + this.autowireCandidatePatterns = autowireCandidatePatterns; + return this; + } + + String[] autowireCandidatePatterns() { + return this.autowireCandidatePatterns; + } + + + /** + * Create a ComponentScanSpec from a single string containing + * delimited package names. + * @see ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS + */ + static ComponentScanSpec forDelimitedPackages(String basePackages) { + Assert.notNull(basePackages, "base packages must not be null"); + return new ComponentScanSpec( + StringUtils.tokenizeToStringArray(basePackages, + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); + } + + public void doValidate(SimpleProblemCollector problems) { + if(this.basePackages.isEmpty()) { + problems.error("At least one base package must be specified"); + } + + if(this.beanNameGenerator instanceof String) { + this.beanNameGenerator = instantiateUserDefinedType("bean name generator", BeanNameGenerator.class, this.beanNameGenerator, this.classLoader, problems); + } + + if(this.scopeMetadataResolver instanceof String) { + this.scopeMetadataResolver = instantiateUserDefinedType("scope metadata resolver", ScopeMetadataResolver.class, this.scopeMetadataResolver, this.classLoader, problems); + } + + if (this.scopedProxyMode instanceof String) { + if ("targetClass".equalsIgnoreCase((String)this.scopedProxyMode)) { + this.scopedProxyMode = ScopedProxyMode.TARGET_CLASS; + } + else if ("interfaces".equalsIgnoreCase((String)this.scopedProxyMode)) { + this.scopedProxyMode = ScopedProxyMode.INTERFACES; + } + else if ("no".equalsIgnoreCase((String)this.scopedProxyMode)) { + this.scopedProxyMode = ScopedProxyMode.NO; + } + else { + problems.error("invalid scoped proxy mode [%s] supported modes are " + + "'no', 'interfaces' and 'targetClass'"); + this.scopedProxyMode = null; + } + } + + if (this.scopeMetadataResolver != null && this.scopedProxyMode != null) { + problems.error("Cannot define both scope metadata resolver and scoped proxy mode"); + } + + for (int i = 0; i < this.includeFilters.size(); i++) { + if (this.includeFilters.get(i) instanceof FilterTypeDescriptor) { + this.includeFilters.set(i, ((FilterTypeDescriptor)this.includeFilters.get(i)).createTypeFilter(problems)); + } + } + + for (int i = 0; i < this.excludeFilters.size(); i++) { + if (this.excludeFilters.get(i) instanceof FilterTypeDescriptor) { + this.excludeFilters.set(i, ((FilterTypeDescriptor)this.excludeFilters.get(i)).createTypeFilter(problems)); + } + } + } + + private static Object instantiateUserDefinedType(String description, Class targetType, Object className, ClassLoader classLoader, SimpleProblemCollector problems) { + Assert.isInstanceOf(String.class, className, "userType must be of type String"); + Assert.notNull(classLoader, "classLoader must not be null"); + Assert.notNull(targetType, "targetType must not be null"); + Object instance = null; + try { + instance = classLoader.loadClass((String)className).newInstance(); + if (!targetType.isAssignableFrom(instance.getClass())) { + problems.error(description + " class name must be assignable to " + targetType.getSimpleName()); + instance = null; + } + } + catch (ClassNotFoundException ex) { + problems.error(String.format(description + " class [%s] not found", className), ex); + } + catch (Exception ex) { + problems.error(String.format("Unable to instantiate %s class [%s] for " + + "strategy [%s]. Has a no-argument constructor been provided?", + description, className, targetType.getClass().getSimpleName()), ex); + } + return instance; + } + + private void setClassLoader(ClassLoader classLoader) { + Assert.notNull(classLoader, "classLoader must not be null"); + if (this.classLoader == null) { + this.classLoader = classLoader; + } + else { + Assert.isTrue(this.classLoader == classLoader, "A classLoader has already been assigned " + + "and the supplied classLoader is not the same instance. Use the same classLoader " + + "for all string-based class properties."); + } + } + + @SuppressWarnings("unchecked") + private static T nullSafeTypedObject(Object object, Class type) { + if (object != null) { + if (!(type.isAssignableFrom(object.getClass()))) { + throw new IllegalStateException( + String.format("field must be of type %s but was actually of type %s", type, object.getClass())); + } + } + return (T)object; + } + + private static String[] packagesFor(Class[] classes) { + ArrayList packages = new ArrayList(); + for (Class clazz : classes) { + packages.add(clazz.getPackage().getName()); + } + return packages.toArray(new String[packages.size()]); + } + + + private static class FilterTypeDescriptor { + private String filterType; + private String expression; + private ClassLoader classLoader; + + FilterTypeDescriptor(String filterType, String expression, ClassLoader classLoader) { + Assert.notNull(filterType, "filterType must not be null"); + Assert.notNull(expression, "expression must not be null"); + Assert.notNull(classLoader, "classLoader must not be null"); + this.filterType = filterType; + this.expression = expression; + this.classLoader = classLoader; + } + + @SuppressWarnings("unchecked") + TypeFilter createTypeFilter(SimpleProblemCollector problems) { + try { + if ("annotation".equalsIgnoreCase(this.filterType)) { + return new AnnotationTypeFilter((Class) this.classLoader.loadClass(this.expression)); + } + else if ("assignable".equalsIgnoreCase(this.filterType) + || "assignable_type".equalsIgnoreCase(this.filterType)) { + return new AssignableTypeFilter(this.classLoader.loadClass(this.expression)); + } + else if ("aspectj".equalsIgnoreCase(this.filterType)) { + return new AspectJTypeFilter(this.expression, this.classLoader); + } + else if ("regex".equalsIgnoreCase(this.filterType)) { + return new RegexPatternTypeFilter(Pattern.compile(this.expression)); + } + else if ("custom".equalsIgnoreCase(this.filterType)) { + Class filterClass = this.classLoader.loadClass(this.expression); + if (!TypeFilter.class.isAssignableFrom(filterClass)) { + problems.error(String.format("custom type filter class [%s] must be assignable to %s", + this.expression, TypeFilter.class)); + } + return (TypeFilter) BeanUtils.instantiateClass(filterClass); + } + else { + problems.error(String.format("Unsupported filter type [%s]; supported types are: " + + "'annotation', 'assignable[_type]', 'aspectj', 'regex', 'custom'", this.filterType)); + } + } catch (ClassNotFoundException ex) { + problems.error("Type filter class not found: " + this.expression, ex); + } catch (Exception ex) { + problems.error(ex.getMessage(), ex.getCause()); + } + + return new PlaceholderTypeFilter(); + } + + + private class PlaceholderTypeFilter implements TypeFilter { + + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + throw new UnsupportedOperationException( + String.format("match() method for placeholder type filter for " + + "{filterType=%s,expression=%s} should never be invoked", + filterType, expression)); + } + + } + } + + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java index a277ff9efe2..2c8e6f40a85 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -59,7 +59,7 @@ import org.springframework.stereotype.Component; * @see AnnotationConfigApplicationContext * @see org.springframework.context.annotation.Profile */ -@Target({ElementType.TYPE}) +@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 9aae2ecf32e..22a26ebaf47 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -175,5 +175,6 @@ final class ConfigurationClass { getSimpleName(), count, methodName), new Location(getResource(), getMetadata())); } } - + + } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 238d92bb845..a75a9d14457 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -27,12 +27,13 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; import org.springframework.beans.factory.parsing.Location; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; @@ -43,23 +44,28 @@ import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.ExecutorContext; +import org.springframework.context.config.FeatureSpecification; import org.springframework.core.Conventions; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** - * Reads a given fully-populated configuration model, registering bean definitions - * with the given {@link BeanDefinitionRegistry} based on its contents. + * Reads a given fully-populated set of ConfigurationClass instances, registering bean + * definitions with the given {@link BeanDefinitionRegistry} based on its contents. * *

This class was modeled after the {@link BeanDefinitionReader} hierarchy, but does - * not implement/extend any of its artifacts as a configuration model is not a {@link Resource}. + * not implement/extend any of its artifacts as a set of configuration classes is not a + * {@link Resource}. * * @author Chris Beams * @author Juergen Hoeller @@ -85,6 +91,7 @@ class ConfigurationClassBeanDefinitionReader { private final MetadataReaderFactory metadataReaderFactory; + private ExecutorContext executorContext; /** * Create a new {@link ConfigurationClassBeanDefinitionReader} instance that will be used @@ -92,13 +99,20 @@ class ConfigurationClassBeanDefinitionReader { * @param problemReporter * @param metadataReaderFactory */ - public ConfigurationClassBeanDefinitionReader(BeanDefinitionRegistry registry, SourceExtractor sourceExtractor, - ProblemReporter problemReporter, MetadataReaderFactory metadataReaderFactory) { + public ConfigurationClassBeanDefinitionReader(final BeanDefinitionRegistry registry, SourceExtractor sourceExtractor, + ProblemReporter problemReporter, MetadataReaderFactory metadataReaderFactory, + ResourceLoader resourceLoader, Environment environment) { this.registry = registry; this.sourceExtractor = sourceExtractor; this.problemReporter = problemReporter; this.metadataReaderFactory = metadataReaderFactory; + this.executorContext = new ExecutorContext(); + this.executorContext.setRegistry(this.registry); + this.executorContext.setRegistrar(new SimpleComponentRegistrar(this.registry)); + this.executorContext.setResourceLoader(resourceLoader); + this.executorContext.setEnvironment(environment); + this.executorContext.setProblemReporter(problemReporter); } @@ -117,15 +131,38 @@ class ConfigurationClassBeanDefinitionReader { * class itself, all its {@link Bean} methods */ private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) { + AnnotationMetadata metadata = configClass.getMetadata(); + processFeatureAnnotations(metadata); doLoadBeanDefinitionForConfigurationClassIfNecessary(configClass); - for (ConfigurationClassMethod method : configClass.getMethods()) { - loadBeanDefinitionsForModelMethod(method); + for (ConfigurationClassMethod beanMethod : configClass.getMethods()) { + loadBeanDefinitionsForBeanMethod(beanMethod); } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); } + private void processFeatureAnnotations(AnnotationMetadata metadata) { + try { + for (String annotationType : metadata.getAnnotationTypes()) { + MetadataReader metadataReader = new SimpleMetadataReaderFactory().getMetadataReader(annotationType); + if (metadataReader.getAnnotationMetadata().isAnnotated(FeatureAnnotation.class.getName())) { + Map annotationAttributes = metadataReader.getAnnotationMetadata().getAnnotationAttributes(FeatureAnnotation.class.getName(), true); + // TODO SPR-7420: this is where we can catch user-defined types and avoid instantiating them for STS purposes + FeatureAnnotationParser processor = (FeatureAnnotationParser) BeanUtils.instantiateClass(Class.forName((String)annotationAttributes.get("parser"))); + FeatureSpecification spec = processor.process(metadata); + spec.execute(this.executorContext); + } + } + } catch (BeanDefinitionParsingException ex) { + throw ex; + } + catch (Exception ex) { + // TODO SPR-7420: what exception to throw? + throw new RuntimeException(ex); + } + } + /** - * Registers the {@link Configuration} class itself as a bean definition. + * Register the {@link Configuration} class itself as a bean definition. */ private void doLoadBeanDefinitionForConfigurationClassIfNecessary(ConfigurationClass configClass) { if (configClass.getBeanName() != null) { @@ -161,9 +198,9 @@ class ConfigurationClassBeanDefinitionReader { * Read a particular {@link ConfigurationClassMethod}, registering bean definitions * with the BeanDefinitionRegistry based on its contents. */ - private void loadBeanDefinitionsForModelMethod(ConfigurationClassMethod method) { - ConfigurationClass configClass = method.getConfigurationClass(); - MethodMetadata metadata = method.getMetadata(); + private void loadBeanDefinitionsForBeanMethod(ConfigurationClassMethod beanMethod) { + ConfigurationClass configClass = beanMethod.getConfigurationClass(); + MethodMetadata metadata = beanMethod.getMetadata(); RootBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass); beanDef.setResource(configClass.getResource()); @@ -176,7 +213,7 @@ class ConfigurationClassBeanDefinitionReader { // consider name and any aliases Map beanAttributes = metadata.getAnnotationAttributes(Bean.class.getName()); List names = new ArrayList(Arrays.asList((String[]) beanAttributes.get("name"))); - String beanName = (names.size() > 0 ? names.remove(0) : method.getMetadata().getMethodName()); + String beanName = (names.size() > 0 ? names.remove(0) : beanMethod.getMetadata().getMethodName()); for (String alias : names) { this.registry.registerAlias(beanName, alias); } @@ -190,7 +227,7 @@ class ConfigurationClassBeanDefinitionReader { // overriding is legal, return immediately if (logger.isDebugEnabled()) { logger.debug(String.format("Skipping loading bean definition for %s: a definition for bean " + - "'%s' already exists. This is likely due to an override in XML.", method, beanName)); + "'%s' already exists. This is likely due to an override in XML.", beanMethod, beanName)); } return; } @@ -255,7 +292,8 @@ class ConfigurationClassBeanDefinitionReader { registry.registerBeanDefinition(beanName, beanDefToRegister); } - + + private void loadBeanDefinitionsFromImportedResources(Map> importedResources) { Map, BeanDefinitionReader> readerInstanceCache = new HashMap, BeanDefinitionReader>(); for (Map.Entry> entry : importedResources.entrySet()) { @@ -357,7 +395,7 @@ class ConfigurationClassBeanDefinitionReader { @Override public boolean isFactoryMethod(Method candidate) { - return (super.isFactoryMethod(candidate) && AnnotationUtils.findAnnotation(candidate, Bean.class) != null); + return (super.isFactoryMethod(candidate) && BeanAnnotationHelper.isBeanAnnotated(candidate)); } @Override @@ -372,14 +410,12 @@ class ConfigurationClassBeanDefinitionReader { * declare at least one {@link Bean @Bean} method. */ private static class InvalidConfigurationImportProblem extends Problem { - public InvalidConfigurationImportProblem(String className, Resource resource, AnnotationMetadata metadata) { - super(String.format("%s was imported as a Configuration class but is not annotated " + - "with @Configuration nor does it declare any @Bean methods. Update the class to " + - "meet either of these requirements or do not attempt to import it.", className), + super(String.format("%s was @Import'ed as a but is not annotated with @FeatureConfiguration or " + + "@Configuration nor does it declare any @Bean methods. Update the class to " + + "meet one of these requirements or do not attempt to @Import it.", className), new Location(resource, metadata)); } - } } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index e3f0ae2938a..174ac4eff08 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -74,7 +74,7 @@ class ConfigurationClassEnhancer { // handling a @Bean-annotated method; otherwise, return index of the NoOp callback. callbackFilter = new CallbackFilter() { public int accept(Method candidateMethod) { - return (AnnotationUtils.findAnnotation(candidateMethod, Bean.class) != null ? 0 : 1); + return (BeanAnnotationHelper.isBeanAnnotated(candidateMethod) ? 0 : 1); } }; } @@ -162,19 +162,21 @@ class ConfigurationClassEnhancer { /** * Enhance a {@link Bean @Bean} method to check the supplied BeanFactory for the * existence of this bean object. + * + * @throws ProxyCreationException if an early bean reference proxy should be + * created but the return type of the bean method being intercepted is not an + * interface and thus not a candidate for JDK proxy creation. + * @throws Throwable as a catch-all for any exception that may be thrown when + * invoking the super implementation of the proxied method i.e., the actual + * {@code @Bean} method. */ - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - // by default the bean name is the name of the @Bean-annotated method - String beanName = method.getName(); + public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, + MethodProxy cglibMethodProxy) throws ProxyCreationException, Throwable { - // check to see if the user has explicitly set the bean name - Bean bean = AnnotationUtils.findAnnotation(method, Bean.class); - if (bean != null && bean.name().length > 0) { - beanName = bean.name()[0]; - } + String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); // determine whether this bean is a scoped-proxy - Scope scope = AnnotationUtils.findAnnotation(method, Scope.class); + Scope scope = AnnotationUtils.findAnnotation(beanMethod, Scope.class); if (scope != null && scope.proxyMode() != ScopedProxyMode.NO) { String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName); if (this.beanFactory.isCurrentlyInCreation(scopedBeanName)) { @@ -206,8 +208,7 @@ class ConfigurationClassEnhancer { return this.beanFactory.getBean(beanName); } - // no cached instance of the bean exists - actually create and return the bean - return proxy.invokeSuper(obj, args); + return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); } /** diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassMethod.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassMethod.java index a176db7ddad..429ba06bce0 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassMethod.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -67,7 +67,7 @@ final class ConfigurationClassMethod { } } } - + @Override public String toString() { return String.format("[%s:name=%s,declaringClass=%s]", diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index aa5681648c8..83dd95beec4 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,9 +16,15 @@ package org.springframework.context.annotation; +import static java.lang.String.format; + import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -26,10 +32,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.parsing.FailFastProblemReporter; import org.springframework.beans.factory.parsing.PassThroughSourceExtractor; import org.springframework.beans.factory.parsing.ProblemReporter; @@ -38,12 +46,24 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.config.ExecutorContext; +import org.springframework.context.config.FeatureSpecification; +import org.springframework.context.config.SourceAwareSpecification; +import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; /** * {@link BeanFactoryPostProcessor} used for bootstrapping processing of @@ -63,7 +83,7 @@ import org.springframework.util.ClassUtils; * @since 3.0 */ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, - BeanClassLoaderAware, EnvironmentAware { + ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware { /** Whether the CGLIB2 library is present on the classpath */ private static final boolean cglibAvailable = ClassUtils.isPresent( @@ -76,6 +96,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private ProblemReporter problemReporter = new FailFastProblemReporter(); + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(); @@ -88,6 +110,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private Environment environment; + private ConfigurationClassBeanDefinitionReader reader; + /** * Set the {@link SourceExtractor} to use for generated bean definitions @@ -118,6 +142,11 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo this.setMetadataReaderFactoryCalled = true; } + public void setResourceLoader(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + } + public void setBeanClassLoader(ClassLoader beanClassLoader) { this.beanClassLoader = beanClassLoader; if (!this.setMetadataReaderFactoryCalled) { @@ -126,6 +155,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo } public void setEnvironment(Environment environment) { + Assert.notNull(environment, "Environment must not be null"); this.environment = environment; } @@ -143,7 +173,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo "postProcessBeanFactory already called for this post-processor"); } this.postProcessBeanDefinitionRegistryCalled = true; - processConfigBeanDefinitions(registry); + processConfigurationClasses(registry); } /** @@ -158,18 +188,196 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo this.postProcessBeanFactoryCalled = true; if (!this.postProcessBeanDefinitionRegistryCalled) { // BeanDefinitionRegistryPostProcessor hook apparently not supported... - // Simply call processConfigBeanDefinitions lazily at this point then. - processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory); + // Simply call processConfigurationClasses lazily at this point then. + processConfigurationClasses((BeanDefinitionRegistry)beanFactory); } - enhanceConfigurationClasses(beanFactory); } + /** + * Find and process all @Configuration classes with @Feature methods in the given registry. + */ + private void processConfigurationClasses(BeanDefinitionRegistry registry) { + ConfigurationClassBeanDefinitionReader reader = getConfigurationClassBeanDefinitionReader(registry); + processConfigBeanDefinitions(registry, reader); + enhanceConfigurationClasses((ConfigurableListableBeanFactory)registry); + processFeatureConfigurationClasses((ConfigurableListableBeanFactory) registry); + } + + /** + * Process any @FeatureConfiguration classes + */ + private void processFeatureConfigurationClasses(final ConfigurableListableBeanFactory beanFactory) { + Map featureConfigBeans = retrieveFeatureConfigurationBeans(beanFactory); + + if (featureConfigBeans.size() == 0) { + return; + } + + for (final Object featureConfigBean : featureConfigBeans.values()) { + checkForBeanMethods(featureConfigBean.getClass()); + } + + if (!cglibAvailable) { + throw new IllegalStateException("CGLIB is required to process @FeatureConfiguration classes. " + + "Either add CGLIB to the classpath or remove the following @FeatureConfiguration bean definitions: " + + featureConfigBeans.keySet()); + } + + final EarlyBeanReferenceProxyCreator proxyCreator = new EarlyBeanReferenceProxyCreator(beanFactory); + final ExecutorContext executorContext = createExecutorContext(beanFactory); + + for (final Object featureConfigBean : featureConfigBeans.values()) { + ReflectionUtils.doWithMethods(featureConfigBean.getClass(), + new ReflectionUtils.MethodCallback() { + public void doWith(Method featureMethod) throws IllegalArgumentException, IllegalAccessException { + processFeatureMethod(featureMethod, featureConfigBean, executorContext, proxyCreator); + } }, + new ReflectionUtils.MethodFilter() { + public boolean matches(Method candidateMethod) { + return candidateMethod.isAnnotationPresent(Feature.class); + } }); + } + } + + /** + * Alternative to {@link ListableBeanFactory#getBeansWithAnnotation(Class)} that avoids + * instantiating FactoryBean objects. FeatureConfiguration types cannot be registered as + * FactoryBeans, so ignoring them won't cause a problem. On the other hand, using gBWA() + * at this early phase of the container would cause all @Bean methods to be invoked, as they + * are ultimately FactoryBeans underneath. + */ + private Map retrieveFeatureConfigurationBeans(ConfigurableListableBeanFactory beanFactory) { + Map fcBeans = new HashMap(); + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName); + if (isFeatureConfiguration(beanDef)) { + fcBeans.put(beanName, beanFactory.getBean(beanName)); + } + } + return fcBeans; + } + + private boolean isFeatureConfiguration(BeanDefinition candidate) { + if (!(candidate instanceof AbstractBeanDefinition) || (candidate.getBeanClassName() == null)) { + return false; + } + AbstractBeanDefinition beanDef = (AbstractBeanDefinition) candidate; + if (beanDef.hasBeanClass()) { + Class beanClass = beanDef.getBeanClass(); + if (AnnotationUtils.findAnnotation(beanClass, FeatureConfiguration.class) != null) { + return true; + } + } + else { + // in the case of @FeatureConfiguration classes included with @Import the bean class name + // will still be in String form. Since we don't know whether the current bean definition + // is a @FeatureConfiguration or not, carefully check for the annotation using ASM instead + // eager classloading. + String className = null; + try { + className = beanDef.getBeanClassName(); + MetadataReader metadataReader = new SimpleMetadataReaderFactory().getMetadataReader(className); + AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata(); + if (annotationMetadata.isAnnotated(FeatureConfiguration.class.getName())) { + return true; + } + } + catch (IOException ex) { + throw new IllegalStateException("Could not create MetadataReader for class " + className, ex); + } + } + return false; + } + + private void checkForBeanMethods(final Class featureConfigClass) { + ReflectionUtils.doWithMethods(featureConfigClass, + new ReflectionUtils.MethodCallback() { + public void doWith(Method beanMethod) throws IllegalArgumentException, IllegalAccessException { + throw new FeatureMethodExecutionException( + format("@FeatureConfiguration classes must not contain @Bean-annotated methods. " + + "%s.%s() is annotated with @Bean and must be removed in order to proceed. " + + "Consider moving this method into a dedicated @Configuration class and " + + "injecting the bean as a parameter into any @Feature method(s) that need it.", + beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName())); + } }, + new ReflectionUtils.MethodFilter() { + public boolean matches(Method candidateMethod) { + return BeanAnnotationHelper.isBeanAnnotated(candidateMethod); + } }); + } + + /** + * TODO SPR-7420: this method invokes user-supplied code, which is not going to fly for STS + * + * consider introducing some kind of check to see if we're in a tooling context and make guesses + * based on return type rather than actually invoking the method and processing the the specification + * object that returns. + * @param beanFactory + * @throws SecurityException + */ + private void processFeatureMethod(final Method featureMethod, Object configInstance, + ExecutorContext executorContext, EarlyBeanReferenceProxyCreator proxyCreator) { + try { + // get the return type + if (!(FeatureSpecification.class.isAssignableFrom(featureMethod.getReturnType()))) { + // TODO SPR-7420: raise a Problem instead? + throw new IllegalArgumentException( + "return type from @Feature methods must be assignable to FeatureSpecification"); + } + + List beanArgs = new ArrayList(); + Class[] parameterTypes = featureMethod.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + MethodParameter mp = new MethodParameter(featureMethod, i); + DependencyDescriptor dd = new DependencyDescriptor(mp, true, false); + Object proxiedBean = proxyCreator.createProxy(dd); + beanArgs.add(proxiedBean); + } + + // reflectively invoke that method + FeatureSpecification spec; + featureMethod.setAccessible(true); + spec = (FeatureSpecification) featureMethod.invoke(configInstance, beanArgs.toArray(new Object[beanArgs.size()])); + + Assert.notNull(spec, + format("The specification returned from @Feature method %s.%s() must not be null", + featureMethod.getDeclaringClass().getSimpleName(), featureMethod.getName())); + + if (spec instanceof SourceAwareSpecification) { + ((SourceAwareSpecification)spec).source(featureMethod); + ((SourceAwareSpecification)spec).sourceName(featureMethod.getName()); + } + spec.execute(executorContext); + } catch (Exception ex) { + throw new FeatureMethodExecutionException(ex); + } + } + + private ExecutorContext createExecutorContext(ConfigurableListableBeanFactory beanFactory) { + final BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setEnvironment(this.environment); + executorContext.setResourceLoader(this.resourceLoader); + executorContext.setRegistry(registry); + executorContext.setRegistrar(new SimpleComponentRegistrar(registry)); + // TODO SPR-7420: how to get hold of the current problem reporter here? + executorContext.setProblemReporter(new FailFastProblemReporter()); + return executorContext; + } + + private ConfigurationClassBeanDefinitionReader getConfigurationClassBeanDefinitionReader(BeanDefinitionRegistry registry) { + if (this.reader == null) { + this.reader = new ConfigurationClassBeanDefinitionReader( + registry, this.sourceExtractor, this.problemReporter, this.metadataReaderFactory, this.resourceLoader, this.environment); + } + return this.reader; + } /** * Build and validate a configuration model based on the registry of * {@link Configuration} classes. */ - public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { + public void processConfigBeanDefinitions(BeanDefinitionRegistry registry, ConfigurationClassBeanDefinitionReader reader) { Set configCandidates = new LinkedHashSet(); for (String beanName : registry.getBeanDefinitionNames()) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); @@ -202,8 +410,6 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo parser.validate(); // Read the model and create bean definitions based on its content - ConfigurationClassBeanDefinitionReader reader = new ConfigurationClassBeanDefinitionReader( - registry, this.sourceExtractor, this.problemReporter, this.metadataReaderFactory); reader.loadBeanDefinitions(parser.getConfigurationClasses()); } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxy.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxy.java new file mode 100644 index 00000000000..c0cd0965496 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxy.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +/** + * Marker interface indicating that an object is a proxy for a bean referenced + * from within a {@link Feature @Feature} method. + * + * @author Chris Beams + * @since 3.1 + */ +public interface EarlyBeanReferenceProxy { + + Object dereferenceTargetBean(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreator.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreator.java new file mode 100644 index 00000000000..121be542bd8 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreator.java @@ -0,0 +1,256 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.CallbackFilter; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Creates proxies for beans referenced from within @Feature methods. + * + * TODO SPR-7420: document + * - discuss why proxies are important (avoiding side effects of early instantiation) + * - discuss benefits of interface-based proxies over concrete proxies + * - make it clear that both of the above are possible + * - discuss invocation of @Bean methods and how they too return proxies. + * this 'proxy returning a proxy' approach can be confusing at first, but the + * implementation should help in making it clear. + * + * @author Chris Beams + * @since 3.1 + */ +class EarlyBeanReferenceProxyCreator { + + static final String FINAL_CLASS_ERROR_MESSAGE = + "Cannot create subclass proxy for bean type %s because it is a final class. " + + "Make the class non-final or inject the bean by interface rather than by concrete class."; + + static final String MISSING_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE = + "Cannot create subclass proxy for bean type %s because it does not have a no-arg constructor. " + + "Add a no-arg constructor or attempt to inject the bean by interface rather than by concrete class."; + + static final String PRIVATE_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE = + "Cannot create subclass proxy for bean type %s because its no-arg constructor is private. " + + "Increase the visibility of the no-arg constructor or attempt to inject the bean by interface rather " + + "than by concrete class."; + + private final AutowireCapableBeanFactory beanFactory; + + + /** + * Create a new proxy creator that will dereference proxy target beans against + * the given bean factory. + */ + public EarlyBeanReferenceProxyCreator(AutowireCapableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + /** + * Create a proxy that will ultimately dereference its target object using + * the given dependency descriptor. + */ + public Object createProxy(DependencyDescriptor descriptor) { + return doCreateProxy(new ResolveDependencyTargetBeanDereferencingInterceptor(descriptor)); + } + + /** + * Create a proxy that looks up target beans using the given dereferencing interceptor. + * + * @see EarlyBeanReferenceProxy#dereferenceTargetBean() + */ + private Object doCreateProxy(TargetBeanDereferencingInterceptor targetBeanDereferencingInterceptor) { + Enhancer enhancer = new Enhancer(); + Class targetBeanType = targetBeanDereferencingInterceptor.getTargetBeanType(); + if (targetBeanType.isInterface()) { + enhancer.setSuperclass(Object.class); + enhancer.setInterfaces(new Class[] {targetBeanType, EarlyBeanReferenceProxy.class}); + } else { + assertClassIsProxyCapable(targetBeanType); + enhancer.setSuperclass(targetBeanType); + enhancer.setInterfaces(new Class[] {EarlyBeanReferenceProxy.class}); + } + enhancer.setCallbacks(new Callback[] { + new BeanMethodInterceptor(), + new ObjectMethodsInterceptor(), + targetBeanDereferencingInterceptor, + new TargetBeanDelegatingMethodInterceptor() + }); + enhancer.setCallbackFilter(new CallbackFilter() { + public int accept(Method method) { + if (BeanAnnotationHelper.isBeanAnnotated(method)) { + return 0; + } + if (ReflectionUtils.isObjectMethod(method)) { + return 1; + } + if (method.getName().equals("dereferenceTargetBean")) { + return 2; + } + return 3; + } + }); + return enhancer.create(); + } + + /** + * Return whether the given class is capable of being subclass proxied by CGLIB. + */ + private static void assertClassIsProxyCapable(Class clazz) { + Assert.isTrue(!clazz.isInterface(), "class parameter must be a concrete type"); + if ((clazz.getModifiers() & Modifier.FINAL) != 0) { + throw new ProxyCreationException(String.format(FINAL_CLASS_ERROR_MESSAGE, clazz.getName())); + } + try { + // attempt to retrieve the no-arg constructor for the class + Constructor noArgCtor = clazz.getDeclaredConstructor(); + if ((noArgCtor.getModifiers() & Modifier.PRIVATE) != 0) { + throw new ProxyCreationException(String.format(PRIVATE_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE, clazz.getName())); + } + } catch (NoSuchMethodException ex) { + throw new ProxyCreationException(String.format(MISSING_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE, clazz.getName())); + } + } + + + /** + * Interceptor for @Bean-annotated methods called from early-proxied bean instances, such as + * @Configuration class instances. Invoking instance methods on early-proxied beans usually + * causes an eager bean lookup, but in the case of @Bean methods, it is important to return + * a proxy. + */ + private class BeanMethodInterceptor implements MethodInterceptor { + + public Object intercept(Object obj, final Method beanMethod, Object[] args, MethodProxy proxy) throws Throwable { + return doCreateProxy(new ByNameLookupTargetBeanDereferencingInterceptor(beanMethod)); + } + + } + + + /** + * Interceptor for methods declared by java.lang.Object() + */ + private static class ObjectMethodsInterceptor implements MethodInterceptor { + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + if (method.getName().equals("toString")) { + return String.format("EarlyBeanReferenceProxy for bean of type %s", + obj.getClass().getSuperclass().getSimpleName()); + } + if (method.getName().equals("hashCode")) { + return System.identityHashCode(obj); + } + if (method.getName().equals("equals")) { + return obj == args[0]; + } + if (method.getName().equals("finalize")) { + return null; + } + return proxy.invokeSuper(obj, args); + } + + } + + + /** + * Strategy interface allowing for various approaches to dereferencing (i.e. 'looking up') + * the target bean for an early bean reference proxy. + * + * @see EarlyBeanReferenceProxy#dereferenceTargetBean() + */ + private interface TargetBeanDereferencingInterceptor extends MethodInterceptor { + Class getTargetBeanType(); + } + + + /** + * Interceptor that dereferences the target bean for the proxy by calling + * {@link AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String)}. + * + * @see EarlyBeanReferenceProxy#dereferenceTargetBean() + */ + private class ResolveDependencyTargetBeanDereferencingInterceptor implements TargetBeanDereferencingInterceptor { + + private final DependencyDescriptor descriptor; + + public ResolveDependencyTargetBeanDereferencingInterceptor(DependencyDescriptor descriptor) { + this.descriptor = descriptor; + } + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return beanFactory.resolveDependency(descriptor, null); + } + + public Class getTargetBeanType() { + return this.descriptor.getDependencyType(); + } + + } + + + /** + * Interceptor that dereferences the target bean for the proxy by calling BeanFactory#getBean(String). + * + * @see EarlyBeanReferenceProxy#dereferenceTargetBean() + */ + private class ByNameLookupTargetBeanDereferencingInterceptor implements TargetBeanDereferencingInterceptor { + + private final Method beanMethod; + + public ByNameLookupTargetBeanDereferencingInterceptor(Method beanMethod) { + this.beanMethod = beanMethod; + } + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return beanFactory.getBean(BeanAnnotationHelper.determineBeanNameFor(beanMethod)); + } + + public Class getTargetBeanType() { + return beanMethod.getReturnType(); + } + + } + + + /** + * Interceptor that dereferences the target bean for the proxy and delegates the + * current method call to it. + * @see TargetBeanDereferencingInterceptor + */ + private static class TargetBeanDelegatingMethodInterceptor implements MethodInterceptor { + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + Object targetBean = ((EarlyBeanReferenceProxy)obj).dereferenceTargetBean(); + return method.invoke(targetBean, args); + } + + } + +} + diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Feature.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Feature.java new file mode 100644 index 00000000000..dc54a0e94d7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Feature.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Feature { + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotation.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotation.java new file mode 100644 index 00000000000..20bb2802ba0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotation.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Meta-annotation indicating that an annotation should be processed + * to produce a {@code FeatureSpecification}. + * + *

See {@link ComponentScan @ComponentScan} for an implementation example. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScan + * @see org.springframework.context.config.FeatureSpecification + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface FeatureAnnotation { + + /** + * Indicate the class that should be used to parse this annotation + * into a {@code FeatureSpecification}. + */ + Class parser(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotationParser.java new file mode 100644 index 00000000000..65a402de417 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotationParser.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import org.springframework.context.config.FeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Interface for parsing {@link AnnotationMetadata} from a {@link FeatureAnnotation} + * into a {@link FeatureSpecification} object. Used in conjunction with a + * {@link FeatureSpecificationExecutor} to provide a source-agnostic approach to + * handling configuration metadata. + * + *

For example, Spring's component-scanning can be configured via XML using + * the {@code context:component-scan} element or via the {@link ComponentScan} + * annotation. In either case, the metadata is the same -- only the source + * format differs. {@link ComponentScanBeanDefinitionParser} is used to create + * a specification from the {@code } XML element, while + * {@link ComponentScanAnnotationParser} creates a specification from the + * the annotation style. They both produce a {@link ComponentScanSpec} + * object that is ultimately delegated to a {@link ComponentScanExecutor} + * which understands how to configure a {@link ClassPathBeanDefinitionScanner}, + * perform actual scanning, and register actual bean definitions against the + * container. + * + *

Implementations must be instantiable via a no-arg constructor. + * + * TODO SPR-7420: documentation (clean up) + * TODO SPR-7420: rework so annotations declare their creator. + * + * + * @author Chris Beams + * @since 3.1 + * @see FeatureAnnotation#parser() + * @see FeatureSpecification + * @see FeatureSpecificationExecutor + */ +public interface FeatureAnnotationParser { + + /** + * Parse the given annotation metadata and populate a {@link FeatureSpecification} + * object suitable for execution by a {@link FeatureSpecificationExecutor}. + */ + FeatureSpecification process(AnnotationMetadata metadata); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureConfiguration.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureConfiguration.java new file mode 100644 index 00000000000..6fbfbf2b98b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + * @see Configuration + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface FeatureConfiguration { + + /** + * Explicitly specify the name of the Spring bean definition associated + * with this FeatureConfiguration class. If left unspecified (the common case), + * a bean name will be automatically generated. + * + *

The custom name applies only if the FeatureConfiguration class is picked up via + * component scanning or supplied directly to a {@link AnnotationConfigApplicationContext}. + * If the FeatureConfiguration class is registered as a traditional XML bean definition, + * the name/id of the bean element will take precedence. + * + * @return the specified bean name, if any + * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator + */ + String value() default ""; +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureMethodExecutionException.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureMethodExecutionException.java new file mode 100644 index 00000000000..e5aa6f54365 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureMethodExecutionException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +@SuppressWarnings("serial") +class FeatureMethodExecutionException extends RuntimeException { + public FeatureMethodExecutionException(Throwable cause) { + super(cause); + } + + public FeatureMethodExecutionException(String message) { + super(message); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ProxyCreationException.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ProxyCreationException.java new file mode 100644 index 00000000000..f85f311f83e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ProxyCreationException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +@SuppressWarnings("serial") +class ProxyCreationException extends RuntimeException { + + public ProxyCreationException(String message) { + super(message); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/SimpleComponentRegistrar.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/SimpleComponentRegistrar.java new file mode 100644 index 00000000000..4c5e47e0c6f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/SimpleComponentRegistrar.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; + +public class SimpleComponentRegistrar implements ComponentRegistrar { + + private final BeanDefinitionRegistry registry; + + public SimpleComponentRegistrar(BeanDefinitionRegistry registry) { + this.registry = registry; + } + + public String registerWithGeneratedName(BeanDefinition beanDefinition) { + return BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, this.registry); + } + + public void registerBeanComponent(BeanComponentDefinition component) { + BeanDefinitionReaderUtils.registerBeanDefinition(component, this.registry); + registerComponent(component); + } + + public void registerComponent(ComponentDefinition component) { + // no-op + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/AbstractFeatureSpecification.java b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractFeatureSpecification.java new file mode 100644 index 00000000000..ac0a8a80f64 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractFeatureSpecification.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2011 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.context.config; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.parsing.SimpleProblemCollector; + + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public abstract class AbstractFeatureSpecification implements SourceAwareSpecification { + + private static final Object DUMMY_SOURCE = new Object(); + private static final String DUMMY_SOURCE_NAME = "dummySource"; + + protected Class executorType; + + private Object source = DUMMY_SOURCE; + private String sourceName = DUMMY_SOURCE_NAME; + + protected AbstractFeatureSpecification(Class executorType) { + this.executorType = executorType; + } + + public final boolean validate(ProblemReporter problemReporter) { + SimpleProblemCollector collector = new SimpleProblemCollector(this.source()); + this.doValidate(collector); + collector.reportProblems(problemReporter); + return collector.hasErrors() ? false : true; + } + + protected abstract void doValidate(SimpleProblemCollector reporter); + + public AbstractFeatureSpecification source(Object source) { + this.source = source; + return this; + } + + public Object source() { + return this.source; + } + + public AbstractFeatureSpecification sourceName(String sourceName) { + this.sourceName = sourceName; + return this; + } + + public String sourceName() { + return this.sourceName; + } + + public void execute(ExecutorContext executorContext) { + FeatureSpecificationExecutor executor = + BeanUtils.instantiateClass(this.executorType, FeatureSpecificationExecutor.class); + executor.execute(this, executorContext); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/AbstractSpecificationExecutor.java b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractSpecificationExecutor.java new file mode 100644 index 00000000000..34d7bc08980 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractSpecificationExecutor.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2011 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.context.config; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.util.Assert; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public abstract class AbstractSpecificationExecutor implements FeatureSpecificationExecutor { + + /** + * {@inheritDoc} + *

This implementation {@linkplain FeatureSpecification#validate() validates} the + * given specification and delegates it to {@link #doExecute(FeatureSpecification)} + * only if valid. + */ + @SuppressWarnings("unchecked") + public final void execute(FeatureSpecification spec, ExecutorContext executorContext) { + Assert.notNull(spec, "Specification must not be null"); + Assert.notNull(spec, "ExecutorContext must not be null"); + Class typeArg = GenericTypeResolver.resolveTypeArgument(this.getClass(), AbstractSpecificationExecutor.class); + Assert.isTrue(typeArg.equals(spec.getClass()), "Specification cannot be executed by this executor"); + if (spec.validate(executorContext.getProblemReporter())) { + doExecute((S)spec, executorContext); + } + } + + /** + * Execute the given specification, usually resulting in registration of bean definitions + * against a bean factory. + * @param specification the {@linkplain FeatureSpecification#validate() validated} specification + */ + protected abstract void doExecute(S specification, ExecutorContext executorContext); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/AdviceMode.java b/org.springframework.context/src/main/java/org/springframework/context/config/AdviceMode.java new file mode 100644 index 00000000000..57d9516a68a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/AdviceMode.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2011 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.context.config; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public enum AdviceMode { + PROXY, + ASPECTJ +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/ExecutorContext.java b/org.springframework.context/src/main/java/org/springframework/context/config/ExecutorContext.java new file mode 100644 index 00000000000..7c6a3a26dd7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/ExecutorContext.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2011 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.context.config; + +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +/** + * TODO: rename to SpecificationContext? + * + * @author Chris Beams + * @since 3.1 + */ +public class ExecutorContext { + + private BeanDefinitionRegistry registry; + private ComponentRegistrar registrar; + private ResourceLoader resourceLoader; + private Environment environment; + private ProblemReporter problemReporter; + + public ExecutorContext() { } + + public void setRegistry(BeanDefinitionRegistry registry) { + this.registry = registry; + } + + public BeanDefinitionRegistry getRegistry() { + return this.registry; + } + + public void setRegistrar(ComponentRegistrar registrar) { + this.registrar = registrar; + } + + public ComponentRegistrar getRegistrar() { + return this.registrar; + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + public Environment getEnvironment() { + return this.environment; + } + + public void setProblemReporter(ProblemReporter problemReporter) { + this.problemReporter = problemReporter; + } + + public ProblemReporter getProblemReporter() { + return this.problemReporter; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecification.java b/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecification.java new file mode 100644 index 00000000000..7cca49ee45a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecification.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2011 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.context.config; + +import org.springframework.beans.factory.parsing.ProblemReporter; + +/** + * Interface to be implemented by objects that specify the configuration of a particular feature + * of the Spring container e.g., component-scanning, JMX MBean exporting, AspectJ auto-proxying, + * annotation-driven transaction management, and so on. + * + *

Many features of the Spring container can be configured using either XML or annotations. + * As one example, Spring's component scanning feature may be configured using + * either the {@code } XML element or (as of Spring 3.1) the + * {@code @ComponentScan} annotation. These two options are equivalent to one another, and users may + * choose between them as a matter of convention or preference. Fundamentally, both are declarative + * mechanisms for specifying how the Spring container should be configured. A {@code + * FeatureSpecification} object, then, is a way of representing this configuration information independent + * of its original source format be it XML, annotations, or otherwise. + * + *

A {@code FeatureSpecification} is responsible for {@linkplain #validate validating itself}. + * For example, a component-scanning specification would check that at least one base package has + * been specified, and otherwise register a {@code Problem} with a {@link ProblemReporter}. Taking + * this approach as opposed to throwing exceptions allows for maximum tooling and error reporting + * flexibility. + * + *

A {@link FeatureSpecificationExecutor} is used to carry out the instructions within a populated + * {@code FeatureSpecification}; this is where the "real work" happens. In the case of component scanning + * as above, it is within a {@code FeatureSpecificationExecutor} that a bean definition scanner is created, + * configured and invoked against the base packages specified. + * + *

{@code FeatureSpecification} objects may be populated and executed by Spring XML namespace element + * parsers on order to separate the concerns of XML parsing from Spring bean definition creation and + * registration. This type of use is mostly a framework-internal matter. More interesting to end users is + * the use of {@code FeatureSpecification} objects within {@code @FeatureConfiguration} classes and their + * {@code @Feature} methods. This functionality is new in Spring 3.1 and is the logical evolution of Spring's + * Java-based configuration support first released in Spring 3.0 (see {@code @Configuration} classes and + * {@code @Bean} methods). The primary goal of {@code Feature}-related support is to round out this + * Java-based support and allow users to configure all aspects of the Spring-container without requiring + * the use of XML. See "design notes" below for an example of this kind of use. + * + *

Notes on designing {@code FeatureSpecification} implementations

+ * + *

The public API of a {@code FeatureSpecification} should be designed for maximum ease of use + * within {@code @Feature} methods. Traditional JavaBean-style getters and setters should be dropped + * for a more fluent style that allows for method chaining. Consider the following example of a + * {@code @Feature} method: + * + *

+ * @Feature
+ * public TxAnnotationDriven tx(PlatformTransactionManager txManager) {
+ *    return new TxAnnotationDriven(txManager).proxyTargetClass(true);
+ * }
+ * 
+ * + * Notice how the creation and configuration of the {@code TxAnnotationDriven} specification is + * concise and reads well. This is facilitated by mutator methods that always return the + * specification object's 'this' reference, allowing for method chaining. A secondary design goal + * of this approach is that the resulting code tends to mirror corresponding XML namespace + * declarations, which most Spring users are already familiar with. For example, compare the + * code above with its XML counterpart: + * + *

{@code } + * + *

Typically, a user will call only the constructor and 'mutator' methods of a specification + * object. The accessor/getter methods, however, are typically called only by the specification's + * {@linkplain FeatureSpecificationExecutor executor} for the purpose of populating and registering + * bean definitions with the Spring container.For this reason, it is recommended that accessor + * methods be given package-private visibility. This creates a better experience for users from + * an IDE content-assist point of view as they will see only the public mutator methods, reducing + * any possible confusion. + * + *

Implementations should take care to allow for use of string-based bean names, placeholder + * ("${...}") and SpEL ("#{...}) expressions wherever they may be useful. + * While it is generally desirable to refer to dependent beans in pure Java, in certain cases a + * user may wish or need to refer by bean name. For example, the {@code TxAnnotationDriven} specification + * referenced above allows for specifying its transaction-manager reference by {@code String} or by + * {@code PlatformTransactionManager} reference. Such strings should always be candidates for placeholder + * replacement and SpEL evaluation for maximum configurability as well as parity with the featureset + * available in Spring XML. With regard to SpEL expressions, users should assume that only expressions + * evaluating to a bean name will be supported. While it is technically possible with SpEL to resolve + * a bean instance, specification executors will not support such use unless explicitly indicated. + * + *

See the Javadoc for {@code @FeatureConfiguration} classes and {@code @Feature} methods for + * information on their lifecycle and semantics. + * + * @author Chris Beams + * @since 3.1 + * @see FeatureSpecificationExecutor + * @see AbstractSpecificationExecutor + * @see org.springframework.context.annotation.Feature + * @see org.springframework.context.annotation.FeatureConfiguration + */ +public interface FeatureSpecification { + + /** + * Validate this specification instance to ensure all required properties + * have been set, including checks on mutually exclusive or mutually + * dependent properties. May in some cases modify the state of the + * specification e.g., instantiating types specified as strings. + * @see AbstractSpecificationExecutor#execute(Specification) + * @return whether any problems occurred during validation + */ + boolean validate(ProblemReporter problemReporter); + + /** + * Execute this specification instance, carrying out the instructions + * specified within. Should work by delegating to an underlying + * {@link FeatureSpecificationExecutor} for proper separation of concerns. + */ + void execute(ExecutorContext executorContext); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecificationExecutor.java b/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecificationExecutor.java new file mode 100644 index 00000000000..42f1c0bdaa5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecificationExecutor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2011 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.context.config; + +/** + * Interface for executing a populated {@link FeatureSpecification}. Provides + * a generic mechanism for handling container configuration metadata regardless of + * origin in XML, annotations, or other source format. + * + * TODO SPR-7420: document (clean up) + * + * @author Chris Beams + * @since 3.1 + * @see AbstractSpecificationExecutor + * @see org.springframework.beans.factory.xml.BeanDefinitionParser + * @see org.springframework.context.annotation.FeatureAnnotationParser + * @see org.springframework.context.annotation.ComponentScanExecutor + */ +public interface FeatureSpecificationExecutor { + + /** + * Execute the given specification, usually resulting in registration + * of bean definitions against a bean factory. + */ + void execute(FeatureSpecification spec, ExecutorContext executorContext); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/SourceAwareSpecification.java b/org.springframework.context/src/main/java/org/springframework/context/config/SourceAwareSpecification.java new file mode 100644 index 00000000000..82b6ea09cdd --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/SourceAwareSpecification.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2011 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.context.config; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public interface SourceAwareSpecification extends FeatureSpecification { + + String sourceName(); + + SourceAwareSpecification sourceName(String sourceName); + + Object source(); + + SourceAwareSpecification source(Object source); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java index 4afbb314e8f..a4fb136b2b1 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -22,16 +22,30 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; /** - * A factory for a ConversionService that installs default converters appropriate for most environments. - * Set the {@link #setConverters "converters"} property to supplement or override the default converters. + * A factory providing convenient access to a ConversionService configured with + * converters appropriate for most environments. Set the {@link #setConverters + * "converters"} property to supplement the default converters. + * + *

This implementation creates a {@link DefaultConversionService}. Subclasses + * may override {@link #createConversionService()} in order to return a + * {@link GenericConversionService} instance of their choosing. + * + *

Like all {@code FactoryBean} implementations, this class is suitable for + * use when configuring a Spring application context using Spring {@code } + * XML. When configuring the container with + * {@link org.springframework.context.annotation.Configuration @Configuration} + * classes, simply instantiate, configure and return the appropriate + * {@code ConversionService} object from a {@link + * org.springframework.context.annotation.Bean @Bean} method. * * @author Keith Donald * @author Juergen Hoeller + * @author Chris Beams * @since 3.0 - * @see ConversionServiceFactory#createDefaultConversionService() */ public class ConversionServiceFactoryBean implements FactoryBean, InitializingBean { @@ -52,17 +66,17 @@ public class ConversionServiceFactoryBean implements FactoryBeanCreates a simple {@link GenericConversionService} instance by default. - * Subclasses may override to customize the ConversionService instance that gets created. + * Subclasses may override to customize the ConversionService instance that + * gets created. */ protected GenericConversionService createConversionService() { - return new GenericConversionService(); + return new DefaultConversionService(); } diff --git a/org.springframework.context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java b/org.springframework.context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java new file mode 100644 index 00000000000..47a410b7fbe --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2011 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.format.support; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar; +import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringValueResolver; + +/** + * A specialization of {@link FormattingConversionService} configured by default with + * converters and formatters appropriate for most applications. + * + *

Designed for direct instantiation but also exposes the static {@link #addDefaultFormatters} + * utility method for ad hoc use against any {@code FormatterRegistry} instance, just + * as {@code DefaultConversionService} exposes its own + * {@link DefaultConversionService#addDefaultConverters addDefaultConverters} method. + * + * @author Chris Beams + * @since 3.1 + */ +public class DefaultFormattingConversionService extends FormattingConversionService { + + private static final boolean jodaTimePresent = ClassUtils.isPresent( + "org.joda.time.LocalDate", DefaultFormattingConversionService.class.getClassLoader()); + + /** + * Create a new {@code DefaultFormattingConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters default converters} and + * {@linkplain #addDefaultFormatters default formatters}. + */ + public DefaultFormattingConversionService() { + this(null, true); + } + + /** + * Create a new {@code DefaultFormattingConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters default converters} and, + * based on the value of {@code registerDefaultFormatters}, the set of + * {@linkplain #addDefaultFormatters default formatters}. + * @param registerDefaultFormatters whether to register default formatters + */ + public DefaultFormattingConversionService(boolean registerDefaultFormatters) { + this(null, registerDefaultFormatters); + } + + /** + * Create a new {@code DefaultFormattingConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters default converters} and, + * based on the value of {@code registerDefaultFormatters}, the set of + * {@linkplain #addDefaultFormatters default formatters} + * @param embeddedValueResolver delegated to {@link #setEmbeddedValueResolver(StringValueResolver)} + * prior to calling {@link #addDefaultFormatters}. + * @param registerDefaultFormatters whether to register default formatters + */ + public DefaultFormattingConversionService(StringValueResolver embeddedValueResolver, boolean registerDefaultFormatters) { + this.setEmbeddedValueResolver(embeddedValueResolver); + DefaultConversionService.addDefaultConverters(this); + if (registerDefaultFormatters) { + addDefaultFormatters(this); + } + } + + /** + * Add formatters appropriate for most environments, including number formatters and a Joda-Time + * date formatter if Joda-Time is present on the classpath. + * @param formatterRegistry the service to register default formatters against + */ + public static void addDefaultFormatters(FormatterRegistry formatterRegistry) { + formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); + if (jodaTimePresent) { + new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry); + } else { + formatterRegistry.addFormatterForFieldAnnotation(new NoJodaDateTimeFormatAnnotationFormatterFactory()); + } + } + + + /** + * Dummy AnnotationFormatterFactory that simply fails if @DateTimeFormat is being used + * without the JodaTime library being present. + */ + private static final class NoJodaDateTimeFormatAnnotationFormatterFactory + implements AnnotationFormatterFactory { + + private final Set> fieldTypes; + + public NoJodaDateTimeFormatAnnotationFormatterFactory() { + Set> rawFieldTypes = new HashSet>(4); + rawFieldTypes.add(Date.class); + rawFieldTypes.add(Calendar.class); + rawFieldTypes.add(Long.class); + this.fieldTypes = Collections.unmodifiableSet(rawFieldTypes); + } + + public Set> getFieldTypes() { + return this.fieldTypes; + } + + public Printer getPrinter(DateTimeFormat annotation, Class fieldType) { + throw new IllegalStateException("JodaTime library not available - @DateTimeFormat not supported"); + } + + public Parser getParser(DateTimeFormat annotation, Class fieldType) { + throw new IllegalStateException("JodaTime library not available - @DateTimeFormat not supported"); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java index d1469f116a0..278d86830ad 100644 --- a/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java +++ b/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java @@ -16,10 +16,6 @@ package org.springframework.format.support; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; import java.util.Set; import org.springframework.beans.factory.FactoryBean; @@ -32,48 +28,51 @@ import org.springframework.format.FormatterRegistrar; import org.springframework.format.FormatterRegistry; import org.springframework.format.Parser; import org.springframework.format.Printer; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar; -import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; -import org.springframework.util.ClassUtils; import org.springframework.util.StringValueResolver; /** - *

A factory for a {@link FormattingConversionService} that installs default - * converters and formatters for common types such as numbers and datetimes. - * - *

Converters and formatters can be registered declaratively through + * A factory providing convenient access to a {@code FormattingConversionService} + * configured with converters and formatters for common types such as numbers and + * datetimes. + * + *

Additional converters and formatters can be registered declaratively through * {@link #setConverters(Set)} and {@link #setFormatters(Set)}. Another option - * is to register converters and formatters in code by implementing the - * {@link FormatterRegistrar} interface. You can then configure provide the set + * is to register converters and formatters in code by implementing the + * {@link FormatterRegistrar} interface. You can then configure provide the set * of registrars to use through {@link #setFormatterRegistrars(Set)}. - * - *

A good example for registering converters and formatters in code is - * JodaTimeFormatterRegistrar, which registers a number of + * + *

A good example for registering converters and formatters in code is + * JodaTimeFormatterRegistrar, which registers a number of * date-related formatters and converters. For a more detailed list of cases - * see {@link #setFormatterRegistrars(Set)} - * + * see {@link #setFormatterRegistrars(Set)} + * + *

Like all {@code FactoryBean} implementations, this class is suitable for + * use when configuring a Spring application context using Spring {@code } + * XML. When configuring the container with + * {@link org.springframework.context.annotation.Configuration @Configuration} + * classes, simply instantiate, configure and return the appropriate + * {@code FormattingConversionService} object from a + * {@link org.springframework.context.annotation.Bean @Bean} method. + * * @author Keith Donald * @author Juergen Hoeller * @author Rossen Stoyanchev + * @author Chris Beams * @since 3.0 */ public class FormattingConversionServiceFactoryBean implements FactoryBean, EmbeddedValueResolverAware, InitializingBean { - private static final boolean jodaTimePresent = ClassUtils.isPresent( - "org.joda.time.LocalDate", FormattingConversionService.class.getClassLoader()); - private Set converters; private Set formatters; private Set formatterRegistrars; - private StringValueResolver embeddedValueResolver; - private FormattingConversionService conversionService; + private StringValueResolver embeddedValueResolver; + private boolean registerDefaultFormatters = true; /** @@ -129,12 +128,12 @@ public class FormattingConversionServiceFactoryBean this.registerDefaultFormatters = registerDefaultFormatters; } + + // implementing InitializingBean + public void afterPropertiesSet() { - this.conversionService = new FormattingConversionService(); - this.conversionService.setEmbeddedValueResolver(this.embeddedValueResolver); - ConversionServiceFactory.addDefaultConverters(this.conversionService); + this.conversionService = new DefaultFormattingConversionService(this.embeddedValueResolver, this.registerDefaultFormatters); ConversionServiceFactory.registerConverters(this.converters, this.conversionService); - addDefaultFormatters(); registerFormatters(); } @@ -162,24 +161,14 @@ public class FormattingConversionServiceFactoryBean * through FormatterRegistrars. * @see #setFormatters(Set) * @see #setFormatterRegistrars(Set) + * @deprecated since Spring 3.1 in favor of {@link #setFormatterRegistrars(Set)} */ + @Deprecated protected void installFormatters(FormatterRegistry registry) { } // private helper methods - private void addDefaultFormatters() { - if (registerDefaultFormatters) { - this.conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); - if (jodaTimePresent) { - new JodaTimeFormatterRegistrar().registerFormatters(this.conversionService); - } else { - this.conversionService - .addFormatterForFieldAnnotation(new NoJodaDateTimeFormatAnnotationFormatterFactory()); - } - } - } - private void registerFormatters() { if (this.formatters != null) { for (Object formatter : this.formatters) { @@ -201,34 +190,4 @@ public class FormattingConversionServiceFactoryBean installFormatters(this.conversionService); } - /** - * Dummy AnnotationFormatterFactory that simply fails if @DateTimeFormat is being used - * without the JodaTime library being present. - */ - private static final class NoJodaDateTimeFormatAnnotationFormatterFactory - implements AnnotationFormatterFactory { - - private final Set> fieldTypes; - - public NoJodaDateTimeFormatAnnotationFormatterFactory() { - Set> rawFieldTypes = new HashSet>(4); - rawFieldTypes.add(Date.class); - rawFieldTypes.add(Calendar.class); - rawFieldTypes.add(Long.class); - this.fieldTypes = Collections.unmodifiableSet(rawFieldTypes); - } - - public Set> getFieldTypes() { - return this.fieldTypes; - } - - public Printer getPrinter(DateTimeFormat annotation, Class fieldType) { - throw new IllegalStateException("JodaTime library not available - @DateTimeFormat not supported"); - } - - public Parser getParser(DateTimeFormat annotation, Class fieldType) { - throw new IllegalStateException("JodaTime library not available - @DateTimeFormat not supported"); - } - } - } diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java index 2458de6f845..28c6bc6a62e 100644 --- a/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java +++ b/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2011 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. diff --git a/org.springframework.context/src/main/resources/org/springframework/context/config/spring-context-3.1.xsd b/org.springframework.context/src/main/resources/org/springframework/context/config/spring-context-3.1.xsd index ad93b101270..59903b4e05a 100644 --- a/org.springframework.context/src/main/resources/org/springframework/context/config/spring-context-3.1.xsd +++ b/org.springframework.context/src/main/resources/org/springframework/context/config/spring-context-3.1.xsd @@ -191,6 +191,7 @@ diff --git a/org.springframework.context/src/test/java/example/scannable/FooServiceImpl.java b/org.springframework.context/src/test/java/example/scannable/FooServiceImpl.java index d31815f1eb2..ef2b6094b31 100644 --- a/org.springframework.context/src/test/java/example/scannable/FooServiceImpl.java +++ b/org.springframework.context/src/test/java/example/scannable/FooServiceImpl.java @@ -65,6 +65,7 @@ public class FooServiceImpl implements FooService { private boolean initCalled = false; + @SuppressWarnings("unused") @PostConstruct private void init() { if (this.initCalled) { diff --git a/org.springframework.context/src/test/java/example/scannable/_package.java b/org.springframework.context/src/test/java/example/scannable/_package.java new file mode 100644 index 00000000000..4be3b9447bc --- /dev/null +++ b/org.springframework.context/src/test/java/example/scannable/_package.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2010 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 example.scannable; + + +/** + * Marker class for example.scannable package. + * + * @see org.springframework.context.annotation.ComponentScan#basePackageClasses() + */ +public class _package { } diff --git a/org.springframework.context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java b/org.springframework.context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java new file mode 100644 index 00000000000..b34d50318f8 --- /dev/null +++ b/org.springframework.context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2011 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 example.scannable_scoped; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.stereotype.Component; + +@Component +@MyScope(BeanDefinition.SCOPE_PROTOTYPE) +public class CustomScopeAnnotationBean { +} diff --git a/org.springframework.context/src/test/java/example/scannable_scoped/MyScope.java b/org.springframework.context/src/test/java/example/scannable_scoped/MyScope.java new file mode 100644 index 00000000000..512740b90b4 --- /dev/null +++ b/org.springframework.context/src/test/java/example/scannable_scoped/MyScope.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2011 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 example.scannable_scoped; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ScopedProxyMode; + +public @interface MyScope { + String value() default BeanDefinition.SCOPE_SINGLETON; + ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT; +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/BeanFactoryAwareFeatureConfigurationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/BeanFactoryAwareFeatureConfigurationTests.java new file mode 100644 index 00000000000..42f1c9b2a27 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/BeanFactoryAwareFeatureConfigurationTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +/** + * Tests that @FeatureConfiguration classes may implement Aware interfaces, + * such as BeanFactoryAware. This is not generally recommended but occasionally + * useful, particularly in testing. + * + * @author Chris Beams + * @since 3.1 + */ +public class BeanFactoryAwareFeatureConfigurationTests { + @Test + public void test() { + ConfigurableApplicationContext ctx = + new AnnotationConfigApplicationContext(FeatureConfig.class); + FeatureConfig fc = ctx.getBean(FeatureConfig.class); + assertThat(fc.featureMethodWasCalled, is(true)); + assertThat(fc.gotBeanFactoryInTime, is(true)); + assertThat(fc.beanFactory, is(ctx.getBeanFactory())); + } + + @FeatureConfiguration + static class FeatureConfig implements BeanFactoryAware { + + ConfigurableListableBeanFactory beanFactory; + boolean featureMethodWasCalled = false; + boolean gotBeanFactoryInTime = false; + + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (ConfigurableListableBeanFactory)beanFactory; + } + + @Feature + public FeatureSpecification f() { + this.featureMethodWasCalled = true; + this.gotBeanFactoryInTime = (this.beanFactory != null); + return new StubSpecification(); + } + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java new file mode 100644 index 00000000000..a418258fe2d --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; + +import java.io.IOException; +import java.util.HashSet; + +import org.junit.Test; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.CustomAutowireConfigurer; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.SimpleMapScope; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.ComponentScanParserTests.CustomAnnotationAutowiredBean; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.SerializationTestUtils; + +import example.scannable.FooService; +import example.scannable.ScopedProxyTestBean; +import example.scannable_scoped.CustomScopeAnnotationBean; +import example.scannable_scoped.MyScope; + +/** + * Integration tests for processing ComponentScan-annotated Configuration + * classes. + * + * @author Chris Beams + * @since 3.1 + */ +public class ComponentScanAnnotationIntegrationTests { + @Test + public void controlScan() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan(example.scannable._package.class.getPackage().getName()); + ctx.refresh(); + assertThat("control scan for example.scannable package failed to register FooServiceImpl bean", + ctx.containsBean("fooServiceImpl"), is(true)); + } + + @Test + public void viaContextRegistration() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanAnnotatedConfig.class); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfig.class); + ctx.getBean(TestBean.class); + assertThat("config class bean not found", ctx.containsBeanDefinition("componentScanAnnotatedConfig"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected", + ctx.containsBean("fooServiceImpl"), is(true)); + } + + @Test + public void viaContextRegistration_WithValueAttribute() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanAnnotatedConfig_WithValueAttribute.class); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfig_WithValueAttribute.class); + ctx.getBean(TestBean.class); + assertThat("config class bean not found", ctx.containsBeanDefinition("componentScanAnnotatedConfig_WithValueAttribute"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected", + ctx.containsBean("fooServiceImpl"), is(true)); + } + + @Test + public void viaBeanRegistration() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("componentScanAnnotatedConfig", + genericBeanDefinition(ComponentScanAnnotatedConfig.class).getBeanDefinition()); + bf.registerBeanDefinition("configurationClassPostProcessor", + genericBeanDefinition(ConfigurationClassPostProcessor.class).getBeanDefinition()); + GenericApplicationContext ctx = new GenericApplicationContext(bf); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfig.class); + ctx.getBean(TestBean.class); + assertThat("config class bean not found", ctx.containsBeanDefinition("componentScanAnnotatedConfig"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered " + + "as bean definition did not trigger component scanning as expected", + ctx.containsBean("fooServiceImpl"), is(true)); + } + + @Test + public void invalidComponentScanDeclaration_noPackagesSpecified() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithNoPackagesConfig.class); + try { + ctx.refresh(); + fail("Expected exception when parsing @ComponentScan definition that declares no packages"); + } catch (BeanDefinitionParsingException ex) { + assertThat(ex.getMessage(), containsString("At least one base package must be specified")); + } + } + + @Test + public void withCustomBeanNameGenerator() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithBeanNameGenenerator.class); + ctx.refresh(); + assertThat(ctx.containsBean("custom_fooServiceImpl"), is(true)); + assertThat(ctx.containsBean("fooServiceImpl"), is(false)); + } + + @Test + public void withScopeResolver() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithScopeResolver.class); + // custom scope annotation makes the bean prototype scoped. subsequent calls + // to getBean should return distinct instances. + assertThat(ctx.getBean(CustomScopeAnnotationBean.class), not(sameInstance(ctx.getBean(CustomScopeAnnotationBean.class)))); + } + + @Test + public void withCustomTypeFilter() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithCustomTypeFilter.class); + CustomAnnotationAutowiredBean testBean = ctx.getBean(CustomAnnotationAutowiredBean.class); + assertThat(testBean.getDependency(), notNullValue()); + } + + @Test + public void withScopedProxy() throws IOException, ClassNotFoundException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithScopedProxy.class); + ctx.getBeanFactory().registerScope("myScope", new SimpleMapScope()); + ctx.refresh(); + // should cast to the interface + FooService bean = (FooService) ctx.getBean("scopedProxyTestBean"); + // should be dynamic proxy + assertThat(AopUtils.isJdkDynamicProxy(bean), is(true)); + // test serializability + assertThat(bean.foo(1), equalTo("bar")); + FooService deserialized = (FooService) SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(deserialized, notNullValue()); + assertThat(deserialized.foo(1), equalTo("bar")); + } + + @Test + public void withBasePackagesAndValueAlias() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithBasePackagesAndValueAlias.class); + ctx.refresh(); + assertThat(ctx.containsBean("fooServiceImpl"), is(true)); + } + +} + + +@Configuration +@ComponentScan(basePackageClasses=example.scannable._package.class) +class ComponentScanAnnotatedConfig { + @Bean + public TestBean testBean() { + return new TestBean(); + } +} + +@Configuration +@ComponentScan("example.scannable") +class ComponentScanAnnotatedConfig_WithValueAttribute { + @Bean + public TestBean testBean() { + return new TestBean(); + } +} + +@Configuration +@ComponentScan +class ComponentScanWithNoPackagesConfig { } + +@Configuration +@ComponentScan(basePackages="example.scannable", nameGenerator=MyBeanNameGenerator.class) +class ComponentScanWithBeanNameGenenerator { } + +class MyBeanNameGenerator extends AnnotationBeanNameGenerator { + @Override + public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { + return "custom_" + super.generateBeanName(definition, registry); + } +} + +@Configuration +@ComponentScan(basePackages="example.scannable_scoped", scopeResolver=MyScopeMetadataResolver.class) +class ComponentScanWithScopeResolver { } + +class MyScopeMetadataResolver extends AnnotationScopeMetadataResolver { + MyScopeMetadataResolver() { + this.scopeAnnotationType = MyScope.class; + } +} + +@Configuration +@ComponentScan(basePackages="org.springframework.context.annotation", + useDefaultFilters=false, + includeFilters=@Filter(type=FilterType.CUSTOM, value=ComponentScanParserTests.CustomTypeFilter.class), + // exclude this class from scanning since it's in the scanned package + excludeFilters=@Filter(type=FilterType.ASSIGNABLE_TYPE, value=ComponentScanWithCustomTypeFilter.class)) +class ComponentScanWithCustomTypeFilter { + @Bean + @SuppressWarnings({ "rawtypes", "serial", "unchecked" }) + public CustomAutowireConfigurer customAutowireConfigurer() { + CustomAutowireConfigurer cac = new CustomAutowireConfigurer(); + cac.setCustomQualifierTypes(new HashSet() {{ add(ComponentScanParserTests.CustomAnnotation.class); }}); + return cac; + } + + public ComponentScanParserTests.CustomAnnotationAutowiredBean testBean() { + return new ComponentScanParserTests.CustomAnnotationAutowiredBean(); + } +} + +@Configuration +@ComponentScan(basePackages="example.scannable", + scopedProxy=ScopedProxyMode.INTERFACES, + useDefaultFilters=false, + includeFilters=@Filter(type=FilterType.ASSIGNABLE_TYPE, value=ScopedProxyTestBean.class)) +class ComponentScanWithScopedProxy { } + +@Configuration +@ComponentScan( + value="example.scannable", + basePackages="example.scannable", + basePackageClasses=example.scannable._package.class) +class ComponentScanWithBasePackagesAndValueAlias { } diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java index 39f8dffc112..56045f669b6 100644 --- a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java @@ -18,8 +18,7 @@ package org.springframework.context.annotation; import org.junit.Test; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; -import org.springframework.context.annotation.ComponentScan.ExcludeFilter; -import org.springframework.context.annotation.ComponentScan.IncludeFilter; +import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.core.type.filter.TypeFilter; /** @@ -27,11 +26,13 @@ import org.springframework.core.type.filter.TypeFilter; * * @author Chris Beams * @since 3.1 + * @see ComponentScanAnnotationIntegrationTests */ public class ComponentScanAnnotationTests { @Test - public void test() { - + public void noop() { + // no-op; the @ComponentScan-annotated MyConfig class below simply excercises + // available attributes of the annotation. } } @@ -39,22 +40,22 @@ public class ComponentScanAnnotationTests { @Configuration @ComponentScan( - packageOf={TestBean.class}, + basePackageClasses={TestBean.class}, nameGenerator = DefaultBeanNameGenerator.class, scopedProxy = ScopedProxyMode.NO, scopeResolver = AnnotationScopeMetadataResolver.class, useDefaultFilters = false, resourcePattern = "**/*custom.class", includeFilters = { - @IncludeFilter(type = FilterType.ANNOTATION, value = MyAnnotation.class) + @Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class) }, excludeFilters = { - @ExcludeFilter(type = FilterType.CUSTOM, value = TypeFilter.class) + @Filter(type = FilterType.CUSTOM, value = TypeFilter.class) } ) class MyConfig { } -@ComponentScan(packageOf=example.scannable.NamedComponent.class) +@ComponentScan(basePackageClasses=example.scannable.NamedComponent.class) class SimpleConfig { } \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanExecutorTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanExecutorTests.java new file mode 100644 index 00000000000..287c05a024f --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanExecutorTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.config.ExecutorContext; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.mock.env.MockEnvironment; + +/** + * Unit tests for {@link ComponentScanExecutor}. + * + * @author Chris Beams + * @since 3.1 + */ +public class ComponentScanExecutorTests { + + private ComponentScanExecutor executor; + private ExecutorContext executorContext; + private DefaultListableBeanFactory bf; + + @Before + public void setUp() { + this.bf = new DefaultListableBeanFactory(); + this.executor = new ComponentScanExecutor(); + this.executorContext = new ExecutorContext(); + this.executorContext.setRegistry(bf); + this.executorContext.setResourceLoader(new DefaultResourceLoader()); + this.executorContext.setEnvironment(new MockEnvironment()); + this.executorContext.setRegistrar(new SimpleComponentRegistrar(bf)); + this.executorContext.setProblemReporter(new FailFastProblemReporter()); + } + + @Test + public void validSpec() { + this.executor.execute(new ComponentScanSpec("example.scannable"), this.executorContext); + assertThat(bf.containsBean("fooServiceImpl"), is(true)); + } + + @Test(expected=BeanDefinitionParsingException.class) + public void invalidSpec() { + // ff problem reporter should throw due to no packages specified + this.executor.execute(new ComponentScanSpec(), this.executorContext); + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanFeatureTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanFeatureTests.java new file mode 100644 index 00000000000..8ddbd03240f --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanFeatureTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +public class ComponentScanFeatureTests { + @Test + public void viaContextRegistration() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanFeatureConfig.class); + ctx.register(ComponentScanFeatureConfig.Features.class); + ctx.refresh(); + ctx.getBean(ComponentScanFeatureConfig.class); + ctx.getBean(TestBean.class); + assertThat("config class bean not found", ctx.containsBeanDefinition("componentScanFeatureConfig"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected", + ctx.containsBean("fooServiceImpl"), is(true)); + } +} + +@Configuration +//@Import(ComponentScanFeatureConfig.Features.class) +class ComponentScanFeatureConfig { + @FeatureConfiguration + static class Features { + @Feature + public ComponentScanSpec componentScan() { + return new ComponentScanSpec(example.scannable._package.class); + } + } + + @Bean + public TestBean testBean() { + return new TestBean(); + } +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanSpecTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanSpecTests.java new file mode 100644 index 00000000000..cc9f1ecbb81 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanSpecTests.java @@ -0,0 +1,411 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.springframework.util.StringUtils.arrayToCommaDelimitedString; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.beans.factory.parsing.Problem; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Unit tests for {@link ComponentScanSpec}. + * + * @author Chris Beams + * @since 3.1 + */ +public class ComponentScanSpecTests { + + private CollatingProblemReporter problemReporter; + private ClassLoader classLoader; + + + @Before + public void setUp() { + problemReporter = new CollatingProblemReporter(); + classLoader = ClassUtils.getDefaultClassLoader(); + } + + @Test + public void includeAnnotationConfig() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.includeAnnotationConfig(), nullValue()); + spec.includeAnnotationConfig(true); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(true)); + spec.includeAnnotationConfig(false); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(false)); + spec.includeAnnotationConfig("trUE"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(true)); + spec.includeAnnotationConfig("falSE"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(false)); + spec.includeAnnotationConfig(""); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(false)); + spec.includeAnnotationConfig((String)null); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(false)); + spec.includeAnnotationConfig((Boolean)null); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), nullValue()); + } + + @Test + public void resourcePattern() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.resourcePattern(), nullValue()); + assertThat(spec.validate(problemReporter), is(true)); + spec.resourcePattern("**/Foo*.class"); + assertThat(spec.resourcePattern(), is("**/Foo*.class")); + assertThat(spec.validate(problemReporter), is(true)); + spec.resourcePattern(""); + assertThat(spec.resourcePattern(), is("**/Foo*.class")); + assertThat(spec.validate(problemReporter), is(true)); + spec.resourcePattern(null); + assertThat(spec.resourcePattern(), is("**/Foo*.class")); + assertThat(spec.validate(problemReporter), is(true)); + } + + @Test + public void useDefaultFilters() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.useDefaultFilters(), nullValue()); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters((Boolean)null); + assertThat(spec.useDefaultFilters(), nullValue()); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters(true); + assertThat(spec.useDefaultFilters(), is(true)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters(false); + assertThat(spec.useDefaultFilters(), is(false)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters("trUE"); + assertThat(spec.useDefaultFilters(), is(true)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters("falSE"); + assertThat(spec.useDefaultFilters(), is(false)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters(""); + assertThat(spec.useDefaultFilters(), is(false)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters((String)null); + assertThat(spec.useDefaultFilters(), is(false)); + assertThat(spec.validate(problemReporter), is(true)); + } + + @Test + public void includeFilters() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.includeFilters( + new AnnotationTypeFilter(MyAnnotation.class), + new AssignableTypeFilter(Object.class)); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeFilters().length, is(2)); + } + + @Test + public void stringIncludeExcludeFilters() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.addIncludeFilter("annotation", MyAnnotation.class.getName(), classLoader); + spec.addExcludeFilter("assignable", Object.class.getName(), classLoader); + spec.addExcludeFilter("annotation", Override.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeFilters().length, is(1)); + assertThat(spec.excludeFilters().length, is(2)); + } + + @Test + public void bogusStringIncludeFilter() throws IOException { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.addIncludeFilter("bogus-type", "bogus-expr", classLoader); + assertThat(spec.validate(problemReporter), is(false)); + assertThat(spec.includeFilters().length, is(1)); + try { + spec.includeFilters()[0].match(null, null); + fail("expected placholder TypeFilter to throw exception"); + } catch (UnsupportedOperationException ex) { + // expected + } + } + + @Test + public void exerciseFilterTypes() throws IOException { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.addIncludeFilter("aspectj", "*..Bogus", classLoader); + assertThat(spec.validate(problemReporter), is(true)); + spec.addIncludeFilter("regex", ".*Foo", classLoader); + assertThat(spec.validate(problemReporter), is(true)); + spec.addIncludeFilter("custom", StubTypeFilter.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(true)); + spec.addIncludeFilter("custom", "org.NonExistentTypeFilter", classLoader); + assertThat(spec.validate(problemReporter), is(false)); + spec.addIncludeFilter("custom", NonNoArgResolver.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void missingBasePackages() { + ComponentScanSpec spec = new ComponentScanSpec(); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withBasePackageViaAdderMethod() { + ComponentScanSpec spec = new ComponentScanSpec(); + spec.addBasePackage("org.p1"); + spec.addBasePackage("org.p2"); + assertThat(spec.validate(problemReporter), is(true)); + assertExactContents(spec.basePackages(), "org.p1", "org.p2"); + } + + @Test + public void withBasePackagesViaStringConstructor() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1", "org.p2"); + assertThat(spec.validate(problemReporter), is(true)); + assertExactContents(spec.basePackages(), "org.p1", "org.p2"); + } + + @Test + public void withBasePackagesViaClassConstructor() { + ComponentScanSpec spec = new ComponentScanSpec(java.lang.Object.class, java.io.Closeable.class); + assertThat(spec.validate(problemReporter), is(true)); + assertExactContents(spec.basePackages(), "java.lang", "java.io"); + } + + @Test + public void forDelimitedPackages() { + ComponentScanSpec spec = ComponentScanSpec.forDelimitedPackages("pkg.one,pkg.two"); + assertTrue(ObjectUtils.containsElement(spec.basePackages(), "pkg.one")); + assertTrue(ObjectUtils.containsElement(spec.basePackages(), "pkg.two")); + assertThat(spec.basePackages().length, is(2)); + } + + @Test + public void withSomeEmptyBasePackages() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1", "", "org.p3"); + assertThat(spec.validate(problemReporter), is(true)); + assertExactContents(spec.basePackages(), "org.p1", "org.p3"); + } + + @Test + public void withAllEmptyBasePackages() { + ComponentScanSpec spec = new ComponentScanSpec("", "", ""); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withInstanceBeanNameGenerator() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.beanNameGenerator(), nullValue()); + BeanNameGenerator bng = new DefaultBeanNameGenerator(); + spec.beanNameGenerator(bng); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.beanNameGenerator(), is(bng)); + } + + @Test + public void withStringBeanNameGenerator() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.beanNameGenerator(DefaultBeanNameGenerator.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.beanNameGenerator(), instanceOf(DefaultBeanNameGenerator.class)); + } + + @Test + public void withInstanceScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.scopeMetadataResolver(), nullValue()); + ScopeMetadataResolver smr = new AnnotationScopeMetadataResolver(); + spec.scopeMetadataResolver(smr); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopeMetadataResolver(), is(smr)); + } + + @Test + public void withStringScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver(AnnotationScopeMetadataResolver.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopeMetadataResolver(), instanceOf(AnnotationScopeMetadataResolver.class)); + } + + @Test + public void withNonAssignableStringScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver(Object.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withNonExistentStringScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver("org.Bogus", classLoader); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withNonNoArgStringScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver(NonNoArgResolver.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withStringScopedProxyMode() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopedProxyMode("targetCLASS"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopedProxyMode(), is(ScopedProxyMode.TARGET_CLASS)); + spec.scopedProxyMode("interFACES"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopedProxyMode(), is(ScopedProxyMode.INTERFACES)); + spec.scopedProxyMode("nO"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopedProxyMode(), is(ScopedProxyMode.NO)); + spec.scopedProxyMode("bogus"); + assertThat(spec.validate(problemReporter), is(false)); + assertThat(spec.scopedProxyMode(), nullValue()); + } + + @Test + public void withScopeMetadataResolverAndScopedProxyMode() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver(new AnnotationScopeMetadataResolver()); + assertThat(spec.validate(problemReporter), is(true)); + spec.scopedProxyMode(ScopedProxyMode.INTERFACES); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void addBasePackage() { + ComponentScanSpec spec = new ComponentScanSpec(); + spec.addBasePackage("foo.bar"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.basePackages().length, is(1)); + } + + @Test + public void addBasePackageWithConstructor() { + ComponentScanSpec spec = new ComponentScanSpec("my.pkg"); + spec.addBasePackage("foo.bar"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.basePackages().length, is(2)); + } + + @Test + public void addExcludeFilterString() { + ComponentScanSpec spec = new ComponentScanSpec("my.pkg"); + spec.addExcludeFilter("annotation", MyAnnotation.class.getName(), ClassUtils.getDefaultClassLoader()); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.excludeFilters().length, is(1)); + assertThat(spec.excludeFilters()[0], instanceOf(AnnotationTypeFilter.class)); + } + + @Test(expected=BeanDefinitionParsingException.class) + public void withFailFastProblemReporter() { + new ComponentScanSpec().validate(new FailFastProblemReporter()); + } + + private void assertExactContents(T[] actual, T... expected) { + if (actual.length >= expected.length) { + for (int i = 0; i < expected.length; i++) { + assertThat( + String.format("element number %d in actual is incorrect. actual: [%s], expected: [%s]", + i, arrayToCommaDelimitedString(actual), arrayToCommaDelimitedString(expected)), + actual[i], equalTo(expected[i])); + } + } + assertThat(String.format("actual contains incorrect number of arguments. actual: [%s], expected: [%s]", + arrayToCommaDelimitedString(actual), arrayToCommaDelimitedString(expected)), + actual.length, equalTo(expected.length)); + } + + + private static class CollatingProblemReporter implements ProblemReporter { + + private List errors = new ArrayList(); + + private List warnings = new ArrayList(); + + + public void fatal(Problem problem) { + throw new BeanDefinitionParsingException(problem); + } + + public void error(Problem problem) { + this.errors.add(problem); + } + + @SuppressWarnings("unused") + public Problem[] getErrors() { + return this.errors.toArray(new Problem[this.errors.size()]); + } + + public void warning(Problem problem) { + System.out.println(problem); + this.warnings.add(problem); + } + + @SuppressWarnings("unused") + public Problem[] getWarnings() { + return this.warnings.toArray(new Problem[this.warnings.size()]); + } + } + + + private static class NonNoArgResolver implements ScopeMetadataResolver { + public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { + throw new UnsupportedOperationException(); + } + } + + private static class StubTypeFilter implements TypeFilter { + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + throw new UnsupportedOperationException(); + } + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreatorTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreatorTests.java new file mode 100644 index 00000000000..ae481bdae0d --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreatorTests.java @@ -0,0 +1,316 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.springframework.context.annotation.EarlyBeanReferenceProxyCreator.FINAL_CLASS_ERROR_MESSAGE; +import static org.springframework.context.annotation.EarlyBeanReferenceProxyCreator.MISSING_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE; +import static org.springframework.context.annotation.EarlyBeanReferenceProxyCreator.PRIVATE_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.MethodParameter; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * Unit tests for {@link EarlyBeanReferenceProxyCreator}, ensuring that + * {@link EarlyBeanReferenceProxy} objects behave properly. + * + * @author Chris Beams + * @since 3.1 + */ +public class EarlyBeanReferenceProxyCreatorTests { + + private DefaultListableBeanFactory bf; + + @Before + public void setUp() { + bf = new DefaultListableBeanFactory(); + } + + @Test + public void proxyToStringAvoidsEagerInstantiation() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy.toString(), equalTo("EarlyBeanReferenceProxy for bean of type TestBean")); + } + + @Test(expected=NoSuchBeanDefinitionException.class) + public void proxyThrowsNoSuchBeanDefinitionExceptionWhenDelegatingMethodCallToNonExistentBean() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + proxy.getName(); + } + + @Test + public void proxyHashCodeAvoidsEagerInstantiation() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy.hashCode(), equalTo(System.identityHashCode(proxy))); + } + + @Test + public void proxyEqualsAvoidsEagerInstantiation() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy.equals(new Object()), is(false)); + assertThat(proxy.equals(proxy), is(true)); + + TestBean proxy2 = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy, not(sameInstance(proxy2))); + assertThat(proxy.equals(proxy2), is(false)); + assertThat(proxy2.equals(proxy), is(false)); + assertThat(proxy2.equals(proxy2), is(true)); + } + + @Test + public void proxyFinalizeAvoidsEagerInstantiation() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + + BeanWithFinalizer proxy = (BeanWithFinalizer) pc.createProxy(descriptorFor(BeanWithFinalizer.class)); + + assertThat(BeanWithFinalizer.finalizerWasCalled, is(false)); + BeanWithFinalizer.class.getDeclaredMethod("finalize").invoke(proxy); + assertThat(BeanWithFinalizer.finalizerWasCalled, is(false)); + } + + @Test + public void proxyMethodsDelegateToTargetBeanCausingSingletonRegistrationIfNecessary() throws Exception { + bf.registerBeanDefinition("testBean", + BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "testBeanName").getBeanDefinition()); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(bf.containsBeanDefinition("testBean"), is(true)); + assertThat(bf.containsSingleton("testBean"), is(false)); + assertThat(proxy.getName(), equalTo("testBeanName")); + assertThat(bf.containsSingleton("testBean"), is(true)); + } + + @Test + public void beanAnnotatedMethodsReturnEarlyProxyAsWell() throws Exception { + bf.registerBeanDefinition("componentWithInterfaceBeanMethod", new RootBeanDefinition(ComponentWithInterfaceBeanMethod.class)); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ComponentWithInterfaceBeanMethod proxy = (ComponentWithInterfaceBeanMethod) pc.createProxy(descriptorFor(ComponentWithInterfaceBeanMethod.class)); + + ITestBean bean = proxy.aBeanMethod(); + assertThat(bean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(bf.containsBeanDefinition("componentWithInterfaceBeanMethod"), is(true)); + assertThat("calling a @Bean method on an EarlyBeanReferenceProxy object " + + "should not cause its instantation/registration", + bf.containsSingleton("componentWithInterfaceBeanMethod"), is(false)); + + Object obj = proxy.normalInstanceMethod(); + assertThat(bf.containsSingleton("componentWithInterfaceBeanMethod"), is(true)); + assertThat(obj, not(instanceOf(EarlyBeanReferenceProxy.class))); + } + + @Test + public void proxiesReturnedFromBeanAnnotatedMethodsDereferenceAndDelegateToTheirTargetBean() throws Exception { + bf.registerBeanDefinition("componentWithConcreteBeanMethod", new RootBeanDefinition(ComponentWithConcreteBeanMethod.class)); + RootBeanDefinition beanMethodBeanDef = new RootBeanDefinition(); + beanMethodBeanDef.setFactoryBeanName("componentWithConcreteBeanMethod"); + beanMethodBeanDef.setFactoryMethodName("aBeanMethod"); + bf.registerBeanDefinition("aBeanMethod", beanMethodBeanDef); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ComponentWithConcreteBeanMethod proxy = (ComponentWithConcreteBeanMethod) pc.createProxy(descriptorFor(ComponentWithConcreteBeanMethod.class)); + + TestBean bean = proxy.aBeanMethod(); + assertThat(bean.getName(), equalTo("concrete")); + } + + @Test + public void interfaceBeansAreProxied() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ITestBean proxy = (ITestBean) pc.createProxy(descriptorFor(ITestBean.class)); + + assertThat(proxy, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(AopUtils.isCglibProxyClass(proxy.getClass()), is(true)); + assertEquals( + "interface-based bean proxies should have Object as superclass", + proxy.getClass().getSuperclass(), Object.class); + } + + @Test + public void concreteBeansAreProxied() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(AopUtils.isCglibProxyClass(proxy.getClass()), is(true)); + assertEquals( + "concrete bean proxies should have the bean class as superclass", + proxy.getClass().getSuperclass(), TestBean.class); + } + + @Test + public void beanAnnotatedMethodsWithInterfaceReturnTypeAreProxied() throws Exception { + bf.registerBeanDefinition("componentWithInterfaceBeanMethod", new RootBeanDefinition(ComponentWithInterfaceBeanMethod.class)); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ComponentWithInterfaceBeanMethod proxy = (ComponentWithInterfaceBeanMethod) pc.createProxy(descriptorFor(ComponentWithInterfaceBeanMethod.class)); + + ITestBean bean = proxy.aBeanMethod(); + assertThat(bean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(AopUtils.isCglibProxyClass(bean.getClass()), is(true)); + assertEquals( + "interface-based bean proxies should have Object as superclass", + bean.getClass().getSuperclass(), Object.class); + } + + @Test + public void beanAnnotatedMethodsWithConcreteReturnTypeAreProxied() throws Exception { + bf.registerBeanDefinition("componentWithConcreteBeanMethod", new RootBeanDefinition(ComponentWithConcreteBeanMethod.class)); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ComponentWithConcreteBeanMethod proxy = (ComponentWithConcreteBeanMethod) pc.createProxy(descriptorFor(ComponentWithConcreteBeanMethod.class)); + + TestBean bean = proxy.aBeanMethod(); + assertThat(bean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(AopUtils.isCglibProxyClass(bean.getClass()), is(true)); + assertEquals( + "concrete bean proxies should have the bean class as superclass", + bean.getClass().getSuperclass(), TestBean.class); + } + + @Test + public void attemptToProxyClassMissingNoArgConstructorFailsGracefully() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + try { + pc.createProxy(descriptorFor(BeanMissingNoArgConstructor.class)); + fail("expected ProxyCreationException"); + } catch(ProxyCreationException ex) { + assertThat(ex.getMessage(), + equalTo(format(MISSING_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE, BeanMissingNoArgConstructor.class.getName()))); + } + } + + @Test + public void attemptToProxyClassWithPrivateNoArgConstructorFailsGracefully() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + try { + pc.createProxy(descriptorFor(BeanWithPrivateNoArgConstructor.class)); + fail("expected ProxyCreationException"); + } catch(ProxyCreationException ex) { + assertThat(ex.getMessage(), + equalTo(format(PRIVATE_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE, BeanWithPrivateNoArgConstructor.class.getName()))); + } + } + + @Test + public void attemptToProxyFinalClassFailsGracefully() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + try { + pc.createProxy(descriptorFor(FinalBean.class)); + fail("expected ProxyCreationException"); + } catch(ProxyCreationException ex) { + assertThat(ex.getMessage(), + equalTo(format(FINAL_CLASS_ERROR_MESSAGE, FinalBean.class.getName()))); + } + } + + private DependencyDescriptor descriptorFor(Class paramType) throws Exception { + @SuppressWarnings("unused") + class C { + void m(ITestBean p) { } + void m(TestBean p) { } + void m(BeanMissingNoArgConstructor p) { } + void m(BeanWithPrivateNoArgConstructor p) { } + void m(FinalBean p) { } + void m(BeanWithFinalizer p) { } + void m(ComponentWithConcreteBeanMethod p) { } + void m(ComponentWithInterfaceBeanMethod p) { } + } + + Method targetMethod = C.class.getDeclaredMethod("m", new Class[] { paramType }); + MethodParameter mp = new MethodParameter(targetMethod, 0); + DependencyDescriptor dd = new DependencyDescriptor(mp, true, false); + return dd; + } + + + static class BeanMissingNoArgConstructor { + BeanMissingNoArgConstructor(Object o) { } + } + + + static class BeanWithPrivateNoArgConstructor { + private BeanWithPrivateNoArgConstructor() { } + } + + + static final class FinalBean { + } + + + static class BeanWithFinalizer { + static Boolean finalizerWasCalled = false; + + @Override + protected void finalize() throws Throwable { + finalizerWasCalled = true; + } + } + + + static class ComponentWithConcreteBeanMethod { + @Bean + public TestBean aBeanMethod() { + return new TestBean("concrete"); + } + + public Object normalInstanceMethod() { + return new Object(); + } + } + + + static class ComponentWithInterfaceBeanMethod { + @Bean + public ITestBean aBeanMethod() { + return new TestBean("interface"); + } + + public Object normalInstanceMethod() { + return new Object(); + } + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationClassTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationClassTests.java new file mode 100644 index 00000000000..a7831d0eaa3 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationClassTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import org.junit.Test; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +public class FeatureConfigurationClassTests { + + @Test(expected=FeatureMethodExecutionException.class) + public void featureConfigurationClassesMustNotContainBeanAnnotatedMethods() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(FeatureConfigWithBeanAnnotatedMethod.class); + ctx.refresh(); + } + +} + + +@FeatureConfiguration +class FeatureConfigWithBeanAnnotatedMethod { + /** + * This is illegal use. @FeatureConfiguration classes cannot have @Bean methods. + */ + @Bean + public TestBean testBean() { + return new TestBean(); + } + + /** + * This will never get called. An exception will first be raised regarding the illegal @Bean method above. + */ + @Feature + public FeatureSpecification feature() { + return new StubSpecification(); + } +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml new file mode 100644 index 00000000000..930cadfa379 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests.java new file mode 100644 index 00000000000..466e65b14e3 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +import test.beans.TestBean; + +/** + * Tests proving that @FeatureConfiguration classes may be use @ImportResource + * and then parameter autowire beans declared in the imported resource(s). + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureConfigurationImportResourceTests { + + @Test + public void importResourceFromFeatureConfiguration() { + ApplicationContext ctx = + new AnnotationConfigApplicationContext(ImportingFeatureConfig.class); + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName(), equalTo("beanFromXml")); + + // and just quickly prove that the target of the bean proxied for the Feature method + // is indeed the same singleton instance as the one we just pulled from the container + ImportingFeatureConfig ifc = ctx.getBean(ImportingFeatureConfig.class); + TestBean proxyBean = ifc.testBean; + assertThat(proxyBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertNotSame(proxyBean, testBean); + assertSame(((EarlyBeanReferenceProxy)proxyBean).dereferenceTargetBean(), testBean); + } + + @FeatureConfiguration + @ImportResource("org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml") + static class ImportingFeatureConfig { + TestBean testBean; + + @Feature + public FeatureSpecification f(TestBean testBean) { + this.testBean = testBean; + return new StubSpecification(); + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportTests.java new file mode 100644 index 00000000000..53141541225 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +/** + * Tests proving that @Configuration classes may @Import @FeatureConfiguration + * classes, and vice versa. + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureConfigurationImportTests { + + @Test + public void importFeatureConfigurationFromConfiguration() { + ConfigurableApplicationContext ctx = + new AnnotationConfigApplicationContext(ImportingConfig.class); + ImportedFeatureConfig ifc = ctx.getBean(ImportedFeatureConfig.class); + assertThat( + "@FeatureConfiguration class was imported and registered " + + "as a bean but its @Feature method was never called", + ifc.featureMethodWasCalled, is(true)); + } + + @Test + public void importConfigurationFromFeatureConfiguration() { + ConfigurableApplicationContext ctx = + new AnnotationConfigApplicationContext(ImportingFeatureConfig.class); + ImportingFeatureConfig ifc = ctx.getBean(ImportingFeatureConfig.class); + ImportedConfig ic = ctx.getBean(ImportedConfig.class); + assertThat( + "@FeatureConfiguration class was registered directly against " + + "the container but its @Feature method was never called", + ifc.featureMethodWasCalled, is(true)); + assertThat( + "@Configuration class was @Imported but its @Bean method" + + "was never registered / called", + ic.beanMethodWasCalled, is(true)); + } + + + @Configuration + @Import(ImportedFeatureConfig.class) + static class ImportingConfig { + } + + + @FeatureConfiguration + static class ImportedFeatureConfig { + boolean featureMethodWasCalled = false; + + @Feature + public FeatureSpecification f() { + this.featureMethodWasCalled = true; + return new StubSpecification(); + } + } + + + @Configuration + static class ImportedConfig { + boolean beanMethodWasCalled = true; + @Bean + public TestBean testBean() { + this.beanMethodWasCalled = true; + return new TestBean(); + } + } + + + @FeatureConfiguration + @Import(ImportedConfig.class) + static class ImportingFeatureConfig { + boolean featureMethodWasCalled = false; + + @Feature + public FeatureSpecification f() { + this.featureMethodWasCalled = true; + return new StubSpecification(); + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodBeanReferenceTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodBeanReferenceTests.java new file mode 100644 index 00000000000..bab82187afb --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodBeanReferenceTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * Tests proving that @Feature methods may reference the product of @Bean methods. + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureMethodBeanReferenceTests { + + @Test + public void test() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(FeatureConfig.class, Config.class); + ctx.refresh(); + + TestBean registeredBean = ctx.getBean("testBean", TestBean.class); + TestBean proxiedBean = ctx.getBean(FeatureConfig.class).testBean; + + assertThat(registeredBean, not(instanceOf(EarlyBeanReferenceProxy.class))); + assertThat(proxiedBean, notNullValue()); + assertThat(proxiedBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(proxiedBean.getSpouse(), is(registeredBean.getSpouse())); + } + + + @FeatureConfiguration + static class FeatureConfig { + TestBean testBean; + + @Feature + public FeatureSpecification f(TestBean testBean) { + this.testBean = testBean; + return new StubSpecification(); + } + } + + + @Configuration + static class Config { + @Bean + public ITestBean testBean() { + return new TestBean(new TestBean("mySpouse")); + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodEarlyBeanProxyTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodEarlyBeanProxyTests.java new file mode 100644 index 00000000000..62784a84a34 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodEarlyBeanProxyTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * Tests that @Bean methods referenced from within @Feature methods + * get proxied early to avoid premature instantiation of actual + * bean instances. + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureMethodEarlyBeanProxyTests { + + @Test + public void earlyProxyCreationAndBeanRegistrationLifecycle() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(FeatureConfig.class); + + // + // see additional assertions in FeatureConfig#feature() + // + + // sanity check that all the bean definitions we expecting are present + assertThat(ctx.getBeanFactory().containsBeanDefinition("lazyHelperBean"), is(true)); + assertThat(ctx.getBeanFactory().containsBeanDefinition("eagerHelperBean"), is(true)); + assertThat(ctx.getBeanFactory().containsBeanDefinition("lazyPassthroughBean"), is(true)); + assertThat(ctx.getBeanFactory().containsBeanDefinition("eagerPassthroughBean"), is(true)); + + + // the lazy helper bean had methods invoked during feature method execution. it should be registered + assertThat(ctx.getBeanFactory().containsSingleton("lazyHelperBean"), is(true)); + + // the eager helper bean had methods invoked but should be registered in any case is it is non-lazy + assertThat(ctx.getBeanFactory().containsSingleton("eagerHelperBean"), is(true)); + + // the lazy passthrough bean was referenced in the feature method, but never invoked. it should not be registered + assertThat(ctx.getBeanFactory().containsSingleton("lazyPassthroughBean"), is(false)); + + // the eager passthrough bean should be registered in any case as it is non-lazy + assertThat(ctx.getBeanFactory().containsSingleton("eagerPassthroughBean"), is(true)); + + + // now actually fetch all the beans. none should be proxies + assertThat(ctx.getBean("lazyHelperBean"), not(instanceOf(EarlyBeanReferenceProxy.class))); + assertThat(ctx.getBean("eagerHelperBean"), not(instanceOf(EarlyBeanReferenceProxy.class))); + assertThat(ctx.getBean("lazyPassthroughBean"), not(instanceOf(EarlyBeanReferenceProxy.class))); + assertThat(ctx.getBean("eagerPassthroughBean"), not(instanceOf(EarlyBeanReferenceProxy.class))); + } + + + @Test + public void earlyProxyBeansMayBeInterfaceBasedOrConcrete() { + new AnnotationConfigApplicationContext(FeatureConfigReferencingNonInterfaceBeans.class); + } +} + + +@FeatureConfiguration +@Import(TestBeanConfig.class) +class FeatureConfig implements BeanFactoryAware { + private DefaultListableBeanFactory beanFactory; + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (DefaultListableBeanFactory)beanFactory; + } + + @Feature + public StubSpecification feature(TestBeanConfig beans) { + + assertThat( + "The @Configuration class instance itself should be an early-ref proxy", + beans, instanceOf(EarlyBeanReferenceProxy.class)); + + // invocation of @Bean methods within @Feature methods should return proxies + ITestBean lazyHelperBean = beans.lazyHelperBean(); + ITestBean eagerHelperBean = beans.eagerHelperBean(); + ITestBean lazyPassthroughBean = beans.lazyPassthroughBean(); + ITestBean eagerPassthroughBean = beans.eagerPassthroughBean(); + + assertThat(lazyHelperBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(eagerHelperBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(lazyPassthroughBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(eagerPassthroughBean, instanceOf(EarlyBeanReferenceProxy.class)); + + // but at this point, the proxy instances should not have + // been registered as singletons with the container. + assertThat(this.beanFactory.containsSingleton("lazyHelperBean"), is(false)); + assertThat(this.beanFactory.containsSingleton("eagerHelperBean"), is(false)); + assertThat(this.beanFactory.containsSingleton("lazyPassthroughBean"), is(false)); + assertThat(this.beanFactory.containsSingleton("eagerPassthroughBean"), is(false)); + + // invoking a method on the proxy should cause it to pass through + // to the container, instantiate the actual bean in question and + // register that actual underlying instance as a singleton. + assertThat(lazyHelperBean.getName(), equalTo("lazyHelper")); + assertThat(eagerHelperBean.getName(), equalTo("eagerHelper")); + + assertThat(this.beanFactory.containsSingleton("lazyHelperBean"), is(true)); + assertThat(this.beanFactory.containsSingleton("eagerHelperBean"), is(true)); + + // since no methods were called on the passthrough beans, they should remain + // uncreated / unregistered. + assertThat(this.beanFactory.containsSingleton("lazyPassthroughBean"), is(false)); + assertThat(this.beanFactory.containsSingleton("eagerPassthroughBean"), is(false)); + + return new StubSpecification(); + } +} + + +@Configuration +class TestBeanConfig { + + @Lazy @Bean + public ITestBean lazyHelperBean() { + return new TestBean("lazyHelper"); + } + + @Bean + public ITestBean eagerHelperBean() { + return new TestBean("eagerHelper"); + } + + @Lazy @Bean + public ITestBean lazyPassthroughBean() { + return new TestBean("lazyPassthrough"); + } + + @Bean + public ITestBean eagerPassthroughBean() { + return new TestBean("eagerPassthrough"); + } + +} + + +@FeatureConfiguration +@Import(NonInterfaceBeans.class) +class FeatureConfigReferencingNonInterfaceBeans { + @Feature + public FeatureSpecification feature1(NonInterfaceBeans beans) throws Throwable { + beans.testBean(); + return new StubSpecification(); + } + + @Feature + public FeatureSpecification feature2(TestBean testBean) throws Throwable { + return new StubSpecification(); + } +} + + +@Configuration +class NonInterfaceBeans { + @Bean + public TestBean testBean() { + return new TestBean("invalid"); + } +} + diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodQualifiedBeanReferenceTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodQualifiedBeanReferenceTests.java new file mode 100644 index 00000000000..5ea134eb330 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodQualifiedBeanReferenceTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * Tests proving that @Feature methods may reference beans using @Qualifier + * as a parameter annotation. + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureMethodQualifiedBeanReferenceTests { + + @Test + public void test() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Features.class, TestBeans.class); + ctx.refresh(); + } + + @FeatureConfiguration + static class Features { + + @Feature + public FeatureSpecification f(@Qualifier("testBean1") ITestBean testBean) { + assertThat(testBean.getName(), equalTo("one")); + return new StubSpecification(); + } + + } + + @Configuration + static class TestBeans { + @Bean + public ITestBean testBean1() { + return new TestBean("one"); + } + + @Bean + public ITestBean testBean2() { + return new TestBean("two"); + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/SimpleFeatureMethodProcessingTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/SimpleFeatureMethodProcessingTests.java new file mode 100644 index 00000000000..4a06cf3e518 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/SimpleFeatureMethodProcessingTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2011 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.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.ExecutorContext; +import org.springframework.context.config.FeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.util.Assert; + +/** + * Simple tests to ensure that @Feature methods are invoked and that the + * resulting returned {@link FeatureSpecification} object is delegated to + * the correct {@link FeatureSpecificationExecutor}. + * + * @author Chris Beams + * @since 3.1 + */ +public class SimpleFeatureMethodProcessingTests { + + @Test + public void test() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(FeatureConfig.class); + assertThat(MySpecificationExecutor.executeMethodWasCalled, is(false)); + ctx.refresh(); + assertThat(MySpecificationExecutor.executeMethodWasCalled, is(true)); + } + + @FeatureConfiguration + static class FeatureConfig { + @Feature + public FeatureSpecification f() { + return new StubSpecification(MySpecificationExecutor.class); + } + } + + static class MySpecificationExecutor implements FeatureSpecificationExecutor { + static boolean executeMethodWasCalled = false; + public void execute(FeatureSpecification spec, ExecutorContext executorContext) { + Assert.state(executeMethodWasCalled == false); + executeMethodWasCalled = true; + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java index e5ab36f3404..566aa340e35 100644 --- a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -16,11 +16,13 @@ package org.springframework.context.annotation.configuration; -import static org.junit.Assert.*; -import org.junit.Test; -import test.beans.ITestBean; -import test.beans.TestBean; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Required; @@ -43,7 +45,9 @@ import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.annotation.Scope; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.PriorityOrdered; + +import test.beans.ITestBean; +import test.beans.TestBean; /** * Miscellaneous system tests covering {@link Bean} naming, aliases, scoping and error diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java new file mode 100644 index 00000000000..c81b3d9ab86 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2011 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.context.annotation.configuration; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * A configuration class that registers a placeholder configurer @Bean method + * cannot also have @Value fields. Logically, the config class must be instantiated + * in order to invoke the placeholder configurer bean method, and it is a + * chicken-and-egg problem to process the @Value field. + * + * Therefore, placeholder configurers should be put in separate configuration classes + * as has been done in the test below. Simply said, placeholder configurer @Bean methods + * and @Value fields in the same configuration class are mutually exclusive. + * + * @author Chris Beams + */ +public class ConfigurationClassWithPlaceholderConfigurerBeanTests { + + /** + * Intentionally ignored test proving that a property placeholder bean + * cannot be declared in the same configuration class that has a @Value + * field in need of placeholder replacement. It's an obvious chicken-and-egg issue. + * The solution is to do as {@link #valueFieldsAreProcessedWhenPlaceholderConfigurerIsSegregated()} + * does and segragate the two bean definitions across configuration classes. + */ + @Ignore @Test + public void valueFieldsAreNotProcessedWhenPlaceholderConfigurerIsIntegrated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithValueFieldAndPlaceholderConfigurer.class); + System.setProperty("test.name", "foo"); + ctx.refresh(); + System.clearProperty("test.name"); + + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName(), nullValue()); + } + + @Test + public void valueFieldsAreProcessedWhenPlaceholderConfigurerIsSegregated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithValueField.class); + ctx.register(ConfigWithPlaceholderConfigurer.class); + System.setProperty("test.name", "foo"); + ctx.refresh(); + System.clearProperty("test.name"); + + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName(), equalTo("foo")); + } +} + +@Configuration +class ConfigWithValueField { + + @Value("${test.name}") + private String name; + + @Bean + public ITestBean testBean() { + return new TestBean(this.name); + } +} + +@Configuration +class ConfigWithPlaceholderConfigurer { + + @Bean + public PropertySourcesPlaceholderConfigurer ppc() { + return new PropertySourcesPlaceholderConfigurer(); + } + +} + +@Configuration +class ConfigWithValueFieldAndPlaceholderConfigurer { + + @Value("${test.name}") + private String name; + + @Bean + public ITestBean testBean() { + return new TestBean(this.name); + } + + @Bean + public PropertySourcesPlaceholderConfigurer ppc() { + return new PropertySourcesPlaceholderConfigurer(); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodAndAutowiredFieldTests.xml b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodAndAutowiredFieldTests.xml new file mode 100644 index 00000000000..0418dc8251a --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodAndAutowiredFieldTests.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodLifecycleIssueTestSuite.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodLifecycleIssueTestSuite.java new file mode 100644 index 00000000000..68baf7f7bfc --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodLifecycleIssueTestSuite.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2011 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.context.annotation.configuration; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.context.annotation.FeatureMethodEarlyBeanProxyTests; + +/** + * Test suite that groups all tests related to @Feature method lifecycle issues. + * + * @author Chris Beams + */ +@RunWith(Suite.class) +@SuiteClasses({ + FeatureMethodEarlyBeanProxyTests.class, + ConfigurationClassWithPlaceholderConfigurerBeanTests.class, +}) +public class FeatureMethodLifecycleIssueTestSuite { + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/StubSpecification.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/StubSpecification.java new file mode 100644 index 00000000000..94d7aef2bfd --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/StubSpecification.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2011 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.context.annotation.configuration; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.ExecutorContext; +import org.springframework.context.config.FeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; + +public class StubSpecification extends AbstractFeatureSpecification { + + public StubSpecification() { + this(StubSpecificationExecutor.class); + } + + public StubSpecification(Class excecutorType) { + super(excecutorType); + } + + @Override + protected void doValidate(SimpleProblemCollector reporter) { + } + +} + +class StubSpecificationExecutor implements FeatureSpecificationExecutor { + + public void execute(FeatureSpecification spec, ExecutorContext executorContext) { + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java b/org.springframework.context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java index 1539f69d04e..9c4f6837aae 100644 --- a/org.springframework.context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java +++ b/org.springframework.context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java @@ -36,7 +36,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.MutablePropertyValues; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.support.FormattingConversionService; @@ -54,7 +54,7 @@ public class JodaTimeFormattingTests { @Before public void setUp() { - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); registrar.registerFormatters(conversionService); diff --git a/org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java b/org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java index 510dbbfea48..7ee161efe62 100644 --- a/org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java +++ b/org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -27,7 +27,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.MutablePropertyValues; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.annotation.NumberFormat; import org.springframework.format.annotation.NumberFormat.Style; import org.springframework.format.support.FormattingConversionService; @@ -46,7 +46,7 @@ public class NumberFormattingTests { @Before public void setUp() { - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.setEmbeddedValueResolver(new StringValueResolver() { public String resolveStringValue(String strVal) { if ("${pattern}".equals(strVal)) { diff --git a/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java b/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java index ec27aa11fc6..9fe778dc926 100644 --- a/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java +++ b/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,9 @@ package org.springframework.format.support; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + import java.text.ParseException; import java.util.ArrayList; import java.util.Date; @@ -27,10 +30,8 @@ import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; import org.junit.After; -import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; - import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessorFactory; @@ -40,7 +41,7 @@ import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.datetime.joda.DateTimeParser; import org.springframework.format.datetime.joda.JodaDateTimeFormatAnnotationFormatterFactory; import org.springframework.format.datetime.joda.ReadablePartialPrinter; @@ -57,7 +58,7 @@ public class FormattingConversionServiceTests { @Before public void setUp() { formattingService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(formattingService); + DefaultConversionService.addDefaultConverters(formattingService); LocaleContextHolder.setLocale(Locale.US); } diff --git a/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java b/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java index 526f5a2ba3a..753748ff7d9 100644 --- a/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -30,8 +30,6 @@ import java.util.TreeSet; import junit.framework.TestCase; -import org.junit.Ignore; -import org.junit.Test; import org.springframework.beans.BeanWithObjectProperty; import org.springframework.beans.DerivedTestBean; import org.springframework.beans.ITestBean; @@ -46,7 +44,7 @@ import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.context.support.StaticMessageSource; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.number.NumberFormatter; import org.springframework.format.support.FormattingConversionService; import org.springframework.util.StringUtils; @@ -319,7 +317,7 @@ public class DataBinderTests extends TestCase { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -350,7 +348,7 @@ public class DataBinderTests extends TestCase { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -372,7 +370,7 @@ public class DataBinderTests extends TestCase { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -393,7 +391,7 @@ public class DataBinderTests extends TestCase { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -415,7 +413,7 @@ public class DataBinderTests extends TestCase { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); binder.initDirectFieldAccess(); @@ -448,7 +446,7 @@ public class DataBinderTests extends TestCase { DataBinder binder = new DataBinder(tb); binder.initDirectFieldAccess(); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -553,7 +551,7 @@ public class DataBinderTests extends TestCase { assertEquals(1, disallowedFields.length); assertEquals("age", disallowedFields[0]); - Map m = binder.getBindingResult().getModel(); + Map m = binder.getBindingResult().getModel(); assertTrue("There is one element in map", m.size() == 2); TestBean tb = (TestBean) m.get("person"); assertTrue("Same object", tb.equals(rod)); @@ -1415,6 +1413,7 @@ public class DataBinderTests extends TestCase { assertEquals("badName", nameError.getCode()); } + @SuppressWarnings("unchecked") public void testBindingWithResortedList() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java index 0b3664cf91f..c397ab55ba9 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java @@ -29,66 +29,16 @@ import org.springframework.core.convert.converter.GenericConverter; * * @author Keith Donald * @author Juergen Hoeller + * @author Chris Beams * @since 3.0 */ public abstract class ConversionServiceFactory { /** - * Create a new default ConversionService instance that can be safely modified. - */ - public static GenericConversionService createDefaultConversionService() { - GenericConversionService conversionService = new GenericConversionService(); - addDefaultConverters(conversionService); - return conversionService; - } - - /** - * Populate the given ConversionService instance with all applicable default converters. - */ - public static void addDefaultConverters(GenericConversionService conversionService) { - conversionService.addConverter(new ArrayToCollectionConverter(conversionService)); - conversionService.addConverter(new CollectionToArrayConverter(conversionService)); - - conversionService.addConverter(new ArrayToStringConverter(conversionService)); - conversionService.addConverter(new StringToArrayConverter(conversionService)); - - conversionService.addConverter(new ArrayToObjectConverter(conversionService)); - conversionService.addConverter(new ObjectToArrayConverter(conversionService)); - - conversionService.addConverter(new CollectionToStringConverter(conversionService)); - conversionService.addConverter(new StringToCollectionConverter(conversionService)); - - conversionService.addConverter(new CollectionToObjectConverter(conversionService)); - conversionService.addConverter(new ObjectToCollectionConverter(conversionService)); - - conversionService.addConverter(new ArrayToArrayConverter(conversionService)); - conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); - conversionService.addConverter(new MapToMapConverter(conversionService)); - - conversionService.addConverter(new PropertiesToStringConverter()); - conversionService.addConverter(new StringToPropertiesConverter()); - - conversionService.addConverter(new StringToBooleanConverter()); - conversionService.addConverter(new StringToCharacterConverter()); - conversionService.addConverter(new StringToLocaleConverter()); - conversionService.addConverterFactory(new StringToNumberConverterFactory()); - conversionService.addConverterFactory(new StringToEnumConverterFactory()); - - conversionService.addConverter(new NumberToCharacterConverter()); - conversionService.addConverterFactory(new CharacterToNumberFactory()); - - conversionService.addConverterFactory(new NumberToNumberConverterFactory()); - - conversionService.addConverter(new ObjectToStringConverter()); - conversionService.addConverter(new ObjectToObjectConverter()); - conversionService.addConverter(new IdToEntityConverter(conversionService)); - } - - /** - * Register the given converter objects with the given target registry. + * Register the given Converter objects with the given target ConverterRegistry. * @param converters the converter objects: implementing {@link Converter}, * {@link ConverterFactory}, or {@link GenericConverter} - * @param registry the target registry to register with + * @param registry the target registry */ public static void registerConverters(Set converters, ConverterRegistry registry) { if (converters != null) { @@ -110,4 +60,22 @@ public abstract class ConversionServiceFactory { } } + /** + * Create a new default ConversionService instance that can be safely modified. + * + * @deprecated in Spring 3.1 in favor of {@link DefaultConversionService#DefaultConversionService()} + */ + public static GenericConversionService createDefaultConversionService() { + return new DefaultConversionService(); + } + + /** + * Populate the given ConversionService instance with all applicable default converters. + * + * @deprecated in Spring 3.1 in favor of {@link DefaultConversionService#addDefaultConverters} + */ + public static void addDefaultConverters(GenericConversionService conversionService) { + DefaultConversionService.addDefaultConverters(conversionService); + } + } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java new file mode 100644 index 00000000000..1eea0c986aa --- /dev/null +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2011 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.convert.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterRegistry; + +/** + * A specialization of {@link GenericConversionService} configured by default with + * converters appropriate for most applications. + * + *

Designed for direct instantiation but also exposes the static + * {@link #addDefaultConverters} utility method for ad hoc use against any + * {@code GenericConversionService} instance. + * + * @author Chris Beams + * @since 3.1 + */ +public class DefaultConversionService extends GenericConversionService { + + /** + * Create a new {@code DefaultConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters default converters}. + */ + public DefaultConversionService() { + addDefaultConverters(this); + } + + /** + * Add converters appropriate for most environments. + * @param conversionService the service to register default formatters against + */ + public static void addDefaultConverters(GenericConversionService conversionService) { + conversionService.addConverter(new ArrayToCollectionConverter(conversionService)); + conversionService.addConverter(new CollectionToArrayConverter(conversionService)); + + conversionService.addConverter(new ArrayToStringConverter(conversionService)); + conversionService.addConverter(new StringToArrayConverter(conversionService)); + + conversionService.addConverter(new ArrayToObjectConverter(conversionService)); + conversionService.addConverter(new ObjectToArrayConverter(conversionService)); + + conversionService.addConverter(new CollectionToStringConverter(conversionService)); + conversionService.addConverter(new StringToCollectionConverter(conversionService)); + + conversionService.addConverter(new CollectionToObjectConverter(conversionService)); + conversionService.addConverter(new ObjectToCollectionConverter(conversionService)); + + conversionService.addConverter(new ArrayToArrayConverter(conversionService)); + conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); + conversionService.addConverter(new MapToMapConverter(conversionService)); + + conversionService.addConverter(new PropertiesToStringConverter()); + conversionService.addConverter(new StringToPropertiesConverter()); + + conversionService.addConverter(new StringToBooleanConverter()); + conversionService.addConverter(new StringToCharacterConverter()); + conversionService.addConverter(new StringToLocaleConverter()); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + conversionService.addConverterFactory(new StringToEnumConverterFactory()); + + conversionService.addConverter(new NumberToCharacterConverter()); + conversionService.addConverterFactory(new CharacterToNumberFactory()); + + conversionService.addConverterFactory(new NumberToNumberConverterFactory()); + + conversionService.addConverter(new ObjectToStringConverter()); + conversionService.addConverter(new ObjectToObjectConverter()); + conversionService.addConverter(new IdToEntityConverter(conversionService)); + } + +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/org.springframework.core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java index b489bf6520c..35e20d9dbcd 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java +++ b/org.springframework.core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -24,11 +24,10 @@ import static org.springframework.util.SystemPropertyUtils.VALUE_SEPARATOR; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; - /** * Abstract base class for resolving properties against any underlying source. * @@ -39,7 +38,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe protected final Log logger = LogFactory.getLog(getClass()); - protected ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + protected ConversionService conversionService = new DefaultConversionService(); private PropertyPlaceholderHelper nonStrictHelper; private PropertyPlaceholderHelper strictHelper; diff --git a/org.springframework.core/src/main/java/org/springframework/core/env/Environment.java b/org.springframework.core/src/main/java/org/springframework/core/env/Environment.java index 32cacfb2ed3..433aad68992 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/env/Environment.java +++ b/org.springframework.core/src/main/java/org/springframework/core/env/Environment.java @@ -69,7 +69,7 @@ public interface Environment extends PropertyResolver { * Return the set of profiles explicitly made active for this environment. Profiles are used for * creating logical groupings of bean definitions to be registered conditionally, often based on * deployment environment. Profiles can be activated by setting {@linkplain - * AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME "spring.profiles.active"} as a system property + * AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME "spring.profile.active"} as a system property * or by calling {@link ConfigurableEnvironment#setActiveProfiles(String...)}. * *

If no profiles have explicitly been specified as active, then any 'default' profiles will implicitly diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/MethodMetadata.java b/org.springframework.core/src/main/java/org/springframework/core/type/MethodMetadata.java index b9417e8873d..caea4339e4b 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/MethodMetadata.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/MethodMetadata.java @@ -36,6 +36,11 @@ public interface MethodMetadata { */ String getMethodName(); + /** + * Return the fully-qualified name of the return type of the method. + */ + String getMethodReturnType(); + /** * Return the fully-qualified name of the class that declares this method. */ diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java b/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java index c6c8f28f80e..328e1f4318c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java @@ -58,7 +58,11 @@ public class StandardMethodMetadata implements MethodMetadata { public String getMethodName() { return this.introspectedMethod.getName(); } - + + public String getMethodReturnType() { + return this.introspectedMethod.getReturnType().getName(); + } + public String getDeclaringClassName() { return this.introspectedMethod.getDeclaringClass().getName(); } diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java index 311955368d1..77bec9fa47c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -62,7 +62,7 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - return new MethodMetadataReadingVisitor(name, access, this.getClassName(), this.classLoader, this.methodMetadataMap); + return new MethodMetadataReadingVisitor(name, getReturnTypeFromAsmMethodDescriptor(desc), access, this.getClassName(), this.classLoader, this.methodMetadataMap); } @Override @@ -125,6 +125,19 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor } value = convArray; } + else if (classValuesAsString) { + if (value instanceof Class) { + value = ((Class) value).getName(); + } + else if (value instanceof Class[]) { + Class[] clazzArray = (Class[]) value; + String[] newValue = new String[clazzArray.length]; + for (int i = 0; i < clazzArray.length; i++) { + newValue[i] = clazzArray[i].getName(); + } + value = newValue; + } + } result.put(entry.getKey(), value); } catch (Exception ex) { @@ -148,4 +161,46 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor return annotatedMethods; } + /** + * Convert a type descriptor to a classname suitable for classloading with + * Class.forName(). + * + * @param typeDescriptor see ASM guide section 2.1.3 + */ + private static String convertAsmTypeDescriptorToClassName(String typeDescriptor) { + final String internalName; // See ASM guide section 2.1.2 + + if ("V".equals(typeDescriptor)) + return Void.class.getName(); + if ("I".equals(typeDescriptor)) + return Integer.class.getName(); + if ("Z".equals(typeDescriptor)) + return Boolean.class.getName(); + + // strip the leading array/object/primitive identifier + if (typeDescriptor.startsWith("[[")) + internalName = typeDescriptor.substring(3); + else if (typeDescriptor.startsWith("[")) + internalName = typeDescriptor.substring(2); + else + internalName = typeDescriptor.substring(1); + + // convert slashes to dots + String className = internalName.replace('/', '.'); + + // and strip trailing semicolon (if present) + if (className.endsWith(";")) + className = className.substring(0, internalName.length() - 1); + + return className; + } + + /** + * @param methodDescriptor see ASM guide section 2.1.4 + */ + private static String getReturnTypeFromAsmMethodDescriptor(String methodDescriptor) { + String returnTypeDescriptor = methodDescriptor.substring(methodDescriptor.indexOf(')') + 1); + return convertAsmTypeDescriptorToClassName(returnTypeDescriptor); + } + } diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java index 1bb40cbbee8..83f4a3dde01 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java @@ -43,6 +43,8 @@ final class MethodMetadataReadingVisitor extends MethodAdapter implements Method private final int access; + private String returnType; + private String declaringClassName; private final ClassLoader classLoader; @@ -51,10 +53,11 @@ final class MethodMetadataReadingVisitor extends MethodAdapter implements Method private final Map> attributeMap = new LinkedHashMap>(2); - public MethodMetadataReadingVisitor(String name, int access, String declaringClassName, ClassLoader classLoader, + public MethodMetadataReadingVisitor(String name, String returnType, int access, String declaringClassName, ClassLoader classLoader, MultiValueMap methodMetadataMap) { super(new EmptyVisitor()); this.name = name; + this.returnType = returnType; this.access = access; this.declaringClassName = declaringClassName; this.classLoader = classLoader; @@ -72,6 +75,10 @@ final class MethodMetadataReadingVisitor extends MethodAdapter implements Method return this.name; } + public String getMethodReturnType() { + return this.returnType; + } + public boolean isStatic() { return ((this.access & Opcodes.ACC_STATIC) != 0); } diff --git a/org.springframework.core/src/main/java/org/springframework/util/ObjectUtils.java b/org.springframework.core/src/main/java/org/springframework/util/ObjectUtils.java index c590dfa806d..16a59cbbad7 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/org.springframework.core/src/main/java/org/springframework/util/ObjectUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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. @@ -32,6 +32,7 @@ import java.util.Arrays; * @author Keith Donald * @author Rod Johnson * @author Rob Harrop + * @author Chris Beams * @since 19.03.2004 * @see org.apache.commons.lang.ObjectUtils */ @@ -121,6 +122,54 @@ public abstract class ObjectUtils { return false; } + /** + * Check whether the given array of enum constants contains a constant with the given name, + * ignoring case when determining a match. + * @param enumValues the enum values to check, typically the product of a call to MyEnum.values() + * @param constant the constant name to find (must not be null or empty string) + * @return whether the constant has been found in the given array + */ + public static boolean containsConstant(Enum[] enumValues, String constant) { + return containsConstant(enumValues, constant, false); + } + + /** + * Check whether the given array of enum constants contains a constant with the given name. + * @param enumValues the enum values to check, typically the product of a call to MyEnum.values() + * @param constant the constant name to find (must not be null or empty string) + * @param caseSensitive whether case is significant in determining a match + * @return whether the constant has been found in the given array + */ + public static boolean containsConstant(Enum[] enumValues, String constant, boolean caseSensitive) { + for (Enum candidate : enumValues) { + if (caseSensitive ? + candidate.toString().equals(constant) : + candidate.toString().equalsIgnoreCase(constant)) { + return true; + } + } + return false; + } + + /** + * Case insensitive alternative to {@link Enum#valueOf(Class, String)}. + * @param the concrete Enum type + * @param enumValues the array of all Enum constants in question, usually per Enum.values() + * @param constant the constant to get the enum value of + * @throws IllegalArgumentException if the given constant is not found in the given array + * of enum values. Use {@link #containsConstant(Enum[], String)} as a guard to avoid this exception. + */ + public static > E caseInsensitiveValueOf(E[] enumValues, String constant) { + for (E candidate : enumValues) { + if(candidate.toString().equalsIgnoreCase(constant)) { + return candidate; + } + } + throw new IllegalArgumentException( + String.format("constant [%s] does not exist in enum type %s", + constant, enumValues.getClass().getComponentType().getName())); + } + /** * Append the given object to the given array, returning a new array * consisting of the input array contents plus the given object. diff --git a/org.springframework.core/src/main/java/org/springframework/util/ReflectionUtils.java b/org.springframework.core/src/main/java/org/springframework/util/ReflectionUtils.java index f7ab95038a5..c9588608faa 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/org.springframework.core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -372,6 +372,20 @@ public abstract class ReflectionUtils { return (method != null && method.getName().equals("toString") && method.getParameterTypes().length == 0); } + /** + * Determine whether the given method is originally declared by {@link java.lang.Object}. + */ + public static boolean isObjectMethod(Method method) { + try { + Object.class.getDeclaredMethod(method.getName(), method.getParameterTypes()); + return true; + } catch (SecurityException ex) { + return false; + } catch (NoSuchMethodException ex) { + return false; + } + } + /** * Make the given field accessible, explicitly setting it accessible if * necessary. The setAccessible(true) method is only called diff --git a/org.springframework.core/src/test/java/org/springframework/core/convert/support/DefaultConversionTests.java b/org.springframework.core/src/test/java/org/springframework/core/convert/support/DefaultConversionTests.java index 2ac63df918d..7b7ff79d149 100644 --- a/org.springframework.core/src/test/java/org/springframework/core/convert/support/DefaultConversionTests.java +++ b/org.springframework.core/src/test/java/org/springframework/core/convert/support/DefaultConversionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,7 @@ import org.springframework.core.convert.converter.ConverterRegistry; */ public class DefaultConversionTests { - private ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + private ConversionService conversionService = new DefaultConversionService(); @Test public void testStringToCharacter() { diff --git a/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java b/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java index cbe398d945b..1ab672dd93e 100644 --- a/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java +++ b/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -78,6 +78,7 @@ public class GenericConversionServiceTests { } @Test + @SuppressWarnings("rawtypes") public void addConverterNoSourceTargetClassInfoAvailable() { try { conversionService.addConverter(new Converter() { @@ -109,7 +110,7 @@ public class GenericConversionServiceTests { } public void convertNullTargetClass() { - assertNull(conversionService.convert("3", (Class) null)); + assertNull(conversionService.convert("3", (Class) null)); assertNull(conversionService.convert("3", TypeDescriptor.NULL)); } @@ -226,7 +227,7 @@ public class GenericConversionServiceTests { @Test public void testStringArrayToResourceArray() { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new MyStringArrayToResourceArrayConverter()); Resource[] converted = conversionService.convert(new String[] {"x1", "z3"}, Resource[].class); assertEquals(2, converted.length); @@ -236,7 +237,7 @@ public class GenericConversionServiceTests { @Test public void testStringArrayToIntegerArray() { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new MyStringArrayToIntegerArrayConverter()); Integer[] converted = conversionService.convert(new String[] {"x1", "z3"}, Integer[].class); assertEquals(2, converted.length); @@ -246,7 +247,7 @@ public class GenericConversionServiceTests { @Test public void testStringToIntegerArray() { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new MyStringToIntegerArrayConverter()); Integer[] converted = conversionService.convert("x1,z3", Integer[].class); assertEquals(2, converted.length); @@ -256,7 +257,7 @@ public class GenericConversionServiceTests { @Test public void testWildcardMap() throws Exception { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); Map input = new LinkedHashMap(); input.put("key", "value"); Object converted = conversionService.convert(input, new TypeDescriptor(getClass().getField("wildcardMap"))); @@ -265,14 +266,14 @@ public class GenericConversionServiceTests { @Test public void testListOfList() { - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); List> list = Collections.singletonList(Collections.singletonList("Foo")); assertNotNull(service.convert(list, String.class)); } @Test public void testStringToString() { - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); String value = "myValue"; String result = service.convert(value, String.class); assertSame(value, result); @@ -280,7 +281,7 @@ public class GenericConversionServiceTests { @Test public void testStringToObject() { - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); String value = "myValue"; Object result = service.convert(value, Object.class); assertSame(value, result); @@ -288,7 +289,7 @@ public class GenericConversionServiceTests { @Test public void testIgnoreCopyConstructor() { - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); WithCopyConstructor value = new WithCopyConstructor(); Object result = service.convert(value, WithCopyConstructor.class); assertSame(value, result); @@ -296,7 +297,7 @@ public class GenericConversionServiceTests { @Test public void testPerformance1() { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); StopWatch watch = new StopWatch("integer->string conversionPerformance"); watch.start("convert 4,000,000 with conversion service"); for (int i = 0; i < 4000000; i++) { @@ -313,7 +314,7 @@ public class GenericConversionServiceTests { @Test public void testPerformance2() throws Exception { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); StopWatch watch = new StopWatch("list -> list conversionPerformance"); watch.start("convert 4,000,000 with conversion service"); List source = new LinkedList(); @@ -340,7 +341,7 @@ public class GenericConversionServiceTests { @Test public void testPerformance3() throws Exception { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); StopWatch watch = new StopWatch("map -> map conversionPerformance"); watch.start("convert 4,000,000 with conversion service"); Map source = new HashMap(); @@ -387,6 +388,7 @@ public class GenericConversionServiceTests { TypeDescriptor sourceType = TypeDescriptor.forObject(list); TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyListDifferentTarget")); assertTrue(conversionService.canConvert(sourceType, targetType)); + @SuppressWarnings("unchecked") LinkedList result = (LinkedList) conversionService.convert(list, sourceType, targetType); assertEquals(LinkedList.class, result.getClass()); assertTrue(result.isEmpty()); @@ -437,6 +439,7 @@ public class GenericConversionServiceTests { TypeDescriptor sourceType = TypeDescriptor.forObject(map); TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyMapDifferentTarget")); assertTrue(conversionService.canConvert(sourceType, targetType)); + @SuppressWarnings("unchecked") LinkedHashMap result = (LinkedHashMap) conversionService.convert(map, sourceType, targetType); assertEquals(map, result); assertEquals(LinkedHashMap.class, result.getClass()); diff --git a/org.springframework.core/src/test/java/org/springframework/util/ObjectUtilsTests.java b/org.springframework.core/src/test/java/org/springframework/util/ObjectUtilsTests.java index d929faa6922..fcfe1b38d40 100644 --- a/org.springframework.core/src/test/java/org/springframework/util/ObjectUtilsTests.java +++ b/org.springframework.core/src/test/java/org/springframework/util/ObjectUtilsTests.java @@ -16,6 +16,9 @@ package org.springframework.util; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + import java.io.IOException; import java.sql.SQLException; @@ -43,10 +46,10 @@ public final class ObjectUtilsTests extends TestCase { } public void testIsCompatibleWithThrowsClause() { - Class[] empty = new Class[0]; - Class[] exception = new Class[] {Exception.class}; - Class[] sqlAndIO = new Class[] {SQLException.class, IOException.class}; - Class[] throwable = new Class[] {Throwable.class}; + Class[] empty = new Class[0]; + Class[] exception = new Class[] {Exception.class}; + Class[] sqlAndIO = new Class[] {SQLException.class, IOException.class}; + Class[] throwable = new Class[] {Throwable.class}; assertTrue(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException(), null)); assertTrue(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException(), empty)); @@ -617,6 +620,35 @@ public final class ObjectUtilsTests extends TestCase { assertEquals("null", ObjectUtils.nullSafeToString((String[]) null)); } + enum Tropes { FOO, BAR, baz }; + + public void testContainsConstant() { + assertThat(ObjectUtils.containsConstant(Tropes.values(), "FOO"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "foo"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BaR"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "bar"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BAZ"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "baz"), is(true)); + + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BOGUS"), is(false)); + + assertThat(ObjectUtils.containsConstant(Tropes.values(), "FOO", true), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "foo", true), is(false)); + } + + public void testCaseInsensitiveValueOf() { + assertThat(ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "foo"), is(Tropes.FOO)); + assertThat(ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "BAR"), is(Tropes.BAR)); + try { + ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "bogus"); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), + is("constant [bogus] does not exist in enum type " + + "org.springframework.util.ObjectUtilsTests$Tropes")); + } + } + private void assertEqualHashCodes(int expected, Object array) { int actual = ObjectUtils.nullSafeHashCode(array); assertEquals(expected, actual); diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java index e6223d86365..8a5d757f410 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,7 @@ import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; @@ -45,7 +45,7 @@ public class StandardTypeConverter implements TypeConverter { public StandardTypeConverter() { synchronized (this) { if (defaultConversionService == null) { - defaultConversionService = ConversionServiceFactory.createDefaultConversionService(); + defaultConversionService = new DefaultConversionService(); } } this.conversionService = defaultConversionService; diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestsUsingCoreConversionService.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestsUsingCoreConversionService.java index 6d148d4322d..c083f29109f 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestsUsingCoreConversionService.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestsUsingCoreConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,7 +29,7 @@ import org.junit.Test; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypeConverter; @@ -139,7 +139,7 @@ public class ExpressionTestsUsingCoreConversionService extends ExpressionTestCas */ private static class TypeConvertorUsingConversionService implements TypeConverter { - private final ConversionService service = ConversionServiceFactory.createDefaultConversionService(); + private final ConversionService service = new DefaultConversionService(); public Object convertValue(Object value, TypeDescriptor typeDescriptor) throws EvaluationException { return this.service.convert(value, typeDescriptor); diff --git a/org.springframework.integration-tests/src/test/java/org/springframework/context/annotation/FeatureTestSuite.java b/org.springframework.integration-tests/src/test/java/org/springframework/context/annotation/FeatureTestSuite.java new file mode 100644 index 00000000000..51bed0fac12 --- /dev/null +++ b/org.springframework.integration-tests/src/test/java/org/springframework/context/annotation/FeatureTestSuite.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2011 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.context.annotation; + + +/** + * Tests directly or indirectly related to {@link FeatureConfiguration} class and + * {@link Feature} method processing. + * + * @author Chris Beams + * @since 3.1 + */ +/* + * commented due to classpath visibility differences between Eclipse + * and Ant/Ivy at the command line. Eclipse can see classes across + * project test folders, Ant/Ivy are not configured to do so. Uncomment + * as necessary when doing @Feature-related work. + * +@RunWith(Suite.class) +@SuiteClasses({ + EarlyBeanReferenceProxyCreatorTests.class, + SimpleFeatureMethodProcessingTests.class, + BeanFactoryAwareFeatureConfigurationTests.class, + FeatureMethodBeanReferenceTests.class, + FeatureMethodQualifiedBeanReferenceTests.class, + FeatureConfigurationClassTests.class, + FeatureMethodEarlyBeanProxyTests.class, + FeatureConfigurationImportTests.class, + FeatureConfigurationImportResourceTests.class, + + // context:component-scan related + ComponentScanFeatureTests.class, + ComponentScanSpecTests.class, + ComponentScanAnnotationTests.class, + ComponentScanAnnotationIntegrationTests.class, + + // tx-related + TxAnnotationDrivenFeatureTests.class, + TxNamespaceHandlerTests.class, + AnnotationTransactionNamespaceHandlerTests.class, + AnnotationDrivenTests.class, + + // mvc-related + AnnotationDrivenBeanDefinitionParserTests.class, + MvcAnnotationDrivenFeatureTests.class, + MvcViewControllersTests.class, + MvcResourcesTests.class, + MvcDefaultServletHandlerTests.class, + MvcNamespaceTests.class, +}) + */ +public class FeatureTestSuite { + +} \ No newline at end of file diff --git a/org.springframework.integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java b/org.springframework.integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java index ae91a4055f1..922b39b709d 100644 --- a/org.springframework.integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java +++ b/org.springframework.integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java @@ -1,4 +1,21 @@ +/* + * Copyright 2002-2011 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.expression.spel.support; + import java.beans.PropertyEditor; import org.springframework.beans.BeansException; @@ -8,7 +25,7 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.TypeConverter; /** @@ -26,7 +43,7 @@ class BeanFactoryTypeConverter implements TypeConverter, BeanFactoryAware { public BeanFactoryTypeConverter() { synchronized (this) { if (defaultConversionService == null) { - defaultConversionService = ConversionServiceFactory.createDefaultConversionService(); + defaultConversionService = new DefaultConversionService(); } } this.conversionService = defaultConversionService; diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java index dd28db09596..cce36984602 100644 --- a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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,24 +16,18 @@ package org.springframework.transaction.config; -import org.w3c.dom.Element; - import org.springframework.aop.config.AopNamespaceUtils; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.parsing.CompositeComponentDefinition; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; -import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; -import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.context.config.ExecutorContext; +import org.w3c.dom.Element; /** - * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} - * implementation that allows users to easily configure all the infrastructure - * beans required to enable annotation-driven transaction demarcation. + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser + * BeanDefinitionParser} implementation that allows users to easily configure + * all the infrastructure beans required to enable annotation-driven transaction + * demarcation. * *

By default, all proxies are created as JDK proxies. This may cause some * problems if you are injecting objects as concrete classes rather than @@ -43,7 +37,9 @@ import org.springframework.transaction.interceptor.TransactionInterceptor; * * @author Juergen Hoeller * @author Rob Harrop + * @author Chris Beams * @since 2.0 + * @see TxAnnotationDriven */ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { @@ -51,95 +47,43 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { * The bean name of the internally managed transaction advisor (mode="proxy"). */ public static final String TRANSACTION_ADVISOR_BEAN_NAME = - "org.springframework.transaction.config.internalTransactionAdvisor"; + TxAnnotationDrivenExecutor.TRANSACTION_ADVISOR_BEAN_NAME; /** * The bean name of the internally managed transaction aspect (mode="aspectj"). */ public static final String TRANSACTION_ASPECT_BEAN_NAME = - "org.springframework.transaction.config.internalTransactionAspect"; - - private static final String TRANSACTION_ASPECT_CLASS_NAME = - "org.springframework.transaction.aspectj.AnnotationTransactionAspect"; + TxAnnotationDrivenExecutor.TRANSACTION_ASPECT_BEAN_NAME; /** - * Parses the '<tx:annotation-driven/>' tag. Will + * Parses the {@code } tag. Will * {@link AopNamespaceUtils#registerAutoProxyCreatorIfNecessary register an AutoProxyCreator} * with the container as necessary. */ public BeanDefinition parse(Element element, ParserContext parserContext) { - String mode = element.getAttribute("mode"); - if ("aspectj".equals(mode)) { - // mode="aspectj" - registerTransactionAspect(element, parserContext); - } - else { - // mode="proxy" - AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); - } + new TxAnnotationDriven(element.getAttribute("transaction-manager")) + .order(element.getAttribute("order")) + .mode(element.getAttribute("mode")) + .proxyTargetClass(Boolean.valueOf(element.getAttribute("proxy-target-class"))) + .source(parserContext.extractSource(element)) + .sourceName(element.getTagName()) + .execute(createExecutorContext(parserContext)); return null; } - private void registerTransactionAspect(Element element, ParserContext parserContext) { - if (!parserContext.getRegistry().containsBeanDefinition(TRANSACTION_ASPECT_BEAN_NAME)) { - RootBeanDefinition def = new RootBeanDefinition(); - def.setBeanClassName(TRANSACTION_ASPECT_CLASS_NAME); - def.setFactoryMethodName("aspectOf"); - registerTransactionManager(element, def); - parserContext.registerBeanComponent(new BeanComponentDefinition(def, TRANSACTION_ASPECT_BEAN_NAME)); - } - } - - private static void registerTransactionManager(Element element, BeanDefinition def) { - def.getPropertyValues().add("transactionManagerBeanName", - TxNamespaceHandler.getTransactionManagerName(element)); - } - - /** - * Inner class to just introduce an AOP framework dependency when actually in proxy mode. + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util */ - private static class AopAutoProxyConfigurer { - - public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { - AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); - - if (!parserContext.getRegistry().containsBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME)) { - Object eleSource = parserContext.extractSource(element); - - // Create the TransactionAttributeSource definition. - RootBeanDefinition sourceDef = new RootBeanDefinition(AnnotationTransactionAttributeSource.class); - sourceDef.setSource(eleSource); - sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); - - // Create the TransactionInterceptor definition. - RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); - interceptorDef.setSource(eleSource); - interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registerTransactionManager(element, interceptorDef); - interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); - String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); - - // Create the TransactionAttributeSourceAdvisor definition. - RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); - advisorDef.setSource(eleSource); - advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); - advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); - if (element.hasAttribute("order")) { - advisorDef.getPropertyValues().add("order", element.getAttribute("order")); - } - parserContext.getRegistry().registerBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME, advisorDef); - - CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); - compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); - compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); - compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, TRANSACTION_ADVISOR_BEAN_NAME)); - parserContext.registerComponent(compositeDef); - } - } + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/JtaTransactionManagerBeanDefinitionParser.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/JtaTransactionManagerBeanDefinitionParser.java index f90ca8b6b77..ba0a7eab788 100644 --- a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/JtaTransactionManagerBeanDefinitionParser.java +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/JtaTransactionManagerBeanDefinitionParser.java @@ -74,7 +74,7 @@ public class JtaTransactionManagerBeanDefinitionParser extends AbstractSingleBea @Override protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { - return TxNamespaceHandler.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; + return TxAnnotationDriven.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; } } diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAdviceBeanDefinitionParser.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAdviceBeanDefinitionParser.java index 9c2bea94a0c..a8a88ca5993 100644 --- a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAdviceBeanDefinitionParser.java +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAdviceBeanDefinitionParser.java @@ -37,50 +37,55 @@ import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; /** - * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} - * for the <tx:advice> tag. + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser + * BeanDefinitionParser} for the {@code } tag. * * @author Rob Harrop * @author Juergen Hoeller * @author Adrian Colyer + * @author Chris Beams * @since 2.0 */ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { - private static final String ATTRIBUTES = "attributes"; + private static final String TRANSACTION_MANAGER_ATTRIBUTE = "transaction-manager"; - private static final String TIMEOUT = "timeout"; + private static final String METHOD_ELEMENT = "method"; - private static final String READ_ONLY = "read-only"; + private static final String METHOD_NAME_ATTRIBUTE = "name"; - private static final String NAME_MAP = "nameMap"; + private static final String ATTRIBUTES_ELEMENT = "attributes"; - private static final String PROPAGATION = "propagation"; + private static final String TIMEOUT_ATTRIBUTE = "timeout"; - private static final String ISOLATION = "isolation"; + private static final String READ_ONLY_ATTRIBUTE = "read-only"; - private static final String ROLLBACK_FOR = "rollback-for"; + private static final String PROPAGATION_ATTRIBUTE = "propagation"; - private static final String NO_ROLLBACK_FOR = "no-rollback-for"; + private static final String ISOLATION_ATTRIBUTE = "isolation"; + + private static final String ROLLBACK_FOR_ATTRIBUTE = "rollback-for"; + + private static final String NO_ROLLBACK_FOR_ATTRIBUTE = "no-rollback-for"; @Override - protected Class getBeanClass(Element element) { + protected Class getBeanClass(Element element) { return TransactionInterceptor.class; } @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { - builder.addPropertyReference("transactionManager", TxNamespaceHandler.getTransactionManagerName(element)); + builder.addPropertyReference("transactionManager", element.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE)); - List txAttributes = DomUtils.getChildElementsByTagName(element, ATTRIBUTES); + List txAttributes = DomUtils.getChildElementsByTagName(element, ATTRIBUTES_ELEMENT); if (txAttributes.size() > 1) { parserContext.getReaderContext().error( "Element is allowed at most once inside element ", element); } else if (txAttributes.size() == 1) { // Using attributes source. - Element attributeSourceElement = (Element) txAttributes.get(0); + Element attributeSourceElement = txAttributes.get(0); RootBeanDefinition attributeSourceDefinition = parseAttributeSource(attributeSourceElement, parserContext); builder.addPropertyValue("transactionAttributeSource", attributeSourceDefinition); } @@ -92,20 +97,21 @@ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { } private RootBeanDefinition parseAttributeSource(Element attrEle, ParserContext parserContext) { - List methods = DomUtils.getChildElementsByTagName(attrEle, "method"); - ManagedMap transactionAttributeMap = new ManagedMap(methods.size()); + List methods = DomUtils.getChildElementsByTagName(attrEle, METHOD_ELEMENT); + ManagedMap transactionAttributeMap = + new ManagedMap(methods.size()); transactionAttributeMap.setSource(parserContext.extractSource(attrEle)); for (Element methodEle : methods) { - String name = methodEle.getAttribute("name"); + String name = methodEle.getAttribute(METHOD_NAME_ATTRIBUTE); TypedStringValue nameHolder = new TypedStringValue(name); nameHolder.setSource(parserContext.extractSource(methodEle)); RuleBasedTransactionAttribute attribute = new RuleBasedTransactionAttribute(); - String propagation = methodEle.getAttribute(PROPAGATION); - String isolation = methodEle.getAttribute(ISOLATION); - String timeout = methodEle.getAttribute(TIMEOUT); - String readOnly = methodEle.getAttribute(READ_ONLY); + String propagation = methodEle.getAttribute(PROPAGATION_ATTRIBUTE); + String isolation = methodEle.getAttribute(ISOLATION_ATTRIBUTE); + String timeout = methodEle.getAttribute(TIMEOUT_ATTRIBUTE); + String readOnly = methodEle.getAttribute(READ_ONLY_ATTRIBUTE); if (StringUtils.hasText(propagation)) { attribute.setPropagationBehaviorName(RuleBasedTransactionAttribute.PREFIX_PROPAGATION + propagation); } @@ -121,16 +127,16 @@ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { } } if (StringUtils.hasText(readOnly)) { - attribute.setReadOnly(Boolean.valueOf(methodEle.getAttribute(READ_ONLY))); + attribute.setReadOnly(Boolean.valueOf(methodEle.getAttribute(READ_ONLY_ATTRIBUTE))); } List rollbackRules = new LinkedList(); - if (methodEle.hasAttribute(ROLLBACK_FOR)) { - String rollbackForValue = methodEle.getAttribute(ROLLBACK_FOR); + if (methodEle.hasAttribute(ROLLBACK_FOR_ATTRIBUTE)) { + String rollbackForValue = methodEle.getAttribute(ROLLBACK_FOR_ATTRIBUTE); addRollbackRuleAttributesTo(rollbackRules,rollbackForValue); } - if (methodEle.hasAttribute(NO_ROLLBACK_FOR)) { - String noRollbackForValue = methodEle.getAttribute(NO_ROLLBACK_FOR); + if (methodEle.hasAttribute(NO_ROLLBACK_FOR_ATTRIBUTE)) { + String noRollbackForValue = methodEle.getAttribute(NO_ROLLBACK_FOR_ATTRIBUTE); addNoRollbackRuleAttributesTo(rollbackRules,noRollbackForValue); } attribute.setRollbackRules(rollbackRules); @@ -140,7 +146,7 @@ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { RootBeanDefinition attributeSourceDefinition = new RootBeanDefinition(NameMatchTransactionAttributeSource.class); attributeSourceDefinition.setSource(parserContext.extractSource(attrEle)); - attributeSourceDefinition.getPropertyValues().add(NAME_MAP, transactionAttributeMap); + attributeSourceDefinition.getPropertyValues().add("nameMap", transactionAttributeMap); return attributeSourceDefinition; } diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDriven.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDriven.java new file mode 100644 index 00000000000..299ed4854b4 --- /dev/null +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDriven.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2011 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.transaction.config; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.AdviceMode; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public final class TxAnnotationDriven extends AbstractFeatureSpecification { + + static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; + + private static final Class EXECUTOR_TYPE = TxAnnotationDrivenExecutor.class; + + private Object txManager = null; + + private Object order = null; + + private Boolean proxyTargetClass = false; + + private Object mode = AdviceMode.PROXY; + + /** + * Create a {@code TxAnnotationDriven} specification assumes the presence of a + * {@link PlatformTransactionManager} bean named {@value #DEFAULT_TRANSACTION_MANAGER_BEAN_NAME}. + * + *

See the alternate constructors defined here if your transaction manager does + * not follow this default naming or you wish to refer to it by bean instance rather + * than by bean name. + * @see #TxAnnotationDriven(String) + * @see #TxAnnotationDriven(PlatformTransactionManager) + */ + public TxAnnotationDriven() { + this(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME); + } + + /** + * Create a new {@code TxAnnotationDriven} specification that will use the specified + * transaction manager bean name. + * + * @param txManagerBeanName name of {@link PlatformTransactionManager} bean or a + * ${placeholder} or SpEL #{expression} resolving to bean name. If {@code null}, + * falls back to default value of {@value #DEFAULT_TRANSACTION_MANAGER_BEAN_NAME}. + */ + public TxAnnotationDriven(String txManagerBeanName) { + super(EXECUTOR_TYPE); + this.txManager = txManagerBeanName != null ? + txManagerBeanName : + DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; + } + + /** + * Create a new TxAnnotationDriven specification that will use the specified transaction + * manager. + * + * @param txManager the {@link PlatformTransactionManager} bean to use. Must not be {@code null}. + */ + public TxAnnotationDriven(PlatformTransactionManager txManager) { + super(EXECUTOR_TYPE); + Assert.notNull(txManager, "transaction manager must not be null"); + this.txManager = txManager; + } + + /** + * Return the transaction manager to use. May be a {@link PlatformTransactionManager} + * instance or a String representing the bean name or a placeholder or SpEL expression + * that resolves to the bean name. + */ + Object transactionManager() { + return this.txManager; + } + + /** + * Indicate how transactional advice should be applied. + * @see AdviceMode + */ + public TxAnnotationDriven mode(AdviceMode mode) { + this.mode = mode; + return this; + } + + /** + * Indicate how transactional advice should be applied. + * @param name matching one of the labels in the AdviceMode enum; + * placeholder and SpEL expressions are not allowed. + * @see AdviceMode + */ + TxAnnotationDriven mode(String mode) { + if (StringUtils.hasText(mode)) { + this.mode = mode; + } + return this; + } + + /** + * Return how transactional advice should be applied. + */ + AdviceMode mode() { + if (this.mode instanceof AdviceMode) { + return (AdviceMode)this.mode; + } + if (this.mode instanceof String) { + return ObjectUtils.caseInsensitiveValueOf(AdviceMode.values(), (String)this.mode); + } + // TODO SPR-7420: deal with in validate & raise problem + throw new IllegalStateException( + "invalid type for field 'mode' (must be of type AdviceMode or String): " + + this.mode.getClass().getName()); + } + + /** + * Indicate whether class-based (CGLIB) proxies are to be created as opposed + * to standard Java interface-based proxies. + * + *

Note: Class-based proxies require the {@link Transactional @Transactional} + * annotation to be defined on the concrete class. Annotations in interfaces will + * not work in that case (they will rather only work with interface-based proxies)! + */ + public TxAnnotationDriven proxyTargetClass(Boolean proxyTargetClass) { + this.proxyTargetClass = proxyTargetClass; + return this; + } + + /** + * Return whether class-based (CGLIB) proxies are to be created as opposed + * to standard Java interface-based proxies. + */ + Boolean proxyTargetClass() { + return this.proxyTargetClass; + } + + /** + * Indicate the ordering of the execution of the transaction advisor + * when multiple advice executes at a specific joinpoint. The default is + * {@code null}, indicating that default ordering should be used. + */ + public TxAnnotationDriven order(int order) { + this.order = order; + return this; + } + + /** + * Indicate the ordering of the execution of the transaction advisor + * when multiple advice executes at a specific joinpoint. The default is + * {@code null}, indicating that default ordering should be used. + */ + public TxAnnotationDriven order(String order) { + if (StringUtils.hasText(order)) { + this.order = order; + } + return this; + } + + /** + * Return the ordering of the execution of the transaction advisor + * when multiple advice executes at a specific joinpoint. May return + * {@code null}, indicating that default ordering should be used. + */ + Object order() { + return this.order; + } + + @Override + protected void doValidate(SimpleProblemCollector problems) { + if (this.mode instanceof String) { + if (!ObjectUtils.containsConstant(AdviceMode.values(), (String)this.mode)) { + problems.error("no such mode name: " + this.mode); + } + } + } + +} \ No newline at end of file diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDrivenExecutor.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDrivenExecutor.java new file mode 100644 index 00000000000..cf971c0c672 --- /dev/null +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDrivenExecutor.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2011 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.transaction.config; + +import org.springframework.aop.config.AopNamespaceUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.util.Assert; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +final class TxAnnotationDrivenExecutor extends AbstractSpecificationExecutor { + + /** + * The bean name of the internally managed transaction advisor (used when mode == PROXY). + */ + public static final String TRANSACTION_ADVISOR_BEAN_NAME = + "org.springframework.transaction.config.internalTransactionAdvisor"; + + /** + * The bean name of the internally managed transaction aspect (used when mode == ASPECTJ). + */ + public static final String TRANSACTION_ASPECT_BEAN_NAME = + "org.springframework.transaction.config.internalTransactionAspect"; + + private static final String TRANSACTION_ASPECT_CLASS_NAME = + "org.springframework.transaction.aspectj.AnnotationTransactionAspect"; + + + @Override + protected void doExecute(TxAnnotationDriven txSpec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ComponentRegistrar registrar = executorContext.getRegistrar(); + switch (txSpec.mode()) { + case ASPECTJ: + registerTransactionAspect(txSpec, registry, registrar); + break; + case PROXY: + AopAutoProxyConfigurer.configureAutoProxyCreator(txSpec, registry, registrar); + break; + default: + throw new IllegalArgumentException( + String.format("AdviceMode %s is not supported", txSpec.mode())); + } + } + + private void registerTransactionAspect(TxAnnotationDriven spec, BeanDefinitionRegistry registry, ComponentRegistrar registrar) { + if (!registry.containsBeanDefinition(TRANSACTION_ASPECT_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(TRANSACTION_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + registerTransactionManager(spec, def); + registrar.registerBeanComponent(new BeanComponentDefinition(def, TRANSACTION_ASPECT_BEAN_NAME)); + } + } + + private static void registerTransactionManager(TxAnnotationDriven spec, BeanDefinition def) { + Object txManager = spec.transactionManager(); + Assert.notNull(txManager, "transactionManager must be specified"); + if (txManager instanceof String) { + def.getPropertyValues().add("transactionManagerBeanName", txManager); + } else { + def.getPropertyValues().add("transactionManager", txManager); + } + } + + /** + * Inner class to just introduce an AOP framework dependency when actually in proxy mode. + */ + private static class AopAutoProxyConfigurer { + + public static void configureAutoProxyCreator(TxAnnotationDriven txSpec, BeanDefinitionRegistry registry, ComponentRegistrar registrar) { + Object source = txSpec.source(); + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(registry, registrar, source, txSpec.proxyTargetClass()); + + if (!registry.containsBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME)) { + + // Create the TransactionAttributeSource definition. + RootBeanDefinition sourceDef = new RootBeanDefinition(AnnotationTransactionAttributeSource.class); + sourceDef.setSource(source); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String sourceName = registrar.registerWithGeneratedName(sourceDef); + + // Create the TransactionInterceptor definition. + RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); + interceptorDef.setSource(source); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registerTransactionManager(txSpec, interceptorDef); + interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + String interceptorName = registrar.registerWithGeneratedName(interceptorDef); + + // Create the TransactionAttributeSourceAdvisor definition. + RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); + advisorDef.setSource(source); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (txSpec.order() != null) { + advisorDef.getPropertyValues().add("order", txSpec.order()); + } + registry.registerBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME, advisorDef); + + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(txSpec.sourceName(), source); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, TRANSACTION_ADVISOR_BEAN_NAME)); + registrar.registerComponent(compositeDef); + } + } + } +} diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxNamespaceHandler.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxNamespaceHandler.java index ac657898304..bdf376f2473 100644 --- a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxNamespaceHandler.java +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxNamespaceHandler.java @@ -39,17 +39,6 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport; */ public class TxNamespaceHandler extends NamespaceHandlerSupport { - static final String TRANSACTION_MANAGER_ATTRIBUTE = "transaction-manager"; - - static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; - - - static String getTransactionManagerName(Element element) { - return (element.hasAttribute(TRANSACTION_MANAGER_ATTRIBUTE) ? - element.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE) : DEFAULT_TRANSACTION_MANAGER_BEAN_NAME); - } - - public void init() { registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); diff --git a/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java b/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java index 86921c7c57e..2b813663196 100644 --- a/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java +++ b/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java @@ -19,6 +19,7 @@ package org.springframework.transaction.annotation; import java.lang.management.ManagementFactory; import java.util.Collection; import java.util.Map; + import javax.management.MBeanServer; import javax.management.ObjectName; diff --git a/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/TxAnnotationDrivenFeatureTests.java b/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/TxAnnotationDrivenFeatureTests.java new file mode 100644 index 00000000000..7071426d260 --- /dev/null +++ b/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/TxAnnotationDrivenFeatureTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2011 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.transaction.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.context.config.AdviceMode; +import org.springframework.stereotype.Service; +import org.springframework.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.AnnotationTransactionNamespaceHandlerTests.TransactionalTestBean; +import org.springframework.transaction.config.TxAnnotationDriven; + +/** + * Integration tests for {@link TxAnnotationDriven} support within @Configuration + * classes. Adapted from original tx: namespace tests at + * {@link AnnotationTransactionNamespaceHandlerTests}. + * + * @author Chris Beams + * @since 3.1 + */ +public class TxAnnotationDrivenFeatureTests { + @Test + public void transactionProxyIsCreated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TxFeature.class, TxManagerConfig.class); + ctx.refresh(); + TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class); + assertThat("testBean is not a proxy", AopUtils.isAopProxy(bean), is(true)); + Map services = ctx.getBeansWithAnnotation(Service.class); + assertThat("Stereotype annotation not visible", services.containsKey("testBean"), is(true)); + } + + @Test + public void txManagerIsResolvedOnInvocationOfTransactionalMethod() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TxFeature.class, TxManagerConfig.class); + ctx.refresh(); + TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class); + + // invoke a transactional method, causing the PlatformTransactionManager bean to be resolved. + bean.findAllFoos(); + } + + @Test + public void txManagerIsResolvedCorrectlyWhenMultipleManagersArePresent() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TxFeature.class, MultiTxManagerConfig.class); + ctx.refresh(); + TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class); + + // invoke a transactional method, causing the PlatformTransactionManager bean to be resolved. + bean.findAllFoos(); + } + + /** + * A cheap test just to prove that in ASPECTJ mode, the AnnotationTransactionAspect does indeed + * get loaded -- or in this case, attempted to be loaded at which point the test fails. + */ + @Test + public void proxyTypeAspectJCausesRegistrationOfAnnotationTransactionAspect() { + try { + new AnnotationConfigApplicationContext(TxWithAspectJFeature.class, TxManagerConfig.class); + fail("should have thrown CNFE when trying to load AnnotationTransactionAspect. " + + "Do you actually have org.springframework.aspects on the classpath?"); + } catch (CannotLoadBeanClassException ex) { + ClassNotFoundException cause = (ClassNotFoundException) ex.getCause(); + assertThat(cause.getMessage(), equalTo("org.springframework.transaction.aspectj.AnnotationTransactionAspect")); + } + } + +} + +@FeatureConfiguration +class TxFeature { + + @Feature + public TxAnnotationDriven tx(TxManagerConfig txManagerConfig) { + return new TxAnnotationDriven(txManagerConfig.txManager()); + } +} + + +@FeatureConfiguration +class TxWithAspectJFeature { + + @Feature + public TxAnnotationDriven tx(PlatformTransactionManager txManager) { + return new TxAnnotationDriven(txManager).mode(AdviceMode.ASPECTJ); + } + +} + + +@Configuration +class TxManagerConfig { + + @Bean + public TransactionalTestBean testBean() { + return new TransactionalTestBean(); + } + + @Bean + public PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + +} + + +@Configuration +class MultiTxManagerConfig extends TxManagerConfig { + + @Bean + public PlatformTransactionManager txManager2() { + return new CallCountingTransactionManager(); + } + +} + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java deleted file mode 100644 index 13f10cf673b..00000000000 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2002-2010 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.web.servlet.config; - -import org.w3c.dom.Element; - -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.beans.factory.xml.BeanDefinitionParser; -import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; - -/** - * Abstract base class for {@link BeanDefinitonParser}s that register an HttpRequestHandler. - * - * @author Jeremy Grelle - * @since 3.0.4 - */ -abstract class AbstractHttpRequestHandlerBeanDefinitionParser implements BeanDefinitionParser{ - - private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"; - - public BeanDefinition parse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - registerHandlerAdapterIfNecessary(parserContext, source); - doParse(element, parserContext); - return null; - } - - public abstract void doParse(Element element, ParserContext parserContext); - - private void registerHandlerAdapterIfNecessary(ParserContext parserContext, Object source) { - if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { - RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(HttpRequestHandlerAdapter.class); - handlerAdapterDef.setSource(source); - handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - parserContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); - } - } - -} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index 9965b8c9e10..6a9ea3af459 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -18,264 +18,86 @@ package org.springframework.web.servlet.config; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.ManagedList; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.core.convert.ConversionService; -import org.springframework.format.support.FormattingConversionServiceFactoryBean; -import org.springframework.http.converter.ByteArrayHttpMessageConverter; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.ResourceHttpMessageConverter; -import org.springframework.http.converter.StringHttpMessageConverter; -import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; -import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; -import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; -import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; -import org.springframework.http.converter.xml.SourceHttpMessageConverter; -import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; -import org.springframework.util.ClassUtils; +import org.springframework.context.config.ExecutorContext; import org.springframework.util.xml.DomUtils; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; -import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; -import org.springframework.web.servlet.handler.MappedInterceptor; -import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; -import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver; -import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; -import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; -import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; import org.w3c.dom.Element; /** - * {@link BeanDefinitionParser} that parses the {@code annotation-driven} element to configure a Spring MVC web - * application. + * {@link BeanDefinitionParser} that parses the {@code annotation-driven} element + * to configure a Spring MVC web application. * - *

Responsible for: - *

    - *
  1. Registering a DefaultAnnotationHandlerMapping bean for mapping HTTP Servlet Requests to @Controller methods - * using @RequestMapping annotations. - *
  2. Registering a AnnotationMethodHandlerAdapter bean for invoking annotated @Controller methods. - * Will configure the HandlerAdapter's webBindingInitializer property for centrally configuring - * {@code @Controller} {@code DataBinder} instances: - *
      - *
    • Configures the conversionService if specified, otherwise defaults to a fresh {@link ConversionService} instance - * created by the default {@link FormattingConversionServiceFactoryBean}. - *
    • Configures the validator if specified, otherwise defaults to a fresh {@link Validator} instance created by the - * default {@link LocalValidatorFactoryBean} if the JSR-303 API is present on the classpath. - *
    • Configures standard {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters}, - * including the {@link Jaxb2RootElementHttpMessageConverter} if JAXB2 is present on the classpath, and - * the {@link MappingJacksonHttpMessageConverter} if Jackson is present on the classpath. - *
    - *
- * - * @author Keith Donald - * @author Juergen Hoeller - * @author Arjen Poutsma * @author Rossen Stoyanchev * @since 3.0 + * @see MvcAnnotationDriven + * @see MvcAnnotationDrivenExecutor */ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { - private static final boolean jsr303Present = ClassUtils.isPresent( - "javax.validation.Validator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); - - private static final boolean jaxb2Present = - ClassUtils.isPresent("javax.xml.bind.Binder", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); - - private static final boolean jacksonPresent = - ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) && - ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); - - private static boolean romePresent = - ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); - - + /** + * Parses the {@code } tag. + */ public BeanDefinition parse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - - CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source); - parserContext.pushContainingComponent(compDefinition); - - RootBeanDefinition annMappingDef = new RootBeanDefinition(DefaultAnnotationHandlerMapping.class); - annMappingDef.setSource(source); - annMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - annMappingDef.getPropertyValues().add("order", 0); - String annMappingName = parserContext.getReaderContext().registerWithGeneratedName(annMappingDef); - - RuntimeBeanReference conversionService = getConversionService(element, source, parserContext); - RuntimeBeanReference validator = getValidator(element, source, parserContext); - RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element, source, parserContext); - - RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); - bindingDef.setSource(source); - bindingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - bindingDef.getPropertyValues().add("conversionService", conversionService); - bindingDef.getPropertyValues().add("validator", validator); - bindingDef.getPropertyValues().add("messageCodesResolver", messageCodesResolver); - - ManagedList messageConverters = getMessageConverters(element, source, parserContext); - ManagedList argumentResolvers = getArgumentResolvers(element, source, parserContext); - - RootBeanDefinition annAdapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); - annAdapterDef.setSource(source); - annAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - annAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef); - annAdapterDef.getPropertyValues().add("messageConverters", messageConverters); - if (argumentResolvers != null) { - annAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers); - } - String annAdapterName = parserContext.getReaderContext().registerWithGeneratedName(annAdapterDef); - - RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class); - csInterceptorDef.setSource(source); - csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService); - RootBeanDefinition mappedCsInterceptorDef = new RootBeanDefinition(MappedInterceptor.class); - mappedCsInterceptorDef.setSource(source); - mappedCsInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null); - mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef); - String mappedInterceptorName = parserContext.getReaderContext().registerWithGeneratedName(mappedCsInterceptorDef); - - RootBeanDefinition annExceptionResolver = new RootBeanDefinition(AnnotationMethodHandlerExceptionResolver.class); - annExceptionResolver.setSource(source); - annExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - annExceptionResolver.getPropertyValues().add("messageConverters", messageConverters); - annExceptionResolver.getPropertyValues().add("order", 0); - String annExceptionResolverName = - parserContext.getReaderContext().registerWithGeneratedName(annExceptionResolver); - - RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class); - responseStatusExceptionResolver.setSource(source); - responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - responseStatusExceptionResolver.getPropertyValues().add("order", 1); - String responseStatusExceptionResolverName = - parserContext.getReaderContext().registerWithGeneratedName(responseStatusExceptionResolver); - - RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class); - defaultExceptionResolver.setSource(source); - defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - defaultExceptionResolver.getPropertyValues().add("order", 2); - String defaultExceptionResolverName = - parserContext.getReaderContext().registerWithGeneratedName(defaultExceptionResolver); - - parserContext.registerComponent(new BeanComponentDefinition(annMappingDef, annMappingName)); - parserContext.registerComponent(new BeanComponentDefinition(annAdapterDef, annAdapterName)); - parserContext.registerComponent(new BeanComponentDefinition(annExceptionResolver, annExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName)); - parserContext.popAndRegisterContainingComponent(); - + MvcAnnotationDriven spec = createSpecification(element, parserContext); + spec.execute(createExecutorContext(parserContext)); return null; } - - private RuntimeBeanReference getConversionService(Element element, Object source, ParserContext parserContext) { - RuntimeBeanReference conversionServiceRef; + + private MvcAnnotationDriven createSpecification(Element element, ParserContext parserContext) { + MvcAnnotationDriven spec = new MvcAnnotationDriven(); if (element.hasAttribute("conversion-service")) { - conversionServiceRef = new RuntimeBeanReference(element.getAttribute("conversion-service")); + String conversionService = element.getAttribute("conversion-service"); + spec.conversionService(conversionService); } - else { - RootBeanDefinition conversionDef = new RootBeanDefinition(FormattingConversionServiceFactoryBean.class); - conversionDef.setSource(source); - conversionDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - String conversionName = parserContext.getReaderContext().registerWithGeneratedName(conversionDef); - parserContext.registerComponent(new BeanComponentDefinition(conversionDef, conversionName)); - conversionServiceRef = new RuntimeBeanReference(conversionName); - } - return conversionServiceRef; - } - - private RuntimeBeanReference getValidator(Element element, Object source, ParserContext parserContext) { if (element.hasAttribute("validator")) { - return new RuntimeBeanReference(element.getAttribute("validator")); + spec.validator(element.getAttribute("validator")); } - else if (jsr303Present) { - RootBeanDefinition validatorDef = new RootBeanDefinition(LocalValidatorFactoryBean.class); - validatorDef.setSource(source); - validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - String validatorName = parserContext.getReaderContext().registerWithGeneratedName(validatorDef); - parserContext.registerComponent(new BeanComponentDefinition(validatorDef, validatorName)); - return new RuntimeBeanReference(validatorName); - } - else { - return null; - } - } - - private RuntimeBeanReference getMessageCodesResolver(Element element, Object source, ParserContext parserContext) { if (element.hasAttribute("message-codes-resolver")) { - return new RuntimeBeanReference(element.getAttribute("message-codes-resolver")); - } else { - return null; + spec.messageCodesResolver(element.getAttribute("message-codes-resolver")); } - } - - private ManagedList getArgumentResolvers(Element element, Object source, ParserContext parserContext) { - Element resolversElement = DomUtils.getChildElementByTagName(element, "argument-resolvers"); - if (resolversElement != null) { - ManagedList argumentResolvers = new ManagedList(); - argumentResolvers.setSource(source); - for (Element resolver : DomUtils.getChildElementsByTagName(resolversElement, "bean")) { - BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(resolver); - beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(resolver, beanDef); - argumentResolvers.add(beanDef); - } - return argumentResolvers; - } - return null; - } - - private ManagedList getMessageConverters(Element element, Object source, ParserContext parserContext) { Element convertersElement = DomUtils.getChildElementByTagName(element, "message-converters"); if (convertersElement != null) { - ManagedList messageConverters = new ManagedList(); - messageConverters.setSource(source); - for (Element converter : DomUtils.getChildElementsByTagName(convertersElement, "bean")) { - BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(converter); - beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(converter, beanDef); - messageConverters.add(beanDef); + if (convertersElement.hasAttribute("register-defaults")) { + spec.shouldRegisterDefaultMessageConverters(Boolean.valueOf(convertersElement + .getAttribute("register-defaults"))); } - return messageConverters; - } else { - ManagedList messageConverters = new ManagedList(); - messageConverters.setSource(source); - messageConverters.add(createConverterBeanDefinition(ByteArrayHttpMessageConverter.class, source)); - - RootBeanDefinition stringConverterDef = createConverterBeanDefinition(StringHttpMessageConverter.class, - source); - stringConverterDef.getPropertyValues().add("writeAcceptCharset", false); - messageConverters.add(stringConverterDef); - - messageConverters.add(createConverterBeanDefinition(ResourceHttpMessageConverter.class, source)); - messageConverters.add(createConverterBeanDefinition(SourceHttpMessageConverter.class, source)); - messageConverters.add(createConverterBeanDefinition(XmlAwareFormHttpMessageConverter.class, source)); - if (jaxb2Present) { - messageConverters - .add(createConverterBeanDefinition(Jaxb2RootElementHttpMessageConverter.class, source)); - } - if (jacksonPresent) { - messageConverters.add(createConverterBeanDefinition(MappingJacksonHttpMessageConverter.class, source)); - } - if (romePresent) { - messageConverters.add(createConverterBeanDefinition(AtomFeedHttpMessageConverter.class, source)); - messageConverters.add(createConverterBeanDefinition(RssChannelHttpMessageConverter.class, source)); - } - return messageConverters; + spec.messageConverters(extractBeanSubElements(convertersElement, parserContext)); } + Element resolversElement = DomUtils.getChildElementByTagName(element, "argument-resolvers"); + if (resolversElement != null) { + spec.argumentResolvers(extractBeanSubElements(resolversElement, parserContext)); + } + + spec.source(parserContext.extractSource(element)); + spec.sourceName(element.getTagName()); + return spec; } - private RootBeanDefinition createConverterBeanDefinition(Class converterClass, - Object source) { - RootBeanDefinition beanDefinition = new RootBeanDefinition(converterClass); - beanDefinition.setSource(source); - beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - - return beanDefinition; + private ManagedList extractBeanSubElements(Element parentElement, ParserContext parserContext) { + ManagedList list = new ManagedList(); + list.setSource(parserContext.extractSource(parentElement)); + for (Element beanElement : DomUtils.getChildElementsByTagName(parentElement, "bean")) { + BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(beanElement); + beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(beanElement, beanDef); + list.add(beanDef); + } + return list; } + /** + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util + */ + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; + } + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java index fe60722234d..a0b05042d32 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java @@ -16,20 +16,15 @@ package org.springframework.web.servlet.config; -import java.util.Map; - -import org.w3c.dom.Element; - import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.support.ManagedMap; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.config.ExecutorContext; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; +import org.w3c.dom.Element; /** * {@link BeanDefinitionParser} that parses a {@code default-servlet-handler} element to @@ -37,37 +32,39 @@ import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler * {@link SimpleUrlHandlerMapping} for mapping resource requests, and a * {@link HttpRequestHandlerAdapter} if necessary. * - * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 3.0.4 */ -class DefaultServletHandlerBeanDefinitionParser extends AbstractHttpRequestHandlerBeanDefinitionParser { +class DefaultServletHandlerBeanDefinitionParser implements BeanDefinitionParser { - @Override - public void doParse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - - String defaultServletName = element.getAttribute("default-servlet-name"); - RootBeanDefinition defaultServletHandlerDef = new RootBeanDefinition(DefaultServletHttpRequestHandler.class); - defaultServletHandlerDef.setSource(source); - defaultServletHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - if (StringUtils.hasText(defaultServletName)) { - defaultServletHandlerDef.getPropertyValues().add("defaultServletName", defaultServletName); - } - String defaultServletHandlerName = parserContext.getReaderContext().generateBeanName(defaultServletHandlerDef); - parserContext.getRegistry().registerBeanDefinition(defaultServletHandlerName, defaultServletHandlerDef); - parserContext.registerComponent(new BeanComponentDefinition(defaultServletHandlerDef, defaultServletHandlerName)); - - Map urlMap = new ManagedMap(); - urlMap.put("/**", defaultServletHandlerName); - - RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); - handlerMappingDef.setSource(source); - handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - handlerMappingDef.getPropertyValues().add("urlMap", urlMap); - - String handlerMappingBeanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef); - parserContext.getRegistry().registerBeanDefinition(handlerMappingBeanName, handlerMappingDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, handlerMappingBeanName)); + /** + * Parses the {@code } tag. + */ + public BeanDefinition parse(Element element, ParserContext parserContext) { + MvcDefaultServletHandler spec = createSpecification(element, parserContext); + spec.execute(createExecutorContext(parserContext)); + return null; + } + + private MvcDefaultServletHandler createSpecification(Element element, ParserContext parserContext) { + String defaultServletHandler = element.getAttribute("default-servlet-handler"); + MvcDefaultServletHandler spec = StringUtils.hasText(defaultServletHandler) ? new MvcDefaultServletHandler( + defaultServletHandler) : new MvcDefaultServletHandler(); + return spec; + } + + /** + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util + */ + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDriven.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDriven.java new file mode 100644 index 00000000000..d975321ea82 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDriven.java @@ -0,0 +1,249 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.FormattingConversionServiceFactoryBean; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; +import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.support.WebArgumentResolver; + +/** + * Specifies the Spring MVC "annotation-driven" container feature. The + * feature provides the following fine-grained configuration: + * + *
    + *
  • {@code DefaultAnnotationHandlerMapping} bean for mapping HTTP Servlet Requests + * to {@code @Controller} methods using {@code @RequestMapping} annotations. + *
  • {@code AnnotationMethodHandlerAdapter} bean for invoking annotated + * {@code @Controller} methods. + *
  • {@code HandlerExceptionResolver} beans for invoking {@code @ExceptionHandler} + * controller methods and for mapping Spring exception to HTTP status codes. + *
+ * + *

The {@code HandlerAdapter} is further configured with the following, which apply + * globally (across controllers invoked though the {@code AnnotationMethodHandlerAdapter}): + * + *

    + *
  • {@link ConversionService} - a custom instance can be provided via + * {@link #conversionService(ConversionService)}. Otherwise it defaults to a fresh + * {@link ConversionService} instance created by the default + * {@link FormattingConversionServiceFactoryBean}. + *
  • {@link Validator} - a custom instance can be provided via + * {@link #validator(Validator)}. Otherwise it defaults to a fresh {@code Validator} + * instance created by the default {@link LocalValidatorFactoryBean} assuming + * JSR-303 API is present on the classpath. + *
  • {@code HttpMessageConverter} beans including the {@link + * Jaxb2RootElementHttpMessageConverter} assuming JAXB2 is present on the + * classpath, the {@link MappingJacksonHttpMessageConverter} assuming Jackson + * is present on the classpath, and the {@link AtomFeedHttpMessageConverter} and the + * {@link RssChannelHttpMessageConverter} converters assuming Rome is present on + * the classpath. + *
  • Optionally, custom {@code WebArgumentResolver} beans to use for resolving + * custom arguments to handler methods. These are typically implemented to detect + * special parameter types, resolving well-known argument values for them. + *
+ * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public final class MvcAnnotationDriven extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcAnnotationDrivenExecutor.class; + + private Object conversionService; + + private Object validator; + + private Object messageCodesResolver; + + private boolean shouldRegisterDefaultMessageConverters = true; + + private ManagedList messageConverters = new ManagedList(); + + private ManagedList argumentResolvers = new ManagedList(); + + /** + * Creates an MvcAnnotationDriven specification. + */ + public MvcAnnotationDriven() { + super(EXECUTOR_TYPE); + } + + /** + *

The ConversionService bean instance to use for type conversion during + * field binding. This is not required input. It only needs to be provided + * explicitly if custom converters or formatters need to be configured. + * + *

If not provided, a default FormattingConversionService is registered + * that contains converters to/from standard JDK types. In addition, full + * support for date/time formatting will be installed if the Joda Time + * library is present on the classpath. + * + * @param conversionService the ConversionService instance to use + */ + public MvcAnnotationDriven conversionService(ConversionService conversionService) { + this.conversionService = conversionService; + return this; + } + + /** + *

The ConversionService to use for type conversion during field binding. + * This is an alternative to {@link #conversionService(ConversionService)} + * allowing you to provide a bean name rather than a bean instance. + * + * @param conversionService the ConversionService bean name + */ + public MvcAnnotationDriven conversionService(String conversionService) { + this.conversionService = conversionService; + return this; + } + + Object conversionService() { + return this.conversionService; + } + + /** + * The HttpMessageConverter types to use for converting @RequestBody method + * parameters and @ResponseBody method return values. HttpMessageConverter + * registrations provided here will take precedence over HttpMessageConverter + * types registered by default. + * Also see {@link #shouldRegisterDefaultMessageConverters(boolean)} if + * default registrations are to be turned off altogether. + * + * @param converters the message converters + */ + public MvcAnnotationDriven messageConverters(HttpMessageConverter... converters) { + for (HttpMessageConverter converter : converters) { + this.messageConverters.add(converter); + } + return this; + } + + void messageConverters(ManagedList messageConverters) { + this.messageConverters = messageConverters; + } + + ManagedList messageConverters() { + return this.messageConverters; + } + + /** + * Indicates whether or not default HttpMessageConverter registrations should + * be added in addition to the ones provided via + * {@link #messageConverters(HttpMessageConverter...). + * + * @param shouldRegister true will result in registration of defaults. + */ + public MvcAnnotationDriven shouldRegisterDefaultMessageConverters(boolean shouldRegister) { + this.shouldRegisterDefaultMessageConverters = shouldRegister; + return this; + } + + boolean shouldRegisterDefaultMessageConverters() { + return this.shouldRegisterDefaultMessageConverters; + } + + public MvcAnnotationDriven argumentResolvers(WebArgumentResolver... resolvers) { + for (WebArgumentResolver resolver : resolvers) { + this.argumentResolvers.add(resolver); + } + return this; + } + + void argumentResolvers(ManagedList argumentResolvers) { + this.argumentResolvers = argumentResolvers; + } + + ManagedList argumentResolvers() { + return this.argumentResolvers; + } + + /** + * The Validator bean instance to use to validate Controller model objects. + * This is not required input. It only needs to be specified explicitly if + * a custom Validator needs to be configured. + * + *

If not specified, JSR-303 validation will be installed if a JSR-303 + * provider is present on the classpath. + * + * @param validator the Validator bean instance + */ + public MvcAnnotationDriven validator(Validator validator) { + this.validator = validator; + return this; + } + + /** + * The Validator bean instance to use to validate Controller model objects. + * This is an alternative to {@link #validator(Validator)} allowing you to + * provide a bean name rather than a bean instance. + * + * @param validator the Validator bean name + */ + public MvcAnnotationDriven validator(String validator) { + this.validator = validator; + return this; + } + + Object validator() { + return this.validator; + } + + /** + * The MessageCodesResolver to use to build message codes from data binding + * and validation error codes. This is not required input. If not specified + * the DefaultMessageCodesResolver is used. + * + * @param messageCodesResolver the MessageCodesResolver bean instance + */ + public MvcAnnotationDriven messageCodesResolver(MessageCodesResolver messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + return this; + } + + /** + * The MessageCodesResolver to use to build message codes from data binding + * and validation error codes. This is an alternative to + * {@link #messageCodesResolver(MessageCodesResolver)} allowing you to provide + * a bean name rather than a bean instance. + * + * @param messageCodesResolver the MessageCodesResolver bean name + */ + public MvcAnnotationDriven messageCodesResolver(String messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + return this; + } + + Object messageCodesResolver() { + return this.messageCodesResolver; + } + + @Override + protected void doValidate(SimpleProblemCollector reporter) { + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDrivenExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDrivenExecutor.java new file mode 100644 index 00000000000..5e6515a6792 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDrivenExecutor.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.format.support.FormattingConversionServiceFactoryBean; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.ResourceHttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; +import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; +import org.springframework.util.ClassUtils; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; +import org.springframework.web.servlet.handler.MappedInterceptor; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; +import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; +import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; + +/** + * Executes {@link MvcAnnotationDriven} specifications, creating and registering + * bean definitions as appropriate based on the configuration within. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1 + * @see MvcAnnotationDriven + */ +final class MvcAnnotationDrivenExecutor extends AbstractSpecificationExecutor { + + private static final boolean jsr303Present = ClassUtils.isPresent("javax.validation.Validator", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + private static final boolean jacksonPresent = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) + && ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + private static boolean romePresent = ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + @Override + public void doExecute(MvcAnnotationDriven spec, ExecutorContext executorContext) { + ComponentRegistrar registrar = executorContext.getRegistrar(); + Object source = spec.source(); + + RootBeanDefinition annMappingDef = new RootBeanDefinition(DefaultAnnotationHandlerMapping.class); + annMappingDef.setSource(source); + annMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + annMappingDef.getPropertyValues().add("order", 0); + String annMappingName = registrar.registerWithGeneratedName(annMappingDef); + + Object conversionService = getConversionService(spec, registrar); + Object validator = getValidator(spec, registrar); + Object messageCodesResolver = getMessageCodesResolver(spec, registrar); + + RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); + bindingDef.setSource(source); + bindingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + bindingDef.getPropertyValues().add("conversionService", conversionService); + bindingDef.getPropertyValues().add("validator", validator); + bindingDef.getPropertyValues().add("messageCodesResolver", messageCodesResolver); + + ManagedList messageConverters = getMessageConverters(spec, registrar); + + RootBeanDefinition annAdapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); + annAdapterDef.setSource(source); + annAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + annAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef); + annAdapterDef.getPropertyValues().add("messageConverters", messageConverters); + if (!spec.argumentResolvers().isEmpty()) { + annAdapterDef.getPropertyValues().add("customArgumentResolvers", spec.argumentResolvers()); + } + String annAdapterName = registrar.registerWithGeneratedName(annAdapterDef); + + RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class); + csInterceptorDef.setSource(source); + csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService); + RootBeanDefinition mappedCsInterceptorDef = new RootBeanDefinition(MappedInterceptor.class); + mappedCsInterceptorDef.setSource(source); + mappedCsInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null); + mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef); + String mappedInterceptorName = registrar.registerWithGeneratedName(mappedCsInterceptorDef); + + RootBeanDefinition annExceptionResolver = new RootBeanDefinition(AnnotationMethodHandlerExceptionResolver.class); + annExceptionResolver.setSource(source); + annExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + annExceptionResolver.getPropertyValues().add("messageConverters", messageConverters); + annExceptionResolver.getPropertyValues().add("order", 0); + String annExceptionResolverName = registrar.registerWithGeneratedName(annExceptionResolver); + + RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition( + ResponseStatusExceptionResolver.class); + responseStatusExceptionResolver.setSource(source); + responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + responseStatusExceptionResolver.getPropertyValues().add("order", 1); + String responseStatusExceptionResolverName = registrar + .registerWithGeneratedName(responseStatusExceptionResolver); + + RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class); + defaultExceptionResolver.setSource(source); + defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + defaultExceptionResolver.getPropertyValues().add("order", 2); + String defaultExceptionResolverName = registrar.registerWithGeneratedName(defaultExceptionResolver); + + CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(spec.sourceName(), source); + compDefinition.addNestedComponent(new BeanComponentDefinition(annMappingDef, annMappingName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(annAdapterDef, annAdapterName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(annExceptionResolver, annExceptionResolverName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(responseStatusExceptionResolver, + responseStatusExceptionResolverName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(defaultExceptionResolver, + defaultExceptionResolverName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName)); + registrar.registerComponent(compDefinition); + } + + private Object getConversionService(MvcAnnotationDriven spec, ComponentRegistrar registrar) { + if (spec.conversionService() != null) { + return getBeanOrReference(spec.conversionService()); + } else { + RootBeanDefinition conversionDef = new RootBeanDefinition(FormattingConversionServiceFactoryBean.class); + conversionDef.setSource(spec.source()); + conversionDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String conversionName = registrar.registerWithGeneratedName(conversionDef); + registrar.registerComponent(new BeanComponentDefinition(conversionDef, conversionName)); + return new RuntimeBeanReference(conversionName); + } + } + + private Object getValidator(MvcAnnotationDriven spec, ComponentRegistrar registrar) { + if (spec.validator() != null) { + return getBeanOrReference(spec.validator()); + } else if (jsr303Present) { + RootBeanDefinition validatorDef = new RootBeanDefinition(LocalValidatorFactoryBean.class); + validatorDef.setSource(spec.source()); + validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String validatorName = registrar.registerWithGeneratedName(validatorDef); + registrar.registerComponent(new BeanComponentDefinition(validatorDef, validatorName)); + return new RuntimeBeanReference(validatorName); + } else { + return null; + } + } + + private Object getMessageCodesResolver(MvcAnnotationDriven spec, ComponentRegistrar registrar) { + if (spec.messageCodesResolver() != null) { + return getBeanOrReference(spec.messageCodesResolver()); + } else { + return null; + } + } + + private ManagedList getMessageConverters(MvcAnnotationDriven spec, ComponentRegistrar registrar) { + ManagedList messageConverters = new ManagedList(); + Object source = spec.source(); + messageConverters.setSource(source); + messageConverters.addAll(spec.messageConverters()); + if (spec.shouldRegisterDefaultMessageConverters()) { + messageConverters.add(createConverterBeanDefinition(ByteArrayHttpMessageConverter.class, source)); + RootBeanDefinition stringConverterDef = createConverterBeanDefinition(StringHttpMessageConverter.class, + source); + stringConverterDef.getPropertyValues().add("writeAcceptCharset", false); + messageConverters.add(stringConverterDef); + messageConverters.add(createConverterBeanDefinition(ResourceHttpMessageConverter.class, source)); + messageConverters.add(createConverterBeanDefinition(SourceHttpMessageConverter.class, source)); + messageConverters.add(createConverterBeanDefinition(XmlAwareFormHttpMessageConverter.class, source)); + if (jaxb2Present) { + messageConverters + .add(createConverterBeanDefinition(Jaxb2RootElementHttpMessageConverter.class, source)); + } + if (jacksonPresent) { + messageConverters.add(createConverterBeanDefinition(MappingJacksonHttpMessageConverter.class, source)); + } + if (romePresent) { + messageConverters.add(createConverterBeanDefinition(AtomFeedHttpMessageConverter.class, source)); + messageConverters.add(createConverterBeanDefinition(RssChannelHttpMessageConverter.class, source)); + } + } + return messageConverters; + } + + @SuppressWarnings("rawtypes") + private RootBeanDefinition createConverterBeanDefinition(Class converterClass, + Object source) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(converterClass); + beanDefinition.setSource(source); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + return beanDefinition; + } + + private Object getBeanOrReference(Object bean) { + if (bean != null && bean instanceof String) { + return new RuntimeBeanReference((String) bean); + } else { + return bean; + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandler.java new file mode 100644 index 00000000000..50f96fd5148 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; + +/** + * Specifies the Spring MVC "default-servlet-handler" container feature. The + * feature provides the following fine-grained configuration: + * + *
    + *
  • {@link DefaultServletHttpRequestHandler} for serving static files by + * forwarding to the Servlet container's "default" Servlet. + *
  • {@link SimpleUrlHandlerMapping} to map the above request handler to "/**" + *
  • {@link HttpRequestHandlerAdapter} to enable the DispatcherServlet to be + * able to invoke the above request handler. + *
+ * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcDefaultServletHandler extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcDefaultServletHandlerExecutor.class; + + private String defaultServletName; + + /** + *

Creates an instance of MvcDefaultServletHandler without. + * If this constructor is used the {@link DefaultServletHttpRequestHandler} + * will try to auto-detect the container's default Servlet at startup time + * using a list of known names. + * + *

If the default Servlet cannot be detected because of using an + * unknown container or because it has been manually configured, an + * alternate constructor provided here can be used to specify the + * servlet name explicitly. + */ + public MvcDefaultServletHandler() { + super(EXECUTOR_TYPE); + } + + /** + * The name of the default Servlet to forward to for static resource requests. + * The {@link DefaultServletHttpRequestHandler} will try to auto-detect the + * container's default Servlet at startup time using a list of known names. + * However if the default Servlet cannot be detected because of using an unknown + * container or because it has been manually configured, you can use this + * constructor to set the servlet name explicitly. + * + * @param defaultServletName the name of the default servlet + */ + public MvcDefaultServletHandler(String defaultServletName) { + this(); + this.defaultServletName = defaultServletName; + } + + String defaultServletName() { + return this.defaultServletName; + } + + @Override + protected void doValidate(SimpleProblemCollector reporter) { + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerExecutor.java new file mode 100644 index 00000000000..475e3a83ef8 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerExecutor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import java.util.Map; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; + +/** + * Executes {@link MvcDefaultServletHandler} specifications, creating and + * registering bean definitions as appropriate based on the configuration + * within. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 3.1 + */ +final class MvcDefaultServletHandlerExecutor extends AbstractSpecificationExecutor { + + private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"; + + @Override + protected void doExecute(MvcDefaultServletHandler spec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ComponentRegistrar registrar = executorContext.getRegistrar(); + Object source = spec.source(); + + if (!registry.containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { + RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(HttpRequestHandlerAdapter.class); + handlerAdapterDef.setSource(source); + handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); + registrar.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); + } + + RootBeanDefinition defaultServletHandlerDef = new RootBeanDefinition(DefaultServletHttpRequestHandler.class); + defaultServletHandlerDef.setSource(source); + defaultServletHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + if (StringUtils.hasText(spec.defaultServletName())) { + defaultServletHandlerDef.getPropertyValues().add("defaultServletName", spec.defaultServletName()); + } + String defaultServletHandlerName = registrar.registerWithGeneratedName(defaultServletHandlerDef); + registry.registerBeanDefinition(defaultServletHandlerName, defaultServletHandlerDef); + registrar.registerComponent(new BeanComponentDefinition(defaultServletHandlerDef, defaultServletHandlerName)); + + Map urlMap = new ManagedMap(); + urlMap.put("/**", defaultServletHandlerName); + RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); + handlerMappingDef.setSource(source); + handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + handlerMappingDef.getPropertyValues().add("urlMap", urlMap); + String handlerMappingBeanName = registrar.registerWithGeneratedName(handlerMappingDef); + registry.registerBeanDefinition(handlerMappingBeanName, handlerMappingDef); + registrar.registerComponent(new BeanComponentDefinition(handlerMappingDef, handlerMappingBeanName)); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResources.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResources.java new file mode 100644 index 00000000000..8a1ee991894 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResources.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.core.Ordered; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; + +/** + * Specifies the Spring MVC "resources" container feature. The + * feature provides the following fine-grained configuration: + * + *

    + *
  • {@link ResourceHttpRequestHandler} to serve static resources from a + * list of web-root relative, classpath, or other locations. + *
  • {@link SimpleUrlHandlerMapping} to map the above request handler to a + * a specific path pattern (e.g. "/resources/**"). + *
  • {@link HttpRequestHandlerAdapter} to enable the DispatcherServlet to be + * able to invoke the above request handler. + *
+ * + * @author Rossen Stoynchev + * @since 3.1 + */ +public final class MvcResources extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcResourcesExecutor.class; + + private Object[] locations; + + private String mapping; + + private Object cachePeriod; + + private Object order = Ordered.LOWEST_PRECEDENCE - 1; + + /** + * Create an MvcResources specification instance. See alternate constructor + * you prefer to use {@link Resource} instances instead of {@code String}-based + * resource locations. + * + * @param mapping - the URL path pattern within the current Servlet context to + * use to identify resource requests (e.g. "/resources/**"). + * @param locations - locations of resources containing static content to be + * served. Each location must point to a valid directory. Locations will be + * checked in the order specified. For example if "/" and + * "classpath:/META-INF/public-web-resources/" are configured resources will + * be served from the Web root and from any JAR on the classpath that contains + * a /META-INF/public-web-resources/ directory, with resources under the Web root + * taking precedence. + */ + public MvcResources(String mapping, String... locations) { + super(EXECUTOR_TYPE); + this.locations = locations; + this.mapping = mapping; + } + + /** + * Create an MvcResources specification instance. See alternate constructor + * defined here if you prefer to use String-based path patterns. + * + * @param mapping - the URL path pattern within the current Servlet context to + * use to identify resource requests (e.g. "/resources/**"). + * @param resources - Spring {@link Resource} objects containing static + * content to be served. Resources will be checked in the order specified. + */ + public MvcResources(String mapping, Resource... resources) { + super(EXECUTOR_TYPE); + this.locations = resources; + this.mapping = mapping; + } + + /** + * The period of time resources should be cached for in seconds. + * The default is to not send any cache headers but rather to rely on + * last-modified timestamps only. + *

Set this to 0 in order to send cache headers that prevent caching, + * or to a positive number of seconds in order to send cache headers + * with the given max-age value. + * + * @param cachePeriod the cache period in seconds + */ + public MvcResources cachePeriod(Integer cachePeriod) { + this.cachePeriod = cachePeriod; + return this; + } + + /** + * Specify a cachePeriod as a String. An alternative to {@link #cachePeriod(Integer)}. + * The String must represent an Integer after placeholder and SpEL expression + * resolution. + * + * @param cachePeriod the cache period in seconds + */ + public MvcResources cachePeriod(String cachePeriod) { + this.cachePeriod = cachePeriod; + return this; + } + + /** + * Specify a cachePeriod as a String. An alternative to {@link #cachePeriod(Integer)}. + * The String must represent an Integer after placeholder and SpEL expression + * resolution. + *

Sets the order for the SimpleUrlHandlerMapping used to match resource + * requests relative to order value for other HandlerMapping instances + * such as the {@link DefaultAnnotationHandlerMapping} used to match + * controller requests. + * + * @param order the order to use. The default value is + * {@link Ordered#LOWEST_PRECEDENCE} - 1. + */ + public MvcResources order(Integer order) { + this.order = order; + return this; + } + + /** + * Specify an order as a String. An alternative to {@link #order(Integer)}. + * The String must represent an Integer after placeholder and SpEL expression + * resolution. + * + * @param order the order to use. The default value is + * {@link Ordered#LOWEST_PRECEDENCE} - 1. + */ + public MvcResources order(String order) { + this.order = order; + return this; + } + + // Package private accessors + + Object cachePeriod() { + return cachePeriod; + } + + Object[] locations() { + return this.locations; + } + + String mapping() { + return mapping; + } + + Object order() { + return order; + } + + @Override + protected void doValidate(SimpleProblemCollector problems) { + if (!StringUtils.hasText(mapping)) { + problems.error("Mapping is required"); + } + if (locations == null || locations.length == 0) { + problems.error("At least one location is required"); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResourcesExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResourcesExecutor.java new file mode 100644 index 00000000000..34add4cbdfb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResourcesExecutor.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import java.util.Map; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; + +/** + * Executes {@link MvcResources} specifications, creating and registering + * bean definitions as appropriate based on the configuration within. + * + * @author Keith Donald + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 3.1 + */ +final class MvcResourcesExecutor extends AbstractSpecificationExecutor { + + private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"; + + @Override + protected void doExecute(MvcResources spec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ComponentRegistrar registrar = executorContext.getRegistrar(); + Object source = spec.source(); + + if (!registry.containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { + RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(HttpRequestHandlerAdapter.class); + handlerAdapterDef.setSource(source); + handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); + registrar.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); + } + + RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class); + resourceHandlerDef.setSource(source); + resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + resourceHandlerDef.getPropertyValues().add("locations", spec.locations()); + if (spec.cachePeriod() != null) { + resourceHandlerDef.getPropertyValues().add("cacheSeconds", spec.cachePeriod()); + } + String resourceHandlerBeanName = registrar.registerWithGeneratedName(resourceHandlerDef); + registry.registerBeanDefinition(resourceHandlerBeanName, resourceHandlerDef); + registrar.registerComponent(new BeanComponentDefinition(resourceHandlerDef, resourceHandlerBeanName)); + + Map urlMap = new ManagedMap(); + urlMap.put(spec.mapping(), resourceHandlerBeanName); + RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); + handlerMappingDef.setSource(source); + handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + handlerMappingDef.getPropertyValues().add("urlMap", urlMap); + if (spec.order() != null) { + handlerMappingDef.getPropertyValues().add("order", spec.order()); + } + String beanName = registrar.registerWithGeneratedName(handlerMappingDef); + registry.registerBeanDefinition(beanName, handlerMappingDef); + registrar.registerComponent(new BeanComponentDefinition(handlerMappingDef, beanName)); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllers.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllers.java new file mode 100644 index 00000000000..5ef0b9ff049 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllers.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; + +/** + * Specifies the Spring MVC "View Controllers" container feature. The + * feature allows specifying one or more path to view name mappings. + * It sets up the following fine-grained configuration: + * + *

    + *
  • {@link ParameterizableViewController} for each path/view name pair. + *
  • {@link SimpleUrlHandlerMapping} mapping each view controller to its path. + *
  • {@link SimpleControllerHandlerAdapter} to enable the DispatcherServlet + * to invoke the view controllers. + *
+ * + * @author Rossen Stoyanchev + * @author Chris Beams + * @since 3.1 + */ +public final class MvcViewControllers extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcViewControllersExecutor.class; + + private Map mappings = new HashMap(); + + public MvcViewControllers(String path) { + this(path, null); + } + + public MvcViewControllers(String path, String viewName) { + super(EXECUTOR_TYPE); + this.mappings.put(path, viewName); + } + + public MvcViewControllers viewController(String path) { + return this.viewController(path, null); + } + + public MvcViewControllers viewController(String path, String viewName) { + this.mappings.put(path, viewName); + return this; + } + + Map mappings() { + return Collections.unmodifiableMap(mappings); + } + + @Override + protected void doValidate(SimpleProblemCollector problems) { + if (mappings.size() == 0) { + problems.error("At least one ViewController must be defined"); + } + for (String path : mappings.keySet()) { + if (!StringUtils.hasText(path)) { + problems.error("The path attribute in a ViewController is required"); + } + String viewName = mappings.get(path); + if (viewName != null && viewName.isEmpty()) { + problems.error("The view name in a ViewController may be null but not empty."); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllersExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllersExecutor.java new file mode 100644 index 00000000000..46304241ffb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllersExecutor.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import java.util.Map; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; + +/** + * Executes {@link MvcViewControllers} specification, creating and registering + * bean definitions as appropriate based on the configuration within. + * + * @author Keith Donald + * @author Christian Dupuis + * @author Rossen Stoyanchev + * @since 3.1 + */ +final class MvcViewControllersExecutor extends AbstractSpecificationExecutor { + + private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.config.viewControllerHandlerAdapter"; + + private static final String HANDLER_MAPPING_BEAN_NAME = "org.springframework.web.servlet.config.viewControllerHandlerMapping"; + + @Override + protected void doExecute(MvcViewControllers spec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ComponentRegistrar registrar = executorContext.getRegistrar(); + Object source = spec.source(); + + if (!registry.containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { + RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(SimpleControllerHandlerAdapter.class); + handlerAdapterDef.setSource(source); + handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); + registrar.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); + } + + BeanDefinition handlerMappingBeanDef = null; + if (!registry.containsBeanDefinition(HANDLER_MAPPING_BEAN_NAME)) { + RootBeanDefinition beanDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); + beanDef.setSource(source); + beanDef.getPropertyValues().add("order", "1"); + beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME, beanDef); + registrar.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_BEAN_NAME)); + handlerMappingBeanDef = beanDef; + } else { + handlerMappingBeanDef = registry.getBeanDefinition(HANDLER_MAPPING_BEAN_NAME); + } + + for (Map.Entry entry : spec.mappings().entrySet()) { + RootBeanDefinition viewControllerDef = new RootBeanDefinition(ParameterizableViewController.class); + viewControllerDef.setSource(source); + if (entry.getValue() != null) { + viewControllerDef.getPropertyValues().add("viewName", entry.getValue()); + } + if (!handlerMappingBeanDef.getPropertyValues().contains("urlMap")) { + handlerMappingBeanDef.getPropertyValues().add("urlMap", new ManagedMap()); + } + @SuppressWarnings("unchecked") + Map urlMap = (Map) handlerMappingBeanDef + .getPropertyValues().getPropertyValue("urlMap").getValue(); + urlMap.put(entry.getKey(), viewControllerDef); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index 03654ab9e5b..a054634a063 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,89 +16,72 @@ package org.springframework.web.servlet.config; -import java.util.Map; - -import org.w3c.dom.Element; - import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.support.ManagedMap; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.core.Ordered; +import org.springframework.context.config.ExecutorContext; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; -import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; -import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; +import org.w3c.dom.Element; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a - * {@code resources} element to register a {@link ResourceHttpRequestHandler}. - * Will also register a {@link SimpleUrlHandlerMapping} for mapping resource requests, - * and a {@link HttpRequestHandlerAdapter} if necessary. + * {@code resources} element. * - * @author Keith Donald - * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 3.0.4 + * @see MvcResources + * @see MvcResourcesExecutor */ -class ResourcesBeanDefinitionParser extends AbstractHttpRequestHandlerBeanDefinitionParser implements BeanDefinitionParser { +class ResourcesBeanDefinitionParser implements BeanDefinitionParser { - @Override - public void doParse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - registerResourceMappings(parserContext, element, source); + /** + * Parses the {@code } tag + */ + public BeanDefinition parse(Element element, ParserContext parserContext) { + MvcResources spec = createSpecification(element, parserContext); + if (spec != null) { + spec.execute(createExecutorContext(parserContext)); + } + return null; } - - private void registerResourceMappings(ParserContext parserContext, Element element, Object source) { - String resourceHandlerName = registerResourceHandler(parserContext, element, source); - if (resourceHandlerName == null) { - return; + + private MvcResources createSpecification(Element element, ParserContext parserContext) { + String mapping = element.getAttribute("mapping"); + if (!StringUtils.hasText(mapping)) { + parserContext.getReaderContext().error("The 'mapping' attribute is required.", + parserContext.extractSource(element)); + return null; } - - Map urlMap = new ManagedMap(); - String resourceRequestPath = element.getAttribute("mapping"); - if (!StringUtils.hasText(resourceRequestPath)) { - parserContext.getReaderContext().error("The 'mapping' attribute is required.", parserContext.extractSource(element)); - return; + String[] locations = StringUtils.commaDelimitedListToStringArray(element.getAttribute("location")); + if (locations.length == 0) { + parserContext.getReaderContext().error("The 'location' attribute is required.", + parserContext.extractSource(element)); + return null; } - urlMap.put(resourceRequestPath, resourceHandlerName); - - RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); - handlerMappingDef.setSource(source); - handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - handlerMappingDef.getPropertyValues().add("urlMap", urlMap); - - String order = element.getAttribute("order"); - // use a default of near-lowest precedence, still allowing for even lower precedence in other mappings - handlerMappingDef.getPropertyValues().add("order", StringUtils.hasText(order) ? order : Ordered.LOWEST_PRECEDENCE - 1); - - String beanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef); - parserContext.getRegistry().registerBeanDefinition(beanName, handlerMappingDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, beanName)); + MvcResources spec = new MvcResources(mapping, locations); + if (element.hasAttribute("cache-period")) { + spec.cachePeriod(element.getAttribute("cache-period")); + } + if (element.hasAttribute("order")) { + spec.order(element.getAttribute("order")); + } + spec.source(parserContext.extractSource(element)); + spec.sourceName(element.getTagName()); + return spec; } - - private String registerResourceHandler(ParserContext parserContext, Element element, Object source) { - String locationAttr = element.getAttribute("location"); - if (!StringUtils.hasText(locationAttr)) { - parserContext.getReaderContext().error("The 'location' attribute is required.", parserContext.extractSource(element)); - return null; - } - RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class); - resourceHandlerDef.setSource(source); - resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - resourceHandlerDef.getPropertyValues().add("locations", StringUtils.commaDelimitedListToStringArray(locationAttr)); - - String cacheSeconds = element.getAttribute("cache-period"); - if (StringUtils.hasText(cacheSeconds)) { - resourceHandlerDef.getPropertyValues().add("cacheSeconds", cacheSeconds); - } - - String beanName = parserContext.getReaderContext().generateBeanName(resourceHandlerDef); - parserContext.getRegistry().registerBeanDefinition(beanName, resourceHandlerDef); - parserContext.registerComponent(new BeanComponentDefinition(resourceHandlerDef, beanName)); - return beanName; + /** + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util + */ + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java index 36b985e99c3..e50fae09f4d 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java @@ -16,88 +16,49 @@ package org.springframework.web.servlet.config; -import java.util.Map; - -import org.w3c.dom.Element; - import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.support.ManagedMap; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.context.config.ExecutorContext; import org.springframework.web.servlet.mvc.ParameterizableViewController; -import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; +import org.w3c.dom.Element; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a * {@code view-controller} element to register a {@link ParameterizableViewController}. - * Will also register a {@link SimpleUrlHandlerMapping} for view controllers. * - * @author Keith Donald - * @author Christian Dupuis + * @author Rossen Stoyanchev * @since 3.0 + * @see MvcViewControllers + * @see MvcViewControllersExecutor */ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser { - private static final String HANDLER_ADAPTER_BEAN_NAME = - "org.springframework.web.servlet.config.viewControllerHandlerAdapter"; - - private static final String HANDLER_MAPPING_BEAN_NAME = - "org.springframework.web.servlet.config.viewControllerHandlerMapping"; - - + /** + * Parses the {@code } tag. + */ public BeanDefinition parse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - - // Register handler adapter - registerHanderAdapter(parserContext, source); - - // Register handler mapping - BeanDefinition handlerMappingDef = registerHandlerMapping(parserContext, source); - - // Create view controller bean definition - RootBeanDefinition viewControllerDef = new RootBeanDefinition(ParameterizableViewController.class); - viewControllerDef.setSource(source); - if (element.hasAttribute("view-name")) { - viewControllerDef.getPropertyValues().add("viewName", element.getAttribute("view-name")); - } - Map urlMap; - if (handlerMappingDef.getPropertyValues().contains("urlMap")) { - urlMap = (Map) handlerMappingDef.getPropertyValues().getPropertyValue("urlMap").getValue(); - } - else { - urlMap = new ManagedMap(); - handlerMappingDef.getPropertyValues().add("urlMap", urlMap); - } - urlMap.put(element.getAttribute("path"), viewControllerDef); + String path = element.getAttribute("path"); + String viewName = element.getAttribute("view-name"); + new MvcViewControllers(path, viewName.isEmpty() ? null : viewName) + .source(parserContext.extractSource(element)) + .sourceName(element.getTagName()) + .execute(createExecutorContext(parserContext)); return null; } - - private void registerHanderAdapter(ParserContext parserContext, Object source) { - if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { - RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(SimpleControllerHandlerAdapter.class); - handlerAdapterDef.setSource(source); - handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - parserContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); - } - } - - private BeanDefinition registerHandlerMapping(ParserContext parserContext, Object source) { - if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_MAPPING_BEAN_NAME)) { - RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); - handlerMappingDef.setSource(source); - handlerMappingDef.getPropertyValues().add("order", "1"); - handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - parserContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME, handlerMappingDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME)); - return handlerMappingDef; - } - else { - return parserContext.getRegistry().getBeanDefinition(HANDLER_MAPPING_BEAN_NAME); - } + + /** + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util + */ + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd b/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd index 7a53516a12e..b788c933144 100644 --- a/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd +++ b/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd @@ -23,7 +23,8 @@ @@ -36,6 +37,13 @@ + + + + + diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java index 11f60904e8c..1c2be54e33a 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -64,8 +64,15 @@ public class AnnotationDrivenBeanDefinitionParserTests { @Test public void testMessageConverters() { loadBeanDefinitions("mvc-config-message-converters.xml"); - verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerAdapter.class)); - verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerExceptionResolver.class)); + verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerAdapter.class), true); + verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerExceptionResolver.class), true); + } + + @Test + public void testMessageConvertersWithoutDefaultRegistrations() { + loadBeanDefinitions("mvc-config-message-converters-defaults-off.xml"); + verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerAdapter.class), false); + verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerExceptionResolver.class), false); } @Test @@ -88,12 +95,18 @@ public class AnnotationDrivenBeanDefinitionParserTests { appContext.refresh(); } - private void verifyMessageConverters(Object bean) { + private void verifyMessageConverters(Object bean, boolean hasDefaultRegistrations) { assertNotNull(bean); Object converters = new DirectFieldAccessor(bean).getPropertyValue("messageConverters"); assertNotNull(converters); assertTrue(converters instanceof HttpMessageConverter[]); - assertEquals(2, ((HttpMessageConverter[]) converters).length); + if (hasDefaultRegistrations) { + assertTrue("Default converters are registered in addition to custom ones", + ((HttpMessageConverter[]) converters).length > 2); + } else { + assertTrue("Default converters should not be registered", + ((HttpMessageConverter[]) converters).length == 2); + } assertTrue(((HttpMessageConverter[]) converters)[0] instanceof StringHttpMessageConverter); assertTrue(((HttpMessageConverter[]) converters)[1] instanceof ResourceHttpMessageConverter); } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcAnnotationDrivenFeatureTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcAnnotationDrivenFeatureTests.java new file mode 100644 index 00000000000..a1f3d633cca --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcAnnotationDrivenFeatureTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; + +/** + * Integration tests for the {@link MvcAnnotationDriven} feature specification. + * @author Rossen Stoyanchev + * @author Chris Beams + * @since 3.1 + */ +public class MvcAnnotationDrivenFeatureTests { + + @Test + public void testMessageCodesResolver() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcFeature.class, MvcBeans.class); + ctx.refresh(); + AnnotationMethodHandlerAdapter adapter = ctx.getBean(AnnotationMethodHandlerAdapter.class); + assertNotNull(adapter); + Object initializer = new DirectFieldAccessor(adapter).getPropertyValue("webBindingInitializer"); + assertNotNull(initializer); + MessageCodesResolver resolver = ((ConfigurableWebBindingInitializer) initializer).getMessageCodesResolver(); + assertNotNull(resolver); + assertEquals("test.foo.bar", resolver.resolveMessageCodes("foo", "bar")[0]); + Object argResolvers = new DirectFieldAccessor(adapter).getPropertyValue("customArgumentResolvers"); + assertNotNull(argResolvers); + WebArgumentResolver[] argResolversArray = (WebArgumentResolver[]) argResolvers; + assertEquals(1, argResolversArray.length); + assertTrue(argResolversArray[0] instanceof TestWebArgumentResolver); + Object converters = new DirectFieldAccessor(adapter).getPropertyValue("messageConverters"); + assertNotNull(converters); + HttpMessageConverter[] convertersArray = (HttpMessageConverter[]) converters; + assertTrue("Default converters are registered in addition to the custom one", convertersArray.length > 1); + assertTrue(convertersArray[0] instanceof StringHttpMessageConverter); + } + +} + +@FeatureConfiguration +class MvcFeature { + @Feature + public MvcAnnotationDriven annotationDriven(MvcBeans mvcBeans) { + return new MvcAnnotationDriven() + .conversionService(mvcBeans.conversionService()) + .messageCodesResolver(mvcBeans.messageCodesResolver()) + .validator(mvcBeans.validator()) + .messageConverters(new StringHttpMessageConverter()) + .argumentResolvers(new TestWebArgumentResolver()); + } +} + +@Configuration +class MvcBeans { + @Bean + public FormattingConversionService conversionService() { + return new DefaultFormattingConversionService(); + } + @Bean + public Validator validator() { + return new LocalValidatorFactoryBean(); + } + @Bean MessageCodesResolver messageCodesResolver() { + return new TestMessageCodesResolver(); + } +} + diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerTests.java new file mode 100644 index 00000000000..74317c6bb88 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; + +/** + * Test fixture for {@link MvcDefaultServletHandler} feature specification. + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcDefaultServletHandlerTests { + + @Test + public void testDefaultServletHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcDefaultServletHandlerFeature.class); + ctx.refresh(); + HttpRequestHandlerAdapter adapter = ctx.getBean(HttpRequestHandlerAdapter.class); + assertNotNull(adapter); + DefaultServletHttpRequestHandler handler = ctx.getBean(DefaultServletHttpRequestHandler.class); + assertNotNull(handler); + String defaultServletHandlerName = (String) new DirectFieldAccessor(handler) + .getPropertyValue("defaultServletName"); + assertEquals("foo", defaultServletHandlerName); + } + + @FeatureConfiguration + private static class MvcDefaultServletHandlerFeature { + + @SuppressWarnings("unused") + @Feature + public MvcDefaultServletHandler defaultServletHandler() { + return new MvcDefaultServletHandler("foo"); + } + + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcResourcesTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcResourcesTests.java new file mode 100644 index 00000000000..bf71da49afa --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcResourcesTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.List; + +import org.junit.Test; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.core.io.Resource; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; + +/** + * Test fixture for {@link MvcResources} feature specification. + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcResourcesTests { + + @Test + public void testResources() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcResourcesFeature.class); + ctx.refresh(); + HttpRequestHandlerAdapter adapter = ctx.getBean(HttpRequestHandlerAdapter.class); + assertNotNull(adapter); + ResourceHttpRequestHandler handler = ctx.getBean(ResourceHttpRequestHandler.class); + assertNotNull(handler); + @SuppressWarnings("unchecked") + List locations = (List) new DirectFieldAccessor(handler).getPropertyValue("locations"); + assertNotNull(locations); + assertEquals(2, locations.size()); + assertEquals("foo", locations.get(0).getFilename()); + assertEquals("bar", locations.get(1).getFilename()); + SimpleUrlHandlerMapping mapping = ctx.getBean(SimpleUrlHandlerMapping.class); + assertEquals(1, mapping.getOrder()); + assertSame(handler, mapping.getHandlerMap().get("/resources/**")); + } + + @Test + public void testInvalidResources() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(InvalidMvcResourcesFeature.class); + try { + ctx.refresh(); + fail("Invalid feature spec should not validate"); + } catch (RuntimeException e) { + assertTrue(e.getCause().getMessage().contains("Mapping is required")); + // TODO : should all problems be in the message ? + } + } + + @FeatureConfiguration + private static class MvcResourcesFeature { + + @SuppressWarnings("unused") + @Feature + public MvcResources resources() { + return new MvcResources("/resources/**", new String[] { "/foo", "/bar" }).cachePeriod(86400).order(1); + } + + } + + @FeatureConfiguration + private static class InvalidMvcResourcesFeature { + + @SuppressWarnings("unused") + @Feature + public MvcResources resources() { + return new MvcResources(" ", new String[] {}); + } + + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcViewControllersTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcViewControllersTests.java new file mode 100644 index 00000000000..fdb27d71557 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcViewControllersTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; + +/** + * Test fixture for {@link MvcViewControllers} feature specification. + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcViewControllersTests { + + @Test + public void testMvcViewControllers() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcViewControllersFeature.class); + ctx.refresh(); + SimpleControllerHandlerAdapter adapter = ctx.getBean(SimpleControllerHandlerAdapter.class); + assertNotNull(adapter); + SimpleUrlHandlerMapping handler = ctx.getBean(SimpleUrlHandlerMapping.class); + assertNotNull(handler); + Map urlMap = handler.getUrlMap(); + assertNotNull(urlMap); + assertEquals(2, urlMap.size()); + ParameterizableViewController controller = (ParameterizableViewController) urlMap.get("/"); + assertNotNull(controller); + assertEquals("home", controller.getViewName()); + controller = (ParameterizableViewController) urlMap.get("/account"); + assertNotNull(controller); + assertNull(controller.getViewName()); + } + + @Test + public void testEmptyPath() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EmptyPathViewControllersFeature.class); + try { + ctx.refresh(); + fail("expected exception"); + } catch (Exception ex) { + assertTrue(ex.getCause().getMessage().contains("path attribute")); + } + } + + @Test + public void testEmptyViewName() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EmptyViewNameViewControllersFeature.class); + try { + ctx.refresh(); + fail("expected exception"); + } catch (Exception ex) { + assertTrue(ex.getCause().getMessage().contains("not empty")); + } + } + + @Test + public void testNullViewName() { + FailFastProblemReporter problemReporter = new FailFastProblemReporter(); + assertThat(new MvcViewControllers("/some/path").validate(problemReporter), is(true)); + } + + + @FeatureConfiguration + private static class MvcViewControllersFeature { + + @SuppressWarnings("unused") + @Feature + public MvcViewControllers mvcViewControllers() { + return new MvcViewControllers("/", "home").viewController("/account"); + } + + } + + + @FeatureConfiguration + private static class EmptyViewNameViewControllersFeature { + + @SuppressWarnings("unused") + @Feature + public MvcViewControllers mvcViewControllers() { + return new MvcViewControllers("/some/path", ""); + } + + } + + + @FeatureConfiguration + private static class EmptyPathViewControllersFeature { + + @SuppressWarnings("unused") + @Feature + public MvcViewControllers mvcViewControllers() { + return new MvcViewControllers("", "someViewName"); + } + + } + +} + diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7766Tests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7766Tests.java index 117f5d70b0c..93a18beb330 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7766Tests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7766Tests.java @@ -1,10 +1,26 @@ +/* + * Copyright 2002-2011 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.web.servlet.mvc.annotation; import java.awt.Color; import org.junit.Test; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -16,7 +32,7 @@ public class Spr7766Tests { public void test() throws Exception { AnnotationMethodHandlerAdapter adapter = new AnnotationMethodHandlerAdapter(); ConfigurableWebBindingInitializer binder = new ConfigurableWebBindingInitializer(); - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); service.addConverter(new ColorConverter()); binder.setConversionService(service); adapter.setWebBindingInitializer(binder); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7839Tests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7839Tests.java index 5ed4d0b3f29..26012a43790 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7839Tests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7839Tests.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2011 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.web.servlet.mvc.annotation; import static org.junit.Assert.assertEquals; @@ -9,7 +25,7 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -30,7 +46,7 @@ public class Spr7839Tests { @Before public void setUp() { ConfigurableWebBindingInitializer binder = new ConfigurableWebBindingInitializer(); - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); service.addConverter(new Converter() { public NestedBean convert(String source) { return new NestedBean(source); diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-message-converters-defaults-off.xml b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-message-converters-defaults-off.xml new file mode 100644 index 00000000000..22344c79966 --- /dev/null +++ b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-message-converters-defaults-off.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java b/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java index dab8d8ed78a..ffd07d479f2 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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,12 @@ package org.springframework.http; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; @@ -23,11 +29,9 @@ import java.util.Comparator; import java.util.List; import java.util.Random; -import static org.junit.Assert.*; import org.junit.Test; - import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; /** * @author Arjen Poutsma @@ -490,7 +494,7 @@ public class MediaTypeTests { @Test public void testWithConversionService() { - ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + ConversionService conversionService = new DefaultConversionService(); assertTrue(conversionService.canConvert(String.class, MediaType.class)); MediaType mediaType = MediaType.parseMediaType("application/xml"); assertEquals(mediaType, conversionService.convert("application/xml", MediaType.class)); diff --git a/spring-framework-reference/src/mvc.xml b/spring-framework-reference/src/mvc.xml index c49e1ef2248..c9e983ce74e 100644 --- a/spring-framework-reference/src/mvc.xml +++ b/spring-framework-reference/src/mvc.xml @@ -3368,8 +3368,10 @@ public class SimpleController { - This list of HttpMessageConverters used can be replaced through - the mvc:message-converters sub-element of mvc:annotation-driven. + You can provide your own HttpMessageConverters through the + mvc:message-converters sub-element of mvc:annotation-driven. + Message converters you provide will take precedence over the + ones registered by default.