上一篇 基于Redis实现的日志记录组件——超实用(3)核心类描述
四、AOP实现
以下是日志记录的核心切面代码:
Slf4j
@Aspect
@Order(0) // 越小越先执行
public class RedisLogOptAspect {
@Resource
private RedisLogSpelHandler redisLogSpelHandler;
@Resource
private RedisLogService redisLogService;
@Pointcut("@annotation(cn.hengyumo.dawn.core.log.redisLogger.RedisLogOpt)")
public void redisLogOptPointCut(){}
@Around("redisLogOptPointCut()")
public Object aroundRedisLogOptPointCur(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Throwable exception = null;
Object result = null;
long start = System.currentTimeMillis();
long timeUseMillis;
try {
result = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
exception = throwable;
} finally {
timeUseMillis = System.currentTimeMillis() - start;
}
try {
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
Object target = proceedingJoinPoint.getTarget();
Method method = methodSignature.getMethod();
RedisLogOpt redisLogOpt = AnnotationUtils.getAnnotation(method, RedisLogOpt.class);
Assert.notNull(redisLogOpt, "never null");
String component = redisLogOpt.component();
if (redisLogService.checkComponent(component)) {
RedisLogComponent redisLogComponent = redisLogService.getComponent(component);
String opt = redisLogOpt.opt();
String spel = redisLogOpt.spel();
String description = redisLogOpt.description();
HttpServletRequest request = getRequest();
HttpServletResponse response = getResponse();
Assert.notNull(request, "request is null");
Assert.notNull(response, "response is null");
Map<String, Object> params = getParams(proceedingJoinPoint);
if (StringUtils.hasLength(spel)) {
// spel 优先
EvaluationContext context = redisLogSpelHandler.buildContext(
redisLogComponent,
redisLogOpt,
request,
response,
target,
method,
params,
result,
timeUseMillis,
exception
);
description = redisLogSpelHandler.parseLogSpel(context, spel);
}
// 加入通用前缀
String pre = redisLogService.createNormalLogPre(request, response, timeUseMillis);
redisLogService.log(component, opt, pre + description);
} else {
log.warn("找不到RedisLoggerComponent:{},请先注册", component);
}
} catch (Throwable throwable) {
log.error("RedisLog 日志记录失败!");
throwable.printStackTrace();
}
if (exception != null) {
throw exception;
}
return result;
}
/**
* 获取当前请求
*
* @return 当前请求
*/
private HttpServletRequest getRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
if (sra != null) {
return sra.getRequest();
}
return null;
}
/**
* 获取当前响应
*
* @return 当前请求
*/
private HttpServletResponse getResponse() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
if (sra != null) {
return sra.getResponse();
}
return null;
}
/**
* 构造方法的参数Map
*
* @param proceedingJoinPoint 过程切点
* @return Map<String, Object>
*/
private Map<String, Object> getParams(ProceedingJoinPoint proceedingJoinPoint) {
Map<String, Object> map = new HashMap<>();
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
String[] paramsNames = signature.getParameterNames();
if (paramsNames != null && paramsNames.length > 0) {
Object[] args = proceedingJoinPoint.getArgs();
for (int i = 0; i < paramsNames.length; i++) {
map.put(paramsNames[i], args[i]);
}
}
return map;
}
}
1、@Order(0) 是设置切面的执行顺序,当多个切面应用到同一个方法时。Order顺序小的优先被包围在方法的外层。日志记录应该在方法返回的最后一个执行。故而Order的值要小于其它切面的值。
2、当同时声明日志内容和spel表达式时,原则是spel表达式优先。
3、AOP拦截时,同时还会获取其它重要信息,如请求内容,响应内容,请求耗时,目标方法,目标Bean等,这些参数可以用于SPEL表达式生成日志信息。
4、AOP不主动实现具体日志记录,而是通过调用redisLogService里的方法。
五、SPEL表达式实现
在AOP中截取了程序运行的一系列重要信息,通过SPEL表达式既可以利用这些信息来生成日志,以下是spel表达式处理的核心代码:
@Slf4j
@Component
public class RedisLogSpelHandler {
public EvaluationContext buildContext(RedisLogComponent redisLogComponent,
RedisLogOpt redisLogOpt,
HttpServletRequest request,
HttpServletResponse response,
Object target,
Method method,
Map<String, Object> params,
Object result,
long timeUseMillis,
Throwable throwable) {
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("request", request);
context.setVariable("url", request.getRequestURL());
context.setVariable("httpMethod", request.getMethod());
context.setVariable("ip", request.getRemoteAddr());
context.setVariable("userAgent", request.getHeader(HttpHeaders.USER_AGENT));
context.setVariable("cookie", request.getHeader(HttpHeaders.COOKIE));
context.setVariable("contentType", request.getHeader(HttpHeaders.CONTENT_TYPE));
context.setVariable("session", request.getSession());
context.setVariable("user", null);
context.setVariable("userName", null);
context.setVariable("userId", null);
context.setVariable("this", target);
context.setVariable("method", method);
context.setVariable("redisLogOpt", redisLogOpt);
context.setVariable("redisLogComponent", redisLogComponent);
context.setVariable("params", params);
context.setVariable("time", DateUtil.getDateTime(Calendar.getInstance().getTime()));
context.setVariable("response", response);
context.setVariable("status", response.getStatus());
context.setVariable("result", result);
context.setVariable("timeUseMillis", timeUseMillis);
context.setVariable("throwable", throwable);
return context;
}
// 111.7.96.181 - - [23/Jun/2021:20:34:09 +0800] "HEAD / HTTP/1.1" 301 0 "-" "Chrome/54.0 (Windows NT 10.0)"
public String parseLogSpel(EvaluationContext context, String spel) {
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
Object obj = expression.getValue(context);
if (obj == null) {
log.warn("parse null: {}, context: {}", expression, context);
return null;
}
return obj.toString();
}
}
1、spel 表达式的解析,有两个过程,一个是构建上下文,buildContext的目的就是根据传入的参数构建spel语句运行的上下文。主要是将各种参数设入上下文中。后一个则是根据传入的spel语句配合上下文执行spel表达式,生成目标结果。
2、spel表达式使用简单,例如:
spel = "'查询产品' + #params.toString() + ',结果:' + #result.toString()"
spel = "'添加一个产品' + #params['product'].toString()"
spel = "'根据ID删除产品-' + #params['id']"
未完待续~