ClassUtils.getInterfaceMethodIfPossible overhead in cached methods for SpEL key/condition expressions #24206
Labels
in: core
Issues in core modules (aop, beans, core, context, expression)
status: backported
An issue that has been backported to maintenance branches
type: regression
A bug that is also a regression
Milestone
Affects: 5.1.0+
Since 'ReflectiveMethodExecutor invokes interface method if possible' (c4df335) has been committed there is a new method getInterfaceMethodIfPossible.
This method unfortunately triggers exception in reflection code which is being catched and ignored as many times as there are interfaces on a class implementing the method (and all of its superclasses), which has an impact on performance (NoSuchMethodException is not pre-allocated so it is not optimized by OmitStackTraceInFastThrow JVM option) especially when such a method is not found (all interfaces have to be traversed).
The issue has been observed in repository code that uses @Cacheable and @CachePut annotations that use SpEL to select keys and describe conditions under which objects are cached.
Spring cache infrastructure creates a new SpEL evaluation context for invocation of cached method when
key
orcondition
expressions are specified and populates that new SpEL context with new instances of reflection accessors. The accessors then will trigger ClassUtils.getInterfaceMethodIfPossible in canRead and then cache the result. However since the new SpEL context is created for each method invocation this is caching ineffective.This will affect probably any SpEL expression frequently executed in the system that is run with a new evaluation context for each invocation.
One possible remedy is to introduce cache on ClassUtils.getInterfaceMethodIfPossible (which I did) or try to fix the spring caching infrastructure (more complex).
I'm sharing the repository with sample code showing the effect here : https://github.com/tawek/classutils-interface-methods-issue
On my HW the overhead of calling @Cacheable method with issue mentioned above is about 127us (~ 7800 calls per second).
When cache is introduced into ClassUtils.getInterfaceMethodIfPossible to avoid exception throwing the overhead is reduced to 10us (~91 000 calls per second). The measurements where done on OpenJDK8 ( OpenJDK11 gives similar results).
The issue is visible if SpEL expressions are more complex and use property access or method calls and if there are more interfaces on navigated objects. The workaround is not to use SpEL expressions if you want low cache overhead and perform lots of cached calls. :(
Potentially this may hit other heavy users of SpEL since that ClassUtils method is invoked from org.springframework.expression.spel.support.ReflectivePropertyAccessor.canRead and org.springframework.expression.spel.support.ReflectiveMethodExecutor. which are both quite common.
The text was updated successfully, but these errors were encountered: