我们注意到,从 Spring Boot 2.3.0 开始,访问不存在的属性时,JSP EL 表达式求值变得明显变慢。这个问题似乎影响 Spring Boot 2.3.x(2.3.0 至 2.3.7)和 2.4.x(2.4.0 和 2.4.1)的所有当前版本,但不影响 2.2.x。
从 Spring Boot 2.3.0 开始,会发生以下情况:对于在 JSP 上进行 Spring EL 表达式评估期间访问的每个不存在的属性(例如,当使用${myProperty}
或<c:if test="${myProperty}">...</c:if>
并且视图模型中未定义此类属性时),都会进行类加载器调用来查找类“ java.lang.<attribute>
”、“ java.servlet.<attribute>
”、“ java.servlet.http.<attribute>
”和“ java.servlet.jsp.<attribute>
”。
每次请求(或每个 JSP/标签上下文)的每个唯一属性都会发生一次这种情况;在相同上下文中再次访问相同属性不会导致额外的类查找。这显著减慢了 EL 表达式评估的速度,因为在空闲机器上,每个属性的 4 次查找需要几毫秒。当访问确实存在的属性时,不同的 Spring Boot 版本之间没有性能差异。至少在我们的 HTML 前端中,我们通常只在某些情况下设置模型属性,然后 JSP/标签检查它们是否已设置,因此对不存在的属性的访问非常频繁,这会导致明显的性能问题。
根本原因似乎是ExpressionFactory
使用了不同的版本(com.sun.el.ExpressionFactoryImpl
在 2.3.0+ 中而不是org.apache.el.ExpressionFactoryImpl
在 2.2.x 中)。后者将某个上下文属性设置为 true(见下文),但前者没有。
包含ScopedAttributeELResolver
以下代码:
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 是类org.apache.el.parser.AstIdentifier
。
在 Spring Boot 2.2.x 中,由于上下文布尔值为,因此resolveClass
设置为,而在 2.3.0 及更高版本中,它仍为。当调用时会出现性能问题,它会执行四个类查找。false
true
true
importHandler.resolveClass(key)
通常应该设置此属性的类是org.apache.el.parser.AstIdentifier
(在 Spring Boot 2.3.0+ 中没有使用,有com.sun.el.parser.AstIdentifier
使用),使用以下代码:
/* Putting the Boolean into the ELContext is part of a performance
* optimisation for ScopedAttributeELResolver. When looking up "foo",
* the resolver can't differentiate between ${ foo } and ${ foo.bar }.
* This is important because the expensive class lookup only needs to
* be performed in the later case. This flag tells the resolver if the
* lookup can be skipped.
*/
if (parent instanceof AstValue) {
ctx.putContext(this.getClass(), Boolean.FALSE);
} else {
ctx.putContext(this.getClass(), Boolean.TRUE);
}
设置时类加载变得可见 logging.level.org.apache.catalina.loader=DEBUG
在 Spring Boot 2.3.0 及更高版本中,访问时每个请求都会记录以下内容${myProperty}
:
o.a.c.loader.WebappClassLoaderBase : findClass(java.lang.myProperty)
o.a.c.loader.WebappClassLoaderBase : --> Returning ClassNotFoundException
o.a.c.loader.WebappClassLoaderBase : findClass(javax.servlet.myProperty)
o.a.c.loader.WebappClassLoaderBase : --> Returning ClassNotFoundException
o.a.c.loader.WebappClassLoaderBase : findClass(javax.servlet.http.myProperty)
o.a.c.loader.WebappClassLoaderBase : --> Returning ClassNotFoundException
o.a.c.loader.WebappClassLoaderBase : findClass(javax.servlet.jsp.myProperty)
o.a.c.loader.WebappClassLoaderBase : --> Returning ClassNotFoundException
这在极简的 Spring Boot 应用程序中很容易重现。在本地,禁用类加载器日志记录后,在 Spring Boot 2.2.12 中,当我敲击一个简单的 JSP 时,该 JSP 访问 30 个不同的不存在的模型属性时,我每秒收到大约 4000 个请求。在所有后续的 Spring Boot 版本中,我每秒只收到大约 33 个请求。
您可以在此处找到测试应用程序:http://www.schuerger.com/spring-boot/test-app.tar.gz
运行它
mvn clean compile spring-boot:run
然后使用ApacheBench
它来进行 URL 攻击:
ab -n 800 -c 8 http://localhost:8080/
之后,编辑 pom.xml 以切换到版本 2.2.12.RELEASE,重复并比较。禁用类加载日志记录以获得更真实的结果。
将此与将TestController
所有 30 个属性设置为虚拟值(而不是保留它们未定义)进行比较。之后,Spring Boot 版本之间的性能相似。
请注意,这很可能不是 Tomcat 的问题。我已经在本地使用不同的 Tomcat 版本(9.0.34、9.0.35 和 9.0.41)和不同的 Spring Boot 组合进行了测试。请注意,Spring Boot 2.2.7 默认使用版本 9.0.34,2.2.12 使用 9.0.41,2.3.0 使用 9.0.35。无论使用哪个 Tomcat 版本,此问题都会出现在 2.3.x 和 2.4.x 中,而无论使用哪个 Tomcat 版本,此问题都不会出现在 2.2.x 中。