一、SpringBoot记录日志
文章目录
- 一、SpringBoot记录日志
- 1.1、环境搭建
- 1.2、配置FastJson
- 1.3、自定义LogRecord注解
- 1.4、定义日志实体类
- 1.5、创建HttpRequestUtil工具类
- 1.6、定义AOP切面
- 1.7、编写测试类
- 1.8、运行测试
1.1、环境搭建
- 搭建SpringBoot工程。
- 引入【spring-boot-starter-parent】依赖。
- 引入【spring-boot-starter-web】依赖。
- 引入【spring-boot-starter-aop】依赖。
- 引入【fastjson】依赖。
<!-- 引入 SpringBoot 父工程依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<!-- 引入 web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除 jackson 依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入 aop 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 引入 fastjson 依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.77</version>
</dependency>
1.2、配置FastJson
package com.spring.boot.demo.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* @author Js
* @version 1.0.0
* @Date: 2023/11/02 12:47
* @Description FastJson 配置类
*/
@Configuration
public class CustomFastJsonConfig {
@Bean
public HttpMessageConverters fastjsonHttpMessageConverters() {
// 创建 FastJsonHttpMessageConverter 消息转换器对象
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
// 创建 FastJsonConfig 配置类对象
FastJsonConfig fastJsonConfig = new FastJsonConfig();
// 设置编码字符集
fastJsonConfig.setCharset(StandardCharsets.UTF_8);
// 设置日期格式
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
// 设置序列化特征: SerializerFeature 是一个枚举,可以选择不同的序列化特征
SerializerFeature[] serializerFeatures = new SerializerFeature[] {
// WriteNullStringAsEmpty: 如果字符串等于 null,那么会被序列化成空字符串 ""
SerializerFeature.WriteNullStringAsEmpty,
// WriteNullNumberAsZero: 如果数字等于 null,那么会被序列化成 0
SerializerFeature.WriteNullNumberAsZero,
// WriteNullBooleanAsFalse: 如果布尔类型等于 null,那么会被序列化成 false
SerializerFeature.WriteNullBooleanAsFalse,
// PrettyFormat: 美化JSON
SerializerFeature.PrettyFormat
};
fastJsonConfig.setSerializerFeatures(serializerFeatures);
// 配置添加到消息转换器里面
fastJsonHttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
// 设置响应JSON格式数据
List<MediaType> mediaTypeList = new ArrayList<>();
mediaTypeList.add(MediaType.APPLICATION_JSON); // JSON 格式数据
// 设置消息转换器支持的格式
fastJsonHttpMessageConverter.setSupportedMediaTypes(mediaTypeList);
// 返回消息转换器
return new HttpMessageConverters(fastJsonHttpMessageConverter);
}
}
1.3、自定义LogRecord注解
- 这里我们自定义一个@LogRecord注解,该注解使用在方法上面,用于标记AOP切面会拦截这个方法,并且记录请求日志信息。
package com.spring.boot.demo.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Js
* @version 1.0.0
* @Date: 2023/11/02 12:47
* @Description 自定义日志注解
*/
// 注解可以保留到运行期间
@Retention(RetentionPolicy.RUNTIME)
// 注解使用在方法上面
@Target(ElementType.METHOD)
public @interface LogRecord {
/**
* 操作名称
*/
String opName();
/**
* 描述信息
*/
String desc() default "";
}
1.4、定义日志实体类
为了能够收集请求日志的信息,这里定义一个日志实体类来保存每一次请求的日志信息。
package com.spring.boot.demo.pojo;
import java.io.Serializable;
/**
* @author Js
* @version 1.0.0
* @Date: 2023/11/2 22:45
* @Description 日志实体类
*/
public class LogRecordEntity implements Serializable {
/** 日志唯一标识 */
private String id;
/** 操作名称 */
private String opName;
/** 请求路径 */
private String path;
/** 请求方式 */
private String method;
/** 请求IP地址 */
private String requestIp;
/** 全限定类名称 */
private String qualifiedName;
/** 请求入参 */
private String inputParam;
/** 请求出参 */
private String outputParam;
/** 异常信息 */
private String errorMsg;
/** 请求开始时间 */
private String requestTime;
/** 请求响应时间 */
private String responseTime;
/** 接口耗时,单位:ms */
private String costTime;
/** 请求是否成功 */
private String status;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getOpName() {
return opName;
}
public void setOpName(String opName) {
this.opName = opName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getRequestIp() {
return requestIp;
}
public void setRequestIp(String requestIp) {
this.requestIp = requestIp;
}
public String getQualifiedName() {
return qualifiedName;
}
public void setQualifiedName(String qualifiedName) {
this.qualifiedName = qualifiedName;
}
public String getInputParam() {
return inputParam;
}
public void setInputParam(String inputParam) {
this.inputParam = inputParam;
}
public String getOutputParam() {
return outputParam;
}
public void setOutputParam(String outputParam) {
this.outputParam = outputParam;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public String getRequestTime() {
return requestTime;
}
public void setRequestTime(String requestTime) {
this.requestTime = requestTime;
}
public String getResponseTime() {
return responseTime;
}
public void setResponseTime(String responseTime) {
this.responseTime = responseTime;
}
public String getCostTime() {
return costTime;
}
public void setCostTime(String costTime) {
this.costTime = costTime;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@Override
public String toString() {
return "LogRecordEntity{" +
"id='" + id + '\'' +
", opName='" + opName + '\'' +
", path='" + path + '\'' +
", method='" + method + '\'' +
", requestIp='" + requestIp + '\'' +
", qualifiedName='" + qualifiedName + '\'' +
", inputParam='" + inputParam + '\'' +
", outputParam='" + outputParam + '\'' +
", errorMsg='" + errorMsg + '\'' +
", requestTime='" + requestTime + '\'' +
", responseTime='" + responseTime + '\'' +
", costTime='" + costTime + '\'' +
", status='" + status + '\'' +
'}';
}
}
1.5、创建HttpRequestUtil工具类
- 创建一个获取HTTP请求和响应对象的工具类,在SpringBoot框架中,可以通过RequestContextHolder类获取到HTTP请求属性对象,通过该对象可以获取到Request、Response对象。
package com.spring.boot.demo.util;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Js
* @version 1.0.0
* @Date: 2023/11/2 23:03
* @Description HTTP请求的工具类,用于获取Request、Response相关信息
*/
public final class HttpRequestUtil {
/**
* 从 SpringBoot 中获取 Request 请求对象
* @return 返回当前请求的 Request 对象
*/
public static HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return null;
}
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
return attributes.getRequest();
}
/**
* 从 SpringBoot 中获取 Response 请求对象
* @return 返回当前请求的 Response 对象
*/
public static HttpServletResponse getResponse() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return null;
}
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
return attributes.getResponse();
}
}
1.6、定义AOP切面
package com.spring.boot.demo.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.spring.boot.demo.anno.LogRecord;
import com.spring.boot.demo.pojo.LogRecordEntity;
import com.spring.boot.demo.util.HttpRequestUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* @author Js
* @version 1.0.0
* @Date: 2023/11/2 12:52
* @Description 自定义日志切面
*/
// 标记当前类是一个切面类
@Aspect
// 将当前类放入IOC容器
@Component
public class LogAspect {
/**
* 创建线程局部变量
*/
private ThreadLocal<LogRecordEntity> threadLocal = new ThreadLocal<>();
/**
* 定义切入点,这里我们使用AOP切入自定义【@LogRecord】注解的方法
*/
@Pointcut("@annotation(com.spring.boot.demo.anno.LogRecord)")
public void pointCut() {}
/**
* 前置通知,【执行Controller方法之前】执行该通知方法
*/
@Before("pointCut()")
public void beforeAdvice() {
System.out.println("前置通知......"); // TODO delete
}
/**
* 后置通知,【Controller方法执行完成,返回方法的返回值之前】执行该通知方法
*/
@After("pointCut()")
public void afterAdvice() {
System.out.println("后置通知......"); // TODO delete
}
/**
* 环绕通知,执行Controller方法的前后执行
* @param joinPoint 连接点
*/
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 请求开始时间
String requestTime = String.valueOf(System.currentTimeMillis());
System.out.println("环绕通知之前....."); // TODO delete
// 获取当前请求对象
HttpServletRequest request = HttpRequestUtil.getRequest();
if (request == null) {
return null;
}
// 获取请求相关信息
LogRecordEntity entity = new LogRecordEntity();
entity.setId(UUID.randomUUID().toString().replace("-", ""));
entity.setPath(request.getRequestURI());
entity.setMethod(request.getMethod());
entity.setRequestIp(request.getRemoteHost());
entity.setRequestTime(requestTime);
// 反射获取调用方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (method.isAnnotationPresent(LogRecord.class)) {
// 获取注解信息
LogRecord annotation = method.getAnnotation(LogRecord.class);
entity.setOpName(annotation.opName());
}
// 获取全限定类名称
String name = method.getName();
// 获取请求参数
String inputParam = JSONObject.toJSONString(joinPoint.getArgs());
entity.setInputParam(inputParam);
// 设置局部变量
threadLocal.set(entity);
// 调用Controller方法
Object ret = joinPoint.proceed();
System.out.println("环绕通知之后....."); // TODO delete
return ret;
}
/**
* 返回值通知,Controller执行完成之后,返回方法的返回值时候执行
* @param ret 返回值的名称
*/
@AfterReturning(pointcut = "pointCut()", returning = "ret")
public Object afterReturning(Object ret) {
System.out.println("返回值通知......ret=" + ret); // TODO delete
// 获取日志实体对象
LogRecordEntity entity = this.getEntity();
String outputParam = JSON.toJSONString(ret);
entity.setOutputParam(outputParam); // 保存响应参数
entity.setStatus("成功"); // 设置成功标识
// TODO 这里就可以做一些持久胡操作,例如:保存日志到数据表里面
// 一定要删除 ThreadLocal 变量
threadLocal.remove();
System.out.println(entity); // TODO delete
return ret;
}
/**
* 异常通知,当Controller方法执行过程中出现异常时候,执行该通知
* @param ex 异常名称
*/
@AfterThrowing(pointcut = "pointCut()", throwing = "ex")
public void throwingAdvice(Throwable ex) {
System.out.println("异常通知......"); // TODO delete
// 获取日志实体对象
LogRecordEntity entity = this.getEntity();
StringWriter errorMsg = new StringWriter();
ex.printStackTrace(new PrintWriter(errorMsg, true));
entity.setErrorMsg(errorMsg.toString()); // 保存响应参数
entity.setStatus("失败"); // 设置成功标识
// TODO 这里就可以做一些持久胡操作,例如:保存日志到数据表里面
// 一定要删除 ThreadLocal 变量
threadLocal.remove();
System.out.println(entity); // TODO delete
}
/****************************************************/
private LogRecordEntity getEntity() {
// 获取局部变量
LogRecordEntity entity = threadLocal.get();
long start = Long.parseLong(entity.getRequestTime());
long end = System.currentTimeMillis();
// 获取响应时间、耗时
entity.setCostTime((end - start) + "ms");
entity.setResponseTime(String.valueOf(end));
return entity;
}
}
1.7、编写测试类
package com.spring.boot.demo.controller;
import com.spring.boot.demo.anno.LogRecord;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Js
* @version 1.0.0
* @Date: 2023/11/2 22:58
* @Description
*/
@RestController
@RequestMapping("/api/aop")
public class LogController {
@LogRecord(opName = "测试日志", desc = "测试日志描述内容")
@GetMapping("/log")
public String demo() {
System.out.println("开始执行业务逻辑代码......");
return "success.";
}
@LogRecord(opName = "测试日志", desc = "测试日志描述内容")
@GetMapping("/error")
public String error() {
System.out.println("开始执行业务逻辑代码......");
int i = 10 / 0;
return "success.";
}
}
1.8、运行测试
启动工程,浏览器分别访问两个地址【http://127.0.0.1:8081/api/aop/log】和【http://127.0.0.1:8081/api/aop/error】,查看控制台日志输出。
到此,SpringBoot利用AOP和自定义注解实现日志记录就成功啦;目前是直接在控制台打印,也可以将其信息保存到数据库中;