基于AspectJ注解实现AOP

AOP前奏:AOP的相关理论介绍

1、Spring对AOP的支持

Spring提供了3种类型的AOP支持:

  • 基于AspectJ注解驱动的切面(推荐):使用注解的方式,这是最简洁和最方便的!
  • 基于XML的AOP:使用XML配置,aop命名空间
  • 基于代理的经典SpringAOP:需要实现接口,手动创建代理

2、AspectJ相关的注解

AspectJ相关注解:

  • @Aspect:标记这个类是一个切面类。

AspectJ增强相关注解:

注解 描述
@Before 表示将当前方法标记为前置通知
@AfterReturning 表示将当前方法标记为返回通知
@AfterThrowing 表示将当前方法标记为异常通知
@After 表示将当前方法标记为后置通知
@Around 表示将当前方法标记为环绕通知
@Pointcut 表示定义重用切入点表达式,一次定义,处处使用,一处修改,处处生效
@DeclareParents 表示将当前方法标记为引介通知(不要求掌握)

PointCut Designators 切点指示器),是切点表达式的重要组成部分

3、注解AOP的简单例子

①、编写代理对象接口

/**
 * 代理对象接口
 */
public interface IUserService {

    void addUser(String userName,Integer age);
}

②、编写代理对象接口的实现类

/**
 * 目标类,代理对象实现类,会被动态代理
 */
@Service
public class UserServiceImpl implements IUserService{

    @Override
    public void addUser(String userName, Integer age) {
        System.out.println(userName+":"+age);
    }
}

③、编写切面类

注意:AspectJ切入点表达式语法:execution(<访问修饰符>? <返回类型> <全限定名>? <方法名称>(<参数类型>) <异常>?),通过execution函数,可以定义切入的方法。

代码块中带?符号的匹配式都是可选的,对于execution必不可少的只有三个:

  • 返回类型
  • 方法名
  • 参数
/**
 * 创建日志切面类
 */
@Aspect // @Aspect注解标记这个类是一个切面类
@Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
public class LogAspect {  //定义一个日志切面类

    // @Before注解将当前方法标记为前置通知
    // value属性:配置当前通知的切入点表达式,通俗来说就是这个通知往谁身上套
    @Before(value = "execution(public void com.thr.aop.target.UserServiceImpl.addUser(String,Integer))")
    public void doBefore(JoinPoint joinPoint) { // 在通知方法中,声明JoinPoint类型的形参,就可以在Spring调用当前方法时把这个类型的对象传入

        // 1.通过JoinPoint对象获取目标方法的签名
        // 所谓方法的签名就是指方法声明时指定的相关信息,包括方法名、方法所在类等等
        Signature signature = joinPoint.getSignature();

        // 2.通过方法签名对象可以获取方法名
        String methodName = signature.getName();

        // 3.通过JoinPoint对象获取目标方法被调用时传入的参数
        Object[] args = joinPoint.getArgs();

        // 4.为了方便展示参数数据,把参数从数组类型转换为List集合
        List<Object> argList = Arrays.asList(args);

        System.out.println("[前置通知]"+ methodName +"方法开始执行,参数列表是:" + argList);
    }
}

④、编写配置文件

<?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 https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.thr.aop"/>
    <!-- 开启基于AspectJ注解的AOP功能 -->
    <aop:aspectj-autoproxy/>

</beans>

⑤、编写测试类

public class AOPTest {

    //创建ApplicationContext对象
    private ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

    @Test
    public void testAOP(){
        // 1.从IOC容器中获取接口类型的对象
        IUserService userService = ac.getBean(IUserService.class);

        // 2.调用方法查看是否应用了切面中的通知
        userService.addUser("张三",20);
    }
}

⑥、运行结果

基于AspectJ注解实现AOP

4、切入点表达式语法(重要)

在上面的例子中,切入点表达式是写死的,如果有很多地方要切入的话,就要在切面类中编写大量重复性的代码,扩展性和实用性不高,所以下面来学习一下更加强大的切入点表达式。

注意:AspectJ切入点表达式语法:execution(<访问修饰符>? <返回类型> <全限定名>? <方法名称>(<参数类型>) <异常>?),通过execution函数,可以定义切入的方法。代码块中带?符号的匹配式都是可选的,对于execution必不可少的只有三个:

  • 返回类型
  • 方法名
  • 参数

完整的传统切入点表达式:execution(public void com.thr.aop.target.UserServiceImpl.addUser(String,Integer))

上面最大可以简写为:execution(* *..*.*(..)) 表示匹配任意修饰符,返回值,包,类,方法,参数。

  • *号代替“权限修饰符”和“返回值”部分,表示“权限修饰符”和“返回值”不限,即任意类型,注意:这里一个*代表两部分,下面有介绍
  • 在包名的部分,使用*表示包名任意
  • 在包名的部分,使用*..表示包名任意、包的层次深度任意
  • 在类名的部分,使用*号表示类名任意,也可以可以使用*号代替类名的一部分,例如:
*Service

上面例子*Service表示匹配所有类名、接口名以Service结尾的类或接口(*号位置不限)

  • 在方法名部分,使用*号表示方法名任意,也可以使用*号代替方法名的一部分,例如:
*Operation

上面例子*Operation表示匹配所有方法名以Operation结尾的方法(*号位置不限)

  • 在方法参数列表部分,使用(..)表示参数列表任意
  • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头,后面的任意
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
execution(public int *..*Service.*(.., int))

上面例子是对的,而下面例子是错的:

execution(* int *..*Service.*(.., int))
  • 对于execution()表达式整体可以使用三个逻辑运算符号(了解,几乎不用)
    • execution() || execution()表示满足两个execution()中的任何一个即可
    • execution() && execution()表示两个execution()表达式必须都满足
    • !execution()表示不满足表达式的其他方法

AOP切入点表达式补充:

基于AspectJ注解实现AOP

上面相关函数的详细使用可以参考:spring aop中pointcut表达式完整版

4、重用切入点表达式

这里需要用到@Pointcut注解。在一处声明切入点表达式之后,在其它有需要的地方引用这个切入点表达式就好。易于维护,一处修改,处处生效。声明方式如下:

// 切入点表达式重用
@Pointcut("execution(* *..*.add*(..))")
public void doPointCut() {}

在同一个类内部引用时:

@Before(value = "doPointCut()")
public void doBefore(JoinPoint joinPoint) {

在不同类中引用:

@Before(value = "com.thr.aop.aspect.LogAspect.doPointCut")
public void doBefore(JoinPoint joinPoint) {

5、注解AOP的完整例子

基于前面简单的例子,除了切面类LogAspect代码需要改变之外,其它的类中代码都不变。

/**
 * 创建日志切面类
 */

@Aspect // @Aspect注解标记这个类是一个切面类
@Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
public class LogAspect {  //定义一个日志切面类

    // 使用@Pointcut注解重用切入点表达式
    // 当前类引用时:doPointCut()
    // 其他类引用时:com.thr.aop.aspect.LogAspect.doPointCut()
    @Pointcut(value = "execution(* *..*.add*(..))")
    public void doPointCut() {
    }

    // @Before注解将当前方法标记为前置通知
    // value属性:配置当前通知的切入点表达式,通俗来说就是这个通知往谁身上套
    @Before(value = "doPointCut()")
    public void doBefore(JoinPoint joinPoint) { // 在通知方法中,声明JoinPoint类型的形参,就可以在Spring调用当前方法时把这个类型的对象传入

        // 1.通过JoinPoint对象获取目标方法的签名
        // 所谓方法的签名就是指方法声明时指定的相关信息,包括方法名、方法所在类等等
        Signature signature = joinPoint.getSignature();

        // 2.通过方法签名对象可以获取方法名
        String methodName = signature.getName();

        // 3.通过JoinPoint对象获取目标方法被调用时传入的参数
        Object[] args = joinPoint.getArgs();

        // 4.为了方便展示参数数据,把参数从数组类型转换为List集合
        List<Object> argList = Arrays.asList(args);

        System.out.println("[前置通知]" + methodName + "方法开始执行,参数列表是:" + argList);
    }

    // @AfterReturning注解将当前方法标记为返回通知
    // 使用returning指定一个形参名,Spring会在调用当前方法时,把目标方法的返回值从这个位置传入
    @AfterReturning(value = "doPointCut()", returning = "returnValue")
    public void doAfterReturning(JoinPoint joinPoint, Object returnValue) {

        String methodName = joinPoint.getSignature().getName();

        System.out.println("[返回通知]" + methodName + "方法成功结束,返回值是:" + returnValue);
    }

    // @AfterThrowing注解将当前方法标记为异常通知
    // 使用throwing属性指定一个形参名称,Spring调用当前方法时,会把目标方法抛出的异常对象从这里传入
    @AfterThrowing(value = "doPointCut()", throwing = "throwable")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable throwable) {

        String methodName = joinPoint.getSignature().getName();

        System.out.println("[异常通知]" + methodName + "方法异常结束,异常信息是:" + throwable.getMessage());
    }

    // @After注解将当前方法标记为后置通知
    @After(value = "doPointCut()")
    public void doAfter(JoinPoint joinPoint) {

        String methodName = joinPoint.getSignature().getName();

        System.out.println("[后置通知]" + methodName + "方法最终结束");
    }
}

运行结果:

基于AspectJ注解实现AOP

小细节,通知执行的顺序

  • Spring版本5.3.x以前:
    • 前置通知
    • 目标操作
    • 后置通知
    • 返回通知或异常通知
  • Spring版本5.3.x以后:
    • 前置通知
    • 目标操作
    • 返回通知或异常通知
    • 后置通知

6、环绕通知的举例

环绕通知就是前面四个通知的结合,但Spring官方建议选用“能实现所需行为的功能最小的通知类型”: 提供最简单的编程模式,减少了出错的可能性。,本例在环绕通知中触发异常通知。

①、修改代理对象接口的实现类

/**
 * 目标类,会被动态代理
 */
@Service
public class UserServiceImpl implements IUserService {

    @Override
    public void addUser(String userName, Integer age) {
        //出现异常
        int i = 1;
        int j = 0;
        int x = i / j;
        System.out.println(userName + ":" + age);
    }
}

②、编写环绕通知切面类

/**
 * 创建日志环绕通知切面类
 */
@Aspect // @Aspect注解标记这个类是一个切面类
@Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
public class Log1Aspect {  //定义一个日志切面类

    // 使用@Pointcut注解重用切入点表达式
    // 当前类引用时:doPointCut()
    // 其他类引用时:com.thr.aop.aspect.LogAspect.doPointCut()
    @Pointcut(value = "execution(* *..*.add*(..))")
    public void doPointCut() {
    }

    // 使用表示当前方法是环绕通知
    @Around(value = "doPointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {

        // 获取目标方法名
        String methodName = joinPoint.getSignature().getName();

        // 声明一个变量,用来接收目标方法的返回值
        Object targetMethodReturnValue = null;

        // 获取外界调用目标方法时传入的实参
        Object[] args = joinPoint.getArgs();

        try {
            // 调用目标方法之前的位置相当于前置通知
            System.out.println("[环绕通知]" + methodName + "方法开始执行,参数列表:" + Arrays.asList(args));

            // 通过ProceedingJoinPoint对象的proceed(Object[] var1)调用目标方法
            targetMethodReturnValue = joinPoint.proceed();

            // 调用目标方法成功返回之后的位置相当于返回通知
            System.out.println("[环绕通知]" + methodName + "方法成功返回,返回值是:" + targetMethodReturnValue);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            // 调用目标方法抛出异常之后的位置相当于异常通知
            System.out.println("[环绕通知]" + methodName + "方法抛出异常,异常信息:" + throwable.getMessage());
        } finally {
            // 调用目标方法最终结束之后的位置相当于后置通知
            System.out.println("[环绕通知]" + methodName + "方法最终结束");
        }

        // 将目标方法的返回值返回
        // 这里如果环绕通知没有把目标方法的返回值返回,外界将无法获取这个返回值数据
        return targetMethodReturnValue;
    }
}

③、运行结果

基于AspectJ注解实现AOP

7、切面的优先级

[1]概念:相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

基于AspectJ注解实现AOP

[2]实际意义:实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。

基于AspectJ注解实现AOP

此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。

基于AspectJ注解实现AOP


参考资料:

上一篇:java-将Ajc编译器与Spring问题AspectJ一起使用


下一篇:java-Spring AOP-未调用切入点/拦截器