Spring AOP

一、 AOP 简介

1.1 什么是 AOP

AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

1.2 为什么需要 AOP

OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。通过案例,感受一下:
A类:

public class A {
    public void executeA(){
        //其他业务操作省略......
        recordLog();
    }

    public void recordLog(){
        //....记录日志并上报日志系统
    }
}

B类:

public class B {
    public void executeB(){
        //其他业务操作省略......
        recordLog();
    }

    public void recordLog(){
        //....记录日志并上报日志系统
    }
}

C类:

public class C {
    public void executeC(){
        //其他业务操作省略......
        recordLog();
    }

    public void recordLog(){
        //....记录日志并上报日志系统
    }
}

假设存在A、B、C三个类,需要对它们的方法访问进行日志记录,在代码中各种存在recordLog方法进行日志记录并上报,或许对现在的工程师来说几乎不可能写出如此糟糕的代码,但在OOP这样的写法是允许的,而且在OOP开始阶段这样的代码确实并大量存在着,直到工程师实在忍受不了一次修改,到处挖坟时(修改recordLog内容),才下定决心解决该问题,为了解决程序间过多冗余代码的问题,工程师便开始使用下面的编码方式:

//A类
public class A {
    public void executeA(){
        //其他业务操作省略...... args 参数,一般会传递类名,方法名称 或信息(这样的信息一般不轻易改动)
        Report.recordLog(args ...);
    }
}

//B类
public class B {
    public void executeB(){
        //其他业务操作省略......
        Report.recordLog(args ...);
    }
}

//C类
public class C {
    public void executeC(){
        //其他业务操作省略......
        Report.recordLog(args ...);
    }
}

//record
public class Report {
    public static void recordLog(args ...){
        //....记录日志并上报日志系统
    }
}

这样操作后,我们欣喜地发现问题似乎得到了解决,当上报信息内部方法需要调整时,只需调整Report类中recordLog方法体,也就避免了随处挖坟的问题,大大降低了软件后期维护的复杂度。确实如此,而且除了上述的解决方案,还存在一种通过继承来解决的方式,采用这种方式,只需把相通的代码放到一个类(一般是其他类的父类)中,其他的类(子类)通过继承父类获取相通的代码,如下:

//通用父类
public class Dparent {
    public void commond(){
        //通用代码
    }
}
//A 继承 Dparent 
public class A extends Dparent {
    public void executeA(){
        //其他业务操作省略......
        commond();
    }
}
//B 继承 Dparent 
public class B extends Dparent{
    public void executeB(){
        //其他业务操作省略......
        commond();
    }
}
//C 继承 Dparent 
public class C extends Dparent{
    public void executeC(){
        //其他业务操作省略......
        commond();
    }
}

显然代码冗余也得到了解决,这种通过继承抽取通用代码的方式也称为纵向拓展,与之对应的还有横向拓展。事实上有了上述两种解决方案后,在大部分业务场景的代码冗余问题也得到了实实在在的解决。
但是随着软件开发的系统越来越复杂,工程师认识到,传统的OOP程序经常表现出一些不自然的现象,核心业务中总掺杂着一些不相关联的特殊业务,如日志记录,权限验证,事务控制,性能检测,错误信息检测等等,这些特殊业务可以说和核心业务没有根本上的关联而且核心业务也不关心它们,比如在用户管理模块中,该模块本身只关心与用户相关的业务信息处理,至于其他的业务完全可以不理会,我们看一个简单例子协助理解这个问题

public interface IUserService {

    void saveUser();

    void deleteUser();

    void findAllUser();
}
//实现类
public class UserServiceImpl implements IUserService {

    //核心数据成员

    //日志操作对象

    //权限管理对象

    //事务控制对象

    @Override
    public void saveUser() {

        //权限验证(假设权限验证丢在这里)

        //事务控制

        //日志操作

        //进行Dao层操作
        userDao.saveUser();

    }

    @Override
    public void deleteUser() {

    }

    @Override
    public void findAllUser() {

    }
}

上述代码中我们注意到一些问题,权限,日志,事务都不是用户管理的核心业务,也就是说用户管理模块除了要处理自身的核心业务外,还需要处理权限,日志,事务等待这些杂七杂八的不相干业务的外围操作,而且这些外围操作同样会在其他业务模块中出现,这样就会造成如下问题

  • 代码混乱:核心业务模块可能需要兼顾处理其他不相干的业务外围操作,这些外围操作可能会混乱核心操作的代码,而且当外围模块有重大修改时也会影响到核心模块,这显然是不合理的。
  • 代码分散和冗余:同样的功能代码,在其他的模块几乎随处可见,导致代码分散并且冗余度高。
  • 代码质量低扩展难:由于不太相关的业务代码混杂在一起,无法专注核心业务代码,当进行类似无关业务扩展时又会直接涉及到核心业务的代码,导致拓展性低。
    显然前面分析的两种解决方案已束手无策了,那么该如何解决呢?事实上我们知道诸如日志,权限,事务,性能监测等业务几乎涉及到了所有的核心模块,如果把这些特殊的业务代码直接到核心业务模块的代码中就会造成上述的问题,而工程师更希望的是这些模块可以实现热插拔特性而且无需把外围的代码入侵到核心模块中,这样在日后的维护和扩展也将会有更佳的表现,假设现在我们把日志、权限、事务、性能监测等外围业务看作单独的关注点(也可以理解为单独的模块),每个关注点都可以在需要它们的时刻及时被运用而且无需提前整合到核心模块中,这种形式相当下图:


    20170215092953013.png

从图可以看出,每个关注点与核心业务模块分离,作为单独的功能,横切几个核心业务模块,这样的做的好处是显而易见的,每份功能代码不再单独入侵到核心业务类的代码中,即核心模块只需关注自己相关的业务,当需要外围业务(日志,权限,性能监测、事务控制)时,这些外围业务会通过一种特殊的技术自动应用到核心模块中,这些关注点有个特殊的名称,叫做“横切关注点”,上图也很好的表现出这个概念,另外这种抽象级别的技术就叫AOP(面向切面编程),正如上图所展示的横切核心模块的整面,因此AOP的概念就出现了,而所谓的特殊技术也就面向切面编程的实现技术,AOP的实现技术有多种,其中与Java无缝对接的是一种称为AspectJ的技术。那么这种切面技术(AspectJ)是如何在Java中的应用呢?不必担心,也不必全面了解AspectJ,对于AspectJ,我们只会进行简单的了解,从而为理解Spring中的AOP打下良好的基础(Spring AOP 与AspectJ 实现原理上并不完全一致,但功能上是相似的),毕竟Spring中已实现AOP主要功能,开发中直接使用Spring中提供的AOP功能即可,除非我们想单独使用AspectJ的其他功能。这里还需要注意的是,AOP的出现确实解决外围业务代码与核心业务代码分离的问题,但它并不会替代OOP,如果说OOP的出现是把编码问题进行模块化,那么AOP就是把涉及到众多模块的某一类问题进行统一管理,因此在实际开发中AOP和OOP同时存在并不奇怪,后面将会慢慢体会带这点,好的,让我们开始AspectJ吧。

二、AspectJ-AOP的领跑者

2.1环境搭建

首先通过maven仓库下载工具包aspectjtools-1.8.9.jar,该工具包包含ajc核心编译器,然后打开idea检查是否已安装aspectJ的插件:
20201014155354.png

配置项目使用ajc编译器(替换javac)如下图:
20201014155710.png

如果使用maven开发(否则在libs目录自行引入jar)则在pom文件中添加aspectJ的核心依赖包,包含了AspectJ运行时的核心库文件:

<dependency>
    <groupId>aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.5.4</version>
</dependency>

新建文件处创建aspectJ文件,然后就可以像运行java文件一样,操作aspect文件了。
20201014161547.png

2.2简单示例

编写一个HelloWord的类,然后利用AspectJ技术切入该类的执行过程。

public class HelloWorld {
    public void sayHello(){
        System.out.println("hello world!");
    }

    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.sayHello();
    }
}

编写AspectJ类,注意关键字为aspect(MyAspectJDemo.aj,其中aj为AspectJ的后缀),含义与class相同,即定义一个AspectJ的类

public aspect MyAspectJDemo {
    /**
     *  定义切点,日志记录切点
     */
    pointcut recordLog():call(* HelloWorld.sayHello(..));

    /**
     *  定义切点,权限验证
     */
    pointcut authCheck():call(* HelloWorld.sayHello(..));

    /**
     *  定义前置通知
     */
    before():authCheck(){
        System.out.println("sayHello方法执行前验证权限");
    }

    /**
     *  定义后置通知
     */
    after():recordLog(){
        System.out.println("sayHello方法执行后记录日志");
    }
}

运行helloworld的main函数:
20201014162815.png

我们发现,明明只运行了main函数,却在sayHello函数运行前后分别进行了权限验证和日志记录,事实上这就是AspectJ的功劳了。对aspectJ有了感性的认识后,再来聊聊aspectJ到底是什么?AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器),可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。在案例中,我们使用aspect关键字定义了一个类,这个类就是一个切面,它可以是单独的日志切面(功能),也可以是权限切面或者其他,在切面内部使用了pointcut定义了两个切点,一个用于权限验证,一个用于日志记录,而所谓的切点就是那些需要应用切面的方法,如需要在sayHello方法执行前后进行权限验证和日志记录,那么就需要捕捉该方法,而pointcut就是定义这些需要捕捉的方法(常常是不止一个方法的),这些方法也称为目标方法,最后还定义了两个通知,通知就是那些需要在目标方法前后执行的函数,如before()即前置通知在目标方法之前执行,即在sayHello()方法执行前进行权限验证,另一个是after()即后置通知,在sayHello()之后执行,如进行日志记录。到这里也就可以确定,切面就是切点和通知的组合体,组成一个单独的结构供后续使用
简单说明一下切点的定义语法:关键字为pointcut,定义切点,后面跟着函数名称,最后编写匹配表达式,此时函数一般使用call()或者execution()进行匹配,这里我们统一使用call()

pointcut 函数名 : 匹配表达式

关于定义通知的语法:首先通知有5种类型分别如下:

  • before 目标方法执行前执行,前置通知
  • after 目标方法执行后执行,后置通知
  • after returning 目标方法返回时执行 ,后置返回通知
  • after throwing 目标方法抛出异常时执行 异常通知
  • around 在目标函数执行中执行,可控制目标函数是否执行,环绕通知
    语法:
[返回值类型] 通知函数名称(参数) [returning/throwing 表达式]:连接点函数(切点函数){ 
函数体 
}

案例如下,其中要注意around通知即环绕通知,可以通过proceed()方法控制目标函数是否执行。

/**
  * 定义前置通知
  *
  * before(参数):连接点函数{
  *     函数体
  * }
  */
 before():authCheck(){
     System.out.println("sayHello方法执行前验证权限");
 }

 /**
  * 定义后置通知
  * after(参数):连接点函数{
  *     函数体
  * }
  */
 after():recordLog(){
     System.out.println("sayHello方法执行后记录日志");
 }


 /**
  * 定义后置通知带返回值
  * after(参数)returning(返回值类型):连接点函数{
  *     函数体
  * }
  */
 after()returning(int x): get(){
     System.out.println("返回值为:"+x);
 }

 /**
  * 异常通知
  * after(参数) throwing(返回值类型):连接点函数{
  *     函数体
  * }
  */
 after() throwing(Exception e):sayHello2(){
     System.out.println("抛出异常:"+e.toString());
 }



 /**
  * 环绕通知 可通过proceed()控制目标函数是否执行
  * Object around(参数):连接点函数{
  *     函数体
  *     Object result=proceed();//执行目标函数
  *     return result;
  * }
  */
 Object around():aroundAdvice(){
     System.out.println("sayAround 执行前执行");
     Object result=proceed();//执行目标函数
     System.out.println("sayAround 执行后执行");
     return result;
 }

切入点(pointcut)和通知(advice)的概念已比较清晰,而切面则是定义切入点和通知的组合如上述使用aspect关键字定义的MyAspectJDemo,把切面应用到目标函数的过程称为织入(weaving)。在前面定义的HelloWord类中除了sayHello函数外,还有main函数,以后可能还会定义其他函数,而这些函数都可以称为目标函数,也就是说这些函数执行前后也都可以切入通知的代码,这些目标函数统称为连接点,切入点(pointcut)的定义正是从这些连接点中过滤出来的,下图协助理解。
20170216232225542.png

2.3 AspectJ的织入方式及其原理概要

经过前面的简单介绍,我们已初步掌握了AspectJ的一些语法和概念,但这样仍然是不够的,我们仍需要了解AspectJ应用到java代码的过程(这个过程称为织入),对于织入这个概念,可以简单理解为aspect(切面)应用到目标函数(类)的过程。对于这个过程,一般分为动态织入和静态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术,这点后面会分析,这里主要重点分析一下静态织入,ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
20170219102612181.png

关于ajc编译器,是一种能够识别aspect语法的编译器,它是采用java语言编写的,由于javac并不能识别aspect语法,便有了ajc编译器,注意ajc编译器也可编译java文件。为了更直观了解aspect的织入方式,我们打开前面案例中已编译完成的HelloWord.class文件,反编译后的java代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.gaara.aspectJ;

import org.aspectj.lang.NoAspectBoundException;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class MyAspectJDemo {
    static {
        try {
            ajc$postClinit();
        } catch (Throwable var1) {
            ajc$initFailureCause = var1;
        }

    }

    public MyAspectJDemo() {
    }

    @Before(
        value = "authCheck()",
        argNames = ""
    )
    public void ajc$before$com_gaara_aspectJ_MyAspectJDemo$1$22c5541() {
        System.out.println("sayHello方法执行前验证权限");
    }

    @After(
        value = "recordLog()",
        argNames = ""
    )
    public void ajc$after$com_gaara_aspectJ_MyAspectJDemo$2$4d789574() {
        System.out.println("sayHello方法执行后记录日志");
    }

    public static MyAspectJDemo aspectOf() {
        if (ajc$perSingletonInstance == null) {
            throw new NoAspectBoundException("com_gaara_aspectJ_MyAspectJDemo", ajc$initFailureCause);
        } else {
            return ajc$perSingletonInstance;
        }
    }

    public static boolean hasAspect() {
        return ajc$perSingletonInstance != null;
    }
}

AspectJ的织入原理已很明朗了,当然除了编译期织入,还存在链接期(编译后)织入,即将aspect类和java目标类同时编译成字节码文件后,再进行织入处理,这种方式比较有助于已编译好的第三方jar和Class文件进行织入操作,掌握以上AspectJ知识点就足以协助理解Spring AOP了。

三、Spring AOP

3.1 简单示例

Spring AOP 与ApectJ 的目的一致,都是为了统一处理横切业务,但与AspectJ不同的是,Spring AOP 并不尝试提供完整的AOP功能(即使它完全可以实现),Spring AOP 更注重的是与Spring IOC容器的结合,并结合该优势来解决横切业务的问题,因此在AOP的功能完善方面,相对来说AspectJ具有更大的优势,同时,Spring注意到AspectJ在AOP的实现方式上依赖于特殊编译器(ajc编译器),因此Spring很机智回避了这点,转向采用动态代理技术的实现原理来构建Spring AOP的内部机制(动态织入),这是与AspectJ(静态织入)最根本的区别。在AspectJ 1.5后,引入@Aspect形式的注解风格的开发,Spring也非常快地跟进了这种方式,因此Spring 2.0后便使用了与AspectJ一样的注解。请注意,Spring 只是使用了与 AspectJ 5 一样的注解,但仍然没有使用 AspectJ 的编译器,底层依是动态代理技术的实现,因此并不依赖于 AspectJ 的编译器。下面我们先通过一个简单的案例来演示Spring AOP的入门程序
接口类:

public interface UserDao {
    int addUser();

    void updateUser();

    void deleteUser();

    void findUser();
}

实现类:

@Repository
public class UserDaoImpl implements UserDao{
    public int addUser() {
        System.out.println("add user ......");
        return 6666;
    }

    public void updateUser() {
        System.out.println("update user ......");
    }

    public void deleteUser() {
        System.out.println("delete user ......");
    }

    public void findUser() {
        System.out.println("find user ......");
    }
}

aspect类:

@Aspect
public class MyAspect {

    /**
     * 前置通知
     */
    @Before("execution(* com.gaara.aop.dao.UserDao.addUser(..))")
    public void  before(){
        System.out.println("前置通知....");
    }

    /**
     * 后置通知
     * returnVal,切点方法执行后的返回值
     */
    @AfterReturning(value = "execution(* com.gaara.aop.dao.UserDao.addUser(..))", returning = "returnVal")
    public void afterReturning(Object returnVal){
        System.out.println("后置通知...."+returnVal);
    }

    /**
     * 环绕通知
     * @param joinPoint 可用于执行切点的类
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.gaara.aop.dao.UserDao.addUser(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知前....");
        Object obj= joinPoint.proceed();
        System.out.println("环绕通知后....");
        return obj;
    }

    /**
     * 抛出通知
     * @param e
     */
    @AfterThrowing(value = "execution(* com.gaara.aop.dao.UserDao.addUser(..))", throwing = "e")
    public void afterThrowable(Throwable e){
        System.out.println("出现异常:msg="+e.getMessage());
    }

    /**
     * 无论什么情况下都会执行的方法
     */
    @After(value = "execution(* com.gaara.aop.dao.UserDao.addUser(..))")
    public void after(){
        System.out.println("最终通知....");
    }
}

conf:

@Configuration
@EnableAspectJAutoProxy
public class ConfigOfAOP {

    @Bean
    public UserDao userDao(){
        return new UserDaoImpl();
    }

    @Bean
    public MyAspect myAspect(){
        return new MyAspect();
    }
}

测试类:

public class UserDaoAspectJTest {
    AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(ConfigOfAOP.class);

    @Test
    public void aspectJTest(){
        UserDao userDao = app.getBean(UserDao.class);
        userDao.addUser();
    }
}

简单说明一下,定义了一个目标类UserDaoImpl,利用Spring2.0引入的aspect注解开发功能定义aspect类即MyAspect,在该aspect类中,编写了5种注解类型的通知函数,分别是前置通知@Before、后置通知@AfterReturning、环绕通知@Around、异常通知@AfterThrowing、最终通知@After,这5种通知与前面分析AspectJ的通知类型几乎是一样的,并注解通知上使用execution关键字定义的切点表达式,即指明该通知要应用的目标函数,当只有一个execution参数时,value属性可以省略,当含两个以上的参数,value必须注明,如存在返回值时。当然除了把切点表达式直接传递给通知注解类型外,还可以使用@pointcut来定义切点匹配表达式,这个与AspectJ使用关键字pointcut是一样的,后面分析。
运行程序,结果符合预期:

20201015160104.png

3.2 AOP 术语

AOP 领域中的术语:

  • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
  • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
  • 切点(PointCut): 可以插入增强处理的连接点。
  • 切面(Aspect): 切面是通知和切点的结合。
  • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。

3.3 定义切入点函数

在案例中,定义过滤切入点函数时,是直接把execution已定义匹配表达式作为值传递给通知类型的如下:

@After(value = "execution(* com.gaara.aop.dao.UserDao.addUser(..))")
public void after(){
    System.out.println("最终通知....");
}

除了上述方式外,还可采用与ApectJ中使用pointcut关键字类似的方式定义切入点表达式如下,使用@Pointcut注解:

/**
 * 使用Pointcut定义切点
 */
@Pointcut("execution(* com.gaara.aop.dao.UserDao.addUser(..))")
private void myPointcut(){}

/**
 * 应用切入点函数
 */
@After(value="myPointcut()")
public void after(){
    System.out.println("最终通知....");
}

3.4 切入点指示符

为了方法通知应用到相应过滤的目标方法上,SpringAOP提供了匹配表达式,这些表达式也叫切入点指示符,在前面的案例中,它们已多次出现。

3.4.1 通配符

在定义匹配表达式时,通配符几乎随处可见,如*、.. 、+ ,它们的含义如下:

  • .. :匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包
//任意返回值,任意名称,任意参数的公共方法
execution(public * *(..))
//匹配com.gaara.aop.dao包及其子包中所有类中的所有方法
within(com.gaara.aop.dao..*)
  • + :匹配给定类的任意子类
//匹配实现了DaoUser接口的所有子类的方法
within(com.gaara.aop.dao.DaoUser+)
  • * :匹配任意数量的字符
//匹配com.gaara.service包及其子包中所有类的所有方法
within(com.gaara.service..*)
//匹配以set开头,参数为int类型,任意返回值的方法
execution(* set*(int))

3.4.2 类型签名表达式

为了方便类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字。其语法格式如下:

within(<type name>)

type name 则使用包名或者类名替换即可,来点案例吧。

//匹配com.gaara.aop.dao包及其子包中所有类中的所有方法
@Pointcut("within(com.gaara.aop.dao..*)")

//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.gaara.aop.dao.UserDaoImpl)")

//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.gaara.aop.dao.UserDaoImpl+)")

//匹配所有实现UserDao接口的类的所有方法
@Pointcut("within(com.gaara.aop.dao.UserDao+)")

3.4.3 方法签名表达式

如果想根据方法签名进行过滤,关键字execution可以帮到我们,语法表达式如下

//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))

对于给定的作用域、返回值类型、完全限定类名以及参数匹配的方法将会应用切点函数指定的通知,这里给出模型案例:

//匹配UserDaoImpl类中的所有方法
@Pointcut("execution(* com.gaara.aop.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中的所有公共的方法
@Pointcut("execution(public * com.gaara.aop.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public int com.gaara.aop.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法
@Pointcut("execution(public * com.gaara.aop.dao.UserDaoImpl.*(int , ..))")

3.4.4 其他指示符

  • bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;
//匹配名称中带有后缀Service的Bean。
@Pointcut("bean(*Service)")
private void myPointcut(){}
  • this :用于匹配当前AOP代理对象类型的执行方法;请注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
//匹配了任意实现了UserDao接口的代理对象的方法进行过滤
@Pointcut("this(com.gaara.dao.UserDao)")
private void myPointcut(){}
  • target :用于匹配当前目标对象类型的执行方法;
//匹配了任意实现了UserDao接口的目标对象的方法进行过滤
@Pointcut("target(com.gaara.dao.UserDao)")
private void myPointcut(){}
  • @within:用于匹配所以持有指定注解类型内的方法;请注意与within是有区别的, within是用于匹配指定类型内的方法执行;
//匹配使用了MarkerAnnotation注解的类(注意是类)
@Pointcut("@within(com.gaara.annotation.MarkerAnnotation)")
private void myPointcut(){}
  • @annotation: 根据所应用的注解进行方法过滤
//匹配使用了MarkerAnnotation注解的方法(注意是方法)
@Pointcut("@annotation(com.gaara.annotation.MarkerAnnotation)")
private void myPointcut(){}

关于表达式指示符就介绍到这,我们主要关心前面几个常用的即可,不常用过印象即可。这里最后说明一点,切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或者&&、||、!),如下一个简单例子:

//匹配了任意实现了UserDao接口的目标对象的方法并且该接口不在com.gaara.dao包及其子包下
@Pointcut("target(com.gaara.spring.springAop.dao.UserDao) !within(com.gaara.dao..*)")
private void myPointcut(){}
//匹配了任意实现了UserDao接口的目标对象的方法并且该方法名称为addUser
@Pointcut("target(com.gaara.spring.springAop.dao.UserDao)&&execution(* com.gaara.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut(){}

3.5 通知函数以及传递参数

3.5.1 5种通知函数

通知在前面的aspectJ和Spring AOP的入门案例已见过面,在Spring中与AspectJ一样,通知主要分5种类型,分别是前置通知、后置通知、异常通知、最终通知以及环绕通知,下面分别介绍。

  • 前置通知@Before
    前置通知通过@Before注解进行标注,并可直接传入切点表达式的值,该通知在目标函数执行前执行,注意JoinPoint,是Spring提供的静态变量,通过joinPoint 参数,可以获取目标对象的信息,如类名称,方法参数,方法名称等,,该参数是可选的。
/**
 * 前置通知
 * @param joinPoint 该参数可以获取目标对象的信息,如类名称,方法参数,方法名称等
 */
@Before("execution(* com.gaara.aop.dao.UserDao.addUser(..))")
public void before(JoinPoint joinPoint){
    System.out.println("我是前置通知");
}
  • 后置通知@AfterReturning
    通过@AfterReturning注解进行标注,该函数在目标函数执行完成后执行,并可以获取到目标函数最终的返回值returnVal,当目标函数没有返回值时,returnVal将返回null,必须通过returning = “returnVal”注明参数的名称而且必须与通知函数的参数名称相同。请注意,在任何通知中这些参数都是可选的,需要使用时直接填写即可,不需要使用时,可以完成不用声明出来。如下
/**
* 后置通知,不需要参数时可以不提供
*/
@AfterReturning(value="execution(* com.gaara.aop.dao.UserDao.*User(..))")
public void AfterReturning(){
   System.out.println("我是后置通知...");
}
/**
* 后置通知
* returnVal,切点方法执行后的返回值
*/
@AfterReturning(value="execution(* com.gaara.aop.dao.UserDao.*User(..))",returning = "returnVal")
public void AfterReturning(JoinPoint joinPoint,Object returnVal){
   System.out.println("我是后置通知...returnVal+"+returnVal);
}
  • 异常通知 @AfterThrowing
    该通知只有在异常时才会被触发,并由throwing来声明一个接收异常信息的变量,同样异常通知也用于Joinpoint参数,需要时加上即可,如下:
/**
* 抛出通知
* @param e 抛出异常的信息
*/
@AfterThrowing(value="execution(* com.gaara.aop.dao.UserDao.addUser(..))",throwing = "e")
public void afterThrowable(Throwable e){
  System.out.println("出现异常:msg="+e.getMessage());
}
  • 最终通知 @After
    该通知有点类似于finally代码块,只要应用了无论什么情况下都会执行。
/**
 * 无论什么情况下都会执行的方法
 * joinPoint 参数
 */
@After("execution(* com.gaara.aop.dao.UserDao.*User(..))")
public void after(JoinPoint joinPoint) {
    System.out.println("最终通知....");
}
  • 环绕通知@Around
    环绕通知既可以在目标方法前执行也可在目标方法之后执行,更重要的是环绕通知可以控制目标方法是否指向执行,但即使如此,我们应该尽量以最简单的方式满足需求,在仅需在目标方法前执行时,应该采用前置通知而非环绕通知。案例代码如下第一个参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来执行目标函数,proceed()的返回值就是环绕通知的返回值。同样的,ProceedingJoinPoint对象也是可以获取目标对象的信息,如类名称,方法参数,方法名称等等。
@Around("execution(* com.gaara.aop.dao.UserDao.*User(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("我是环绕通知前....");
    //执行目标函数
    Object obj= joinPoint.proceed();
    System.out.println("我是环绕通知后....");
    return obj;
}

3.5.2 通知传递参数

在Spring AOP中,除了execution和bean指示符不能传递参数给通知方法,其他指示符都可以将匹配的方法相应参数或对象自动传递给通知方法。获取到匹配的方法参数后通过”argNames”属性指定参数名。如下,需要注意的是args(指示符)、argNames的参数名与before()方法中参数名 必须保持一致即param。

@Before(value="args(param)", argNames="param") //明确指定了    
public void before(int param) {    
    System.out.println("param:" + param);    
}  

当然也可以直接使用args指示符不带argNames声明参数,如下:

@Before("execution(public * com.gaara..*.addUser(..)) && args(userId,..)")  
public void before(int userId) {  
    //调用addUser的方法时如果与addUser的参数匹配则会传递进来会传递进来
    System.out.println("userId:" + userId);  
} 

args(userId,..)该表达式会保证只匹配那些至少接收一个参数而且传入的类型必须与userId一致的方法,记住传递的参数可以简单类型或者对象,而且只有参数和目标方法也匹配时才有会有值传递进来。

3.6 Aspect优先级

在不同的切面中,如果有多个通知需要在同一个切点函数指定的过滤目标方法上执行,那些在目标方法前执行(”进入”)的通知函数,最高优先级的通知将会先执行,在执行在目标方法后执行(“退出”)的通知函数,最高优先级会最后执行。而对于在同一个切面定义的通知函数将会根据在类中的声明顺序执行。如下:

package com.gaara.aop.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectOne {
    @Pointcut("execution(* com.gaara.aop.dao.UserDao.deleteUser(..))")
    private void myPointcut(){}

    @Before("myPointcut()")
    public void beforeOne(){
        System.out.println("前置通知....执行顺序1");
    }

    @Before("myPointcut()")
    public void beforeTwo(){
        System.out.println("前置通知....执行顺序2");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningThree(){
        System.out.println("后置通知....执行顺序3");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningFour(){
        System.out.println("后置通知....执行顺序4");
    }
}

在同一个切面中定义多个通知响应同一个切点函数,执行顺序为声明顺序:
Aspect优先级

如果在不同的切面中定义多个通知响应同一个切点,进入时则优先级高的切面类中的通知函数优先执行,退出时则最后执行,如下定义AspectOne类和AspectTwo类并实现org.springframework.core.Ordered接口,该接口用于控制切面类的优先级,同时重写getOrder方法,定制返回值,返回值(int类型)越小优先级越大。其中AspectOne返回值为0,AspectTwo返回值为2,显然AspectOne优先级高于AspectTwo。

package com.gaara.aop.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.Ordered;


@Aspect
public class AspectOne implements Ordered{
    @Pointcut("execution(* com.gaara.aop.dao.UserDao.deleteUser(..))")
    private void myPointcut(){}

    @Before("myPointcut()")
    public void beforeOne(){
        System.out.println("前置通知..AspectOne..执行顺序1");
    }

    @Before("myPointcut()")
    public void beforeTwo(){
        System.out.println("前置通知..AspectOne..执行顺序2");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningThree(){
        System.out.println("后置通知..AspectOne..执行顺序3");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningFour(){
        System.out.println("后置通知..AspectOne..执行顺序4");
    }

    /**
     * 定义优先级,值越低,优先级越高
     * @return
     */
    public int getOrder() {
        return 0;
    }
}


@Aspect
public class AspectTwo implements Ordered{
    @Pointcut("execution(* com.gaara.aop.dao.UserDao.deleteUser(..))")
    private void myPointcut(){}

    @Before("myPointcut()")
    public void beforeOne(){
        System.out.println("前置通知..AspectTwo..执行顺序1");
    }

    @Before("myPointcut()")
    public void beforeTwo(){
        System.out.println("前置通知..AspectTwo..执行顺序2");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningThree(){
        System.out.println("后置通知..AspectTwo..执行顺序3");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningFour(){
        System.out.println("后置通知..AspectTwo..执行顺序4");
    }

    /**
     * 定义优先级,值越低,优先级越高
     * @return
     */
    public int getOrder() {
        return 2;
    }
}

运行结果如下:
aop2.png

虽然只演示了前置通知和后置通知,但其他通知也遵循相同的规则。

四、 Spring AOP的实现原理概要

SpringAop的实现原理是基于动态织入技术,而AspectJ则是静态织入,而动态代理技术又分为Java JDK动态代理和CGLIB动态代理,前者是基于反射技术实现,后者是基于继承的机制实现。

4.1 JDK动态代理

示例:

// 自定义的接口类,JDK动态代理的实现必须有对应的接口类
public interface ExInterface {
    void execute();
}

// A类,实现了ExInterface接口类
public class A implements ExInterface{
    public void execute() {
        System.out.println("执行A的execute方法...");
    }
}

// 代理类的实现
public class JDKProxy implements InvocationHandler{
    // 要被代理的目标对象
    private A target;

    public JDKProxy(A target) {
        this.target = target;
    }

    /**
     * 创建代理类
     * @return
     */
    public ExInterface createProxy(){
        return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    /**
     * 调用被代理类(目标对象)的任意方法都会触发invoke方法
     * @param proxy 代理类
     * @param method 被代理类的方法
     * @param args 被代理类的方法参数
     * @return
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 过滤不需要该业务的方法
        if ("execute".equals(method.getName())){
            // 调用前验证权限
            System.out.println("执行前验证用户权限...");
            // 调用目标对象的方法
            Object result = method.invoke(target, args);
            // 记录日志数据
            System.out.println("记录日志并上报...");
            return result;
        }else if ("delete".equals(method.getName())){

        }
        // 如果不需要增强直接执行原方法
        return method.invoke(target, args);
    }
}

//测试
public void JDKProxyTest(){
        A a = new A();
        // 创建JDK代理
        JDKProxy jdkProxy = new JDKProxy(a);
        // 创建代理对象
        ExInterface proxy = jdkProxy.createProxy();
        // 执行代理对象方法
        proxy.execute();
    }

运行结果:
jdkproxy.png

在A的execute方法里面没有任何权限和日志的代码,也没有直接操作a对象,相反的只是调用了proxy代理对象的方法,最终结果却是预期的,这就是动态代理技术,是不是跟SpringAOP似曾相识,实际上动态代理的底层是通过反射技术来实现,只要拿到A类的class文件和A类的实现接口,很自然就可以生成相同接口的代理类并调用a对象的方法了,但实现java动态代理是有先决条件的,改条件是目标对象必须带接口,如A类的接口是ExInterface,通过ExInterface接口动态代理技术便可以创建于A类类型相同的代理对象。
代理对象的创建时通过Proxy类达到的,Proxy类由Java JDK提供,利用Proxy#newProxyInstance方法便可以动态生成代理对象,底层通过反射实现,改方法需要3个参数

/**
* @param loader 类加载器,一般传递目标对象(A类即被代理的对象)的类加载器
* @param interfaces 目标对象(A)的实现接口
* @param h 回调处理句柄
*/
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

创建代理类proxy的代码如下:

public ExInterface createProxy(){
        return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

到此并没有结束,因为有接口还是远远不够,代理类还需要实现InvocationHandler接口,也是由JDK提供,代理类必须实现并重写invoke方法,完全可以把InvocationHandler看成一个回调函数,Proxy方法创建代理对象后,调用用execute方法时,将会回调InvocationHandler#invoke方法,因此我们可以在invoke方法中来控制被代理对象的方法执行,从而在该方法前后动态增加其他需要执行的业务。

invoke方法有三个参数:

  • Object proxy:生成的代理对象
  • Method method:目标对象的方法,通过反射调用
  • Object[] args:目标对象方法的参数

这就是是Java JDK动态代理的代码实现过程,运用JDK动态代理,被代理类,必须已有实现接口,因为JDK提供的Proxy类将通过目标对象的类加载器ClassLoader和Interface,以及句柄创建与A类拥有相同接口的代理对象proxy,改代理对象将拥有接口中的所有方法,同时代理类必须实现一个类似回调函数的InvocationHandler接口并重写改接口中的invoke方法,当调用proxy的每个方法时,invoke方法将被调用,利用该特性,可以在invoke方法中对目标对象方法执行的前后动态添加其他外围业务操作,此时无需触及目标对象的任何代码,也就实现了外围业务的操作与目标对象完全解耦合的目的。当然缺点也很明显,需要拥有接口,这也就有了后来的CGLIB动态代理了。

4.2 CGLIB动态代理

通过CGLIB动态代理实现上述功能并不要求目标对象拥有接口类,实际上CGLIB动态代理是通过继承的方式实现的,因此可以减少没必要的接口,下面直接通过简单案例协助理解

// 被代理的类
public class A {
    public void execute() {
        System.out.println("执行A的execute方法...");
    }
}

// 代理类
public class CGLIBProxy implements MethodInterceptor{
    /**
     * 被代理的目标类
     */
    private A target;

    public CGLIBProxy(A target) {
        super();
        this.target = target;
    }

    /**
     * 创建代理对象
     * @return
     */
    public A createProxy(){
        // 使用CGLIB生成代理
        // 1.声明增强类实例,用于生产代理类
        Enhancer enhancer = new Enhancer();
        // 2.设置被代理类字节码,CGLIB根据字节码生成被代理类的子类
        enhancer.setSuperclass(target.getClass());
        // 3.设置回调函数,即一个方法拦截
        enhancer.setCallback(this);
        // 创建代理
        return (A) enhancer.create();
    }

    /**
     * 回调函数
     * @param proxy 代理对象
     * @param method 委托类方法
     * @param args 方法参数
     * @param methodProxy 每个被代理的方法都对应一个MethodProxy对象,
     *                    methodProxy.invokeSuper方法最终调用委托类(目标类)的原始方法
     * @return
     * @throws Throwable
     */
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 过滤不需要该业务的方法
        if ("execute".equals(method.getName())){
            // 调用前验证权限
            System.out.println("执行前验证用户权限...");
            // 调用目标对象的方法
            Object result = methodProxy.invokeSuper(proxy, args);
            // 记录日志数据
            System.out.println("记录日志并上报...");
            return result;
        }else if ("delete".equals(method.getName())){

        }
        // 如果不需要增强直接执行原方法
        return methodProxy.invokeSuper(proxy, args);
    }
}

从代码看被代理的类无需接口即可实现动态代理,而CGLIBProxy代理类需要实现一个方法拦截器接口MethodInterceptor并重写intercept方法,类似JDk动态代理的InvocationHandler接口,也是理解为回调函数,同理每次调用代理对象的方法时,intercept方法都会被调用,利用该方法便可以在运行时对方法执行前后进行动态增强。关于代理对象创建则通过Enhancer类来设置的,Enhancer是一个用于产生代理对象的类,作用类似JDK的Proxy类,因为CGLIB底层是通过继承实现的动态代理,因此需要传递目标对象的Class,同时需要设置一个回调函数对调用方法进行拦截并进行相应处理,最后create()创建目标对象的代理对象,运行结果与前面的JDK动态代理效果相同。
通过这些我们也应该明白SpringAOP确实是通过CGLIB或者JDK代理来动态的生成代理对象,这个代理对象指的就是AOP代理类,而AOP代理类的方法则通过在目标对象的切点动态地织入增强处理,从而完成了对目标方法的增强。这里并没有非常深入去分析这两种技术,只是演示了SpringAOP底层实现的最简化的模型代码,SpringAOP内部已都实现了这两种技术,SpringAOP在使用时机上也进行自动化调整,当有接口时会自动选择JDK动态代理技术,如果没有则选择CGLIB技术,当然SpringAOP的底层实现并没有这么简单,为更简便生成代理对象,SpringAOP内部实现了一个专注于生成代理对象的工厂类,这样就避免了大量的手动编码,这一点也是十分人性化的,但最核心的还是动态代理技术。

五、 Spring AOP源码解析

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

推荐阅读更多精彩内容