SpringBoot 记录操作日志
我们在写一个完整项目时候记录接口操作日志是必不可少的,但是要想记录比较详细的日志不是很简单,如下是之前记录日志的时候遇到的一些bug。
- 在我们使用SpringBoot+Aop进行记录操作日志的时候,一般会使用@RestControllerAdvice+@ExceptionHandler 进行统一处理异常,这时候假设出现了BindException 参数错误异常时,你可以发现你的aop代码是不会出发的,因为它会被 @RestControllerAdvice 中的异常处理方法拦截处理。
- 假设我们使用SpringBoot+Interceptor进行记录操作日志的时候,因为我们使用的是@RestController = @Controller + @ResponseBody 所有的响应结果已json格式封装,这时候你的postHandler (ModelAndView)是拿不到返回结果的,但是我们的操作日志恰好是需要记录响应结果的,并且假设出现了异常,虽然我们是在拦截器中的afterCompletion() 方法中拿到,但是我们通过参数Exception ex获取异常信息时,我们可以发现ex==null,会触发空指针异常。所以我们使用 @RestControllerAdvice + Interceptor 进行记录操作日志,下面是具体代码。
实体类
package com.project.model.access.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author li
*/
@Data
@ApiModel("操作日志")
@Entity(name = "lv_operation_log")
public class OperationLog implements Serializable {
public static final String EVENT_LEVEL_INFO = "INFO";
public static final String EVENT_LEVEL_WARN = "WARN";
public static final String EVENT_LEVEL_ERROR = "ERROR";
public static final String REQUEST_METHOD_GET = "GET";
public static final String REQUEST_METHOD_POST = "POST";
public static final String REQUEST_METHOD_PUT = "PUT";
public static final String REQUEST_METHOD_DELETE = "DELETE";
public static final String APP_NAME = "LV_STORE";
public static final String HEADER_USER_AGENT = "User-Agent";
public static final String HEADER_HOST = "Host";
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ApiModelProperty(value = "日志ID", notes = "主键")
private Long eventId;
@ApiModelProperty(value = "应用名称")
private String appName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@ApiModelProperty(value = "请求时间")
private LocalDateTime dateTime;
@ApiModelProperty(value = "事件名称")
private String eventName;
@ApiModelProperty(value = "事件级别")
private String eventLevel;
@ApiModelProperty(value = "事件描述")
private String description;
@ApiModelProperty(value = "用户id")
private String username;
@ApiModelProperty(value = "用户代理")
private String useragent;
@ApiModelProperty(value = "源IP")
private String sourceIp;
@ApiModelProperty(value = "主机IP")
private String hostIp;
@ApiModelProperty(value = "主机名")
private String hostname;
@ApiModelProperty(value = "协议")
private String protocol;
@ApiModelProperty(value = "端口")
private Integer port;
@ApiModelProperty(value = "请求URI")
private String requestUri;
@ApiModelProperty(value = "请求方法")
private String requestMethod;
@ApiModelProperty(value = "响应编码")
private String response;
@ApiModelProperty(value = "返回信息")
private String message;
@ApiModelProperty(value = "请求参数")
private String requestMessage;
@ApiModelProperty(value = "传入字节数")
private Integer bytesSent;
@ApiModelProperty(value = "响应字节数")
private Integer bytesReceived;
@ApiModelProperty(value = "请求耗时")
private Integer duration = 0;
@ApiModelProperty(value = "行为")
private String action;
@ApiModelProperty(value = "客户地理位置信息")
private String geographyInfo;
}
mapper 层 (SpringDataJpa)
package com.project.repository;
import com.project.model.access.domain.OperationLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* @author li
*/
@Repository
public interface OperationLogRepository extends JpaRepository<OperationLog, Long>, JpaSpecificationExecutor<OperationLog> {
}
service 层
package com.project.service;
import com.project.model.access.domain.OperationLog;
import javax.servlet.http.HttpServletRequest;
/**
* @author li
*/
public interface OperationLogService {
/**
* 保存操作日志
*
* @param request 请求
* @param handler 处理器
* @return 操作日志
*/
OperationLog saveOperationLog(HttpServletRequest request, Object handler);
/**
* 获取ApiOperation注解的value值
*
* @param handler 处理器
* @return value值
*/
String getApiOperationValue(Object handler);
/**
* 获取类名和方法名
*
* @param handler 处理器
* @return 类名和方法名
*/
String getClassAndMethodName(Object handler);
}
package com.project.service.impl;
import com.project.common.util.SecurityUtils;
import com.project.model.access.domain.OperationLog;
import com.project.repository.OperationLogRepository;
import com.project.service.OperationLogService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import net.dreamlu.mica.ip2region.core.Ip2regionSearcher;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
/**
* @author li
*/
@Slf4j
@Service
public class OperationLogServiceImpl implements OperationLogService {
@Resource
private OperationLogRepository operationLogRepository;
@Resource
private Ip2regionSearcher ip2regionSearcher;
@Override
public OperationLog saveOperationLog(HttpServletRequest request, Object handler) {
OperationLog operationLog = new OperationLog();
log.info("========> save operation log start <========");
operationLog.setAppName(OperationLog.APP_NAME);
operationLog.setDateTime(LocalDateTime.now());
operationLog.setEventName(this.getClassAndMethodName(handler));
operationLog.setAction(this.getClassAndMethodName(handler));
operationLog.setDescription(this.getApiOperationValue(handler));
String userId = null;
try {
userId = SecurityUtils.getUserId();
} catch (Exception e) {
log.error("========> The interface does not require authorization <========");
}
operationLog.setUsername(userId);
operationLog.setUseragent(request.getHeader(OperationLog.HEADER_USER_AGENT));
operationLog.setSourceIp(request.getRemoteAddr());
operationLog.setHostIp(request.getHeader(OperationLog.HEADER_HOST));
operationLog.setHostname(request.getRemoteHost());
operationLog.setProtocol(request.getScheme());
operationLog.setPort(request.getRemotePort());
operationLog.setRequestUri(request.getRequestURI());
operationLog.setRequestMethod(request.getMethod());
// 获取请求参数
String queryString = request.getQueryString();
operationLog.setBytesSent(StringUtils.hasText(queryString) ? queryString.getBytes(StandardCharsets.UTF_8).length : 0);
operationLog.setRequestMessage(queryString);
// 获取地理位置
operationLog.setGeographyInfo(this.ip2regionSearcher.getAddress(request.getRemoteAddr()));
log.info("========> save operation log end <========");
return this.operationLogRepository.save(operationLog);
}
@Override
public String getApiOperationValue(Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
ApiOperation apiOperation = AnnotationUtils.findAnnotation(method, ApiOperation.class);
if (apiOperation != null) {
return apiOperation.value();
}
}
return "未知操作";
}
@Override
public String getClassAndMethodName(Object handler) {
if (handler instanceof HandlerMethod) {
// 强制转换为 HandlerMethod
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取方法名
String methodName = handlerMethod.getMethod().getName();
// 获取类名
String className = handlerMethod.getBeanType().getName();
return className + "." + methodName;
}
return "";
}
}
interceptor
package com.project.interceptor;
import com.project.model.access.domain.OperationLog;
import com.project.service.OperationLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author li
*/
@Slf4j
@Component
public class AccessLogInterceptor implements HandlerInterceptor {
@Resource
private OperationLogService operationLogService;
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler) throws Exception {
try {
OperationLog operationLog = this.operationLogService.saveOperationLog(request, handler);
// 将日志记录id写入本地线程中
LogContext.setRequestId(operationLog.getEventId());
} catch (Throwable ex) {
ex.printStackTrace();
log.error("========> save operation log error:{}", ex.getMessage());
throw ex;
}
return true;
}
@Override
public void postHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler, @Nullable Exception ex) throws Exception {
}
}
package com.project.config;
import com.project.interceptor.AccessLogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* @author li
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private AccessLogInterceptor accessLogInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLogInterceptor).excludePathPatterns(
"/error/**",
"/swagger-ui/**",
"/v3/**",
"/swagger-resources/**",
"/operation-api/**"
);
}
}
ResponseBodyAdvice
ResponseBodyAdvice 是用于接收响应结果,并且假设抛出了一场,但是我们在之前的@RestControllerAdvice+@ExceptionHandler 处理了异常,但会的结果其实也是一个json,所以我们在这个都可以接收到,但是我们要注意,上面拦截器中我们已经记录了日志,这个地方直接将返回结果的一些信息写入那条数据集中就好了,所以我们这里用到了本地线程。
package com.project.interceptor;
/**
* @author li
* 用于记录日志的上下文
*/
public class LogContext {
private static final ThreadLocal<Long> REQUEST_LOG_ID_HOLDER = new ThreadLocal<>();
public static void setRequestId(Long requestId) {
REQUEST_LOG_ID_HOLDER.set(requestId);
}
public static Long getRequestId() {
Long requestId = REQUEST_LOG_ID_HOLDER.get();
REQUEST_LOG_ID_HOLDER.remove();
return requestId;
}
}
package com.project.interceptor;
import com.alibaba.fastjson2.JSON;
import com.project.common.result.R;
import com.project.common.result.Result;
import com.project.model.access.domain.OperationLog;
import com.project.repository.OperationLogRepository;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
/**
* @author li
*/
@RestControllerAdvice
public class CustomResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Resource
private OperationLogRepository operationLogRepository;
@Override
public boolean supports(@NonNull MethodParameter returnType,
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType,
@NonNull MediaType selectedContentType,
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest request,
@NonNull ServerHttpResponse response) {
if (body instanceof Result || body instanceof R) {
// 获取本地线程中的日志记录id
Long requestId = LogContext.getRequestId();
if (null != requestId) {
OperationLog operationLog = this.operationLogRepository.findById(requestId).orElse(null);
if (null != operationLog) {
String result = JSON.toJSONString(body);
operationLog.setMessage(result);
// 获取响应字节数
operationLog.setBytesReceived(
StringUtils.hasText(result) ? result.getBytes(StandardCharsets.UTF_8).length : 0);
// 获取响应状态码
String responseCode = this.getResponseCode(body);
operationLog.setEventLevel(this.getEventLevel(responseCode, response));
operationLog.setResponse(this.getResponseCode(body));
this.operationLogRepository.save(operationLog);
}
}
}
return body;
}
/**
* 获取响应状态码
*
* @return 响应状态码
*/
public String getResponseCode(Object body) {
if (body instanceof R) {
R r = (R) body;
return String.valueOf(r.getCode());
}
return "200";
}
/**
* 获取事件级别
*
* @param responseCode 响应状态码
* @return 事件级别F
* @descripeion 200:事件级别INFO;500:事件级别ERROR;其他:事件级别WARN
*/
public String getEventLevel(String responseCode, ServerHttpResponse response) {
if (responseCode.equals(String.valueOf(HttpStatus.OK.value()))) {
response.setStatusCode(HttpStatus.OK);
return OperationLog.EVENT_LEVEL_INFO;
} else if (responseCode.equals(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()))) {
response.setStatusCode(HttpStatus.OK);
return OperationLog.EVENT_LEVEL_ERROR;
}
response.setStatusCode(HttpStatus.BAD_REQUEST);
return OperationLog.EVENT_LEVEL_WARN;
}
}
至此,完整的适用于SpringBoot记录操作日志的代码就完成了!