一、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 {@Beanpublic 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,那么会被序列化成 0SerializerFeature.WriteNullNumberAsZero,// WriteNullBooleanAsFalse: 如果布尔类型等于 null,那么会被序列化成 falseSerializerFeature.WriteNullBooleanAsFalse,// PrettyFormat: 美化JSONSerializerFeature.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;}@Overridepublic 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 deletereturn 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 deletereturn 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和自定义注解实现日志记录就成功啦;目前是直接在控制台打印,也可以将其信息保存到数据库中;