Improved performance of getter/setter method caching in OgnlRuntime #12
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
The current implementation of the cache for getter/setter methods in
ognl.OgnlRuntime
has three issues, mostly from the performance standpoint:Map
implementation being used for thecacheGetMethod
andcacheSetMethod
static variables isjava.util.HashMap
, which does not allow concurrent access. It seems that an attempt to solve this was made by adding asynchronized {...}
block around the lines of code where new values areput(...)
to these maps, but this is not completely correct because it would allow read access to those maps by a thread at the same time some other thread is inside that synchronized block modifying the map, which is a concurrency issue.CacheKey
, which contains two properties:Class clazz
andString propertyName
. The problem here is that every execution ofgetGetMethod(...)
orgetSetMethod(...)
will need to create a newCacheKey
object in order to access the cache. And givengetGetMethod(...)
, used for obtaining getter methods, is one of the most executed OGNL methods in most environments, this means huge amounts ofCacheKey
objects created in heavy-load scenarios, which goes against memory efficiency and latency mitigation.cache.get(...)
call made in order to obtain the cached method (if it exists in cache), acache.containsKey()
call is performed too. This is done in order to correctly resolve cachedMethod
s which might be null (because they don't exist and must be searched elsewhere), but in a large percent of the cases it means two accesses to the cache map when one (theget
) would suffice.In order to solve these three issues, I've performed the following actions, included in this pull request:
Action 1. Create a new private, internal class called
ClassPropertyMethodCache
so that both propertiescacheGetMethod
andcacheSetMethod
inOgnlRuntime
are instances of this class. This new class will cache theMethod
s corresponding to a specificpropertyName
of aclazz
by means of aConcurrentHashMap<Class<ConcurrentHashMap<String,Method>>
, this is, a two-levelConcurrentHashMap
which resolves indexed byclazz
at the first level, and then bypropertyName
at the second.By using
ConcurrentHashMap
, this new class solves the concurrency issue in this cache. And by using a two-level structure it removes the need to create any kind of complex key object, be it a specialized class likeCacheKey
or aString
created by appending the class name and the property name like it was in previous versions. This means there will not be thousands of cache key objects created just for accessing the cache under heavy load, and as a result there will be a noticeable save in memory usage. Besides, bothjava.lang.Class
andjava.lang.String
--the two classes used as keys in the maps-- have very fast and cachedhashCode()
andequals()
methods, which means Map access will be very fast.Also, the amount of
ConcurrentHashMap
instances created using this new structure (one per class) should not be too worrying, as the total amount of different classes for which getter methods are explored is normally very small in most OGNL scenarios. And in any case, it will be a tiny number compared to the number of differentCacheKey
objects being created right now (one per expression evaluation).Action 2. Reorganize code in
OgnlRuntime#getGetMethod(...)
andOgnlRuntime#getSetMethod(...)
so that calls tocontainsKey(...)
are avoided when a cached getter actually exists (which is the most common case), and also removedsynchronized {...}
blocks, now unneeded thanks to the use ofConcurrentHashMaps
s.