【问题标题】:AspectJ Load time weaver doesn't detect all classesAspectJ 加载时间编织器未检测到所有类
【发布时间】:2010-09-01 15:08:35
【问题描述】:

我在“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 转换器是否通过了课程。我在ClassPreProcessorAgent.transform(..) 中设置了一个断点,尽管Lang 类与其他类(Jetty 的WebAppClassLoader 的一个实例)是由同一个类加载器加载的,但它似乎从未到达该方法。

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

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

下一条线索:我打开了-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 是否会因为不可知的原因缓存类实例?

【问题讨论】:

    标签: java aop aspectj spring-aop


    【解决方案1】:

    好的,我已经解决了这个问题。本质上,这是与一些自定义扩展结合使用的 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 定义,以搜索带有 @987654325 注释的 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 类的 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。

    鳍。

    【讨论】:

      【解决方案2】:

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

      另外,你没有提到 AspectJ 版本 - 如果你在 1.6.7 上,除了微不足道的 aop.xml 之外,还有其他问题。您应该使用 1.6.8 或 1.6.9。

      ltw 实际上是如何工作的?

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

      安迪·克莱门特
      AspectJ 项目负责人

      【讨论】:

      • 我的战争库中有 1.6.9 的 aspectjweaver 和 aspectjrt。 Lang.class.getClassLoader() 返回与编织的另一个类相同的类加载器实例。然后我通过在 ClassPreProcessorAgent 中放置一个断点进行了实验,事实上,Lang 从未传递给它的 transform() 方法。也许我应该尝试将它部署在 Tomcat 上。
      • 当你提到它被“过早”加载的问题时,它提醒我我以前在 Spring 上见过它。快速搜索 spring Jira 带来了jira.springframework.org/browse/SPR-4963,这是一个类似的问题。它建议将类重新转换作为答案(其中类将再次通过加载过程驱动,使用原始字节,但第二次通过 AspectJ 会看到它),但据我所知,我们没有做任何事情春季还没有 - 我认为 3.1 也没有任何计划。
      【解决方案3】:

      选项 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”看不到任何东西,但它可能存在(或错误)

      【讨论】:

      • 感谢您的回答。有趣的想法是字符串“Lang”可能会导致问题。但我应该提到,还有其他类(至少一个)也没有被编织。另一个已知的称为 CommServer。所以我怀疑这是问题所在。不过,我可能不得不开始查看 AspectJ 源代码。
      猜你喜欢
      • 1970-01-01
      • 2019-05-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-27
      • 1970-01-01
      • 1970-01-01
      • 2012-01-26
      相关资源
      最近更新 更多