Grails JSON 使用内省的编组导致 Classloader.loadClass() 严重瓶颈

Grails JSON marhsaling using introspection causes severe bottleneck on Classloader.loadClass()

我正在使用 Grails 2.2.4,并且有一个控制器端点可以将域对象列表转换为 JSON。在负载下(少至 5 个并发请求),编组性能非常差。进行线程转储时,线程被阻塞:

java.lang.ClassLoader.loadClass(ClassLoader.java:291)

注册了一个编组器以使用反射和内省编组所有域对象。意识到反射和内省比直接方法调用慢,我仍然看到意想不到的行为,因为 class 加载器每次都是调用者,进而发生阻塞。示例堆栈跟踪如下:

java.lang.Thread.State: BLOCKED (on object monitor)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:291)
    - waiting to lock <785e31830> (a org.grails.plugins.tomcat.ParentDelegatingClassLoader)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
    at java.beans.Introspector.instantiate(Introspector.java:1470)
    at java.beans.Introspector.findExplicitBeanInfo(Introspector.java:431)
    at java.beans.Introspector.<init>(Introspector.java:380)
    at java.beans.Introspector.getBeanInfo(Introspector.java:167)
    at java.beans.Introspector.getBeanInfo(Introspector.java:230)
    at java.beans.Introspector.<init>(Introspector.java:389)
    at java.beans.Introspector.getBeanInfo(Introspector.java:167)
    at java.beans.Introspector.getBeanInfo(Introspector.java:230)
    at java.beans.Introspector.<init>(Introspector.java:389)
    at java.beans.Introspector.getBeanInfo(Introspector.java:167)
    at java.beans.Introspector.getBeanInfo(Introspector.java:230)
    at java.beans.Introspector.<init>(Introspector.java:389)
    at java.beans.Introspector.getBeanInfo(Introspector.java:167)
    at org.springframework.beans.CachedIntrospectionResults.<init>(CachedIntrospectionResults.java:217)
    at org.springframework.beans.CachedIntrospectionResults.forClass(CachedIntrospectionResults.java:149)
    at org.springframework.beans.BeanWrapperImpl.getCachedIntrospectionResults(BeanWrapperImpl.java:324)
    at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:727)
    at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:721)
    at org.springframework.beans.PropertyAccessor$getPropertyValue.call(Unknown Source)
    at com.ngs.id.RestDomainClassMarshaller.extractValue(RestDomainClassMarshaller.groovy:203)
...
...

加载具有相同参数的相同端点的简单基准测试导致 loadClass 调用。

我的印象是,classes 至少会被 class 加载程序缓存,而不是在每次方法调用时加载以获取 属性 进行编组。

获取属性值的代码如下:

BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(domainObject);
return beanWrapper.getPropertyValue(property.getName());

是否需要配置设置来确保 classes 只加载一次?或者可能是另一种获取 属性 的方法不会每次都导致 class 加载?或者也许是一种更高效的方法来实现这一点?

为每个域编写自定义封送拆收器 class 可以避免反射和自省,但会产生大量重复代码。

感谢任何意见。

经过大量挖掘,这就是我的发现。

使用 BeanUtils.getPropertyDescriptors 和 getValue 将始终尝试找到一个 BeanInfo class 来描述使用 class 加载器的 bean。在这种情况下,我们不为我们的 grails 域 classes 提供 BeanInfo classes,所以这个调用是多余的。我找到了一些信息,您可以在其中提供自定义 BeanInfoFactory 来绕过它并排除您​​的包,但我找不到如何使用 Grails 配置它。

还搜索 springframework 文档,您可以传递一个配置选项 Introspector.IGNORE_ALL_BEANINFO,它会告诉 CachedIntorspectionResults 从不查找 bean classes。然而,这在 grails 2.2.4 当前的 springframework 3.1.4 版本中不可用。较新的版本似乎有此选项。

因此,如果使用 BeanUtils,您不能绕过 class 加载器上的初始查找。但是,后续加载程序应由 CachedIntrospectionResults 缓存。不幸的是,这不会发生在我们的场景中。在查看查找是否可缓存的测试中似乎存在错误。请参阅下面的详细信息。

解决方法最终是退回到使用纯反射。而不是使用:

beanWrapper.getPropertyValue(property.getName());

使用:

PropertyDescription pd = BeanUtils.getPropertyDescriptor(domainObject.getClass(), property.getName())
pd.readMethod.invoke(domainObject)

pd缓存的地方。

修复此问题后,探查器仍然显示开箱即用的 grails 编组器在 CachedIntorspectionResults 上缺少缓存。这是由于 CachedIntrospectionResults 中的错误缓存实现所致。解决此问题的方法是将正确的 class 加载器添加到 CachedIntrospectionResults 中的 acceptedClassLoaders。

public class EnhanceCachedIntrospectionResultsAcceptedClassLoadersListener implements ServletContextListener {

    public void contextInitialized(ServletContextEvent event) {
        CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader().getParent());
    }

    public void contextDestroyed(ServletContextEvent event) {
        CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader().getParent());
        Introspector.flushCaches();
    }
}

请注意,需要将父级添加到接受的 class 加载器列表而不是当前的 class 加载器。不确定这是否特定于 grails,但这解决了问题。我不确定此修复是否有副作用。

总而言之,在使用直接反射并修复 CachedIntrospectionResults 缓存后,我们从原始设置中的 10 requests/sec 增加到 120 requests/sec。

然而,真正让我们大开眼界的是,如果我们对每个域使用 1-1 编组器 class,我们会看到性能比通用编组器再提高 2 倍,在通用编组器中我们测试对象是否是class 等。我们使用通用编组器节省了大量代码,但要获得与编写 1-1 编组器相当的性能,还有很多工作要做。

希望这对遇到此问题的其他人有用...