使用SpEL记录操作日志的详细信息

操作日志

操作日志就是记录用户请求了什么接口,干了啥事儿。常见且简单的实现就是通过spring的aop + 自定义注解完成。

HandlerMethod方法上标识自定义注解,在注解上设置一些自定义的基本属性,例如字符串属性 operation: "删除了用户信息"。
在Aop切面中,获取到这个注解,读取到预定义的信息,配合当前用户的Token,那么就确定了执行操作的用户和执行的操作。就可以创建一条“操作日志”。

不够详细

这种方式,也有一个缺点显而易见,就是日志的内容,被固定住了。我们只能知道“谁删除了用户”,但是不知道删除了哪些用户。
“被删除用户”的请求信息包含在了请求体或者是查询参数中,在Aop切面中,可以获取到request/response/Handler的参数等对象
但是不同的业务接口,参数一般都不相同,这就导致同一个日记录器不能公用。为每一个接口,写一个日志记录Aop?这很显然不是一个很好的办法。

SpEL

SpEL(Spring Expression Language),Spring表达式语言。通俗的理解,就是可以通过一些字符串的表达式,完成一些“编程”的功能。
例如,读取/设置某些对象的属性。

有了这个东西后,我们就可以在日志注解中,设置一些字符串形式“表达式”,通过表达式来完成对某些属性(body/header/param)的读取。
这就非常地灵活,不同的接口,表达式不同,就可以读取到不同的信息,生成不同的日志。

学习SpEL详情可以查看官方文档,这里不会涉及太多。

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions

演示

OperationLog

自定义的日志注解

package io.springcloud.web.log;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 
 * 访问日志注解
 * @author KevinBlandy
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
    
    /**
     * SpEL 模板表达式
     * 当前HandlerMethod的所有形参都会被放入到SpEL的Context,名称就是方法参数名称。
     * @return
     */
    String expression();
}

OperationLogAop

日志的Aop实现

package io.springcloud.web.log;

import java.lang.reflect.Method;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import lombok.extern.slf4j.Slf4j;

@Aspect  
@Component 
@Order(-1)
@Slf4j
public class OperationLogAop {

    // 需要被SpEl解析的模板前缀和后缀 {{ expression  }}
    public static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext("{{", "}}");

    // SpEL解析器
    public static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
    
    
    @Pointcut(value="@annotation(io.springcloud.web.log.OperationLog)")
    public void controller() {};
    
    @Before(value = "controller()")
    public void actionLog (JoinPoint joinPoint) throws Throwable {
        
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        // 参数
        Object[] args = joinPoint.getArgs();
        
        // 参数名称
        String[] parameterNames = signature.getParameterNames();

        // 目标方法
        Method targetMethod = signature.getMethod();

        // 方法上的日志注解
        OperationLog operationLog = targetMethod.getAnnotation(OperationLog.class);

        // request
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        // response
        // HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        
        try {
            /**
             * SpEL解析的上下文,把 HandlerMethod 的形参都添加到上下文中,并且使用参数名称作为KEY
             */
            EvaluationContext evaluationContext = new StandardEvaluationContext();
            for (int i = 0; i < args.length; i ++) {
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
            
            String logContent = EXPRESSION_PARSER.parseExpression(operationLog.expression(), TEMPLATE_PARSER_CONTEXT).getValue(evaluationContext, String.class);
            
            // TODO 异步存储日志
            
            log.info("operationLog={}", logContent);
        } catch (Exception e) {
            log.error("操作日志SpEL表达式解析异常: {}", e.getMessage());
        }
    }
}

Controller

package io.springcloud.web.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import lombok.Data;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import io.springcloud.web.log.OperationLog;

import java.util.List;

@Data
class Payload {
    @NotNull
    private Integer id;
    @NotBlank
    private String userName;
    @Size(min = 1, max = 5)
    private List<String> hobby;
}

@RestController
@RequestMapping("/test")
@Validated
public class TestController {

    /**
     * expression,中通过 {{ expression }} 来设置表达式,通过 '#变量名称' 可以访问到Handler形参的属性,方法。
     * Handler形参名称,就是表达式中的变量名称
     */
    @PostMapping
    @OperationLog(expression = "更新了用户"
            + ", userAgent = {{ #request.getHeader('User-Agent') }}"
            + ", action = {{ #action }}"
            + ", id = {{ #payload.id }}"
            + ", userName = {{ #payload.userName }}"
            + ", hobbyLength = {{ #payload.hobby.size() }}")
    public Object test (HttpServletRequest request,
                        HttpServletResponse response,
                        @RequestParam("action") String action,
                        @RequestBody @Validated Payload payload) {
        return ResponseEntity.ok(payload);
    }
}

测试

Request/Response

POST /test?action=update HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: c9564e75-03ad-42f0-8fea-5e34912bcfb8
Host: localhost
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 80
 
{
"id": "1",
"userName": "cxk",
"hobby": ["唱", "跳", "Rap"]
}
 
HTTP/1.1 200 OK
X-Response-Time: 2
Connection: keep-alive
Server: PHP/7.3.1
X-Request-Id: 553b4c5d-5363-4e53-9978-a0671e9aa16d
Content-Type: application/json;charset=UTF-8
Content-Length: 84
Date: Tue, 19 Oct 2021 04:28:46 GMT
 
{
"id": 1,
"userName": "cxk",
"hobby": [
"唱",
"跳",
"Rap"
]
}

日志

成功读取到了对应的属性

2021-10-19 12:28:46.831  INFO 2692 --- [  XNIO-1 task-1] io.springcloud.web.log.OperationLogAop   : operationLog=更新了用户, userAgent = PostmanRuntime/7.28.4, action = update, id = 1, userName = cxk, hobbyLength = 3

首发:https://springboot.io/t/topic/4248

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容