【SpringBoot应用篇】SpringBoot集成Aspect AOP面向切面编程

【SpringBoot应用篇】SpringBoot集成Aspect AOP面向切面编程

  • 回顾AOP
  • Spring AOP和Aspect AOP
  • 常用注解使用
    • @Aspect
    • @Pointcut
    • @Before
    • @AfterReturning
    • @AfterThrowing
    • @After
    • @Around
      • 获取请求参数
      • 修改请求参数
      • 修改返回值
  • 基于注解的切入点测试
    • pom
    • 自定义注解
    • 定义一个切面
    • controller
    • 启动类
  • JointPoint使用详解
    • 1、获取切入点所在目标对象
    • 2、获取切入点方法的名字
    • 3、获取方法上的注解
    • 4、获取方法的参数

回顾AOP

aop,又叫面向切面编程,通俗理解就是,将那些与业务无关,却为业务模块所共同调用的逻辑代码封装起来,形成一个切面,减少重复代码,降低模块间的耦合度,方便后期操作和维护。

Spring AOP和Aspect AOP

Spring AOP属于运行时的增强,而Aspect AOP属于编译时的增强。SpringAOP是基于代理(Proxying),而AspectAOP是基于字节码操作。如果切面比较少,两者差不多,如果切面太多,最好使用AspectAOP,它比SpringAOP快很多。

使用spring-boot-starter-aop启动器默认帮我们引入了对Aspectj的实现
在这里插入图片描述

常用注解使用

 @Aspect -- 作用是把当前类标识为一个切面供容器读取
 @Pointcut -- (切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
 @Before -- 标识一个前置增强方法,相当于BeforeAdvice的功能
 @AfterReturning -- 后置增强,相当于AfterReturningAdvice,方法退出时执行
 @AfterThrowing -- 异常抛出增强,相当于ThrowsAdvice
 @After -- final增强,不管是抛出异常或者正常退出都会执行
 @Around -- 环绕增强,相当于MethodInterceptor

@Aspect

@Aspect – 作用是把当前类标识为一个切面供容器读取

@Aspect
@Component
@Slf4j
public class LogAspect {
}

@Pointcut

@Pointcut -- (切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式或者基于注解的切入点

基于注解的切入点

@Pointcut("@annotation(cn.zysheep.annotation.Log)")
//@annotation(cn.zysheep.annotation.Log),为自定义注解
public void Pointcut() {}

基于切入点表达式
execution()是最常用的切点函数

@Pointcut("execution(* cn.zysheep.springaop.service.impl..*.*(..))")
public void Pointcut() {}

整个表达式可以分为四个部分:
1. 第一个*号:表示返回类型, *号表示所有的类型
2. 包名: 表示需要拦截的包名,后面的..表示当前包和当前包的所有子包,cn.zysheep.springaop.service.impl包、子孙包下所有类的方法。
3. 第二个*: 表示类名,*号表示所有的类。
4. *(..):最后这个星号表示方法名, *号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数

// cn.zysheep.controller包中所有的类的所有方法切面
// @Pointcut("execution(public * cn.zysheep.controller.*.*(..))")

// 只针对 UserController 类切面
// @Pointcut("execution(public * cn.zysheep.controller.UserController.*(..))")

// 统一切点,对cn.zysheep及其子包中所有的类的所有方法切面
// @Pointcut("execution(* cn.zysheep.controller.*.*(..))")

个人认为基于注解的方式可以实现更加细粒度的操作,比如日志管理,只要自己声明一个自定义的注解@Log,可以在任意三层中定义到不同的方法中

@Before

标识一个前置增强方法,相当于BeforeAdvice的功能

//@Before: 前置通知 ,Pointcut()定义的切入点函数
 @Before("Pointcut()")
 public void beforeMethod() {
     log.info("调用了前置通知");
 }

@AfterReturning

后置增强,相当于AfterReturningAdvice,方法退出时执行

 //@AfterRunning: 返回通知    result为切入点返回内容
@AfterReturning(value="Pointcut()",returning="result")
public void afterReturningMethod(JoinPoint joinPoint,Object result){
    log.info("调用了返回通知,result :{}",result);
}

@AfterThrowing

异常抛出增强,相当于ThrowsAdvice

//@AfterThrowing: 异常通知  e为切入点执行异常的信息
 @AfterThrowing(value="Pointcut()",throwing="e")
 public void doAfterThrowing(JoinPoint joinPoint, Exception e){
     log.info("调用了异常通知  joinPoint:{},e :{}",joinPoint, e.getStackTrace());
 }

@After

final增强,不管是抛出异常或者正常退出都会执行

//@After: 后置通知
@After("Pointcut()")
public void afterMethod(JoinPoint joinPoint){
	//joinPoint: 当前的连接点,即执行的切入点
    log.info("调用了后置通知,joinPoint:{}",joinPoint);
}

@Around

@Around功能非常强大,作用如下:

  • 可以在目标方法之前增加逻辑,也可以在执行目标方法之后增加逻辑.
  • 可以决定目标方法在什么时候执行,如何执行,也可以阻止目标目标方法执行.
  • 可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值.
//@Around: 环绕通知
@Around("Pointcut()")
public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    log.info("around执行方法之前");
    // 执行方法
    Object object = proceedingJoinPoint.proceed();
    // 执行时长(毫秒)
    long time = System.currentTimeMillis() - beginTime;
    log.info("around执行方法之后--返回值: " +object);
    return object;
}

获取请求参数

1、通过RequestContextHolder获取url中的参数,RequestContextHolder就是一个典型的ThreadLocal应用,用于在当前线程中获取当前请求及其属性,如果要在service层中使用request,或者其他任何地方,都可以直接调用RequestContextHolder来获取request对象和response对象。
2、 通过 joinPoint.getArgs()获取请求参数数组

@Before("BrokerAspect()")
public void doBefore(JoinPoint joinPoint){
   ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
   logger.info("URL参数={}",requestAttributes.getRequest().getQueryString());

}

修改请求参数

业务场景: 生产环境用户的隐私数据,比如手机号、身份证或者一些账号配置等信息,入库时都要进行不落地脱敏,也就是在进入我们系统时就要实时的脱敏处理。

用户数据进入系统,脱敏处理后持久化到数据库,用户查询数据时还要进行反向解密。这种场景一般需要全局处理,那么用AOP切面来实现在适合不过了。

使用ProceedingJoinPoint时如果要改变参数,必须调用 proceed(Object[] var1)方法,传入新的参数数组,数组元素类型必须和目标方法相互对应,否则会报ClassCastException异常。

【SpringBoot应用篇】SpringBoot集成Aspect AOP面向切面编程_第1张图片

@Slf4j
@Aspect
@Component
public class EncryptHandler {

    @Autowired
    private StringEncryptor stringEncryptor;

    @Pointcut("@annotation(cn.zysheep.annotation.EncryptMethod)")
    public void pointCut() {
    }

    /**
     * 1、获取参数进行加密
     * 2、执行目标方法,业务操作加密数据
     * 3、返回前端为解密数据
     * @param joinPoint
     * @return
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        /**
         * 1、获取参数进行加密
         */
        Object[] encrypt = encrypt(joinPoint);
		/**
         * 2、修改请求参数,执行目标方法,业务操作加密数据
         */
        Object result = joinPoint.proceed(encrypt); // 目标方法返回值
        /**
         * 3、修改返回值,返回前端为解密数据
         */
        Object decrypt = decrypt(result);
        return decrypt;
    }
    public Object[] encrypt(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();

        if (args.length != 0) {
            for (int i = 0; i < args.length; i++) {
                Object o = args[i];
                if (o instanceof String) {
                    args[i] = stringEncryptor.encrypt(String.valueOf(o));
                }
            }
        }

        return args;
    }

    public Object decrypt(Object result) {
        String decrypt = null;
        if (result instanceof String) {
             decrypt = stringEncryptor.decrypt(String.valueOf(result));
        }
        return decrypt;
    }
}
@EncryptMethod
@GetMapping("/getParam")
public String getParam( @EncryptField String username) {
    System.out.println("保存数据库业务操作===>username: "+username);
    return username;
}

【SpringBoot应用篇】SpringBoot集成Aspect AOP面向切面编程_第2张图片

修改返回值

proceed方法的返回值就是目标方法的返回值,我们可以拿到他做一些修改,或者返回新的数据。
【SpringBoot应用篇】SpringBoot集成Aspect AOP面向切面编程_第3张图片

基于注解的切入点测试

【SpringBoot应用篇】SpringBoot集成Aspect AOP面向切面编程_第4张图片

pom

<parent>
     <groupId>org.springframework.bootgroupId>
     <artifactId>spring-boot-starter-parentartifactId>
     <version>2.4.0version>
parent>
 
<dependencies>
	<dependency>
	    <groupId>org.springframework.bootgroupId>
	    <artifactId>spring-boot-starter-webartifactId>
	dependency>
	
	<dependency>
	    <groupId>org.springframework.bootgroupId>
	    <artifactId>spring-boot-starter-aopartifactId>
	dependency>
	
	<dependency>
	    <groupId>org.projectlombokgroupId>
	    <artifactId>lombokartifactId>
	dependency>
dependencies>

自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    //模块名
    String value() default "";
}

定义一个切面

@within和@annotation的区别:

  • @within 对象级别
  • @annotation 方法级别
@Around("@within(cn.zysheep.annotation.Log") :这个用于拦截标注在类上面的@Log注解

@Around("@annotation(cn.zysheep.annotation.Log") :这个用于拦截标注在方法上面的@Log注解
@Aspect
@Component
@Slf4j
public class LogAspect {

    long beginTime;

    @Pointcut("@annotation(cn.zysheep.annotation.Log)")
    public void Pointcut() {}

    @Before("Pointcut()")
    public void beforeMethod() {
        log.info("调用了前置通知@Before");
        beginTime = System.currentTimeMillis();
    }

    @AfterReturning(value="Pointcut()",returning="result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result){
        log.info("调用了返回通知@AfterReturning,result :{}",result);
    }

    @AfterThrowing(value="Pointcut()",throwing="e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e){
        log.info("调用了异常通知@AfterThrowing  joinPoint:{},e :{}",joinPoint,  e.getMessage());
    }

    @After("Pointcut()")
    public void afterMethod(JoinPoint joinPoint){
        //joinPoint: 当前的连接点,即执行的切入点
        log.info("调用了后置通知@After,joinPoint:{}",joinPoint);
    }

    @Around("Pointcut()")
    public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("@Around执行方法之前");
        // 执行方法
        Object object = proceedingJoinPoint.proceed();
        // 执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        log.info("@Around执行方法之后--返回值: " +object);
        return object;
    }
}

controller

@RestController
@Slf4j
public class TestAopController {

    @GetMapping("/save")
    @Log("保存数据")
    public String save() {
        log.info("执行了controller中的save方法");
        return "保存数据";
    }
}

启动类

@SpringBootApplication
public class AOPApplication {
    public static void main(String[] args) {
        SpringApplication.run(AOPApplication.class,args);
    }
}

在这里插入图片描述
执行流程:

  1. @Around前置增强
  2. @Before前置通知
  3. 执行了controller中的save方法
  4. @AfterReturning返回通知
  5. @After后置通知
  6. @Around后置增强

如果切入点出现异常:
【SpringBoot应用篇】SpringBoot集成Aspect AOP面向切面编程_第5张图片
在这里插入图片描述
执行流程:

  1. @Around前置增强
  2. @Before前置通知
  3. 执行了controller中的save方法,如果异常在之前执行,不会输出
  4. @AfterThrowing异常通知
  5. @After后置通知

JointPoint使用详解

1、获取切入点所在目标对象

Object targetObj =joinPoint.getTarget();

// 可以发挥反射的功能获取关于类的任何信息,例如获取类名如下
String className = joinPoint.getTarget().getClass().getName();

2、获取切入点方法的名字

getSignature():是获取到这样的信息 :修饰符+ 包名+组件名(类名) +方法名

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

3、获取方法上的注解

Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();

// 获取方法上的注解
Log annoObj= method.getAnnotation(Log.class);

4、获取方法的参数

Object[] args = joinPoint.getArgs();

你可能感兴趣的:(#,SpringBoot,#,Spring全家桶,aop,springboot)