本汪作为一名资深的哈士奇
每天除了闲逛,拆家,就是啃博客了
作为不是在戏精,就是在戏精的路上的二哈
今天就来给大家说说前后端分离项目-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报出过栈满溢出的问题,但是我实在是想不起来是什么地方导致的了,这个就不给大家说了,万分抱歉