使用AOP+SPEL实现业务日志收集

188 阅读3分钟

@[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"
}