Spring注解驱动之AOP环境

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

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

面向切面编程实现:通过动态代理(Java动态代理或者Gglib动态代理)方式实现

  • 什么是代理模式?Java中实现:静态代理和动态代理
  • Java动态代理和Cglib的动态代理的区别?
    • Java动态代理:必须存在接口和实现类(符合编程习惯,面向接口开发,建立约定),代理类必须实现
    • Code Generation Library:用类实现代理,对执行目标类(被代理类)动态生成一个子类,子类将重写父类的方法,有上述的约定,那么目标类必须可以被继承,所以目标类不能使用final修饰
  • 代理对象=目标对象(被代理对象)+增强代码

总之一句话:AOP是指在程序的运行期间动态的将某段代码切入到指定方法、指定位置进行运行的编程方式。AOP的底层是使用动态代理实现的。个人理解,在不改变原来的代码基础增强新的功能。

搭建环境

1.导入AOP依赖

要想搭建AOP环境,首先,我们就需要在项目的pom.xml文件中引入AOP的依赖,如下所示,我使用的模块开发,版本继承于父模块

<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
  </dependency>
</dependencies>
复制代码

2.定义目标类

com.hanpang.service.MathService包下创建一个MathService类,用于处理数学计算上的一些逻辑。比如,我们在MathService类中定义了一个加法操作,返回两个整数类型值的和,如下所示:

package com.hanpang.service;

public class MathService {
    public int sum(int i,int j){
        System.out.println("目标方法执行");
        return i+j;
    }
}
复制代码

3.定义切面类

com.hanpang.aspect包下创建一个LogAspect切面类:

package com.hanpang.aspect;

import org.aspectj.lang.annotation.Aspect;

/**
 * 打造切面类(包裹目标类)
 */
//告知Spring容器,该类是切面类
@Aspect
public class LogAspect {
}
复制代码

4.将目标类和切面类键入到IoC容器中,但是他们之间还没有产生联系

com.hanpang.config包中,新建AopConfig类,并使用@Configuration注解标注这是一个Spring的配置类,同时使用@EnableAspectJAutoProxy注解开启基于注解的AOP模式。在SpringConfig类中,使用@Bean注解将MathService类和LogAspect类加入到IOC容器中,如下所示:

package com.hanpang.config;

import com.hanpang.aspect.LogAspect;
import com.hanpang.service.MathService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**@Configuration配置类*/
@Configuration
/**开启AOP的注解模式,可以视同AspectJ第三方实现的注解*/
@EnableAspectJAutoProxy
public class SpringConfig {
    /**
     * 目标对象
     */
    @Bean
    public MathService mathService(){
        return new MathService();
    }

    /**
     * AOP对象,因为在LogAspect使用@Aspect注解
     */
    @Bean
    public LogAspect logAspect(){
        return new LogAspect();
    }
}

复制代码

5.通过表达式建立联系:实际就是通知什么时候什么地方执行什么方法?

package com.hanpang.aspect;

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

/**
 * 打造切面类(包裹目标类)
 */
//告知Spring容器,该类是切面类
@Aspect
public class LogAspect {
  /*
    * 切面类中,有提供AOP注解和表达式
    * */

  /**=====完整的标注写法:增强代码要跟MathServce类的sum方法产生反应======*/
  /**修饰符[可省略] 返回值类型 目标类全路径.方法名称(形参个数和类型))*/
  @Before("execution(public int com.hanpang.service.MathService.sum(int,int))")
  public void logStart(){
    System.out.println("加法运行开始之前,增加了功能");
  }
}
复制代码

6.创建测试类

在 com.hanpang.test包中创建AopTest测试类,并在AopTest类中创建testAop01()方法,如下所示。

package io.mykit.spring.test;
import com.hanpang.service.MathService;
import com.hanpang.aspect.AopConfig;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest {

    @Test
    public void testAop01(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
        MathService mathService = context.getBean(MathService.class);
        mathService.add(1, 2);
        context.close();
    }
}
复制代码

运行AopTest类中的testAop01()方法,输出的结果信息如下所示。

加法运行开始之前,增加了功能
目标方法执行
复制代码

可以看到,执行了切面类中的方法,并打印出了相关信息。但是没有打印参数列表和运行结果。

7.切面类中获取参数列表

那如果需要打印出参数列表和运行结果,该怎么办呢?别急,我们继续往下看。

要想打印出参数列表和运行结果,就需要对LogAspect类中的方法进行优化,优化后的结果如下所示。

/*
    返回值类型使用:通配符*,代表任意的返回值类型
    com.hanpang..service: 代表以com.hanpang开头并且以service包结尾,中间可以包含任意的层次
    *Service:目标类必须是以为Service结尾的类
    方法:通配符*,代表该类下的所有方法
    形参中:使用..代表是任意个形参个数和类型
 */
@Before("execution(* com.hanpang..service.*Service.*(..))")
public void logStart02(JoinPoint joinPoint){
  System.out.println("增强代码:获取参数");
  System.out.println("参数:");
  Arrays.stream(joinPoint.getArgs()).forEach(System.out::println);
  System.out.println("增强的方法:"+joinPoint.getSignature().getName());
  System.out.println("增强的目标类:"+joinPoint.getTarget().getClass().getName());
}
复制代码

这里,需要注意的是:JoinPoint参数一定要放在参数的第一位。

至此,我们的AOP测试环境就搭建成功了。

通知类型

通过代码来描述一下通知类型

package com.hanpang.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**当我容器创建对象,不确定是哪个层次的时候,就使用Component注解,标注该组件进行实例化*/
@Component
@Aspect//标注该类是切面类
public class LogAdviceAspect {
    //通过表达式定义切面,在什么地方
    @Pointcut("execution(public int com.hanpang.service.MathService.sum(int,int))")
    public void pointCutSum(){}
    @Pointcut("execution(public int com.hanpang.service.MathService.div(int,int))")
    public void pointCutDiv(){}
    @Pointcut("execution(public int com.hanpang.service.MathService.sum(int,int)) || execution(public int com.hanpang.service.MathService.div(int,int))")
    public void pointCutAA(){}
    @Pointcut("pointCutSum() || pointCutDiv()")
    public void pointCut(){}
    //在什么地方的什么时候做什么
    //告知在什么时候执行
    @Before("pointCut()")
    public void logStart(JoinPoint jp){
        //增强代码:做了什么
        System.out.println("前置通知:无论执行代码是否正确,都会在执行代码之前,执行");
    }

    @After("pointCut()")
    public void logEnd(JoinPoint jp){
        //增强代码:做了什么
        System.out.println("后置通知:无论执行代码是否正确,都会在执行代码之后,执行");
    }

    @AfterReturning(value="pointCut()",returning = "result")
    public void logReturn(JoinPoint jp,int result){
        //增强代码:做了什么
        System.out.println("返回通知:执行代码是正确,执行,"+result);
    }
    @AfterThrowing(value="pointCut()",throwing = "ex")
    public void logException(JoinPoint jp,Exception ex){
        //增强代码:做了什么
        System.out.println("异常通知:执行代码出现异常,执行,"+ex.getMessage());
    }

}
复制代码
package com.hanpang.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class AroundAdviceAspect {
    @Pointcut("execution(* com.hanpang..service.*Service.*(..))")
    public void pointCut(){}
    /**
     * 环绕通知必须有返回值类型,包含了刚才我们使用所有通知类型
     * */
    @Around("pointCut()")
    public Object aroundAdviceLog(ProceedingJoinPoint proceedingJoinPoint){
        Object result = -1;
        System.out.println("前置通知....前置增减...想想一下,调用了别方法,写了一些乱七八糟的代码");
        System.out.println(Arrays.asList(proceedingJoinPoint.getArgs()));
        try {
            result = proceedingJoinPoint.proceed();
            System.out.println("后置通知...后置增强...."+result);
            System.out.println(proceedingJoinPoint.getTarget().getClass().getName());
        } catch (Throwable e) {
            System.out.println("异常通知...异常增强...."+e.getMessage());
        }
        System.out.println("后置通知...后置增强..如果异常处理进行throw new Exception或者return处理,不会执行");

        return result;

    }
}
复制代码

专业术语

image-20211112203312931.png

通知(Advice):包含了需要用于多个应用对象的横切行为(增强的方法),“什么时候”和“做什么”

连接点(Join Point):是程序执行过程中通知哪些点(方法)

切点(PointCut):是定义在“什么地方”进行切入,哪些连接点会得到通知

切面(Aspect):是通知和切点的结合,通知和切点共同定义了切面的全部内容,“是什么”

增强的方法方式,可以通过引入和织入的方式进行代理

  • 引入(Introduction):允许我们向现有的类中添加新方法或者属性
  • 织入(Weaving):是包切面应用到目标对象并且创建新的代理对象的过程,(编译器织入、类加载期织入和运行期织入)

おすすめ

転載: juejin.im/post/7032071782236422174