SpringBoot+AOP(@Around)

虽然SpringBoot很方便,可以使我们不太懂原理的情况下都可以轻松的写出一个CRUD的项目,但是SpringBoot的控制反转和依赖注入我们时时刻刻在用,可面向切面编程我们却不常用,接下来我们来一次简单的面向切面编程实现日志增强的例子(AOP的主要功能就是将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来)

pom文件导入依赖

<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.9</version>
        </dependency>

其实有更新的版本,但是不知道为什么,1.9.6版本我就是导入不成功,所以就退而求其次了。

二、新建一个@Annootation

目录右键新建JavaClass,选择@Annotation

package com.crud.aop;

import org.springframework.stereotype.Component;

import java.lang.annotation.*;

@Documented //这个其实个人感觉没啥用,主要是在javadoc上生成注解用的
@Target(ElementType.METHOD)//注解放置的目标位置,METHOD是可注解在方法级别上
/**
 *     1.CONSTRUCTOR:用于描述构造器
 *     2.FIELD:用于描述域
 *     3.LOCAL_VARIABLE:用于描述局部变量
 *     4.METHOD:用于描述方法
 *     5.PACKAGE:用于描述包
 *     6.PARAMETER:用于描述参数
 *     7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
 * **/
@Retention(RetentionPolicy.RUNTIME)
/**
 * 1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
 * 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
 * 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
 * 首先要明确生命周期长度SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,
 * 那只能用RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如ButterKnife),就用CLASS注解;如果只是做一些检查性的操作,
 * 比如@Override 和@SuppressWarnings,则可选用SOURCE 注解。
 * **/

public @interface LogAnnotation {
    String moudle() default ""; //模块
    String operate() default ""; //操作
}

三、创建切面类

package com.crud.aop;


import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonAlias;
import lombok.extern.slf4j.Slf4j;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
public class LogAspect {
    /**
     * 设置操作日志切入点 记录操作日志 在注解的位置切入代码
     *     @Pointcut("execution(* com.crud.controller.BlogController.getBlogById())")
     *     上面的这种是excution表达式,比如说我并不想开发一个专门的注解,我就想在某一个或者某一类的方法上添加这个aop,那就可以通过excution表达式来做
     * @Pointcut("execution(public * com.crud.controller.*.*(..))")
     * ..两个点表明多个,*代表一个, 上面表达式代表切入com.xhx.springboot.controller包下的所有类的所有方法,
     * 方法参数不限,返回类型不限。 其中访问修饰符可以不写,不能用*,,第一个*代表返回类型不限,第二个*表示所有类,第三个*表示所有方法,..两个点表示方法里的参数不限
     * 有一点非常重要,Spring的AOP只能支持到方法级别的切入。换句话说,切入点只能是某个方法
     */
    @Pointcut("@annotation(com.crud.aop.LogAnnotation)")
    public void logAspectPoint(){}
    @Around("logAspectPoint()")
    public Object log(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //前置
        long startTime= System.currentTimeMillis();
        /**
         * 环绕通知=前置+目标方法执行+后置,proceed方法就是用于启动目标方法执行的
         * Proceedingjoinpoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
         * 暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,,这也是环绕通知和前置、后置通知方法的一个最大区别。这跟切面类型有关)
         * */
        Object result=proceedingJoinPoint.proceed();
        //后置
        long time=System.currentTimeMillis()-startTime;
        recordLog(proceedingJoinPoint,time);
        return result;
    }
    public void recordLog(ProceedingJoinPoint proceedingJoinPoint,long time){
        //getSignature());是获取到这样的信息 :修饰符+ 包名+组件名(类名) +方法名
        MethodSignature methodSignature= (MethodSignature) proceedingJoinPoint.getSignature();
        Method method=methodSignature.getMethod();
        //getAnnotation:方法如果存在这样的注释,则返回指定类型的元素的注释,否则为null
        LogAnnotation logAnnotation=method.getAnnotation(LogAnnotation.class);
        log.info("==============================开始记录日志===============================");
        log.info("moudle:{}",logAnnotation.moudle());
        log.info("operato:{}",logAnnotation.operate());
        //proceedingJoinPoint.getTarget():获取切入点所在目标对象
        String className=proceedingJoinPoint.getTarget().getClass().getName();
        String methodName=methodSignature.getName();
        log.info("请求的方法是:{}",className+"."+methodName+"()");
        //这里返回的是切入点方法的参数列表
        Object[] args=proceedingJoinPoint.getArgs();
        String params= JSON.toJSONString(args[0]);
        log.info("请求的参数是:{}",params);
        log.info("执行时间总共为:{}",time);
        log.info("=================================end===================================");
    }
}

四、方法上添加注解

package com.crud.controller;
import com.crud.aop.LogAnnotation;
import com.crud.entity.Blog;
import com.crud.service.impl.BlogServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/crud")
@Slf4j
public class BlogController {
    @Autowired
    private BlogServiceImpl blogServiceImpl;
    @LogAnnotation(moudle = "博客",operate = "根据博客id获取博客详情")
    @RequestMapping(value = "/getById",method = RequestMethod.POST)
    public List<Blog>  getBlogById(@RequestParam(value = "id") Integer id){
        return blogServiceImpl.getBlogByUserId(id);
    }
}

五、查看执行结果

15:19:00.279 [http-nio-8082-exec-1] INFO  com.crud.aop.LogAspect - ==============================开始记录日志===============================
15:19:00.280 [http-nio-8082-exec-1] INFO  com.crud.aop.LogAspect - moudle:博客
15:19:00.280 [http-nio-8082-exec-1] INFO  com.crud.aop.LogAspect - operato:根据博客id获取博客详情
15:19:00.280 [http-nio-8082-exec-1] INFO  com.crud.aop.LogAspect - 请求的方法是:com.crud.controller.BlogController.getBlogById()
15:19:00.325 [http-nio-8082-exec-1] INFO  com.crud.aop.LogAspect - 请求的参数是:1
15:19:00.325 [http-nio-8082-exec-1] INFO  com.crud.aop.LogAspect - 执行时间总共为:875
15:19:00.325 [http-nio-8082-exec-1] INFO  com.crud.aop.LogAspect - =================================end===================================