zoukankan      html  css  js  c++  java
  • 使用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

  • 相关阅读:
    CCF NOI1032 菱形
    CCF NOI1031 等腰三角形
    CCF NOI1030 角谷猜想
    CCF NOI1029 信息加密
    CCF NOI1028 判断互质
    CCF NOI1027 数字之和
    CCF NOI1026 表演打分
    CCF NOI1025 统计奖牌
    CCF NOI1024 因子个数
    CCF NOI1023 最大跨度
  • 原文地址:https://www.cnblogs.com/kevinblandy/p/15429446.html
Copyright © 2011-2022 走看看