3.2、cglib 生成代理对象的玩法
除了 jdk 能实现动态的创建代理对象以外,还有一个非常有名的第三方框架:cglib,它也可以做到运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。
cglib 特点如下:
- cglib 不仅可以代理接口还可以代理类,而 JDK 的动态代理只能代理接口
- cglib 是一个强大的高性能的代码生成包,它广泛的被许多 AOP 的框架使用,例如我们所熟知的 Spring AOP,cglib 为他们提供方法的 interception(拦截)。
- CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类,速度非常快。
在使用 cglib 之前,我们需要添加依赖包,如果你已经有spring-core
的jar
包,则无需引入,因为spring
中包含了cglib
。
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.5</version> </dependency>
下面,我们还是以两数相加
为例,介绍具体的玩法!
- 创建接口
public interface CglibCalculator { /** * 计算两个数之和 * @param num1 * @param num2 * @return */ Integer add(Integer num1, Integer num2); }
- 目标对象
public class CglibCalculatorImpl implements CglibCalculator { @Override public Integer add(Integer num1, Integer num2) { Integer result = num1 + num2; return result; } }
- 动态代理对象
public class CglibProxyFactory implements MethodInterceptor { /** * 维护一个目标对象 */ private Object target; public CglibProxyFactory(Object target) { this.target = target; } /** * 为目标对象生成代理对象 * @return */ public Object getProxyInstance() { //工具类 Enhancer en = new Enhancer(); //设置父类 en.setSuperclass(target.getClass()); //设置回调函数 en.setCallback(this); //创建子类对象代理 return en.create(); } @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("方法调用前,可以添加其他功能...."); // 执行目标对象方法 Object returnValue = method.invoke(target, args); System.out.println("方法调用后,可以添加其他功能...."); return returnValue; } }
- 测试类
public class TestCglibProxy { public static void main(String[] args) { //目标对象 CglibCalculator target = new CglibCalculatorImpl(); System.out.println(target.getClass()); //代理对象 CglibCalculator proxyClassObj = (CglibCalculator) new CglibProxyFactory(target).getProxyInstance(); System.out.println(proxyClassObj.getClass()); //执行代理方法 Integer result = proxyClassObj.add(1,2); System.out.println("相加结果:" + result); } }
- 输出结果
class com.example.java.proxy.cglib1.CglibCalculatorImpl class com.example.java.proxy.cglib1.CglibCalculatorImpl$$EnhancerByCGLIB$$3ceadfe4 方法调用前,可以添加其他功能.... 方法调用后,可以添加其他功能.... 相加结果:3
将 cglib 生成的代理类改写为静态实现类大概长这样:
public class CglibCalculatorImplByCGLIB extends CglibCalculatorImpl implements Factory { private static final MethodInterceptor methodInterceptor; private static final Method method; public final Integer add(Integer var1, Integer var2) { return methodInterceptor.intercept(this, method, new Object[]{var1, var2}, methodProxy); } //.... }
其中,拦截思路与 JDK 类似,都是通过一个接口方法进行拦截处理!
在上文中咱们还介绍到了,cglib 不仅可以代理接口还可以代理类,下面我们试试代理类。
- 创建新的目标对象
public class CglibCalculatorClass { /** * 计算两个数之和 * @param num1 * @param num2 * @return */ public Integer add(Integer num1, Integer num2) { Integer result = num1 + num2; return result; } }
- 测试类
public class TestCglibProxyClass { public static void main(String[] args) { //目标对象 CglibCalculatorClass target = new CglibCalculatorClass(); System.out.println(target.getClass()); //代理对象 CglibCalculatorClass proxyClassObj = (CglibCalculatorClass) new CglibProxyFactory(target).getProxyInstance(); System.out.println(proxyClassObj.getClass()); //执行代理方法 Integer result = proxyClassObj.add(1,2); System.out.println("相加结果:" + result); } }
- 输出结果
class com.example.java.proxy.cglib1.CglibCalculatorClass class com.example.java.proxy.cglib1.CglibCalculatorClass$$EnhancerByCGLIB$$e68ff36c 方法调用前,可以添加其他功能.... 方法调用后,可以添加其他功能.... 相加结果:3
四、静态织入
在上文中,我们介绍的代理方案都是在代码运行时动态的生成class
文件达到动态代理的目的。
回到问题的本质,其实动态代理的技术目的,主要为了解决静态代理模式中当目标接口发生了扩展,代理类也要跟着一遍变动的问题,避免造成了工作伤的繁琐和复杂。
在 Java 生态里面,还有一个非常有名的第三方代理框架,那就是AspectJ
,AspectJ
通过特定的编译器可以将目标类编译成class
字节码的时候,在方法周围加上业务逻辑,从而达到静态代理的效果。
采用AspectJ
进行方法植入,主要有四种:
- 方法调用前拦截
- 方法调用后拦截
- 调用方法结束拦截
- 抛出异常拦截
使用起来也非常简单,首先是在项目中添加AspectJ
编译器插件。
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <goals> <goal>compile</goal> <goal>test-compile</goal> </goals> </execution> </executions> <configuration> <source>1.6</source> <target>1.6</target> <encoding>UTF-8</encoding> <complianceLevel>1.6</complianceLevel> <verbose>true</verbose> <showWeaveInfo>true</showWeaveInfo> </configuration> </plugin>
然后,编写一个方法,准备进行代理。
@RequestMapping({"/hello"}) public String hello(String name) { String result = "Hello World"; System.out.println(result); return result; }
编写代理配置类
@Aspect public class ControllerAspect { /*** * 定义切入点 */ @Pointcut("execution(* com.example.demo.web..*.*(..))") public void methodAspect(){} /** * 方法调用前拦截 */ @Before("methodAspect()") public void before(){ System.out.println("代理 -> 调用方法执行之前......"); } /** * 方法调用后拦截 */ @After("methodAspect()") public void after(){ System.out.println("代理 -> 调用方法执行之后......"); } /** * 调用方法结束拦截 */ @AfterReturning("methodAspect()") public void afterReturning(){ System.out.println("代理 -> 调用方法结束之后......"); } /** * 抛出异常拦截 */ @AfterThrowing("methodAspect()") public void afterThrowing() { System.out.println("代理 -> 调用方法异常......"); } }
编译后,hello
方法会变成这样。
@RequestMapping({"/hello"}) public String hello(Integer name) throws SQLException { JoinPoint var2 = Factory.makeJP(ajc$tjp_0, this, this, name); Object var7; try { Object var5; try { //调用before Aspectj.aspectOf().doBeforeTask2(var2); String result = "Hello World"; System.out.println(result); var5 = result; } catch (Throwable var8) { Aspectj.aspectOf().after(var2); throw var8; } //调用after Aspectj.aspectOf().after(var2); var7 = var5; } catch (Throwable var9) { //调用抛出异常 Aspectj.aspectOf().afterthrowing(var2); throw var9; } //调用return Aspectj.aspectOf().afterRutuen(var2); return (String)var7; }
很显然,代码被AspectJ
编译器修改了,AspectJ
并不是动态的在运行时生成代理类,而是在编译的时候就植入代码到class
文件。
由于是静态织入的,所以性能相对来说比较好!
五、小结
看到上面的介绍静态织入
方案,跟我们现在使用Spring AOP
的方法极其相似,可能有的同学会发出疑问,我们现在使用的Spring AOP
动态代理,到底是动态生成的还是静态织入的呢?
实际上,Spring AOP
代理是对JDK
代理和CGLIB
代理做了一层封装,同时引入了AspectJ
中的一些注解@pointCut
、@after
,@before
等等,本质是使用的动态代理技术。
总结起来就三点:
- 如果目标是接口的话,默认使用 JDK 的动态代理技术;
- 如果目标是类的话,使用 cglib 的动态代理技术;
- 引入了
AspectJ
中的一些注解@pointCut
、@after
,@before
,主要是为了简化使用,跟AspectJ
的关系并不大;
那为什么Spring AOP
不使用AspectJ
这种静态织入
方案呢?
虽然AspectJ
编译器非常强,性能非常高,但是只要目标类发生了修改就需要重新编译,主要原因可能还是AspectJ
的编译器太过于复杂,还不如动态代理来的省心!