diff --git a/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java index 55c0a9c0c..fbdb569d2 100644 --- a/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java +++ b/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java @@ -27,6 +27,8 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.util.AnnotationDetectionMethodCallback; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** @@ -34,6 +36,7 @@ import org.springframework.util.ReflectionUtils; * to evaluate the contained SpEL expression to define the outcome of the method call. * * @author Oliver Gierke + * @author Thomas Darimont * @since 1.10 */ public class SpelAwareProxyProjectionFactory extends ProxyProjectionFactory implements BeanFactoryAware { @@ -41,6 +44,7 @@ public class SpelAwareProxyProjectionFactory extends ProxyProjectionFactory impl private final Map, Boolean> typeCache = new HashMap, Boolean>(); private BeanFactory beanFactory; + private SpelExpressionParser parser = new SpelExpressionParser(); /* * (non-Javadoc) @@ -51,13 +55,25 @@ public class SpelAwareProxyProjectionFactory extends ProxyProjectionFactory impl this.beanFactory = beanFactory; } + /** + * Set the {@link SpelExpressionParser} to use. + * + * @param parser must not be {@literal null} + */ + public void setParser(SpelExpressionParser parser) { + + Assert.notNull(parser, "Parser must not be null!"); + + this.parser = parser; + } + /** * Inspects the given target type for methods with {@link Value} annotations and caches the result. Will create a * {@link SpelEvaluatingMethodInterceptor} if an annotation was found or return the delegate as is if not. * + * @param interceptor the root {@link MethodInterceptor}. * @param source The backing source object. * @param projectionType the proxy target type. - * @param delegate the root {@link MethodInterceptor}. * @return */ @Override @@ -72,8 +88,8 @@ public class SpelAwareProxyProjectionFactory extends ProxyProjectionFactory impl typeCache.put(projectionType, callback.hasFoundAnnotation()); } - return typeCache.get(projectionType) ? new SpelEvaluatingMethodInterceptor(interceptor, source, beanFactory) - : interceptor; + return typeCache.get(projectionType) ? new SpelEvaluatingMethodInterceptor(interceptor, source, beanFactory, + parser, projectionType) : interceptor; } /* diff --git a/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java index 34faac185..ff6e92afd 100644 --- a/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java +++ b/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java @@ -16,6 +16,8 @@ package org.springframework.data.projection; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.aopalliance.intercept.MethodInterceptor; @@ -38,14 +40,15 @@ import org.springframework.util.StringUtils; * delegate {@link MethodInterceptor} if no {@link Value} annotation is found. * * @author Oliver Gierke + * @author Thomas Darimont * @see 1.10 */ class SpelEvaluatingMethodInterceptor implements MethodInterceptor { - private final SpelExpressionParser parser; - private final ParserContext parserContext; + private static final ParserContext PARSER_CONTEXT = new TemplateParserContext(); private final EvaluationContext evaluationContext; private final MethodInterceptor delegate; + private final Map expressions; /** * Creates a new {@link SpelEvaluatingMethodInterceptor} delegating to the given {@link MethodInterceptor} as fallback @@ -55,11 +58,16 @@ class SpelEvaluatingMethodInterceptor implements MethodInterceptor { * @param delegate must not be {@literal null}. * @param target must not be {@literal null}. * @param beanFactory can be {@literal null}. + * @param parser must not be {@literal null}. + * @param targetInterface must not be {@literal null}. */ - public SpelEvaluatingMethodInterceptor(MethodInterceptor delegate, Object target, BeanFactory beanFactory) { + public SpelEvaluatingMethodInterceptor(MethodInterceptor delegate, Object target, BeanFactory beanFactory, + SpelExpressionParser parser, Class targetInterface) { Assert.notNull(delegate, "Delegate MethodInterceptor must not be null!"); Assert.notNull(target, "TargetObject must not be null!"); + Assert.notNull(parser, "TargetObject must not be null!"); + Assert.notNull(targetInterface, "TargetInterface must not be null!"); StandardEvaluationContext evaluationContext = new StandardEvaluationContext(new TargetWrapper(target)); @@ -71,12 +79,45 @@ class SpelEvaluatingMethodInterceptor implements MethodInterceptor { evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); } + this.expressions = potentiallyCreateExpressionsForMethodsOnTargetInterface(parser, targetInterface); + this.evaluationContext = evaluationContext; - this.parser = new SpelExpressionParser(); - this.parserContext = new TemplateParserContext(); this.delegate = delegate; } + /** + * Eagerly parses {@link Expression} defined on {@link Value} annotations. Returns a map with + * {@code method.hashCode()} as key and the parsed {@link Expression} or an {@link Collections#emptyMap()} if no + * {@code Expressions} were found. + * + * @param parser + * @param targetInterface + * @return + */ + private Map potentiallyCreateExpressionsForMethodsOnTargetInterface(SpelExpressionParser parser, + Class targetInterface) { + + Map expressions = new HashMap(); + + for (Method method : targetInterface.getMethods()) { + + if (!method.isAnnotationPresent(Value.class)) { + continue; + } + + Value value = method.getAnnotation(Value.class); + if (!StringUtils.hasText(value.value())) { + throw new IllegalStateException(String.format("@Value annotation on %s contains empty expression!", method)); + } + + Expression expression = parser.parseExpression(value.value(), PARSER_CONTEXT); + + expressions.put(method.hashCode(), expression); + } + + return expressions.isEmpty() ? Collections. emptyMap() : expressions; + } + /* * (non-Javadoc) * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) @@ -84,18 +125,12 @@ class SpelEvaluatingMethodInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { - Method method = invocation.getMethod(); - Value annotation = method.getAnnotation(Value.class); + Expression expression = expressions.get(invocation.getMethod().hashCode()); - if (annotation == null) { + if (expression == null) { return delegate.invoke(invocation); } - if (!StringUtils.hasText(annotation.value())) { - throw new IllegalStateException(String.format("@Value annotation on %s contains empty expression!", method)); - } - - Expression expression = parser.parseExpression(annotation.value(), parserContext); return expression.getValue(evaluationContext); } diff --git a/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java b/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java index bcc033e33..fe0e75588 100644 --- a/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/projection/SpelAwareProxyProjectionFactoryUnitTests.java @@ -20,17 +20,25 @@ import static org.junit.Assert.*; import java.util.List; +import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Value; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * Unit tests for {@link SpelAwareProxyProjectionFactory}. * * @author Oliver Gierke + * @author Thomas Darimont */ public class SpelAwareProxyProjectionFactoryUnitTests { - private final SpelAwareProxyProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + private SpelAwareProxyProjectionFactory factory; + + @Before + public void setup() { + factory = new SpelAwareProxyProjectionFactory(); + } /** * @see DATAREST-221, DATACMNS-630 @@ -58,6 +66,22 @@ public class SpelAwareProxyProjectionFactoryUnitTests { assertThat(properties, hasItem("firstname")); } + /** + * @see DATACMNS-661 + */ + @Test + public void shouldSupportConfiguringFactoryWithCustomSpelParser() { + + Customer customer = new Customer(); + customer.firstname = "Dave"; + customer.lastname = "Matthews"; + + factory.setParser(new SpelExpressionParser()); + CustomerExcerpt excerpt = factory.createProjection(CustomerExcerpt.class, customer); + + assertThat(excerpt.getFullName(), is("Dave Matthews")); + } + static class Customer { public String firstname, lastname; diff --git a/src/test/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptorUnitTests.java index d71540df8..42ca68d0e 100644 --- a/src/test/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptorUnitTests.java @@ -30,11 +30,13 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * Unit tests for {@link SpelEvaluatingMethodInterceptor}. * * @author Oliver Gierke + * @author Thomas Darimont */ @RunWith(MockitoJUnitRunner.class) public class SpelEvaluatingMethodInterceptorUnitTests { @@ -42,6 +44,8 @@ public class SpelEvaluatingMethodInterceptorUnitTests { @Mock MethodInterceptor delegate; @Mock MethodInvocation invocation; + SpelExpressionParser parser = new SpelExpressionParser(); + /** * @see DATAREST-221, DATACMNS-630 */ @@ -50,7 +54,8 @@ public class SpelEvaluatingMethodInterceptorUnitTests { when(invocation.getMethod()).thenReturn(Projection.class.getMethod("propertyFromTarget")); - MethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), null); + MethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), null, parser, + Projection.class); assertThat(interceptor.invoke(invocation), is((Object) "property")); } @@ -66,7 +71,8 @@ public class SpelEvaluatingMethodInterceptorUnitTests { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); factory.registerSingleton("someBean", new SomeBean()); - SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), factory); + SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), factory, + parser, Projection.class); assertThat(interceptor.invoke(invocation), is((Object) "value")); } @@ -80,7 +86,7 @@ public class SpelEvaluatingMethodInterceptorUnitTests { when(invocation.getMethod()).thenReturn(Projection.class.getMethod("getName")); SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), - new DefaultListableBeanFactory()); + new DefaultListableBeanFactory(), parser, Projection.class); interceptor.invoke(invocation); @@ -93,10 +99,10 @@ public class SpelEvaluatingMethodInterceptorUnitTests { @Test(expected = IllegalStateException.class) public void rejectsEmptySpelExpression() throws Throwable { - when(invocation.getMethod()).thenReturn(Projection.class.getMethod("getAddress")); + when(invocation.getMethod()).thenReturn(InvalidProjection.class.getMethod("getAddress")); SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), - new DefaultListableBeanFactory()); + new DefaultListableBeanFactory(), parser, InvalidProjection.class); interceptor.invoke(invocation); } @@ -113,7 +119,7 @@ public class SpelEvaluatingMethodInterceptorUnitTests { when(invocation.getMethod()).thenReturn(Projection.class.getMethod("propertyFromTarget")); SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, map, - new DefaultListableBeanFactory()); + new DefaultListableBeanFactory(), parser, Projection.class); assertThat(interceptor.invoke(invocation), is((Object) "Dave")); } @@ -128,6 +134,11 @@ public class SpelEvaluatingMethodInterceptorUnitTests { String getName(); + String getAddress(); + } + + interface InvalidProjection { + @Value("") String getAddress(); }