说说在 Spring AOP 中如何实现类加载期织入(LTW)

我们可以在类加载期通过字节码编辑技术将切面织入目标类,这种方式叫做 LTW(Load Time Weaving)。

AspectJ LTW 使用 Java 5.0 提供的代理功能实现织入工作 。JDK 的代理功能能够让代理器访问到 JVM 的底层部件,借此向 JVM 注册类文件转换器,在类加载时对类文件的字节码进行转换 。 AspectJ LTW 是基于 JDK 动态代理技术实现的,所以它的作用范围是整个 JVM ,因此这种方式较为粗放,对于单一 JVM 多个应用的场景并不适用 。

相对于 AspectJ LTW, Spring LTW 提供了细粒度的控制,支持在单个 ClassLoader 范围内实施类文件转换,且配置更为简单。

1 instrument 包的工作原理

JDK5.0 新增了 java.lang.instrument 包,它包含能对 JVM 底层组件进行访问的类 。 我们可以在启动时通过 JVM 的 java agent 代理参数获取 JVM 内部组件的引用,以便在后续操作中使用 。 借助 JDK 动态代理,我们可以在 JVM 启动时装配并应用 ClassTransformer,对类字节码进行转换,从而实现 AOP 功能。

java.lang.instrument 包含两个重要接口。

1.1 ClassFileTransformer

它是 Class文件转换器接口,它定义了一个方法:

 byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

该方法实现对 Class 文件的字节码进行转换, classfileBuffer 是类文件对应的字节码数组,返回的 byte[] 为转换后的字节码 。 如果返回 null ,则表示不进行字节码转换。

1.2 Instrumentation

代表 JVM 内部的一个构件 可以通过该接口的方法向 JVM 的 Instrumentation 注册一些 ClassFileTransformer ,注册转换器的接口方法为:

void
    addTransformer(ClassFileTransformer transformer, boolean canRetransform);

当 ClassFileTransformer 实例注册到 JVM 后, JVM 在加载 Class 文件时,会先调用这个 ClassFileTransformer 的 transform() 方法对 Class 文件的字节码进行转换 。 如果向 JVM 中注册多个 ClassFileTransformer ,它们将按注册的顺序被调用 。 这样 ClassFileTransformer 的实现者就可以从 JVM 层面截获所有类的字节码,并加入希望添加的逻辑 。

下图描绘了拥有多个转换器的 JVM 从加载类到最终生成对应的类字节码的过程:

2 使用 LTW 织入切面

Spring LTW 支持 AspectJ 定义的切面,既可以是直接采用 AspectJ 语法定义的切面,也可以是采用基于 @AspectJ 注解通过 Java 类定义的切面。Spring LTW 采用了与 AspectJ LTW 相同的基础架构,即利用类路径下的 META-INF/aop.xml 配置文件找到切面定义及切面所要实施的候选目标类,通过 LoadTimeWeaver 在 ClassLoader 加载类文件时将切面织入目标类中。

利用特定 Web 容器的 ClassLoader ,通过 LoadTimeWeaver 将 Spring 提供的 ClassFileTransformer 注册到容器的 ClassLoader 中 。在类加载期间,注册的 ClassFileTransformer 会读取 AspectJ 的配置文件,即类路径下的 META-INF/aop.xml 文件,获取切面,接着对 Bean 类进行字节码转换,织入切面 。Spring 容器初始化 Bean 实例时,采用的 Bean 类就是已经被织入切面的类。

2.1 LoadTimeWeaver

大多数的 Web 应用服务器 ( 除了 Tomcat ) 的 ClassLoader 无需通过 javaagent 参数指定代理,即可支持直接访问 Instrument。拥有这种能力的 ClassLoader 被称之为 “ 组件使能( instrumentation-capable ) ”。 通过组件使能能力,我们就可以方便地访问 ClassLoader 的 Instrument。 Spring 使用 Web 应用服务器类加载器,为这些应用服务器分别提供了专门的 LoadTimeWeaver, 以便向特定的 ClassLoader 注册 ClassFileTransformer ,然后再对指定的类进行字节码转换,实现切面的织入 。

Spring 的 org.springframework.instrument.classloading.LoadTimeWeaver 接口定义了类加载期织入器的高层协议,该接口包含三个方法。

方法 说明
void addTransformer(ClassFileTransformer transformer) 新增 transformer 到织入器。
ClassLoader getInstrumentableClassLoader() 返回具有 Instrument 功能的 ClassLoader。
ClassLoader getThrowawayClassLoader() 返回可丢弃的 ClassLoader。

2.2 配置 Spring


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    
    <context:load-time-weaver/>
beans>

2.3 配置 Tomcat

这里以 Tomcat6.x 和 Tomcat7.x 为例。

  1. 首先在该页面中下载对应 Spring 框架版本的 spring-instrument-tomcat-{version}.jar,然后复制到 /lib 下,该 JAR 只有两个类,其中一个即是 TomcatInstrumentableClassLoader 类。
  2. 接着在 META-INF 目录下,新建 context.xml,内容为:

<Context path="/xxx" docBase="xxx">
    <loader
            loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"
            useSystemClassLoaderAsParent="false"/>
Context>

通过以上配置,Tomcat 容器就支持直接在类加载期实现织入功能啦O(∩_∩)O~

2.4 具有组件使能的 Web 应用服务器

具有组件使能的 Web 应用服务器有以下这几种:

  • BEA WebLogic 10.0+
  • OC4J 10.1.3.1+
  • Resin 3.1+
  • JBoss 5.x+

如果应用是运行在这些Web 应用服务器中,那么我们就只需在 Spring 中配置 就可以啦O(∩_∩)O~

你可能感兴趣的:(Spring)