@[TOC]
一、背景
公司日志系统进行统一,用于记录操作人员的关键操作日志。
二、使用APO+SPEL实现业务日志收集
1、定义@Log
import java.lang.annotation.*;
/**
* 日志注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Log {
enum Actions {
ADD, DELETE, UPDATE
}
enum Business {
User, // 用户
Role, // 角色
Order, // 订单
Product, // 产品
}
/**
* 操作的动作,如增、删、改等
*/
Actions action();
/**
* 操作的业务类型,如用户、角色、订单等
*/
Business business();
/**
* 操作的用户的ID(Spel)
*/
String userId();
/**
* 操作的资源的ID(如订单id)
*/
String objectId();
/**
* 操作之前的原数据(SpEL)
*/
String before() default "";
/**
* 操作之后的新数据(SpEL)
*/
String after() default "";
}
2、定义切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Date;
/**
* 日志切面
*/
@Aspect
@Component
public class LogAspect {
@Pointcut(value = "@annotation(com.spel.Log)")
public void pointcut() {}
@Around(value = "pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
if (AopUtils.isAopProxy(point.getTarget())) {
return point.proceed();
}
// 获取目标方法
Method method = getMethodSignature(point).getMethod();
if (method == null) {
return point.proceed();
}
// 如果@Log可以多个标注的话,可以:Log[] annotations = method.getAnnotationsByType(Log.class);
Log annotation = method.getAnnotation(Log.class);
if (annotation == null) {
return point.proceed();
}
// 获取目标真实方法
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(point.getTarget());
Object[] args = point.getArgs();
LogAspectMethodInfo logAspectMethodInfo = new LogAspectMethodInfo(targetClass, method, args, getMethodSignature(point).getParameterNames(), annotation);
try {
// 计算方法执行时间
long startTimestamp = new Date().getTime();
// 获取方法的返回值
Object returned = point.proceed();
// 计算方法执行时间
long execTimestamp = new Date().getTime() - startTimestamp;
logAspectMethodInfo.setReturned(returned);
logAspectMethodInfo.setExecTimestamp(execTimestamp);
} catch (Exception ex) {
logAspectMethodInfo.setThrown(ex);
}
try {
LogContext.refreshContext(logAspectMethodInfo);
// 解析信息并记录
LogUtil.recordLog(logAspectMethodInfo);
} catch (Exception e) {
// TODO 记录日志出现异常
throw e;
} finally {
LogContext.clearContext();
}
if (logAspectMethodInfo.isErrored()) {
throw logAspectMethodInfo.getThrown();
}
return logAspectMethodInfo.getReturned();
}
/**
* 获取目标方法
*/
private MethodSignature getMethodSignature(ProceedingJoinPoint point) {
Signature signature = point.getSignature();
if (signature instanceof MethodSignature) {
return ((MethodSignature) signature);
}
return null;
}
}
3、定义全局方法信息
import java.lang.reflect.Method;
/**
* 切面方法基本信息
*/
public class LogAspectMethodInfo {
// 目标类
private final Class<?> targetClass;
// 方法
private final Method method;
// 参数
private final Object[] args;
// 参数名
private final String[] paramNames;
// 返回值
private Object returned;
// 异常信息
private Throwable thrown;
// 方法执行时间
private Long execTimestamp;
// 注解信息
private Log annotation;
public LogAspectMethodInfo(Class<?> targetClass, Method method, Object[] args, String[] paramNames, Log annotation) {
this.targetClass = targetClass;
this.method = method;
this.args = args;
this.paramNames = paramNames;
this.annotation = annotation;
}
public boolean isErrored() {
return this.thrown != null;
}
public Class<?> getTargetClass() {
return targetClass;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object getReturned() {
return returned;
}
public void setReturned(Object returned) {
this.returned = returned;
}
public Throwable getThrown() {
return thrown;
}
public void setThrown(Throwable thrown) {
this.thrown = thrown;
}
public Long getExecTimestamp() {
return execTimestamp;
}
public void setExecTimestamp(Long execTimestamp) {
this.execTimestamp = execTimestamp;
}
public Log getAnnotation() {
return annotation;
}
public void setAnnotation(Log annotation) {
this.annotation = annotation;
}
public String[] getParamNames() {
return paramNames;
}
}
4、定义Spel解析上下文
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class LogContext {
// 定义Parser,可以定义全局的parser
private final static ExpressionParser parser = new SpelExpressionParser();
/**
* 表达式模板
*/
static class TemplateParserContext implements ParserContext {
public static final String beginStr = "${";
public static final String endStr = "}";
public String getExpressionPrefix() {
return beginStr;
}
public String getExpressionSuffix() {
return endStr;
}
public boolean isTemplate() {
return true;
}
}
/**
* 线程全局的Context
*/
private static final ThreadLocal<StandardEvaluationContext> threadLocalContext = new ThreadLocal<StandardEvaluationContext>(){
@Override
protected StandardEvaluationContext initialValue() {
return new StandardEvaluationContext();
}
};
public static void refreshContext(LogAspectMethodInfo logAspectMethodInfo) {
// if (beanFactory != null) {
// 加入Bean的引用。可以使用Bean,这里如果用的话,需要implements BeanFactoryAware,获取到BeanFactory
// context.setBeanResolver(new BeanFactoryResolver(beanFactory));
// }
// TODO 可以把返回值设置进spel变量中,或者自定义全局的变量
if (logAspectMethodInfo.getReturned() != null) {
getContext().setVariable("_returned", logAspectMethodInfo.getReturned());
}
// 把方法的所有参数,放到变量中
if (logAspectMethodInfo.getParamNames() != null) {
for (int i = 0; i < logAspectMethodInfo.getParamNames().length; i++) {
getContext().setVariable(logAspectMethodInfo.getParamNames()[i], logAspectMethodInfo.getArgs()[i]);
}
}
}
public static EvaluationContext getContext() {
return threadLocalContext.get();
}
public static void clearContext() {
threadLocalContext.remove();
}
public static void setBefore(String before) {
getContext().setVariable("_before", before);
}
public static void setAfter(String after) {
getContext().setVariable("_after", after);
}
public static Object getBefore() {
return parseSpel("${#_before}");
}
public static Object getAfter() {
return parseSpel("${#_after}");
}
/**
* 解析spel表达式
*/
public static Object parseSpel(String spel) {
// TODO 这里可以将表达式解析的结果做缓存,性能提高2倍
return parser.parseExpression(spel, new TemplateParserContext()).getValue(getContext(), Object.class);
}
}
5、定义入库工具类
public class LogUtil {
/**
* 解析注解信息 记录日志
*/
public static void recordLog(LogAspectMethodInfo logAspectMethodInfo) {
// 拿到注解元信息
Log annotation = logAspectMethodInfo.getAnnotation();
Object userId = LogContext.parseSpel(annotation.userId());
Object objectId = LogContext.parseSpel(annotation.objectId());
// TODO 入库/ES
System.out.println("action:" + annotation.action());
System.out.println("business:" + annotation.business());
System.out.println("userId:" + userId);
System.out.println("objectId:" + objectId);
System.out.println("before:" + LogContext.getBefore());
System.out.println("after:" + LogContext.getAfter());
}
}
6、使用
@Log(
action = Log.Actions.ADD,
business = Log.Business.User,
userId = "获取到userid:${#user.id}",
objectId = "获取到objectId:${#user.objectId}"
)
@PostMapping
public String addUser(@RequestBody User user) {
LogContext.setBefore("userId = " + user.getId());
LogContext.setAfter("userId = " + 2);
return "success";
}
static class User {
private String id;
private String objectId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getObjectId() {
return objectId;
}
public void setObjectId(String objectId) {
this.objectId = objectId;
}
}
{
"id": "1",
"objectId": "11"
}
执行结果: action:ADD business:User userId:获取到userid:1 objectId:获取到objectId:11 before:userId = 1 after:userId = 2
三、优化:基于缓存的spel表达式解析
1、定义@Log
import java.lang.annotation.*;
/**
* 日志注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Log {
enum Actions {
ADD, DELETE, UPDATE
}
enum Business {
User, // 用户
Role, // 角色
Order, // 订单
Product, // 产品
}
/**
* 操作的动作,如增、删、改等
*/
Actions action();
/**
* 操作的业务类型,如用户、角色、订单等
*/
Business business();
/**
* 操作的用户的ID(Spel)
*/
String userId();
/**
* 操作的资源的ID(如订单id)
*/
String objectId();
/**
* 操作之前的原数据(SpEL)
*/
String before() default "";
/**
* 操作之后的新数据(SpEL)
*/
String after() default "";
}
2、定义切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Date;
/**
* 日志切面
*/
@Aspect
@Component
public class LogAspect extends LogContext {
@Pointcut(value = "@annotation(com.log.Log)")
public void pointcut() {}
@Around(value = "pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
if (AopUtils.isAopProxy(point.getTarget())) {
return point.proceed();
}
// 获取目标方法
Method method = getMethodSignature(point).getMethod();
if (method == null) {
return point.proceed();
}
// 如果@Log可以多个标注的话,可以:Log[] annotations = method.getAnnotationsByType(Log.class);
Log annotation = method.getAnnotation(Log.class);
if (annotation == null) {
return point.proceed();
}
// 获取目标真实方法
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(point.getTarget());
Object[] args = point.getArgs();
LogAspectMethodInfo logAspectMethodInfo = new LogAspectMethodInfo(targetClass, method, args, getMethodSignature(point).getParameterNames(), annotation);
try {
// 计算方法执行时间
long startTimestamp = new Date().getTime();
// 获取方法的返回值
Object returned = point.proceed();
// 计算方法执行时间
long execTimestamp = new Date().getTime() - startTimestamp;
logAspectMethodInfo.setReturned(returned);
logAspectMethodInfo.setExecTimestamp(execTimestamp);
} catch (Exception ex) {
logAspectMethodInfo.setThrown(ex);
}
try {
// 解析信息并记录
recordLog(logAspectMethodInfo);
} catch (Exception e) {
throw e;
} finally {
clearVariables();
}
if (logAspectMethodInfo.isErrored()) {
throw logAspectMethodInfo.getThrown();
}
return logAspectMethodInfo.getReturned();
}
/**
* TODO 记录日志
*/
private void recordLog(LogAspectMethodInfo logAspectMethodInfo) {
StandardEvaluationContext standardEvaluationContext = initContext(logAspectMethodInfo);
LogExpressionEvaluator expressionEvaluator = getExpressionEvaluator();
AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(logAspectMethodInfo.getMethod(), logAspectMethodInfo.getTargetClass());
// 拿到注解元信息
Log annotation = logAspectMethodInfo.getAnnotation();
Object userId = expressionEvaluator.parseExpression(annotation.userId(), annotatedElementKey, standardEvaluationContext);
Object objectId = expressionEvaluator.parseExpression(annotation.objectId(), annotatedElementKey, standardEvaluationContext);
Object before = expressionEvaluator.parseExpression(annotation.before(), annotatedElementKey, standardEvaluationContext);
Object after = expressionEvaluator.parseExpression(annotation.after(), annotatedElementKey, standardEvaluationContext);
// TODO 入库/ES
System.out.println("action:" + annotation.action());
System.out.println("business:" + annotation.business());
System.out.println("userId:" + userId);
System.out.println("objectId:" + objectId);
System.out.println("before:" + before);
System.out.println("after:" + after);
}
/**
* 获取目标方法
*/
private MethodSignature getMethodSignature(ProceedingJoinPoint point) {
Signature signature = point.getSignature();
if (signature instanceof MethodSignature) {
return ((MethodSignature) signature);
}
return null;
}
}
3、定义全局方法信息
import java.lang.reflect.Method;
/**
* 切面方法基本信息
*/
public class LogAspectMethodInfo {
// 目标类
private final Class<?> targetClass;
// 方法
private final Method method;
// 参数
private final Object[] args;
// 参数名
private final String[] paramNames;
// 返回值
private Object returned;
// 异常信息
private Throwable thrown;
// 方法执行时间
private Long execTimestamp;
// 注解信息
private Log annotation;
public LogAspectMethodInfo(Class<?> targetClass, Method method, Object[] args, String[] paramNames, Log annotation) {
this.targetClass = targetClass;
this.method = method;
this.args = args;
this.paramNames = paramNames;
this.annotation = annotation;
}
public boolean isErrored() {
return this.thrown != null;
}
public Class<?> getTargetClass() {
return targetClass;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object getReturned() {
return returned;
}
public void setReturned(Object returned) {
this.returned = returned;
}
public Throwable getThrown() {
return thrown;
}
public void setThrown(Throwable thrown) {
this.thrown = thrown;
}
public Long getExecTimestamp() {
return execTimestamp;
}
public void setExecTimestamp(Long execTimestamp) {
this.execTimestamp = execTimestamp;
}
public Log getAnnotation() {
return annotation;
}
public void setAnnotation(Log annotation) {
this.annotation = annotation;
}
public String[] getParamNames() {
return paramNames;
}
}
4、定义缓存器
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class LogExpressionEvaluator extends CachedExpressionEvaluator {
/**
* 表达式模板
*/
static class TemplateParserContext implements ParserContext {
public static final String beginStr = "${";
public static final String endStr = "}";
public String getExpressionPrefix() {
return beginStr;
}
public String getExpressionSuffix() {
return endStr;
}
public boolean isTemplate() {
return true;
}
}
private final Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);
private final Map<AnnotatedElementKey, Method> methodCache = new ConcurrentHashMap<>(64);
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
public Object parseExpression(String conditionExpr, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
return getExpression(this.expressionCache, methodKey, conditionExpr).getValue(evalContext, Object.class);
}
@Override
protected Expression parseExpression(String expression) {
return getParser().parseExpression(expression, new TemplateParserContext());
}
public StandardEvaluationContext createEvaluationContext(Class<?> targetClass, Method method, Object[] args, BeanFactory beanFactory) {
Method targetMethod = getTargetMethod(targetClass, method);
StandardEvaluationContext context = new MethodBasedEvaluationContext(null, targetMethod, args, parameterNameDiscoverer);
if (beanFactory != null) {
// 要访问工厂bean本身,应该在bean名称前加上&符号
// parser.parseExpression("&foo").getValue(context);
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
return context;
}
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
return methodCache.computeIfAbsent(methodKey, k -> AopUtils.getMostSpecificMethod(method, targetClass));
}
}
5、定义上下文
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.HashMap;
import java.util.Map;
public class LogContext implements BeanFactoryAware {
// 定义Evaluator 内含Evaluator
private final static LogExpressionEvaluator expressionEvaluator = new LogExpressionEvaluator();
protected BeanFactory beanFactory;
// 用户定义的变量
private static final ThreadLocal<Map<String, Object>> VARIABLES = new ThreadLocal<Map<String, Object>>() {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
public StandardEvaluationContext initContext(LogAspectMethodInfo logAspectMethodInfo) {
StandardEvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(logAspectMethodInfo.getTargetClass(),
logAspectMethodInfo.getMethod(),
logAspectMethodInfo.getArgs(),
beanFactory
);
// 可以把返回值设置进spel变量中,或者自定义全局的变量
if (logAspectMethodInfo.getReturned() != null) {
evaluationContext.setVariable("_returned", logAspectMethodInfo.getReturned());
}
// 把所有变量放入上下文
evaluationContext.setVariables(getVariables());
return evaluationContext;
}
public LogExpressionEvaluator getExpressionEvaluator() {
return expressionEvaluator;
}
public static Map<String, Object> getVariables() {
return VARIABLES.get();
}
public static void clearVariables() {
VARIABLES.remove();
}
public static void setVariable(String key, String variable) {
getVariables().put(key, variable);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
6、使用
@Log(
action = Log.Actions.ADD,
business = Log.Business.User,
userId = "获取到userid:${#user.id}",
objectId = "获取到objectId:${#user.objectId}",
before = "${#before}",
after = "${#after}"
)
@PostMapping
public String addUser(@RequestBody User user) {
LogContext.setVariable("before", user.getId());
LogContext.setVariable("after", "2");
return "success";
}
static class User {
private String id;
private String objectId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getObjectId() {
return objectId;
}
public void setObjectId(String objectId) {
this.objectId = objectId;
}
}
{
"id": "1",
"objectId": "11"
}