SpringBoot 记录操作日志

SpringBoot 记录操作日志

我们在写一个完整项目时候记录接口操作日志是必不可少的,但是要想记录比较详细的日志不是很简单,如下是之前记录日志的时候遇到的一些bug。

  1. 在我们使用SpringBoot+Aop进行记录操作日志的时候,一般会使用@RestControllerAdvice+@ExceptionHandler 进行统一处理异常,这时候假设出现了BindException 参数错误异常时,你可以发现你的aop代码是不会出发的,因为它会被 @RestControllerAdvice 中的异常处理方法拦截处理。
  2. 假设我们使用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记录操作日志的代码就完成了!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值