SourceCode Java SpringBoot Linux Spring Code

Java日志记录工具

Posted on 2022-04-27,22 min read
  • 在Java中,我们经常会使用AOP来记录日志,这也是AOP一个比较经典的使用场景。但是问题是,如果我们需要支持动态的记录日志(比如SpEl)来记录日志,这样就需要对切面进行改造了。
  • 在美团技术团队的博客上,有一篇名为:如何优雅地记录操作日志? 的文章,提供了一个很好的模板。其作者也把代码开源在了Github 上。但是,这篇文章的日志记录工具功能和我所需要的日志记录还差了一些功能,也多了一些不需要的功能,于是就自己写了一个日志切面。
  • 这个日志切面能通过注解做到以下功能:
    • 可以记录接口的调用时间。
    • 可以记录一些比较重要的内容,并且这个需要支持SpEl表达式来动态的生成内容。
    • 支持类似@Cacheable注解一样的condition,在满足某些条件的情况下才能记录。
    • 支持多种保存模式,比如可能一个方法我需要用Mysql来保存日志,一个方法我需要用Redis来保存日志。
  • 由于本文给的代码只是一个Demo,可能会有考虑不周全的地方,本文仅作一个参考。
    • 本文的代码将会被放在仓库内:aop-log

方案分析

  • 在有了需求过后,就需要对实现方案进行分析。

  • 针对记录调用时间,相对来说实现比较简单,我们只需要在环绕通知的前后分别记录一个起始和结束时间,然后就能统计出调用时间了。

  • 对于 第二点和第三点,可以通过在注解上指定SpEl表达式来解决。对于第二点,直接将表达式使用SpEl进行解析,对于第三点则使用SpEl进行解析后,返回true,才记录日志。SpEl的使用如下:

    public static void main(String[] args) {
            SpelExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression("#root.purchaseName");
            Order order = new Order();
            order.setPurchaseName("张三");
            System.out.println(expression.getValue(order));
    }
    
  • 针对四点,很明显我们可以想到使用策略模式来进行实现,这样就可以在注解上指定不同的策略名称来选择不同的保存方案了。

代码实现

注解定义

  • 针对上面的需求,我们可以抽象出如下的一个注解:

    /**
     * 日志
     *
     * @author Gloduck
     * @date 2022/04/25
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Documented
    public @interface LogRecord {
        /**
         * 方法名称
         *
         * @return {@link String}
         */
        String method() default "";
    
        /**
         * 内容,使用占位符${}来编写el表达式,同时不要有不匹配的{}括号,否则会导致解析出错。
         * 编写示例如下:
         * <pre>
         *     "用户:${{transfer.fromUser}} 向用户:${{transfer.toUser}}转了一笔账,时间为:${T(System).currentTimeMillis()}"
         * </pre>
         * 上面例子中的【{transfer.fromUser}】、【{transfer.toUser}】、【T(System).currentTimeMillis()】均为SpEl表达式,会使用SpEl进行解析。
         * 如果因为缺少必要参数等原因导致无法解析,解析后的值将会为占位符里面的值。
         *
         * @return {@link String}
         */
        String content() default "";
    
        /**
         * 日志分类
         *
         * @return {@link String}
         */
        String group() default "";
    
        /**
         * 保存条件,支持SpringEl表达式,返回true才能保存
         *
         * @return {@link String}
         */
        String condition() default "true";
    
        /**
         * 保存环境
         * 此参数和{@link LogSaveStrategy#getStrategyName()}对应。
         *
         * @return {@link String}
         */
        String saveStrategy();
    
    }
    
  • 这个注解是方法级别的,同时作为一个AOP的一个指示器,对使用了此注解的方法进行一个代理。

实体定义

  • 对应上面的注解,我们需要定义一个实体来接收内容。

    package cn.gloduck.unittest.log.entity;
    
    import lombok.Getter;
    import lombok.ToString;
    
    /**
     * 日志实体
     *
     * @author Gloduck
     * @date 2022/04/25
     */
    @Getter
    @ToString
    public class LogEntity {
        /**
         * 方法
         */
        private final String method;
    
        /**
         * 内容
         */
        private final String content;
    
        /**
         * 分类
         */
        private final String group;
    
        /**
         * 请求参数
         */
        private final Object[] params;
    
        /**
         * 花费时间
         */
        private final Long costTime;
    
    
        /**
         * 异常,只有执行失败的时候才会有值,默认为null
         */
        private final Throwable exception;
    
        public LogEntity(String method, String content, String group,  Object[] params, Long costTime, Throwable exception) {
            this.method = method;
            this.content = content;
            this.group = group;
            this.params = params;
            this.costTime = costTime;
            this.exception = exception;
        }
    }
    
    
  • 这是一块标注的实体对象,并且为了规范,这里所有的变量都是应该不能修改的,并且只能通过构造方法来初始化此对象。

策略模式的实现

  • 在上面的注解上,我们有一个saveStrategy变量,来指定我们的策略,它是一个String类型的变量。这意味着我们每个策略必须得有一个名称。于是我们就开始写策略模式,策略模式在以前的文章已经出现很多次了,这里就不过多的进行描述了。

  • 策略接口:

    /**
     * 日志保存策略
     * 实现了此策略,并将其注册成Bean,则可以使用。或者调用{@link LogStrategyContext#registryStrategy(LogSaveStrategy)}注册
     *
     * @author Gloduck
     * @date 2022/04/25
     */
    public interface LogSaveStrategy {
        /**
         * 保存日志
         *
         * @param log 日志
         */
        void saveLog(LogEntity log);
    
        /**
         * 得到策略名称
         *
         * @return {@link String}
         */
        String getStrategyName();
    }
    
    
  • 策略调度器:

    /**
     * 日志策略上下文
     *
     * @author Gloduck
     * @date 2022/04/25
     */
    public class LogStrategyContext {
        private final Logger logger = LoggerFactory.getLogger(LogStrategyContext.class);
        private final Map<String, LogSaveStrategy> logStrategyMap;
        private static final LogStrategyContext SINGLETON = new LogStrategyContext();
        public LogStrategyContext() {
            this.logStrategyMap = new HashMap<>(4);
        }
    
        /**
         * 注册日志保存策略
         *
         * @param strategy 策略
         */
        public static  <T extends LogSaveStrategy> void  registryStrategy(T strategy){
            if(SINGLETON.logStrategyMap.containsKey(strategy.getStrategyName())){
                SINGLETON.logger.warn("Register strategy ({}) failed, the strategy with same name already exist in container", strategy.getStrategyName());
            }
            SINGLETON.logStrategyMap.put(strategy.getStrategyName(), strategy);
        }
    
        /**
         * 获取日志保存策略
         *
         * @param name 名字 {@link LogSaveStrategy#getStrategyName()}
         * @return {@link LogSaveStrategy}
         */
        public static LogSaveStrategy getStrategy(String name){
            return SINGLETON.logStrategyMap.get(name);
        }
    }
    
    
  • 在这里,我们需要与SpringBoot进行集成,所以需要添加自动注册策略的功能,这里我们选择Aware来实现(不清楚请参考的笔记中的八股文)。

    /**
     * 日志策略注册类
     *
     * @author Gloduck
     * @date 2022/04/25
     */
    @Component
    public class LogStrategyRegistry implements ApplicationContextAware {
        private final Logger logger = LoggerFactory.getLogger(LogStrategyRegistry.class);
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            Map<String, LogSaveStrategy> saveStrategyMap = applicationContext.getBeansOfType(LogSaveStrategy.class);
            saveStrategyMap.forEach((beanName, logSaveStrategy) -> {
                logger.info("Register log save strategy ({}) in container.", logSaveStrategy.getStrategyName());
                LogStrategyContext.registryStrategy(logSaveStrategy);
            });
        }
    }
    
    
    • 代码很简单,只是从BeanFactory中获取实现了LogSaveStrategy接口的Bean,然后注册到容器中。
  • 这样,我们就完成了策略模式的实现。

SpEl实现

SpEl解析

  • SpEl的简单使用,已经在上面给出来了。但是既然需要是与框架集成的东西,肯定不可能走上面的实现去解析的。

  • 这里需要对两个核心类有一定的了解,一个是:CachedExpressionEvaluator;一个是:MethodBasedEvaluationContext

  • 前者可以点开看一下源码,很简单,里面主要的就是一个SpelExpressionParser,就是上面用来解析SpEl的那个类。这个类的独特之处就在于,它是可以缓存表达式的,防止多次解析SpEl表达式。我们重点关注一下这个方法:

    protected Expression getExpression(Map<ExpressionKey, Expression> cache,
                                       AnnotatedElementKey elementKey, String expression) {
    
        ExpressionKey expressionKey = createKey(elementKey, expression);
        Expression expr = cache.get(expressionKey);
        if (expr == null) {
            expr = parseExpression(expression);
            cache.put(expressionKey, expr);
        }
        return expr;
    }
    
    • 它干的事情很简单,方法传入一个cache,如果cache有解析结果,就直接取出来用,否则就解析后放在缓存里面。
  • 基于此,我们可以提供一个子类实现,子类的成员变量维护一个缓存。如下:

    /**
     * 日志记录表达式求值程序
     *
     * @author Gloduck
     * @date 2022/04/25
     */
    public class LogRecordExpressionEvaluator extends CachedExpressionEvaluator {
        private final Logger logger = LoggerFactory.getLogger(LogRecordExpressionEvaluator.class);
    
        private final Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);
    
        /**
         * 解析值
         *
         * @param keyExpression spel表达式
         * @param methodKey     方法描述
         * @param evalContext   eval上下文
         * @return {@link String}
         */
        public String parseValue(String keyExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
            String parseExpression;
            try {
                parseExpression = getExpression(expressionCache, methodKey, keyExpression).getValue(evalContext, String.class);
            } catch (Exception e) {
                parseExpression = keyExpression;
                logger.error("Parse expression [" + keyExpression + "] error, check out if the expression is correct, we will return expression itself.", e);
            }
            return parseExpression;
        }
    }
    
    
    • 这个子类就是我们后面用来解析我们SpEl的类,它就定义了个缓存,同时对getExpression方法封装了一下,直接在解析完成后调用getValue获取值,同时捕捉了一下异常,使得在解析出现异常的时候,直接返回表达式,防止因为这里跑出来的RuntimeException来影响我们后续的业务代码。
  • 对于后者,MethodBasedEvaluationContext这个类其实就是保存上下文的地方,SpEl会从这里尝试获取在解析SpEl表达式时,可能需要的变量等东西。

    • 为什么叫MethodBased呢?因为在Java被编译成类后,是不会有方法参数名称的信息的,这就需要我们从其他可能的地方去获取参数名称。不知道在使用Spring提供的支持SpEl注解的时候有没有发现这样一个问题。就是在取值的时候我们可以通过以下三种方式来获取变量值:

      @Cacheable(condition = "{#param.money}")
      @Cacheable(condition = "{#p0.money}")
      @Cacheable(condition = "{#a0.money}")
      
    • 这个就是因为在MethodBasedEvaluationContext这个类的lazyLoadArguments()方法里面,同时在Map中同时用三个不同的名称对这个变量存放了一边。因此可以这样取值(具体请看代码实现)。

自定义语法

  • 因为我们注解上的content是支持SpEl的,但是SpEl来处理字符串看着是很不舒服的。就像这样:

    @Cacheable(condition = "'登录用户为:' + {#user.userId}")
    
  • 所以我们需要自定义一套语法,使用占位符${}来实现,占位符里面是SpEl表达式。这样我们content里面的内容就可以这样编写:

    "用户:${{transfer.fromUser}} 向用户:${{transfer.toUser}}转了一笔账,时间为:${T(System).currentTimeMillis()}"
    
  • 这意味着我们需要一套单独的代码来解析语法(注,本人算法较菜,下面解析可能没考虑到边界条件等。如有算法大佬,建议自己优化一下):

    /**
         * 替换占位符中的值
         *
         * @param expression 目标表达式
         * @param varMap     变量map映射
         * @return {@link String}
         */
    public static String replacePlaceHolder(String expression, Map<String, String> varMap, List<PlaceHolder> placeHolders){
        if(placeHolders.size() == 0){
            return expression;
        }
        StringBuilder builder = new StringBuilder();
        char[] chars = expression.toCharArray();
        Iterator<PlaceHolder> iterator = placeHolders.iterator();
        PlaceHolder placeHolder = iterator.next();
        for (int i = 0; i < chars.length; i++) {
            if(i < placeHolder.startIndex){
                builder.append(chars[i]);
            }else if(i == placeHolder.endIndex){
                builder.append(varMap.get(placeHolder.value));
                if(iterator.hasNext()){
                    placeHolder = iterator.next();
                } else if(i + 1 < chars.length){
                    builder.append(expression.substring(i + 1));
                }
            }
    
        }
        return builder.toString();
    }
    /**
         * 解析出表达式中占位符的内容
         *
         * @param expression 表达式
         * @return {@link List}<{@link String}>
         */
    public static List<PlaceHolder> parsePlaceHolder(String expression){
        char[] chars = expression.toCharArray();
        List<PlaceHolder> resList = new ArrayList<>();
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < chars.length; i++) {
            if(chars[i] == '$'){
                continue;
            }
            if(chars[i] == '{' && i - 1 >= 0 && chars[i - 1] == '$'){
                // 找到占位符开始符
                int count = 1;
                int j = i + 1;
                for (; j < chars.length; j++) {
                    // 括号匹配结束符
                    if(chars[j] == '{'){
                        count++;
                    } else if(chars[j] == '}'){
                        count--;
                    }
                    if(count == 0){
                        PlaceHolder holder = new PlaceHolder(i - 1, j, builder.toString());
                        resList.add(holder);
                        builder = new StringBuilder();
                        break;
                    }
                    builder.append(chars[j]);
                }
                i = j + 1;
            }
        }
        return resList;
    }
    @Getter
    private static class PlaceHolder{
        private final int startIndex;
        private final int endIndex;
        private final String value;
    
        public PlaceHolder(int startIndex, int endIndex, String value) {
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.value = value;
        }
    }
    
  • 这样就能通过parsePlaceHolder来解析出占位符中的表达式,然后进行解析,并通过replacePlaceHolder将字符串里的占位符替换。

代码中传递变量

  • 有时候,变量可能并不能单单从请求参数或返回值中拿到,可能需要一些业务代码中动态赋值的变量。所以我们引入了一个单例,LogRecordHolder来用于保存用户在业务代码中传的变量,并在最后放在MethodBasedEvaluationContext以提供给SpEl解析。(尽管这并不是一个优雅的设计

  • 代码如下:

    /**
     * Log变量保持器。
     * 如果需要在SpEl插入额外的变量,可以通过此类来设置。
     *
     * @author Gloduck
     * @date 2022/04/26
     */
    public class LogRecordHolder {
        private static final InheritableThreadLocal<Stack<Map<String, Object>>> variableMapStack = new InheritableThreadLocal<>();
    
        /**
         * 放入方法变量
         *
         * @param key  键
         * @param value 值
         */
        public static void putVariable(String key, Object value) {
            getVariableMap(false).put(key, value);
        }
    
    
        /**
         * 获取方法变量
         *
         * @param key 键
         * @return {@link Object}
         */
        public static Object getVariable(String key) {
            return getVariableMap(false).get(key);
        }
    
        /**
         * 获取方法变量Map
         *
         * @return {@link Map}<{@link String}, {@link Object}>
         */
        public static Map<String, Object> getVariableMap() {
            return getVariableMap(false);
        }
    
        /**
         * 清理Log变量Map
         */
         static void clear() {
            Stack<Map<String, Object>> mapStack = variableMapStack.get();
            if(mapStack == null || mapStack.size() == 0){
                variableMapStack.remove();
            } else {
                mapStack.pop();
            }
        }
    
    
        /**
         * 获取变量map
         *
         * @param createNew 是否创建一个新的map
         * @return {@link Map}<{@link String}, {@link Object}>
         */
        static Map<String, Object> getVariableMap(boolean createNew){
            Stack<Map<String, Object>> mapStack = variableMapStack.get();
            if (mapStack == null) {
                mapStack = new Stack<>();
                variableMapStack.set(mapStack);
            }
            Map<String, Object> variableMap;
            if(createNew || mapStack.size() == 0){
                variableMap = new HashMap<>(4);
                mapStack.push(variableMap);
            } else {
                variableMap = mapStack.peek();
            }
            return variableMap;
        }
    }
    
  • 这里为什么要使用InheritableThreadLocalStack建议参考美团技术团队的文章中[日志上下文实现]部分,上面给出了原因。此处就不过多的赘述。

切面代码

  • 在有了上面所有的代码作为铺垫后,下面是最重要的切面的代码:

    @Aspect
    @Component
    public class LogAspect {
        private final Logger logger = LoggerFactory.getLogger(LogAspect.class);
    
    
        /**
         * Log SpEl表达式
         */
        private final LogRecordExpressionEvaluator evaluator = new LogRecordExpressionEvaluator();
    
    
        private final static String TRUE = "true";
    
        private final static String FALSE = "false";
    
        public static final Object NO_RESULT = new Object();
    
    
        /**
         * 切入点
         */
        @Pointcut("@annotation(logRecord)")
        public void pointcut(LogRecord logRecord) {
        }
    
    
        @Around(value = "pointcut(logRecord)", argNames = "joinPoint,logRecord")
        public Object process(ProceedingJoinPoint joinPoint, LogRecord logRecord) throws Throwable {
            Object result = null;
            Throwable exception = null;
    
            // 初始化JoinPoint的数据
            Object target = joinPoint.getThis();
            if (target == null) {
                target = NO_RESULT;
            }
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            Object[] args = joinPoint.getArgs();
            EvaluationContext context = new MethodBasedEvaluationContext(target, method, args, new DefaultParameterNameDiscoverer());
            AnnotatedElementKey elementKey = new AnnotatedElementKey(method, target.getClass());
            LogRecordHolder.getVariableMap(true);
    
            // 执行具体逻辑代码
            long startTime = System.currentTimeMillis();
            try {
                result = joinPoint.proceed();
            } catch (Throwable e) {
                exception = e;
            }
            long endTime = System.currentTimeMillis();
            // 记录日志
            boolean saveLog = condition(logRecord.condition(), elementKey, context);
            if (saveLog) {
                context.setVariable("_res", result);
                context.setVariable("_ex", exception);
                Map<String, Object> variableMap = LogRecordHolder.getVariableMap(false);
                variableMap.forEach(context::setVariable);
                String expressionContent = logRecord.content();
                String content = null;
                if(expressionContent != null){
                    List<PlaceHolder> placeHolders = parsePlaceHolder(expressionContent);
                    Map<String, String> parseValMap = placeHolders.stream().map(PlaceHolder::getValue).distinct().collect(Collectors.toMap(s -> s, o -> evaluator.parseValue(o, elementKey, context)));
                    content = replacePlaceHolder(expressionContent, parseValMap, placeHolders);
                }
                LogEntity entity = new LogEntity(logRecord.method(), content, logRecord.group(), args, endTime - startTime, exception);
                doRecord(logRecord.saveStrategy(), entity);
            }
            LogRecordHolder.clear();
            logger.debug("LogAspect execute Log and parse spel cost {} millisecond.", System.currentTimeMillis() - endTime);
    
    
            // 抛出执行业务时的异常
            if (exception != null) {
                throw exception;
            }
            return result;
        }
    
        /**
         * 替换占位符中的值
         *
         * @param expression 目标表达式
         * @param varMap     变量map映射
         * @return {@link String}
         */
        public static String replacePlaceHolder(String expression, Map<String, String> varMap, List<PlaceHolder> placeHolders){
            if(placeHolders.size() == 0){
                return expression;
            }
            StringBuilder builder = new StringBuilder();
            char[] chars = expression.toCharArray();
            Iterator<PlaceHolder> iterator = placeHolders.iterator();
            PlaceHolder placeHolder = iterator.next();
            for (int i = 0; i < chars.length; i++) {
                if(i < placeHolder.startIndex){
                    builder.append(chars[i]);
                }else if(i == placeHolder.endIndex){
                    builder.append(varMap.get(placeHolder.value));
                    if(iterator.hasNext()){
                        placeHolder = iterator.next();
                    } else if(i + 1 < chars.length){
                        builder.append(expression.substring(i + 1));
                    }
                }
    
            }
            return builder.toString();
        }
        /**
         * 解析出表达式中占位符的内容
         *
         * @param expression 表达式
         * @return {@link List}<{@link String}>
         */
        public static List<PlaceHolder> parsePlaceHolder(String expression){
            char[] chars = expression.toCharArray();
            List<PlaceHolder> resList = new ArrayList<>();
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < chars.length; i++) {
                if(chars[i] == '$'){
                    continue;
                }
                if(chars[i] == '{' && i - 1 >= 0 && chars[i - 1] == '$'){
                    // 找到占位符开始符
                    int count = 1;
                    int j = i + 1;
                    for (; j < chars.length; j++) {
                        // 括号匹配结束符
                        if(chars[j] == '{'){
                            count++;
                        } else if(chars[j] == '}'){
                            count--;
                        }
                        if(count == 0){
                            PlaceHolder holder = new PlaceHolder(i - 1, j, builder.toString());
                            resList.add(holder);
                            builder = new StringBuilder();
                            break;
                        }
                        builder.append(chars[j]);
                    }
                    i = j + 1;
                }
            }
            return resList;
        }
    
    
    
        /**
         * 记录日志
         *
         * @param strategy 策略
         * @param entity   日志实体
         */
        private void doRecord(String strategy, LogEntity entity) {
            LogSaveStrategy logSaveStrategy = LogStrategyContext.getStrategy(strategy);
            if (logSaveStrategy == null) {
                /// 获取不到策略,则不记录
                logger.warn("The strategy ({}) annotated in LogRecord can't be found, check out if the strategy has been registered", strategy);
                return;
            }
            logSaveStrategy.saveLog(entity);
        }
    
    
        /**
         * 根据条件判断是否需要记录日志,同时如果字符串为true或false直接进行判断,而不需要走el表达式。
         *
         * @param expression 表达式
         * @param elementKey 关键元素
         * @param context    上下文
         * @return boolean
         */
        private boolean condition(String expression, AnnotatedElementKey elementKey, EvaluationContext context) {
            if (TRUE.equalsIgnoreCase(expression)) {
                return true;
            }
            if (FALSE.equalsIgnoreCase(expression)) {
                return false;
            }
            String parseValue = evaluator.parseValue(expression, elementKey, context);
            if(!(TRUE.equals(parseValue) || FALSE.equals(parseValue))){
                logger.warn("Condition return false for expression [{}], because the expression is invalid or return invalid boolean value, check out you spel expression",
                        expression);
                return false;
            }
            return TRUE.equals(parseValue);
        }
    
    
        @Getter
        private static class PlaceHolder{
            private final int startIndex;
            private final int endIndex;
            private final String value;
    
            public PlaceHolder(int startIndex, int endIndex, String value) {
                this.startIndex = startIndex;
                this.endIndex = endIndex;
                this.value = value;
            }
        }
    
    }
    
  • 这里使用了AspectJ来做动态代理,所以这意味着你需要引入AspectJ的依赖。

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
    
  • 代码里注释已经写的比较全了,这里不做过多的赘述了。核心处理代码位于:process方法上,然后调度策略模式进行日志记录的代码位于:doRecord上。

使用

  • 在上面代码定义好后,使用起来就比较简单了。

  • 首先,我们需要实现两个日志记录的策略。并将其定义为Bean。(我们没有提供默认的实现)。这里为了方便,直接使用System.out.println来模拟。

    @Service
    public class RedisLogSaveStrategy implements LogSaveStrategy {
        @Override
        public void saveLog(LogEntity log) {
            System.out.println("使用Redis进行了存储,值为:" + log.toString());
        }
    
        @Override
        public String getStrategyName() {
            return "redis";
        }
    }
    
    @Service
    public class MySqlLogSaveStrategy implements LogSaveStrategy {
        @Override
        public void saveLog(LogEntity log) {
            System.out.println("使用MySql进行了存储,值为:" + log.toString());
        }
    
        @Override
        public String getStrategyName() {
            return "mysql";
        }
    }
    
  • 然后编写一个转账接口,并且使用我们的注解定义。其中content中有4个变量,date变量是业务代码里面通过LogRecordHolder赋值的,其余变量是参数里获取的。然后使用mysql指定,使用mysql策略来存储。

    • 返回值通过_res获取,异常通过_ex获取。但请尽量不要使用,因为如果是使用ControlerAdvice + ExceptionHandler来处理异常的话,在业务代码抛出异常的时候就直接_res是获取不到值的。因为根本没有返回值。
        @LogRecord(method = "Controller层转账方法", content = "用户:${{#param.fromUserId}} 于时间点 ${{#date}} 向用户:${{#param.toUserId}} 转账:${{#param.money}} 元", saveStrategy = "mysql")
        @PostMapping("")
        public Result<Object> transferMoney(@RequestBody TransferParam param){
            LogRecordHolder.putVariable("date", System.currentTimeMillis());
            return transferService.doTransferMoney(param.fromUserId, param.toUserId, param.money) ? Result.success() : Result.failed();
        }
    
  • 然后在Controller层调用的Service层也加一个注解:

    @Service
    public class TransferService {
        @LogRecord(method = "Service层转账方法", content = "用户:${{#fromUserId}} 于时间点 ${T(System).currentTimeMillis()} 向用户:${{#toUserId}} 转账:${{#money}} 元", saveStrategy = "redis")
        public boolean doTransferMoney(Integer fromUserId, Integer toUserId, Double money){
            /* do something */
    
            return true;
        }
    }
    
  • 最后,启动应用。

  • 通过http client进行请求

    POST http://localhost:8080/transfer
    Content-Type: application/json
    
    {"fromUserId": 1000, "toUserId": 2000, "money": 32.2}
    
  • 打印出来的结果为:

    使用Redis进行了存储,值为:LogEntity(method=Service层转账方法, content=用户:1000 于时间点 1651027193983 向用户:2000 转账:32.2 元, group=, params=[1000, 2000, 32.2], costTime=3, exception=null)
    使用MySql进行了存储,值为:LogEntity(method=Controller层转账方法, content=用户:1000 于时间点 1651027193963 向用户:2000 转账:32.2 元, group=, params=[TransferController.TransferParam(fromUserId=1000, toUserId=2000, money=32.2)], costTime=42, exception=null)
    
  • 很明显,已经达到了我们想要的目的。


下一篇: Graalvm打包报错:UnsupportedCharsetException解决方案→

loading...