spring LTW agent方式在tomcat 8下无效

在常规的spring ltw中,我们对tomcat使用ltw,一般是以下两种方式。

//1 在启动项中添加javaagent选项
-javaagent:e:/spring-instrument-4.1.1.RELEASE.jar

//2 使用自定义的启动器,在META-INF/context.xml中增加以下内容
<?xml version="1.0" encoding="UTF-8" ?>
<Context>
 <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader" /> 
</Context>

这两种方式在tomcat6以及tomcat7均可以正常工作。但对于tomcat8,第1种方式已经不能再工作,仅能使用第二种方式。原因就在于tomcat8的webClassLoader已经提供了InstrumentableClassLoader接口。此接口将导致spring直接将aspectj的AspectJClassBypassingClassFileTransformer 直接添加到tomcat的classLoader中。但由于不是很正确的实现方式,导致aspect在使用tomcat8提供的classLoader时,并不能有效地对自己的advice进行weaver,导致报以下的错误信息: 

java.lang.NoSuchMethodError: XXXAdvice.aspectOf()LXXXAdvice;
 atXXX.index(AbcController.java:30)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:606)
 at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:215)
 at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
 at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)

这个错误的产生在于aspectJ的初始化过程和classLoader之间的交互行为,以及tomcat8中不准确的缓存行为。

1    aspectj的初始化

aspectj的初始化是由类ClassPreProcessorAgentAdapter来完成的。而初始化,则是在第一次准备instrument时实施。在类Aj中,需要获取一个ClassLoaderWeavingAdaptor对象,即伴随着此对象的初始化。在类ClassLoaderWeavingAdaptor方法initialize中,进行获取相应的定义进行初始化。

定义即我们使用的aop.xml文件,aspectj需要从classpath中查找所有的信息,以完成加载过程。在获取完定义之后,aspectj即需要加载在aop.xml中定义的aspect文件。这时候即需要读取具体的aspect类。相应的代码即在类ClassLoaderWeavingAdaptor的registerAspects中,即注册aspect对象。

具体加载aspect对象的方法,则由类ClassLoaderRepository方法loadClass(String className)来完成。这里并不是返回一个class对象,而是一个javaClass对象(可以理解为javassist中的CtClass对象,即并没有初始化此类)。相应的代码如下所示:

 String classFile = className.replace('.', '/');
   InputStream is = (useSharedCache ? url.openStream() : loaderRef.getClassLoader().getResourceAsStream(
     classFile + ".class"));//这里使用class后缀,与后续tomcat8的判断直接吻和
   if (is == null) {
    throw new ClassNotFoundException(className + " not found using url " + url);
   }
   ClassParser parser = new ClassParser(is, className);
   clazz = parser.parse();

如上所示,最终使用classLoader来加载资源信息,只不过这里的资源是一个class文件资源。接下来,就交给tomcat的classLoader来加载资源文件了。

2    tomcat加载资源文件

tomcat加载文件有两个地方,一种是需要loadClass的时候需要找到相应的class定义,而一种则是需要getResource这种加载资源文件的方式。在tomcat8中,这两种方式都是通过方法findResourceInternal来完成的。类WebappClassLoaderBase中,主要代码如下所示:

 protected ResourceEntry findResourceInternal(final String name, final String path) {
......
        boolean isClassResource = path.endsWith(CLASS_FILE_SUFFIX); //这里实现的问题
......
        if (isClassResource && entry.binaryContent != null &&
                this.transformers.size() > 0) {
......
            for (ClassFileTransformer transformer : this.transformers) { 
                try {
                    byte[] transformed = transformer.transform(
                            this, internalName, null, null, entry.binaryContent
                    );
                    if (transformed != null) {
                        entry.binaryContent = transformed;
                    }
        } 
.....
    }

代码如下所示,在tomcat8中,需要发现加载的资源是一个class后续,则认为是正在加载一个class,那么就会调用transformer进行解析。即会调用aspect进行LTW。但是在此时,aspectj正在初始化。如果按照正常的理论,这里就是一个死循环。aspectj调用tomcat8,tomcat8调用aspect。还好aspectj在处理时有一些状态变量进行记录。

因为aspectj正在初始化,其状态enable(类WeavingAdaptor)仍为false,那么这里aspectj直接返回原始的字节码。即我们使用javac编译之后的aspect class文件。
这时tomcat8将未发生变化的aspect 字节码返回给aspectj,aspectj解析正常,完成初始化工作。

3    加载advice weaver类,准备LTW(失败)

在aop.xml中定义的aspect类并不是一开始就直接初始化好,仍要按照正常的引用进行加载。即在第一次使用aspect的地方,这个地方即是被weaver的类的切入点。如execute(A.method),那么aspect类被初始化的时刻即在执行A.method的时候(这里被织入Aspect.aspectOf的类似方法)。

OK,正式开始开始aspect类,即我们的advice。我们说因为是LTW,我们的advice需要被aspectj再次weaver,才能成为一个正确的aspect类,否则就是一个普通的java类。
but,记住在第2步,tomcat8认为这个类所在的class文件已经被找到了,而且也已经被transformer进行transform过了,所以直接读取缓存的字节码,进行加载。结果加载的就是一个普通的java对象,在加载完之后,调用相应的aspectOf方法,报NoSuchMethodException了。

4    tomcat7 tomcat6下面

因为旧版本的tomat webAppClassLoader中并没有实现InstrumentableClassLoader接口(不保证,7.0.44版本后或许会加入此接口)。因此,spring将LTW的职责交由上层,即我们定义的javaagent来处理,这样即不存在相应的问题。

至于在META-INF文件中增加context.xml,修改默认classLoader,则属于在每次读取缓存资源时,spring提供的TomcatInstrumentableClassLoader都会主动交给aspectj进行编织。即在第二次,即使已经缓存,spring仍会交给aspectj编织,因此也避免了相应的问题(但感觉有点怪….,是否每次读取class文件都需要编织一次呢…)

转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201412070001.html

相关文章:

作者: flym

I am flym,the master of the site:)

发表评论

邮箱地址不会被公开。 必填项已用*标注