本文主要记录如何使用 注解+aop切面+异步监听
的方式来实现日志记录功能。java
主要记录的信息有: 操做人,操做IP,方法名,参数,消耗时间,日志类型,操做类型(操做日志和异常日志)以及增删改查记录,操做时间等。git
主要流程:github
AOP切面获得请求数据 -> 发布监听事件 -> 异步监听日志入库web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
复制代码
主要标注日志的具体用处也就是具体操做spring
package com.xd.pre.log;
import java.lang.annotation.*;
//元注解,定义注解被保留策略,通常有三种策略
//一、RetentionPolicy.SOURCE 注解只保留在源文件中,在编译成class文件的时候被遗弃
//二、RetentionPolicy.CLASS 注解被保留在class中,可是在jvm加载的时候北欧抛弃,这个是默认的声明周期
//三、RetentionPolicy.RUNTIME 注解在jvm加载的时候仍被保留
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) //定义了注解声明在哪些元素以前
@Documented
public @interface SysLog {
//定义成员
String descrption() default "" ;//描述
}
复制代码
AOP切面类是最主要的,能够使用自定义注解或针对包名实现AOP加强。设计模式
1)这里实现了对自定义注解的切点,对使用了自定义注解的方法进行AOP切面处理;浏览器
2)对方法运行时间进行监控;bash
3)对方法名,参数名,参数值,对日志描述以及异常信息的优化处理;mybatis
4)发布监听事件,日志异步入库app
在方法上增长@Aspect 注解声明切面,使用@Pointcut 注解定义切点,标记方法。
package com.xd.pre.log;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.xd.pre.security.PreUser;
import com.xd.pre.security.util.SecurityUtil;
import com.xd.pre.utils.LogUtil;
import com.xd.pre.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;
/**
* @Classname SysLogAspect
* @Description 系统日志切面
* @Author 李号东 lihaodongmail@163.comgit reset --merge
* @Date 2019-04-22 23:52
* @Version 1.0
* ①切面注解获得请求数据 -> ②发布监听事件 -> ③异步监听日志入库
*/
@Slf4j
@Aspect
@Component
public class SysLogAspect {
private com.xd.pre.domain.SysLog sysLog = new com.xd.pre.domain.SysLog();
/**
* 事件发布是由ApplicationContext对象管控的,咱们发布事件前须要注入ApplicationContext对象调用publishEvent方法完成事件发布
**/
@Autowired
private ApplicationContext applicationContext;
/***
* 定义controller切入点拦截规则,拦截SysLog注解的方法
*/
@Pointcut("@annotation(com.xd.pre.log.SysLog)")
public void sysLogAspect() {
}
/***
* 拦截控制层的操做日志
* @param joinPoint
* @return
* @throws Throwable
*/
@Before(value = "sysLogAspect()")
public void recordLog(JoinPoint joinPoint) throws Throwable {
// 开始时间
long beginTime = Instant.now().toEpochMilli();
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
PreUser securityUser = SecurityUtil.getUser();
sysLog.setUserName(securityUser.getUsername());
sysLog.setActionUrl(URLUtil.getPath(request.getRequestURI()));
sysLog.setStartTime(LocalDateTime.now());
sysLog.setRequestIp(ServletUtil.getClientIP(request));
sysLog.setRequestMethod(request.getMethod());
sysLog.setUa(request.getHeader("user-agent"));
//访问目标方法的参数 可动态改变参数值
Object[] args = joinPoint.getArgs();
//获取执行的方法名
sysLog.setActionMethod(joinPoint.getSignature().getName());
// 类名
sysLog.setClassPath(joinPoint.getTarget().getClass().getName());
sysLog.setActionMethod(joinPoint.getSignature().getName());
sysLog.setFinishTime(LocalDateTime.now());
// 参数
sysLog.setParams(Arrays.toString(args));
sysLog.setDescription(LogUtil.getControllerMethodDescription(joinPoint));
long endTime = Instant.now().toEpochMilli();
sysLog.setConsumingTime(endTime - beginTime);
}
/**
* 返回通知
* @param ret
* @throws Throwable
*/
@AfterReturning(returning = "ret", pointcut = "sysLogAspect()")
public void doAfterReturning(Object ret) {
// 处理完请求,返回内容
R r = Convert.convert(R.class, ret);
if (r.getCode() == 200){
// 正常返回
sysLog.setType(1);
} else {
sysLog.setType(2);
sysLog.setExDetail(r.getMsg());
}
// 发布事件
applicationContext.publishEvent(new SysLogEvent(sysLog));
}
/**
* 异常通知
* @param e
*/
@AfterThrowing(pointcut = "sysLogAspect()",throwing = "e")
public void doAfterThrowable(Throwable e){
// 异常
sysLog.setType(2);
// 异常对象
sysLog.setExDetail(LogUtil.getStackTrace(e));
// 异常信息
sysLog.setExDesc(e.getMessage());
// 发布事件
applicationContext.publishEvent(new SysLogEvent(sysLog));
}
}
复制代码
目前使用: 前置通知,后置通知,异常通知
事件发布是由ApplicationContext
对象管控的,咱们发布事件前须要注入ApplicationContext
对象调用publishEvent方法完成事件发布
主要记录日志的详细信息
package com.xd.pre.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 系统日志
* </p>
*
* @author lihaodong
* @since 2019-04-27
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 操做IP
*/
private String requestIp;
/**
* 操做类型 1 操做记录 2异常记录
*/
private Integer type;
/**
* 操做人ID
*/
private String userName;
/**
* 操做描述
*/
private String description;
/**
* 请求方法
*/
private String actionMethod;
/**
* 请求url
*/
private String actionUrl;
/**
* 请求参数
*/
private String params;
/**
* 浏览器
*/
private String ua;
/**
* 类路径
*/
private String classPath;
/**
* 请求方法
*/
private String requestMethod;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 完成时间
*/
private LocalDateTime finishTime;
/**
* 消耗时间
*/
private Long consumingTime;
/**
* 异常详情信息 堆栈信息
*/
private String exDetail;
/**
* 异常描述 e.getMessage
*/
private String exDesc;
}
复制代码
ApplicationEvent以及Listener是Spring为咱们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式,设计初衷也是为了系统业务逻辑之间的解耦,提升可扩展性以及可维护性。事件发布者并不须要考虑谁去监听,监听具体的实现内容是什么,发布者的工做只是为了发布事件而已。
其实生活中有不少这样的例子: 咱们在平时常见的比赛中,裁判员或者主持人给咱们开始的信号,也就是给咱们发布了一个开始的事件,而参赛的双方人员都在监听着这个事件,一旦事件发布后双方人员就开始拼命努力赢得比赛。而裁判或者主持人并不关心你比赛的过程,只是给你发布事件你执行就能够了
系统日志事件
package com.xd.pre.log;
import com.xd.pre.domain.SysLog;
import org.springframework.context.ApplicationEvent;
/**
* @Classname SysLogEvent
* @Description 系统日志事件
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-04-28 11:34
* @Version 1.0
*/
public class SysLogEvent extends ApplicationEvent {
public SysLogEvent(SysLog source) {
super(source);
}
}
复制代码
咱们自定义事件SysLogEvent继承了ApplicationEvent,继承后必须重载构造函数,构造函数的参数能够任意指定,其中source参数指的是发生事件的对象,通常咱们在发布事件时使用的是this关键字代替本类对象,而sysLog参数是咱们自定义的注册用户对象,该对象能够在监听内被获取。
异步监听日志事件
package com.xd.pre.log;
import com.xd.pre.domain.SysLog;
import com.xd.pre.service.ISysLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* @Classname SysLogListener
* @Description 注解形式的监听 异步监听日志事件
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-04-28 11:34
* @Version 1.0
*/
@Slf4j
@Component
public class SysLogListener {
@Autowired
private ISysLogService sysLogService;
@Async
@Order
@EventListener(SysLogEvent.class)
public void saveSysLog(SysLogEvent event) {
SysLog sysLog = (SysLog) event.getSource();
// 保存日志
sysLogService.save(sysLog);
}
}
复制代码
咱们只须要让咱们的监听类被Spring所管理便可,在咱们用户注册监听实现方法上添加@EventListener注解,该注解会根据方法内配置的事件完成监听。
只须要在你控制层要记录日志的方法上加上@SysLog注解便可
/**
* 获取用户列表集合
*
* @param page
* @param userDTO
* @return
*/
@SysLog(descrption = "查询用户集合")
@GetMapping
@PreAuthorize("hasAuthority('sys:user:view')")
public R getList(Page page, UserDTO userDTO) {
return R.ok(userService.getUsersWithRolePage(page, userDTO));
}
复制代码
源码地址 码云: gitee.com/li_haodong/…
GitHub: github.com/LiHaodong88…