Spring之AOP

    科技2022-08-01  95

    一、AOP概述

    1、什么是AOP

    OOP(Object-Oriented Programming),面向对象编程。

    AOP(Aspect-Oriented Programming),面向切面编程。

    在AOP中,“切面”是最为重要的一个概念。能否理解“切面”的概念,决定了是否能熟练掌握AOP技术。

    2、生活中的“面向切面”案例
    1)案例一 “丰巢”快递柜

    “丰巢”智能快递柜起源于2015年,是一种面向所有快递公司、电商物流使用的24小时自助开放平台。最初的出现是为了提供快递行业最后一公里方案服务。

    整个物品的寄送过程大致如下:

    【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】 【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】 【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】 【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】 【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】 【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】

    上述流程中,前几个步骤几乎都是程式化进行。唯独最后一个环节送货上门最容易出现问题。例如,小区禁止快递员进入,收货人不在家,收货人电话打不通……并且,这种现象并不是个例,而是所有快递公司以及所有街道的快递员都会面临这些问题。

    而“丰巢”很好地解决了这个问题。在小区门口设立智能快递柜,并面向所有快递公司开放。顺丰、圆通、韵达、中通等所有快递都可以存放到丰巢中,而用户可以统一地到丰巢中取自己的快件。

    不同的快递公司有不同的业务流程。例如,顺丰在长途运输方式上选择空运,而其他公司选择陆运。亦或,从同一个地址寄件发往同一个目的地,可能顺丰会转运三次,圆通会转运四次,韵达会转运两次……但是,丰巢并不关心这些。它所关注的,只是所有快递公司都会面临的问题——最后一公里,送货上门。

    这最后一公里,就相当于是整个快递行业的一个切面。而丰巢,正是针对这一切面存在的问题,所提出的解决方案。

    生活中的面向切面,我们可以理解为用来解决不同领域或相似领域在某一环节存在的相同问题。

    2)银行排号系统

    张三到工行办理开户手续:

    前往工行-> 排队-> 复印身份证-> 填表-> 柜台办理-> 签字-> 离开银行

    李四到农行办理存款:

    前往农行-> 排队-> 提供卡和钱-> 柜台存入-> 回执单签字-> 离开银行

    王五到建行办理预留手机号变更:

    前往建行-> 排队-> 填表-> 柜台办理-> 验证码-> 确认-> 离开银行

    无论到哪个银行,办理何种业务,都要经过排队这一步骤。在早期的银行中,人们必须站成一字长队,通过队列中的位置来决定办理业务的次序。那么这样的方式给人们带来了极大的不便。排队是一件很枯燥的事情,大家站久了都会腰酸腿疼。而且排队期间又不能离队,假如某人辛辛苦苦排了一个小时队,突然内急,那么回来之后又要站到队尾重新排……

    针对这样的情况,“银行排号”类系统便出现在人们的视线中。每个人到银行先取号纸,然后可以在银行设立的等待座区歇息等候,等着系统广播依次叫号即可。

    在这个案例中,“排队”这件事就好比我们从所有银行业务中抽离出来的一个切面。每家银行办理业务的流程略有区别,顾客到银行需要办理的业务也因人而异。但是整个流程中,“排队”这项业务是共同的。那么我们就可以把它抽离出来,当作一个“切面”而单独进行处理。

    3)食堂、快餐、外卖、菜鸟驿站、自动售货机……

    A公司员工-> 上班-> 讲课-> 吃饭-> 买饮料-> 回办公室-> 继续上班

    B公司员工 -> 上班-> 研究销售机会-> 吃饭 -> 买饮料 -> 回办公室 ->上班

    二、程序中的“面向切面”

    程序中也存在很多的“切面”问题,指的是与完整的一条业务逻辑执行流程无关。

    问题只是出现在多条相似流程的同一个环节中。

    例如,传统的三层架构中,我们需要在Service层每个方法开始执行时和执行结束时进行日志记录。

    浏览器 >> 登录 >> LoginServlet >> CustomerService >> CustomerDao >> 数据库

    浏览器 >> 注册 >> RegisterServlet >> CustomerService >> CustomerDao >> 数据库

    浏览器 >> 查询书籍 >> ViewBookServlet >> BookService >> BookDao >> 数据库

    想要实现Service中植入日志记录固然非常简单,直接在每个service方法的开始位置和结束位置调用日志模块即可。但是这样的做法会带来很多问题:代码大量重复、系统需求和业务需求的耦合等……

    三、为什么要使用AOP

    1、AOP是OOP思想的补充和完善

    传统的OOP(面向对象编程)编程在解决实际问题的时候,仍存在一些不足和难以处理的方面。

    而AOP正是为了解决这些问题而产生的。

    2、AOP能做什么

    在OOP中,正是因为分散在各处且非业务相关的代码(横切代码)的存在,使得模块复用难度增加。

    AOP则将封装好的对象剖开,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块,这个模块被命名为“切面”(Aspect)。切面将那些与业务无关,却被业务模块共同调用的逻辑提取并封装起来,减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

    四、Spring AOP和AOP的关系

    注意:AOP只是一种设计思想,并不是某种特定的技术,更不是Spring专属的技术。

    针对AOP思想,许多公司都开发出自己的AOP框架,其实现方式也是不同的。

    例如:AspectJ、JBoss AOP、Nanning Aspects、Spring AOP……

    Spring AOP只是其中之一,也是最优秀的一款。

    五、代理模式

    学习Spring AOP之前,要先了解代理模式。因为Spring中的AOP技术,就是通过动态代理来实现的。

    1、生活中的代理案例

    请明星唱歌、跳舞、拍戏,找经纪人谈价钱

    通过中介租房

    找律师打官司

    通过代售点买火车票、飞机票

    代理思想中的原则:

    目标对象被“隐藏”

    代理模式就是要把委托类对象的控制权隐藏起来,改为访问代理类对象。

    代理者和本尊具有相同的功能

    刘德华技能:唱歌、跳舞、拍戏

    刘德华的经纪人:接唱歌的活儿、接跳舞的活儿、接拍戏的活儿

    代理者只做一些预先处理和善后工作,但是真正的业务还是要靠原对象来完成。

    我们通过经纪人请刘德华演出唱歌,但是歌还是要由刘德华本人来唱的。

    我们通过中介租房,但是房屋最终还是要由我们自己来住的。

    我们委托律师帮忙打官司,庭审还是要自己出庭的。

    如果官司打输了,坐牢也是要自己去坐牢的,而不是律师帮你坐牢。

    2、程序中的代理模式

    代理模式是OOAD设计模式之一,能够让我们在不修改原有代码的基础上,扩展程序的功能。

    例如,某个类需要在现有基础上扩展一些功能,但是又不方便把代码直接写在自己类中,就可以找一个类去做它的代理类。

    让这个代理类对象去完成需要扩展的功能,而核心功能仍依赖于原类对象实现。

    代理模式通常的做法是,给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

    需要扩展功能的类:委托类——委托别人为自己做事的人——刘德华

    委托类对象也称[目标对象]target

    帮助委托类扩展功能的类:代理类——代理别人处理事情的人——刘德华的经纪人

    代理类对象也称[代理对象]proxy
    3、代理模式分类

    静态代理:在程序运行之前,代理类的.class文件就已经存在。

    动态代理:在程序运行时,代理类是运用了反射技术或字节码技术动态创建而成的。

    静态代理需要程序员手动地为每一个委托类编写对应的代理类代码,仅仅为了方便理解动态代理,不可能在生产中使用。

    4、静态代理

    业务逻辑层接口:

    public interface CustomerService { // 用户登录方法 public void login(String username, String password); // 用户注册方法 public void register(String username, String password, String gender, int age); }

    业务逻辑层实现类:

    public class CustomerServiceImpl implements CustomerService { @Override public void login(String username, String password) { // 处理登录业务逻辑的代码 System.out.println("处理用户登录的代码..."); } @Override public void register(String username, String password, String gender, int age) { // 处理注册业务逻辑的代码 System.out.println("处理用户注册的代码..."); } }

    日志类:

    public class Logger { public void info(String msg) { System.out.println("[日志输出]" + msg); } }

    业务逻辑层代理类:

    public class CustomerServiceProxy implements CustomerService { // 目标对象 private CustomerService target; // 日志对象 Logger logger = new Logger(); // 目标对象通过构造器传入 public CustomerServiceProxy(CustomerService target) { this.target = target; } @Override public void login(String username, String password) { // 日志输出 logger.info("login方法被调用了..."); // 让目标对象执行它真正处理业务逻辑的方法 target.login(username, password); // 日志输出 logger.info("login方法调用结束了..."); } @Override public void register(String username,String password, String gender,int age){ // 日志输出 logger.info("register方法被调用了..."); // 让目标对象执行它真正处理业务逻辑的方法 target.register(username, password, gender, age); // 日志输出 logger.info("register方法调用结束了..."); } }

    测试类:

    public class Test { public static void main(String[] args) { // 创建目标对象 CustomerService service = new CustomerServiceImpl(); // 创建代理对象,注入目标对象 CustomerServiceProxy proxy = new CustomerServiceProxy(service); // 访问代理对象方法 proxy.login("tom", "123"); proxy.register("tom","123","男",23); } }

    上述案例为我们展示了静态代理的基本实现过程,具有如下特点:

    1)委托类对象(目标对象)被“隐藏”

    代理模式的主要方式就是要将目标对象隐藏起来,控制外界对它的直接访问。而改为访问代理对象。

    2)委托类和代理类实现相同的接口

    目的是让代理类具有和委托类相同的行为。

    刘德华:唱歌、跳舞、拍戏…

    刘德华经纪人:接唱歌的活儿、接跳舞的活儿、接拍戏的活儿…

    3)代理类中包含委托类对象(目标对象)

    代理对象只是做预先工作和善后工作,真正的功能还要靠目标对象自己实现。

    通过上述案例,我们可以发现,代理模式很大程度上降低了登录代码和日志代码的耦合。

    首先,在业务逻辑层中看不到任何日志代码的存在。如果将来日志类发生了改变,对业务逻辑层却没有任何影响。

    这样一来,降低了项目后期维护的难度,也易于进行功能扩展。而且不同分工的程序员就更专注地开发自己的组件,提升代码质量。

    但是这样的静态代理也存在一定的缺点,程序员不得不为每一个有需求的类编写一个代理类出来。不仅费时费力,也不利于修改,更是使项目变得十分臃肿。

    我们可以通过使用动态代理来解决这一问题。

    5、动态代理

    动态代理的特点是:在编译期间,不需要手动编写代理类。而是在程序运行期间动态生成一个代理类对象。

    不仅简化了编程工作,而且提高了软件系统的可扩展性,因为使用它可以生成任意类型的动态代理类。

    动态代理有两种实现方式:

    JDK动态代理(要求掌握)

    通过Java反射机制动态创建代理对象。

    CGLib代理(稍作了解)

    通过ASM字节码技术创建代理对象。

    1)JDK动态代理案例

    日志类、业务逻辑层接口、业务逻辑层实现类和静态代理案例中相同:

    class Logger、interface CustomerService、class CustomerServiceImpl

    使用JDK动态代理,首先需要提供一个InvocationHandler接口的实现类。

    这个接口的实现类,类似我们后面在AOP中要提到的“切面类”概念。

    简而言之,我们要对委托类进行扩展的功能,都在这个类中定义。

    也就是说,在静态代理案例中,我们在代理类方法中执行的日志输出代码,现在全部都要定义在这个InvocationHandler的实现类中。


    InvocationHandler实现类:

    public class MyHandler implements InvocationHandler { // 目标对象 Object target; // 日志对象 Logger logger = new Logger(); // 构造器(传入目标对象) public MyHandler(Object target) { this.target = target; } // 参数: // 1.proxy 将来要生成的代理对象 // 2.method 目标对象中的目标方法 // 3.args 调用代理方法时传入的参数列表 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 目标方法被调用前执行的操作 logger.info(method.getName() + "方法被调用了..."); // 调用目标对象的目标方法,并获取返回值 Object returnValue = method.invoke(target, args); // 目标方法调用结束之后执行的操作 logger.info(method.getName() + "方法调用结束了..."); // 将返回值返回 return returnValue; } }

    测试类:

    public class Test { public static void main(String[] args) throws Exception { // 获取委托类镜像对象 Class cls = Class.forName("com.briup.CustomerServiceImpl"); // 获取委托类的类加载器 ClassLoader loader = cls.getClassLoader(); // 获取委托类实现的所有接口 // 返回值类型是装有所有实现接口镜像对象的Class[]数组 Class[] interfaces = cls.getInterfaces(); // 创建目标对象 CustomerService target = new CustomerServiceImpl(); // 创建InvocationHandler接口的实现类 InvocationHandler h = new MyHandler(target); // 动态代理的核心方法,返回值为动态生成的代理对象。 // 参数: // 1.loader:委托类的类加载器 // 2.interfaces:委托类实现的所有接口 // 3.h:invocation实现类对象,指定代理对象要做的事情 CustomerService proxy = (CustomerService) Proxy.newProxyInstance( loader, interfaces, h); // 调用代理对象的方法 proxy.login("username", "password"); proxy.register("username", "password","男",24); } }

    这样的代码看起来十分复杂,类、接口和方法一时难以理解。

    但是这样做的好处也是显而易见的:

    可扩展性强

    如果需要扩展业务逻辑功能,那么只需要在service接口添加方法,并在实现类给出具体实现即可。

    测试代码则无需改动,执行时新添加的方法也会被代理。而在静态代理中,不仅要修改service接口和实现类,每一个受到影响的proxy类也要手动修改。

    代码复用率高

    在静态代理中,一个代理类只能服务于一个委托类。假如将来项目中有十个类需要进行日志输出,那么委托类也要编写十个。

    在动态代理中,我们编写的MyHandler(InvocationHandler接口的实现类),可以为任何一个需要代理的service服务。假设系统中所有的service都需要进行日志输出,那么也只需要编写一个MyHandler即可。

    2)CGLib代理

    JDK动态代理要求目标类实现接口,才能对其进行代理。

    CGLib的好处就是在于对于没有实现接口的类,也可以对其进行动态代理。

    CGLib采用了非常底层的ASM字节码技术,其原理是通过字节码技术为目标类创建一个子类对象,并在子类对象中拦截所有父类方法的调用。然后在方法调用前后调用后都可以加入自己想要执行的代码。

    在CGLib代理中,我们同样需要将“横切代码”定义在某个类中,这个类是MethodInterceptor接口的子类(类似JDK动态代理中InvocationHandler接口的实现类),我们可以把它称之为“拦截器类”。将来目标方法被调用之前,就会先一步被拦截,调用到拦截器中预先定义好的横切代码。


    MethodInterceptor实现类:

    public class MyInterceptor implements MethodInterceptor { // 日志对象 Logger logger = new Logger(); // 参数: // 1.proxyObj:将来生成的代理对象 // 2.method:代理对象中被调用的方法 // 3.args:调用方法传入的参数列表 // 4.mProxy:该参数可以调用父类中的方法 @Override public Object intercept(Object proxyObj, Method method, Object[] args, MethodProxy mProxy) throws Throwable { // 日志输出 logger.info(method.getName()+"方法被调用了..."); // 子类对象调用父类方法 Object returnValue = mProxy.invokeSuper(proxyObj, args); // 日志输出 logger.info(method.getName()+"方法调用结束了..."); // 将目标方法调用结果返回 return returnValue; } }

    测试类:

    public class Test { public static void main(String[] args) { // 创建拦截器对象 MethodInterceptor ic = new MyInterceptor(); // 创建Enhancer对象,用来生成某个类的子类对象 Enhancer enhancer = new Enhancer(); // 指定父类(要生成哪个类的子类) enhancer.setSuperclass(CustomerService.class); // 指定调用哪个拦截器 enhancer.setCallback(ic); // 生成子类对象 CustomerService proxy = (CustomerService) enhancer.create(); // 访问代理对象 proxy.login("tom", "123"); } }

    【面试题】1、请比较JDK动态代理和cglib?

    1)JDK动态代理所需要使用的类和接口全部来自于原生API(java.lang.reflect)

    ​ cglib属于第三方技术,需要额外引入依赖(也可以直接从spring框架中使用它)。

    2)底层的实现方式不同:

    ​ JDK动态代理的思路是使用委托类的镜像、类加载器,通过运行期间内存中的活动,动态生成一个代理对象。该代理对象和目标对象具有相同的行为(通过实现同一个接口实现)。代理对象和目标对象的关系是包含关系(一个类中维护另外一个类的引用),在设计上这种关系称之为弱耦合的聚合或组合关系。

    ​ cglib的实现思路是创建一个目标对象的子类对象,通过继承来实现和它用有相同的特性。在设计上这种关系可以看做强耦合的关系。

    3)JDK动态代理要求被代理的类必须要实现接口,而cglib不做要求,实现也行不实现也行。

    2、Spring中的AOP功能是通过哪种技术实现的?

    默认是使用JDK动态代理实现,但是可以通过配置修改,改为使用cglib。

    六、Spring中AOP的实现

    1、AOP中的基本名词和概念
    名词解释AOP面向切面编程Aspect切面/切面类,比如调用日志类、日志模块、权限验证的代码JoinPoint连接点,即被拦截的方法。在Spring AOP中连接点就是java中的方法。PointCut切入点/切点,一组连接点的集合。Advice通知/拦截器,指定切面类代码织入到切入点之前、之后…… 类似动态代理中的InvocationHandler接口的实现类、MethodInterceptor接口的实现类Advisor增强器,指定类中的哪些方法需要被拦截Target目标对象、委托类对象,被代理的对象,本尊对象Proxy代理对象Wave织入
    2、Advice通知类型

    Advice通知/拦截器用来指定将切面类中的代码织入到连接点的什么位置。

    选择不同的通知类型,可以决定像日志输出这样的操作(切面代码)是发生在执行目标方法之前,还是之后,还是前后都有……具体有下列四种通知类型:

    类型描述前置通知(Before advice)在某些连接点之前执行的通知返回后通知(After returning advice)在某些连接点正常完成后执行的通知(方法正常结束,没有异常)抛出异常后通知(After throwing advice)在某些连接点抛出异常退出时执行的通知环绕通知(Around Advice)包围一个连接点的通知。例如事务的处理,就需要这样的通知。因为事务需要在方法前开启,在方法后提交,以及方法抛出异常时候回滚。
    注:在spring中,连接点(join point)指的就是类中的方法

    在Spring中,通知是以类的形式体现的。而这个类属于哪一种通知,就要看它具体实现了哪一个接口,对应关系如下:

    通知类型实现的接口前置通知MethodBeforeAdvice返回后通知AfterReturningAdvice抛出异常后通知ThrowsAdvice环绕通知MethodInterceptor
    3、五种通知类型案例

    Spring AOP中配置通知有两种方式:

    xml配置文件<aop:config>标签配置

    xml配置文件配置通知过程较为繁琐,实际项目中多使用aop:config的方式配置通知。但是xml配置的方式的使用能够帮助理解aop:config标签的原理和大致过程。

    1)xml配置文件案例

    注意:在如下案例中同时配置了多种通知,每种通知都可以单独配置和使用。


    实体类

    Account.java

    public class Account { private int id; private String name; private double balance;// 余额 // getters&setters... }
    Dao层接口

    AccountDao.java

    public interface AccountDao { // 取款 账号减去多少钱 void withdraw(Account account, double amount); // 存款 账号加上多少钱 void deposit(Account account, double amount); }
    Dao层实现类

    AccountDaoImpl.java

    public class AccountDaoImpl implements AccountDao { @Override public void withdraw(Account account, double amount) { System.out.println("[AccountDao]" + account.getName() + "取款" + amount + "元。"); } @Override public void deposit(Account account, double amount) { System.out.println("[AccountDao]" + account.getName()+"存款" + amount + "元。"); } }
    Service层接口

    AccountService.java

    public interface AccountService { // 模拟某个业务 void bankAction(Account account); }
    Service层实现类

    AccountServiceImpl.java

    public class AccountServiceImpl implements AccountService { // Dao层对象 private AccountDao accountDao; @Override public void bankAction(Account account) { // 存取一百元 accountDao.deposit(account, 100); // 模拟异常抛出,测试异常抛出类通知 // 测试前置、后置、环绕通知时,请将异常抛出代码删除或注释 int a = 0; if (a==0) throw new RuntimeException("测试异常!"); accountDao.withdraw(account, 100); } public AccountDao getAccountDao() { return accountDao; } public void setAccountDao(AccountDao accountDao) { this.accountDao = accountDao; } }
    日志类

    Logger.java

    public class Logger { // 简单的日志输出方法 public void info(String msg) { System.out.println("[日志输出]"+msg); } }
    前置通知

    Before.java

    public class Before implements MethodBeforeAdvice { Logger logger; // 日志对象 @Override public void before(Method method, Object[] args, Object obj) throws Throwable { // 注意,这里不要手动调用method.invoke() // Spring会自动调用一次目标方法 // 我们只需要在这里调用目标方法前要做什么事情即可 // 如果在这里调用了method.invoke(),会导致目标方法被调用两次 logger.info("[前置通知]" + method.getName() + "方法开始执行了..."); // 另外,这里不需要返回值,因为执行到前置通知时, // 目标方法还未被真正调用,所以不存在返回值 } // 用于依赖注入 public void setLogger(Logger logger) { this.logger = logger; } }
    返回后通知

    AfterReturn.java

    public class AfterReturn implements AfterReturningAdvice { Logger logger; @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { // 不要手动调用method.invoke(),原因同上 logger.info("[后置通知]" + method.getName() + "方法执行结束..."); } public void setLogger(Logger logger) { this.logger = logger; } }
    环绕通知

    Around.java

    public class Around implements MethodInterceptor { Logger logger; @Override public Object invoke(MethodInvocation invocation) throws Throwable { logger.info("[环绕前通知]" + invocation.getMethod().getName() + "开始..."); // 注意: // 环绕通过中,我们在目标方法执行前、执行后都需要添加代码 // 所以需要手动调用下面方法告诉Spring目标方法什么时候执行 // 该方法类似于method.invoke() // 另外,这里要处理返回值 Object returnValue = invocation.proceed(); logger.info("[环绕前通知]" + invocation.getMethod().getName() + "结束..."); return returnValue; } public void setLogger(Logger logger) { this.logger = logger; } }
    异常通知

    Throws.java

    public class Throws implements ThrowsAdvice { Logger logger; // 注意,异常通知的处理方法比较特殊 // ThrowsAdvice本身是一个空接口,只做标识使用 // 我们需要自己在类中手动定义方法,以及抛出异常时的处理代码 // 我们自己定义的方法有如下要求: // 1.方法名必须为afterThrowing // 2.参数列表只允许有两种形式,要么1个参数,要么4个参数: // 1)afterThrowing(Exception e) // 2)afterThrowing(Method method, Object[] args, Object obj, Exception e) // 如果只需要获取抛出的异常对象,可以定义为第一种 // 如果需要做更详细的处理,可以定义为第二种 // 注意:如果定义为第二种,4个参数顺序必须是固定的 // 第一种 public void afterThrowing(Exception e) { logger.info(e.getMessage()); } /* 第二种 public void afterThrowing(Method method,Object[] args, Object obj,Exception e) { }*/ public void setLogger(Logger logger) { this.logger = logger; } }
    配置文件

    aop.xml

    <!-- 配置一个日志类对象(切面类) --> <bean id="logger" class="com.briup.spring.day04.aop.Logger"></bean> <!-- 配置一个Account测试账户 --> <bean id="account" class="com.briup.spring.day04.aop.Account"> <property name="id" value="1001"></property> <property name="name" value="工资卡"></property> <property name="balance" value="100000.00"></property> </bean> <!-- 配置一个Service实现类对象(目标对象) --> <bean id="target" class="com.briup.spring.day04.aop.AccountServiceImpl"> <property name="accountDao" ref="accountDao"></property> </bean> <!-- 配置一个Dao实现类对象(目标对象中的属性) --> <bean id="accountDao" class="com.briup.spring.day04.aop.AccountDaoImpl"> </bean> <!-- 配置一个前置通知 --> <bean id="beforeAdvice" class="com.briup.spring.day04.aop.Before"> <property name="logger" ref="logger"></property> </bean> <!-- 配置一个后置通知 --> <bean id="afterAdvice" class="com.briup.spring.day04.aop.AfterReturn"> <property name="logger" ref="logger"></property> </bean> <!-- 配置一个环绕通知 --> <bean id="aroundAdvice" class="com.briup.spring.day04.aop.Around"> <property name="logger" ref="logger"></property> </bean> <!-- 配置一个异常抛出通知 --> <bean id="exceptionAdvice" class="com.briup.spring.day04.aop.Throws"> <property name="logger" ref="logger"></property> </bean> <!-- 配置一个代理对象 --> <bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!-- 告诉它三件事: 1.代理谁 2.委托类实现的接口 3.通知 上述三种信息全部通过注入依赖的方式实现 --> <property name="target" ref="target"></property> <property name="interfaces"> <list> <value>com.briup.spring.day04.aop.AccountService</value> </list> </property> <property name="interceptorNames"> <list> <value>beforeAdvice</value> <value>afterAdvice</value> <value>aroundAdvice</value> <value>exceptionAdvice</value> </list> </property> </bean>
    测试代码

    AopTest.java

    public static void main(String[] args) throws Exception { ApplicationContext context = new ClassPathXmlApplicationContext("aop_bean.xml"); AccountService proxy = (AccountService) context.getBean("proxy"); Account account = (Account) context.getBean("account"); proxy.bankAction(account); }
    5、增强器Advisor

    在上述案例中,我们在定义代理对象时,指定了目标对象target和通知advice的关联关系,这样target中的所有方法都会被代理。

    如果我们只需要代理target中的部分方法,可以使用增强器Advisor来指定目标对象target中哪些方法是我们要的连接点(JoinPoint)。

    增强器Advisor不需要手动编写类,直接在容器中引用Spring中提供的增强器组件即可。但是需要作用于某个advice上面。

    下面案例是使用增强器来筛选只需要代理AccountServiceImpl中的名为bankAction的方法的配置方式。

    配置文件aop.xml

    <!-- 添加如下增强器声明 --> <bean id="beforeAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <!-- 告诉它两件事:1.增强哪个advice 2.拦截哪些方法 --> <property name="advice" ref="beforeAdvice"></property> <property name="patterns"> <list> <value>.*bankAction</value> </list> </property> </bean>

    另外,代理对象advice配置中,应将原有的beforeAdvice通知改为这个beforeAdvisor增强器。

    <property name="interceptorNames"> <list> <value>beforeAdvisor</value> <value>afterAdvice</value> <value>aroundAdvice</value> <value>exceptionAdvice</value> </list> </property>
    6、自动代理

    在配置文件中我们往往需要给很多个目标对象设置代理对象,例如Service层的每一个实现类都需要被代理。

    那么上面例子的方式就需要每个目标对象的代理对象都需要配置一套类似的标签。

    使用自动代理可以用很少的配置为xml文件中的目标对象自动的生成对应的代理对象。

    配置文件

    aop.xml

    <!-- 删除原先的代理对象Bean,改为如下配置 --> <bean id="proxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> </bean> <!-- 使用自动代理的时候需要注意的方面: 1.当前的配置里面一定要有一个advisor的配置 2.不需要向自动代理类中注入任何信息 3.不管目标对象是否实现了一个或多接口,自动代理的方式都能够为它产生代理对象 4.从spring容器中拿代理对象的时候,需要通过目标对象的名字来拿。 5.spring如何确定配置文件中哪个bean是作为目标对象: 通过advisor中筛选的方法,如果这个bean中含有advisor中所配置的方法, 则这个bean将来称为我们的目标对象进行代理 -->
    7、通过名字进行自动代理

    虽然自动代理可以很方便的给容器中的目标对象设置对应的代理对象,但是并不是容器中所有对象都是我们的目标对象。

    我们更希望可以进一步筛选出某几个对象为我们的目标对象。通过名字进行自动代理解决了上面的问题,给我们提供了筛选目标对象的配置方式。

    配置文件

    aop.xml

    <!-- 删除原先的代理对象Bean,改为如下配置 --> <bean id="proxy" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <!-- 声明要代理的对象 --> <property name="beanNames"> <list> <value>target</value> </list> </property> <!-- 声明使用哪些通知或增强器 --> <property name="interceptorNames"> <list> <value>beforeAdvisor</value> <value>afterAdvice</value> <value>aroundAdvice</value> <value>exceptionAdvice</value> </list> </property> </bean>
    8、使用aop:config配置
    1)头部声明
    <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

    该头部声明中添加了aop相关标签的支持,这样我们就可以在eclipse中使用提示功能。

    2)<aop:config>标签

    <aop:config>可以用来代替我们之前在xml中定义的连接点、通知、增强器、代理等规则,并且能够通过简洁的语法,简化上述内容的配置过程。

    3)<aop:pointcut>

    <aop:pointcut/>是<aop:config>下的子标签,该标签的作用是定义一个切入点(Point Cut)。

    切入点是一组连接点的集合,而连接点是需要被代理的方法。我们可以将多个类中的多个方法,声明为一个切入点。将来我们可以直接将某个advice作用于某个切入点。

    属性如下:

    <aop:pointcut expression="" id=""/>

    id属性为当前切入点命名,将来为它配置advice时要通过这个id值引用。

    expression属性值用来筛选方法,内容和规则如下:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

    execution是Spring AOP中最主要的切入点指示符,该用法相对复杂。

    括号中的内容为表达式。内容解释:

    modifiers-pattern:访问修饰符 public、private…[可以省略]

    ret-type-pattern:返回值类型 [不可省略]

    declaring-type-pattern:方法所属的类 [可以省略]

    name-pattern(param-pattern):方法名(参数类型) [不可省略]

    throws-pattern?:抛出异常类型 [可以省略]

    其中,带问号的是必须要有的成分,不带问号的可以省略。

    访问修饰符可以省略,也可以使用*通配。

    返回值不可省略,类型必须写全限类名,或使用*通配。

    所属类名可以省略,类型必须写全限类名,或使用*通配。

    方法名不可省略,也可以使用*通配。

    参数列表(方法名后面的括号)不可省略,支持两个通配符,即"*“和”…",

    其中“*”代表一个任意类型的参数,

    而“…”代表零个或多个任意类型的参数。

    例如,()匹配一个不接受任何参数的方法,而(..)匹配一个接受任意数量参数的方法,(*)匹配了一个接受一个任何类型的参数的方法,(*,String)匹配了一个接受两个参数的方法,其中第一个参数是任意类型,第二个参数必须是String类型。

    下面给出一些常见切入点表达式的例子。

    必要性:访问修饰符0 返回值类型1 方法所属类0 方法名(参数)1 抛出0

    1)任意包下的任意类中的公共方法的执行:

    execution(public * *(..))

    修饰符 返回值 方法名(参数)

    当前项目中所有public修饰的、任意返回值类型的、任意方法名、任意参数列表的方法

    2)任何一个以“set”开始的方法的执行:

    execution(* set*(..))

    3)AccountService 接口的任意方法的执行:

    execution(* com.briup.service.AccountService.*(..))

    4)定义在service包里的任意方法的执行:

    execution(* com.briup.service.*.*(..))

    5)定义在service包或者子包里的任意方法的执行:

    execution(* com.briup.service..*.*(..))

    4)<aop:advisor>

    <aop:advisor>也是<aop:config>下的字标签,用来表示哪些增强器/通知需要作用在哪些切入点上。

    <aop:advisor advice-ref="" pointcut-ref=""/>

    例如如下配置:

    <aop:config> <aop:pointcut expression="execution(* com.briup.spring.day04.*.*(..))" id="myPointCut"/> <aop:advisor advice-ref="beforeAdvice" pointcut-ref="myPointCut"/> </aop:config>

    这样的配置就代表com.briup.spring.day04下面的所有类的所有方法都需要被代理,它们组合起来被命名为myPointCut。而我们为这些方法配置的通知类型是一个前置通知,名为beforeAdvice。而具体这个beforeAdvice是哪个Java类来充当,需要参考配置文件中关于advice的<bean>声明。

    9、使用<aop:aspect>简化切面类配置

    在上面的案例中,每种通知类型都需要专门定义一个类来表示,这样比较繁琐。

    Spring提供了一种方法可以简化这种配置方式。我们可以通过一个切面类来定义不同类型的通知,并且在配置文件中使用<aop:aspect>标签来配置该切面类。

    先定义一个类,不需要实现任何接口。我们可以在该类中定义一些方法用来表示前置、后置等通知:

    // 这个类相当于我们之前的切面类 // 只不过这个切面类中有很多方法都可以织入到切入点上面 // 我们可以控制把这里的任何一个方法织入到任何一个切入点上面 public class MyAspect { public void beforeTest(JoinPoint p){ System.out.println(p.getSignature().getName() + " before..."); } public void afterReturningTest(JoinPoint p){ System.out.println(p.getSignature().getName() + " afterReturning"); } // 在和aroundAdvice结合的时候,这个方法一定要加上这个ProceedingJoinPoint类型的参数 public Object aroundTest(ProceedingJoinPoint pjp) throws Throwable { // JoinPoint对象不能调用连接点所表示的方法 // ProceedingJoinPoint能调用连接点所表示的方法 pjp.proceed() System.out.println(pjp.getSignature().getName() + " is start.."); // 调用到连接点方法 Object obj = pjp.proceed(); System.out.println(pjp.getSignature().getName() + " is end.."); return obj; } public void throwingTest(JoinPoint p,Exception ex){ System.out.println(p.getSignature().getName() + " is throwing..." + ex.getMessage()); } }

    xml文件配置:

    <!-- ..前面应用组件配置相同 --> <!-- 配置切面类 --> <bean id="handler" class="com.briup.spring.day04.aop.MyAspect"></bean> <!-- 配置aop的代理 --> <aop:config> <!-- 定义切入点名为myPointCut --> <aop:pointcut expression="execution(public * com.briup.spring.day04.*.*(..))" id="myPointCut"/> <!-- 定义切面类 以及需要使用的advice --> <aop:aspect id="aspect" ref="handler"> <!-- 表示beforeAdvice会把切面类handler中的beforeTest方法织入到名字叫myPointCut的切入点上面 --> <aop:before method="beforeTest" pointcut-ref="myPointCut"/> <!-- after-returning表示方法正常结束才会起作用(抛异常时候不起作用) --> <aop:after-returning method="afterReturningTest" pointcut-ref="myPointCut"/> <aop:around method="aroundTest" pointcut-ref="myPointCut"/> <!-- throwing="ex"表示throwingTest方法中接收异常对象的名字一定要是ex --> <aop:after-throwing method="throwingTest" pointcut-ref="myPointCut" throwing="ex"/> </aop:aspect> </aop:config
    10、基于注解配置的AOP

    使用注解配置AOP其实就是在上面的类MyAspect中加入上注解,然后去掉xml中的aop标签配置。

    例子:

    @Component @Aspect public class AnnotationHandler { /* * 在一个方法上面加上注解来定义切入点 * 这个切入点的名字就是这个方法的名字 * 这个方法本身不需要有什么作用 * 这个方法的意义就是:给这个 @Pointcut注解一个可以书写的地方 * 因为注解只能写在方法、属性、类的上面 */ @Pointcut("execution(public * com.briup.spring.day04.aop..*.*(..))") public void myPointCut(){} //注:这里面的所有方法的JoinPoint类型参数都可以去掉不写,如果确实用不上的话 @Before("myPointCut()") public void beforeTest(JoinPoint p){ System.out.println(p.getSignature().getName() + " before..."); } @AfterReturning("myPointCut()") public void afterReturningTest(JoinPoint p){ System.out.println(p.getSignature().getName()+" afterReturning"); } @Around("myPointCut()") public Object aroundTest(ProceedingJoinPoint pjp)throws Throwable{ System.out.println(pjp.getSignature().getName()+" is start.."); //调用连接点的方法去执行 Object obj = pjp.proceed(); System.out.println(pjp.getSignature().getName()+" is end.."); return obj; } //在切入点中的方法执行期间抛出异常的时候,会调用这个 @AfterThrowing注解所标注的方法 @AfterThrowing(value="myPointCut()",throwing="ex") public void throwingTest(JoinPoint p,Exception ex){ System.out.println(p.getSignature().getName()+" is throwing..."+ex.getMessage()); } }

    xml配置:注意给例子中使用的其他的类上面也使用注解

    <aop:aspectj-autoproxy/> <context:component-scan base-package="com.briup"/>
    Processed: 0.010, SQL: 8