Add RetryInterceptorBuilder support
* Add `RetryInterceptorBuilder` and its tests * Add usage from `AnnotationAwareRetryOperationsInterceptor` * Add `Retryable#interceptor()` option to use full Retry Interceptor from `BeanFactory` * Fix `RetryConfiguration` `beanFactory` propagation Fixes gh-11
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2014 the original author or authors.
|
||||
* Copyright 2014 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,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.aop.IntroductionInterceptor;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.retry.RetryPolicy;
|
||||
import org.springframework.retry.backoff.BackOffPolicy;
|
||||
@@ -35,35 +39,38 @@ import org.springframework.retry.backoff.UniformRandomBackOffPolicy;
|
||||
import org.springframework.retry.interceptor.MethodArgumentsKeyGenerator;
|
||||
import org.springframework.retry.interceptor.MethodInvocationRecoverer;
|
||||
import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier;
|
||||
import org.springframework.retry.interceptor.RetryOperationsInterceptor;
|
||||
import org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor;
|
||||
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
|
||||
import org.springframework.retry.policy.MapRetryContextCache;
|
||||
import org.springframework.retry.policy.RetryContextCache;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodCallback;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* WrappeMethodInterceptorr interceptor that interprets the retry metadata on the method it is invoking and
|
||||
* delegates to an appropriate RetryOperationsInterceptor.
|
||||
*
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 2.0
|
||||
* @author Artem Bilan
|
||||
* @since 1.1
|
||||
*
|
||||
*/
|
||||
public class AnnotationAwareRetryOperationsInterceptor implements IntroductionInterceptor {
|
||||
public class AnnotationAwareRetryOperationsInterceptor implements IntroductionInterceptor, BeanFactoryAware {
|
||||
|
||||
private Map<Method, MethodInterceptor> delegates = new HashMap<Method, MethodInterceptor>();
|
||||
private final Map<Method, MethodInterceptor> delegates = new HashMap<Method, MethodInterceptor>();
|
||||
|
||||
private RetryContextCache retryContextCache = new MapRetryContextCache();
|
||||
|
||||
private MethodArgumentsKeyGenerator methodArgumentsKeyGenerator;
|
||||
|
||||
private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier;
|
||||
|
||||
|
||||
private Sleeper sleeper;
|
||||
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
/**
|
||||
* @param sleeper the sleeper to set
|
||||
*/
|
||||
@@ -73,7 +80,7 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
|
||||
|
||||
/**
|
||||
* Public setter for the {@link RetryContextCache}.
|
||||
*
|
||||
*
|
||||
* @param retryContextCache the {@link RetryContextCache} to set.
|
||||
*/
|
||||
public void setRetryContextCache(RetryContextCache retryContextCache) {
|
||||
@@ -81,78 +88,80 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
|
||||
}
|
||||
|
||||
/**
|
||||
* @param methodArgumentsKeyGenerator
|
||||
* @param methodArgumentsKeyGenerator the {@link MethodArgumentsKeyGenerator}
|
||||
*/
|
||||
public void setKeyGenerator(MethodArgumentsKeyGenerator methodArgumentsKeyGenerator) {
|
||||
this.methodArgumentsKeyGenerator = methodArgumentsKeyGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param newMethodArgumentsIdentifier
|
||||
* @param newMethodArgumentsIdentifier the {@link NewMethodArgumentsIdentifier}
|
||||
*/
|
||||
public void setNewItemIdentifier(
|
||||
NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) {
|
||||
public void setNewItemIdentifier(NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) {
|
||||
this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implementsInterface(Class<?> intf) {
|
||||
return org.springframework.retry.interceptor.Retryable.class.isAssignableFrom(intf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
|
||||
return delegate.invoke(invocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implementsInterface(Class<?> intf) {
|
||||
return Retryable.class.isAssignableFrom(intf);
|
||||
}
|
||||
|
||||
private MethodInterceptor getDelegate(Object target, Method method) {
|
||||
if (!delegates.containsKey(method)) {
|
||||
synchronized (delegates) {
|
||||
if (!delegates.containsKey(method)) {
|
||||
Retryable retryable = AnnotationUtils.findAnnotation(method,
|
||||
Retryable.class);
|
||||
if (!this.delegates.containsKey(method)) {
|
||||
synchronized (this.delegates) {
|
||||
if (!this.delegates.containsKey(method)) {
|
||||
Retryable retryable = AnnotationUtils.findAnnotation(method, Retryable.class);
|
||||
if (retryable == null) {
|
||||
retryable = AnnotationUtils.findAnnotation(
|
||||
method.getDeclaringClass(), Retryable.class);
|
||||
retryable = AnnotationUtils.findAnnotation(method.getDeclaringClass(), Retryable.class);
|
||||
}
|
||||
MethodInterceptor delegate;
|
||||
if (retryable.stateful()) {
|
||||
if (StringUtils.hasText(retryable.interceptor())) {
|
||||
delegate = this.beanFactory.getBean(retryable.interceptor(), MethodInterceptor.class);
|
||||
}
|
||||
else if (retryable.stateful()) {
|
||||
delegate = getStatefulInterceptor(target, method, retryable);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
delegate = getStatelessInterceptor(target, method, retryable);
|
||||
}
|
||||
delegates.put(method, delegate);
|
||||
this.delegates.put(method, delegate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return delegates.get(method);
|
||||
return this.delegates.get(method);
|
||||
}
|
||||
|
||||
private MethodInterceptor getStatelessInterceptor(Object target, Method method, Retryable retryable) {
|
||||
RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor();
|
||||
RetryTemplate template = new RetryTemplate();
|
||||
template.setRetryPolicy(getRetryPolicy(retryable));
|
||||
template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
|
||||
interceptor.setRetryOperations(template);
|
||||
interceptor.setRecoverer(getRecoverer(target, method));
|
||||
return interceptor;
|
||||
return RetryInterceptorBuilder.stateless()
|
||||
.retryPolicy(getRetryPolicy(retryable))
|
||||
.backOffPolicy(getBackoffPolicy(retryable.backoff()))
|
||||
.recoverer(getRecoverer(target, method))
|
||||
.build();
|
||||
}
|
||||
|
||||
private MethodInterceptor getStatefulInterceptor(Object target, Method method, Retryable retryable) {
|
||||
StatefulRetryOperationsInterceptor interceptor = new StatefulRetryOperationsInterceptor();
|
||||
if (methodArgumentsKeyGenerator != null) {
|
||||
interceptor.setKeyGenerator(methodArgumentsKeyGenerator);
|
||||
}
|
||||
if (newMethodArgumentsIdentifier != null) {
|
||||
interceptor.setNewItemIdentifier(newMethodArgumentsIdentifier);
|
||||
}
|
||||
RetryTemplate template = new RetryTemplate();
|
||||
template.setRetryContextCache(retryContextCache);
|
||||
template.setRetryContextCache(this.retryContextCache);
|
||||
template.setRetryPolicy(getRetryPolicy(retryable));
|
||||
template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
|
||||
interceptor.setRetryOperations(template);
|
||||
interceptor.setRecoverer(getRecoverer(target, method));
|
||||
return interceptor;
|
||||
|
||||
return RetryInterceptorBuilder.stateful()
|
||||
.retryOperations(template)
|
||||
.recoverer(getRecoverer(target, method))
|
||||
.keyGenerator(this.methodArgumentsKeyGenerator)
|
||||
.newMethodArgumentsIdentifier(this.newMethodArgumentsIdentifier)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MethodInvocationRecoverer<?> getRecoverer(Object target, Method method) {
|
||||
@@ -164,11 +173,12 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
|
||||
@Override
|
||||
public void doWith(Method method) throws IllegalArgumentException,
|
||||
IllegalAccessException {
|
||||
if (AnnotationUtils.findAnnotation(method, Recover.class)!=null) {
|
||||
if (AnnotationUtils.findAnnotation(method, Recover.class) != null) {
|
||||
foundRecoverable.set(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!foundRecoverable.get()) {
|
||||
return null;
|
||||
}
|
||||
@@ -193,39 +203,37 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
|
||||
for (Class<? extends Throwable> type : excludes) {
|
||||
policyMap.put(type, false);
|
||||
}
|
||||
SimpleRetryPolicy simple = new SimpleRetryPolicy(retryable.maxAttempts(),
|
||||
policyMap, true);
|
||||
return simple;
|
||||
return new SimpleRetryPolicy(retryable.maxAttempts(), policyMap, true);
|
||||
}
|
||||
|
||||
private BackOffPolicy getBackoffPolicy(Backoff backoff) {
|
||||
long min = backoff.delay()==0 ? backoff.value() : backoff.delay();
|
||||
long min = backoff.delay() == 0 ? backoff.value() : backoff.delay();
|
||||
long max = backoff.maxDelay();
|
||||
if (backoff.multiplier()>0) {
|
||||
if (backoff.multiplier() > 0) {
|
||||
ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy();
|
||||
if (backoff.random()) {
|
||||
policy = new ExponentialRandomBackOffPolicy();
|
||||
}
|
||||
policy.setInitialInterval(min);
|
||||
policy.setMultiplier(backoff.multiplier());
|
||||
policy.setMaxInterval(max>min ? max : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL);
|
||||
if (sleeper!=null) {
|
||||
policy.setMaxInterval(max > min ? max : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL);
|
||||
if (sleeper != null) {
|
||||
policy.setSleeper(sleeper);
|
||||
}
|
||||
return policy;
|
||||
}
|
||||
if (max>min) {
|
||||
if (max > min) {
|
||||
UniformRandomBackOffPolicy policy = new UniformRandomBackOffPolicy();
|
||||
policy.setMinBackOffPeriod(min);
|
||||
policy.setMaxBackOffPeriod(max);
|
||||
if (sleeper!=null) {
|
||||
if (sleeper != null) {
|
||||
policy.setSleeper(sleeper);
|
||||
}
|
||||
return policy;
|
||||
}
|
||||
FixedBackOffPolicy policy = new FixedBackOffPolicy();
|
||||
policy.setBackOffPeriod(min);
|
||||
if (sleeper!=null) {
|
||||
if (sleeper != null) {
|
||||
policy.setSleeper(sleeper);
|
||||
}
|
||||
return policy;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
* Copyright 2014 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 java.util.Set;
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.aopalliance.aop.Advice;
|
||||
|
||||
import org.springframework.aop.ClassFilter;
|
||||
import org.springframework.aop.IntroductionAdvisor;
|
||||
import org.springframework.aop.Pointcut;
|
||||
@@ -44,9 +45,10 @@ import org.springframework.retry.policy.RetryContextCache;
|
||||
* {@link RetryContextCache}, {@link MethodArgumentsKeyGenerator} or
|
||||
* {@link NewMethodArgumentsIdentifier} it will be used by the corresponding
|
||||
* retry interceptor (otherwise sensible defaults are adopted).
|
||||
*
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 2.0
|
||||
* @author Artem Bilan
|
||||
* @since 1.1
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@@ -70,13 +72,17 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements
|
||||
@Autowired(required = false)
|
||||
private Sleeper sleeper;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(
|
||||
1);
|
||||
Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(1);
|
||||
retryableAnnotationTypes.add(Retryable.class);
|
||||
this.pointcut = buildPointcut(retryableAnnotationTypes);
|
||||
this.advice = buildAdvice();
|
||||
if (this.advice instanceof BeanFactoryAware) {
|
||||
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,9 +91,7 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements
|
||||
*/
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) {
|
||||
if (this.advice instanceof BeanFactoryAware) {
|
||||
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
|
||||
}
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,7 +101,7 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements
|
||||
|
||||
@Override
|
||||
public Class<?>[] getInterfaces() {
|
||||
return new Class[] { org.springframework.retry.interceptor.Retryable.class };
|
||||
return new Class[] {org.springframework.retry.interceptor.Retryable.class};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -133,22 +137,20 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements
|
||||
|
||||
/**
|
||||
* Calculate a pointcut for the given retry annotation types, if any.
|
||||
*
|
||||
*
|
||||
* @param retryAnnotationTypes
|
||||
* the retry annotation types to introspect
|
||||
* @return the applicable Pointcut object, or {@code null} if none
|
||||
*/
|
||||
protected Pointcut buildPointcut(
|
||||
Set<Class<? extends Annotation>> retryAnnotationTypes) {
|
||||
protected Pointcut buildPointcut(Set<Class<? extends Annotation>> retryAnnotationTypes) {
|
||||
ComposablePointcut result = null;
|
||||
for (Class<? extends Annotation> retryAnnotationType : retryAnnotationTypes) {
|
||||
Pointcut cpc = new AnnotationMatchingPointcut(retryAnnotationType,
|
||||
true);
|
||||
Pointcut mpc = AnnotationMatchingPointcut
|
||||
.forMethodAnnotation(retryAnnotationType);
|
||||
Pointcut cpc = new AnnotationMatchingPointcut(retryAnnotationType, true);
|
||||
Pointcut mpc = AnnotationMatchingPointcut.forMethodAnnotation(retryAnnotationType);
|
||||
if (result == null) {
|
||||
result = new ComposablePointcut(cpc).union(mpc);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
result.union(cpc).union(mpc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
* Copyright 2014 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,9 +24,10 @@ import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation for a method invocation that is retryable.
|
||||
*
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 2.0
|
||||
* @author Artem Bilan
|
||||
* @since 1.1
|
||||
*
|
||||
*/
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@@ -34,10 +35,16 @@ import java.lang.annotation.Target;
|
||||
@Documented
|
||||
public @interface Retryable {
|
||||
|
||||
/**
|
||||
* Retry interceptor bean name to be applied for retryable method.
|
||||
* Is mutually exclusive with other attributes.
|
||||
* @return the retry interceptor bean name
|
||||
*/
|
||||
String interceptor() default "";
|
||||
|
||||
/**
|
||||
* Exception types that are retryable. Synonym for includes(). Defaults to
|
||||
* empty (and if excludes is also empty all exceptions are retried).
|
||||
*
|
||||
* @return exception types to retry
|
||||
*/
|
||||
Class<? extends Throwable>[] value() default {};
|
||||
@@ -45,7 +52,6 @@ public @interface Retryable {
|
||||
/**
|
||||
* Exception types that are retryable. Defaults to empty (and if excludes is
|
||||
* also empty all exceptions are retried).
|
||||
*
|
||||
* @return exception types to retry
|
||||
*/
|
||||
Class<? extends Throwable>[] include() default {};
|
||||
@@ -53,7 +59,6 @@ public @interface Retryable {
|
||||
/**
|
||||
* Exception types that are not retryable. Defaults to empty (and if
|
||||
* includes is also empty all exceptions are retried).
|
||||
*
|
||||
* @return exception types to retry
|
||||
*/
|
||||
Class<? extends Throwable>[] exclude() default {};
|
||||
@@ -63,7 +68,6 @@ public @interface Retryable {
|
||||
* but the retry policy is applied with the same policy to subsequent
|
||||
* invocations with the same arguments. If false then retryable exceptions
|
||||
* are not re-thrown.
|
||||
*
|
||||
* @return true if retry is stateful, default false
|
||||
*/
|
||||
boolean stateful() default false;
|
||||
@@ -78,7 +82,6 @@ public @interface Retryable {
|
||||
* Specify the backof properties for retrying this operation. The default is
|
||||
* no backoff, but it can be a good idea to pause between attempts (even at
|
||||
* the cost of blocking a thread).
|
||||
*
|
||||
* @return a backoff specification
|
||||
*/
|
||||
Backoff backoff() default @Backoff();
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright 2014 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.retry.interceptor;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
|
||||
import org.springframework.retry.RetryOperations;
|
||||
import org.springframework.retry.RetryPolicy;
|
||||
import org.springframework.retry.backoff.BackOffPolicy;
|
||||
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <p>Simplified facade to make it easier and simpler to build a
|
||||
* {@link StatefulRetryOperationsInterceptor} or
|
||||
* (stateless) {@link RetryOperationsInterceptor}
|
||||
* by providing a fluent interface to defining the behavior on error.
|
||||
* <p>
|
||||
* Typical example:
|
||||
* </p>
|
||||
*
|
||||
* <pre class="code">
|
||||
* StatefulRetryOperationsInterceptor interceptor =
|
||||
* RetryInterceptorBuilder.stateful()
|
||||
* .maxAttempts(5)
|
||||
* .backOffOptions(1, 2, 10) // initialInterval, multiplier, maxInterval
|
||||
* .build();
|
||||
* </pre>
|
||||
*
|
||||
* @author James Carr
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
* @since 1.1
|
||||
*
|
||||
* @param <T> The type of {@link org.aopalliance.intercept.MethodInterceptor}
|
||||
* returned by the builder's {@link #build()} method.
|
||||
*/
|
||||
public abstract class RetryInterceptorBuilder<T extends MethodInterceptor> {
|
||||
|
||||
protected final RetryTemplate retryTemplate = new RetryTemplate();
|
||||
|
||||
protected final SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
|
||||
|
||||
protected RetryOperations retryOperations;
|
||||
|
||||
protected MethodInvocationRecoverer<?> recoverer;
|
||||
|
||||
private boolean templateAltered;
|
||||
|
||||
private boolean backOffPolicySet;
|
||||
|
||||
private boolean retryPolicySet;
|
||||
|
||||
private boolean backOffOptionsSet;
|
||||
|
||||
/**
|
||||
* Create a builder for a stateful retry interceptor.
|
||||
* @return The interceptor builder.
|
||||
*/
|
||||
public static StatefulRetryInterceptorBuilder stateful() {
|
||||
return new StatefulRetryInterceptorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder for a stateless retry interceptor.
|
||||
* @return The interceptor builder.
|
||||
*/
|
||||
public static StatelessRetryInterceptorBuilder stateless() {
|
||||
return new StatelessRetryInterceptorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the retry operations - once this is set, other properties can no longer be set; can't
|
||||
* be set if other properties have been applied.
|
||||
* @param retryOperations The retry operations.
|
||||
* @return this.
|
||||
*/
|
||||
public RetryInterceptorBuilder<T> retryOperations(RetryOperations retryOperations) {
|
||||
Assert.isTrue(!this.templateAltered, "Cannot set retryOperations when the default has been modified");
|
||||
this.retryOperations = retryOperations;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the max attempts - a SimpleRetryPolicy will be used. Cannot be used if a custom retry operations
|
||||
* or retry policy has been set.
|
||||
* @param maxAttempts the max attempts.
|
||||
* @return this.
|
||||
*/
|
||||
public RetryInterceptorBuilder<T> maxAttempts(int maxAttempts) {
|
||||
Assert.isNull(this.retryOperations, "cannot alter the retry policy when a custom retryOperations has been set");
|
||||
Assert.isTrue(!this.retryPolicySet, "cannot alter the retry policy when a custom retryPolicy has been set");
|
||||
this.simpleRetryPolicy.setMaxAttempts(maxAttempts);
|
||||
this.retryTemplate.setRetryPolicy(this.simpleRetryPolicy);
|
||||
this.templateAltered = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the backoff options. Cannot be used if a custom retry operations, or back off policy has been set.
|
||||
* @param initialInterval The initial interval.
|
||||
* @param multiplier The multiplier.
|
||||
* @param maxInterval The max interval.
|
||||
* @return this.
|
||||
*/
|
||||
public RetryInterceptorBuilder<T> backOffOptions(long initialInterval, double multiplier, long maxInterval) {
|
||||
Assert.isNull(this.retryOperations, "cannot set the back off policy when a custom retryOperations has been set");
|
||||
Assert.isTrue(!this.backOffPolicySet, "cannot set the back off options when a back off policy has been set");
|
||||
ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy();
|
||||
policy.setInitialInterval(initialInterval);
|
||||
policy.setMultiplier(multiplier);
|
||||
policy.setMaxInterval(maxInterval);
|
||||
this.retryTemplate.setBackOffPolicy(policy);
|
||||
this.backOffOptionsSet = true;
|
||||
this.templateAltered = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the retry policy - cannot be used if a custom retry template has been provided, or the max attempts or
|
||||
* back off options or policy have been applied.
|
||||
* @param policy The policy.
|
||||
* @return this.
|
||||
*/
|
||||
public RetryInterceptorBuilder<T> retryPolicy(RetryPolicy policy) {
|
||||
Assert.isNull(this.retryOperations, "cannot set the retry policy when a custom retryOperations has been set");
|
||||
Assert.isTrue(!this.templateAltered, "cannot set the retry policy if max attempts or back off policy or options changed");
|
||||
this.retryTemplate.setRetryPolicy(policy);
|
||||
this.retryPolicySet = true;
|
||||
this.templateAltered = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the back off policy. Cannot be used if a custom retry operations, or back off policy has been applied.
|
||||
* @param policy The policy.
|
||||
* @return this.
|
||||
*/
|
||||
public RetryInterceptorBuilder<T> backOffPolicy(BackOffPolicy policy) {
|
||||
Assert.isNull(this.retryOperations, "cannot set the back off policy when a custom retryOperations has been set");
|
||||
Assert.isTrue(!this.backOffOptionsSet, "cannot set the back off policy when the back off policy options have been set");
|
||||
this.retryTemplate.setBackOffPolicy(policy);
|
||||
this.templateAltered = true;
|
||||
this.backOffPolicySet = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a {@link MethodInvocationRecoverer} for the Retry interceptor.
|
||||
* @param recoverer The recoverer.
|
||||
* @return this.
|
||||
*/
|
||||
public RetryInterceptorBuilder<T> recoverer(MethodInvocationRecoverer<?> recoverer) {
|
||||
this.recoverer = recoverer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract T build();
|
||||
|
||||
|
||||
private RetryInterceptorBuilder() {
|
||||
}
|
||||
|
||||
public static class StatefulRetryInterceptorBuilder extends RetryInterceptorBuilder<StatefulRetryOperationsInterceptor> {
|
||||
|
||||
private final StatefulRetryOperationsInterceptor interceptor = new StatefulRetryOperationsInterceptor();
|
||||
|
||||
private MethodArgumentsKeyGenerator keyGenerator;
|
||||
|
||||
private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier;
|
||||
|
||||
/**
|
||||
* Stateful retry requires items to be identifiable.
|
||||
* @param keyGenerator The key generator.
|
||||
* @return this.
|
||||
*/
|
||||
public StatefulRetryInterceptorBuilder keyGenerator(MethodArgumentsKeyGenerator keyGenerator) {
|
||||
this.keyGenerator = keyGenerator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a custom new item identifier.
|
||||
* @param newMethodArgumentsIdentifier The new item identifier.
|
||||
* @return this.
|
||||
*/
|
||||
public StatefulRetryInterceptorBuilder newMethodArgumentsIdentifier(NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) {
|
||||
this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatefulRetryInterceptorBuilder retryOperations(
|
||||
RetryOperations retryOperations) {
|
||||
super.retryOperations(retryOperations);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatefulRetryInterceptorBuilder maxAttempts(int maxAttempts) {
|
||||
super.maxAttempts(maxAttempts);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatefulRetryInterceptorBuilder backOffOptions(long initialInterval,
|
||||
double multiplier, long maxInterval) {
|
||||
super.backOffOptions(initialInterval, multiplier, maxInterval);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatefulRetryInterceptorBuilder retryPolicy(RetryPolicy policy) {
|
||||
super.retryPolicy(policy);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatefulRetryInterceptorBuilder backOffPolicy(BackOffPolicy policy) {
|
||||
super.backOffPolicy(policy);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatefulRetryInterceptorBuilder recoverer(MethodInvocationRecoverer<?> recoverer) {
|
||||
super.recoverer(recoverer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatefulRetryOperationsInterceptor build() {
|
||||
if (this.recoverer != null) {
|
||||
this.interceptor.setRecoverer(this.recoverer);
|
||||
}
|
||||
if (this.retryOperations != null) {
|
||||
this.interceptor.setRetryOperations(this.retryOperations);
|
||||
}
|
||||
else {
|
||||
this.interceptor.setRetryOperations(this.retryTemplate);
|
||||
}
|
||||
if (this.keyGenerator != null) {
|
||||
this.interceptor.setKeyGenerator(this.keyGenerator);
|
||||
}
|
||||
if (this.newMethodArgumentsIdentifier != null) {
|
||||
this.interceptor.setNewItemIdentifier(this.newMethodArgumentsIdentifier);
|
||||
}
|
||||
return this.interceptor;
|
||||
}
|
||||
|
||||
private StatefulRetryInterceptorBuilder() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static class StatelessRetryInterceptorBuilder extends RetryInterceptorBuilder<RetryOperationsInterceptor> {
|
||||
|
||||
private final RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor();
|
||||
|
||||
@Override
|
||||
public RetryOperationsInterceptor build() {
|
||||
if (this.recoverer != null) {
|
||||
this.interceptor.setRecoverer(this.recoverer);
|
||||
}
|
||||
if (this.retryOperations != null) {
|
||||
this.interceptor.setRetryOperations(this.retryOperations);
|
||||
}
|
||||
else {
|
||||
this.interceptor.setRetryOperations(this.retryTemplate);
|
||||
}
|
||||
return this.interceptor;
|
||||
}
|
||||
|
||||
private StatelessRetryInterceptorBuilder() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
* Copyright 2014 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,16 +21,20 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.retry.backoff.Sleeper;
|
||||
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @since 1.1
|
||||
*/
|
||||
public class EnableRetryTests {
|
||||
|
||||
@@ -92,7 +96,8 @@ public class EnableRetryTests {
|
||||
try {
|
||||
service.service();
|
||||
fail("Expected IllegalStateException");
|
||||
} catch (IllegalStateException e) {
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
}
|
||||
assertEquals(1, service.getCount());
|
||||
context.close();
|
||||
@@ -106,7 +111,8 @@ public class EnableRetryTests {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
try {
|
||||
service.service(1);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertEquals("Planned", e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -114,6 +120,15 @@ public class EnableRetryTests {
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExternalInterceptor() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
|
||||
InterceptableService service = context.getBean(InterceptableService.class);
|
||||
service.service();
|
||||
assertEquals(5, service.getCount());
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableRetry(proxyTargetClass = true)
|
||||
protected static class TestProxyConfiguration {
|
||||
@@ -163,6 +178,17 @@ public class EnableRetryTests {
|
||||
return new ExcludesService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MethodInterceptor retryInterceptor() {
|
||||
return RetryInterceptorBuilder.stateless()
|
||||
.maxAttempts(5)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InterceptableService serviceWithExternalInterceptor() {
|
||||
return new InterceptableService();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class Service {
|
||||
@@ -185,6 +211,7 @@ public class EnableRetryTests {
|
||||
protected static class RecoverableService {
|
||||
|
||||
private int count = 0;
|
||||
|
||||
private Throwable cause;
|
||||
|
||||
@Retryable(RuntimeException.class)
|
||||
@@ -258,4 +285,21 @@ public class EnableRetryTests {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class InterceptableService {
|
||||
|
||||
private int count = 0;
|
||||
|
||||
@Retryable(interceptor = "retryInterceptor")
|
||||
public void service() {
|
||||
if (count++ < 4) {
|
||||
throw new RuntimeException("Planned");
|
||||
}
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2014 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.retry.interceptor;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.retry.RetryOperations;
|
||||
import org.springframework.retry.backoff.FixedBackOffPolicy;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.retry.util.test.TestUtils;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
* @since 1.1
|
||||
*
|
||||
*/
|
||||
public class RetryInterceptorBuilderTests {
|
||||
|
||||
@Test
|
||||
public void testBasic() {
|
||||
StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful().build();
|
||||
assertEquals(3, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithCustomRetryTemplate() {
|
||||
RetryOperations retryOperations = new RetryTemplate();
|
||||
StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful()
|
||||
.retryOperations(retryOperations)
|
||||
.build();
|
||||
assertEquals(3, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
|
||||
assertSame(retryOperations, TestUtils.getPropertyValue(interceptor, "retryOperations"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithMoreAttempts() {
|
||||
StatefulRetryOperationsInterceptor interceptor =
|
||||
RetryInterceptorBuilder.stateful()
|
||||
.maxAttempts(5)
|
||||
.build();
|
||||
assertEquals(5, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithCustomizedBackOffMoreAttempts() {
|
||||
StatefulRetryOperationsInterceptor interceptor =
|
||||
RetryInterceptorBuilder.stateful()
|
||||
.maxAttempts(5)
|
||||
.backOffOptions(1, 2, 10)
|
||||
.build();
|
||||
|
||||
assertEquals(5, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
|
||||
assertEquals(1L, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.initialInterval"));
|
||||
assertEquals(2.0, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.multiplier"));
|
||||
assertEquals(10L, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.maxInterval"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithCustomBackOffPolicy() {
|
||||
StatefulRetryOperationsInterceptor interceptor =
|
||||
RetryInterceptorBuilder.stateful()
|
||||
.maxAttempts(5)
|
||||
.backOffPolicy(new FixedBackOffPolicy())
|
||||
.build();
|
||||
|
||||
assertEquals(5, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
|
||||
assertEquals(1000L, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithCustomNewMessageIdentifier() throws Exception {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
StatefulRetryOperationsInterceptor interceptor =
|
||||
RetryInterceptorBuilder.stateful()
|
||||
.maxAttempts(5)
|
||||
.newMethodArgumentsIdentifier(new NewMethodArgumentsIdentifier() {
|
||||
|
||||
@Override
|
||||
public boolean isNew(Object[] args) {
|
||||
latch.countDown();
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.backOffPolicy(new FixedBackOffPolicy())
|
||||
.build();
|
||||
|
||||
assertEquals(5, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
|
||||
assertEquals(1000L, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod"));
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
Foo delegate = createDelegate(interceptor, count);
|
||||
Object message = "";
|
||||
try {
|
||||
delegate.onMessage("", message);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
assertEquals("foo", e.getMessage());
|
||||
}
|
||||
assertEquals(1, count.get());
|
||||
assertTrue(latch.await(0, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWitCustomRetryPolicyTraverseCause() {
|
||||
StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful()
|
||||
.retryPolicy(new SimpleRetryPolicy(15, Collections
|
||||
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true), true))
|
||||
.build();
|
||||
assertEquals(15, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithCustomKeyGenerator() throws Exception {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful()
|
||||
.keyGenerator(new MethodArgumentsKeyGenerator() {
|
||||
|
||||
@Override
|
||||
public Object getKey(Object[] item) {
|
||||
latch.countDown();
|
||||
return "foo";
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
assertEquals(3, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
Foo delegate = createDelegate(interceptor, count);
|
||||
Object message = "";
|
||||
try {
|
||||
delegate.onMessage("", message);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
assertEquals("foo", e.getMessage());
|
||||
}
|
||||
assertEquals(1, count.get());
|
||||
assertTrue(latch.await(0, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
private Foo createDelegate(MethodInterceptor interceptor, final AtomicInteger count) {
|
||||
Foo delegate = new Foo() {
|
||||
|
||||
@Override
|
||||
public void onMessage(String s, Object message) {
|
||||
count.incrementAndGet();
|
||||
throw new RuntimeException("foo", new RuntimeException("bar"));
|
||||
}
|
||||
|
||||
};
|
||||
ProxyFactory factory = new ProxyFactory();
|
||||
factory.addAdvisor(new DefaultPointcutAdvisor(Pointcut.TRUE, interceptor));
|
||||
factory.setProxyTargetClass(false);
|
||||
factory.addInterface(Foo.class);
|
||||
factory.setTarget(delegate);
|
||||
delegate = (Foo) factory.getProxy();
|
||||
return delegate;
|
||||
}
|
||||
|
||||
static interface Foo {
|
||||
|
||||
void onMessage(String s, Object message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.retry.util.test;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* See Spring Integration TestUtils.
|
||||
* @author Mark Fisher
|
||||
* @author Iwein Fuld
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
*/
|
||||
public class TestUtils {
|
||||
|
||||
/**
|
||||
* Uses nested {@link org.springframework.beans.DirectFieldAccessor}s to obtain a property using dotted notation
|
||||
* to traverse fields; e.g.
|
||||
* "foo.bar.baz" will obtain a reference to the baz field of the bar field of foo. Adopted from Spring Integration.
|
||||
* @param root The object.
|
||||
* @param propertyPath The path.
|
||||
* @return The field.
|
||||
*/
|
||||
public static Object getPropertyValue(Object root, String propertyPath) {
|
||||
Object value = null;
|
||||
DirectFieldAccessor accessor = new DirectFieldAccessor(root);
|
||||
String[] tokens = propertyPath.split("\\.");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
value = accessor.getPropertyValue(tokens[i]);
|
||||
if (value != null) {
|
||||
accessor = new DirectFieldAccessor(value);
|
||||
}
|
||||
else if (i == tokens.length - 1) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("intermediate property '" + tokens[i] + "' is null");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getPropertyValue(Object root, String propertyPath, Class<T> type) {
|
||||
Object value = getPropertyValue(root, propertyPath);
|
||||
if (value != null) {
|
||||
Assert.isAssignable(type, value.getClass());
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user