2020年,悲伤!
那天,我突然好想打球。
前言
上篇文章主要是针对性的日志记录,这篇文章为切面式的日志记录。总体结构为通过自定义注解的方式,通过 aop 切点拦截来实现日志记录。
项目源码:https://github.com/XGLLHZ/springboot-frame.git
正文
1、首先我们需要一个日志实体类。像主键、操作名(比如删除、新增等等)、类名、方法名、请求 api、请求参数、请求参数
、异常详情(如果是异常记录)等这些属性都得有。
@Data
@Accessors(chain = true)
@TableName("sys_log")
public class LogEntity extends BaseEntityUtil implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id; //主键
private String userName; //用户名
private String operateName; //操作名
private String className; //类名 + 方法名
private String requestApi; //请求 api
private String requestParams; //请求参数
private Long requestTime; //请求耗时
private Integer logType; //日志类型:1:正常日志;2:异常日志
private String requestIp; //请求 ip
private String address; //地址
private String browser; //浏览器
private String exceptionDetail; //异常详情
private Integer deleteFlag; //删除状态:0:未删除;1:已删除
private Timestamp createTime; //创建时间
private Timestamp updateTime; //修改时间
}
2、其次、我们的日志是存储在数据库中的,所以肯定得有 mapper、service 等这一套来讲他存到数据库中。事务层的主要代码如下:
@Service
@Primary
public class LogServiceImpl extends ServiceImpl<LogMapper, LogEntity> implements LogService {
@Autowired
LogMapper logMapper;
@Autowired
RedisUtil redisUtil;
@Autowired
OnlineUserService onlineUserService;
@Override
@Transactional(rollbackFor = Exception.class)
public Integer insertLog(ProceedingJoinPoint joinPoint, LogEntity logEntity) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
HttpServletRequest request = HttpUtil.getRequest();
//操作
if (logEntity != null) {
logEntity.setOperateName(logAnnotation.value());
}
assert logEntity != null;
//用户名
String ip = StringUtil.getUserIp(request);
List<String> list = redisUtil.getListKey(ConstConfig.ONLINE_KEY + "*");
Collections.reverse(list);
List<OnlineUserEntity> list1 = new ArrayList<>();
OnlineUserEntity onlineUserEntity1 = null;
for (String key : list) {
onlineUserEntity1 = (OnlineUserEntity) redisUtil.getValue(key);
if (onlineUserEntity1 != null) {
if (onlineUserEntity1.toString().contains(ip)) {
list1.add(onlineUserEntity1);
}
}
}
logEntity.setUserName(list1.get(0).getUserName());
//用户 ip
logEntity.setRequestIp(ip);
//类名 + 方法名
String className = joinPoint.getTarget().getClass().getName() + "." + methodSignature.getName() + "()";
logEntity.setClassName(className);
//参数
StringBuilder stringBuilder = new StringBuilder("{");
Object[] objects = joinPoint.getArgs();
String[] strings = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
if (objects != null) {
for (int i = 0; i < strings.length; i++) {
stringBuilder.append(" ").append(strings[i]).append(": ").append(objects[i]);
}
}
logEntity.setRequestParams(stringBuilder + "}");
//请求 api
String url = request.getRequestURI();
logEntity.setRequestApi(url);
//用户地址
logEntity.setAddress(StringUtil.getAddressByIp(ip));
//浏览器
logEntity.setBrowser(StringUtil.getUserBrowser(request));
Integer res = logMapper.insert(logEntity);
return res;
}
}
上面的代码主要用来获取日志中的用户、堆栈信息,并将它们存放到数据库中。其中的参数 ProceedingJoinPoint ,通过这个类可以获取方法运行时的堆栈信息、路径、参数等,有兴趣的可以百度哈。我们自定义了一个注解 LogAnnotation,通过 其中的 value() 方法可以获取操作名,比如 @LogAnnotation("删除用户"),则此操作的名称就是删除用户。用户名等信息从 redis 中获取(用户登录时将其信息放入 redis 中)。用户的 ip、api、用户地址、所用浏览器等信息可以通过 request 来获取,笔者这里写了一个工具类,有兴趣的同学可以瞅瞅。然后再通过 ProceedingJoinPoint 对象来获取日志产生的方法路径、参数、文件名等信息。最后再将这些信息放入数据库。
3、然后自定义注解,@LogAnnotation
@Target(ElementType.METHOD) //日志作用范围(CLASS:类;METHOD:方法 等)
@Retention(RetentionPolicy.RUNTIME) //日志有效范围(RUNTIME:运行时 等)
public @interface LogAnnotation {
String value() default "";
}
其中 @Target 注解意为这个注解的作用范围,比如作用在类、方法、接口上等。@Retention 可以理解为声明时候出发,比如方法运行时,类被实例化时等等.
4、然后我们需要通过 spring 的 aop 来实现日志的切点拦截。
@Slf4j
@Aspect
@Component
public class LogAspect {
//spring 4 之后推荐构造方法式的注入
private final LogService logService;
ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public LogAspect(LogService logService) {
this.logService = logService;
}
/**
* 配置切入点
*/
@Pointcut("@annotation(org.huangzi.main.common.annotation.LogAnnotation)")
public void logPointCut() {
//让同类中其他方法使用此切入点
}
/**
* 配置通知(环绕通知,即方法执行完后)
* 正常通知
* @param joinPoint
*/
@Around("logPointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object object;
//请求耗时
threadLocal.set(System.currentTimeMillis());
LogEntity logEntity = new LogEntity();
logEntity.setLogType(ConstConfig.ORDINARY_LOG_CODE);
object = joinPoint.proceed(); //执行方法,object 为方法返回值
logEntity.setRequestTime(System.currentTimeMillis() - threadLocal.get());
logEntity.setExceptionDetail("");
threadLocal.remove();
logService.insertLog(joinPoint, logEntity);
return object;
}
/**
* 配置通知
* 异常通知
* @param joinPoint
* @param throwable
*/
@AfterThrowing(pointcut = "logPointCut()", throwing = "throwable")
public void logException(JoinPoint joinPoint, Throwable throwable) {
//请求耗时
LogEntity logEntity = new LogEntity();
logEntity.setLogType(ConstConfig.ERROR_LOG_CODE);
logEntity.setRequestTime(System.currentTimeMillis() - threadLocal.get());
threadLocal.remove();
//异常详情
logEntity.setExceptionDetail(ThrowableUtil.getStackTrace(throwable));
logService.insertLog((ProceedingJoinPoint) joinPoint, logEntity);
}
}
其中 @Aspect、@Pointcut等注解为 spring aop 中的相关注解,其意如下:
@Aspect:作用是把当前类标识为一个切面供容器读取
@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
@Around:环绕增强,相当于MethodInterceptor
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice
@After: final增强,不管是抛出异常或者正常退出都会执行
在面向切面编程中,切面 = 切点 + 通知。上面的方法中 logPointcut() 方法为切入点,也就是注解作用的地方为切入点,logAround() 和 logException() 为通知,也就是什么时候产生日志。其中第一个为正常日志通知,也就是当 @LogAnnotation 注解作用的地方的方法正常执行完或者类被实例化后进行日志记录;第二个是异常日志,也就是当注解作用的地方代码运行产生异常时记录日志。
5、示例。这里我们将 controller 层作为日志记录面,如下:
@RestController
@RequestMapping("/admin/log")
public class LogController {
@Autowired
LogService logService;
@LogAnnotation("日志列表")
@RequestMapping("/list")
public APIResponse getList(@RequestBody LogEntity logEntity) {
return logService.getList(logEntity);
}
}
当我们在接口方法上加入 @LogAnnotation 注解,放访问 /admin/log/list 接口时,就会将相关日志信息存入数据库中。
(请忽略乱码 0.0)
00000 000
00 00 00 00
00 00 00
00 000000000000000
00 00
0000000000 00