Java中的AOP(原理)

这是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), 红色矩形代表用户关注的业务逻辑


Java中的AOP(原理)_第1张图片
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();
    }

以下是输出结果


Java中的AOP(原理)_第2张图片

相信这样子的例子能让我们更好地理解jdk动态代理的使用机制

1.代理类和被代理类使用同样的对象引用, 如上客户端代码所示的serviceproxy(统一性), 因此我们可以神不知鬼不觉地使用我们的真实业务类, 而无需关注在它周围的切面逻辑(独立性), 这样子做的最大好处是, 我们在实际开发过程中, 最先实现基础功能, 然后使用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中静态代理和动态代理框架的主要运行机制. 笔者没有介绍设计模式中的代理模式, 感兴趣的同学可以去看看.

你可能感兴趣的:(Java中的AOP(原理))