diff options
Diffstat (limited to 'spring-context/src/main/java')
100 files changed, 2170 insertions, 921 deletions
diff --git a/spring-context/src/main/java/org/springframework/cache/Cache.java b/spring-context/src/main/java/org/springframework/cache/Cache.java index 3e8a7978..ebd26020 100644 --- a/spring-context/src/main/java/org/springframework/cache/Cache.java +++ b/spring-context/src/main/java/org/springframework/cache/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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,8 @@ package org.springframework.cache; +import java.util.concurrent.Callable; + /** * Interface that defines common cache operations. * @@ -25,6 +27,7 @@ package org.springframework.cache; * * @author Costin Leau * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.1 */ public interface Cache { @@ -35,7 +38,7 @@ public interface Cache { String getName(); /** - * Return the the underlying native cache provider. + * Return the underlying native cache provider. */ Object getNativeCache(); @@ -74,6 +77,23 @@ public interface Cache { <T> T get(Object key, Class<T> type); /** + * Return the value to which this cache maps the specified key, obtaining + * that value from {@code valueLoader} if necessary. This method provides + * a simple substitute for the conventional "if cached, return; otherwise + * create, cache and return" pattern. + * <p>If possible, implementations should ensure that the loading operation + * is synchronized so that the specified {@code valueLoader} is only called + * once in case of concurrent access on the same key. + * <p>If the {@code valueLoader} throws an exception, it is wrapped in + * a {@link ValueRetrievalException} + * @param key the key whose associated value is to be returned + * @return the value to which this cache maps the specified key + * @throws ValueRetrievalException if the {@code valueLoader} throws an exception + * @since 4.3 + */ + <T> T get(Object key, Callable<T> valueLoader); + + /** * Associate the specified value with the specified key in this cache. * <p>If the cache previously contained a mapping for this key, the old * value is replaced by the specified value. @@ -133,4 +153,25 @@ public interface Cache { Object get(); } + + /** + * Wrapper exception to be thrown from {@link #get(Object, Callable)} + * in case of the value loader callback failing with an exception. + * @since 4.3 + */ + @SuppressWarnings("serial") + class ValueRetrievalException extends RuntimeException { + + private final Object key; + + public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) { + super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex); + this.key = key; + } + + public Object getKey() { + return this.key; + } + } + } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java index a2d0c2f3..f12d2119 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,9 @@ import org.springframework.core.annotation.AliasFor; * Annotation indicating that a method (or all methods on a class) triggers a * {@link org.springframework.cache.Cache#evict(Object) cache evict} operation. * + * <p>This annotation may be used as a <em>meta-annotation</em> to create custom + * <em>composed annotations</em> with attribute overrides. + * * @author Costin Leau * @author Stephane Nicoll * @author Sam Brannen @@ -62,18 +65,18 @@ public @interface CacheEvict { * Spring Expression Language (SpEL) expression for computing the key dynamically. * <p>Default is {@code ""}, meaning all method parameters are considered as a key, * unless a custom {@link #keyGenerator} has been set. - * <p>The SpEL expression evaluates again a dedicated context that provides the + * <p>The SpEL expression evaluates against a dedicated context that provides the * following meta-data: * <ul> * <li>{@code #result} for a reference to the result of the method invocation, which * can only be used if {@link #beforeInvocation()} is {@code false}.</li> - * <li>{@code #root.method}, {@code #root.target} and {@code #root.caches} for a - * reference to the {@link java.lang.reflect.Method method}, target object and + * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.</li> * <li>Shortcuts for the method name ({@code #root.methodName}) and target class * ({@code #root.targetClass}) are also available. * <li>Method arguments can be accessed by index. For instance the second argument - * can be access via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments * can also be accessed by name if that information is available.</li> * </ul> */ @@ -108,16 +111,16 @@ public @interface CacheEvict { * Spring Expression Language (SpEL) expression used for making the cache * eviction operation conditional. * <p>Default is {@code ""}, meaning the cache eviction is always performed. - * <p>The SpEL expression evaluates again a dedicated context that provides the + * <p>The SpEL expression evaluates against a dedicated context that provides the * following meta-data: * <ul> - * <li>{@code #root.method}, {@code #root.target} and {@code #root.caches} for a - * reference to the {@link java.lang.reflect.Method method}, target object and + * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.</li> * <li>Shortcuts for the method name ({@code #root.methodName}) and target class * ({@code #root.targetClass}) are also available. * <li>Method arguments can be accessed by index. For instance the second argument - * can be access via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments * can also be accessed by name if that information is available.</li> * </ul> */ diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java index bcea5494..cd48e410 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -33,6 +33,9 @@ import org.springframework.core.annotation.AliasFor; * does not cause the advised method to be skipped. Rather, it always causes the * method to be invoked and its result to be stored in the associated cache. * + * <p>This annotation may be used as a <em>meta-annotation</em> to create custom + * <em>composed annotations</em> with attribute overrides. + * * @author Costin Leau * @author Phillip Webb * @author Stephane Nicoll @@ -67,17 +70,17 @@ public @interface CachePut { * Spring Expression Language (SpEL) expression for computing the key dynamically. * <p>Default is {@code ""}, meaning all method parameters are considered as a key, * unless a custom {@link #keyGenerator} has been set. - * <p>The SpEL expression evaluates again a dedicated context that provides the + * <p>The SpEL expression evaluates against a dedicated context that provides the * following meta-data: * <ul> * <li>{@code #result} for a reference to the result of the method invocation.</li> - * <li>{@code #root.method}, {@code #root.target} and {@code #root.caches} for a - * reference to the {@link java.lang.reflect.Method method}, target object and + * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.</li> * <li>Shortcuts for the method name ({@code #root.methodName}) and target class * ({@code #root.targetClass}) are also available. * <li>Method arguments can be accessed by index. For instance the second argument - * can be access via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments * can also be accessed by name if that information is available.</li> * </ul> */ @@ -112,16 +115,16 @@ public @interface CachePut { * Spring Expression Language (SpEL) expression used for making the cache * put operation conditional. * <p>Default is {@code ""}, meaning the method result is always cached. - * <p>The SpEL expression evaluates again a dedicated context that provides the + * <p>The SpEL expression evaluates against a dedicated context that provides the * following meta-data: * <ul> - * <li>{@code #root.method}, {@code #root.target} and {@code #root.caches} for a - * reference to the {@link java.lang.reflect.Method method}, target object and + * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.</li> * <li>Shortcuts for the method name ({@code #root.methodName}) and target class * ({@code #root.targetClass}) are also available. * <li>Method arguments can be accessed by index. For instance the second argument - * can be access via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments * can also be accessed by name if that information is available.</li> * </ul> */ @@ -132,17 +135,17 @@ public @interface CachePut { * <p>Unlike {@link #condition}, this expression is evaluated after the method * has been called and can therefore refer to the {@code result}. * <p>Default is {@code ""}, meaning that caching is never vetoed. - * <p>The SpEL expression evaluates again a dedicated context that provides the + * <p>The SpEL expression evaluates against a dedicated context that provides the * following meta-data: * <ul> * <li>{@code #result} for a reference to the result of the method invocation.</li> - * <li>{@code #root.method}, {@code #root.target} and {@code #root.caches} for a - * reference to the {@link java.lang.reflect.Method method}, target object and + * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.</li> * <li>Shortcuts for the method name ({@code #root.methodName}) and target class * ({@code #root.targetClass}) are also available. * <li>Method arguments can be accessed by index. For instance the second argument - * can be access via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments * can also be accessed by name if that information is available.</li> * </ul> * @since 3.2 diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index a5d336a6..225ada51 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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,6 +22,7 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.concurrent.Callable; import org.springframework.core.annotation.AliasFor; @@ -39,6 +40,9 @@ import org.springframework.core.annotation.AliasFor; * <p>If no value is found in the cache for the computed key, the target method * will be invoked and the returned value stored in the associated cache. * + * <p>This annotation may be used as a <em>meta-annotation</em> to create custom + * <em>composed annotations</em> with attribute overrides. + * * @author Costin Leau * @author Phillip Webb * @author Stephane Nicoll @@ -73,16 +77,16 @@ public @interface Cacheable { * Spring Expression Language (SpEL) expression for computing the key dynamically. * <p>Default is {@code ""}, meaning all method parameters are considered as a key, * unless a custom {@link #keyGenerator} has been configured. - * <p>The SpEL expression evaluates again a dedicated context that provides the + * <p>The SpEL expression evaluates against a dedicated context that provides the * following meta-data: * <ul> - * <li>{@code #root.method}, {@code #root.target} and {@code #root.caches} for a - * reference to the {@link java.lang.reflect.Method method}, target object and + * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.</li> * <li>Shortcuts for the method name ({@code #root.methodName}) and target class * ({@code #root.targetClass}) are also available. * <li>Method arguments can be accessed by index. For instance the second argument - * can be access via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments * can also be accessed by name if that information is available.</li> * </ul> */ @@ -117,16 +121,16 @@ public @interface Cacheable { * Spring Expression Language (SpEL) expression used for making the method * caching conditional. * <p>Default is {@code ""}, meaning the method result is always cached. - * <p>The SpEL expression evaluates again a dedicated context that provides the + * <p>The SpEL expression evaluates against a dedicated context that provides the * following meta-data: * <ul> - * <li>{@code #root.method}, {@code #root.target} and {@code #root.caches} for a - * reference to the {@link java.lang.reflect.Method method}, target object and + * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.</li> * <li>Shortcuts for the method name ({@code #root.methodName}) and target class * ({@code #root.targetClass}) are also available. * <li>Method arguments can be accessed by index. For instance the second argument - * can be access via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments * can also be accessed by name if that information is available.</li> * </ul> */ @@ -137,21 +141,38 @@ public @interface Cacheable { * <p>Unlike {@link #condition}, this expression is evaluated after the method * has been called and can therefore refer to the {@code result}. * <p>Default is {@code ""}, meaning that caching is never vetoed. - * <p>The SpEL expression evaluates again a dedicated context that provides the + * <p>The SpEL expression evaluates against a dedicated context that provides the * following meta-data: * <ul> * <li>{@code #result} for a reference to the result of the method invocation.</li> - * <li>{@code #root.method}, {@code #root.target} and {@code #root.caches} for a - * reference to the {@link java.lang.reflect.Method method}, target object and + * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.</li> * <li>Shortcuts for the method name ({@code #root.methodName}) and target class * ({@code #root.targetClass}) are also available. * <li>Method arguments can be accessed by index. For instance the second argument - * can be access via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments * can also be accessed by name if that information is available.</li> * </ul> * @since 3.2 */ String unless() default ""; + /** + * Synchronize the invocation of the underlying method if several threads are + * attempting to load a value for the same key. The synchronization leads to + * a couple of limitations: + * <ol> + * <li>{@link #unless()} is not supported</li> + * <li>Only one cache may be specified</li> + * <li>No other cache-related operation can be combined</li> + * </ol> + * This is effectively a hint and the actual cache provider that you are + * using may not support it in a synchronized fashion. Check your provider + * documentation for more details on the actual semantics. + * @since 4.3 + * @see org.springframework.cache.Cache#get(Object, Callable) + */ + boolean sync() default false; + } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Caching.java b/spring-context/src/main/java/org/springframework/cache/annotation/Caching.java index 6802fd5d..08a4482f 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Caching.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Caching.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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. @@ -26,6 +26,9 @@ import java.lang.annotation.Target; /** * Group annotation for multiple cache annotations (of different or the same type). * + * <p>This annotation may be used as a <em>meta-annotation</em> to create custom + * <em>composed annotations</em> with attribute overrides. + * * @author Costin Leau * @author Chris Beams * @since 3.1 diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java index f99b40d7..f7f6fa4e 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -52,7 +52,7 @@ public class CachingConfigurationSelector extends AdviceModeImportSelector<Enabl private static final boolean jsr107Present = ClassUtils.isPresent( "javax.cache.Cache", CachingConfigurationSelector.class.getClassLoader()); - private static final boolean jCacheImplPresent = ClassUtils.isPresent( + private static final boolean jcacheImplPresent = ClassUtils.isPresent( PROXY_JCACHE_CONFIGURATION_CLASS, CachingConfigurationSelector.class.getClassLoader()); @@ -81,7 +81,7 @@ public class CachingConfigurationSelector extends AdviceModeImportSelector<Enabl List<String> result = new ArrayList<String>(); result.add(AutoProxyRegistrar.class.getName()); result.add(ProxyCachingConfiguration.class.getName()); - if (jsr107Present && jCacheImplPresent) { + if (jsr107Present && jcacheImplPresent) { result.add(PROXY_JCACHE_CONFIGURATION_CLASS); } return result.toArray(new String[result.size()]); @@ -94,7 +94,7 @@ public class CachingConfigurationSelector extends AdviceModeImportSelector<Enabl private String[] getAspectJImports() { List<String> result = new ArrayList<String>(); result.add(CACHE_ASPECT_CONFIGURATION_CLASS_NAME); - if (jsr107Present && jCacheImplPresent) { + if (jsr107Present && jcacheImplPresent) { result.add(JCACHE_ASPECT_CONFIGURATION_CLASS_NAME); } return result.toArray(new String[result.size()]); diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java index 511e4de6..0ec602b9 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java @@ -47,7 +47,7 @@ import org.springframework.core.Ordered; * public CacheManager cacheManager() { * // configure and return an implementation of Spring's CacheManager SPI * SimpleCacheManager cacheManager = new SimpleCacheManager(); - * cacheManager.addCaches(Arrays.asList(new ConcurrentMapCache("default"))); + * cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default"))); * return cacheManager; * } * }</pre> @@ -99,7 +99,7 @@ import org.springframework.core.Ordered; * <p>For those that wish to establish a more direct relationship between * {@code @EnableCaching} and the exact cache manager bean to be used, * the {@link CachingConfigurer} callback interface may be implemented. - * Notice the the {@code @Override}-annotated methods below: + * Notice the {@code @Override}-annotated methods below: * * <pre class="code"> * @Configuration @@ -117,7 +117,7 @@ import org.springframework.core.Ordered; * public CacheManager cacheManager() { * // configure and return an implementation of Spring's CacheManager SPI * SimpleCacheManager cacheManager = new SimpleCacheManager(); - * cacheManager.addCaches(Arrays.asList(new ConcurrentMapCache("default"))); + * cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default"))); * return cacheManager; * } * diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java index d6992bd2..d58bd133 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java @@ -27,6 +27,7 @@ import org.springframework.cache.interceptor.CacheEvictOperation; import org.springframework.cache.interceptor.CacheOperation; import org.springframework.cache.interceptor.CachePutOperation; import org.springframework.cache.interceptor.CacheableOperation; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -61,29 +62,29 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria protected Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) { Collection<CacheOperation> ops = null; - Collection<Cacheable> cacheables = getAnnotations(ae, Cacheable.class); - if (cacheables != null) { + Collection<Cacheable> cacheables = AnnotatedElementUtils.findAllMergedAnnotations(ae, Cacheable.class); + if (!cacheables.isEmpty()) { ops = lazyInit(ops); for (Cacheable cacheable : cacheables) { ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable)); } } - Collection<CacheEvict> evicts = getAnnotations(ae, CacheEvict.class); - if (evicts != null) { + Collection<CacheEvict> evicts = AnnotatedElementUtils.findAllMergedAnnotations(ae, CacheEvict.class); + if (!evicts.isEmpty()) { ops = lazyInit(ops); for (CacheEvict evict : evicts) { ops.add(parseEvictAnnotation(ae, cachingConfig, evict)); } } - Collection<CachePut> puts = getAnnotations(ae, CachePut.class); - if (puts != null) { + Collection<CachePut> puts = AnnotatedElementUtils.findAllMergedAnnotations(ae, CachePut.class); + if (!puts.isEmpty()) { ops = lazyInit(ops); for (CachePut put : puts) { ops.add(parsePutAnnotation(ae, cachingConfig, put)); } } - Collection<Caching> cachings = getAnnotations(ae, Caching.class); - if (cachings != null) { + Collection<Caching> cachings = AnnotatedElementUtils.findAllMergedAnnotations(ae, Caching.class); + if (!cachings.isEmpty()) { ops = lazyInit(ops); for (Caching caching : cachings) { Collection<CacheOperation> cachingOps = parseCachingAnnotation(ae, cachingConfig, caching); @@ -101,55 +102,59 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria } CacheableOperation parseCacheableAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, Cacheable cacheable) { - CacheableOperation op = new CacheableOperation(); - - op.setCacheNames(cacheable.cacheNames()); - op.setCondition(cacheable.condition()); - op.setUnless(cacheable.unless()); - op.setKey(cacheable.key()); - op.setKeyGenerator(cacheable.keyGenerator()); - op.setCacheManager(cacheable.cacheManager()); - op.setCacheResolver(cacheable.cacheResolver()); - op.setName(ae.toString()); - - defaultConfig.applyDefault(op); + CacheableOperation.Builder builder = new CacheableOperation.Builder(); + + builder.setName(ae.toString()); + builder.setCacheNames(cacheable.cacheNames()); + builder.setCondition(cacheable.condition()); + builder.setUnless(cacheable.unless()); + builder.setKey(cacheable.key()); + builder.setKeyGenerator(cacheable.keyGenerator()); + builder.setCacheManager(cacheable.cacheManager()); + builder.setCacheResolver(cacheable.cacheResolver()); + builder.setSync(cacheable.sync()); + + defaultConfig.applyDefault(builder); + CacheableOperation op = builder.build(); validateCacheOperation(ae, op); return op; } CacheEvictOperation parseEvictAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, CacheEvict cacheEvict) { - CacheEvictOperation op = new CacheEvictOperation(); - - op.setCacheNames(cacheEvict.cacheNames()); - op.setCondition(cacheEvict.condition()); - op.setKey(cacheEvict.key()); - op.setKeyGenerator(cacheEvict.keyGenerator()); - op.setCacheManager(cacheEvict.cacheManager()); - op.setCacheResolver(cacheEvict.cacheResolver()); - op.setCacheWide(cacheEvict.allEntries()); - op.setBeforeInvocation(cacheEvict.beforeInvocation()); - op.setName(ae.toString()); - - defaultConfig.applyDefault(op); + CacheEvictOperation.Builder builder = new CacheEvictOperation.Builder(); + + builder.setName(ae.toString()); + builder.setCacheNames(cacheEvict.cacheNames()); + builder.setCondition(cacheEvict.condition()); + builder.setKey(cacheEvict.key()); + builder.setKeyGenerator(cacheEvict.keyGenerator()); + builder.setCacheManager(cacheEvict.cacheManager()); + builder.setCacheResolver(cacheEvict.cacheResolver()); + builder.setCacheWide(cacheEvict.allEntries()); + builder.setBeforeInvocation(cacheEvict.beforeInvocation()); + + defaultConfig.applyDefault(builder); + CacheEvictOperation op = builder.build(); validateCacheOperation(ae, op); return op; } CacheOperation parsePutAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, CachePut cachePut) { - CachePutOperation op = new CachePutOperation(); - - op.setCacheNames(cachePut.cacheNames()); - op.setCondition(cachePut.condition()); - op.setUnless(cachePut.unless()); - op.setKey(cachePut.key()); - op.setKeyGenerator(cachePut.keyGenerator()); - op.setCacheManager(cachePut.cacheManager()); - op.setCacheResolver(cachePut.cacheResolver()); - op.setName(ae.toString()); - - defaultConfig.applyDefault(op); + CachePutOperation.Builder builder = new CachePutOperation.Builder(); + + builder.setName(ae.toString()); + builder.setCacheNames(cachePut.cacheNames()); + builder.setCondition(cachePut.condition()); + builder.setUnless(cachePut.unless()); + builder.setKey(cachePut.key()); + builder.setKeyGenerator(cachePut.keyGenerator()); + builder.setCacheManager(cachePut.cacheManager()); + builder.setCacheResolver(cachePut.cacheResolver()); + + defaultConfig.applyDefault(builder); + CachePutOperation op = builder.build(); validateCacheOperation(ae, op); return op; @@ -197,26 +202,6 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria return new DefaultCacheConfig(); } - private <A extends Annotation> Collection<A> getAnnotations(AnnotatedElement ae, Class<A> annotationType) { - Collection<A> anns = new ArrayList<A>(1); - - // look at raw annotation - A ann = ae.getAnnotation(annotationType); - if (ann != null) { - anns.add(AnnotationUtils.synthesizeAnnotation(ann, ae)); - } - - // scan meta-annotations - for (Annotation metaAnn : ae.getAnnotations()) { - ann = metaAnn.annotationType().getAnnotation(annotationType); - if (ann != null) { - anns.add(AnnotationUtils.synthesizeAnnotation(ann, ae)); - } - } - - return (!anns.isEmpty() ? anns : null); - } - /** * Validates the specified {@link CacheOperation}. * <p>Throws an {@link IllegalStateException} if the state of the operation is @@ -277,26 +262,26 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria } /** - * Apply the defaults to the specified {@link CacheOperation}. - * @param operation the operation to update + * Apply the defaults to the specified {@link CacheOperation.Builder}. + * @param builder the operation builder to update */ - public void applyDefault(CacheOperation operation) { - if (operation.getCacheNames().isEmpty() && this.cacheNames != null) { - operation.setCacheNames(this.cacheNames); + public void applyDefault(CacheOperation.Builder builder) { + if (builder.getCacheNames().isEmpty() && this.cacheNames != null) { + builder.setCacheNames(this.cacheNames); } - if (!StringUtils.hasText(operation.getKey()) && !StringUtils.hasText(operation.getKeyGenerator()) && + if (!StringUtils.hasText(builder.getKey()) && !StringUtils.hasText(builder.getKeyGenerator()) && StringUtils.hasText(this.keyGenerator)) { - operation.setKeyGenerator(this.keyGenerator); + builder.setKeyGenerator(this.keyGenerator); } - if (StringUtils.hasText(operation.getCacheManager()) || StringUtils.hasText(operation.getCacheResolver())) { + if (StringUtils.hasText(builder.getCacheManager()) || StringUtils.hasText(builder.getCacheResolver())) { // One of these is set so we should not inherit anything } else if (StringUtils.hasText(this.cacheResolver)) { - operation.setCacheResolver(this.cacheResolver); + builder.setCacheResolver(this.cacheResolver); } else if (StringUtils.hasText(this.cacheManager)) { - operation.setCacheManager(this.cacheManager); + builder.setCacheManager(this.cacheManager); } } diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java index 4942e14c..df41bf5f 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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,10 +16,15 @@ package org.springframework.cache.concurrent; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springframework.cache.support.AbstractValueAdaptingCache; +import org.springframework.core.serializer.support.SerializationDelegate; import org.springframework.util.Assert; /** @@ -37,6 +42,7 @@ import org.springframework.util.Assert; * * @author Costin Leau * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.1 */ public class ConcurrentMapCache extends AbstractValueAdaptingCache { @@ -45,6 +51,8 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache { private final ConcurrentMap<Object, Object> store; + private final SerializationDelegate serialization; + /** * Create a new ConcurrentMapCache with the specified name. @@ -73,14 +81,44 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache { * (adapting them to an internal null holder value) */ public ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) { + this(name, store, allowNullValues, null); + } + + /** + * Create a new ConcurrentMapCache with the specified name and the + * given internal {@link ConcurrentMap} to use. If the + * {@link SerializationDelegate} is specified, + * {@link #isStoreByValue() store-by-value} is enabled + * @param name the name of the cache + * @param store the ConcurrentMap to use as an internal store + * @param allowNullValues whether to allow {@code null} values + * (adapting them to an internal null holder value) + * @param serialization the {@link SerializationDelegate} to use + * to serialize cache entry or {@code null} to store the reference + * @since 4.3 + */ + protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, + boolean allowNullValues, SerializationDelegate serialization) { + super(allowNullValues); Assert.notNull(name, "Name must not be null"); Assert.notNull(store, "Store must not be null"); this.name = name; this.store = store; + this.serialization = serialization; } + /** + * Return whether this cache stores a copy of each entry ({@code true}) or + * a reference ({@code false}, default). If store by value is enabled, each + * entry in the cache must be serializable. + * @since 4.3 + */ + public final boolean isStoreByValue() { + return (this.serialization != null); + } + @Override public final String getName() { return this.name; @@ -96,6 +134,30 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache { return this.store.get(key); } + @SuppressWarnings("unchecked") + @Override + public <T> T get(Object key, Callable<T> valueLoader) { + if (this.store.containsKey(key)) { + return (T) get(key).get(); + } + else { + synchronized (this.store) { + if (this.store.containsKey(key)) { + return (T) get(key).get(); + } + T value; + try { + value = valueLoader.call(); + } + catch (Exception ex) { + throw new ValueRetrievalException(key, valueLoader, ex); + } + put(key, value); + return value; + } + } + } + @Override public void put(Object key, Object value) { this.store.put(key, toStoreValue(value)); @@ -117,4 +179,59 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache { this.store.clear(); } + @Override + protected Object toStoreValue(Object userValue) { + Object storeValue = super.toStoreValue(userValue); + if (this.serialization != null) { + try { + return serializeValue(storeValue); + } + catch (Exception ex) { + throw new IllegalArgumentException("Failed to serialize cache value '" + + userValue + "'. Does it implement Serializable?", ex); + } + } + else { + return storeValue; + } + } + + private Object serializeValue(Object storeValue) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + this.serialization.serialize(storeValue, out); + return out.toByteArray(); + } + finally { + out.close(); + } + } + + @Override + protected Object fromStoreValue(Object storeValue) { + if (this.serialization != null) { + try { + return super.fromStoreValue(deserializeValue(storeValue)); + } + catch (Exception ex) { + throw new IllegalArgumentException("Failed to deserialize cache value '" + + storeValue + "'", ex); + } + } + else { + return super.fromStoreValue(storeValue); + } + + } + + private Object deserializeValue(Object storeValue) throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream((byte[]) storeValue); + try { + return this.serialization.deserialize(in); + } + finally { + in.close(); + } + } + } diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java index 14c6956b..cce957b3 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -23,8 +23,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import org.springframework.core.serializer.support.SerializationDelegate; /** * {@link CacheManager} implementation that lazily builds {@link ConcurrentMapCache} @@ -35,14 +37,16 @@ import org.springframework.cache.CacheManager; * <p>Note: This is by no means a sophisticated CacheManager; it comes with no * cache configuration options. However, it may be useful for testing or simple * caching scenarios. For advanced local caching needs, consider - * {@link org.springframework.cache.guava.GuavaCacheManager} or - * {@link org.springframework.cache.ehcache.EhCacheCacheManager}. + * {@link org.springframework.cache.jcache.JCacheCacheManager}, + * {@link org.springframework.cache.ehcache.EhCacheCacheManager}, + * {@link org.springframework.cache.caffeine.CaffeineCacheManager} or + * {@link org.springframework.cache.guava.GuavaCacheManager}. * * @author Juergen Hoeller * @since 3.1 * @see ConcurrentMapCache */ -public class ConcurrentMapCacheManager implements CacheManager { +public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware { private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16); @@ -50,6 +54,10 @@ public class ConcurrentMapCacheManager implements CacheManager { private boolean allowNullValues = true; + private boolean storeByValue = false; + + private SerializationDelegate serialization; + /** * Construct a dynamic ConcurrentMapCacheManager, @@ -98,9 +106,7 @@ public class ConcurrentMapCacheManager implements CacheManager { if (allowNullValues != this.allowNullValues) { this.allowNullValues = allowNullValues; // Need to recreate all Cache instances with the new null-value configuration... - for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) { - entry.setValue(createConcurrentMapCache(entry.getKey())); - } + recreateCaches(); } } @@ -112,6 +118,42 @@ public class ConcurrentMapCacheManager implements CacheManager { return this.allowNullValues; } + /** + * Specify whether this cache manager stores a copy of each entry ({@code true} + * or the reference ({@code false} for all of its caches. + * <p>Default is "false" so that the value itself is stored and no serializable + * contract is required on cached values. + * <p>Note: A change of the store-by-value setting will reset all existing caches, + * if any, to reconfigure them with the new store-by-value requirement. + * @since 4.3 + */ + public void setStoreByValue(boolean storeByValue) { + if (storeByValue != this.storeByValue) { + this.storeByValue = storeByValue; + // Need to recreate all Cache instances with the new store-by-value configuration... + recreateCaches(); + } + } + + /** + * Return whether this cache manager stores a copy of each entry or + * a reference for all its caches. If store by value is enabled, any + * cache entry must be serializable. + * @since 4.3 + */ + public boolean isStoreByValue() { + return this.storeByValue; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.serialization = new SerializationDelegate(classLoader); + // Need to recreate all Cache instances with new ClassLoader in store-by-value mode... + if (isStoreByValue()) { + recreateCaches(); + } + } + @Override public Collection<String> getCacheNames() { @@ -133,13 +175,22 @@ public class ConcurrentMapCacheManager implements CacheManager { return cache; } + private void recreateCaches() { + for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) { + entry.setValue(createConcurrentMapCache(entry.getKey())); + } + } + /** * Create a new ConcurrentMapCache instance for the specified cache name. * @param name the name of the cache * @return the ConcurrentMapCache (or a decorator thereof) */ protected Cache createConcurrentMapCache(String name) { - return new ConcurrentMapCache(name, isAllowNullValues()); + SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null); + return new ConcurrentMapCache(name, new ConcurrentHashMap<Object, Object>(256), + isAllowNullValues(), actualSerialization); + } } diff --git a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java index 4ea5ccad..d9f90dc2 100644 --- a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -60,11 +60,10 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser private static final String JCACHE_ASPECT_CLASS_NAME = "org.springframework.cache.aspectj.JCacheCacheAspect"; - private static final boolean jsr107Present = ClassUtils.isPresent( "javax.cache.Cache", AnnotationDrivenCacheBeanDefinitionParser.class.getClassLoader()); - private static final boolean jCacheImplPresent = ClassUtils.isPresent( + private static final boolean jcacheImplPresent = ClassUtils.isPresent( "org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource", AnnotationDrivenCacheBeanDefinitionParser.class.getClassLoader()); @@ -91,7 +90,7 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser private void registerCacheAspect(Element element, ParserContext parserContext) { SpringCachingConfigurer.registerCacheAspect(element, parserContext); - if (jsr107Present && jCacheImplPresent) { // Register JCache aspect + if (jsr107Present && jcacheImplPresent) { JCacheCachingConfigurer.registerCacheAspect(element, parserContext); } } @@ -99,7 +98,7 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser private void registerCacheAdvisor(Element element, ParserContext parserContext) { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); SpringCachingConfigurer.registerCacheAdvisor(element, parserContext); - if (jsr107Present && jCacheImplPresent) { // Register JCache advisor + if (jsr107Present && jcacheImplPresent) { JCacheCachingConfigurer.registerCacheAdvisor(element, parserContext); } } diff --git a/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java b/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java index 07bcea27..f4a626a7 100644 --- a/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java +++ b/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -107,15 +107,17 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser { String name = prop.merge(opElement, parserContext.getReaderContext()); TypedStringValue nameHolder = new TypedStringValue(name); nameHolder.setSource(parserContext.extractSource(opElement)); - CacheableOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheableOperation()); - op.setUnless(getAttributeValue(opElement, "unless", "")); + CacheableOperation.Builder builder = prop.merge(opElement, + parserContext.getReaderContext(), new CacheableOperation.Builder()); + builder.setUnless(getAttributeValue(opElement, "unless", "")); + builder.setSync(Boolean.valueOf(getAttributeValue(opElement, "sync", "false"))); Collection<CacheOperation> col = cacheOpMap.get(nameHolder); if (col == null) { col = new ArrayList<CacheOperation>(2); cacheOpMap.put(nameHolder, col); } - col.add(op); + col.add(builder.build()); } List<Element> evictCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_EVICT_ELEMENT); @@ -124,16 +126,17 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser { String name = prop.merge(opElement, parserContext.getReaderContext()); TypedStringValue nameHolder = new TypedStringValue(name); nameHolder.setSource(parserContext.extractSource(opElement)); - CacheEvictOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheEvictOperation()); + CacheEvictOperation.Builder builder = prop.merge(opElement, + parserContext.getReaderContext(), new CacheEvictOperation.Builder()); String wide = opElement.getAttribute("all-entries"); if (StringUtils.hasText(wide)) { - op.setCacheWide(Boolean.valueOf(wide.trim())); + builder.setCacheWide(Boolean.valueOf(wide.trim())); } String after = opElement.getAttribute("before-invocation"); if (StringUtils.hasText(after)) { - op.setBeforeInvocation(Boolean.valueOf(after.trim())); + builder.setBeforeInvocation(Boolean.valueOf(after.trim())); } Collection<CacheOperation> col = cacheOpMap.get(nameHolder); @@ -141,7 +144,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser { col = new ArrayList<CacheOperation>(2); cacheOpMap.put(nameHolder, col); } - col.add(op); + col.add(builder.build()); } List<Element> putCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_PUT_ELEMENT); @@ -150,15 +153,16 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser { String name = prop.merge(opElement, parserContext.getReaderContext()); TypedStringValue nameHolder = new TypedStringValue(name); nameHolder.setSource(parserContext.extractSource(opElement)); - CachePutOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CachePutOperation()); - op.setUnless(getAttributeValue(opElement, "unless", "")); + CachePutOperation.Builder builder = prop.merge(opElement, + parserContext.getReaderContext(), new CachePutOperation.Builder()); + builder.setUnless(getAttributeValue(opElement, "unless", "")); Collection<CacheOperation> col = cacheOpMap.get(nameHolder); if (col == null) { col = new ArrayList<CacheOperation>(2); cacheOpMap.put(nameHolder, col); } - col.add(op); + col.add(builder.build()); } RootBeanDefinition attributeSourceDefinition = new RootBeanDefinition(NameMatchCacheOperationSource.class); @@ -196,45 +200,45 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser { Props(Element root) { String defaultCache = root.getAttribute("cache"); - key = root.getAttribute("key"); - keyGenerator = root.getAttribute("key-generator"); - cacheManager = root.getAttribute("cache-manager"); - condition = root.getAttribute("condition"); - method = root.getAttribute(METHOD_ATTRIBUTE); + this.key = root.getAttribute("key"); + this.keyGenerator = root.getAttribute("key-generator"); + this.cacheManager = root.getAttribute("cache-manager"); + this.condition = root.getAttribute("condition"); + this.method = root.getAttribute(METHOD_ATTRIBUTE); if (StringUtils.hasText(defaultCache)) { - caches = StringUtils.commaDelimitedListToStringArray(defaultCache.trim()); + this.caches = StringUtils.commaDelimitedListToStringArray(defaultCache.trim()); } } - <T extends CacheOperation> T merge(Element element, ReaderContext readerCtx, T op) { + <T extends CacheOperation.Builder> T merge(Element element, ReaderContext readerCtx, T builder) { String cache = element.getAttribute("cache"); // sanity check - String[] localCaches = caches; + String[] localCaches = this.caches; if (StringUtils.hasText(cache)) { localCaches = StringUtils.commaDelimitedListToStringArray(cache.trim()); } else { - if (caches == null) { - readerCtx.error("No cache specified specified for " + element.getNodeName(), element); + if (this.caches == null) { + readerCtx.error("No cache specified for " + element.getNodeName(), element); } } - op.setCacheNames(localCaches); + builder.setCacheNames(localCaches); - op.setKey(getAttributeValue(element, "key", this.key)); - op.setKeyGenerator(getAttributeValue(element, "key-generator", this.keyGenerator)); - op.setCacheManager(getAttributeValue(element, "cache-manager", this.cacheManager)); - op.setCondition(getAttributeValue(element, "condition", this.condition)); + builder.setKey(getAttributeValue(element, "key", this.key)); + builder.setKeyGenerator(getAttributeValue(element, "key-generator", this.keyGenerator)); + builder.setCacheManager(getAttributeValue(element, "cache-manager", this.cacheManager)); + builder.setCondition(getAttributeValue(element, "condition", this.condition)); - if (StringUtils.hasText(op.getKey()) && StringUtils.hasText(op.getKeyGenerator())) { + if (StringUtils.hasText(builder.getKey()) && StringUtils.hasText(builder.getKeyGenerator())) { throw new IllegalStateException("Invalid cache advice configuration on '" + element.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " + "These attributes are mutually exclusive: either set the SpEL expression used to" + "compute the key at runtime or set the name of the KeyGenerator bean to use."); } - return op; + return builder; } String merge(Element element, ReaderContext readerCtx) { diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index d383b281..88d0aec5 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -26,27 +26,24 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodClassKey; import org.springframework.util.ClassUtils; /** - * Abstract implementation of {@link CacheOperation} that caches - * attributes for methods and implements a fallback policy: 1. specific - * target method; 2. target class; 3. declaring method; 4. declaring - * class/interface. + * Abstract implementation of {@link CacheOperation} that caches attributes + * for methods and implements a fallback policy: 1. specific target method; + * 2. target class; 3. declaring method; 4. declaring class/interface. * * <p>Defaults to using the target class's caching attribute if none is - * associated with the target method. Any caching attribute associated - * with the target method completely overrides a class caching attribute. - * If none found on the target class, the interface that the invoked - * method has been called through (in case of a JDK proxy) will be - * checked. + * associated with the target method. Any caching attribute associated with + * the target method completely overrides a class caching attribute. + * If none found on the target class, the interface that the invoked method + * has been called through (in case of a JDK proxy) will be checked. * - * <p>This implementation caches attributes by method after they are - * first used. If it is ever desirable to allow dynamic changing of - * cacheable attributes (which is very unlikely), caching could be made - * configurable. + * <p>This implementation caches attributes by method after they are first + * used. If it is ever desirable to allow dynamic changing of cacheable + * attributes (which is very unlikely), caching could be made configurable. * * @author Costin Leau * @author Juergen Hoeller @@ -69,7 +66,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera protected final Log logger = LogFactory.getLog(getClass()); /** - * Cache of CacheOperations, keyed by {@link AnnotatedElementKey}. + * Cache of CacheOperations, keyed by method on a specific target class. * <p>As this base class is not marked Serializable, the cache will be recreated * after serialization - provided that the concrete subclass is Serializable. */ @@ -117,7 +114,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera * @return the cache key (never {@code null}) */ protected Object getCacheKey(Method method, Class<?> targetClass) { - return new AnnotatedElementKey(method, targetClass); + return new MethodClassKey(method, targetClass); } private Collection<CacheOperation> computeCacheOperations(Method method, Class<?> targetClass) { @@ -140,19 +137,23 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera // Second try is the caching operation on the target class. opDef = findCacheOperations(specificMethod.getDeclaringClass()); - if (opDef != null) { + if (opDef != null && ClassUtils.isUserLevelMethod(method)) { return opDef; } if (specificMethod != method) { - // Fall back is to look at the original method. + // Fallback is to look at the original method. opDef = findCacheOperations(method); if (opDef != null) { return opDef; } - // Last fall back is the class of the original method. - return findCacheOperations(method.getDeclaringClass()); + // Last fallback is the class of the original method. + opDef = findCacheOperations(method.getDeclaringClass()); + if (opDef != null && ClassUtils.isUserLevelMethod(method)) { + return opDef; + } } + return null; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index 6dd21c1d..2de8bd8f 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -23,12 +23,16 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; @@ -36,11 +40,10 @@ import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.cache.support.SimpleValueWrapper; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.expression.EvaluationContext; +import org.springframework.lang.UsesJava8; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -76,17 +79,26 @@ import org.springframework.util.StringUtils; * @since 3.1 */ public abstract class CacheAspectSupport extends AbstractCacheInvoker - implements InitializingBean, SmartInitializingSingleton, ApplicationContextAware { + implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { + + private static Class<?> javaUtilOptionalClass = null; + + static { + try { + javaUtilOptionalClass = + ClassUtils.forName("java.util.Optional", CacheAspectSupport.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Java 8 not available - Optional references simply not supported then. + } + } protected final Log logger = LogFactory.getLog(getClass()); - /** - * Cache of CacheOperationMetadata, keyed by {@link CacheOperationCacheKey}. - */ private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = new ConcurrentHashMap<CacheOperationCacheKey, CacheOperationMetadata>(1024); - private final ExpressionEvaluator evaluator = new ExpressionEvaluator(); + private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(); private CacheOperationSource cacheOperationSource; @@ -94,7 +106,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker private CacheResolver cacheResolver; - private ApplicationContext applicationContext; + private BeanFactory beanFactory; private boolean initialized = false; @@ -163,12 +175,26 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker return this.cacheResolver; } + /** + * Set the containing {@link BeanFactory} for {@link CacheManager} and other + * service lookups. + * @since 4.3 + */ @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + /** + * @deprecated as of 4.3, in favor of {@link #setBeanFactory} + */ + @Deprecated public void setApplicationContext(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; + this.beanFactory = applicationContext; } + @Override public void afterPropertiesSet() { Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " + "If there are no cacheable methods, then don't use a cache aspect."); @@ -180,7 +206,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker if (getCacheResolver() == null) { // Lazily initialize cache resolver via default cache manager... try { - setCacheManager(this.applicationContext.getBean(CacheManager.class)); + setCacheManager(this.beanFactory.getBean(CacheManager.class)); } catch (NoUniqueBeanDefinitionException ex) { throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " + @@ -273,7 +299,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker * Return a bean with the specified name and type. Used to resolve services that * are referenced by name in a {@link CacheOperation}. * @param beanName the name of the bean, as defined by the operation - * @param expectedType type type for the bean + * @param expectedType type for the bean * @return the bean matching that name * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist * @see CacheOperation#keyGenerator @@ -281,7 +307,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker * @see CacheOperation#cacheResolver */ protected <T> T getBean(String beanName, Class<T> expectedType) { - return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.applicationContext, expectedType, beanName); + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName); } /** @@ -293,13 +319,12 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker } protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { - // check whether aspect is enabled - // to cope with cases where the AJ is pulled in automatically + // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically) if (this.initialized) { Class<?> targetClass = getTargetClass(target); Collection<CacheOperation> operations = getCacheOperationSource().getCacheOperations(method, targetClass); if (!CollectionUtils.isEmpty(operations)) { - return execute(invoker, new CacheOperationContexts(operations, method, args, target, targetClass)); + return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass)); } } @@ -328,9 +353,37 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker return targetClass; } - private Object execute(CacheOperationInvoker invoker, CacheOperationContexts contexts) { + private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + // Special handling of synchronized invocation + if (contexts.isSynchronized()) { + CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); + if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { + Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); + Cache cache = context.getCaches().iterator().next(); + try { + return cache.get(key, new Callable<Object>() { + @Override + public Object call() throws Exception { + return invokeOperation(invoker); + } + }); + } + catch (Cache.ValueRetrievalException ex) { + // The invoker wraps any Throwable in a ThrowableWrapper instance so we + // can just make sure that one bubbles up the stack. + throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause(); + } + } + else { + // No caching required, only call the underlying method + return invokeOperation(invoker); + } + } + + // Process any early evictions - processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT); + processCacheEvicts(contexts.get(CacheEvictOperation.class), true, + CacheOperationExpressionEvaluator.NO_RESULT); // Check if we have a cached item matching the conditions Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); @@ -338,42 +391,56 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker // Collect puts from any @Cacheable miss, if no cached item is found List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>(); if (cacheHit == null) { - collectPutRequests(contexts.get(CacheableOperation.class), ExpressionEvaluator.NO_RESULT, cachePutRequests); + collectPutRequests(contexts.get(CacheableOperation.class), + CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); } - Cache.ValueWrapper result = null; + Object cacheValue; + Object returnValue; - // If there are no put requests, just use the cache hit - if (cachePutRequests.isEmpty() && !hasCachePut(contexts)) { - result = cacheHit; + if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) { + // If there are no put requests, just use the cache hit + cacheValue = cacheHit.get(); + if (method.getReturnType() == javaUtilOptionalClass && + (cacheValue == null || cacheValue.getClass() != javaUtilOptionalClass)) { + returnValue = OptionalUnwrapper.wrap(cacheValue); + } + else { + returnValue = cacheValue; + } } - - // Invoke the method if don't have a cache hit - if (result == null) { - result = new SimpleValueWrapper(invokeOperation(invoker)); + else { + // Invoke the method if we don't have a cache hit + returnValue = invokeOperation(invoker); + if (returnValue != null && returnValue.getClass() == javaUtilOptionalClass) { + cacheValue = OptionalUnwrapper.unwrap(returnValue); + } + else { + cacheValue = returnValue; + } } // Collect any explicit @CachePuts - collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests); + collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); // Process any collected put requests, either from @CachePut or a @Cacheable miss for (CachePutRequest cachePutRequest : cachePutRequests) { - cachePutRequest.apply(result.get()); + cachePutRequest.apply(cacheValue); } // Process any late evictions - processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get()); + processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); - return result.get(); + return returnValue; } private boolean hasCachePut(CacheOperationContexts contexts) { - // Evaluate the conditions *without* the result object because we don't have it yet. + // Evaluate the conditions *without* the result object because we don't have it yet... Collection<CacheOperationContext> cachePutContexts = contexts.get(CachePutOperation.class); Collection<CacheOperationContext> excluded = new ArrayList<CacheOperationContext>(); for (CacheOperationContext context : cachePutContexts) { try { - if (!context.isConditionPassing(ExpressionEvaluator.RESULT_UNAVAILABLE)) { + if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) { excluded.add(context); } } @@ -425,7 +492,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker * or {@code null} if none is found */ private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) { - Object result = ExpressionEvaluator.NO_RESULT; + Object result = CacheOperationExpressionEvaluator.NO_RESULT; for (CacheOperationContext context : contexts) { if (isConditionPassing(context, result)) { Object key = generateKey(context, result); @@ -501,18 +568,57 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts = new LinkedMultiValueMap<Class<? extends CacheOperation>, CacheOperationContext>(); + private final boolean sync; + public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method, Object[] args, Object target, Class<?> targetClass) { for (CacheOperation operation : operations) { this.contexts.add(operation.getClass(), getOperationContext(operation, method, args, target, targetClass)); } + this.sync = determineSyncFlag(method); } public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) { Collection<CacheOperationContext> result = this.contexts.get(operationClass); return (result != null ? result : Collections.<CacheOperationContext>emptyList()); } + + public boolean isSynchronized() { + return this.sync; + } + + private boolean determineSyncFlag(Method method) { + List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class); + if (cacheOperationContexts == null) { // no @Cacheable operation at all + return false; + } + boolean syncEnabled = false; + for (CacheOperationContext cacheOperationContext : cacheOperationContexts) { + if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) { + syncEnabled = true; + break; + } + } + if (syncEnabled) { + if (this.contexts.size() > 1) { + throw new IllegalStateException("@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'"); + } + if (cacheOperationContexts.size() > 1) { + throw new IllegalStateException("Only one @Cacheable(sync=true) entry is allowed on '" + method + "'"); + } + CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next(); + CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation(); + if (cacheOperationContext.getCaches().size() > 1) { + throw new IllegalStateException("@Cacheable(sync=true) only allows a single cache on '" + operation + "'"); + } + if (StringUtils.hasText(operation.getUnless())) { + throw new IllegalStateException("@Cacheable(sync=true) does not support unless attribute on '" + operation + "'"); + } + return true; + } + return false; + } } @@ -635,8 +741,8 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker } private EvaluationContext createEvaluationContext(Object result) { - return evaluator.createEvaluationContext( - this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass, result); + return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args, + this.target, this.metadata.targetClass, result, beanFactory); } protected Collection<? extends Cache> getCaches() { @@ -678,7 +784,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker } - private static class CacheOperationCacheKey { + private static final class CacheOperationCacheKey implements Comparable<CacheOperationCacheKey> { private final CacheOperation cacheOperation; @@ -706,6 +812,42 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker public int hashCode() { return (this.cacheOperation.hashCode() * 31 + this.methodCacheKey.hashCode()); } + + @Override + public String toString() { + return this.cacheOperation + " on " + this.methodCacheKey; + } + + @Override + public int compareTo(CacheOperationCacheKey other) { + int result = this.cacheOperation.getName().compareTo(other.cacheOperation.getName()); + if (result == 0) { + result = this.methodCacheKey.compareTo(other.methodCacheKey); + } + return result; + } + } + + + /** + * Inner class to avoid a hard dependency on Java 8. + */ + @UsesJava8 + private static class OptionalUnwrapper { + + public static Object unwrap(Object optionalObject) { + Optional<?> optional = (Optional<?>) optionalObject; + if (!optional.isPresent()) { + return null; + } + Object result = optional.get(); + Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported"); + return result; + } + + public static Object wrap(Object value) { + return Optional.ofNullable(value); + } } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java index 6bb937a5..5f5e0dc0 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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,38 +20,65 @@ package org.springframework.cache.interceptor; * Class describing a cache 'evict' operation. * * @author Costin Leau + * @author Marcin Kamionowski * @since 3.1 */ public class CacheEvictOperation extends CacheOperation { - private boolean cacheWide = false; + private final boolean cacheWide; - private boolean beforeInvocation = false; + private final boolean beforeInvocation; - public void setCacheWide(boolean cacheWide) { - this.cacheWide = cacheWide; + /** + * @since 4.3 + */ + public CacheEvictOperation(CacheEvictOperation.Builder b) { + super(b); + this.cacheWide = b.cacheWide; + this.beforeInvocation = b.beforeInvocation; } + public boolean isCacheWide() { return this.cacheWide; } - public void setBeforeInvocation(boolean beforeInvocation) { - this.beforeInvocation = beforeInvocation; - } - public boolean isBeforeInvocation() { return this.beforeInvocation; } - @Override - protected StringBuilder getOperationDescription() { - StringBuilder sb = super.getOperationDescription(); - sb.append(","); - sb.append(this.cacheWide); - sb.append(","); - sb.append(this.beforeInvocation); - return sb; + + /** + * @since 4.3 + */ + public static class Builder extends CacheOperation.Builder { + + private boolean cacheWide = false; + + private boolean beforeInvocation = false; + + public void setCacheWide(boolean cacheWide) { + this.cacheWide = cacheWide; + } + + public void setBeforeInvocation(boolean beforeInvocation) { + this.beforeInvocation = beforeInvocation; + } + + @Override + protected StringBuilder getOperationDescription() { + StringBuilder sb = super.getOperationDescription(); + sb.append(","); + sb.append(this.cacheWide); + sb.append(","); + sb.append(this.beforeInvocation); + return sb; + } + + public CacheEvictOperation build() { + return new CacheEvictOperation(this); + } } + } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java index 6264a040..271b59f1 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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,45 +27,45 @@ import org.springframework.util.Assert; * * @author Costin Leau * @author Stephane Nicoll + * @author Marcin Kamionowski * @since 3.1 */ public abstract class CacheOperation implements BasicOperation { - private String name = ""; + private final String name; - private Set<String> cacheNames = Collections.emptySet(); + private final Set<String> cacheNames; - private String key = ""; + private final String key; - private String keyGenerator = ""; + private final String keyGenerator; - private String cacheManager = ""; + private final String cacheManager; - private String cacheResolver = ""; + private final String cacheResolver; - private String condition = ""; + private final String condition; + private final String toString; - public void setName(String name) { - Assert.hasText(name); - this.name = name; - } - public String getName() { - return this.name; + /** + * @since 4.3 + */ + protected CacheOperation(Builder b) { + this.name = b.name; + this.cacheNames = b.cacheNames; + this.key = b.key; + this.keyGenerator = b.keyGenerator; + this.cacheManager = b.cacheManager; + this.cacheResolver = b.cacheResolver; + this.condition = b.condition; + this.toString = b.getOperationDescription().toString(); } - public void setCacheName(String cacheName) { - Assert.hasText(cacheName); - this.cacheNames = Collections.singleton(cacheName); - } - public void setCacheNames(String... cacheNames) { - this.cacheNames = new LinkedHashSet<String>(cacheNames.length); - for (String cacheName : cacheNames) { - Assert.hasText(cacheName, "Cache name must be non-null if specified"); - this.cacheNames.add(cacheName); - } + public String getName() { + return this.name; } @Override @@ -73,47 +73,22 @@ public abstract class CacheOperation implements BasicOperation { return this.cacheNames; } - public void setKey(String key) { - Assert.notNull(key); - this.key = key; - } - public String getKey() { return this.key; } - public void setKeyGenerator(String keyGenerator) { - Assert.notNull(keyGenerator); - this.keyGenerator = keyGenerator; - } - public String getKeyGenerator() { return this.keyGenerator; } - public void setCacheManager(String cacheManager) { - Assert.notNull(cacheManager); - this.cacheManager = cacheManager; - } - public String getCacheManager() { return this.cacheManager; } - public void setCacheResolver(String cacheResolver) { - Assert.notNull(cacheManager); - this.cacheResolver = cacheResolver; - } - public String getCacheResolver() { return this.cacheResolver; } - public void setCondition(String condition) { - Assert.notNull(condition); - this.condition = condition; - } - public String getCondition() { return this.condition; } @@ -139,29 +114,116 @@ public abstract class CacheOperation implements BasicOperation { /** * Return an identifying description for this cache operation. - * <p>Has to be overridden in subclasses for correct {@code equals} - * and {@code hashCode} behavior. Alternatively, {@link #equals} - * and {@link #hashCode} can be overridden themselves. + * <p>Returned value is produced by calling {@link Builder#getOperationDescription()} + * during object construction. This method is used in {@link #hashCode} and + * {@link #equals}. + * @see Builder#getOperationDescription() */ @Override - public String toString() { - return getOperationDescription().toString(); + public final String toString() { + return this.toString; } + /** - * Return an identifying description for this caching operation. - * <p>Available to subclasses, for inclusion in their {@code toString()} result. + * @since 4.3 */ - protected StringBuilder getOperationDescription() { - StringBuilder result = new StringBuilder(getClass().getSimpleName()); - result.append("[").append(this.name); - result.append("] caches=").append(this.cacheNames); - result.append(" | key='").append(this.key); - result.append("' | keyGenerator='").append(this.keyGenerator); - result.append("' | cacheManager='").append(this.cacheManager); - result.append("' | cacheResolver='").append(this.cacheResolver); - result.append("' | condition='").append(this.condition).append("'"); - return result; + public abstract static class Builder { + + private String name = ""; + + private Set<String> cacheNames = Collections.emptySet(); + + private String key = ""; + + private String keyGenerator = ""; + + private String cacheManager = ""; + + private String cacheResolver = ""; + + private String condition = ""; + + public void setName(String name) { + Assert.hasText(name); + this.name = name; + } + + public void setCacheName(String cacheName) { + Assert.hasText(cacheName); + this.cacheNames = Collections.singleton(cacheName); + } + + public void setCacheNames(String... cacheNames) { + this.cacheNames = new LinkedHashSet<String>(cacheNames.length); + for (String cacheName : cacheNames) { + Assert.hasText(cacheName, "Cache name must be non-null if specified"); + this.cacheNames.add(cacheName); + } + } + + public Set<String> getCacheNames() { + return this.cacheNames; + } + + public void setKey(String key) { + Assert.notNull(key); + this.key = key; + } + + public String getKey() { + return this.key; + } + + public String getKeyGenerator() { + return this.keyGenerator; + } + + public String getCacheManager() { + return this.cacheManager; + } + + public String getCacheResolver() { + return this.cacheResolver; + } + + public void setKeyGenerator(String keyGenerator) { + Assert.notNull(keyGenerator); + this.keyGenerator = keyGenerator; + } + + public void setCacheManager(String cacheManager) { + Assert.notNull(cacheManager); + this.cacheManager = cacheManager; + } + + public void setCacheResolver(String cacheResolver) { + Assert.notNull(this.cacheManager); + this.cacheResolver = cacheResolver; + } + + public void setCondition(String condition) { + Assert.notNull(condition); + this.condition = condition; + } + + /** + * Return an identifying description for this caching operation. + * <p>Available to subclasses, for inclusion in their {@code toString()} result. + */ + protected StringBuilder getOperationDescription() { + StringBuilder result = new StringBuilder(getClass().getSimpleName()); + result.append("[").append(this.name); + result.append("] caches=").append(this.cacheNames); + result.append(" | key='").append(this.key); + result.append("' | keyGenerator='").append(this.keyGenerator); + result.append("' | cacheManager='").append(this.cacheManager); + result.append("' | cacheResolver='").append(this.cacheResolver); + result.append("' | condition='").append(this.condition).append("'"); + return result; + } + + public abstract CacheOperation build(); } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java index 88ed1484..9111bab0 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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,11 +22,11 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; import org.springframework.cache.Cache; import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.CachedExpressionEvaluator; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -43,7 +43,7 @@ import org.springframework.expression.Expression; * @author Stephane Nicoll * @since 3.1 */ -class ExpressionEvaluator extends CachedExpressionEvaluator { +class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { /** * Indicate that there is no result variable. @@ -60,8 +60,6 @@ class ExpressionEvaluator extends CachedExpressionEvaluator { */ public static final String RESULT_VARIABLE = "result"; - // shared param discoverer since it caches data internally - private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer(); private final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<ExpressionKey, Expression>(64); @@ -75,12 +73,12 @@ class ExpressionEvaluator extends CachedExpressionEvaluator { /** * Create an {@link EvaluationContext} without a return value. - * @see #createEvaluationContext(Collection, Method, Object[], Object, Class, Object) + * @see #createEvaluationContext(Collection, Method, Object[], Object, Class, Object, BeanFactory) */ public EvaluationContext createEvaluationContext(Collection<? extends Cache> caches, - Method method, Object[] args, Object target, Class<?> targetClass) { + Method method, Object[] args, Object target, Class<?> targetClass, BeanFactory beanFactory) { - return createEvaluationContext(caches, method, args, target, targetClass, NO_RESULT); + return createEvaluationContext(caches, method, args, target, targetClass, NO_RESULT, beanFactory); } /** @@ -95,19 +93,23 @@ class ExpressionEvaluator extends CachedExpressionEvaluator { * @return the evaluation context */ public EvaluationContext createEvaluationContext(Collection<? extends Cache> caches, - Method method, Object[] args, Object target, Class<?> targetClass, Object result) { + Method method, Object[] args, Object target, Class<?> targetClass, Object result, + BeanFactory beanFactory) { - CacheExpressionRootObject rootObject = new CacheExpressionRootObject(caches, - method, args, target, targetClass); + CacheExpressionRootObject rootObject = new CacheExpressionRootObject( + caches, method, args, target, targetClass); Method targetMethod = getTargetMethod(targetClass, method); - CacheEvaluationContext evaluationContext = new CacheEvaluationContext(rootObject, - targetMethod, args, this.paramNameDiscoverer); + CacheEvaluationContext evaluationContext = new CacheEvaluationContext( + rootObject, targetMethod, args, getParameterNameDiscoverer()); if (result == RESULT_UNAVAILABLE) { evaluationContext.addUnavailableVariable(RESULT_VARIABLE); } else if (result != NO_RESULT) { evaluationContext.setVariable(RESULT_VARIABLE, result); } + if (beanFactory != null) { + evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } return evaluationContext; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java index 8901ba19..3529438d 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java @@ -30,9 +30,8 @@ package org.springframework.cache.interceptor; public interface CacheOperationInvoker { /** - * Invoke the cache operation defined by this instance. Wraps any - * exception that is thrown during the invocation in a - * {@link ThrowableWrapper}. + * Invoke the cache operation defined by this instance. Wraps any exception + * that is thrown during the invocation in a {@link ThrowableWrapper}. * @return the result of the operation * @throws ThrowableWrapper if an error occurred while invoking the operation */ diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java index e6a61b0a..01cf15f1 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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,27 +21,51 @@ package org.springframework.cache.interceptor; * * @author Costin Leau * @author Phillip Webb + * @author Marcin Kamionowski * @since 3.1 */ public class CachePutOperation extends CacheOperation { - private String unless; + private final String unless; - public String getUnless() { - return unless; + /** + * @since 4.3 + */ + public CachePutOperation(CachePutOperation.Builder b) { + super(b); + this.unless = b.unless; } - public void setUnless(String unless) { - this.unless = unless; + + public String getUnless() { + return this.unless; } - @Override - protected StringBuilder getOperationDescription() { - StringBuilder sb = super.getOperationDescription(); - sb.append(" | unless='"); - sb.append(this.unless); - sb.append("'"); - return sb; + + /** + * @since 4.3 + */ + public static class Builder extends CacheOperation.Builder { + + private String unless; + + public void setUnless(String unless) { + this.unless = unless; + } + + @Override + protected StringBuilder getOperationDescription() { + StringBuilder sb = super.getOperationDescription(); + sb.append(" | unless='"); + sb.append(this.unless); + sb.append("'"); + return sb; + } + + public CachePutOperation build() { + return new CachePutOperation(this); + } } + } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java index f9375a9a..c3414e4d 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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,27 +21,68 @@ package org.springframework.cache.interceptor; * * @author Costin Leau * @author Phillip Webb + * @author Marcin Kamionowski * @since 3.1 */ public class CacheableOperation extends CacheOperation { - private String unless; + private final String unless; + + private final boolean sync; + + + /** + * @since 4.3 + */ + public CacheableOperation(CacheableOperation.Builder b) { + super(b); + this.unless = b.unless; + this.sync = b.sync; + } public String getUnless() { - return unless; + return this.unless; } - public void setUnless(String unless) { - this.unless = unless; + public boolean isSync() { + return this.sync; } - @Override - protected StringBuilder getOperationDescription() { - StringBuilder sb = super.getOperationDescription(); - sb.append(" | unless='"); - sb.append(this.unless); - sb.append("'"); - return sb; + + /** + * @since 4.3 + */ + public static class Builder extends CacheOperation.Builder { + + private String unless; + + private boolean sync; + + public void setUnless(String unless) { + this.unless = unless; + } + + public void setSync(boolean sync) { + this.sync = sync; + } + + @Override + protected StringBuilder getOperationDescription() { + StringBuilder sb = super.getOperationDescription(); + sb.append(" | unless='"); + sb.append(this.unless); + sb.append("'"); + sb.append(" | sync='"); + sb.append(this.sync); + sb.append("'"); + return sb; + } + + @Override + public CacheableOperation build() { + return new CacheableOperation(this); + } } + } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java index 51e95c15..f75fd2e8 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -51,7 +51,7 @@ public class NamedCacheResolver extends AbstractCacheResolver { @Override protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) { - return cacheNames; + return this.cacheNames; } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java index b9304539..e6573045 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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. @@ -57,7 +57,7 @@ public class SimpleKey implements Serializable { @Override public final int hashCode() { - return hashCode; + return this.hashCode; } @Override diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java b/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java index b6f52d61..6f7fdde3 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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,6 +37,6 @@ class VariableNotAvailableException extends EvaluationException { public String getName() { - return name; + return this.name; } } diff --git a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java index 87ab3798..85a1168f 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java @@ -131,7 +131,9 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing /** * Dynamically register an additional Cache with this manager. * @param cache the Cache to register + * @deprecated as of Spring 4.3, in favor of {@link #getMissingCache(String)} */ + @Deprecated protected final void addCache(Cache cache) { String name = cache.getName(); synchronized (this.cacheMap) { @@ -171,10 +173,10 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing /** * Return a missing cache with the specified {@code name} or {@code null} if * such cache does not exist or could not be created on the fly. - * <p>Some caches may be created at runtime in the native provided. If a lookup - * by name does not yield any result, a subclass gets a chance to register - * such a cache at runtime. The returned cache will be automatically added to - * this instance. + * <p>Some caches may be created at runtime if the native provider supports + * it. If a lookup by name does not yield any result, a subclass gets a chance + * to register such a cache at runtime. The returned cache will be automatically + * added to this instance. * @param name the name of the cache to retrieve * @return the missing cache or {@code null} if no such cache exists or could be * created diff --git a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java index 0b486804..c27f59e1 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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,6 +20,7 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -100,6 +101,16 @@ public class NoOpCacheManager implements CacheManager { } @Override + public <T> T get(Object key, Callable<T> valueLoader) { + try { + return valueLoader.call(); + } + catch (Exception ex) { + throw new ValueRetrievalException(key, valueLoader, ex); + } + } + + @Override public String getName() { return this.name; } diff --git a/spring-context/src/main/java/org/springframework/cache/support/package-info.java b/spring-context/src/main/java/org/springframework/cache/support/package-info.java index 38c95daf..cf3f5d4f 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/cache/support/package-info.java @@ -1,5 +1,5 @@ /** - * Support classes for the the org.springframework.cache package. + * Support classes for the org.springframework.cache package. * Provides abstract classes for cache managers and caches. */ package org.springframework.cache.support; diff --git a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java index 09048cbe..7cfeb9e2 100644 --- a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 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. @@ -23,6 +23,7 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; +import org.springframework.core.io.ProtocolResolver; /** * SPI interface to be implemented by most if not all application contexts. @@ -113,9 +114,9 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life * Add a new BeanFactoryPostProcessor that will get applied to the internal * bean factory of this application context on refresh, before any of the * bean definitions get evaluated. To be invoked during context configuration. - * @param beanFactoryPostProcessor the factory processor to register + * @param postProcessor the factory processor to register */ - void addBeanFactoryPostProcessor(BeanFactoryPostProcessor beanFactoryPostProcessor); + void addBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor); /** * Add a new ApplicationListener that will be notified on context events @@ -130,6 +131,15 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life void addApplicationListener(ApplicationListener<?> listener); /** + * Register the given protocol resolver with this application context, + * allowing for additional resource protocols to be handled. + * <p>Any such resolver will be invoked ahead of this context's standard + * resolution rules. It may therefore also override any default rules. + * @since 4.3 + */ + void addProtocolResolver(ProtocolResolver resolver); + + /** * Load or refresh the persistent representation of the configuration, * which might an XML file, properties file, or relational database schema. * <p>As this is a startup method, it should destroy already created singletons diff --git a/spring-context/src/main/java/org/springframework/context/EmbeddedValueResolverAware.java b/spring-context/src/main/java/org/springframework/context/EmbeddedValueResolverAware.java index 98d25eaa..a3345c2c 100644 --- a/spring-context/src/main/java/org/springframework/context/EmbeddedValueResolverAware.java +++ b/spring-context/src/main/java/org/springframework/context/EmbeddedValueResolverAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2016 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,9 @@ import org.springframework.util.StringValueResolver; * @author Juergen Hoeller * @author Chris Beams * @since 3.0.3 - * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#resolveEmbeddedValue + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#resolveEmbeddedValue(String) + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanExpressionResolver() + * @see org.springframework.beans.factory.config.EmbeddedValueResolver */ public interface EmbeddedValueResolverAware extends Aware { diff --git a/spring-context/src/main/java/org/springframework/context/access/ContextSingletonBeanFactoryLocator.java b/spring-context/src/main/java/org/springframework/context/access/ContextSingletonBeanFactoryLocator.java index 57df5694..619e334a 100644 --- a/spring-context/src/main/java/org/springframework/context/access/ContextSingletonBeanFactoryLocator.java +++ b/spring-context/src/main/java/org/springframework/context/access/ContextSingletonBeanFactoryLocator.java @@ -70,7 +70,7 @@ public class ContextSingletonBeanFactoryLocator extends SingletonBeanFactoryLoca } /** - * Returns an instance which uses the the specified selector, as the name of the + * Returns an instance which uses the specified selector, as the name of the * definition file(s). In the case of a name with a Spring "classpath*:" prefix, * or with no prefix, which is treated the same, the current thread's context class * loader's {@code getResources} method will be called with this value to get @@ -112,7 +112,7 @@ public class ContextSingletonBeanFactoryLocator extends SingletonBeanFactoryLoca /** - * Constructor which uses the the specified name as the resource name + * Constructor which uses the specified name as the resource name * of the definition file(s). * @param resourceLocation the Spring resource location to use * (either a URL or a "classpath:" / "classpath*:" pseudo URL) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java index 780f8be0..4c16bb0e 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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. @@ -127,15 +127,13 @@ public class AnnotatedBeanDefinitionReader { registerBean(annotatedClass, null, (Class<? extends Annotation>[]) null); } - public void registerBean(Class<?> annotatedClass, - @SuppressWarnings("unchecked") Class<? extends Annotation>... qualifiers) { - + @SuppressWarnings("unchecked") + public void registerBean(Class<?> annotatedClass, Class<? extends Annotation>... qualifiers) { registerBean(annotatedClass, null, qualifiers); } - public void registerBean(Class<?> annotatedClass, String name, - @SuppressWarnings("unchecked") Class<? extends Annotation>... qualifiers) { - + @SuppressWarnings("unchecked") + public void registerBean(Class<?> annotatedClass, String name, Class<? extends Annotation>... qualifiers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass); if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java index d7d86e96..ddbf312a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -79,9 +79,10 @@ public class AnnotationScopeMetadataResolver implements ScopeMetadataResolver { ScopeMetadata metadata = new ScopeMetadata(); if (definition instanceof AnnotatedBeanDefinition) { AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; - AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(annDef.getMetadata(), this.scopeAnnotationType); + AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor( + annDef.getMetadata(), this.scopeAnnotationType); if (attributes != null) { - metadata.setScopeName(attributes.getAliasedString("value", this.scopeAnnotationType, definition.getSource())); + metadata.setScopeName(attributes.getString("value")); ScopedProxyMode proxyMode = attributes.getEnum("proxyMode"); if (proxyMode == null || proxyMode == ScopedProxyMode.DEFAULT) { proxyMode = this.defaultProxyMode; diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java b/spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java index c73fcc7a..5ec21e9c 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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,6 +27,7 @@ import org.springframework.core.type.AnnotationMetadata; * as appropriate based on a given @{@link EnableAspectJAutoProxy} annotation. * * @author Chris Beams + * @author Juergen Hoeller * @since 3.1 * @see EnableAspectJAutoProxy */ @@ -43,11 +44,14 @@ class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); - AnnotationAttributes enableAJAutoProxy = + AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class); - if (enableAJAutoProxy.getBoolean("proxyTargetClass")) { + if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) { AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); } + if (enableAspectJAutoProxy.getBoolean("exposeProxy")) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java b/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java index cc5c911c..58b54814 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -79,7 +79,7 @@ public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar { logger.warn(String.format("%s was imported but no annotations were found " + "having both 'mode' and 'proxyTargetClass' attributes of type " + "AdviceMode and boolean respectively. This means that auto proxy " + - "creator registration and configuration may not have occured as " + + "creator registration and configuration may not have occurred as " + "intended, and components may not be proxied as expected. Check to " + "ensure that %s has been @Import'ed on the same class where these " + "annotations are declared; otherwise remove the import of %s " + diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java index 626a328f..b28ab049 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,22 +18,19 @@ package org.springframework.context.annotation; import java.lang.reflect.Method; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; /** * Utilities for processing {@link Bean}-annotated methods. * * @author Chris Beams + * @author Juergen Hoeller * @since 3.1 */ class BeanAnnotationHelper { - /** - * Return whether the given method is directly or indirectly annotated with - * the {@link Bean} annotation. - */ public static boolean isBeanAnnotated(Method method) { - return (AnnotationUtils.findAnnotation(method, Bean.class) != null); + return AnnotatedElementUtils.hasAnnotation(method, Bean.class); } public static String determineBeanNameFor(Method beanMethod) { @@ -41,7 +38,7 @@ class BeanAnnotationHelper { String beanName = beanMethod.getName(); // Check to see if the user has explicitly set a custom bean name... - Bean bean = AnnotationUtils.findAnnotation(beanMethod, Bean.class); + Bean bean = AnnotatedElementUtils.findMergedAnnotation(beanMethod, Bean.class); if (bean != null && bean.name().length > 0) { beanName = bean.name()[0]; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java index b811d52d..8e90baa5 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -92,7 +92,7 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo * implementations. * <p>If given a plain {@code BeanDefinitionRegistry}, the default {@code ResourceLoader} * will be a {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}. - * <p>If the the passed-in bean factory also implements {@link EnvironmentCapable} its + * <p>If the passed-in bean factory also implements {@link EnvironmentCapable} its * environment will be used by this reader. Otherwise, the reader will initialize and * use a {@link org.springframework.core.env.StandardEnvironment}. All * {@code ApplicationContext} implementations are {@code EnvironmentCapable}, while diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index f621ea24..c3dbc027 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -58,6 +58,7 @@ import org.springframework.beans.factory.annotation.InjectionMetadata; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.BridgeMethodResolver; @@ -68,6 +69,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; /** * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation @@ -140,6 +142,9 @@ import org.springframework.util.StringUtils; public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable { + // Common Annotations 1.1 Resource.lookup() available? Not present on JDK 6... + private static final Method lookupAttribute = ClassUtils.getMethodIfAvailable(Resource.class, "lookup"); + private static Class<? extends Annotation> webServiceRefClass = null; private static Class<? extends Annotation> ejbRefClass = null; @@ -178,6 +183,8 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean private transient BeanFactory beanFactory; + private transient StringValueResolver embeddedValueResolver; + private transient final Map<String, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<String, InjectionMetadata>(256); @@ -271,12 +278,15 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean } @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + public void setBeanFactory(BeanFactory beanFactory) { Assert.notNull(beanFactory, "BeanFactory must not be null"); this.beanFactory = beanFactory; if (this.resourceFactory == null) { this.resourceFactory = beanFactory; } + if (beanFactory instanceof ConfigurableBeanFactory) { + this.embeddedValueResolver = new EmbeddedValueResolver((ConfigurableBeanFactory) beanFactory); + } } @@ -592,8 +602,8 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean resourceName = Introspector.decapitalize(resourceName.substring(3)); } } - else if (beanFactory instanceof ConfigurableBeanFactory){ - resourceName = ((ConfigurableBeanFactory) beanFactory).resolveEmbeddedValue(resourceName); + else if (embeddedValueResolver != null) { + resourceName = embeddedValueResolver.resolveStringValue(resourceName); } if (resourceType != null && Object.class != resourceType) { checkResourceType(resourceType); @@ -604,7 +614,9 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean } this.name = resourceName; this.lookupType = resourceType; - this.mappedName = resource.mappedName(); + String lookupValue = (lookupAttribute != null ? + (String) ReflectionUtils.invokeMethod(lookupAttribute, resource) : null); + this.mappedName = (StringUtils.hasLength(lookupValue) ? lookupValue : resource.mappedName()); Lazy lazy = ae.getAnnotation(Lazy.class); this.lazyLookup = (lazy != null && lazy.value()); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java index f7ad662c..4d0d9464 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -18,6 +18,7 @@ package org.springframework.context.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -54,6 +55,7 @@ import org.springframework.core.type.filter.TypeFilter; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented +@Repeatable(ComponentScans.class) public @interface ComponentScan { /** @@ -187,6 +189,15 @@ public @interface ComponentScan { * </table> * <p>When multiple classes are specified, <em>OR</em> logic is applied * — for example, "include types annotated with {@code @Foo} OR {@code @Bar}". + * <p>Custom {@link TypeFilter TypeFilters} may optionally implement any of the + * following {@link org.springframework.beans.factory.Aware Aware} interfaces, and + * their respective methods will be called prior to {@link TypeFilter#match match}: + * <ul> + * <li>{@link org.springframework.context.EnvironmentAware EnvironmentAware}</li> + * <li>{@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware} + * <li>{@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware} + * <li>{@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware} + * </ul> * <p>Specifying zero classes is permitted but will have no effect on component * scanning. * @since 4.2 diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java index 1119012b..247c5838 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -25,10 +25,17 @@ import java.util.Set; import java.util.regex.Pattern; import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; @@ -115,7 +122,7 @@ class ComponentScanAnnotationParser { } Set<String> basePackages = new LinkedHashSet<String>(); - String[] basePackagesArray = componentScan.getAliasedStringArray("basePackages", ComponentScan.class, declaringClass); + String[] basePackagesArray = componentScan.getStringArray("basePackages"); for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); @@ -141,11 +148,11 @@ class ComponentScanAnnotationParser { List<TypeFilter> typeFilters = new ArrayList<TypeFilter>(); FilterType filterType = filterAttributes.getEnum("type"); - for (Class<?> filterClass : filterAttributes.getAliasedClassArray("classes", ComponentScan.Filter.class, null)) { + for (Class<?> filterClass : filterAttributes.getClassArray("classes")) { switch (filterType) { case ANNOTATION: Assert.isAssignable(Annotation.class, filterClass, - "An error occured while processing a @ComponentScan ANNOTATION type filter: "); + "An error occurred while processing a @ComponentScan ANNOTATION type filter: "); @SuppressWarnings("unchecked") Class<Annotation> annotationType = (Class<Annotation>) filterClass; typeFilters.add(new AnnotationTypeFilter(annotationType)); @@ -155,8 +162,10 @@ class ComponentScanAnnotationParser { break; case CUSTOM: Assert.isAssignable(TypeFilter.class, filterClass, - "An error occured while processing a @ComponentScan CUSTOM type filter: "); - typeFilters.add(BeanUtils.instantiateClass(filterClass, TypeFilter.class)); + "An error occurred while processing a @ComponentScan CUSTOM type filter: "); + TypeFilter filter = BeanUtils.instantiateClass(filterClass, TypeFilter.class); + invokeAwareMethods(filter); + typeFilters.add(filter); break; default: throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType); @@ -179,4 +188,27 @@ class ComponentScanAnnotationParser { return typeFilters; } + /** + * Invoke {@link ResourceLoaderAware}, {@link BeanClassLoaderAware} and + * {@link BeanFactoryAware} contracts if implemented by the given {@code filter}. + */ + private void invokeAwareMethods(TypeFilter filter) { + if (filter instanceof Aware) { + if (filter instanceof EnvironmentAware) { + ((EnvironmentAware) filter).setEnvironment(this.environment); + } + if (filter instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) filter).setResourceLoader(this.resourceLoader); + } + if (filter instanceof BeanClassLoaderAware) { + ClassLoader classLoader = (this.registry instanceof ConfigurableBeanFactory ? + ((ConfigurableBeanFactory) this.registry).getBeanClassLoader() : + this.resourceLoader.getClassLoader()); + ((BeanClassLoaderAware) filter).setBeanClassLoader(classLoader); + } + if (filter instanceof BeanFactoryAware && this.registry instanceof BeanFactory) { + ((BeanFactoryAware) filter).setBeanFactory((BeanFactory) this.registry); + } + } + } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScans.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScans.java new file mode 100644 index 00000000..c9498556 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScans.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2015 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; + +/** + * Container annotation that aggregates several {@link ComponentScan} annotations. + * + * <p>Can be used natively, declaring several nested {@link ComponentScan} annotations. + * Can also be used in conjunction with Java 8's support for repeatable annotations, + * where {@link ComponentScan} can simply be declared several times on the same method, + * implicitly generating this container annotation. + * + * @author Juergen Hoeller + * @since 4.3 + * @see ComponentScan + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface ComponentScans { + + ComponentScan[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 5cb5c0f3..7fa587fa 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -103,7 +103,7 @@ final class ConfigurationClass { public ConfigurationClass(Class<?> clazz, String beanName) { Assert.hasText(beanName, "Bean name must not be null"); this.metadata = new StandardAnnotationMetadata(clazz, true); - this.resource = new DescriptiveResource(clazz.toString()); + this.resource = new DescriptiveResource(clazz.getName()); this.beanName = beanName; } @@ -117,7 +117,7 @@ final class ConfigurationClass { */ public ConfigurationClass(Class<?> clazz, ConfigurationClass importedBy) { this.metadata = new StandardAnnotationMetadata(clazz, true); - this.resource = new DescriptiveResource(clazz.toString()); + this.resource = new DescriptiveResource(clazz.getName()); this.importedBy.add(importedBy); } @@ -233,7 +233,7 @@ final class ConfigurationClass { @Override public String toString() { - return "ConfigurationClass:beanName=" + this.beanName + ",resource=" + this.resource; + return "ConfigurationClass: beanName '" + this.beanName + "', " + this.resource; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 9031872e..522acf33 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -236,7 +236,7 @@ class ConfigurationClassBeanDefinitionReader { ScopedProxyMode proxyMode = ScopedProxyMode.NO; AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class); if (attributes != null) { - beanDef.setScope(attributes.getAliasedString("value", Scope.class, configClass.getResource())); + beanDef.setScope(attributes.getString("value")); proxyMode = attributes.getEnum("proxyMode"); if (proxyMode == ScopedProxyMode.DEFAULT) { proxyMode = ScopedProxyMode.NO; diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 4aea07f9..63497ff9 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -45,7 +45,7 @@ import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.NoOp; import org.springframework.cglib.transform.ClassEmitterTransformer; import org.springframework.cglib.transform.TransformingClassGenerator; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.objenesis.ObjenesisException; import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.Assert; @@ -311,7 +311,7 @@ class ConfigurationClassEnhancer { String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); // Determine whether this bean is a scoped-proxy - Scope scope = AnnotationUtils.findAnnotation(beanMethod, Scope.class); + Scope scope = AnnotatedElementUtils.findMergedAnnotation(beanMethod, Scope.class); if (scope != null && scope.proxyMode() != ScopedProxyMode.NO) { String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName); if (beanFactory.isCurrentlyInCreation(scopedBeanName)) { @@ -466,7 +466,7 @@ class ConfigurationClassEnhancer { try { fbProxy = fbClass.newInstance(); } - catch (Exception ex) { + catch (Throwable ex) { throw new IllegalStateException("Unable to instantiate enhanced FactoryBean using Objenesis, " + "and regular FactoryBean instantiation via default constructor fails as well", ex); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 023371b4..46d98634 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -66,6 +66,9 @@ import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.DefaultPropertySourceFactory; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertySourceFactory; import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; @@ -102,6 +105,8 @@ import org.springframework.util.StringUtils; */ class ConfigurationClassParser { + private static final PropertySourceFactory DEFAULT_PROPERTY_SOURCE_FACTORY = new DefaultPropertySourceFactory(); + private static final Comparator<DeferredImportSelectorHolder> DEFERRED_IMPORT_COMPARATOR = new Comparator<ConfigurationClassParser.DeferredImportSelectorHolder>() { @Override @@ -177,7 +182,7 @@ class ConfigurationClassParser { catch (BeanDefinitionStoreException ex) { throw ex; } - catch (Exception ex) { + catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex); } @@ -261,15 +266,18 @@ class ConfigurationClassParser { } // Process any @ComponentScan annotations - AnnotationAttributes componentScan = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ComponentScan.class); - if (componentScan != null && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { - // The config class is annotated with @ComponentScan -> perform the scan immediately - Set<BeanDefinitionHolder> scannedBeanDefinitions = - this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); - // Check the set of scanned definitions for any further config classes and parse recursively if necessary - for (BeanDefinitionHolder holder : scannedBeanDefinitions) { - if (ConfigurationClassUtils.checkConfigurationClassCandidate(holder.getBeanDefinition(), this.metadataReaderFactory)) { - parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName()); + Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( + sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); + if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { + for (AnnotationAttributes componentScan : componentScans) { + // The config class is annotated with @ComponentScan -> perform the scan immediately + Set<BeanDefinitionHolder> scannedBeanDefinitions = + this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); + // Check the set of scanned definitions for any further config classes and parse recursively if necessary + for (BeanDefinitionHolder holder : scannedBeanDefinitions) { + if (ConfigurationClassUtils.checkConfigurationClassCandidate(holder.getBeanDefinition(), this.metadataReaderFactory)) { + parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName()); + } } } } @@ -279,8 +287,9 @@ class ConfigurationClassParser { // Process any @ImportResource annotations if (sourceClass.getMetadata().isAnnotated(ImportResource.class.getName())) { - AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); - String[] resources = importResource.getAliasedStringArray("locations", ImportResource.class, sourceClass); + AnnotationAttributes importResource = + AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); + String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); @@ -357,16 +366,26 @@ class ConfigurationClassParser { */ private void processPropertySource(AnnotationAttributes propertySource) throws IOException { String name = propertySource.getString("name"); + if (!StringUtils.hasLength(name)) { + name = null; + } + String encoding = propertySource.getString("encoding"); + if (!StringUtils.hasLength(encoding)) { + encoding = null; + } String[] locations = propertySource.getStringArray("value"); - boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound"); Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required"); + boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound"); + + Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory"); + PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ? + DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass)); + for (String location : locations) { try { String resolvedLocation = this.environment.resolveRequiredPlaceholders(location); Resource resource = this.resourceLoader.getResource(resolvedLocation); - ResourcePropertySource rps = (StringUtils.hasText(name) ? - new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource)); - addPropertySource(rps); + addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding))); } catch (IllegalArgumentException ex) { // from resolveRequiredPlaceholders @@ -383,21 +402,23 @@ class ConfigurationClassParser { } } - private void addPropertySource(ResourcePropertySource propertySource) { + private void addPropertySource(PropertySource<?> propertySource) { String name = propertySource.getName(); MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources(); if (propertySources.contains(name) && this.propertySourceNames.contains(name)) { // We've already added a version, we need to extend it PropertySource<?> existing = propertySources.get(name); + PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ? + ((ResourcePropertySource) propertySource).withResourceName() : propertySource); if (existing instanceof CompositePropertySource) { - ((CompositePropertySource) existing).addFirstPropertySource(propertySource.withResourceName()); + ((CompositePropertySource) existing).addFirstPropertySource(newSource); } else { if (existing instanceof ResourcePropertySource) { existing = ((ResourcePropertySource) existing).withResourceName(); } CompositePropertySource composite = new CompositePropertySource(name); - composite.addPropertySource(propertySource.withResourceName()); + composite.addPropertySource(newSource); composite.addPropertySource(existing); propertySources.replace(name, composite); } @@ -463,7 +484,7 @@ class ConfigurationClassParser { catch (BeanDefinitionStoreException ex) { throw ex; } - catch (Exception ex) { + catch (Throwable ex) { throw new BeanDefinitionStoreException("Failed to process import candidates for configuration class [" + configClass.getMetadata().getClassName() + "]", ex); } @@ -477,7 +498,7 @@ class ConfigurationClassParser { return; } - if (checkForCircularImports && this.importStack.contains(configClass)) { + if (checkForCircularImports && isChainedImportOnStack(configClass)) { this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); } else { @@ -520,7 +541,7 @@ class ConfigurationClassParser { catch (BeanDefinitionStoreException ex) { throw ex; } - catch (Exception ex) { + catch (Throwable ex) { throw new BeanDefinitionStoreException("Failed to process import candidates for configuration class [" + configClass.getMetadata().getClassName() + "]", ex); } @@ -530,6 +551,20 @@ class ConfigurationClassParser { } } + private boolean isChainedImportOnStack(ConfigurationClass configClass) { + if (this.importStack.contains(configClass)) { + String configClassName = configClass.getMetadata().getClassName(); + AnnotationMetadata importingClass = this.importStack.getImportingClassFor(configClassName); + while (importingClass != null) { + if (configClassName.equals(importingClass.getClassName())) { + return true; + } + importingClass = this.importStack.getImportingClassFor(importingClass.getClassName()); + } + } + return false; + } + /** * Invoke {@link ResourceLoaderAware}, {@link BeanClassLoaderAware} and * {@link BeanFactoryAware} contracts if implemented by the given {@code bean}. @@ -836,7 +871,7 @@ class ConfigurationClassParser { private SourceClass getRelated(String className) throws IOException { if (this.source instanceof Class<?>) { try { - Class<?> clazz = resourceLoader.getClassLoader().loadClass(className); + Class<?> clazz = ((Class<?>) this.source).getClassLoader().loadClass(className); return asSourceClass(clazz); } catch (ClassNotFoundException ex) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java b/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java index e2d652c8..bb82665f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java @@ -102,6 +102,7 @@ import java.lang.annotation.Target; * }</pre> * * @author Chris Beams + * @author Juergen Hoeller * @since 3.1 * @see org.aspectj.lang.annotation.Aspect */ @@ -117,4 +118,12 @@ public @interface EnableAspectJAutoProxy { */ boolean proxyTargetClass() default false; + /** + * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal} + * for retrieval via the {@link org.springframework.aop.framework.AopContext} class. + * Off by default, i.e. no guarantees that {@code AopContext} access will work. + * @since 4.3.1 + */ + boolean exposeProxy() default false; + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java index 6795d617..1dce3d41 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java @@ -33,7 +33,7 @@ import org.springframework.core.type.AnnotationMetadata; * {@link org.springframework.beans.factory.Aware Aware} interfaces, and their respective * methods will be called prior to {@link #registerBeanDefinitions}: * <ul> - * <li>{@link org.springframework.context.EnvironmentAware}</li> + * <li>{@link org.springframework.context.EnvironmentAware EnvironmentAware}</li> * <li>{@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware} * <li>{@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware} * <li>{@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java index b98719f9..c684426f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java @@ -27,7 +27,7 @@ import org.springframework.core.type.AnnotationMetadata; * {@link org.springframework.beans.factory.Aware Aware} interfaces, and their respective * methods will be called prior to {@link #selectImports}: * <ul> - * <li>{@link org.springframework.context.EnvironmentAware}</li> + * <li>{@link org.springframework.context.EnvironmentAware EnvironmentAware}</li> * <li>{@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware}</li> * <li>{@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware}</li> * <li>{@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}</li> diff --git a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java index 908f70a7..e4237110 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -23,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.io.support.PropertySourceFactory; + /** * Annotation providing a convenient and declarative mechanism for adding a * {@link org.springframework.core.env.PropertySource PropertySource} to Spring's @@ -138,6 +140,7 @@ import java.lang.annotation.Target; * javadocs for details. * * @author Chris Beams + * @author Juergen Hoeller * @author Phillip Webb * @since 3.1 * @see PropertySources @@ -153,9 +156,8 @@ import java.lang.annotation.Target; public @interface PropertySource { /** - * Indicate the name of this property source. If omitted, a name - * will be generated based on the description of the underlying - * resource. + * Indicate the name of this property source. If omitted, a name will + * be generated based on the description of the underlying resource. * @see org.springframework.core.env.PropertySource#getName() * @see org.springframework.core.io.Resource#getDescription() */ @@ -184,4 +186,19 @@ public @interface PropertySource { */ boolean ignoreResourceNotFound() default false; + /** + * A specific character encoding for the given resources, e.g. "UTF-8". + * @since 4.3 + */ + String encoding() default ""; + + /** + * Specify a custom {@link PropertySourceFactory}, if any. + * <p>By default, a default factory for standard resource files will be used. + * @since 4.3 + * @see org.springframework.core.io.support.DefaultPropertySourceFactory + * @see org.springframework.core.io.support.ResourcePropertySource + */ + Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class; + } diff --git a/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java index 5fb8af96..ae717b09 100644 --- a/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.context.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.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -36,15 +37,22 @@ import org.springframework.util.ClassUtils; */ class LoadTimeWeaverBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { - private static final String WEAVER_CLASS_ATTRIBUTE = "weaver-class"; + /** + * The bean name of the internally managed AspectJ weaving enabler. + * @since 4.3.1 + */ + public static final String ASPECTJ_WEAVING_ENABLER_BEAN_NAME = + "org.springframework.context.config.internalAspectJWeavingEnabler"; - private static final String ASPECTJ_WEAVING_ATTRIBUTE = "aspectj-weaving"; + private static final String ASPECTJ_WEAVING_ENABLER_CLASS_NAME = + "org.springframework.context.weaving.AspectJWeavingEnabler"; private static final String DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME = "org.springframework.context.weaving.DefaultContextLoadTimeWeaver"; - private static final String ASPECTJ_WEAVING_ENABLER_CLASS_NAME = - "org.springframework.context.weaving.AspectJWeavingEnabler"; + private static final String WEAVER_CLASS_ATTRIBUTE = "weaver-class"; + + private static final String ASPECTJ_WEAVING_ATTRIBUTE = "aspectj-weaving"; @Override @@ -65,9 +73,11 @@ class LoadTimeWeaverBeanDefinitionParser extends AbstractSingleBeanDefinitionPar builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); if (isAspectJWeavingEnabled(element.getAttribute(ASPECTJ_WEAVING_ATTRIBUTE), parserContext)) { - RootBeanDefinition weavingEnablerDef = new RootBeanDefinition(); - weavingEnablerDef.setBeanClassName(ASPECTJ_WEAVING_ENABLER_CLASS_NAME); - parserContext.getReaderContext().registerWithGeneratedName(weavingEnablerDef); + if (!parserContext.getRegistry().containsBeanDefinition(ASPECTJ_WEAVING_ENABLER_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(ASPECTJ_WEAVING_ENABLER_CLASS_NAME); + parserContext.registerBeanComponent( + new BeanComponentDefinition(def, ASPECTJ_WEAVING_ENABLER_BEAN_NAME)); + } if (isBeanConfigurerAspectEnabled(parserContext.getReaderContext().getBeanClassLoader())) { new SpringConfiguredBeanDefinitionParser().parse(element, parserContext); diff --git a/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java index f2801597..cb38e6b0 100644 --- a/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -69,7 +69,9 @@ class PropertyPlaceholderBeanDefinitionParser extends AbstractPropertyLoadingBea if (element.hasAttribute("value-separator")) { builder.addPropertyValue("valueSeparator", element.getAttribute("value-separator")); } - + if (element.hasAttribute("trim-values")) { + builder.addPropertyValue("trimValues", element.getAttribute("trim-values")); + } if (element.hasAttribute("null-value")) { builder.addPropertyValue("nullValue", element.getAttribute("null-value")); } diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java index 0eb68e9e..b7dd9186 100644 --- a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -286,7 +286,7 @@ public abstract class AbstractApplicationEventMulticaster /** * Cache key for ListenerRetrievers, based on event type and source type. */ - private static class ListenerCacheKey { + private static final class ListenerCacheKey implements Comparable<ListenerCacheKey> { private final ResolvableType eventType; @@ -311,6 +311,23 @@ public abstract class AbstractApplicationEventMulticaster public int hashCode() { return (ObjectUtils.nullSafeHashCode(this.eventType) * 29 + ObjectUtils.nullSafeHashCode(this.sourceType)); } + + @Override + public String toString() { + return "ListenerCacheKey [eventType = " + this.eventType + ", sourceType = " + this.sourceType.getName() + "]"; + } + + @Override + public int compareTo(ListenerCacheKey other) { + int result = 0; + if (this.eventType != null) { + result = this.eventType.toString().compareTo(other.eventType.toString()); + } + if (result == 0 && this.sourceType != null) { + result = this.sourceType.getName().compareTo(other.sourceType.getName()); + } + return result; + } } diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index 243d1642..a57eb0c6 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -35,7 +35,6 @@ import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.expression.EvaluationContext; import org.springframework.util.Assert; @@ -207,14 +206,14 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe if (StringUtils.hasText(condition)) { Assert.notNull(this.evaluator, "EventExpressionEvaluator must no be null"); EvaluationContext evaluationContext = this.evaluator.createEvaluationContext( - event, this.targetClass, this.method, args); + event, this.targetClass, this.method, args, this.applicationContext); return this.evaluator.condition(condition, this.methodKey, evaluationContext); } return true; } protected <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) { - return AnnotationUtils.findAnnotation(this.method, annotationType); + return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); } /** diff --git a/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java index 9bf4fcf6..8cb00fe8 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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,12 +21,12 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.CachedExpressionEvaluator; import org.springframework.context.expression.MethodBasedEvaluationContext; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -40,23 +40,26 @@ import org.springframework.expression.Expression; */ class EventExpressionEvaluator extends CachedExpressionEvaluator { - // shared param discoverer since it caches data internally - private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer(); - private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<ExpressionKey, Expression>(64); private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<AnnotatedElementKey, Method>(64); + /** * Create the suitable {@link EvaluationContext} for the specified event handling * on the specified method. */ public EvaluationContext createEvaluationContext(ApplicationEvent event, Class<?> targetClass, - Method method, Object[] args) { + Method method, Object[] args, BeanFactory beanFactory) { Method targetMethod = getTargetMethod(targetClass, method); EventExpressionRootObject root = new EventExpressionRootObject(event, args); - return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer); + MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext( + root, targetMethod, args, getParameterNameDiscoverer()); + if (beanFactory != null) { + evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } + return evaluationContext; } /** diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java index e24cac8d..7a95f172 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -30,6 +30,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.aop.scope.ScopedObject; import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.SmartInitializingSingleton; @@ -38,8 +39,8 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.MethodIntrospector; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -131,7 +132,7 @@ public class EventListenerMethodProcessor implements SmartInitializingSingleton, new MethodIntrospector.MetadataLookup<EventListener>() { @Override public EventListener inspect(Method method) { - return AnnotationUtils.findAnnotation(method, EventListener.class); + return AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class); } }); } @@ -152,7 +153,7 @@ public class EventListenerMethodProcessor implements SmartInitializingSingleton, for (Method method : annotatedMethods.keySet()) { for (EventListenerFactory factory : factories) { if (factory.supportsMethod(method)) { - Method methodToUse = MethodIntrospector.selectInvocableMethod( + Method methodToUse = AopUtils.selectInvocableMethod( method, this.applicationContext.getType(beanName)); ApplicationListener<?> applicationListener = factory.createApplicationListener(beanName, targetType, methodToUse); diff --git a/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java b/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java index 8f7f88a9..50812c11 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java +++ b/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java @@ -30,7 +30,7 @@ import org.springframework.util.ObjectUtils; * @since 4.2 * @see CachedExpressionEvaluator */ -public final class AnnotatedElementKey { +public final class AnnotatedElementKey implements Comparable<AnnotatedElementKey> { private final AnnotatedElement element; @@ -66,4 +66,18 @@ public final class AnnotatedElementKey { return this.element.hashCode() + (this.targetClass != null ? this.targetClass.hashCode() * 29 : 0); } + @Override + public String toString() { + return this.element + (this.targetClass != null ? " on " + this.targetClass : ""); + } + + @Override + public int compareTo(AnnotatedElementKey other) { + int result = this.element.toString().compareTo(other.element.toString()); + if (result == 0 && this.targetClass != null) { + result = this.targetClass.getName().compareTo(other.targetClass.getName()); + } + return result; + } + } diff --git a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java index 4bb20b28..d530db80 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java @@ -18,6 +18,8 @@ package org.springframework.context.expression; import java.util.Map; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -35,12 +37,14 @@ public abstract class CachedExpressionEvaluator { private final SpelExpressionParser parser; + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + /** * Create a new instance with the specified {@link SpelExpressionParser}. */ protected CachedExpressionEvaluator(SpelExpressionParser parser) { - Assert.notNull(parser, "Parser must not be null"); + Assert.notNull(parser, "SpelExpressionParser must not be null"); this.parser = parser; } @@ -59,6 +63,14 @@ public abstract class CachedExpressionEvaluator { return this.parser; } + /** + * Return a shared parameter name discoverer which caches data internally. + * @since 4.3 + */ + protected ParameterNameDiscoverer getParameterNameDiscoverer() { + return this.parameterNameDiscoverer; + } + /** * Return the {@link Expression} for the specified SpEL value @@ -84,14 +96,14 @@ public abstract class CachedExpressionEvaluator { } - protected static class ExpressionKey { + protected static class ExpressionKey implements Comparable<ExpressionKey> { - private final AnnotatedElementKey key; + private final AnnotatedElementKey element; private final String expression; - protected ExpressionKey(AnnotatedElementKey key, String expression) { - this.key = key; + protected ExpressionKey(AnnotatedElementKey element, String expression) { + this.element = element; this.expression = expression; } @@ -104,13 +116,27 @@ public abstract class CachedExpressionEvaluator { return false; } ExpressionKey otherKey = (ExpressionKey) other; - return (this.key.equals(otherKey.key) && + return (this.element.equals(otherKey.element) && ObjectUtils.nullSafeEquals(this.expression, otherKey.expression)); } @Override public int hashCode() { - return this.key.hashCode() + (this.expression != null ? this.expression.hashCode() * 29 : 0); + return this.element.hashCode() + (this.expression != null ? this.expression.hashCode() * 29 : 0); + } + + @Override + public String toString() { + return this.element + (this.expression != null ? " with expression \"" + this.expression : "\""); + } + + @Override + public int compareTo(ExpressionKey other) { + int result = this.element.toString().compareTo(other.element.toString()); + if (result == 0 && this.expression != null) { + result = this.expression.compareTo(other.expression); + } + return result; } } diff --git a/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java index b6d783b2..6c564535 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java +++ b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java @@ -34,6 +34,7 @@ import org.springframework.util.ObjectUtils; * </ol> * * @author Stephane Nicoll + * @author Sergey Podgurskiy * @since 4.2 */ public class MethodBasedEvaluationContext extends StandardEvaluationContext { @@ -89,7 +90,7 @@ public class MethodBasedEvaluationContext extends StandardEvaluationContext { String[] parameterNames = this.paramDiscoverer.getParameterNames(this.method); // save parameter names (if discovered) if (parameterNames != null) { - for (int i = 0; i < parameterNames.length; i++) { + for (int i = 0; i < this.args.length; i++) { setVariable(parameterNames[i], this.args[i]); } } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index d21d2817..2e462c01 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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 @@ import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringValueResolver; /** * Abstract implementation of the {@link org.springframework.context.ApplicationContext} @@ -461,8 +462,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader } @Override - public void addBeanFactoryPostProcessor(BeanFactoryPostProcessor beanFactoryPostProcessor) { - this.beanFactoryPostProcessors.add(beanFactoryPostProcessor); + public void addBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor) { + Assert.notNull(postProcessor, "BeanFactoryPostProcessor must not be null"); + this.beanFactoryPostProcessors.add(postProcessor); } @@ -476,6 +478,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader @Override public void addApplicationListener(ApplicationListener<?> listener) { + Assert.notNull(listener, "ApplicationListener must not be null"); if (this.applicationEventMulticaster != null) { this.applicationEventMulticaster.addApplicationListener(listener); } @@ -676,6 +679,13 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader */ protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); + + // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime + // (e.g. through an @Bean method registered by ConfigurationClassPostProcessor) + if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { + beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); + beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); + } } /** @@ -823,6 +833,18 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)); } + // Register a default embedded value resolver if no bean post-processor + // (such as a PropertyPlaceholderConfigurer bean) registered any before: + // at this point, primarily for resolution in annotation attribute values. + if (!beanFactory.hasEmbeddedValueResolver()) { + beanFactory.addEmbeddedValueResolver(new StringValueResolver() { + @Override + public String resolveStringValue(String strVal) { + return getEnvironment().resolvePlaceholders(strVal); + } + }); + } + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); for (String weaverAwareName : weaverAwareNames) { diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java new file mode 100644 index 00000000..404af2eb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java @@ -0,0 +1,208 @@ +/* + * Copyright 2002-2016 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.support; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Abstract base class for {@code MessageSource} implementations based on + * resource bundle conventions, such as {@link ResourceBundleMessageSource} + * and {@link ReloadableResourceBundleMessageSource}. Provides common + * configuration methods and corresponding semantic definitions. + * + * @author Juergen Hoeller + * @since 4.3 + * @see ResourceBundleMessageSource + * @see ReloadableResourceBundleMessageSource + */ +public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource { + + private final Set<String> basenameSet = new LinkedHashSet<String>(4); + + private String defaultEncoding; + + private boolean fallbackToSystemLocale = true; + + private long cacheMillis = -1; + + + /** + * Set a single basename, following the basic ResourceBundle convention + * of not specifying file extension or language codes. The resource location + * format is up to the specific {@code MessageSource} implementation. + * <p>Regular and XMl properties files are supported: e.g. "messages" will find + * a "messages.properties", "messages_en.properties" etc arrangement as well + * as "messages.xml", "messages_en.xml" etc. + * @param basename the single basename + * @see #setBasenames + * @see org.springframework.core.io.ResourceEditor + * @see java.util.ResourceBundle + */ + public void setBasename(String basename) { + setBasenames(basename); + } + + /** + * Set an array of basenames, each following the basic ResourceBundle convention + * of not specifying file extension or language codes. The resource location + * format is up to the specific {@code MessageSource} implementation. + * <p>Regular and XMl properties files are supported: e.g. "messages" will find + * a "messages.properties", "messages_en.properties" etc arrangement as well + * as "messages.xml", "messages_en.xml" etc. + * <p>The associated resource bundles will be checked sequentially when resolving + * a message code. Note that message definitions in a <i>previous</i> resource + * bundle will override ones in a later bundle, due to the sequential lookup. + * <p>Note: In contrast to {@link #addBasenames}, this replaces existing entries + * with the given names and can therefore also be used to reset the configuration. + * @param basenames an array of basenames + * @see #setBasename + * @see java.util.ResourceBundle + */ + public void setBasenames(String... basenames) { + this.basenameSet.clear(); + addBasenames(basenames); + } + + /** + * Add the specified basenames to the existing basename configuration. + * <p>Note: If a given basename already exists, the position of its entry + * will remain as in the original set. New entries will be added at the + * end of the list, to be searched after existing basenames. + * @since 4.3 + * @see #setBasenames + * @see java.util.ResourceBundle + */ + public void addBasenames(String... basenames) { + if (!ObjectUtils.isEmpty(basenames)) { + for (String basename : basenames) { + Assert.hasText(basename, "Basename must not be empty"); + this.basenameSet.add(basename.trim()); + } + } + } + + /** + * Return this {@code MessageSource}'s basename set, containing entries + * in the order of registration. + * <p>Calling code may introspect this set as well as add or remove entries. + * @since 4.3 + * @see #addBasenames + */ + public Set<String> getBasenameSet() { + return this.basenameSet; + } + + /** + * Set the default charset to use for parsing properties files. + * Used if no file-specific charset is specified for a file. + * <p>Default is none, using the {@code java.util.Properties} + * default encoding: ISO-8859-1. + * <p>Only applies to classic properties files, not to XML files. + * @param defaultEncoding the default charset + */ + public void setDefaultEncoding(String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + } + + /** + * Return the default charset to use for parsing properties files, if any. + * @since 4.3 + */ + protected String getDefaultEncoding() { + return this.defaultEncoding; + } + + /** + * Set whether to fall back to the system Locale if no files for a specific + * Locale have been found. Default is "true"; if this is turned off, the only + * fallback will be the default file (e.g. "messages.properties" for + * basename "messages"). + * <p>Falling back to the system Locale is the default behavior of + * {@code java.util.ResourceBundle}. However, this is often not desirable + * in an application server environment, where the system Locale is not relevant + * to the application at all: set this flag to "false" in such a scenario. + */ + public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { + this.fallbackToSystemLocale = fallbackToSystemLocale; + } + + /** + * Return whether to fall back to the system Locale if no files for a specific + * Locale have been found. + * @since 4.3 + */ + protected boolean isFallbackToSystemLocale() { + return this.fallbackToSystemLocale; + } + + /** + * Set the number of seconds to cache loaded properties files. + * <ul> + * <li>Default is "-1", indicating to cache forever (just like + * {@code java.util.ResourceBundle}). + * <li>A positive number will cache loaded properties files for the given + * number of seconds. This is essentially the interval between refresh checks. + * Note that a refresh attempt will first check the last-modified timestamp + * of the file before actually reloading it; so if files don't change, this + * interval can be set rather low, as refresh attempts will not actually reload. + * <li>A value of "0" will check the last-modified timestamp of the file on + * every message access. <b>Do not use this in a production environment!</b> + * </ul> + * <p><b>Note that depending on your ClassLoader, expiration might not work reliably + * since the ClassLoader may hold on to a cached version of the bundle file.</b> + * Prefer {@link ReloadableResourceBundleMessageSource} over + * {@link ResourceBundleMessageSource} in such a scenario, in combination with + * a non-classpath location. + */ + public void setCacheSeconds(int cacheSeconds) { + this.cacheMillis = (cacheSeconds * 1000); + } + + /** + * Set the number of milliseconds to cache loaded properties files. + * Note that it is common to set seconds instead: {@link #setCacheSeconds}. + * <ul> + * <li>Default is "-1", indicating to cache forever (just like + * {@code java.util.ResourceBundle}). + * <li>A positive number will cache loaded properties files for the given + * number of milliseconds. This is essentially the interval between refresh checks. + * Note that a refresh attempt will first check the last-modified timestamp + * of the file before actually reloading it; so if files don't change, this + * interval can be set rather low, as refresh attempts will not actually reload. + * <li>A value of "0" will check the last-modified timestamp of the file on + * every message access. <b>Do not use this in a production environment!</b> + * </ul> + * @since 4.3 + * @see #setCacheSeconds + */ + public void setCacheMillis(long cacheMillis) { + this.cacheMillis = cacheMillis; + } + + /** + * Return the number of milliseconds to cache loaded properties files. + * @since 4.3 + */ + protected long getCacheMillis() { + return this.cacheMillis; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java index 2a521b3b..c3f3dc54 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 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. @@ -23,7 +23,7 @@ import java.security.PrivilegedAction; import org.springframework.beans.BeansException; import org.springframework.beans.factory.Aware; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ConfigurableApplicationContext; @@ -61,12 +61,15 @@ class ApplicationContextAwareProcessor implements BeanPostProcessor { private final ConfigurableApplicationContext applicationContext; + private final StringValueResolver embeddedValueResolver; + /** * Create a new ApplicationContextAwareProcessor for the given context. */ public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicationContext) { this.applicationContext = applicationContext; + this.embeddedValueResolver = new EmbeddedValueResolver(applicationContext.getBeanFactory()); } @@ -103,8 +106,7 @@ class ApplicationContextAwareProcessor implements BeanPostProcessor { ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); } if (bean instanceof EmbeddedValueResolverAware) { - ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver( - new EmbeddedValueResolver(this.applicationContext.getBeanFactory())); + ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver); } if (bean instanceof ResourceLoaderAware) { ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); @@ -126,19 +128,4 @@ class ApplicationContextAwareProcessor implements BeanPostProcessor { return bean; } - - private static class EmbeddedValueResolver implements StringValueResolver { - - private final ConfigurableBeanFactory beanFactory; - - public EmbeddedValueResolver(ConfigurableBeanFactory beanFactory) { - this.beanFactory = beanFactory; - } - - @Override - public String resolveStringValue(String strVal) { - return this.beanFactory.resolveEmbeddedValue(strVal); - } - } - } diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index 9419e14b..521d273f 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -292,7 +292,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor * <p>The default implementation checks for the {@link Phased} interface. * Can be overridden to apply other/further policies. * @param bean the bean to introspect - * @return the phase an an integer value. The suggested default is 0. + * @return the phase an integer value. The suggested default is 0. * @see Phased * @see SmartLifecycle */ diff --git a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java index 70fd2e83..0557b6bd 100644 --- a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java +++ b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java @@ -409,6 +409,11 @@ class PostProcessorRegistrationDelegate { multicaster.removeApplicationListenerBean(beanName); } } + + @Override + public boolean requiresDestruction(Object bean) { + return (bean instanceof ApplicationListener); + } } } diff --git a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java index 577eb93f..4a12a31e 100644 --- a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java +++ b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -167,9 +167,12 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS StringValueResolver valueResolver = new StringValueResolver() { @Override public String resolveStringValue(String strVal) { - String resolved = ignoreUnresolvablePlaceholders ? + String resolved = (ignoreUnresolvablePlaceholders ? propertyResolver.resolvePlaceholders(strVal) : - propertyResolver.resolveRequiredPlaceholders(strVal); + propertyResolver.resolveRequiredPlaceholders(strVal)); + if (trimValues) { + resolved = resolved.trim(); + } return (resolved.equals(nullValue) ? null : resolved); } }; diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index 73de21f4..92fecb1a 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -33,7 +33,6 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.util.Assert; import org.springframework.util.DefaultPropertiesPersister; import org.springframework.util.PropertiesPersister; import org.springframework.util.StringUtils; @@ -58,14 +57,14 @@ import org.springframework.util.StringUtils; * prefix, resources can still be loaded from the classpath, but "cacheSeconds" values * other than "-1" (caching forever) might not work reliably in this case. * - * <p>For a typical web application, message files could be placed into {@code WEB-INF}: - * e.g. a "WEB-INF/messages" basename would fine a "WEB-INF/messages.properties", + * <p>For a typical web application, message files could be placed in {@code WEB-INF}: + * e.g. a "WEB-INF/messages" basename would find a "WEB-INF/messages.properties", * "WEB-INF/messages_en.properties" etc arrangement as well as "WEB-INF/messages.xml", * "WEB-INF/messages_en.xml" etc. Note that message definitions in a <i>previous</i> * resource bundle will override ones in a later bundle, due to sequential lookup. * <p>This MessageSource can easily be used outside of an - * {@link org.springframework.context.ApplicationContext}: It will use a + * {@link org.springframework.context.ApplicationContext}: it will use a * {@link org.springframework.core.io.DefaultResourceLoader} as default, * simply getting overridden with the ApplicationContext's resource loader * if running in a context. It does not have any other specific dependencies. @@ -85,23 +84,15 @@ import org.springframework.util.StringUtils; * @see ResourceBundleMessageSource * @see java.util.ResourceBundle */ -public class ReloadableResourceBundleMessageSource extends AbstractMessageSource implements ResourceLoaderAware { +public class ReloadableResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements ResourceLoaderAware { private static final String PROPERTIES_SUFFIX = ".properties"; private static final String XML_SUFFIX = ".xml"; - private String[] basenames = new String[0]; - - private String defaultEncoding; - private Properties fileEncodings; - private boolean fallbackToSystemLocale = true; - - private long cacheMillis = -1; - private boolean concurrentRefresh = true; private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); @@ -122,71 +113,11 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource /** - * Set a single basename, following the basic ResourceBundle convention of - * not specifying file extension or language codes, but in contrast to - * {@link ResourceBundleMessageSource} referring to a Spring resource location: - * e.g. "WEB-INF/messages" for "WEB-INF/messages.properties", - * "WEB-INF/messages_en.properties", etc. - * <p>XML properties files are also supported: .g. "WEB-INF/messages" will find - * and load "WEB-INF/messages.xml", "WEB-INF/messages_en.xml", etc as well. - * @param basename the single basename - * @see #setBasenames - * @see org.springframework.core.io.ResourceEditor - * @see java.util.ResourceBundle - */ - public void setBasename(String basename) { - setBasenames(basename); - } - - /** - * Set an array of basenames, each following the basic ResourceBundle convention - * of not specifying file extension or language codes, but in contrast to - * {@link ResourceBundleMessageSource} referring to a Spring resource location: - * e.g. "WEB-INF/messages" for "WEB-INF/messages.properties", - * "WEB-INF/messages_en.properties", etc. - * <p>XML properties files are also supported: .g. "WEB-INF/messages" will find - * and load "WEB-INF/messages.xml", "WEB-INF/messages_en.xml", etc as well. - * <p>The associated resource bundles will be checked sequentially when resolving - * a message code. Note that message definitions in a <i>previous</i> resource - * bundle will override ones in a later bundle, due to the sequential lookup. - * @param basenames an array of basenames - * @see #setBasename - * @see java.util.ResourceBundle - */ - public void setBasenames(String... basenames) { - if (basenames != null) { - this.basenames = new String[basenames.length]; - for (int i = 0; i < basenames.length; i++) { - String basename = basenames[i]; - Assert.hasText(basename, "Basename must not be empty"); - this.basenames[i] = basename.trim(); - } - } - else { - this.basenames = new String[0]; - } - } - - /** - * Set the default charset to use for parsing properties files. - * Used if no file-specific charset is specified for a file. - * <p>Default is none, using the {@code java.util.Properties} - * default encoding: ISO-8859-1. - * <p>Only applies to classic properties files, not to XML files. - * @param defaultEncoding the default charset - * @see #setFileEncodings - * @see org.springframework.util.PropertiesPersister#load - */ - public void setDefaultEncoding(String defaultEncoding) { - this.defaultEncoding = defaultEncoding; - } - - /** * Set per-file charsets to use for parsing properties files. * <p>Only applies to classic properties files, not to XML files. * @param fileEncodings Properties with filenames as keys and charset * names as values. Filenames have to match the basename syntax, - * with optional locale-specific appendices: e.g. "WEB-INF/messages" + * with optional locale-specific components: e.g. "WEB-INF/messages" * or "WEB-INF/messages_en". * @see #setBasenames * @see org.springframework.util.PropertiesPersister#load @@ -196,43 +127,11 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource } /** - * Set whether to fall back to the system Locale if no files for a specific - * Locale have been found. Default is "true"; if this is turned off, the only - * fallback will be the default file (e.g. "messages.properties" for - * basename "messages"). - * <p>Falling back to the system Locale is the default behavior of - * {@code java.util.ResourceBundle}. However, this is often not desirable - * in an application server environment, where the system Locale is not relevant - * to the application at all: Set this flag to "false" in such a scenario. - */ - public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { - this.fallbackToSystemLocale = fallbackToSystemLocale; - } - - /** - * Set the number of seconds to cache loaded properties files. - * <ul> - * <li>Default is "-1", indicating to cache forever (just like - * {@code java.util.ResourceBundle}). - * <li>A positive number will cache loaded properties files for the given - * number of seconds. This is essentially the interval between refresh checks. - * Note that a refresh attempt will first check the last-modified timestamp - * of the file before actually reloading it; so if files don't change, this - * interval can be set rather low, as refresh attempts will not actually reload. - * <li>A value of "0" will check the last-modified timestamp of the file on - * every message access. <b>Do not use this in a production environment!</b> - * </ul> - */ - public void setCacheSeconds(int cacheSeconds) { - this.cacheMillis = (cacheSeconds * 1000); - } - - /** * Specify whether to allow for concurrent refresh behavior, i.e. one thread * locked in a refresh attempt for a specific cached properties file whereas * other threads keep returning the old properties for the time being, until * the refresh attempt has completed. - * <p>Default is "true": This behavior is new as of Spring Framework 4.1, + * <p>Default is "true": this behavior is new as of Spring Framework 4.1, * minimizing contention between threads. If you prefer the old behavior, * i.e. to fully block on refresh, switch this flag to "false". * @since 4.1 @@ -273,7 +172,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource */ @Override protected String resolveCodeWithoutArguments(String code, Locale locale) { - if (this.cacheMillis < 0) { + if (getCacheMillis() < 0) { PropertiesHolder propHolder = getMergedProperties(locale); String result = propHolder.getProperty(code); if (result != null) { @@ -281,7 +180,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource } } else { - for (String basename : this.basenames) { + for (String basename : getBasenameSet()) { List<String> filenames = calculateAllFilenames(basename, locale); for (String filename : filenames) { PropertiesHolder propHolder = getProperties(filename); @@ -301,7 +200,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource */ @Override protected MessageFormat resolveCode(String code, Locale locale) { - if (this.cacheMillis < 0) { + if (getCacheMillis() < 0) { PropertiesHolder propHolder = getMergedProperties(locale); MessageFormat result = propHolder.getMessageFormat(code, locale); if (result != null) { @@ -309,7 +208,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource } } else { - for (String basename : this.basenames) { + for (String basename : getBasenameSet()) { List<String> filenames = calculateAllFilenames(basename, locale); for (String filename : filenames) { PropertiesHolder propHolder = getProperties(filename); @@ -339,8 +238,9 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource } Properties mergedProps = newProperties(); mergedHolder = new PropertiesHolder(mergedProps, -1); - for (int i = this.basenames.length - 1; i >= 0; i--) { - List<String> filenames = calculateAllFilenames(this.basenames[i], locale); + String[] basenames = StringUtils.toStringArray(getBasenameSet()); + for (int i = basenames.length - 1; i >= 0; i--) { + List<String> filenames = calculateAllFilenames(basenames[i], locale); for (int j = filenames.size() - 1; j >= 0; j--) { String filename = filenames.get(j); PropertiesHolder propHolder = getProperties(filename); @@ -376,7 +276,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource } List<String> filenames = new ArrayList<String>(7); filenames.addAll(calculateFilenamesForLocale(basename, locale)); - if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) { + if (isFallbackToSystemLocale() && !locale.equals(Locale.getDefault())) { List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault()); for (String fallbackFilename : fallbackFilenames) { if (!filenames.contains(fallbackFilename)) { @@ -447,7 +347,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource if (propHolder != null) { originalTimestamp = propHolder.getRefreshTimestamp(); - if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - this.cacheMillis) { + if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - getCacheMillis()) { // Up to date return propHolder; } @@ -492,7 +392,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource * @param propHolder the current PropertiesHolder for the bundle */ protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) { - long refreshTimestamp = (this.cacheMillis < 0 ? -1 : System.currentTimeMillis()); + long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis()); Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX); if (!resource.exists()) { @@ -501,7 +401,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource if (resource.exists()) { long fileTimestamp = -1; - if (this.cacheMillis >= 0) { + if (getCacheMillis() >= 0) { // Last-modified timestamp of file will just be read if caching with timeout. try { fileTimestamp = resource.lastModified(); @@ -571,7 +471,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource encoding = this.fileEncodings.getProperty(filename); } if (encoding == null) { - encoding = this.defaultEncoding; + encoding = getDefaultEncoding(); } if (encoding != null) { if (logger.isDebugEnabled()) { @@ -631,7 +531,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource @Override public String toString() { - return getClass().getName() + ": basenames=[" + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]"; + return getClass().getName() + ": basenames=" + getBasenameSet(); } diff --git a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java index 1f623674..d7dce2b5 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java @@ -32,11 +32,10 @@ import java.util.Map; import java.util.MissingResourceException; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; +import java.util.Set; import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * {@link org.springframework.context.MessageSource} implementation that @@ -64,15 +63,7 @@ import org.springframework.util.StringUtils; * @see java.util.ResourceBundle * @see java.text.MessageFormat */ -public class ResourceBundleMessageSource extends AbstractMessageSource implements BeanClassLoaderAware { - - private String[] basenames = new String[0]; - - private String defaultEncoding = "ISO-8859-1"; - - private boolean fallbackToSystemLocale = true; - - private long cacheMillis = -1; +public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware { private ClassLoader bundleClassLoader; @@ -101,102 +92,6 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement /** - * Set a single basename, following {@link java.util.ResourceBundle} conventions: - * essentially, a fully-qualified classpath location. If it doesn't contain a - * package qualifier (such as {@code org.mypackage}), it will be resolved - * from the classpath root. - * <p>Messages will normally be held in the "/lib" or "/classes" directory of - * a web application's WAR structure. They can also be held in jar files on - * the class path. - * <p>Note that ResourceBundle names are effectively classpath locations: As a - * consequence, the JDK's standard ResourceBundle treats dots as package separators. - * This means that "test.theme" is effectively equivalent to "test/theme", - * just like it is for programmatic {@code java.util.ResourceBundle} usage. - * @see #setBasenames - * @see java.util.ResourceBundle#getBundle(String) - */ - public void setBasename(String basename) { - setBasenames(basename); - } - - /** - * Set an array of basenames, each following {@link java.util.ResourceBundle} - * conventions: essentially, a fully-qualified classpath location. If it - * doesn't contain a package qualifier (such as {@code org.mypackage}), - * it will be resolved from the classpath root. - * <p>The associated resource bundles will be checked sequentially - * when resolving a message code. Note that message definitions in a - * <i>previous</i> resource bundle will override ones in a later bundle, - * due to the sequential lookup. - * <p>Note that ResourceBundle names are effectively classpath locations: As a - * consequence, the JDK's standard ResourceBundle treats dots as package separators. - * This means that "test.theme" is effectively equivalent to "test/theme", - * just like it is for programmatic {@code java.util.ResourceBundle} usage. - * @see #setBasename - * @see java.util.ResourceBundle#getBundle(String) - */ - public void setBasenames(String... basenames) { - if (basenames != null) { - this.basenames = new String[basenames.length]; - for (int i = 0; i < basenames.length; i++) { - String basename = basenames[i]; - Assert.hasText(basename, "Basename must not be empty"); - this.basenames[i] = basename.trim(); - } - } - else { - this.basenames = new String[0]; - } - } - - /** - * Set the default charset to use for parsing resource bundle files. - * <p>Default is the {@code java.util.ResourceBundle} default encoding: - * ISO-8859-1. - * @since 3.1.3 - */ - public void setDefaultEncoding(String defaultEncoding) { - this.defaultEncoding = defaultEncoding; - } - - /** - * Set whether to fall back to the system Locale if no files for a specific - * Locale have been found. Default is "true"; if this is turned off, the only - * fallback will be the default file (e.g. "messages.properties" for - * basename "messages"). - * <p>Falling back to the system Locale is the default behavior of - * {@code java.util.ResourceBundle}. However, this is often not desirable - * in an application server environment, where the system Locale is not relevant - * to the application at all: Set this flag to "false" in such a scenario. - * @since 3.1.3 - */ - public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { - this.fallbackToSystemLocale = fallbackToSystemLocale; - } - - /** - * Set the number of seconds to cache loaded resource bundle files. - * <ul> - * <li>Default is "-1", indicating to cache forever. - * <li>A positive number will expire resource bundles after the given - * number of seconds. This is essentially the interval between refresh checks. - * Note that a refresh attempt will first check the last-modified timestamp - * of the file before actually reloading it; so if files don't change, this - * interval can be set rather low, as refresh attempts will not actually reload. - * <li>A value of "0" will check the last-modified timestamp of the file on - * every message access. <b>Do not use this in a production environment!</b> - * <li><b>Note that depending on your ClassLoader, expiration might not work reliably - * since the ClassLoader may hold on to a cached version of the bundle file.</b> - * Consider {@link ReloadableResourceBundleMessageSource} in combination - * with resource bundle files in a non-classpath location. - * </ul> - * @since 3.1.3 - */ - public void setCacheSeconds(int cacheSeconds) { - this.cacheMillis = (cacheSeconds * 1000); - } - - /** * Set the ClassLoader to load resource bundles with. * <p>Default is the containing BeanFactory's * {@link org.springframework.beans.factory.BeanClassLoaderAware bean ClassLoader}, @@ -229,14 +124,17 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement */ @Override protected String resolveCodeWithoutArguments(String code, Locale locale) { - String result = null; - for (int i = 0; result == null && i < this.basenames.length; i++) { - ResourceBundle bundle = getResourceBundle(this.basenames[i], locale); + Set<String> basenames = getBasenameSet(); + for (String basename : basenames) { + ResourceBundle bundle = getResourceBundle(basename, locale); if (bundle != null) { - result = getStringOrNull(bundle, code); + String result = getStringOrNull(bundle, code); + if (result != null) { + return result; + } } } - return result; + return null; } /** @@ -245,14 +143,17 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement */ @Override protected MessageFormat resolveCode(String code, Locale locale) { - MessageFormat messageFormat = null; - for (int i = 0; messageFormat == null && i < this.basenames.length; i++) { - ResourceBundle bundle = getResourceBundle(this.basenames[i], locale); + Set<String> basenames = getBasenameSet(); + for (String basename : basenames) { + ResourceBundle bundle = getResourceBundle(basename, locale); if (bundle != null) { - messageFormat = getMessageFormat(bundle, code, locale); + MessageFormat messageFormat = getMessageFormat(bundle, code, locale); + if (messageFormat != null) { + return messageFormat; + } } } - return messageFormat; + return null; } @@ -265,7 +166,7 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement * found for the given basename and Locale */ protected ResourceBundle getResourceBundle(String basename, Locale locale) { - if (this.cacheMillis >= 0) { + if (getCacheMillis() >= 0) { // Fresh ResourceBundle.getBundle call in order to let ResourceBundle // do its native caching, at the expense of more extensive lookup steps. return doGetBundle(basename, locale); @@ -404,7 +305,7 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement */ @Override public String toString() { - return getClass().getName() + ": basenames=[" + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]"; + return getClass().getName() + ": basenames=" + getBasenameSet(); } @@ -453,8 +354,12 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement throw (IOException) ex.getException(); } if (stream != null) { + String encoding = getDefaultEncoding(); + if (encoding == null) { + encoding = "ISO-8859-1"; + } try { - return loadBundle(new InputStreamReader(stream, defaultEncoding)); + return loadBundle(new InputStreamReader(stream, encoding)); } finally { stream.close(); @@ -472,11 +377,12 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement @Override public Locale getFallbackLocale(String baseName, Locale locale) { - return (fallbackToSystemLocale ? super.getFallbackLocale(baseName, locale) : null); + return (isFallbackToSystemLocale() ? super.getFallbackLocale(baseName, locale) : null); } @Override public long getTimeToLive(String baseName, Locale locale) { + long cacheMillis = getCacheMillis(); return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale)); } diff --git a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java index c889df80..3c84ff39 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java @@ -75,6 +75,12 @@ public @interface DateTimeFormat { * <p>Defaults to empty String, indicating no custom pattern String has been specified. * Set this attribute when you wish to format your field in accordance with a custom * date time pattern not represented by a style or ISO format. + * <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat} style, + * as also supported by Joda-Time, with strict parsing semantics towards overflows + * (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy' + * characters indicate a year in the traditional style, not a "year-of-era" as in the + * {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu' + * when going through that {@code DateTimeFormatter} with strict resolution mode). */ String pattern() default ""; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java index ca895d85..fb4f50b3 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.format.datetime.standard; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; +import java.time.format.ResolverStyle; import java.util.TimeZone; import org.springframework.format.annotation.DateTimeFormat.ISO; @@ -174,7 +175,11 @@ public class DateTimeFormatterFactory { public DateTimeFormatter createDateTimeFormatter(DateTimeFormatter fallbackFormatter) { DateTimeFormatter dateTimeFormatter = null; if (StringUtils.hasLength(this.pattern)) { - dateTimeFormatter = DateTimeFormatter.ofPattern(this.pattern); + // Using strict parsing to align with Joda-Time and standard DateFormat behavior: + // otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected. + // However, with strict parsing, a year digit needs to be specified as 'u'... + String patternToUse = this.pattern.replace("yy", "uu"); + dateTimeFormatter = DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT); } else if (this.iso != null && this.iso != ISO.NONE) { switch (this.iso) { diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index ee8d039a..9063f82d 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.format.datetime.standard; import java.text.ParseException; import java.time.Instant; +import java.time.format.DateTimeFormatter; import java.util.Locale; import org.springframework.format.Formatter; @@ -26,18 +27,29 @@ import org.springframework.lang.UsesJava8; /** * {@link Formatter} implementation for a JSR-310 {@link java.time.Instant}, * following JSR-310's parsing rules for an Instant (that is, not using a - * configurable {@link java.time.format.DateTimeFormatter}). + * configurable {@link java.time.format.DateTimeFormatter}): accepting the + * default {@code ISO_INSTANT} format as well as {@code RFC_1123_DATE_TIME} + * (which is commonly used for HTTP date header values), as of Spring 4.3. * * @author Juergen Hoeller * @since 4.0 * @see java.time.Instant#parse + * @see java.time.format.DateTimeFormatter#ISO_INSTANT + * @see java.time.format.DateTimeFormatter#RFC_1123_DATE_TIME */ @UsesJava8 public class InstantFormatter implements Formatter<Instant> { @Override public Instant parse(String text, Locale locale) throws ParseException { - return Instant.parse(text); + if (text.length() > 0 && Character.isDigit(text.charAt(0))) { + // assuming UTC instant a la "2007-12-03T10:15:30.00Z" + return Instant.parse(text); + } + else { + // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" + return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); + } } @Override diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java index f4fa2cc6..ac807eee 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java @@ -22,6 +22,8 @@ import java.lang.reflect.Method; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.DecoratingClassLoader; +import org.springframework.core.OverridingClassLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -129,7 +131,10 @@ public class ReflectiveLoadTimeWeaver implements LoadTimeWeaver { @Override public ClassLoader getThrowawayClassLoader() { if (this.getThrowawayClassLoaderMethod != null) { - return (ClassLoader) ReflectionUtils.invokeMethod(this.getThrowawayClassLoaderMethod, this.classLoader); + ClassLoader target = (ClassLoader) + ReflectionUtils.invokeMethod(this.getThrowawayClassLoaderMethod, this.classLoader); + return (target instanceof DecoratingClassLoader ? target : + new OverridingClassLoader(this.classLoader, target)); } else { return new SimpleThrowawayClassLoader(this.classLoader); diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java index 81222f3f..663a7175 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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,6 +20,7 @@ import java.lang.instrument.ClassFileTransformer; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.springframework.core.OverridingClassLoader; import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -109,7 +110,7 @@ public class GlassFishLoadTimeWeaver implements LoadTimeWeaver { @Override public ClassLoader getThrowawayClassLoader() { try { - return (ClassLoader) this.copyMethod.invoke(this.classLoader); + return new OverridingClassLoader(this.classLoader, (ClassLoader) this.copyMethod.invoke(this.classLoader)); } catch (InvocationTargetException ex) { throw new IllegalStateException("GlassFish copy method threw exception", ex.getCause()); diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java index 1ef36c95..fe0f7980 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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,13 +20,14 @@ import java.lang.instrument.ClassFileTransformer; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.springframework.core.OverridingClassLoader; import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * {@link org.springframework.instrument.classloading.LoadTimeWeaver} implementation for Tomcat's - * new {@link org.apache.tomcat.InstrumentableClassLoader InstrumentableClassLoader}. + * {@link org.springframework.instrument.classloading.LoadTimeWeaver} implementation + * for Tomcat's new {@code org.apache.tomcat.InstrumentableClassLoader}. * Also capable of handling Spring's TomcatInstrumentableClassLoader when encountered. * * @author Juergen Hoeller @@ -103,7 +104,7 @@ public class TomcatLoadTimeWeaver implements LoadTimeWeaver { @Override public ClassLoader getThrowawayClassLoader() { try { - return (ClassLoader) this.copyMethod.invoke(this.classLoader); + return new OverridingClassLoader(this.classLoader, (ClassLoader) this.copyMethod.invoke(this.classLoader)); } catch (InvocationTargetException ex) { throw new IllegalStateException("Tomcat copy method threw exception", ex.getCause()); diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java index 46622173..f4ebb47a 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.instrument.classloading.weblogic; import java.lang.instrument.ClassFileTransformer; +import org.springframework.core.OverridingClassLoader; import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -70,6 +71,8 @@ public class WebLogicLoadTimeWeaver implements LoadTimeWeaver { @Override public ClassLoader getThrowawayClassLoader() { - return this.classLoader.getThrowawayClassLoader(); + return new OverridingClassLoader(this.classLoader.getClassLoader(), + this.classLoader.getThrowawayClassLoader()); } + } diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereLoadTimeWeaver.java index 55c6f863..aeed6929 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereLoadTimeWeaver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.instrument.classloading.websphere; import java.lang.instrument.ClassFileTransformer; +import org.springframework.core.OverridingClassLoader; import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -67,7 +68,8 @@ public class WebSphereLoadTimeWeaver implements LoadTimeWeaver { @Override public ClassLoader getThrowawayClassLoader() { - return this.classLoader.getThrowawayClassLoader(); + return new OverridingClassLoader(this.classLoader.getClassLoader(), + this.classLoader.getThrowawayClassLoader()); } } diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java index fb59a362..9d7c6777 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -63,6 +63,7 @@ import org.springframework.jmx.support.JmxUtils; import org.springframework.jmx.support.ObjectNameManager; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; /** * {@link org.aopalliance.intercept.MethodInterceptor} that routes calls to an @@ -609,7 +610,7 @@ public class MBeanClientInterceptor * Simple wrapper class around a method name and its signature. * Used as the key when caching methods. */ - private static class MethodCacheKey { + private static final class MethodCacheKey implements Comparable<MethodCacheKey> { private final String name; @@ -628,7 +629,7 @@ public class MBeanClientInterceptor @Override public boolean equals(Object other) { - if (other == this) { + if (this == other) { return true; } MethodCacheKey otherKey = (MethodCacheKey) other; @@ -639,6 +640,32 @@ public class MBeanClientInterceptor public int hashCode() { return this.name.hashCode(); } + + @Override + public String toString() { + return this.name + "(" + StringUtils.arrayToCommaDelimitedString(this.parameterTypes) + ")"; + } + + @Override + public int compareTo(MethodCacheKey other) { + int result = this.name.compareTo(other.name); + if (result != 0) { + return result; + } + if (this.parameterTypes.length < other.parameterTypes.length) { + return -1; + } + if (this.parameterTypes.length > other.parameterTypes.length) { + return 1; + } + for (int i = 0; i < this.parameterTypes.length; i++) { + result = this.parameterTypes[i].getName().compareTo(other.parameterTypes[i].getName()); + if (result != 0) { + return result; + } + } + return 0; + } } } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java index 6fc0013e..b9eb86ca 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java @@ -570,7 +570,7 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo * should be exposed to the {@code MBeanServer}. Specifically, if the * supplied {@code mapValue} is the name of a bean that is configured * for lazy initialization, then a proxy to the resource is registered with - * the {@code MBeanServer} so that the the lazy load behavior is + * the {@code MBeanServer} so that the lazy load behavior is * honored. If the bean is already an MBean then it will be registered * directly with the {@code MBeanServer} without any intervention. For * all other beans or bean names, the resource itself is registered with diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java index 0a802724..23d0d3a5 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.jmx.export.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Set; @@ -27,6 +28,7 @@ import org.springframework.beans.annotation.AnnotationBeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.jmx.export.metadata.InvalidMetadataException; import org.springframework.jmx.export.metadata.JmxAttributeSource; @@ -39,6 +41,7 @@ import org.springframework.util.StringValueResolver; * @author Rob Harrop * @author Juergen Hoeller * @author Jennifer Hickey + * @author Stephane Nicoll * @since 1.2 * @see ManagedResource * @see ManagedAttribute @@ -50,25 +53,24 @@ public class AnnotationJmxAttributeSource implements JmxAttributeSource, BeanFac @Override - public void setBeanFactory(final BeanFactory beanFactory) { + public void setBeanFactory(BeanFactory beanFactory) { if (beanFactory instanceof ConfigurableBeanFactory) { - // Not using EmbeddedValueResolverAware in order to avoid a spring-context dependency: - // ConfigurableBeanFactory and its resolveEmbeddedValue live in the spring-beans module. - this.embeddedValueResolver = new StringValueResolver() { - @Override - public String resolveStringValue(String strVal) { - return ((ConfigurableBeanFactory) beanFactory).resolveEmbeddedValue(strVal); - } - }; + this.embeddedValueResolver = new EmbeddedValueResolver((ConfigurableBeanFactory) beanFactory); } } + @Override public org.springframework.jmx.export.metadata.ManagedResource getManagedResource(Class<?> beanClass) throws InvalidMetadataException { ManagedResource ann = AnnotationUtils.findAnnotation(beanClass, ManagedResource.class); if (ann == null) { return null; } + Class<?> declaringClass = AnnotationUtils.findAnnotationDeclaringClass(ManagedResource.class, beanClass); + Class<?> target = (declaringClass != null && !declaringClass.isInterface() ? declaringClass : beanClass); + if (!Modifier.isPublic(target.getModifiers())) { + throw new InvalidMetadataException("@ManagedResource class '" + target.getName() + "' must be public"); + } org.springframework.jmx.export.metadata.ManagedResource managedResource = new org.springframework.jmx.export.metadata.ManagedResource(); AnnotationBeanUtils.copyPropertiesToBean(ann, managedResource, this.embeddedValueResolver); return managedResource; @@ -133,7 +135,7 @@ public class AnnotationJmxAttributeSource implements JmxAttributeSource, BeanFac if (ann == null) { return null; } - T bean = BeanUtils.instantiate(beanClass); + T bean = BeanUtils.instantiateClass(beanClass); AnnotationBeanUtils.copyPropertiesToBean(ann, bean); return bean; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java index 7c2f2aba..42bedaa4 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -25,7 +25,7 @@ import java.lang.annotation.Target; /** * Type-level annotation that indicates JMX notifications emitted by a bean, - * containing multiple {@link ManagedNotification ManagedNotifications} + * containing multiple {@link ManagedNotification ManagedNotifications}. * * @author Rob Harrop * @since 2.0 diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java index 3df5350d..30f5da76 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 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. @@ -72,7 +72,7 @@ public class MethodNameBasedMBeanInfoAssembler extends AbstractConfigurableMBean * @param methodNames an array of method names indicating the methods to use * @see #setMethodMappings */ - public void setManagedMethods(String[] methodNames) { + public void setManagedMethods(String... methodNames) { this.managedMethods = new HashSet<String>(Arrays.asList(methodNames)); } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java index 7db52fb8..96626f07 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java @@ -26,7 +26,7 @@ import org.springframework.util.ObjectUtils; /** * An implementation of the {@code ObjectNamingStrategy} interface that - * creates a name based on the the identity of a given instance. + * creates a name based on the identity of a given instance. * * <p>The resulting {@code ObjectName} will be in the form * <i>package</i>:class=<i>class name</i>,hashCode=<i>identity hash (in hex)</i> diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java index 0f4ed459..5cb99a88 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java @@ -70,7 +70,7 @@ public class KeyNamingStrategy implements ObjectNamingStrategy, InitializingBean /** * Stores the result of merging the {@code mappings} {@code Properties} - * with the the properties stored in the resources defined by {@code mappingLocations}. + * with the properties stored in the resources defined by {@code mappingLocations}. */ private Properties mergedMappings; diff --git a/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java index 5a4d05a7..48d8eb7f 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java @@ -138,7 +138,7 @@ public class ConnectorServerFactoryBean extends MBeanRegistrationSupport * the {@code JMXConnectorServer} will be started in a separate thread. * If the {@code daemon} flag is set to {@code true}, that thread will be * started as a daemon thread. - * @throws JMException if a problem occured when registering the connector server + * @throws JMException if a problem occurred when registering the connector server * with the {@code MBeanServer} * @throws IOException if there is a problem starting the connector server */ diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java index 2aa25520..53aaebb7 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ package org.springframework.jndi; import javax.naming.InitialContext; import javax.naming.NamingException; +import org.springframework.core.SpringProperties; + /** * {@link JndiLocatorSupport} subclass with public lookup methods, * for convenient use as a delegate. @@ -28,6 +30,29 @@ import javax.naming.NamingException; */ public class JndiLocatorDelegate extends JndiLocatorSupport { + /** + * System property that instructs Spring to ignore a default JNDI environment, i.e. + * to always return {@code false} from {@link #isDefaultJndiEnvironmentAvailable()}. + * <p>The default is "false", allowing for regular default JNDI access e.g. in + * {@link JndiPropertySource}. Switching this flag to {@code true} is an optimization + * for scenarios where nothing is ever to be found for such JNDI fallback searches + * to begin with, avoiding the repeated JNDI lookup overhead. + * <p>Note that this flag just affects JNDI fallback searches, not explicitly configured + * JNDI lookups such as for a {@code DataSource} or some other environment resource. + * The flag literally just affects code which attempts JNDI searches based on the + * {@code JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()} check: in particular, + * {@code StandardServletEnvironment} and {@code StandardPortletEnvironment}. + * @since 4.3 + * @see #isDefaultJndiEnvironmentAvailable() + * @see JndiPropertySource + */ + public static final String IGNORE_JNDI_PROPERTY_NAME = "spring.jndi.ignore"; + + + private static final boolean shouldIgnoreDefaultJndiEnvironment = + SpringProperties.getFlag(IGNORE_JNDI_PROPERTY_NAME); + + @Override public Object lookup(String jndiName) throws NamingException { return super.lookup(jndiName); @@ -57,6 +82,9 @@ public class JndiLocatorDelegate extends JndiLocatorSupport { * {@code false} if not */ public static boolean isDefaultJndiEnvironmentAvailable() { + if (shouldIgnoreDefaultJndiEnvironment) { + return false; + } try { new InitialContext().getEnvironment(); return true; diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java index b86838bd..dc22f0fb 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java @@ -219,7 +219,7 @@ public class JndiObjectFactoryBean extends JndiObjectLocator } /** - * Lookup variant that that returns the specified "defaultObject" + * Lookup variant that returns the specified "defaultObject" * (if any) in case of lookup failure. * @return the located object, or the "defaultObject" as fallback * @throws NamingException in case of lookup failure without fallback diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java b/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java index b33fc6d8..e2c40f34 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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,15 @@ public class JndiPropertySource extends PropertySource<JndiLocatorDelegate> { */ @Override public Object getProperty(String name) { + if (getSource().isResourceRef() && name.indexOf(':') != -1) { + // We're in resource-ref (prefixing with "java:comp/env") mode. Let's not bother + // with property names with a colon it since they're probably just containing a + // default value clause, very unlikely to match including the colon part even in + // a textual property source, and effectively never meant to match that way in + // JNDI where a colon indicates a separator between JNDI scheme and actual name. + return null; + } + try { Object value = this.source.lookup(name); if (logger.isDebugEnabled()) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java index 117de48d..7c23ad64 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java @@ -21,7 +21,7 @@ import java.util.concurrent.Executor; import org.springframework.aop.interceptor.AsyncExecutionInterceptor; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; /** * Specialization of {@link AsyncExecutionInterceptor} that delegates method execution to @@ -78,9 +78,9 @@ public class AnnotationAsyncExecutionInterceptor extends AsyncExecutionIntercept protected String getExecutorQualifier(Method method) { // Maintainer's note: changes made here should also be made in // AnnotationAsyncExecutionAspect#getExecutorQualifier - Async async = AnnotationUtils.findAnnotation(method, Async.class); + Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class); if (async == null) { - async = AnnotationUtils.findAnnotation(method.getDeclaringClass(), Async.class); + async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class); } return (async != null ? async.value() : null); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java index 6d67ac3e..fdd33a61 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java @@ -103,18 +103,18 @@ public class AsyncResult<V> implements ListenableFuture<V> { @Override public void addCallback(SuccessCallback<? super V> successCallback, FailureCallback failureCallback) { - if (this.executionException != null) { - Throwable cause = this.executionException.getCause(); - failureCallback.onFailure(cause != null ? cause : this.executionException); - } - else { - try { - successCallback.onSuccess(this.value); + try { + if (this.executionException != null) { + Throwable cause = this.executionException.getCause(); + failureCallback.onFailure(cause != null ? cause : this.executionException); } - catch (Throwable ex) { - failureCallback.onFailure(ex); + else { + successCallback.onSuccess(this.value); } } + catch (Throwable ex) { + // Ignore + } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index d46c7386..045dcd8a 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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,18 +24,22 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Annotation that marks a method to be scheduled. Exactly one of the - * {@link #cron()}, {@link #fixedDelay()}, or {@link #fixedRate()} + * An annotation that marks a method to be scheduled. Exactly one of + * the {@link #cron()}, {@link #fixedDelay()}, or {@link #fixedRate()} * attributes must be specified. * - * <p>The annotated method must expect no arguments and have a - * {@code void} return type. + * <p>The annotated method must expect no arguments. It will typically have + * a {@code void} return type; if not, the returned value will be ignored + * when called through the scheduler. * * <p>Processing of {@code @Scheduled} annotations is performed by * registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be * done manually or, more conveniently, through the {@code <task:annotation-driven/>} * element or @{@link EnableScheduling} annotation. * + * <p>This annotation may be used as a <em>meta-annotation</em> to create custom + * <em>composed annotations</em> with attribute overrides. + * * @author Mark Fisher * @author Dave Syer * @author Chris Beams @@ -72,28 +76,30 @@ public @interface Scheduled { String zone() default ""; /** - * Execute the annotated method with a fixed period between the end - * of the last invocation and the start of the next. + * Execute the annotated method with a fixed period in milliseconds between the + * end of the last invocation and the start of the next. * @return the delay in milliseconds */ long fixedDelay() default -1; /** - * Execute the annotated method with a fixed period between the end - * of the last invocation and the start of the next. + * Execute the annotated method with a fixed period in milliseconds between the + * end of the last invocation and the start of the next. * @return the delay in milliseconds as a String value, e.g. a placeholder * @since 3.2.2 */ String fixedDelayString() default ""; /** - * Execute the annotated method with a fixed period between invocations. + * Execute the annotated method with a fixed period in milliseconds between + * invocations. * @return the period in milliseconds */ long fixedRate() default -1; /** - * Execute the annotated method with a fixed period between invocations. + * Execute the annotated method with a fixed period in milliseconds between + * invocations. * @return the period in milliseconds as a String value, e.g. a placeholder * @since 3.2.2 */ diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index bacd01b5..89d35299 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,9 @@ package org.springframework.scheduling.annotation; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.TimeZone; @@ -36,7 +37,7 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.SmartInitializingSingleton; -import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; @@ -44,16 +45,16 @@ import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.MethodIntrospector; import org.springframework.core.Ordered; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.config.CronTask; import org.springframework.scheduling.config.IntervalTask; +import org.springframework.scheduling.config.ScheduledTask; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.support.CronTrigger; import org.springframework.scheduling.support.ScheduledMethodRunnable; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -83,14 +84,15 @@ import org.springframework.util.StringValueResolver; * @see org.springframework.scheduling.config.ScheduledTaskRegistrar * @see AsyncAnnotationBeanPostProcessor */ -public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, Ordered, - EmbeddedValueResolverAware, BeanFactoryAware, ApplicationContextAware, +public class ScheduledAnnotationBeanPostProcessor implements DestructionAwareBeanPostProcessor, + Ordered, EmbeddedValueResolverAware, BeanFactoryAware, ApplicationContextAware, SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean { /** * The default name of the {@link TaskScheduler} bean to pick up: "taskScheduler". * <p>Note that the initial lookup happens by type; this is just the fallback * in case of multiple scheduler beans found in the context. + * @since 4.2 */ public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler"; @@ -110,6 +112,9 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, Boolean>(64)); + private final Map<Object, Set<ScheduledTask>> scheduledTasks = + new ConcurrentHashMap<Object, Set<ScheduledTask>>(16); + @Override public int getOrder() { @@ -120,6 +125,12 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, * Set the {@link org.springframework.scheduling.TaskScheduler} that will invoke * the scheduled methods, or a {@link java.util.concurrent.ScheduledExecutorService} * to be wrapped as a TaskScheduler. + * <p>If not specified, default scheduler resolution will apply: searching for a + * unique {@link TaskScheduler} bean in the context, or for a {@link TaskScheduler} + * bean named "taskScheduler" otherwise; the same lookup will also be performed for + * a {@link ScheduledExecutorService} bean. If neither of the two is resolvable, + * a local single-threaded default scheduler will be created within the registrar. + * @see #DEFAULT_TASK_SCHEDULER_BEAN_NAME */ public void setScheduler(Object scheduler) { this.scheduler = scheduler; @@ -197,10 +208,13 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, this.beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class)); } catch (NoSuchBeanDefinitionException ex2) { - throw new IllegalStateException("More than one TaskScheduler bean exists within the context, and " + - "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' "+ - "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + - "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback.", ex); + if (logger.isInfoEnabled()) { + logger.info("More than one TaskScheduler bean exists within the context, and " + + "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + + "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + + "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + + ex.getBeanNamesFound()); + } } } catch (NoSuchBeanDefinitionException ex) { @@ -210,14 +224,24 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, this.registrar.setScheduler(this.beanFactory.getBean(ScheduledExecutorService.class)); } catch (NoUniqueBeanDefinitionException ex2) { - throw new IllegalStateException("More than one ScheduledExecutorService bean exists within " + - "the context. Mark one of them as primary; or implement the SchedulingConfigurer " + - "interface and call ScheduledTaskRegistrar#setScheduler explicitly within the " + - "configureTasks() callback.", ex); + try { + this.registrar.setScheduler( + this.beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, ScheduledExecutorService.class)); + } + catch (NoSuchBeanDefinitionException ex3) { + if (logger.isInfoEnabled()) { + logger.info("More than one ScheduledExecutorService bean exists within the context, and " + + "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + + "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + + "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + + ex2.getBeanNamesFound()); + } + } } catch (NoSuchBeanDefinitionException ex2) { - logger.debug("Could not find default ScheduledExecutorService bean", ex); + logger.debug("Could not find default ScheduledExecutorService bean", ex2); // Giving up -> falling back to default scheduler within the registrar... + logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing"); } } } @@ -240,7 +264,7 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, @Override public Set<Scheduled> inspect(Method method) { Set<Scheduled> scheduledMethods = - AnnotationUtils.getRepeatableAnnotations(method, Scheduled.class, Schedules.class); + AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class); return (!scheduledMethods.isEmpty() ? scheduledMethods : null); } }); @@ -269,44 +293,21 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, protected void processScheduled(Scheduled scheduled, Method method, Object bean) { try { - Assert.isTrue(void.class == method.getReturnType(), - "Only void-returning methods may be annotated with @Scheduled"); Assert.isTrue(method.getParameterTypes().length == 0, "Only no-arg methods may be annotated with @Scheduled"); - if (AopUtils.isJdkDynamicProxy(bean)) { - try { - // Found a @Scheduled method on the target class for this JDK proxy -> - // is it also present on the proxy itself? - method = bean.getClass().getMethod(method.getName(), method.getParameterTypes()); - } - catch (SecurityException ex) { - ReflectionUtils.handleReflectionException(ex); - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException(String.format( - "@Scheduled method '%s' found on bean target class '%s' but not " + - "found in any interface(s) for a dynamic proxy. Either pull the " + - "method up to a declared interface or switch to subclass (CGLIB) " + - "proxies by setting proxy-target-class/proxyTargetClass to 'true'.", - method.getName(), method.getDeclaringClass().getSimpleName())); - } - } - else if (AopUtils.isCglibProxy(bean)) { - // Common problem: private methods end up in the proxy instance, not getting delegated. - if (Modifier.isPrivate(method.getModifiers())) { - throw new IllegalStateException(String.format( - "@Scheduled method '%s' found on CGLIB proxy for target class '%s' but cannot " + - "be delegated to target bean. Switch its visibility to package or protected.", - method.getName(), method.getDeclaringClass().getSimpleName())); - } - } - - Runnable runnable = new ScheduledMethodRunnable(bean, method); + Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass()); + Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod); boolean processedSchedule = false; String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; + Set<ScheduledTask> tasks = this.scheduledTasks.get(bean); + if (tasks == null) { + tasks = new LinkedHashSet<ScheduledTask>(4); + this.scheduledTasks.put(bean, tasks); + } + // Determine initial delay long initialDelay = scheduled.initialDelay(); String initialDelayString = scheduled.initialDelayString(); @@ -341,7 +342,7 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, else { timeZone = TimeZone.getDefault(); } - this.registrar.addCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))); + tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); } // At this point we don't need to differentiate between initial delay set or not anymore @@ -354,7 +355,7 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, if (fixedDelay >= 0) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; - this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); + tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay))); } String fixedDelayString = scheduled.fixedDelayString(); if (StringUtils.hasText(fixedDelayString)) { @@ -370,7 +371,7 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, throw new IllegalArgumentException( "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer"); } - this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); + tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay))); } // Check fixed rate @@ -378,7 +379,7 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, if (fixedRate >= 0) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; - this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); + tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay))); } String fixedRateString = scheduled.fixedRateString(); if (StringUtils.hasText(fixedRateString)) { @@ -394,7 +395,7 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, throw new IllegalArgumentException( "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer"); } - this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); + tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay))); } // Check whether we had any attribute set @@ -408,7 +409,29 @@ public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, @Override + public void postProcessBeforeDestruction(Object bean, String beanName) { + Set<ScheduledTask> tasks = this.scheduledTasks.remove(bean); + if (tasks != null) { + for (ScheduledTask task : tasks) { + task.cancel(); + } + } + } + + @Override + public boolean requiresDestruction(Object bean) { + return this.scheduledTasks.containsKey(bean); + } + + @Override public void destroy() { + Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values(); + for (Set<ScheduledTask> tasks : allTasks) { + for (ScheduledTask task : tasks) { + task.cancel(); + } + } + this.scheduledTasks.clear(); this.registrar.destroy(); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java index 3cba4186..d27877ae 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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,6 +30,9 @@ import java.lang.annotation.Target; * where {@link Scheduled} can simply be declared several times on the same method, * implicitly generating this container annotation. * + * <p>This annotation may be used as a <em>meta-annotation</em> to create custom + * <em>composed annotations</em>. + * * @author Juergen Hoeller * @since 4.0 * @see Scheduled diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java index e9f2d255..b888092d 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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. @@ -26,6 +26,7 @@ import javax.enterprise.concurrent.ManagedExecutors; import javax.enterprise.concurrent.ManagedTask; import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.support.TaskExecutorAdapter; import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.scheduling.SchedulingTaskExecutor; @@ -127,6 +128,20 @@ public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, Sche return this.concurrentExecutor; } + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + * <p>Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + * <p>The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + * @since 4.3 + */ + public final void setTaskDecorator(TaskDecorator taskDecorator) { + this.adaptedExecutor.setTaskDecorator(taskDecorator); + } + @Override public void execute(Runnable task) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java index 6cd5ccf9..3391276b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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,6 +30,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskRejectedException; import org.springframework.scheduling.SchedulingTaskExecutor; import org.springframework.util.Assert; @@ -82,6 +83,8 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport private boolean allowCoreThreadTimeOut = false; + private TaskDecorator taskDecorator; + private ThreadPoolExecutor threadPoolExecutor; @@ -177,15 +180,51 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; } + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + * <p>Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + * <p>The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + * @since 4.3 + */ + public void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + + /** + * Note: This method exposes an {@link ExecutorService} to its base class + * but stores the actual {@link ThreadPoolExecutor} handle internally. + * Do not override this method for replacing the executor, rather just for + * decorating its {@code ExecutorService} handle or storing custom state. + */ @Override protected ExecutorService initializeExecutor( ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { BlockingQueue<Runnable> queue = createQueue(this.queueCapacity); - ThreadPoolExecutor executor = new ThreadPoolExecutor( - this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, - queue, threadFactory, rejectedExecutionHandler); + + ThreadPoolExecutor executor; + if (this.taskDecorator != null) { + executor = new ThreadPoolExecutor( + this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, + queue, threadFactory, rejectedExecutionHandler) { + @Override + public void execute(Runnable command) { + super.execute(taskDecorator.decorate(command)); + } + }; + } + else { + executor = new ThreadPoolExecutor( + this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, + queue, threadFactory, rejectedExecutionHandler); + + } + if (this.allowCoreThreadTimeOut) { executor.allowCoreThreadTimeOut(true); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java new file mode 100644 index 00000000..dffdb3e2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2016 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.scheduling.config; + +import java.util.concurrent.ScheduledFuture; + +/** + * A representation of a scheduled task, + * used as a return value for scheduling methods. + * + * @author Juergen Hoeller + * @since 4.3 + * @see ScheduledTaskRegistrar#scheduleTriggerTask + * @see ScheduledTaskRegistrar#scheduleFixedRateTask + */ +public final class ScheduledTask { + + volatile ScheduledFuture<?> future; + + + ScheduledTask() { + } + + + /** + * Trigger cancellation of this scheduled task. + */ + public void cancel() { + ScheduledFuture<?> future = this.future; + if (future != null) { + future.cancel(true); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java index 1cc43ca7..d7fe6be3 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,13 @@ package org.springframework.scheduling.config; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; @@ -67,7 +67,9 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean private List<IntervalTask> fixedDelayTasks; - private final Set<ScheduledFuture<?>> scheduledFutures = new LinkedHashSet<ScheduledFuture<?>>(); + private final Map<Task, ScheduledTask> unresolvedTasks = new HashMap<Task, ScheduledTask>(16); + + private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<ScheduledTask>(16); /** @@ -228,6 +230,7 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean Collections.<IntervalTask>emptyList()); } + /** * Add a Runnable task to be triggered per the given {@link Trigger}. * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) @@ -306,6 +309,7 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean this.fixedDelayTasks.add(task); } + /** * Return whether this {@code ScheduledTaskRegistrar} has any tasks registered. * @since 3.2 @@ -331,56 +335,155 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean * #setTaskScheduler(TaskScheduler) task scheduler}. */ protected void scheduleTasks() { - long now = System.currentTimeMillis(); - if (this.taskScheduler == null) { this.localExecutor = Executors.newSingleThreadScheduledExecutor(); this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor); } if (this.triggerTasks != null) { for (TriggerTask task : this.triggerTasks) { - this.scheduledFutures.add(this.taskScheduler.schedule( - task.getRunnable(), task.getTrigger())); + addScheduledTask(scheduleTriggerTask(task)); } } if (this.cronTasks != null) { for (CronTask task : this.cronTasks) { - this.scheduledFutures.add(this.taskScheduler.schedule( - task.getRunnable(), task.getTrigger())); + addScheduledTask(scheduleCronTask(task)); } } if (this.fixedRateTasks != null) { for (IntervalTask task : this.fixedRateTasks) { - if (task.getInitialDelay() > 0) { - Date startTime = new Date(now + task.getInitialDelay()); - this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate( - task.getRunnable(), startTime, task.getInterval())); - } - else { - this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate( - task.getRunnable(), task.getInterval())); - } + addScheduledTask(scheduleFixedRateTask(task)); } } if (this.fixedDelayTasks != null) { for (IntervalTask task : this.fixedDelayTasks) { - if (task.getInitialDelay() > 0) { - Date startTime = new Date(now + task.getInitialDelay()); - this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay( - task.getRunnable(), startTime, task.getInterval())); - } - else { - this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay( - task.getRunnable(), task.getInterval())); - } + addScheduledTask(scheduleFixedDelayTask(task)); } } } + private void addScheduledTask(ScheduledTask task) { + if (task != null) { + this.scheduledTasks.add(task); + } + } + + + /** + * Schedule the specified trigger task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * @since 4.3 + */ + public ScheduledTask scheduleTriggerTask(TriggerTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(); + newTask = true; + } + if (this.taskScheduler != null) { + scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger()); + } + else { + addTriggerTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + + /** + * Schedule the specified cron task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 4.3 + */ + public ScheduledTask scheduleCronTask(CronTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(); + newTask = true; + } + if (this.taskScheduler != null) { + scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger()); + } + else { + addCronTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + + /** + * Schedule the specified fixed-rate task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 4.3 + */ + public ScheduledTask scheduleFixedRateTask(IntervalTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(); + newTask = true; + } + if (this.taskScheduler != null) { + if (task.getInitialDelay() > 0) { + Date startTime = new Date(System.currentTimeMillis() + task.getInitialDelay()); + scheduledTask.future = + this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), startTime, task.getInterval()); + } + else { + scheduledTask.future = + this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getInterval()); + } + } + else { + addFixedRateTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + + /** + * Schedule the specified fixed-delay task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 4.3 + */ + public ScheduledTask scheduleFixedDelayTask(IntervalTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(); + newTask = true; + } + if (this.taskScheduler != null) { + if (task.getInitialDelay() > 0) { + Date startTime = new Date(System.currentTimeMillis() + task.getInitialDelay()); + scheduledTask.future = + this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), startTime, task.getInterval()); + } + else { + scheduledTask.future = + this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), task.getInterval()); + } + } + else { + addFixedDelayTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + + @Override public void destroy() { - for (ScheduledFuture<?> future : this.scheduledFutures) { - future.cancel(true); + for (ScheduledTask task : this.scheduledTasks) { + task.cancel(); } if (this.localExecutor != null) { this.localExecutor.shutdownNow(); diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java index f53894c7..c7b356fd 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -262,7 +262,7 @@ public class CronSequenceGenerator { */ private void parse(String expression) throws IllegalArgumentException { String[] fields = StringUtils.tokenizeToStringArray(expression, " "); - if (fields.length != 6) { + if (!areValidCronFields(fields)) { throw new IllegalArgumentException(String.format( "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression)); } @@ -379,10 +379,32 @@ public class CronSequenceGenerator { throw new IllegalArgumentException("Range less than minimum (" + min + "): '" + field + "' in expression \"" + this.expression + "\""); } + if (result[0] > result[1]) { + throw new IllegalArgumentException("Invalid inverted range: '" + field + + "' in expression \"" + this.expression + "\""); + } return result; } + /** + * Determine whether the specified expression represents a valid cron pattern. + * <p>Specifically, this method verifies that the expression contains six + * fields separated by single spaces. + * @param expression the expression to evaluate + * @return {@code true} if the given expression is a valid cron expression + * @since 4.3 + */ + public static boolean isValidExpression(String expression) { + String[] fields = StringUtils.tokenizeToStringArray(expression, " "); + return areValidCronFields(fields); + } + + private static boolean areValidCronFields(String[] fields) { + return (fields != null && fields.length == 6); + } + + @Override public boolean equals(Object other) { if (this == other) { diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java index 5fb732dc..57d18c2f 100644 --- a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java @@ -94,8 +94,7 @@ public abstract class BshScriptUtils { return clazz.newInstance(); } catch (Throwable ex) { - throw new IllegalStateException("Could not instantiate script class [" + - clazz.getName() + "]. Root cause is " + ex); + throw new IllegalStateException("Could not instantiate script class: " + clazz.getName(), ex); } } else { diff --git a/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java b/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java index b5452624..f967a3a6 100644 --- a/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java +++ b/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.springframework.util.StringUtils; * @author Mark Fisher * @since 2.5 */ -public class ScriptingDefaultsParser implements BeanDefinitionParser { +class ScriptingDefaultsParser implements BeanDefinitionParser { private static final String REFRESH_CHECK_DELAY_ATTRIBUTE = "refresh-check-delay"; @@ -41,7 +41,7 @@ public class ScriptingDefaultsParser implements BeanDefinitionParser { LangNamespaceUtils.registerScriptFactoryPostProcessorIfNecessary(parserContext.getRegistry()); String refreshCheckDelay = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); if (StringUtils.hasText(refreshCheckDelay)) { - bd.getPropertyValues().add("defaultRefreshCheckDelay", new Long(refreshCheckDelay)); + bd.getPropertyValues().add("defaultRefreshCheckDelay", Long.valueOf(refreshCheckDelay)); } String proxyTargetClass = element.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE); if (StringUtils.hasText(proxyTargetClass)) { diff --git a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java index 439f3c1e..fdc2bb83 100644 --- a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java +++ b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java @@ -266,7 +266,7 @@ public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, Bea } catch (InstantiationException ex) { throw new ScriptCompilationException( - scriptSource, "Could not instantiate Groovy script class: " + scriptClass.getName(), ex); + scriptSource, "Unable to instantiate Groovy script class: " + scriptClass.getName(), ex); } catch (IllegalAccessException ex) { throw new ScriptCompilationException( diff --git a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java index d91573d0..51ff49e1 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -381,7 +381,7 @@ public class ScriptFactoryPostProcessor extends InstantiationAwareBeanPostProces * If the {@link BeanDefinition} has a * {@link org.springframework.core.AttributeAccessor metadata attribute} * under the key {@link #REFRESH_CHECK_DELAY_ATTRIBUTE} which is a valid {@link Number} - * type, then this value is used. Otherwise, the the {@link #defaultRefreshCheckDelay} + * type, then this value is used. Otherwise, the {@link #defaultRefreshCheckDelay} * value is used. * @param beanDefinition the BeanDefinition to check * @return the refresh check delay diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java index bff51a79..f8bc2365 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -154,7 +154,7 @@ public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAwar } catch (InstantiationException ex) { throw new ScriptCompilationException( - scriptSource, "Could not instantiate script class: " + scriptClass.getName(), ex); + scriptSource, "Unable to instantiate script class: " + scriptClass.getName(), ex); } catch (IllegalAccessException ex) { throw new ScriptCompilationException( diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java index 913406f0..33d53bf9 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,9 +31,12 @@ import javax.validation.ConstraintValidatorFactory; import javax.validation.MessageInterpolator; import javax.validation.TraversableResolver; import javax.validation.Validation; +import javax.validation.ValidationProviderResolver; import javax.validation.Validator; import javax.validation.ValidatorContext; import javax.validation.ValidatorFactory; +import javax.validation.bootstrap.GenericBootstrap; +import javax.validation.bootstrap.ProviderSpecificBootstrap; import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; @@ -51,9 +54,9 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** - * This is the central class for {@code javax.validation} (JSR-303) setup - * in a Spring application context: It bootstraps a {@code javax.validation.ValidationFactory} - * and exposes it through the Spring {@link org.springframework.validation.Validator} interface + * This is the central class for {@code javax.validation} (JSR-303) setup in a Spring + * application context: It bootstraps a {@code javax.validation.ValidationFactory} and + * exposes it through the Spring {@link org.springframework.validation.Validator} interface * as well as through the JSR-303 {@link javax.validation.Validator} interface and the * {@link javax.validation.ValidatorFactory} interface itself. * @@ -92,6 +95,8 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter @SuppressWarnings("rawtypes") private Class providerClass; + private ValidationProviderResolver validationProviderResolver; + private MessageInterpolator messageInterpolator; private TraversableResolver traversableResolver; @@ -121,6 +126,15 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter } /** + * Specify a JSR-303 {@link ValidationProviderResolver} for bootstrapping the + * provider of choice, as an alternative to {@code META-INF} driven resolution. + * @since 4.3 + */ + public void setValidationProviderResolver(ValidationProviderResolver validationProviderResolver) { + this.validationProviderResolver = validationProviderResolver; + } + + /** * Specify a custom MessageInterpolator to use for this ValidatorFactory * and its exposed default Validator. */ @@ -216,11 +230,23 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter @Override + @SuppressWarnings({"rawtypes", "unchecked"}) public void afterPropertiesSet() { - @SuppressWarnings({"rawtypes", "unchecked"}) - Configuration<?> configuration = (this.providerClass != null ? - Validation.byProvider(this.providerClass).configure() : - Validation.byDefaultProvider().configure()); + Configuration<?> configuration; + if (this.providerClass != null) { + ProviderSpecificBootstrap bootstrap = Validation.byProvider(this.providerClass); + if (this.validationProviderResolver != null) { + bootstrap = bootstrap.providerResolver(this.validationProviderResolver); + } + configuration = bootstrap.configure(); + } + else { + GenericBootstrap bootstrap = Validation.byDefaultProvider(); + if (this.validationProviderResolver != null) { + bootstrap = bootstrap.providerResolver(this.validationProviderResolver); + } + configuration = bootstrap.configure(); + } // Try Hibernate Validator 5.2's externalClassLoader(ClassLoader) method if (this.applicationContext != null) { diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java index aa88b18c..6d46113d 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import javax.validation.metadata.BeanDescriptor; import javax.validation.metadata.ConstraintDescriptor; import org.springframework.beans.NotReadablePropertyException; +import org.springframework.context.MessageSourceResolvable; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.util.Assert; import org.springframework.validation.BindingResult; @@ -190,9 +191,9 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. * Return FieldError arguments for a validation error on the given field. * Invoked for each violated constraint. * <p>The default implementation returns a first argument indicating the field name - * (of type DefaultMessageSourceResolvable, with "objectName.field" and "field" as codes). - * Afterwards, it adds all actual constraint annotation attributes (i.e. excluding - * "message", "groups" and "payload") in alphabetical order of their attribute names. + * (see {@link #getResolvableField}). Afterwards, it adds all actual constraint + * annotation attributes (i.e. excluding "message", "groups" and "payload") in + * alphabetical order of their attribute names. * <p>Can be overridden to e.g. add further attributes from the constraint descriptor. * @param objectName the name of the target object * @param field the field that caused the binding error @@ -204,14 +205,16 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. */ protected Object[] getArgumentsForConstraint(String objectName, String field, ConstraintDescriptor<?> descriptor) { List<Object> arguments = new LinkedList<Object>(); - String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; - arguments.add(new DefaultMessageSourceResolvable(codes, field)); + arguments.add(getResolvableField(objectName, field)); // Using a TreeMap for alphabetical ordering of attribute names Map<String, Object> attributesToExpose = new TreeMap<String, Object>(); for (Map.Entry<String, Object> entry : descriptor.getAttributes().entrySet()) { String attributeName = entry.getKey(); Object attributeValue = entry.getValue(); if (!internalAnnotationAttributes.contains(attributeName)) { + if (attributeValue instanceof String) { + attributeValue = new ResolvableAttribute(attributeValue.toString()); + } attributesToExpose.put(attributeName, attributeValue); } } @@ -220,6 +223,22 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. } /** + * Build a resolvable wrapper for the specified field, allowing to resolve the field's + * name in a {@code MessageSource}. + * <p>The default implementation returns a first argument indicating the field: + * of type {@code DefaultMessageSourceResolvable}, with "objectName.field" and "field" + * as codes, and with the plain field name as default message. + * @param objectName the name of the target object + * @param field the field that caused the binding error + * @return a corresponding {@code MessageSourceResolvable} for the specified field + * @since 4.3 + */ + protected MessageSourceResolvable getResolvableField(String objectName, String field) { + String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; + return new DefaultMessageSourceResolvable(codes, field); + } + + /** * Extract the rejected value behind the given constraint violation, * for exposure through the Spring errors representation. * @param field the field that caused the binding error @@ -249,13 +268,13 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. @Override public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) { - Assert.notNull(this.targetValidator, "No target Validator set"); + Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.validate(object, groups); } @Override public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) { - Assert.notNull(this.targetValidator, "No target Validator set"); + Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.validateProperty(object, propertyName, groups); } @@ -263,20 +282,50 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. public <T> Set<ConstraintViolation<T>> validateValue( Class<T> beanType, String propertyName, Object value, Class<?>... groups) { - Assert.notNull(this.targetValidator, "No target Validator set"); + Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.validateValue(beanType, propertyName, value, groups); } @Override public BeanDescriptor getConstraintsForClass(Class<?> clazz) { - Assert.notNull(this.targetValidator, "No target Validator set"); + Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.getConstraintsForClass(clazz); } @Override + @SuppressWarnings("unchecked") public <T> T unwrap(Class<T> type) { - Assert.notNull(this.targetValidator, "No target Validator set"); - return this.targetValidator.unwrap(type); + Assert.state(this.targetValidator != null, "No target Validator set"); + return (type != null ? this.targetValidator.unwrap(type) : (T) this.targetValidator); + } + + + /** + * Wrapper for a String attribute which can be resolved via a {@code MessageSource}, + * falling back to the original attribute as a default value otherwise. + */ + private static class ResolvableAttribute implements MessageSourceResolvable { + + private final String resolvableString; + + public ResolvableAttribute(String resolvableString) { + this.resolvableString = resolvableString; + } + + @Override + public String[] getCodes() { + return new String[] {this.resolvableString}; + } + + @Override + public Object[] getArguments() { + return null; + } + + @Override + public String getDefaultMessage() { + return this.resolvableString; + } } } |