Spring AOP 入门


#Spring


建议阅读 关于 Spring AOP (AspectJ) 你该知晓的一切,总结的很好。本文代码部分参考了这篇文章。

示例1:使用XML配置切面

项目结构

.
├── build.gradle
└── src
    ├── main
        ├── java
        │   └── demo
        │       ├── CustomInterrupter.java
        │       ├── Main.java
        │       └── SayHello.java
        └── resources
            └── beans.xml

build.gradle

plugins {
    id 'java'
}

group 'com.example'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8


dependencies {
    implementation group: 'org.springframework', name: 'spring-core', version: '5.0.6.RELEASE'
    implementation group: 'org.springframework', name: 'spring-context', version: '5.0.6.RELEASE'
    implementation group: 'org.springframework', name: 'spring-aop', version: '5.0.6.RELEASE'
    implementation group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.1'

    testImplementation group: 'junit', name: 'junit', version: '4.12'
}

SayHello.java

编写类SayHello:

package demo;

public class SayHello {

    public String sayHello(String msg) {
        System.out.println("正在执行sayHello,收到msg:"+msg);
        return "Hello " + msg;
    }

}

CustomInterrupter.java

编写切面类:

package demo;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class CustomInterrupter {

    public void before(JoinPoint joinPoint){
        System.out.println("我是前置通知");
    }

    public void afterReturning(JoinPoint joinPoint,Object returnVal){
        System.out.println("我是后置通知...");
    }

    public void after(JoinPoint joinPoint) {
        System.out.println("最终通知....");
    }

    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("我是环绕通知前....");
        //执行目标函数
        Object obj= (Object) joinPoint.proceed();
        System.out.println("我是环绕通知后....");
        return obj;
    }

    public void afterThrowing(Throwable throwable){
        System.out.println("异常通知:"+ throwable.getMessage());
    }

}

beans.xml

在 resources 目录添加beans.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"
       xmlns:context="http://www.springframework.org/schema/context"
       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">

    <context:component-scan base-package="demo"/>

    <bean id="sayHello" class="demo.SayHello"/>

    <bean id="customInterrupter" class="demo.CustomInterrupter"/>

    <aop:config>
        <!--定义切点(切入位置)-->
        <aop:pointcut id="pointcut" expression="execution(* demo.SayHello.sayHello(..))" />

        <aop:aspect ref="customInterrupter" order="0">
            <aop:before method="before" pointcut-ref="pointcut" />
            <aop:after method="after" pointcut-ref="pointcut" />
            <aop:after-returning method="afterReturning" pointcut-ref="pointcut"  returning="returnVal" />
            <aop:around method="around" pointcut-ref="pointcut" />
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="throwable"/>
        </aop:aspect>

    </aop:config>

</beans>

Main.java

Main类:

package demo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        SayHello sayHello = (SayHello) ctx.getBean("sayHello");
        String result = sayHello.sayHello("World");

        System.out.println("结果:" + result);
    }

}

执行结果

执行 Main类,输出:

我是前置通知
我是环绕通知前....
正在执行sayHello,收到msg:World
我是环绕通知后....
我是后置通知...
最终通知....
结果:Hello World

另外,环绕通知是可以拦截并修改返回值的。例如把 around 函数改成:

    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("我是环绕通知前....");
        //执行目标函数
        Object obj= (Object) joinPoint.proceed();
        System.out.println("我是环绕通知后....");
        return "拦截返回值";  // 这个返回值类型必须和原函数类型一致,或者可转换,否则会抛出异常
    }

执行Main类,将输出:

我是前置通知
我是环绕通知前....
正在执行sayHello,收到msg:World
我是环绕通知后....
我是后置通知...
最终通知....
结果:拦截返回值

示例2:使用注解配置切面

代码类似示例1。

项目结构

.
├── build.gradle
└── src
    ├── main
        ├── java
        │   └── demo
        │       ├── CustomInterrupter.java
        │       ├── Main.java
        │       ├── Main2.java
        │       └── SayHello.java
        └── resources
            └── beans.xml

beans.xml

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

    <context:component-scan base-package="demo"/>

    <aop:aspectj-autoproxy />

    <bean id="sayHello" class="demo.SayHello"/>

    <bean id="customInterrupter" class="demo.CustomInterrupter"/>

</beans>

CustomInterrupter.java

CustomInterrupter 类中加上注解:

package demo;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;

@Aspect
@Order(0)  // 值越小,优先级越高
public class CustomInterrupter {

    // 定义切点(切入位置)
    @Pointcut("execution(* demo.SayHello.*(..))")
    private void pointcut(){}

    @Before("pointcut()")
    public void before(JoinPoint joinPoint){
        System.out.println("我是前置通知");
    }

    @AfterReturning(value="pointcut()", returning = "returnVal")  // 别忘了returning参数
    public void afterReturning(JoinPoint joinPoint, Object returnVal){
        System.out.println("我是后置通知...,收到的returnVal: " + returnVal);
    }

    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("最终通知....");
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("我是环绕通知前....");
        //执行目标函数
        Object obj= (Object) joinPoint.proceed();
        System.out.println("我是环绕通知后....");
        return obj;
    }

    @AfterThrowing(value="pointcut()", throwing="throwable")   // 别忘了throwing参数
    public void afterThrowing(JoinPoint joinPoint, Throwable throwable){
        System.out.println("异常通知:"+ throwable.getMessage());
    }

}

@After("pointcut()")改成@After("execution(* demo.SayHello.*(..))")也可以。

执行结果

运行 Main 类,输出:

我是环绕通知前....
我是前置通知
正在执行sayHello,收到msg:World
我是环绕通知后....
最终通知....
我是后置通知...,收到的returnVal: Hello World
结果:Hello World

对于后置通知:

    @AfterReturning(value="pointcut()", returning = "returnVal")
    public void afterReturning(JoinPoint joinPoint, Object returnVal){
        System.out.println("我是后置通知...,收到的returnVal: " + returnVal);
    }

如果不关心返回值,也可以改成:

    @AfterReturning(value="pointcut()") 
    public void afterReturning(JoinPoint joinPoint){
        System.out.println("我是后置通知...");
    }

示例3:处理异常

在示例2中,下面这个切面函数并没有用到:

    @AfterThrowing(value="pointcut()", throwing="throwable")   // 别忘了throwing参数
    public void afterThrowing(JoinPoint joinPoint, Throwable throwable){
        System.out.println("异常通知:"+ throwable.getMessage());
    }

我们看下,它是在什么位置被触发的。

首先,在 SayHello 类中增加函数:

public String sayHello2(String msg) throws Exception {
    System.out.println("正在执行sayHello,收到msg:"+msg);
    if (true) {
        throw new Exception("出现错误");
    }
    return "Hello " + msg;
}

编写 Main2 类:

package demo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main2 {

    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        SayHello sayHello = (SayHello) ctx.getBean("sayHello");
        String result = sayHello.sayHello2("World");

        System.out.println("结果:" + result);
    }

}

运行后输出:

我是环绕通知前....
我是前置通知
正在执行sayHello,收到msg:World
最终通知....
异常通知:出现错误
Exception in thread "main" java.lang.Exception: 出现错误
	at demo.SayHello.sayHello2(SayHello.java:13)
	at demo.SayHello$$FastClassBySpringCGLIB$$b5a76f72.invoke(<generated>)


( 本文完 )