AOP理解及底层原理

AOP 基础概念

一、概述

AOP (Aspect Oriented Programming)

银行系统的简易取款流程如图:

将方框里的流程合为一个,另外系统还会有一个查询余额流程,如图:

这两个业务有一个共同的验证流程,如图:

为什么会有面向切面编程(AOP)?Java 是面向对象程序设计(OOP)的,但它有一些弊端。AOP 的真正目的是,业务开发,事先只需考虑主流程,而忽略不重要的流程。AOP 可以把这个验证用户的代码提取出来,不放到主流程里去。比如当要为多个不具有担当关系的工具引入一个公共举动,例如日志、权限验证、事务等功能时,只能在每个工具里引用公共举动。如果这样做不便于维护,并且还会有大量相同的代码。AOP 面向方面编程基于 IOC,是对 OOP 的有益补充。AOP 是一种思想,不同的厂商或企业可能有不同的实现方式,为了更好的应用 AOP 技术,技术专家们成立了 AOP 联盟来探讨 AOP 的标准化。AOP 联盟定义的 AOP 体系结构把与 AOP 相关的概念大致分为由高到低、从使用到实现的三层关系, AOP 联盟定义的 AOP 体系结构如下图:

在 AOP 联盟定义的 AOP 体系结构下有很多的实现者,例如:AspectJ、JBoss AOP、AspectWerkz、Spring AOP 等。

可以把上面业务方框当块板子,这块板子插入一些控制流程,这些控制流程就可以当成是 AOP 中的一个切面。所以 AOP 的本质是在一系列纵向的业务流程中,把那些相同的子流程提取成一个横向的面,如图二,AOP 相当于把相同的地方连一条横线。

图一
图二

验证用户这个控制流程就成了一个条线,也可以理解成一个切面。这里的切面只插了两三个流程,如果其它流程也需要这个子流程,也可以插到其它地方去。

要想理解 Spring AOP,先理解代理模式。
代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。比如 A 对象要做一件事情,在没有代理前,自己来做;在对 A 代理后,由 A 的代理类 B 来做。代理其实是在原实例前后加了一层处理,这也是 AOP 的初级轮廓。

二、代理模式的原理及实践

代理模式又分为静态代理、动态代理。

1️⃣静态代理原理及实践
说白了,就是在程序运行前就已经存在代理类的字节码文件,代理类和原始类的关系在运行前就已经确定。代码如下:

接口:

public interface IUserDao {
    void save();
    void find();
}

目标对象:

class UserDao implements IUserDao{
    @Override
    public void save() {
        System.out.println("模拟:保存用户!");
    }
    @Override
    public void find() {
        System.out.println("模拟:查询用户");
    }
}

静态代理特点:
①目标对象必须要实现接口
②代理对象,要实现与目标对象一样的接口

class UserDaoProxy implements IUserDao{
    // 代理对象,需要维护一个目标对象
    private IUserDao target = new UserDao();
    @Override
    public void save() {
        System.out.println("代理操作: 开启事务...");
        target.save(); // 执行目标对象的方法
        System.out.println("代理操作:提交事务...");
    }
    @Override
    public void find() {
        target.find();
    }
}

静态代理保证了业务类只需要关注逻辑本身,代理对象的一个接口只需要服务于一种类型的对象。如果要代理的方法很多,势必要为每一种方法都进行代理。再者,如果增加一个方法,除了实现类需要实现这个方法外,所有的代理类也要实现此方法,增加了代码的维护成本。那么要如何解决呢?答案是使用动态代理。

2️⃣动态代理原理及实践
动态代理类的源码是在程序运行期间,通过 JVM 反射等机制动态生成。代理类和委托类的关系是运行时才确定的。实例如下:

接口:

public interface IUserDao {
    void save();
    void find();
}

目标对象:

class UserDao implements IUserDao{
    @Override
    public void save() {
        System.out.println("模拟: 保存用户!");
    }
    @Override
    public void find() {
        System.out.println("查询");
    }
}

动态代理:代理工厂,给多个目标对象生成代理对象

class ProxyFactory {
    // 接收一个目标对象
    private Object target;
    public ProxyFactory(Object target) {
        this.target = target;
    }
    // 返回对目标对象(target)代理后的对象(proxy)
    public Object getProxyInstance() {
        Object proxy = Proxy.newProxyInstance(
                               // 目标对象使用的类加载器
                target.getClass().getClassLoader(),
                               // 目标对象实现的所有接口 
                target.getClass().getInterfaces(), 
                              // 执行代理对象方法时候触发
                new InvocationHandler() { 
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args)
                            throws Throwable {
                        // 获取当前执行的方法的方法名
                        String methodName = method.getName();
                        // 方法返回值
                        Object result = null;
                        if ("find".equals(methodName)) {
                            // 直接调用目标对象方法
                            result = method.invoke(target, args);
                        } else {
                            System.out.println("开启事务...");
                            // 执行目标对象方法
                            result = method.invoke(target, args);
                            System.out.println("提交事务...");
                        }
                        return result;
                    }
                }
                );
        return proxy;
    }
}
IUserDao proxy = (IUserDao)new ProxyFactory(target).getProxyInstance();

其实是 JDK 动态生成了一个类去实现接口,隐藏了这个过程:

class $jdkProxy implements IUserDao{}

使用 JDK 生成动态代理的前提是目标类必须有实现的接口。如果某个类没有实现接口,就不能使用 JDK 动态代理。CGLIB 代理可以解决这个问题。CGLIB 是以动态生成的子类继承目标的方式实现,在运行期动态的在内存中构建一个子类。CGLIB 使用的前提是目标类不能是 final 修饰的。因为 final 修饰的类不能被继承。

现在,看看 AOP 的定义:面向切面编程,核心原理是使用动态代理模式在方法执行前后或出现异常时加入相关逻辑。

通过定义和前面代码可以发现3点:

  1. AOP 是基于动态代理模式。
  2. AOP 是方法级别的。
  3. AOP 可以分离业务代码和关注点代码(重复代码)。在执行业务代码时,动态的注入关注点代码。切面就是关注点代码形成的类。

三、Spring AOP原理及实战

Spring 框架把 JDK 代理和 CGLIB 代理两种动态代理在底层都集成了进去,开发者无需自己去实现动态生成代理。那么,Spring 是如何生成代理对象的?

  1. 创建容器对象的时候,根据切入点表达式拦截的类,生成代理对象。
  2. 如果目标类有实现接口,使用 JDK 代理;如果目标类没有实现接口,且 class 不为 final 修饰的,则使用 CGLIB 代理;否则不能进行 Spring AOP 编程。然后从容器获取代理后的对象,在运行期植入“切面”类的方法。通过查看 Spring 源码,在 DefaultAopProxyFactory 类中,找到这样一段话。
DefaultAopProxyFactory

手动实现 Spring 的 AOP:
接口:

public interface IUserDao {
    void save();
}

用于测试 CGLIB 动态代理:

class OrderDao {
    public void save() {
        //int i =1/0; 用于测试异常通知
        System.out.println("保存订单...");
    }
}

用于测试 JDK 动态代理:

class UserDao implements IUserDao {
    public void save() {
        //int i =1/0; 用于测试异常通知
        System.out.println("保存用户...");
    }
}

切面类:

class TransactionAop {
    public void beginTransaction() {
        System.out.println("[前置通知] 开启事务..");
    }
    public void commit() {
        System.out.println("[后置通知] 提交事务..");
    }
    public void afterReturing() {
        System.out.println("[返回后通知]");
    }
    public void afterThrowing() {
        System.out.println("[异常通知]");
    }
    public void arroud(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[环绕前:]");
        pjp.proceed(); // 执行目标方法
        System.out.println("[环绕后:]");
    }
}

Spring 的 XML 配置文件:

<?xml version="1.0" encoding="UTF-8"?>

<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"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
                http://www.springframework.org/schema/aop
                http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- dao实例加入容器 -->
    <bean id="userDao" class="test.spring_aop_anno.UserDao"></bean>
    <!-- dao实例加入容器 -->
    <bean id="orderDao" class="test.spring_aop_anno.OrderDao"></bean>
    <!-- 实例化切面类 -->
    <bean id="transactionAop" class="test.spring_aop_anno.TransactionAop"></bean>

    <!-- Aop相关配置 -->
    <aop:config>
        <!-- 切入点表达式定义 -->
        <aop:pointcut expression="execution(* test.spring_aop_anno.*Dao.*(..))"
            id="transactionPointcut" />

        <!-- 切面配置 -->
        <aop:aspect ref="transactionAop">

            <!-- 【环绕通知】 -->
            <aop:around method="arroud" pointcut-ref="transactionPointcut" />
            <!-- 【前置通知】 在目标方法之前执行 -->
            <aop:before method="beginTransaction" pointcut-ref="transactionPointcut"/>
            <!-- 【后置通知】 -->
            <aop:after method="commit" pointcut-ref="transactionPointcut" />
            <!-- 【返回后通知】 -->
            <aop:after-returning method="afterReturing"
                pointcut-ref="transactionPointcut" />
            <!-- 异常通知 -->
            <aop:after-throwing method="afterThrowing"
                pointcut-ref="transactionPointcut" />

        </aop:aspect>
    </aop:config>
</beans>

四、Spring AOP 应用场景

  1. Spring 声明式事务管理配置;
  2. Controller 层的参数校验;
  3. 使用 Spring AOP 实现 MySQL 数据库读写分离案例分析;
  4. 在执行方法前,判断是否具有权限,Authentication 权限检查;
  5. 对部分函数的调用进行日志记录:监控部分重要函数,若抛出指定的异常,可以以短信或邮件方式通知相关人员;
  6. 信息过滤,页面转发等等功能。
    .
    .
    .
    Caching 缓存
    Context passing 内容传递
    Error handling 错误处理
    Lazy loading 延迟加载
    Debugging  调试
    logging, tracing, profiling and monitoring 日志记录,跟踪,优化,校准
    Performance optimization 性能优化,效率检查
    Persistence  持久化
    Resource pooling 资源池
    Synchronization 同步
    Transactions 事务管理
    另外 Filter 的实现和 struts2 的拦截器的实现都是 AOP 思想的体现。

-------------------------------------------------------------

Spring 不尝试提供最为完善的 AOP 实现,它更侧重于提供一种和 Spring IOC 容器整个的 AOP 实现,用于解决实际的问题,在 Spring 中无缝的整合了 Spring AOP、Spring IOC 和 AspectJ。在使用 Spring AOP 的时候只是简单的配置一下(通过 XML 或注解进行配置),Spring AOP 在内部 ProxyFactory 来创建代理对象,然后调用目标方法。

动态代理或者设计模式很重要!Spring AOP 用到了动态代理,Spring 事务管理用到了动态代理,MyBatis 数据库连接池用到了动态代理,MyBatis 创建 Mapper 用到了动态代理等等!

  1. AOP 面向方面编程基于 IOC,是对 OOP 的有益补充。

  2. AOP 利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,比如日志记录,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

  3. AOP 代表的是一个横向的关系,将“对象”比作一个空心的圆柱体,其中封装的是对象的属性和行为;则面向方面编程的方法,就是将这个圆柱体以切面形式剖开,选择性的提供业务逻辑。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天工的妙手将这些剖开的切面复原,不留痕迹,但完成了效果。

  4. 实现 AOP 的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

  5. Spring 实现 AOP:JDK 动态代理和 CGLIB 代理。JDK 动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理;其核心的两个类是 InvocationHandler 和 Proxy。CGLIB 代理:实现原理类似于 JDK 动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB 是高效的代码生成包,底层是依靠 ASM(开源的 Java 字节码编辑类库)操作字节码实现的,性能比 JDK 强;需要引入包 asm.jar 和 cglib.jar。使用 AspectJ 注入式切面和 @AspectJ 注解驱动的切面实际上底层也是通过动态代理实现的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,682评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,277评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,083评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,763评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,785评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,624评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,358评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,261评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,722评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,900评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,030评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,737评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,360评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,941评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,057评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,237评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,976评论 2 355

推荐阅读更多精彩内容