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.