Hi,

A while ago we provided extended opentracing integration with our spring boot applications. Particularly, we allowed developers to mark their methods with annotations allowing them to add custom data (tags) into opentracing spans. Such annotations may contain SpEL expressions, e.g.:

@TracingSpanTag(key = "pizzaSize", value = "#{args[0].pizzaSize}")
public void swallowPizza(Pizza pizza) {

We implemented an annotation processor and evaluated expressions there. To do that we created custom Scope (TracingScope) with annotated method argument values inside:

class TracingAspectHelper {

    private static final BeanExpressionResolver resolver = new StandardBeanExpressionResolver();
    ....
    public static Object resolveValue(ConfigurableBeanFactory beanFactory, String value, JoinPoint joinPoint) {
            TracingScope tracingScope = new TracingScope();
            tracingScope.addToMethodExecutionContext("args", joinPoint.getArgs());
            String embeddedValue = beanFactory.resolveEmbeddedValue(value);
            // BeanExpressionContext is cached forever in a Map inside StandardBeanExpressionResolver
            return resolver.evaluate(embeddedValue, new BeanExpressionContext(beanFactory, tracingScope));
        }
    ...
    private static class TracingScope implements Scope {

        private final Map<String, Object> methodExecutionContext = new HashMap<>();

        public void addToMethodExecutionContext(String key, Object obj) {
            methodExecutionContext.put(key, obj);
        }

        @Override
        public Object get(String name, ObjectFactory<?> objectFactory) {
            return this.methodExecutionContext.get(name);
        }

        @Override
        public Object remove(String name) {
            return null;
        }

        @Override
        public void registerDestructionCallback(String name, Runnable callback) {
            throw new UnsupportedOperationException("This operation is not supported");
        }

        @Override
        public Object resolveContextualObject(String key) {
            return this.methodExecutionContext.get(key);
        }

        @Override
        public String getConversationId() {
            return null;
        }
    }

But after a while we found a memory leak - StandardBeanExpressionResolver stores the evaluation context forever: https://github.com/spring-projects/spring-framework/blob/259bcd60fbbc5cdb8b230595a5004707f4c6ff23/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java#L165

Also, BeanExpressionContext includes ConfigurableBeanFactory in the hashCode but not the Scope: https://github.com/spring-projects/spring-framework/blob/259bcd60fbbc5cdb8b230595a5004707f4c6ff23/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java#L84 Therefore, collisions happen and the evaluation cache map not only starts eating memory but also becomes very inefficient in lookups.

The dirty solution that will solve our problem would be to create BeanExpressionResolver every time on evaluation, but then we will get performance degradation due to the lack of expression caching that BeanExpressionResolver provides in addition to evaluation context cache.

Is this behavior expected?

Comment From: jhoeller

StandardBeanExpressionResolver isn't really meant to be used in such an individual fashion. It is rather the default implementation of the BeanExpressionResolver SPI, for specific purposes within a ConfigurableBeanFactory implementation. The same applies to the Scope interface, this is an SPI for bean instance scoping, not a general purpose value exposure mechanism for SpEL. That's why we assume that there is a small fixed number of pre-defined Scope instances to deal with (that we can easily cache etc), not fresh Scope instances created on the fly and passed into the bean factory SPI contract.

For custom usage, there's the actual SpEL API: SpelExpressionParser and the configurable StandardEvaluationContext, as used within StandardBeanExpressionResolver. Granted, you'll have to configure the ParserContext with a corresponding prefix/suffix and cache Expression instances for your purposes, but at least you would do so at the right level (instead of struggling with BeanFactory SPI contracts and the semantic mismatch there).

Maybe you could take the StandardBeanExpressionResolver implementation as a starting point and create your own convenient delegate, purely based on the org.springframework.expression API (i.e. with no org.springframework.beans.factory.config SPI types)? If there are some further generic pieces that we can provide for such use cases, I'd be happy to consider it, but for a start I'd recommend a custom implementation on top of the plain SpEL API.

Comment From: tfactor2

Could be indeed quite convenient to have some "public" preconfigured resolvers: fully-packaged (with bean context, map, environment, etc accessors configured), lightweight, etc. Both could support various caching strategies. But the status quo is clear.

Thanks for the quick response, no more questions from my side.

Comment From: jhoeller

Since no common need emerged in the meantime and the individual accessors are all public and can be added to a custom StandardEvaluationContext configuration along the lines of how StandardBeanExpressionResolver sets it up internally, I'm closing this issue.