一、功能介绍
在SpringBoot的项目应用中,常常需要记录日志到数据库中,例如系统访问日志(登录日志)、用户操作日志等,日志文本中需要含有动态的参数(发送http请求的请求参数),此情景下,可基于Spring Expression Language的动态日志信息记录功能。
二、需求场景
场景需求如下:
package com.tang.demo.controller;
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.tang.demo.annotation.Log;
import com.tang.demo.domain.ResponseResult;
@RestController
public class TestController {
@Log(name = "测试模块", description = "'添加一个用户,用户名为:'+#person.name")
@RequestMapping(value = "/test", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
public ResponseResult<Object> getModelNames(@RequestBody @Valid Person person, BindingResult bindResult) {
if (bindResult.getErrorCount() > 0) {
return ResponseResult.failed(-1, bindResult.getFieldError().getDefaultMessage());
}
return ResponseResult.success(person);
}
}
动态拼接参数的日志最终会保存到数据库中,就像redis缓存中key的动态拼接,@Cacheable(value=“RptGroupAgent”,key="‘localAgentName’+#localAgentName") 。
接下来我们通过自定义注解来实现这个功能。
三、实现方法
我们可通过自定义注解、AOP切面、SpEL表达式等来实现,具体步骤如下:
1、定义注解@Log
package com.tang.demo.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
String name() default "";
String description() default "";
}
2、AOP切面的处理
package com.tang.demo.aspect;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import com.tang.demo.annotation.Log;
@Aspect
@Component
public class LogAdvice {
private final static Logger logger = LoggerFactory.getLogger(LogAdvice.class);
private final static ExpressionParser spelParser = new SpelExpressionParser();
@Pointcut("@annotation(com.tang.demo.annotation.Log)")
public void methodAspect() {
}
@After("methodAspect()")
public void after(JoinPoint joinPoint) throws IOException {
logger.info("开始记录日志*************************");
// 获取方法的参数名和参数值
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
List<String> paramNameList = Arrays.asList(methodSignature.getParameterNames());
List<Object> paramList = Arrays.asList(joinPoint.getArgs());
// 将方法的参数名和参数值一一对应的放入上下文中
EvaluationContext ctx = new StandardEvaluationContext();
for (int i = 0; i < paramNameList.size(); i++) {
ctx.setVariable(paramNameList.get(i), paramList.get(i));
}
Method method = methodSignature.getMethod();
Log myAnnotation = method.getAnnotation(Log.class);
// 解析SpEL表达式获取结果
String description = spelParser.parseExpression(myAnnotation.description()).getValue(ctx).toString();
saveLog(myAnnotation.name(), description);
}
@AfterReturning(pointcut = "methodAspect()", returning = "returnValue")
public void afterreturningJoinPoint(JoinPoint joinPoint, Object returnValue) {
}
@Around("methodAspect()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object object = pjp.proceed();
return object;
}
public static void saveLog(String name, String description) {
logger.info(name + "----->" + description);
}
}
3、SpEL表达式解析
在上述代码中:
Log myAnnotation = method.getAnnotation(Log.class);
// 解析SpEL表达式获取结果
String description = spelParser.parseExpression(myAnnotation.description()).getValue(ctx).toString();
的代码片段里对自定义主机@Log的description字段的文本内容进行EL表达式的解析处理,获取完整的字符串结果。
4、编写测试用的实体类
package com.tang.demo.domain;
import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Person{
@NotNull(message = "id不能为空")
private Integer id;
@NotBlank(message = "name不能为空")
private String name;
@NotNull(message = "salary不能为空")
private Float salary;
@NotNull(message = "intrersts不能为空")
private List<String> intrersts;
}
5、编写统一响应的实体类
package com.tang.demo.domain;
import java.io.Serializable;
public class ResponseResult<T> implements Serializable {
private static final long serialVersionUID = -6964481356983562044L;
public static final Integer SUCCESS_STATUS = 0;
public static final String SUCCESS_MESSAGE = "success";
private Integer status;
private String message;
private T data;
public ResponseResult(String message, Integer code) {
this.message = message;
this.status = code;
}
public ResponseResult(String message, Integer code, T data) {
this.message = message;
this.status = code;
this.data = data;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
/
public static <T> ResponseResult<T> success() {
return new ResponseResult<T>(SUCCESS_MESSAGE, SUCCESS_STATUS, null);
}
public static <T> ResponseResult<T> success(T data) {
return new ResponseResult<T>(SUCCESS_MESSAGE, SUCCESS_STATUS, data);
}
public static <T> ResponseResult<T> failed(Integer errorCode, String message) {
return new ResponseResult<T>(message, errorCode);
}
}
6、编写测试用的Controller
package com.tang.demo.controller;
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.tang.demo.annotation.Log;
import com.tang.demo.domain.Person;
import com.tang.demo.domain.ResponseResult;
@RestController
public class TestController {
@Log(name = "测试模块", description = "'添加一个用户,用户名为:'+#person.name")
@RequestMapping(value = "/test", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
public ResponseResult<Object> getModelNames(@RequestBody @Valid Person person, BindingResult bindResult) {
if (bindResult.getErrorCount() > 0) {
return ResponseResult.failed(-1, bindResult.getFieldError().getDefaultMessage());
}
return ResponseResult.success(person);
}
}
7、编写启动类
package com.tang.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoLearnApplication {
public static void main(String[] args) {
SpringApplication.run(DemoLearnApplication.class, args);
}
}
8、启动程序并进行测试:
curl -X POST -H ‘Content-Type: application/json’ -d ‘{“id”:1,“name”:“hello”,“salary”:100.2,“intrersts”:[“song”,“football”]}’ ‘http://127.0.0.1:8080/test’
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.0)
2020-12-03 09:54:56.856 INFO 27248 --- [ main] com.tang.demo.DemoLearnApplication : Starting DemoLearnApplication using Java 1.8.0_131 on DESKTOP-HSUNT2S with PID 27248 (D:\Projects\JavaProject\controller-logger\target\classes started by admin in D:\Projects\JavaProject\controller-logger)
2020-12-03 09:54:56.857 INFO 27248 --- [ main] com.tang.demo.DemoLearnApplication : No active profile set, falling back to default profiles: default
2020-12-03 09:54:57.378 INFO 27248 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-12-03 09:54:57.389 INFO 27248 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-12-03 09:54:57.389 INFO 27248 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.39]
2020-12-03 09:54:57.421 INFO 27248 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-12-03 09:54:57.421 INFO 27248 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 542 ms
2020-12-03 09:54:57.543 INFO 27248 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-12-03 09:54:57.642 INFO 27248 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-12-03 09:54:57.648 INFO 27248 --- [ main] com.tang.demo.DemoLearnApplication : Started DemoLearnApplication in 0.97 seconds (JVM running for 1.419)
2020-12-03 09:55:02.086 INFO 27248 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-12-03 09:55:02.087 INFO 27248 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-12-03 09:55:02.087 INFO 27248 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
2020-12-03 09:55:02.171 INFO 27248 --- [nio-8080-exec-1] com.tang.demo.aspect.LogAdvice : 开始记录日志*************************
2020-12-03 09:55:02.238 INFO 27248 --- [nio-8080-exec-1] com.tang.demo.aspect.LogAdvice : 测试模块----->添加一个用户,用户名为:hello
四、完整代码
https://github.com/tangyibo/controller-logger