在上一篇自定义注解(一)——统一请求拦截中对自定义注解做了简单说明及关于统一token认证的应用示例。其实对于自定义注解,还有一种常用的方法是用于系统日志记录,此处的系统日志记录,区别于@Slf4j或@Log4j把日志文件写入到log文件中,而是直接写入到数据库表中。在AOP切面中可以跟踪入参情况、异常情况、返回值情况,并且把这些关键信息全部持久化到数据库中。以下是示例代码及说明:
文章目录
- 1. 自定义Log注解
- 2. 新建日志记录表
- 3. AOP上定义日志切面方法
- 3-1 处理完成的请求
- 3-2 拦截异常操作
- 3-3 核心逻辑处理
- 3-4 其他辅助方法
- 4. 方法上应用
- 4-1 应用场景
- 4-2 传参记录操作名称
- 5. 总结
1. 自定义Log注解
自定义注解中传入name的参数,用于标记方法中的模块名称
/**
* 日志记录自定义注解
*/
@Retention(RetentionPolicy.RUNTIME) // 注解运行在哪一个时期的
@Target({ ElementType.PARAMETER, ElementType.METHOD }) // 注解用在哪上边
@Documented
public @interface Log {
/** 模块名称 */
public String name() default "";
}
2. 新建日志记录表
日志记录可以按照实际需求进行建表,模块名称在注解中可以作为传参传入,方法名称、请求方式等都可以通过程序获取到,重点记录请求参数、返回值、操作人员和操作时间。
按照数据库表建立映射实体,对应实体如下:
@Data
public class SysOperLog {
/** 主键ID */
private Long operId;
/** 模块名称 */
private String name;
/** 方法名称 */
private String method;
/** 请求方式 */
private String requestMethod;
/** 请求URL */
private String operUrl;
/** 操作人员ID */
private Long operUserId;
/** 请求参数 */
private String operParam;
/** 操作状态 */
private Integer status;
/** 错误消息 */
private String errorMsg;
/** 操作时间 */
private Date operTime;
}
3. AOP上定义日志切面方法
定义LogAspect类用于AOP处理日志逻辑,分别加上@Aspect(AOP管理)、@Component(类放入Spring容器中)、@Slf4j(日志注解)3个注解
3-1 处理完成的请求
需要传入aspectj下的JoinPoint接口,用于正常业务逻辑日志记录
/**
* 处理完请求后执行
*/
@AfterReturning(pointcut = "@annotation(sysLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log sysLog, Object jsonResult) {
handleLog(joinPoint, sysLog, null, jsonResult);
}
3-2 拦截异常操作
需要传入aspectj下的JoinPoint接口,用于try…catch…之外的异常抛出日志记录
/**
* 拦截异常操作
*/
@AfterThrowing(value = "@annotation(sysLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log sysLog, Exception e) {
handleLog(joinPoint, sysLog, e, null);
}
3-3 核心逻辑处理
入参包括:aspectj下的JoinPoint接口、日志记录自定义注解Log、异常类Exception、返回值jsonResult。在方法中设置方法名称、请求方式、参数、操作时间、返回信息等,并通过线程的方式存入到数据库中。
protected void handleLog(final JoinPoint joinPoint, Log sysLog, final Exception e, Object jsonResult) {
try {
// 操作日志处理
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
if (e != null) {
// 方法中出现异常信息
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
} else {
R r = (R)jsonResult;
if (r.getCode() != 0) {
// 方法中未出现异常信息,但是执行错误
operLog.setStatus(r.getCode());
operLog.setErrorMsg(r.getMsg());
}
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 设置名称
operLog.setName(sysLog.name());
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog);
// 设置操作人
Long userId = getUserIdByToken(); //TODO 根据token信息获取操作人ID
operLog.setOperUserId(userId);
// 设置操作时间
operLog.setOperTime(new Date());
// 使用线程池,保存数据库 TODO
log.info("待保存日志:"+JSON.toJSONString(operLog));
} catch (Exception exp) {
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
3-4 其他辅助方法
/**
* 获取请求的参数,放到log中
*/
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception {
String requestMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs());
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
} else {
Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (StringUtils.isNotNull(o)) {
try {
String jsonObj = JSON.toJSONString(o);
params += jsonObj.toString() + " ";
} catch (Exception e) {
}
}
}
}
return params.trim();
}
public enum BusinessStatus {
SUCCESS,
FAIL,
}
4. 方法上应用
4-1 应用场景
一般应用于增、删、改的方法中,查询方法可以不使用(因为查询的数据量比较大,重点记录敏感操作日志)
4-2 传参记录操作名称
在注解括号中传入参数信息,此处的key值与Log注解中的属性保持一致,如下图:
5. 总结
通过自定义Log注解,并且加在需要记录日志表的方法上,就可以满足关键方法的日志记录。大大提升了我们的工作效率和代码整洁度,便于后期调试优化。对于错误方法,主要分为两块,一块是抛出异常之后执行了doAfterThrowing的方法,另一块是程序中做好了try…catch…的异常处理机制,执行doAfterReturning方法时,对 code!=0 的返回结果记录。除此之外,区别与上一篇的Token注解,Token注解的切面是在方法执行之前,Log注解的切面是在方法执行之后!