这是aop体系化学习过程中的第一篇-原理篇, 偏向aop实现中运用到的原理.接下来将陆续发布应用篇, 整理spring中的aop应用. 最后发布实现篇, 尝试自己手写一个动态代理框架, 实现aop. 文章如有不足之处欢迎讨论哦, 一起进步.
大纲
- AOP入门介绍
- AOP分类
- 静态代理
- 动态代理
- AOP实现
- AspectJ
- JDK动态代理
- CGLIB
- JDK动态代理和CGLIB的区别
- 三种aop框架对比
- 更好地理解静态代理和动态代理的区别
AOP入门介绍
aop(aspect oriented programming 面向切面编程)是一种编程思想
典型的应用场景如下: 比如我们写一个银行存款的业务
1.aop 记录日志: a准备向b汇款(AOP类)
2.把a的100块钱存入b的账户(业务类)
3.aop 记录日志:a向b汇款成功/失败(AOP类)
步骤1,3就是aop思想的实现, 我们在一个类中写好汇款业务(步骤2). 在另一个类中运用aop, 在汇款前后记录日志(步骤1,3), 完全不干预存款业务的代码逻辑.
aop还有更多的应用场景, 例如请求到达业务类之前, 判断请求是否合理, 用户是否有权限执行这个请求, 在请求离开业务类, 发送到服务器之前, 使用aop统一编码数据格式, 统一加密返回信息. 数据缓存等等...
对于真正执行对应业务的类来说, 它只负责它应该做的事情, 无需知道请求到来之前已经经过了一系列筛选流程, 所以业务类是独立的.
下图中, 绿色矩形代表切面逻辑(aop), 红色矩形代表用户关注的业务逻辑

在实际开发中, 可能基础功能才是首要目的, 所以基础功能应该首先开发完成. 在这之后开发人员再基于业务逻辑开发切面逻辑, 例如加密解密, 过滤器, 权限验证, 日志记录等等(不过目前的开发应用中加密解密使用的比较多的是拦截过滤器). 有人可能会问, 为什么要把这些功能分开? 首先是为了解耦, 将各个业务逻辑分离开有利于开发人员定位错误, 同时也符合了面向对象开发中单一职责的原则.
AOP分类
根据静态编译时和运行时两种环境, aop可以分为静态代理和动态代理
静态代理主要涉及AspectJ, 动态代理主要涉及Spring AOP, CGLIB.
静态代理与动态代理的区分点主要在于: aop框架(spring aop, aspectj ...)能否在程序运行时动态地创建aop代理.(后面详细解释这句话)
AOP实现
aop是一种编程思想, 弥补了oop的不足. 在java中, 有三个主要的框架实现了aop思想
1. AspectJ
AspectJ主要作用于编译时增强, 也称之为静态代理, 我们在写完一段独立的业务方法saveData()时, 可以使用aspectJ将切面逻辑织入到saveData()中. 比如日志记录.
在使用aspectJ编译代码之后, 我们的class文件中会多出一段代码, 这段代码是aspectJ在编译时增加的aop代码. AspectJ的这种做法可以被称为静态代理
示例如下
public class DataService {
public void saveData() {
//...
System.out.println(" save data...");
}
public static void main(String[] args) {
DataService service = new DataService ();
service.saveData();
}
}
以上是我们主要关注的逻辑
以下是使用aspect进行切面逻辑的编写
public aspect DataAspect {
void around():call(void DataService.saveData()){
System.out.println("before saving data ...");
//执行saveData方法
proceed();
System.out.println("after saving data ...");
}
}
在编译上面的DataService之后
class文件发生了变化
public class DataService {
public void saveData() {
//...
System.out.println(" save data...");
}
public static void main(String[] args) {
DataService service = new DataService ();
//这里是发生变化的代码
saveData_aroundBody1$advice(service, DataAspect.aspectOf(), (AroundClosure)null);
}
//这里是aspectJ在编译时添加的代码
public void ajc$around$com_qcq_aop_DataAspect$1$f54fe983(AroundClosure ajc$aroundClosure) {
System.out.println("before saving data ...");
ajc$around$com_qcq_aop_DataAspect$1$f54fe983proceed(ajc$aroundClosure);
System.out.println("after saving data ...");
}
}
从以上示例我们可以看出:
Aspect在编译期, 为被代理方法织入我们在aspect中定义好的切面逻辑, 以添加字节码的方式(强行添加代码)
2. JDK动态代理
jdk动态代理使用jdk自带的反射机制来完成aop的动态代理, 使用jdk自带的动态代理有如下要求:
1.被代理类(我们的业务类)需要实现统一接口
2.代理类要实现reflect包里面的接口InvocationHandler
3.通过jdkProxy提供的静态方法newProxyInstance(xxx)来动态创建代理类
下面是具体例子
下面定义了一个统一被代理类接口
public interface IService {
void save();
}
下面是接口实现类
public class UserService implements IService{
@Override
public void save() {
System.out.println("save a user ...");
}
}
下面是InvocationHandler的实现
public class MyHandler implements InvocationHandler {
private Object target;
MyHandler(Object target) {
this.target = target;
}
//在这里实现我们的切面逻辑
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before ...");
Object result = method.invoke(target, args);
System.out.println("after ...");
return result;
}
}
下面是客户端代码
public static void main(String[] args) {
//创建被代理类
IService service = new UserService();
//新建代理类,将被代理类注册到里面
MyHandler aop = new MyHandler(service);
//Proxy为MyHandler动态创建一个符合某一接口的代理实例
//第一个参数:获取被代理类的class loader
//第二个参数:获取被代理类实现的接口, 由此可见被代理类需要实现统一的接口
//第三个参数:InvocationHandler的实例, 也就是我们做切面逻辑的类
IService proxy = (IService) Proxy
.newProxyInstance(service.getClass().getClassLoader(),
service.getClass().getInterfaces(),
aop);
//调用代理方法
proxy.save();
}
以下是输出结果

相信这样子的例子能让我们更好地理解jdk动态代理的使用机制
1.代理类和被代理类使用同样的对象引用, 如上客户端代码所示的
service和proxy(统一性), 因此我们可以神不知鬼不觉地使用我们的真实业务类, 而无需关注在它周围的切面逻辑(独立性), 这样子做的最大好处是, 我们在实际开发过程中, 最先实现基础功能, 然后使用aop编程实现我们的切面逻辑, 例如基于基础功能的日志记录, 缓存管理, 权限验证等等...
2.如果我们调用service里面的方法, 那么方法前后不会有切面逻辑的实现, 如果调用proxy里面的方法, 由于proxy是jdk动态生成的代理类, 与service属于同一个类, 但是里面的方法被调用时, jdk自动为我们实现切面逻辑(动态性)
3.被代理类需要实现统一代理接口, 如上IService(局限性)
3. CGLIB
CGLIB的全称: Code Generation Library
翻译过来就是代码生产库, 与AspectJ的机制不一样, CGLIB可以在代码运行时动态生成某个类的子类, 因此使用CGLIB时, 我们不需要像jdk动态代理一样建立统一代理接口.
先来看一看CGLIB的使用机制
1.定义被代理类(业务逻辑)
public class UserService{
public void save() {
System.out.println("save a user ...");
}
}
2.实现MethodInterceptor接口(切面逻辑)
public class UserProxy implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//方法执行前的逻辑
System.out.println("before ...");
//参数o是被代理对象, invokeSuper方法调用父类方法, 在这前后我们可以实现自定义切面逻辑
Object result = methodProxy.invokeSuper(o, objects);
//方法执行后的逻辑
System.out.println("after ...");
return result;
}
}
3.获取被代理类Factory工厂, 该工厂专门生产加入了切面逻辑的被代理类
public class UserServiceFactory {
public static UserService getInstance(UserProxy proxy) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
//设置回调类,强化类调用回调类中的intercept方法来执行我们定义好的切面逻辑
enhancer.setCallback(proxy);
//返回一个添加了切面逻辑的增强类
return (UserService) enhancer.create();
}
}
Enhancer是一个字节码增强器
4.客户端调用
public static void main(String[] args) {
UserProxy proxy=new UserProxy();
//通过工厂方法模式获取增强类
UserService service = UserServiceFactory.getInstance(proxy);
service.save();
}
执行结果如下

4.JDK动态代理和CGLIB的区别
CGLIB使用了底层的字节码技术, 通过字节码处理框架ASM, 在程序运行时为一个类创建子类, 在子类中重写父类的方法, 为父类方法加入切面逻辑(无法代理final修饰的方法). CGLIB创建代理类的代价相对大, 但是运行切面逻辑的速度较快, 适合单例模式.
JDK动态代理使用jdk原生提供的反射机制, 位于sun.reflect包中. 代理类生成过程较快, 但切面逻辑运行速度没有CGLIB动态代理快. 而且被代理类必须基于统一接口, 存在局限性, 实际中并不常用这种方式.
三种aop框架对比
| 框架 | 代理机制 | 作用时间范围 | 作用原理 | 优点 | 缺点 | 使用频率 |
|---|---|---|---|---|---|---|
| AspectJ | 静态代理 | 编译期 | 编译期嵌入代码 | 运行速度最快 | 扩展性差, 无法在运行时创建新代理 | 不常用 |
| JDK动态代理 | 动态代理 | 运行时 | 被代理类加载进jvm之后, 根据声明的统一接口动态生成代理类, 同时将切面逻辑织入代理类, 也称为java的反射机制 | 代理类生成速度快 | 1.需要声明统一代理接口 , 扩展性相对CGLIB差2.相对于直接调用来说, 切面逻辑运行速度慢 3.使用大量反射如果不释放代理对象的引用, 会造成GC负荷大 | 不常用 |
| CGLIB | 动态代理 | 运行时 | 通过字节码框架ASM动态生成子类, 重写父类方法, 同时嵌入切面逻辑 | 1.切面逻辑运行速度快 2.被代理类无需实现统一接口 3.扩展性强 | 1.代理类生成代价高 2.文档少, 难以深入学习 3.使用大量反射如果不释放代理对象的引用, 会造成GC负荷大 | 常用 |
更好地理解静态代理和动态代理的区别
从AspectJ的运行机制中我们可以看出, 我们并没有真正地获取到一个基于被代理对象的代理类, 而是写完切面逻辑之后, 通过AspectJ的编译器, 在程序编译阶段在业务逻辑方法前后添加了相应的字节码, 从而实现我们的aop. 使用AspectJ框架, 并不能在程序运行时进行代理操作, 因为没有代理类, 而且代码已经被写死在业务逻辑中, 谈何动态呢? 因此AspectJ属于静态代理.
jdk动态代理和CGLIB就属于动态代理了
jdk获取代理对象的代码
IService proxy = (IService) Proxy
.newProxyInstance(service.getClass().getClassLoader(),
service.getClass().getInterfaces(),
aop);
CGLIB获取增加了切面逻辑的对象(也可称为代理对象)
UserProxy proxy=new UserProxy();
//通过工厂方法模式获取增强类
UserService service = UserServiceFactory.getInstance(proxy);
我们可以发现: 在编译完成后程序进入运行时状态时, 代理对象还存在于java堆中, 这些代理对象引用可能被GC, 但是我们可以再通过Proxy, 或者工厂来继续生成代理对象, 因此代理类可以在运行时供我们再次使用, 在请求转发到达业务逻辑类时, 我们可以动态创建代理, 代理对象帮我们实现切面逻辑, 请求结束时, 释放代理对象的引用, 反复这个流程, 实现动态性. 而AspectJ编译完形成class文件之后, 并没有所谓的动态代理对象在java堆中, 只是静态的, 不可动态创建的增加了切面逻辑的对象.
动态代理模式并没有那么难以理解, 以上便是java中静态代理和动态代理框架的主要运行机制. 笔者没有介绍设计模式中的代理模式, 感兴趣的同学可以去看看.