From ec905cb0736344a23cb814bc0ae620dbe7e73bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 17 Nov 2023 12:20:28 +0100 Subject: [PATCH] Share internal StandardExecutionContext delegates This commit makes sure that the per-operation execution context for caching and event listening does not recreate the default internal delegates, but rather get initialized with a shared state. This reduces the number of instances created per operation execution, reducing the GC pressure as a result. This also makes sure that any cache, such as the one in StandardTypeLocator, is reused. Closes gh-31617 --- .../cache/interceptor/CacheAspectSupport.java | 10 +- .../CacheEvaluationContextFactory.java | 73 +++++++++++ .../CacheOperationExpressionEvaluator.java | 20 +-- .../ApplicationListenerMethodAdapter.java | 2 +- .../event/EventExpressionEvaluator.java | 32 +++-- .../event/EventListenerMethodProcessor.java | 8 +- .../interceptor/ExpressionEvaluatorTests.java | 17 ++- ...ApplicationListenerMethodAdapterTests.java | 3 +- .../support/StandardEvaluationContext.java | 28 +++- .../support/StandardOperatorOverloader.java | 4 +- .../spel/support/StandardTypeComparator.java | 2 + .../StandardEvaluationContextTests.java | 122 ++++++++++++++++++ 12 files changed, 288 insertions(+), 33 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/support/StandardEvaluationContextTests.java 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 338dc7a1cb..1df57244f3 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 @@ -47,11 +47,13 @@ import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.KotlinDetector; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -99,7 +101,10 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker private final Map metadataCache = new ConcurrentHashMap<>(1024); - private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(); + private final StandardEvaluationContext originalEvaluationContext = new StandardEvaluationContext(); + + private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator( + new CacheEvaluationContextFactory(this.originalEvaluationContext)); @Nullable private final ReactiveCachingHandler reactiveCachingHandler; @@ -222,6 +227,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; + this.originalEvaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); } @@ -852,7 +858,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker private EvaluationContext createEvaluationContext(@Nullable Object result) { return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args, - this.target, this.metadata.targetClass, this.metadata.targetMethod, result, beanFactory); + this.target, this.metadata.targetClass, this.metadata.targetMethod, result); } protected Collection getCaches() { diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java new file mode 100644 index 0000000000..327cfd4d97 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2023 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 + * + * https://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.cache.interceptor; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.function.SingletonSupplier; + +/** + * A factory for {@link CacheEvaluationContext} that makes sure that internal + * delegates are reused. + * + * @author Stephane Nicoll + * @since 6.1.1 + */ +class CacheEvaluationContextFactory { + + private final StandardEvaluationContext originalContext; + + @Nullable + private Supplier parameterNameDiscoverer; + + CacheEvaluationContextFactory(StandardEvaluationContext originalContext) { + this.originalContext = originalContext; + } + + public void setParameterNameDiscoverer(Supplier parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + public ParameterNameDiscoverer getParameterNameDiscoverer() { + if (this.parameterNameDiscoverer == null) { + this.parameterNameDiscoverer = SingletonSupplier.of(new DefaultParameterNameDiscoverer()); + } + return this.parameterNameDiscoverer.get(); + } + + /** + * Creates a {@link CacheEvaluationContext} for the specified operation. + * @param rootObject the {@code root} object to use for the context + * @param targetMethod the target cache {@link Method} + * @param args the arguments of the method invocation + * @return a context suitable for this cache operation + */ + public CacheEvaluationContext forOperation(CacheExpressionRootObject rootObject, + Method targetMethod, Object[] args) { + + CacheEvaluationContext evaluationContext = new CacheEvaluationContext( + rootObject, targetMethod, args, getParameterNameDiscoverer()); + this.originalContext.applyDelegatesTo(evaluationContext); + return evaluationContext; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java index 82892d0ccf..13a49ea102 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,10 +21,8 @@ import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -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.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -67,6 +65,13 @@ class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { private final Map unlessCache = new ConcurrentHashMap<>(64); + private final CacheEvaluationContextFactory evaluationContextFactory; + + public CacheOperationExpressionEvaluator(CacheEvaluationContextFactory evaluationContextFactory) { + super(); + this.evaluationContextFactory = evaluationContextFactory; + this.evaluationContextFactory.setParameterNameDiscoverer(this::getParameterNameDiscoverer); + } /** * Create an {@link EvaluationContext}. @@ -81,21 +86,18 @@ class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { */ public EvaluationContext createEvaluationContext(Collection caches, Method method, Object[] args, Object target, Class targetClass, Method targetMethod, - @Nullable Object result, @Nullable BeanFactory beanFactory) { + @Nullable Object result) { CacheExpressionRootObject rootObject = new CacheExpressionRootObject( caches, method, args, target, targetClass); - CacheEvaluationContext evaluationContext = new CacheEvaluationContext( - rootObject, targetMethod, args, getParameterNameDiscoverer()); + CacheEvaluationContext evaluationContext = this.evaluationContextFactory + .forOperation(rootObject, targetMethod, args); 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/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index b80bd9a19d..d60615b7af 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 @@ -262,7 +262,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe if (StringUtils.hasText(condition)) { Assert.notNull(this.evaluator, "EventExpressionEvaluator must not be null"); return this.evaluator.condition( - condition, event, this.targetMethod, this.methodKey, args, this.applicationContext); + condition, event, this.targetMethod, this.methodKey, args); } return true; } 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 ed1974f47a..acef8846be 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-2019 the original author or authors. + * Copyright 2002-2023 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,14 +20,13 @@ import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -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.expression.EvaluationContext; import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; +import org.springframework.expression.spel.support.StandardEvaluationContext; /** * Utility class for handling SpEL expression parsing for application events. @@ -41,23 +40,32 @@ class EventExpressionEvaluator extends CachedExpressionEvaluator { private final Map conditionCache = new ConcurrentHashMap<>(64); + private final StandardEvaluationContext originalEvaluationContext; + + EventExpressionEvaluator(StandardEvaluationContext originalEvaluationContext) { + this.originalEvaluationContext = originalEvaluationContext; + } /** * Determine if the condition defined by the specified expression evaluates * to {@code true}. */ public boolean condition(String conditionExpression, ApplicationEvent event, Method targetMethod, - AnnotatedElementKey methodKey, Object[] args, @Nullable BeanFactory beanFactory) { - - EventExpressionRootObject root = new EventExpressionRootObject(event, args); - MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext( - root, targetMethod, args, getParameterNameDiscoverer()); - if (beanFactory != null) { - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); - } + AnnotatedElementKey methodKey, Object[] args) { + EventExpressionRootObject rootObject = new EventExpressionRootObject(event, args); + EvaluationContext evaluationContext = createEvaluationContext(rootObject, targetMethod, args); return (Boolean.TRUE.equals(getExpression(this.conditionCache, methodKey, conditionExpression).getValue( evaluationContext, Boolean.class))); } + private EvaluationContext createEvaluationContext(EventExpressionRootObject rootObject, + Method method, Object[] args) { + + MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(rootObject, + method, args, getParameterNameDiscoverer()); + this.originalEvaluationContext.applyDelegatesTo(evaluationContext); + 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 1fea6e198d..4e16ecadfa 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 @@ -39,10 +39,12 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.expression.BeanFactoryResolver; 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.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -75,6 +77,8 @@ public class EventListenerMethodProcessor @Nullable private List eventListenerFactories; + private final StandardEvaluationContext originalEvaluationContext; + @Nullable private final EventExpressionEvaluator evaluator; @@ -82,7 +86,8 @@ public class EventListenerMethodProcessor public EventListenerMethodProcessor() { - this.evaluator = new EventExpressionEvaluator(); + this.originalEvaluationContext = new StandardEvaluationContext(); + this.evaluator = new EventExpressionEvaluator(this.originalEvaluationContext); } @Override @@ -95,6 +100,7 @@ public class EventListenerMethodProcessor @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { this.beanFactory = beanFactory; + this.originalEvaluationContext.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); Map beans = beanFactory.getBeansOfType(EventListenerFactory.class, false, false); List factories = new ArrayList<>(beans.values()); diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java index e86012dcd9..b3f2264043 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java @@ -31,9 +31,12 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.support.StaticApplicationContext; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -47,7 +50,10 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; */ public class ExpressionEvaluatorTests { - private final CacheOperationExpressionEvaluator eval = new CacheOperationExpressionEvaluator(); + private final StandardEvaluationContext originalEvaluationContext = new StandardEvaluationContext(); + + private final CacheOperationExpressionEvaluator eval = new CacheOperationExpressionEvaluator( + new CacheEvaluationContextFactory(this.originalEvaluationContext)); private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); @@ -82,7 +88,7 @@ public class ExpressionEvaluatorTests { Collection caches = Collections.singleton(new ConcurrentMapCache("test")); EvaluationContext evalCtx = this.eval.createEvaluationContext(caches, method, args, - target, target.getClass(), method, CacheOperationExpressionEvaluator.NO_RESULT, null); + target, target.getClass(), method, CacheOperationExpressionEvaluator.NO_RESULT); Collection ops = getOps("multipleCaching"); Iterator it = ops.iterator(); @@ -140,14 +146,17 @@ public class ExpressionEvaluatorTests { return createEvaluationContext(result, null); } - private EvaluationContext createEvaluationContext(Object result, BeanFactory beanFactory) { + private EvaluationContext createEvaluationContext(Object result, @Nullable BeanFactory beanFactory) { + if (beanFactory != null) { + this.originalEvaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } AnnotatedClass target = new AnnotatedClass(); Method method = ReflectionUtils.findMethod( AnnotatedClass.class, "multipleCaching", Object.class, Object.class); Object[] args = new Object[] {new Object(), new Object()}; Collection caches = Collections.singleton(new ConcurrentMapCache("test")); return this.eval.createEvaluationContext( - caches, method, args, target, target.getClass(), method, result, beanFactory); + caches, method, args, target, target.getClass(), method, result); } diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java index f8c4e331fa..1c50a9801a 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java @@ -31,6 +31,7 @@ import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableTypeProvider; import org.springframework.core.annotation.Order; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -322,7 +323,7 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv given(this.context.getBean("testBean")).willReturn(this.sampleEvents); ApplicationListenerMethodAdapter listener = new ApplicationListenerMethodAdapter( "testBean", GenericTestEvent.class, method); - listener.init(this.context, new EventExpressionEvaluator()); + listener.init(this.context, new EventExpressionEvaluator(new StandardEvaluationContext())); GenericTestEvent event = createGenericTestEvent("test"); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index 7219dface4..3178f74a7c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -55,6 +55,7 @@ import org.springframework.util.Assert; * @author Andy Clement * @author Juergen Hoeller * @author Sam Brannen + * @author Stephane Nicoll * @since 3.0 * @see SimpleEvaluationContext * @see ReflectivePropertyAccessor @@ -90,9 +91,9 @@ public class StandardEvaluationContext implements EvaluationContext { @Nullable private TypeConverter typeConverter; - private TypeComparator typeComparator = new StandardTypeComparator(); + private TypeComparator typeComparator = StandardTypeComparator.INSTANCE; - private OperatorOverloader operatorOverloader = new StandardOperatorOverloader(); + private OperatorOverloader operatorOverloader = StandardOperatorOverloader.INSTANCE; private final Map variables = new ConcurrentHashMap<>(); @@ -329,6 +330,29 @@ public class StandardEvaluationContext implements EvaluationContext { resolver.registerMethodFilter(type, filter); } + /** + * Apply the internal delegates of this instance to the specified + * {@code evaluationContext}. Typically invoked right after the new context + * instance has been created to reuse the delegates. Do not modify the + * {@linkplain #setRootObject(Object) root object} or any registered + * {@linkplain #setVariables(Map) variables}. + * @param evaluationContext the evaluation context to update + * @since 6.1.1 + */ + public void applyDelegatesTo(StandardEvaluationContext evaluationContext) { + // Triggers initialization for default delegates + evaluationContext.setConstructorResolvers(new ArrayList<>(this.getConstructorResolvers())); + evaluationContext.setMethodResolvers(new ArrayList<>(this.getMethodResolvers())); + evaluationContext.setPropertyAccessors(new ArrayList<>(this.getPropertyAccessors())); + evaluationContext.setTypeLocator(this.getTypeLocator()); + evaluationContext.setTypeConverter(this.getTypeConverter()); + + evaluationContext.beanResolver = this.beanResolver; + evaluationContext.operatorOverloader = this.operatorOverloader; + evaluationContext.reflectiveMethodResolver = this.reflectiveMethodResolver; + evaluationContext.typeComparator = this.typeComparator; + } + private List initPropertyAccessors() { List accessors = this.propertyAccessors; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java index a2a1ea7310..7b8d892b0d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,8 @@ import org.springframework.lang.Nullable; */ public class StandardOperatorOverloader implements OperatorOverloader { + static final StandardOperatorOverloader INSTANCE = new StandardOperatorOverloader(); + @Override public boolean overridesOperation(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) throws EvaluationException { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java index 4ecb403c22..fc913470dc 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java @@ -37,6 +37,8 @@ import org.springframework.util.NumberUtils; */ public class StandardTypeComparator implements TypeComparator { + static final StandardTypeComparator INSTANCE = new StandardTypeComparator(); + @Override public boolean canCompare(@Nullable Object left, @Nullable Object right) { if (left == null || right == null) { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/StandardEvaluationContextTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/StandardEvaluationContextTests.java new file mode 100644 index 0000000000..34c603bc28 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/StandardEvaluationContextTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2023 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.BeanResolver; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypeLocator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link StandardEvaluationContext}. + * + * @author Stephane Nicoll + */ +class StandardEvaluationContextTests { + + private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + + @Test + void applyDelegatesToSetDelegatesToTarget() { + StandardEvaluationContext target = new StandardEvaluationContext(); + this.evaluationContext.applyDelegatesTo(target); + assertThat(target).hasFieldOrProperty("reflectiveMethodResolver").isNotNull(); + assertThat(target.getBeanResolver()).isSameAs(this.evaluationContext.getBeanResolver()); + assertThat(target.getTypeLocator()).isSameAs(this.evaluationContext.getTypeLocator()); + assertThat(target.getTypeConverter()).isSameAs(this.evaluationContext.getTypeConverter()); + assertThat(target.getOperatorOverloader()).isSameAs(this.evaluationContext.getOperatorOverloader()); + assertThat(target.getPropertyAccessors()).satisfies(hasSameElements( + this.evaluationContext.getPropertyAccessors())); + assertThat(target.getConstructorResolvers()).satisfies(hasSameElements( + this.evaluationContext.getConstructorResolvers())); + assertThat(target.getMethodResolvers()).satisfies(hasSameElements( + this.evaluationContext.getMethodResolvers())); + } + + @Test + void applyDelegatesToSetOverrideDelegatesInTarget() { + StandardEvaluationContext target = new StandardEvaluationContext(); + target.setBeanResolver(mock(BeanResolver.class)); + target.setTypeLocator(mock(TypeLocator.class)); + target.setTypeConverter(mock(TypeConverter.class)); + target.setOperatorOverloader(mock(OperatorOverloader.class)); + target.setPropertyAccessors(new ArrayList<>()); + target.setConstructorResolvers(new ArrayList<>()); + target.setMethodResolvers(new ArrayList<>()); + this.evaluationContext.applyDelegatesTo(target); + assertThat(target).hasFieldOrProperty("reflectiveMethodResolver").isNotNull(); + assertThat(target.getBeanResolver()).isSameAs(this.evaluationContext.getBeanResolver()); + assertThat(target.getTypeLocator()).isSameAs(this.evaluationContext.getTypeLocator()); + assertThat(target.getTypeConverter()).isSameAs(this.evaluationContext.getTypeConverter()); + assertThat(target.getOperatorOverloader()).isSameAs(this.evaluationContext.getOperatorOverloader()); + assertThat(target.getPropertyAccessors()).satisfies(hasSameElements( + this.evaluationContext.getPropertyAccessors())); + assertThat(target.getConstructorResolvers()).satisfies(hasSameElements( + this.evaluationContext.getConstructorResolvers())); + assertThat(target.getMethodResolvers()).satisfies(hasSameElements( + this.evaluationContext.getMethodResolvers())); + } + + @Test + void applyDelegatesToMakesACopyOfPropertyAccessors() { + StandardEvaluationContext target = new StandardEvaluationContext(); + this.evaluationContext.applyDelegatesTo(target); + PropertyAccessor propertyAccessor = mock(PropertyAccessor.class); + this.evaluationContext.getPropertyAccessors().add(propertyAccessor); + assertThat(target.getPropertyAccessors()).doesNotContain(propertyAccessor); + } + + @Test + void applyDelegatesToMakesACopyOfConstructorResolvers() { + StandardEvaluationContext target = new StandardEvaluationContext(); + this.evaluationContext.applyDelegatesTo(target); + ConstructorResolver methodResolver = mock(ConstructorResolver.class); + this.evaluationContext.getConstructorResolvers().add(methodResolver); + assertThat(target.getConstructorResolvers()).doesNotContain(methodResolver); + } + + @Test + void applyDelegatesToMakesACopyOfMethodResolvers() { + StandardEvaluationContext target = new StandardEvaluationContext(); + this.evaluationContext.applyDelegatesTo(target); + MethodResolver methodResolver = mock(MethodResolver.class); + this.evaluationContext.getMethodResolvers().add(methodResolver); + assertThat(target.getMethodResolvers()).doesNotContain(methodResolver); + } + + private Consumer> hasSameElements(List candidates) { + return actual -> { + assertThat(actual.size()).isEqualTo(candidates.size()); + for (int i = 0; i < candidates.size(); i++) { + assertThat(candidates.get(i)).isSameAs(actual.get(i)); + } + }; + } + +}