本文主要讲实现AOP的 代理模式原理,以及静态代理,动态代理的区别和具体实现。
对SpringAOP的概念和使用,可以参考以下文章:
Spring_AOP_01——概念讲解
SpringAOP基础和使用
AOP的实现思路是利用了代理模式,关键在于AOP框架对目标对象创建的AOP代理,实现了对目标对象的增强。
AOP的代理方式主要分为两种,静态代理 和 动态代理。
静态代理的代表为AspectJ
Spring使用动态代理,主要有JDK实现和cglib实现。
Spring创建代理的规则为:
默认使用JDK动态代理来创建AOP代理
当需要代理的类没有实现接口的时候,Spring会切换为使用cglib创建AOP代理
可以在配置文件制定,强制使用cglib代理
本篇主要内容:
- 代理模式
- AOP实现方式
- 静态代理
- AspectJ静态代理原理
- 动态代理
- JDK和cglib实现动态代理原理
关于JDK 代理 和 cglib 代理的 源码层原理,本篇不细讲。
代理模式
说到AOP实现原理,那么就首先需要明白什么是代理模式。这里简单讲解一下代理模式的原理。
代理模式使用代理对象完成用户的请求,控制对元对象的访问,屏蔽用户对真实对象的访问。代理模式是一种弄对象结构型模式。
UML结构图如下:
代理模式的结构
代理模式主要包含三个角色:
-
Subject(抽象主题角色)
它声明了真是主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。
反映在实际环境中,这就是我们平时编程时定义的接口和超类。(分别代表JDK代理和cglib代理)
-
Proxy(代理主题角色)
它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象。在代理主题角色中,提供一个与真实主题角色相同的即可欧,以便在任何时候可以替代真实主题。
代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。
在代理主题角色中,主要任务是定义在代理主题的操作,例如日志打印,权限控制等等,在调用真实操作之前or之后执行的操作。
-
RealSubject(真实主题角色)
它定义了代理角色所代表的真实对象,在真实主题角色中实现了实际的业务操作。客户端可以通过代理角色间接的调用真实角色中定义的操作。
AOP的实现方式
AOP的实现使用的是代理模式。其中又分为静态代理和动态代理。
静态代理的代表为AspectJ
Spring使用动态代理,主要有JDK实现和cglib实现。
静态代理的缺点很明显:
一个代理类只能对一个业务接口的实现类进行包装,如果有多个业务接口的话,就要定义很多实现类进行代理才行。
而且如果代理类对业务方法的预处理,调用后处理都是一样的(例如调用前输出提示,调用后关闭连接),则多个代理类就会有很多重复的代码。
这时我们可以定义一个这样的类,它能代理所有实现类的方法调用:根据传进来的业务实现类和方法名进行具体调用——那就是动态代理。
接下来详细介绍一下各自的实现方式以及区别:
静态代理
静态代理就是在编译期生成代理类的方式。
其实上面的代理模式图,实现的就是静态代理。静态代理通常用于对业务的扩充。通过对真实对象的封装,来实现扩展性。
代码简单实现如下:
// 共同接口
public interface Action {
public void doSomething();
}
//真实对象
public class RealObject implements Action{
public void doSomething() {
System.out.println("do something");
}
}
//代理对象
public class Proxy implements Action {
private Action realObject;
public Proxy(Action realObject) {
this.realObject = realObject;
}
public void doSomething() {
System.out.println("proxy before do");
realObject.doSomething(); //调用真实对象方法
System.out.println("proxy after do");
}
}
//运行代码
Proxy proxy = new Proxy(new RealObject());
proxy.doSomething();
这是一个以接口为抽象主题角色实现的静态代理。可以看到,代理对象和真实对象都实现了共同的接口。在代理对象中,保存了一个真实对象的对象引用。并且在调用对真实对象的操作前后,自己可以做一些其他操作。
AspectJ
AspectJ是一个静态代理的增强。需要明确的是AspectJ并不是Spring框架的一部分,而是一套独立的AOP解决方案。
AspectJ是一个java实现的AOP框架,它能够对Java代码进行AOP编译,让Java代码具有AspectJ的AOP功能(一般在编译器执行,需要aspectJ提供的编译器)
使用AspectJ需要依赖额外的第三方包和aspect的编译器。
AspectJ织入方式
AspectJ主要采用的是静态织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
AspectJ除了编译期织入,还存在链接期(编译后)织入,即将aspect类和java目标类同时编译成字节码后,再进行织入处理,这种方式比较有助于已编译好的第三方jar和class文件进行织入操作。(这不是本篇重点,详细的aspectJ原理可以自行查找资料)
当然,不管是编译期织入还是编译后织入,AspectJ都是静态代理的方式织入的。即先构建好代理类的class文件,再运行。
关于ajc编译器,是一种弄能够识别aspect研发的编译器,它采用java语言编写,由于javac并不鞥识别aspect语法,便有了ajc编译器。注意ajc编译器也可以编译java文件。
我们可以反编译一下ajc织入后的java文件,可以很直观的看到ajc是如何将代码织入的。
//编译后织入aspect类的HelloWord字节码反编译类
public class HelloWord {
public HelloWord() {
}
public void sayHello() {
System.out.println("hello world !");
}
public static void main(String[] args) {
HelloWord helloWord = new HelloWord();
HelloWord var10000 = helloWord;
try {
//MyAspectJDemo 切面类的前置通知织入
MyAspectJDemo.aspectOf().ajc$before$com_zejian_demo_MyAspectJDemo$1$22c5541();
//目标类函数的调用
var10000.sayHello();
} catch (Throwable var3) {
MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574();
throw var3;
}
//MyAspectJDemo 切面类的后置通知织入
MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574();
}
}
动态代理
与静态代理相对的,动态代理的代理类,是在运行时才被动态的创建,所以叫做动态代理。
动态代理的好处是:对代理类的函数进行统一管理。解决了解决静态代理中存在的功能重复和代码重复的问题。SpringAOP提供的就是动态代理支持。
与AspectJ一样,目的都是为了统一处理横切业务,但是与AspectJ不同的是,Spring AOP 并不尝试提供完成的AOP功能,而是更注重与Spring IOC 容器的结合,并利用该优势来解决横切业务的问题。
所以在AOP功能完善方面,AspectJ具有的优势更大。(Spring AOP只能在方法层面做横切,AspectJ可以对属性做横切)
同时,Spring注意到AspectJ在AOP的实现上,依赖于特殊的编译器(ajc编译器),因此Spring回避了这一点,Spring采用动态代理技术的实现原理来构建Spring AOP的内部机制(动态织入)。这是与AspectJ(静态织入)最根本的区别。
在AspectJ 1.5 之后,引入了 @Aspect 形式的注解风格开发,Spring也非常快地跟进了这种方式,在Spring 2.0之后便使用了与Aspect 1.5 一样的注解。
注意:Spring只是使用了AspectJ的注解,而没有使用AspectJ的编译器,低层还是使用动态代理技术实现。(很重要!)
Spring的 动态代理主要有两种方式:
- 使用了JDK Proxy的动态代理
- 使用了cglib Ehancer 的动态代理。
JDK实现动态代理
JDK动态代理的原理,是利用反射机制,生成一个和目标类继承了一样接口的匿名代理类。在调用具体方法前使用InvocationHandler来处理。
JDK动态代理的对象在创建时,需要使用业务实现类的接口作为参数(因为在后面代理方法时需要根据接口内的方法名进行调用)。如果业务实现类没有实现接口,就无法使用JDK动态代理了。并且
关键接口:java.lang.reflect.InvocationHandler
关键类:java.lang.reflect.Proxy
和 java.lang.reflect.Method
代码示例如下:
// JDK代理类
public class JDKProxy implements InvocationHandler {
private Object proxyObject;
public JDKProxy(Object proxyObject) {
this.proxyObject = proxyObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// do something
Object ret = null; //方法返回值
System.out.println("JDK Proxy --- 调用前 --- 调用方法是:"+method.getName() + "---参数是:"+ GsonUtil.toJson(args));
ret = method.invoke(proxyObject, args); //调用invoke方法
System.out.println("JDK Proxy --- 调用后");
return ret;
}
}
// 获取代理对象工厂方法
public class ProxyFactory {
/**
* 工厂方法,获取JDKProxy对象
* @param proxyObject
* @return
*/
public static Object createJDKProxyInstance(Object proxyObject){
JDKProxy jdkProxy = new JDKProxy(proxyObject);
return Proxy.newProxyInstance(proxyObject.getClass().getClassLoader(), proxyObject.getClass().getInterfaces(), jdkProxy);
}
}
// 测试代码
DemoManager jdkDemo = (DemoManager) ProxyFactory.createJDKProxyInstance(new DemoManagerImpl());
jdkDemo.add(1,"Antony");
jdkDemo.delete(2);
// 运行结果
JDK Proxy --- 调用前 --- 调用方法是:add---参数是:[1,"Antony"]
DemoManager add--- 调用啦---id=1name=Antony
JDK Proxy --- 调用后
cglib实现动态代理
cglib动态代理的原理是继承需要代理的类,生成的代理类是目标类的子类。用cglib生成的代理类重写了父类的各个方法,拦截器中的intercept方法内容正好就是代理类中的方法体。
cglib是一个代码生成的类库,低层是使用了ASM提供的字节码操控框架。通过在运行时动态地生成某个类的子类。cglib是采用继承的方式实现的代理,所以被声明为final的类无法代理。
关键接口:org.springframework.cglib.proxy.MethodInterceptor
关键类:org.springframework.cglib.proxy.Enhancer
和 org.springframework.cglib.proxy.MethodProxy
代码示例如下:
// cglib 代理对象的类
public class CGlibProxy implements MethodInterceptor {
private Object proxyObject;
public CGlibProxy(Object proxyObject) {
this.proxyObject = proxyObject;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("CGLib Proxy --- 调用前 --- 调用方法是:"+method.getName() + "---参数是:"+ GsonUtil.toJson(objects));
Object ret = method.invoke(proxyObject, objects);
System.out.println("CGLib Proxy --- 调用后");
return ret;
}
}
// 工厂方法,获取代理对象
public class ProxyFactory {
/**
* 工厂方法,获取cglibProxy对象
* @param proxyObject
* @return
*/
public static Object createCGlibProxyInstance(Object proxyObject){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(proxyObject.getClass());
enhancer.setCallback(new CGlibProxy(proxyObject));
return enhancer.create();
}
}
// 测试代码
DemoManager cgDemo = (DemoManager) ProxyFactory.createCGlibProxyInstance(new DemoManagerImpl());
cgDemo.add(1,"Antony");
cgDemo.delete(2);
// 运行结果
CGLib Proxy --- 调用前 --- 调用方法是:add---参数是:[1,"Antony"]
DemoManager add--- 调用啦---id=1name=Antony
CGLib Proxy --- 调用后
两者的区别
JDK 使用继承接口的方式生成代理类。(实现接口,管理代理实例)
cglib 使用继承目标类的方式生成代理类。(生成目标类的子类,重写方法)
JDK只能对继承了接口的类代理。
cglib 除了 final 类都可以代理。
JDK代理类生成快,但是运行效率较cglib代理差。
cglib代理类生成慢,但是运行效率较JDK代理快。
Spring如何选择这两种代理
- 目标对象实现了接口,默认使用JDK代理。
- 目标对象实现了接口,可以选择强制使用 cglib 代理。
- 目标对象没有实现接口,只能选择使用 cglib 代理。
为什么不全部使用cglib
cglib创建代理类的速度较慢,但是创建后运行的速度则很快。而JDK代理正好相反。如果在运行时全部使用cglib代理,则系统性能会显著下降。所以一般建议在系统初始化的时候使用cglib方式创建代理,放入Spring的ApplicationContext中以备后用。
SpringAOP 和 AspectJ的区别
Spring AOP | AspectJ |
---|---|
在纯 Java 中实现 | 使用 Java 编程语言的扩展实现 |
不需要单独的编译过程 | 除非设置 LTW,否则需要 AspectJ 编译器 (ajc) |
只能使用运行时织入 | 运行时织入不可用。支持编译时、编译后和加载时织入 |
功能不强-仅支持方法级编织 | 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等......。 |
只能在由 Spring 容器管理的 bean 上实现 | 可以在所有域对象上实现 |
仅支持方法执行切入点 | 支持所有切入点 |
代理是由目标对象创建的, 并且切面应用在这些代理上 | 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入 |
比 AspectJ 慢多了 | 更好的性能 |
易于学习和应用 | 相对于 Spring AOP 来说更复杂 |
(如果有什么错误或者建议,欢迎留言指出)
(本文内容是对各个知识点的转载整理,用于个人技术沉淀,以及大家学习交流用)
参考资料:
关于SpringAOP你该知晓的一切
AOP低层实现——JDK和CGLIB的动态代理
基于SpringAOP的 JDK动态代理和CGLIB动态代理
SpringAOP的两种代理方式:JDK动态代理和CGLIB动态代理