JSP Performance degrades from Spring Boot 2.3.x for undefined attributes

Vivek Vara
4 min readJan 30, 2021

Problem Definition

We have noticed that JSP EL expression evaluation has become significantly slower when undefined attributes are accessed, starting with Spring Boot 2.3.x and 2.4.x, but not 2.2.x or previous.

Root Cause Analysis (The Beginning)

Since Spring Boot 2.3.0, For each undefined attribute accessed during EL expression evaluation on a JSP and if such attribute is not defined in view model or not passed from a servlet, there are class loader calls to find the classes “java.lang.<attribute>", "java.servlet.<attribute>", "java.servlet.http.<attribute>" and "java.servlet.jsp.<attribute>"

This happens once per unique attribute on each request (or per JSP/tag context); accessing the same attribute again in the same context does not lead to additional class lookups. This significantly slows down the EL expression evaluation, since the 4 lookups for each attribute take a few milliseconds on an idle machine. When properties that do exist are accessed, there is no performance difference between the different Spring Boot versions.

Hence accesses to undefined attributes leads to noticeable performance problems if it is in high quantity.

The root cause seems to be that a different ExpressionFactory is used.

com.sun.el.ExpressionFactoryImpl in 2.3.0+ (from org.glassfish:jakarta.el of Glassfish reference implementation)

org.apache.el.ExpressionFactoryImplin 2.2.x- (from org.apache.tomcat.embed:tomcat-embed-el of Apache implementation)

org.apache.el.ExpressionFactoryImplinsets a certain context attribute to true but com.sun.el.ExpressionFactoryImpldoes not.

The javax.servlet.jsp.el.ScopedAttributeELResolver contains this code:

boolean resolveClass = true;
// Performance short-cut available when running on Tomcat
if (AST_IDENTIFIER_KEY != null) {
// Tomcat will set this key to Boolean.TRUE if the
// identifier is a stand-alone identifier (i.e.
// identifier) rather than part of an AstValue (i.e.
// identifier.something). Imports do not need to be
// checked if this is a stand-alone identifier
Boolean value = (Boolean) context.getContext(AST_IDENTIFIER_KEY);
if (value != null && value.booleanValue()) {
resolveClass = false;
}
}
// This might be the name of an imported class
ImportHandler importHandler = context.getImportHandler();
if (importHandler != null) {
Class<?> clazz = null;
if (resolveClass) {
clazz = importHandler.resolveClass(key);
}
...

AST_IDENTIFIER_KEY is the class org.apache.el.parser.AstIdentifier

In Spring Boot 2.2.x, resolveClass is set to false since the context boolean is true, in 2.3.0 and beyond it stays at true. The performance problem occurs when importHandler.resolveClass(key) is called, which does the four class lookups.

The class that should normally set this attribute is org.apache.el.parser.AstIdentifier (which is not used in Spring Boot 2.3.0+, there com.sun.el.parser.AstIdentifier is used), comparing below snippet:

com.sun.el.parser.AstIdentifier VS org.apache.el.parser.AstIdentifier

The class loading becomes visible when setting logging.level.org.apache.catalina.loader=DEBUG

Workaround

In 2.3.0 Spring Boot made a change that meant it uses the same EL implementation with all the embedded containers that support it. Refer https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes#validation-starter-no-longer-included-in-web-starters. This is why the expression factory has changed.

Exclude org.glassfish:jakarta.el and add a dependency to restore the previous behavior. This could be a quick workaround for the previous performance behavior.

It might be possible that org.apache.tomcat.embed:tomcat-embed-el already included via other transitive dependencies i.e. org.apache.tomcat.embed:tomcat-embed-jasper. It is recommended that only one implementation is present.

Better Solution

Glassfish reference implementation appears to be faster than Apache EL implementation.

If we want to go with Glassfish reference implementation and also want to improve performance then we need to add scope to each attribute to make it defined. Refer https://bz.apache.org/bugzilla/show_bug.cgi?id=57583

As suggested by tomcat while migrating to tomcat 8 in https://tomcat.apache.org/migration-8.html

JavaServer Pages 2.3

Unified Expression Language 3.0 added support for referencing static fields and methods. Supporting this feature in JSPs required changing the javax.servlet.jsp.el.ScopedAttributeELResolver implementation so that it also checked identifiers to see if they were names of imported classes or fields. In some circumstances, this change triggers significant slow down. This affects identifiers that may refer to a page, request, session or application scoped variable or may be undefined. When undefined, it takes significantly longer to resolve the identifier since it is now also checked to see if it is an imported class or field. To avoid this slow down, code such as:

${undefined}

should be replaced with:

${requestScope.undefined}

or similar, using the appropriate scope for where the variable is defined.

The Conclusion

Spring Boot has considered it as an enhancement in Spring Boot 2.5.0 release onwards and preferred Tomcat’s implementation instead of Glassfish and there is no plan for backporting to 2.3.x and 2.4.x. Refer https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.5.0-M1-Release-Notes#default-expression-language-el-implementation

Thanks & Happy reading!

--

--