咖啡汪推荐————使用AOP实现日志记录,包括bug点,bug讲解(源码在汪哥Github上,自行下载)

本汪作为一名资深的哈士奇
每天除了闲逛,拆家,就是啃博客了
作为不是在戏精,就是在戏精的路上的二哈
今天就来给大家说说前后端分离项目-AOP如何实现日志记录
让我们一起傻嗨浪
告诉大家个秘密————文章最后会有bug讲解哦,

本汪意在使大家能够自己定义AOP注解,而不是只会用别人的,所以讲的会比较贴底层,有些底层,不想看都可以直接跳过,不影响代码复用

在这里插入图片描述

( 一 ) AOP介绍

本汪介绍下:其实她的目的就两个字----省事

本汪说下:基本的概念必须要有所了解,所以下面的介绍,仔细看一下
Spring AOP,“面向切面编程”的简称,建立在IOC基础之上,“权限校验”“日志记录”等服务模块,都可以通过AOP进行解耦,这样我们就可以把更多的精力投入到业务代码中了。

1、概念:面向切面编程:切面Aspect = 切点PointCut + 通知Advisor
2、出现的缘由一个应用系统的业务逻辑一般由两大部分组成 = 核心业务逻辑 + 通用的逻辑
基于spring aop可以将 这一部分 “通用的逻辑” 分离出来,降低与核心业务逻辑的耦合性、让开发者更加专注于业务

3、相关专有名词的概念

切面 Aspect:一个关注点的模块化,比如我们关注点是 “日志” 记录,那么我们将单独将 “日志”这一块抽出来,模块化... 切面用Spring的Advisor或拦截器实现。

连接点 JoinPoint:程序执行过程中明确的点,在这里我们一般指的是 “方法”。

切入点 Pointcut :指定一个通知将被引发的一系列连接点的集合。比如 我们在这里引发通知的方式是通过 指定的 注解.

通知 Advice:在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。

目标对象(被代理的对象):包含连接点的对象,其实就是 某个 类
AOP代理对象:AOP框架创建的对象,包含通知(在Spring中,AOP代理可以是JDK动态代理或CGLIB代理)

( 二 ) 思维导图+源码

本汪来了:看完这些枯燥的概念,本汪带大家一起看看他的思维导图,其实结合思维导图和源码,就可以解决60%的问题了
依照惯例,先上思维导图和源码链接
在这里插入图片描述
源码地址:https://github.com/HuskyYue/ConvenienceServices
这个本汪说下,这是本汪自己的一个开源项目Demo,专门提供给大家学习用,内部有RabbitMQ日志记录,邮件发送,基于Redis发布订阅的商户通知等,有需要可以看本汪相关博客

( 三 )使用AOP进行日志记录

本汪带大家一起来布置一下,随便说下配置原因

  • (1). 先自定义一个注解,名字只要符合规范就行
@Target(ElementType.METHOD)//触发对象为方法
@Retention(RetentionPolicy.RUNTIME)//运行时做持久化
@Documented //java.lang.annotation注解
public @interface LogAopAnnotation {
    
    
    String value() default "";//操作名称-新增,更新,删除
    String operatorName() default "";
    String operatorTable() default "";//所操作的表
}

@Target(ElementType.METHOD)
{ ElementType这个枚举类型的常量描述注解可用于的数据类型,它们与元注解(@Target)一起指定注解的作用对象

这边本汪讲下,ElementType类位于package java.lang.annotation;下
内部参数有10个,汪哥带大家看一下:

1 .TYPE 触发对象为类,接口(包括注解类型),枚举, Class, interface (including annotation type), or enum declaration ;

2 .FIELD触发对象为成员变量,包括(枚举)Field declaration (includes enum constants),

  这个本汪插一嘴,大家要分清楚field和variable的区别:
  成员变量(field)是指类的数据成员,
  而方法内部的局部变量(local variable)、
  参数变量(parameter variable)不能称作 field。
  field 属于 variable,也就是说variable的范围要更大,
  用的时候注意下

3 .METHOD 触发对象为方法 Method declaration

4 . PARAMETER 触发对象为通用参数 Formal parameter declaration

5 .CONSTRUCTOR 触发对象为构造器 Constructor declaration

6 .LOCAL_VARIABLE,触发对象为局部变量 Local variable declaration

7 .ANNOTATION_TYPE,触发对象为注解类型 Annotation type declaration

8 . PACKAGE,触发对象为包 Package declaration

9 . TYPE_PARAMETER,触发对象为类型参数 (Java8新加入的)Type parameter declaration

10 .TYPE_USE 触发对象为类型引用 (java8新加入的) Use of a type
}

讲一下另外两个注解:
@Retention({RetentionPolicy.Runtime})
{ RetentionPolicy这个枚举类型的常量描述保留注解的各种策略,它们与元注解(@Retention)一起指定注释要保留多长时间
这边本汪讲下,RetentionPolicy类也是位于package java.lang.annotation;下
内部参数有3个,汪哥带大家看一下:
SOURCE,源代码级别保留,编译时被忽略 Annotations are to be discarded by the compiler
CLASS, 被编译器在类文件中记录,但在运行时不需要JVM保留。这是默认的
RUNTIME, 被编译器记录在类文件中,在运行时被JVM保留,因此可以反读

}
@Documented 注解表明这个注解是由 javadoc记录的, 如果一个类型声明被注解了文档化,它的注解成为公共API的一部分

  • (2). 定义一个日志记录的切面,
    这边配置了三处,
    1.切入点的位置:
    @Pointcut("@annotation(LogAopAnnotation)")就是我们刚刚自己定义的注解
    2.设置环绕,获取运行所需时间
    3.获取日志记录所需的全部参数
    就是怎么简单明了
@Aspect
@Component
public class LogAopAspect {
    
    
    @Autowired
    private LogService logService;

    //切点;使用了特定注解的地方将触发通知(做日志的记录)- 切点
    @Pointcut("@annotation(com.bigdata.convenience.server.service.log.LogAopAnnotation)")
    public void logPointCut(){
    
    

    }

    //通知:环绕通知(前置通知+后置通知的结合),其实就是指定的注解所在的方法 执行前 + 执行后 + 监控
    @Around(value = "logPointCut()")
    public Object executeAround(ProceedingJoinPoint joinPoint) throws Throwable{
    
    

        Long start = System.currentTimeMillis();

        Object res = joinPoint.proceed();//获取运行结果

        Long time = System.currentTimeMillis() - start;

        saveLog(joinPoint,time,res);

        return res;
    }

    //记录日志(aop - 动态代理 - 底层Java的reflect反射来实现)
    private void saveLog(ProceedingJoinPoint joinPoint, Long time, Object res) throws Throwable{
    
    
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        SysLog entity = new SysLog();

        //获取注解上用户操作描述
        LogAopAnnotation annotation = method.getAnnotation(LogAopAnnotation.class);
        if ( annotation != null ) {
    
    
            entity.setOperation(annotation.value());
            entity.setOperatorTable(annotation.operatorTable());
        }

        //获取操作的方法名(为了防止重名,我们可以把包名,类名,方法名拼起来)
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        entity.setMethod(new StringBuilder(className).append(".").append(methodName).append("()").toString());

        //获取方法请求参数
        Object[] args = joinPoint.getArgs();
        String params = new Gson().toJson(args[0]);
        entity.setParams(params);

        //获取所需其他的参数
        entity.setTime(time);
        entity.setUsername(Constant.logOperateUser);
        entity.setCreateDate(DateTime.now().toDate());

        if (res != null && StringUtils.isNotBlank(new Gson().toJson(res))){
    
    
            entity.setMemo(new Gson().toJson(res));
        }

        logService.recordLog(entity);


    }
  • (3). 这样就可以拿来使用了,只需要在对应的方法上配置@LogAopAnnotation(operatorName = “新增操作” ,operatorTable = “user”)就可以了
@RequestMapping("user/add/aop")
@LogAopAnnotation(operatorName = "新增操作"  ,operatorTable = "user")
    public BaseResponse addUserVip(@RequestBody @Validated User user, BindingResult result) {
    
    
        String checkRes = ValidatorUtil.checkResult(result);
        if (StringUtil.isNotBlank(checkRes)) {
    
    
            return new BaseResponse(StatusCode.InvalidParams.getCode(),checkRes);
        }
        BaseResponse response = new BaseResponse(StatusCode.Success);
        try {
    
    
            //TODO:写真正的核心业务逻辑
            userMapper.insertSelective(user);

        } catch (Exception e) {
    
    
            response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
        }
        return response;
    }

在这里插入图片描述

( 四)使用AOP进行日志记录的BUG,以及解决方法和BUG的原因

1.下面这样的用法会报异常:

@LogAopAnnotation(operatorName = "新增操作"  ,operatorTable = "user")
private boolean addUser(User user){
    
    ...}
Caused by: org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type

这是什么原因呢?本汪给大家科普一下:
好多小伙伴知道怎么改,却不知道她真正的原因!
由于我们使用了AOP做代理,当这个方法进入切面的时候,她的返回类型如果是Java基本类型,都会被默认赋void,他们的封装类型,Boolean,Integer,Double等,也都会自动拆箱为基本类型,然后被赋予void
结果:
在这里插入图片描述
解决方法:
对返回类型进行自行包装就OK,凡是我们自定义的类型,是不会被复位void的

2.另外在给大家拓展一下,我们使用了异步线程去处理,同样会有这样的问题,这又是什么原因呢?

@Async("threadPoolTaskExecutor")
public boolean sendSimpleEmail(final String subject,final String content,final String ... tos) {
    
    

汪哥再给大家科普下:
先看下他的报错:
(1)第一个错,大意是说为了给你发这些消息,本机器已经非常疲倦了,不想再重试了,要试你自己试,大爷不陪你玩了反正就是罢工了

 RejectAndDontRequeueRecoverer:
  Retries exhausted for message。。。。。。。 )

(2)第二个错这个就是刚刚将的那个问题了,原因————@Asyncy她也是由AOP代理的,后面的自己构想一下

Caused by: org.springframework.aop.AopInvocationException:
 Null return value from advice does 
 not match primitive return type for: 
 public boolean com.bigdata.convenience.server.
 service.mail.MailService.sendSimpleEmail
 (java.lang.String,java.lang.String,java.lang.String[])

2)第三个错

Caused by: org.springframework.amqp.rabbit.listener.exception.
ListenerExecutionFailedException: Listener method 
consume(request.MailRequest,com.rabbitmq.client.Channel)
 throws java.io.IOException' threw exception

这个就涉及到springframework 的重试支持了
位于package org.springframework.retry.support;下,
RetryTemplate默认重试3次,由于AOP环绕导致的无法匹配,
RetryTemplate.doExecute()方法在重试了3次后,抛出了IOException
当然,我们也可以自定义SimpleRetryPolicy的重试次数,但不解决根本问题,依旧没什么用
3.实际上我在做的时候,还因为同样原因,undertow报出过栈满溢出的问题,但是我实在是想不起来是什么地方导致的了,这个就不给大家说了,万分抱歉

猜你喜欢

转载自blog.csdn.net/weixin_42994251/article/details/108302819