tips:本文首发在公众号逆锋起笔 ,本文源代码在公众号回复aop 即可查看。

什么是AOP?

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

为什么需要AOP?

实际开发中我们应该都遇到过类似这样的场景:在多个模块间有某段重复的代码,我们通常是怎么处理的?

在传统的面向过程编程中,我们也会将这段代码,抽象成一个方法,然后在需要的地方分别调用这个方法,这样当这段代码需要修改时,我们只需要改变这个方法就可以了。然而需求总是变化的,有一天,新增了一个需求,需要再多出做修改,我们需要再抽象出一个方法,然后再在需要的地方分别调用这个方法,又或者我们不需要这个方法了,我们还是得删除掉每一处调用该方法的地方。实际上涉及到多个地方具有相同的修改的问题我们都可以通过 AOP 来解决。

AOP的本质

AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,看到这其实应该明白了,AOP 其实就是代理模式的典型应用。
按照 AOP 框架修改源代码的时机,可以将其分为两类:

  • 静态 AOP 实现, AOP 框架在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ。
  • 动态 AOP 实现, AOP 框架在运行阶段对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP。

AOP术语

AOP 领域中的术语:

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

这些术语不同书籍翻译有区别,关键要结合程序理解之后,就明白各个术语的意思了。

第一个Spring AOP项目

新建module

名字随着各位心情来,我这里是sping-aop

配置

pom.xml 文件中增加下面两个依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.14</version>
</dependency>

<!-- 添加能提供AOP注解功能的依赖,该依赖并非Spring提供-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.14</version>
</dependency>

模拟业务

1、新建包service,提供如下接口

public interface SomeService {

    void doSome();

    void doSome(String name,Integer num);
}

2、在该包下,新建子包impl,并提供实现类如下

public class SomeServiceImpl implements SomeService {

    @Override
    public void doSome() {
        System.out.println("原来的业务方法,在实现类中");
    }

    @Override
    public void doSome(String name, Integer num) {
        System.out.println("原来的业务方法,在实现类中,有两个参数:" + name + "->" + num);
    }

}

3、再单独新建一个包,名字随意,我这里名为handle,其下有一个名为 MyAspect 的类,代码如下:

@Aspect
public class MyAspect {

    /**
     * execution() 这个叫 切点表达式
     * <p>
     * 语法依次是:方法修饰符(可省略)、方法返回类型、方法所在包名全路径+方法名+方法参数类型列表
     */
    @Before(value = "execution(public void com.javafirst.service.impl.SomeServiceImpl.doSome(java.lang.String, java.lang.Integer))")
    public void aop_before() {
        System.out.println("在原有业务方法之前执行逻辑,这里自动代理功能要执行的代码.");
    }

}

这里使用了注解 @Aspect,这就是我们在前面添加的依赖,也就是 Spring 提供的注解。这里大家需要知道一个概念:切点表达式

4、Spring核心配置文件applicationConext.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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--  目标对象  -->
    <bean id="someService" class="com.javafirst.service.impl.SomeServiceImpl"/>

    <!--  切面类  -->
    <bean id="my_handle" class="com.javafirst.handle.MyAspect"/>

    <!--  代理生成器  -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

这里会接触到一个新标签 &lt;aspect-autoproxy&gt; ,代理生成器,系统自动在内存中生成代理类,无需我们手动显示写代理类。

5、和我们上一节学习一样,本节的测试代码基本不会变

/**
 * @Before 在目标方法之前执行
 */
@Test
public void testBefore() {
    String config = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);

    SomeService someService = (SomeService) context.getBean("someService");
    someService.doSome();
    someService.doSome("无崖子", 87);
}

6、结果展示:

在原有业务方法之前执行逻辑,这里自动代理功能要执行的代码.
原来的业务方法,在实现类中,有两个参数:无崖子->87

小结

通过上面这个流程,我们简单掌握一下通过注解来实现代理功能的神奇,当然了前面提到的切点表达式也是有语法的,且Spring AOP中的通知注解也不止@Before这一种,这两点就是本文的重点。

接着往下看↓

Spring AOP 5 种通知注解

在学习通知注解前,我们先看下前面提到的切点表达式,以上面的例子,做几个变形,各位可以测试一下结果,方便理解,这并不难。

  • execution(public void com.javafirst.service.impl.SomeServiceImpl.doSome(java.lang.String, java.lang.Integer)):这表示注解功能代码在一个修饰符为public、返回值为void 、在这个com.javafirst.service.impl路径下的 SomeServiceImpl 类中的doSome(java.lang.String, java.lang.Integer) 方法之前执行。
  • execution(void com.javafirst.service.impl.SomeServiceImpl.doSome(java.lang.String, java.lang.Integer)):方法修饰符可省略
  • execution(void com.javafirst.service.impl.*.doSome(java.lang.String, java.lang.Integer)):指定包下所有类的 doSome(java.lang.String, java.lang.Integer) 方法
  • execution(void com.javafirst.service.impl.*.*(java.lang.String, java.lang.Integer)):指定包下所有类中带有两个参数(java.lang.String, java.lang.Integer)的方法
  • execution(* com.javafirst.service.*.*(java.lang.String, java.lang.Integer)):指定包下及其子包下所有方法,不区分方法返回类型(各位可以在接口中多写一个方法做测试)
  • execution(void com.javafirst.service.*.*(..)):指定包下及其子包下所有方法,不区分是否有参数。(这个可以测试出我们前面接口中定义的另一个方法在执行前是否也会执行代理方法中的内容)

多个匹配之间我们可以使用链接符 &&||来表示 “且”、“或”、“非”的关系。但是在使用 XML 文件配置时,这些符号有特殊的含义,所以我们使用 “and”、“or”、“not”来表示。这里就不举例了,下面看通知注解

SpringAOP注解

@Before

前置通知:通知方法在目标方法之前调用。

使用我们已经学习过了,这里再了解一下该注解的参数,方法名字可以自定义,那么系统的一个参数是JoinPoint 要使用的话,必须是该方法的形参列表第一个,功能类似于Java反射中的Method类,可以获取方法名、方法参数等,用于做不同的逻辑处理。

比如我们将前面的代码做个变形,就可以验证结果:

@Before(value = "execution(void com.javafirst.service.*.*(..))")
public void aop_before(JoinPoint point) {
    System.out.println();
    System.out.println("指定包下及其子包下所有方法,不区分是否有参数-在原有业务方法之前执行逻辑,这里自动代理功能要执行的代码.");

    System.out.println("方法名:" + point.getSignature().getName());
    System.out.println("方法参数:" + point.getArgs().length + " 个参数");
    if (point.getArgs().length == 2) {
        System.out.println("要执行 两个参数的方法");
    } else {
        System.out.println("要执行 无参数的方法");
    }
}

@AfterReturning

后置通知:在目标方法执行完之后再执行。从该方法的参数也可以接收到目标方法的返回结果,推荐使用Object类型接收。

1、在原来的接口类基础上新增一个方法:

String returnPrice(double price,float discount);

我们要测试后置通知,就要在执行代理方法之前拿到目标方法的返回值,所以这里定义的是有返回值并且带参数的方法。

2、实现类中的实现逻辑(实际开发根据业务来)示例:

@Override
public String returnPrice(double price, float discount) {
    if (discount > 0.0f && price > 0.0) {
        return "折扣价:" + (price * discount);
    }
    return "原价:" + price;
}

3、切面类中定义后置通知方法

/**
  * 后置通知
  *
  * @param res 参数名必须和 returning 值保持一致,表示目标方法的返回值
  */
@AfterReturning(value = "execution(java.lang.String com.javafirst.service.impl.*.return*(..))", returning = "res")
public void aop_afterReturning(Object res) {
    System.out.println();
    System.out.println("目标方法执行结果:" + res);
    System.out.println("在目标方法执行后 输出.");
}

这里的切入点表达式和之前的一样,重点是这里的returning的值和方法的形参名之间的关系,这两者必须保持一致,否则拿不到目标方法的返回结果。

4、测试代码就不贴了,各位自己测试

@Around

将目标方法封装起来,简单理解:可以在目标方法之前和只有都执行相关代码逻辑。下面看示例:

1、定义接口方法

String doWork(String name);

2、接口实现类中代码:

@Override
public String doWork(String name) {
    System.out.println("正在撸码的工程师:" + name);
    return "员工姓名:" + name;
}

注意这里的返回值执行时机。

3、切面类中定义的注解通知代码:

/**
 * 环绕通知
 * <p>
 * 方法的定义:
 * - 必须是public
 * - 必须有返回值,推荐是Object
 * - 参数必须有且是 ProceedingJoinPoint
 *
 * @param joinPoint
 * @return
 */
@Around(value = "execution(public java.lang.String com.javafirst.service.impl.SomeServiceImpl.doWork(java.lang.String))")
public Object aop_around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println();
    System.out.println("我是@Around 方法里面的输出");

    // 通过参数 ProceedJoinPoint 控制目标方法是否可以被执行
    Object args[] = joinPoint.getArgs();
    if (null != args && args.length > 0) {
        // 执行目标方法
        if (null != args[0]) {
            joinPoint.proceed();
            return "您传入的参数是:" + args[0];
        }
    }

    return "我是@Around 方法return的内容";
}

这里如果不通过参数 ProceedingJoinPoint 处理执行目标方法,默认目标方法是不会执行的;注意这里的输出语句和返回值执行逻辑,从而理解环绕通知。

4、测试代码

/**
 * @Around 在目标方法之前之后都可以执行相关代码
 */
@Test
public void testAround() {
    String config = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);

    SomeService someService = (SomeService) context.getBean("someService");
    String result = someService.doWork("李淳罡");
    //String result = someService.doWork(null);
    System.out.println("测试结果输出:"+result);
}

结果大家亲自体验,通过改变参数观察我们的通知代码执行逻辑,环绕通知并不等于 @Before + @AfterReturning ,环绕通知是可以修改目标方法的返回值的。

@AfterThrowing(了解)

目标方法抛出异常后执行,不抛异常则不执行(也就是说**如果目标方法自己try-catch 了异常,则通知方法是不会执行的)。下面是示例:

1、定义接口方法

void doOrder(Integer num);

为了验证结果,我们就要通过参数来制造异常,为了方便简单,容易理解,这里就在目标方法内采用 200/num ,如果参数为0,异常就出现了。

2、实现类,也就是目标类实现代码如下:

@Override
public void doOrder(Integer num) {
    System.out.println("目标方法内输出,如果参数为0,则计算(2022/num)会抛出异常");
    num = 2022 / num;
}

3、切面类,定义通知方法代码:

/**
 * 异常通知:目标方法抛出异常后调用,不抛出异常,则不调用
 * <p>
 * 作用:起到监控目标方法的作用,如果有异常了,方便开发人员定位问题,修复bug
 *
 * @param ex
 */
@AfterThrowing(value = "execution(public void com.javafirst.service.impl.SomeServiceImpl.doOrder(java.lang.Integer))", throwing = "ex")
public void aop_afterThrowing(Exception ex) {
    System.out.println("目标方法出现异常了,才会输出这句!\n异常信息:" + ex.getMessage());
}

以上就是核心代码了,结果各位自行测试哈。

@After(了解)

最终通知:目标方法返回或异常后调用,该通知方法始终会被调用到,适合做一些收尾工作,比如:清楚缓存、删除某些数据等。

1、定义接口方法

void payMoney(String address);

同上,我们通过方法的参数来“制造异常”。

2、实现类中代码:

@Override
public void payMoney(String address) {
    System.out.println("目标方法第一句内容,输出参数:" + address);
    System.out.println("截取地址的前三个字:" + (address.substring(0, 3)));
    //try {
    //    System.out.println("截取地址的前三个字:" + (address.substring(0, 3)));
    //}catch (Exception ex){
    //    System.out.println("目标方法有try-catch");
    //}
    System.out.println("目标方法异常后的输出语句.");
}

3、切面类中定义通知代码:

/**
 * 最终通知:一定会被执行到,且在目标方法之后
 */
@After(value = "execution(* *..SomeServiceImpl.payMoney(..))")
public void aop_after() {
    System.out.println("\n切面类中的输出内容!");
}

结果各位自己验证哈,这个不难理解,类似于Java中的try-catch-finally 中的 finally{} 代码块,总是会被执行到。

@Pointcut

  • 解决什么问题:我们在定义通知时,会在每个方法上面加注解,注解中有切入点表达式,而当我们定义的方法多了之后,需要改动路径或者方法名等其他时,就比较繁琐,还容易出错,那么 @Pointcut 注解就解决了这个问题。

  • 使用方式:定义一个方法,方法体内无需内容,在该方法上面加 @Pointcut 注解,你应该都想到了,该注解也是有 value 属性的,那么表达式就写在这里,以后要改就只改这个地方;而原来加注解通知方法的地方只需把切入点表达水的值改为这里定义的方法名即可(要带括号)

我们来演示一下,就以我们本文最后学的一个注解代码为例:

@After(value = "aop_pointcut()")
public void aop_after() {
    System.out.println("\naop_after() 后置通知:切面类中的输出内容!");
}

/**
 * 前置通知,为了测试 @Pointcut
 */
@Before(value = "aop_pointcut()")
public void aop_before_pointcut() {
    System.out.println("aop_before_pointcut() 前置通知:切面类中的输出内容!\n");
}

/**
 * 定义 @Pointcut 注解
 */
@Pointcut(value = "execution(* *..SomeServiceImpl.payMoney(..))")
private void aop_pointcut() {
}

测试代码无需修改,看结果就行,这个其实没什么难度,和我们之前学习动态SQL的时候,用到的 include 标签是一样的功能。

总结

看到这里,关于 Spring 的两大核心内容我们就学习结束了,后面还会学习两大内容:Spring 集成 MyBatisSpring事务。

  • Spring面向切面编程是一种编程思想,其实理解起来没有想象中那么难,尤其在你有java编程基础时。
  • 记住本文演示的 5 种通知注解,和其用法,以及代码执行流程,和那个阶段我们应该处理什么业务逻辑,这在以后会用到。

tips:本文的源代码在公众号推荐学java 中回复aop 即可查看。

学编程,推荐选 Java 语言,这是毋庸置疑的,如果你还对 Java 体系掌握不够,可以联系小编,跟着小编一起进阶!

看完记得点赞、评论支持小编哈,码字不易~