Spring AOP实践、自定义注解的使用

面试说道AOP都会说作用是对每层记录日志。但从来都没真正实践过。

场景:编写的业务,每个函数,特别是Controller和Dao层 想把 各个函数的参数记录到日志中,方便排查之后生产上的问题。

解决:使用AOP技术。具体实践如下:

文件目录结构:

 第一步:编写注解

记得引入aspect的依赖,文章最下方贴出来了。

package com.example.demo.anno;

import java.lang.annotation.*;

/*创建日志注解*/
@Retention(RetentionPolicy.RUNTIME) //指定注解的生命周期 在运行时
@Target(ElementType.METHOD)  // 指定注解只作用在 方法(函数)上
@Documented
@Inherited //父类写了该注解 则子类默认继承的话
public @interface Logs {   //@interface 注解的格式
    String action() default ""; //注解 可以接受的参数。
}

第二步:编写切入点的逻辑

package com.example.demo.anno;

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

/*切入每个函数的的入口且有@log注解标记的函数 调用该部分:功能打印函数参数*/
@Aspect
@Component
public class AOPOfLog {

    @Around(value = "within(com.example.demo.anno..*) && @annotation(logs)")
    public Object doLogs(ProceedingJoinPoint joinPoint,Logs logs) throws Throwable {

        System.out.println("打印注解的参数:"+logs.action());

        Object[] args = joinPoint.getArgs();//函数有几个参数,agrs这个数组就会有几个元素

        System.out.println("打印切入点的函数,传入的参数是:");
        for (int i = 0; i < args.length; i++) {
            //如果是字符串或者数字则直接打印,否则打印 对象.toStirng()
            if(args[i] instanceof String || args[i] instanceof Integer) {
                System.out.print(" " + args[i]);
            }else {
                System.out.print(" "+args[i].toString());
            }
        }

        //调用切入点 的函数  即:放行拦截
        Object obj = joinPoint.proceed();
        return obj; //如果返回void 则相当于没放行

    }

}

第三步:使用注解

package com.example.demo.anno;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/")
@ResponseBody
public class TestController {

    // post请求测试 http://localhost:8080/test1?parm1=1&parm2=2
    @Logs(action = "我是test1的注解")
    @RequestMapping("/test1")
    public  Object test1(String parm1,String parm2){
        return "成功了";
    }

    //post请求测试 http://localhost:8080/test2?name=王建伟&age=18
    @Logs(action = "我是test2的注解")
    @RequestMapping("/test2")
    public  Object  test2(User user){
        return "成功了";
    }
}

class  User{
    private String name;
    private int age;

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

现象结果:在访问包下的 com.example.demo.anno 的所有标记了 @Logs的函数 都会打印传入该函数的 参数值。

前端:

后台打印:

 总结:AOP就是在执行每个函数前、后、中 都可以“插入某个代码段”,也就是面试中常说的,将每个相同业务的公共部分抽取出来 进行统一处理,不仅提高了开发效率,还提高了日后代码可维护性(实现一处修改全局修改)

本项目的完整pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.mybatis</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>



    <repositories>
        <repository>
            <id>com.e-iceblue</id>
            <name>e-iceblue</name>
            <url>http://repo.e-iceblue.com/nexus/content/groups/public/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.12</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13</version>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>com.itextpdf</groupId>-->
<!--            <artifactId>itext-asian</artifactId>-->
<!--            <version>5.2.0</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.9.5</version>
        </dependency>

        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>

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

        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>

        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext7-core</artifactId>
            <version>7.1.12</version>
            <type>pom</type>
        </dependency>

        <dependency>
            <groupId> e-iceblue </groupId>
            <artifactId>spire.pdf</artifactId>
            <version>3.11.6</version>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>


            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.2</version>

            </plugin>
        </plugins>
    </build>

</project>

另一个版本的日志打印(使用:joinPoint.proceed()  存在bug。会多调用一次。)

借助:swagger的注解,在方法头上加 标题。形如

日志切面:

package com.XXX.product.aspect;

import com.patpat.product.helpers.LogAspectHelper;
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.springframework.stereotype.Component;
import org.springframework.util.StopWatch;


@Slf4j
@Aspect
@Component
public class LogAspect {

    @Pointcut("execution(public * com.XX.product.controller..*.*(..)) " +
            "&& !execution(public * com.XX.product.controller.XXController.*(..))")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {
        String title = LogAspectHelper.getMethodTitle(joinPoint);
        String param = LogAspectHelper.getMethodParam(joinPoint);
        String paramReturn = LogAspectHelper.getReturnResult(joinPoint);
        log.info("[Controller start], {}, param->{},", title, param);
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        try {
            return joinPoint.proceed();
        } finally {
            stopWatch.stop();
            log.info("[Controller end], {}, elapsed time->{}ms,resultParam->{}", title, stopWatch.getTotalTimeMillis(),paramReturn);
        }
    }

}

//切面拦截后所做的工作:

package com.XX.product.helpers;

import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;


public class LogAspectHelper {

    private LogAspectHelper() {
    }

    public static String getMethodParam(ProceedingJoinPoint joinPoint) {
        try {
            JsonMapper jsonMapper = new JsonMapper();
            return jsonMapper.writeValueAsString(Arrays.stream(joinPoint.getArgs())
                    .filter(param -> !(param instanceof HttpServletRequest)
                            && !(param instanceof HttpServletResponse)
                            && !(param instanceof MultipartFile)
                            && !(param instanceof MultipartFile[])
                    ).collect(Collectors.toList()));

        } catch (Exception e) {
            return "";
        }
    }

    public static String getMethodTitle(ProceedingJoinPoint joinPoint) {
        String title = "";
        try {
            Method[] methods = joinPoint.getSignature().getDeclaringType().getMethods();
            for (Method method : methods) {
                if (StringUtils.equalsIgnoreCase(method.getName(), joinPoint.getSignature().getName())) {
                    ApiOperation annotation = method.getAnnotation(ApiOperation.class);
                    if (ObjectUtils.isNotEmpty(annotation)) {
                        title = annotation.value();
                        break;
                    }
                }
            }
            if (StringUtils.isBlank(title)) {
                title = getMethodName(joinPoint);
            }
        } catch (Exception e) {
            title = "";
        }
        return title;
    }

    private static String getMethodName(ProceedingJoinPoint joinPoint) {
        return joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
    }

    public static String getReturnResult(ProceedingJoinPoint joinPoint) {
        Object proceed = null;
        try {
            proceed = joinPoint.proceed();//会导致 方法多调用一次
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        return JSONUtil.toJsonStr(proceed);
    }
}

使用方式:

 @ApiOperation("搜索-list")
    @PostMapping("/list")
    public ResponseDTO<OpenSearchQueryResult> XX(@RequestBody XXXhReq req){
        return openSearchService.XXX(req);
    }

每次调用/list 会打印:

[Controller start], openSearch搜索-list, param->[{XXX}]
。。。。。。其他业务日志
[Controller end], openSearch搜索-list, elapsed time->818ms,resultParam->{XXX}

可参考:(118条消息) springboot项目获取请求中的参数和返回值_小小逗比唐的博客-CSDN博客

AOP的5个注解:

本质:运用AOP的原理:5大方式

  1. 方法前执行                                     @Before
  2. 方法后执行                                     @AfterReturning
  3. 方法前后执行                                 @Around
  4. 方法执行抛出异常时执行               @AfterThrowing
  5. 方法执行后一定执行                      @After

其中,@AfterReturning是方法正常执行完后会执行,如果方法抛出异常则不会再执行

@After类似于try{}finally{}的finally一定会被执行

以上注解 都是针对"切面"进行的,所以我们还需要了解如何找切面,我们这里找切面类似于,正则表达式,匹配到的包、类、方法、权限修饰符 才被算 切点

如何写切点表达式呢?使用@PointCut("exexcution(XXXX)")

并且运用上面这5个注解呢?基本模板如下:

@Aspect
@Component
public class logUtils {
      @Pointcut("execution(public int caculator.Interface.MyCaculator.*(int,int))")
      public void test(){}


    @Before("test()")
    public static void logStart(JoinPoint joinPoint){
        System.out.println("logUtil前置方法【"+joinPoint.getSignature().getName()+"】开始执行" + "调用的参数是:" + Arrays.asList(joinPoint.getArgs()));
    }


    @AfterReturning(value = "test()",returning = "result")
    public void logReturn(JoinPoint joinPoint,Object result){
        System.out.println("logUtil返回方法【"+joinPoint.getSignature().getName()+"】执行完毕" + "返回的结果是:" + result);
    }

    @AfterThrowing(value= "test()",throwing = "exception")
    public void logException(JoinPoint joinPoint,Exception exception){
        System.out.println("logUtil异常方法【"+joinPoint.getSignature().getName()+"】出现异常" + "异常的原因是:" + exception);
    }

    @After("test()")
    public static void logAfter(JoinPoint joinPoint){
          System.out.println("logUtil结束方法【"+joinPoint.getSignature().getName()+"】结束执行");
      }
}      

猜你喜欢

转载自blog.csdn.net/u013372493/article/details/121340300