13

我在“aspectj”模式下使用 Spring 的声明性事务(@Transactional 注释)。在大多数情况下,它的工作原理与应有的完全一样,但对于其中一种情况却没有。我们可以调用它Lang(因为它实际上是这样调用的)。

我已经能够将问题定位到加载时间编织器。通过在 aop.xml 中打开调试和详细日志记录,它列出了所有正在编织的类。Lang日志中确实根本没有提到有问题的类。

然后我在 的顶部放了一个断点Lang,导致 Eclipse 在Lang加载类时挂起线程。这个断点在 LTW 编织其他类时被命中!所以我猜它要么尝试编织Lang并且失败并且不输出它,或者其他一些类有一个引用,它Lang在它实际有机会编织它之前强制它加载。

但是我不确定如何继续调试它,因为我无法以较小的规模重现它。关于如何继续的任何建议?


更新:也欢迎其他线索。例如,LTW 实际上是如何工作的?似乎发生了很多魔术。是否有任何选项可以从 LTW 获得更多调试输出?我目前有:

<weaver options="-XnoInline -Xreweavable -verbose -debug -showWeaveInfo">

我忘了汤姆之前提到过:spring-agent被用来允许 LTW,即InstrumentationLoadTimeWeaver.


根据 Andy Clement 的建议,我决定检查 AspectJ 变压器是否通过了课程。我在 中放了一个断点,尽管它是由与其他类相同的类加载器(Jetty 的 WebAppClassLoader 的一个实例)加载的,但该类ClassPreProcessorAgent.transform(..)似乎甚至从未到达该方法。Lang

然后我继续在InstrumentationLoadTimeWeaver$FilteringClassFileTransformer.transform(..). 甚至没有被击中Lang。而且我相信应该为所有加载的类调用该方法,无论它们使用什么类加载器。这开始看起来像:

  1. 我的调试有问题。可能Lang在 Eclipse 报告它的时候没有加载
  2. 爪哇错误?有点牵强,但我想它确实会发生。

下一条线索:我打开了电源-verbose:class,它似乎Lang 过早地加载了——可能是在将变压器添加到 Instrumentation 之前。奇怪的是,我的 Eclipse 断点没有捕捉到这个加载。

这意味着 Spring 是新的嫌疑人。似乎有一些处理ConfigurationClassPostProcessor加载类来检查它们。这可能与我的问题有关。


这些行ConfigurationClassBeanDefinitionReader导致Lang类被读取:

else if (metadata.isAnnotated(Component.class.getName()) ||
        metadata.hasAnnotatedMethods(Bean.class.getName())) {
    beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
    return true;
}

特别是对类的metadata.hasAnnotatedMethods()调用getDeclaredMethods(),它会加载该类中所有方法的所有参数类。我猜这可能不是问题的结束,因为我认为这些类应该被卸载。JVM 会因为不可知的原因缓存类实例吗?

4

3 回答 3

7

好的,我已经解决了这个问题。本质上,这是与一些自定义扩展结合的 Spring 问题。如果有人遇到类似的事情,我将尝试逐步解释正在发生的事情。

首先,我们BeanDefintionParser的项目中有一个自定义。此类具有以下定义:

private static class ControllerBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    protected Class<?> getBeanClass(Element element) {
        try {
            return Class.forName(element.getAttribute("class"));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Class " + element.getAttribute("class") + "not found.", e);
        }
    }

// code to parse XML omitted for brevity

}

现在,在读取所有 bean 定义并BeanDefinitionRegistryPostProcessor开始执行之后,问题就出现了。在这个阶段,一个名为的类ConfigurationClassPostProcessor开始查看所有 bean 定义,以搜索带有注释@Configuration或方法的 bean 类@Bean

在读取 bean 的注解的过程中,它使用了AnnotationMetadata接口。对于大多数常规 bean,使用了一个名为的子类AnnotationMetadataVisitor。但是,在解析 bean 定义时,如果您已经重写了该getBeanClass()方法以返回一个类实例,就像我们所做的那样,而是StandardAnnotationMetadata使用一个实例。当StandardAnnotationMetadata.hasAnnotatedMethods(..)被调用时,它会调用Class.getDeclaredMethods(),这反过来会导致类加载器加载在该类中用作参数的所有类。以这种方式加载的类没有正确卸载,因此永远不会编织,因为这发生在 AspectJ 转换器注册之前。

现在,我的问题是我有这样的课程:

public class Something {
    private Lang lang;
    public void setLang(Lang lang) {
        this.lang = lang;
    }
}

Something然后,我有一个使用我们的 custom 解析的类 bean ControllerBeanDefinitionParser。这触发了错误的注释检测过程,触发了意外的类加载,这意味着 AspectJ 永远没有机会编织Lang.

解决方案是不覆盖getBeanClass(..),而是覆盖getBeanClassName(..),根据文档,这更可取:

private static class ControllerBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    protected String getBeanClassName(Element element) {
        return element.getAttribute("class");
    }

// code to parse XML omitted for brevity

}

getBeanClass每日一课:除非你真的是认真的,否则不要覆盖。实际上,除非您知道自己在做什么,否则不要尝试编写自己的 BeanDefinitionParser。

鳍。

于 2010-09-07T14:26:08.807 回答
4

如果 -verbose/-debug 输出中未提及您的类,这表明它没有被您认为的加载器加载。你能 100% 确定“Lang”不在层次结构中更高的类加载器的类路径上吗?当您触发断点时,哪个类加载器正在加载 Lang?

此外,您没有提到 AspectJ 版本 - 如果您在 1.6.7 上,除了微不足道的 aop.xml 之外的任何问题都与 ltw 有关。您应该使用 1.6.8 或 1.6.9。

ltw 实际上是如何工作的?

简而言之,为每个可能想要编织代码的类加载器创建一个 AspectJ 编织器。AspectJ 被询问是否要在定义到 VM 之前修改一个类的字节。AspectJ 查看它可以通过相关类加载器“看到”(作为资源)的任何 aop.xml 文件,并使用它们来配置自身。配置完成后,它会按照指定编织方面,同时考虑所有包含/排除子句。

Andy Clement
AspectJ 项目负责人

于 2010-09-07T01:02:02.230 回答
1

选项 1) Aspect J 是开源的。打开它,看看发生了什么。

选项 2)将您的课程重命名为 Bang,看看它是否开始工作

如果有硬编码可以跳过“lang”,我不会感到惊讶,尽管我不能说为什么。

编辑 -

在源码中看到这样的代码

        if (superclassnameIndex > 0) { // May be zero -> class is java.lang.Object
            superclassname = cpool.getConstantString(superclassnameIndex, Constants.CONSTANT_Class);
            superclassname = Utility.compactClassName(superclassname, false);

} else {
            superclassname = "java.lang.Object";
        }

看起来他们正试图跳过 java.lang.stuff 的编织......没有看到任何“lang”但它可能存在(或错误)

于 2010-09-01T15:28:14.993 回答