在常规的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