统一的日志处理

接着上一个crud项目

目录
  1. 基于AOP
    AOP+注解
    AOP 扫包
  2. 基于前置过滤器,后置过滤器
1. 基于AOP
AOP+注解

这种方式比较灵活,不受文件夹跟包的限制,给需要记录日志的接口加上注解,不需要记录的不加。

  1. 创建一个LogAutoRecord注解
import java.lang.annotation.*;

/**
 * @author zhoust
 * @Date 2021/9/17 22:49
 * @Desc 日志记录切入点
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAutoRecord {
    // 强制记录方法功能
    String methodDesc();
}
  1. 创建aop切面 LogAspect
package com.zhoust.fastdome.aspect;

import com.alibaba.fastjson.JSONObject;
import com.zhoust.fastdome.annnotation.LogAutoRecord;
import com.zhoust.fastdome.business.entity.CommonLog;
import com.zhoust.fastdome.business.service.CommonLogService;
import com.zhoust.fastdome.common.CommonRequestBody;
import com.zhoust.fastdome.common.CommonResponse;
import com.zhoust.fastdome.utils.IPUtils;
import com.zhoust.fastdome.utils.SnowflakeIdWorker;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
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.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;

/**
 * @author zhoust
 * @Date 2021/9/17 22:49
 * @Desc 日志记录切面
 */
@Slf4j
@Aspect
@Component
public class LogAspect {

    final CommonLogService commonLogService;

    final HttpServletRequest request;

    public LogAspect(CommonLogService commonLogService, HttpServletRequest request) {
        this.commonLogService = commonLogService;
        this.request = request;
    }

    /**
     * 定义切入点
     */
    @Pointcut("@annotation(com.zhoust.fastdome.annnotation.LogAutoRecord)")
    public void getPointCutByAnnotation(){
    }

    @Around("getPointCutByAnnotation()")
    public Object saveLog(ProceedingJoinPoint joinPoint){

        Object result = "";
        // 执行目标 获取返回值
        try {
            result = joinPoint.proceed();
            saveLog(JSONObject.toJSONString(result),joinPoint);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            log.error("执行方法失败,没有记录返回报文,不报错");
        }
        return result;
    }

    /**
     * 获取参数插入日志表
     * @param responseBody
     * @param joinPoint
     */
    private void saveLog(String responseBody,ProceedingJoinPoint joinPoint){
        String logId = String.valueOf(SnowflakeIdWorker.generateId());
        // 获取 工号信息 (一般在session获取,根据自己业务)
        String operaType = "common";
        String operaName = "测试";
        String operaNo = "6666";
        String requestIp = IPUtils.getIpAddr(request);

        String requestBody = getRequestBody(joinPoint);
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String methodName = method.getName();
        LogAutoRecord annotation = method.getAnnotation(LogAutoRecord.class);
        String methodDesc = annotation.methodDesc();
        String requestURI = request.getRequestURI();

        CommonLog commonLog = new CommonLog();
        commonLog.setLogId(logId);
        commonLog.setOperaType(operaType);
        commonLog.setOperaNam(operaName);
        commonLog.setOperaNo(operaNo);
        commonLog.setMethodName(methodName);
        commonLog.setRemoteIp(requestIp);
        commonLog.setRequestUri(requestURI);
        commonLog.setRequestBody(requestBody);
        commonLog.setResponseBody(responseBody);
        commonLog.setUriDesc(methodDesc);
        commonLog.setCreateTime(new Date());
        // 建议使用异步线程记录,避免堵塞(切记request的操作要放到主线程中,不能作为参数传到子线程中进行操作,不然后报错。)
        commonLogService.save(commonLog);
    }

    /**
     * 新项目建议定义好DTO请求参数统一使用一个继承一个公共的对象,
     *  我这里定义了一个 CommonRequestBody,用来存公共的流水,及系统来源。
     *  在别的项目中 参数可能是在parameter中,也可能在请求的请求体中。
     *  根据实际项目去判断
     * @param joinPoint
     * @return
     */
    private String getRequestBody(ProceedingJoinPoint joinPoint){
        String requestBody = "";
        Object[] args = joinPoint.getArgs();
        for(Object arg :args){
            // 如果获取到自定义对象,就将自定义对象存储
            if(arg instanceof CommonRequestBody){
                requestBody = JSONObject.toJSONString(arg);
                break;
            }
        }
        // 如果body 中没有,就去parameter中取
        if(StringUtils.isEmpty(requestBody)){
            Map<String, String[]> parameterMap = request.getParameterMap();
            requestBody = JSONObject.toJSONString(parameterMap);
        }
        return requestBody;
    }
}
  1. 上面从body中获取参数使用了自定义统一入参,所有请求参数DTO都继承该类 CommonRequestBody
@Data
public class CommonRequestBody {

    private String transId;

    private String systemId;
}
  1. 使用方式
@Slf4j
@RestController
public class UserTestLogController {

    private final UserMapper userMapper;
    private final UserService userService;
    public UserTestLogController(UserMapper userMapper, UserService userService) {
        this.userMapper = userMapper;
        this.userService = userService;
    }
    @GetMapping("/getUserByParameter")
    @LogAutoRecord(methodDesc = "根据用户ID查询用户信息")
    public CommonResponse getUserById(String id){
        String userById = userService.getUserById(id);
        return CommonResponse.succeed(userById);
    }
    @GetMapping("/getUserByCommon")
    @LogAutoRecord(methodDesc = "根据统一请求查询用户信息")
    public CommonResponse getUserByCommonRequest(@RequestBody UserRequestDTO userRequestDTO){
        String userById = userService.getUserById(userRequestDTO.getUserId());
        return CommonResponse.succeed(userById);
    }
}

只用在需要记录日志的地方加上注解@LogAutoRecord(methodDesc = "")即可,方法描述可以根据自己的需求是否记录

AOP扫包

AOP扫包跟注解的形式差不多,本质都是通过AOP,但确定哪些包下的请求需要记录日志,就可以字节配置包路径即可,不用一个一个的写注解。

/**
     * 1、execution(): 表达式主体。
     * 2、第一个*号:表示返回类型, *号表示所有的类型。
     * 3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.zhoust.fastdome.business.controller.scan包、子孙包下所有类的方法。
     * 4、第二个*号:表示类名,*号表示所有的类。
     * 5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
     */
    @Pointcut("execution(* com.zhoust.fastdome.business.controller.scan.*.*(..))")
    public void getPointCutByScanPackage(){
    }

通过 execution 指定要记录日志的包。

表中数据记录

2. 基于拦截器

在spring项目中实现拦截器有两种方式,在项目中使用,可以有两种方式,选择实现HandlerInterceptor接口或者继承HandlerInterceptorAdapter类,两种方式类似。HandlerInterceptorAdapter实现了AsyncHandlerInterceptorAsyncHandlerInterceptor又继承了HandlerInterceptor,本质这三者都可以。

  1. 考虑到返回值不容易获取,可以从response 中获取,也可以用一些野路子。
    我这里将返回值放到 setAttribute 中了 ,使用@ControllerAdvice。实现ResponseBodyAdvice的beforeBodyWrite方法
@ControllerAdvice
public class LogAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    /**
     * 将返回报文放入
     * @param body
     * @param returnType
     * @param selectedContentType
     * @param selectedConverterType
     * @param request
     * @param response
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //通过RequestContextHolder获取request
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        httpServletRequest.setAttribute("responseBody", body);
        return body;
    }
  1. 实现拦截器
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor{
    private final CommonLogService commonLogService;

    public LogInterceptor(CommonLogService commonLogService) {
        this.commonLogService = commonLogService;
    }

    /**
     * 前置拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //通过前置拦截器获取请求数据(如果是地址栏参数从parameter中获取,其他的从body中获取,另外还有地址栏跟body中都有的,这种变态报文尽量在定义接口的时候就杜绝掉)
        String logId = String.valueOf(SnowflakeIdWorker.generateId());
        request.setAttribute("logId",logId);
        commonLogService.saveLogByHandler(request);
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        log.info("后置处理器开始");
        Object responseBody = request.getAttribute("responseBody");
        String s = JSONObject.toJSONString(responseBody);
        commonLogService.updateLogByHandler(request,s);
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
  1. 注册拦截器
@Configuration
public class WebAdapterConfig implements WebMvcConfigurer {

    private final LogInterceptor interceptor;

    public WebAdapterConfig(LogInterceptor interceptor) {
        this.interceptor = interceptor;
    }

    /**
     * 注册拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**");
        WebMvcConfigurer.super.addInterceptors(registry);
    }
}
基于拦截器的日志记录在获取请求参数在body中的数据有点困难。

请求参数在parameter中还好,直接get就能拿到,但是在body (前台传的json串)中的数据不是那个好拿,因为request 的 getInputStream() 或只能调用一次,多次调用会报错,下面会记录报错信息。然而我们在controller中一般会使用@RequestBody获取数据映射为实体,在这个过程中会调用getInputStream()。所以就会导致两者只能取其一。(所以才会强调请求统一格式,要么都在parameter要么都以json传递)

针对这种方式我找到了两种方式

1 重写request2 修改controller的获取请求方式
  1. 在filter在重写request
    这种方式不需要改动controller方法,适合已经有很多接口的controller项目。
1 重写request2 新建过滤器3 从Request中获取请求
  • 重写request
    java EE 提供了HttpServletRequestWrapper 便于我们构造自定义的servletRequest。这里新建MyRequestUtils.java
package com.zhoust.fastdome.utils;import lombok.extern.slf4j.Slf4j;import javax.servlet.ReadListener;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;import java.io.*;/** * @author zhoust * @Date 2021/9/28 16:01 * @Desc <TODO DESC> */@Slf4jpublic class MyRequestUtils extends HttpServletRequestWrapper {    private final String body;    public MyRequestUtils(HttpServletRequest request) throws IOException {        super(request);        this.body = getBody(request);    }    /**     * 从 request 中获取请求体(每次请求request.getReader()只能获取一次,所以这个在后面通过@RequestBody映射实体时会报错。后面有解决方法)     * @param request     * @return     * @throws IOException     */    private String getBody(HttpServletRequest request) throws IOException{        StringBuilder body = new StringBuilder();        BufferedReader reader = request.getReader();        String readLine = reader.readLine();        while (readLine != null){            body.append(readLine);            readLine = reader.readLine();        }        return body.toString();    }    public String getBody() {        return body;    }    @Override    public ServletInputStream getInputStream()  {        final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());        return new ServletInputStream() {            @Override            public boolean isFinished() {                return false;            }            @Override            public boolean isReady() {                return false;            }            @Override            public void setReadListener(ReadListener readListener) {            }            @Override            public int read(){                return bais.read();            }        };    }    @Override    public BufferedReader getReader(){        return new BufferedReader(new InputStreamReader(this.getInputStream()));    }}
  • 新建过滤器
    新建过滤器MyFilter。(在spring项目中记得注入到ioc容器中,我忘了,报错了)
package com.zhoust.fastdome.filter;import com.zhoust.fastdome.utils.MyRequestUtils;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import java.io.IOException;/** * @author zhoust * @Date 2021/9/28 16:56 * @Desc <TODO DESC> */@Component@Slf4jpublic class MyFilter implements Filter {    @Override    public void init(FilterConfig filterConfig) throws ServletException {        log.info(">>>>>>>>>>过滤器出生<<<<<<<<<<<<");        Filter.super.init(filterConfig);    }    @Override    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {        log.info(">>>>>>>>>>开始奋斗的过滤器<<<<<<<<<<<<");        // 更改request        MyRequestUtils myRequestUtils = new MyRequestUtils((HttpServletRequest) request);        chain.doFilter(myRequestUtils,response);        log.info(">>>>>>>>>>奋斗完成的过滤器<<<<<<<<<<<<");    }    @Override    public void destroy() {        log.info(">>>>>>>>>>过滤器度过光荣的一生<<<<<<<<<<<<");        Filter.super.destroy();    }}
  • 从Request中获取请求
    在MyRequestUtils中将读取到的requestbody信息放到了body 中,后续只需要getBody就可以获取。
String requestBody = "";try {    MyRequestUtils myRequestUtils = (MyRequestUtils)request;    requestBody = myRequestUtils.getBody();}catch (Exception e){    log.info("获取请求报文失败,{}",e);}
  1. 拦截器获取后
    这种方式需要改动controller的取值方式,不使用@RequestBody,直接从Request中获取。
// 伪代码    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        //通过前置拦截器获取请求数据(如果是地址栏参数从parameter中获取,其他的从body中获取,另外还有地址栏跟body中都有的,这种变态报文尽量在定义接口的时候就杜绝掉)        String logId = String.valueOf(SnowflakeIdWorker.generateId());        request.setAttribute("logId",logId);                // 获取放回放到setAttribute中        String requestBody = getBody(request);        request.setAttribute("requestBody",requestBody);        commonLogService.saveLogByHandler(request);        return HandlerInterceptor.super.preHandle(request, response, handler);    }	// 获取请求体    private String getBody(HttpServletRequest request) throws IOException{        StringBuilder body = new StringBuilder();        BufferedReader reader = request.getReader();        String readLine = reader.readLine();        while (readLine != null){            body.append(readLine);            readLine = reader.readLine();        }        return body.toString();    }    public String getBody() {        return body;    }	// controller 这里用了阿里的fast json       @GetMapping("/getUserById")    public CommonResponse getUserById(HttpServletRequest request){                // 从Attribute 中获取数据        String requestBody = request.getAttribute("requestBody");        UserRequestDTO userRequestDTO = JSONObject.parseObject(requestBody, UserRequestDTO.class);        String userById = userService.getUserById(userRequestDTO.getUserId());        return CommonResponse.succeed(userById);    }
日志记录结果

java批量操作es java批量操作如何记录日志_AOP

报错记录

  1. getInputStream() has already been called for this request
    request 的 getInputStream() 只能调用一次,多次调用会报错。()
    上面在拦截器记录日志中记录了两种方式获取。
java.lang.IllegalStateException: getInputStream() has already been called for this request	at org.apache.catalina.connector.Request.getReader(Request.java:1212) ~[tomcat-embed-core-9.0.13.jar:9.0.13]	at org.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504) ~[tomcat-embed-core-9.0.13.jar:9.0.13]	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_20]	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_20]	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_20]	at java.lang.reflect.Method.invoke(Method.java:483) ~[na:1.8.0_20]	at org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler.invoke(AutowireUtils.java:305) ~[spring-beans-5.1.3.RELEASE.jar:5.1.3.RELEASE]	at com.sun.proxy.$Proxy65.getReader(Unknown Source) ~[na:na]
  1. request 不能在多线程下作为参数传递,request只能在主线程中使用。(No thread-bound request found)
Exception in thread "Thread-2" java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

若想获取主线程中request中的信息,请先获取出来,然后赋值给共享对象,传递给子线程

非AOP拦截到的异常。

另外,有些异常可能不是在方法(controller)执行中发生的,比如请求在拦截器中校验用户的真实性,该用户是否有权限访问该接口等。一些不属不会被我们的Aop 拦截到的异常我们也是需要记录的,并且不会往前台返回一大串的异常信息,通常是<操作失败,日志流水:xxxxxxxx>,类似这种。

0001-Java项目中统一返回统一异常处理.md一笔记中记录到统一异常处理。借助@ControllerAdvice

@Slf4j@ControllerAdvicepublic class ExceptionHandle {    /**     * 处理未知异常     * @param e     * @return     */    @ExceptionHandler(Exception.class)    @ResponseBody    public CommonResponse handleException(Exception e){        log.error("系统异常:{}",e.getMessage());        return CommonResponse.error(e.getMessage());    }    /**     * 处理主动抛出的自定义异常     * @param e     * @return     */    @ExceptionHandler(BusinessException.class)    @ResponseBody    public CommonResponse handleBusinessException(BusinessException e){         log.error("自定义异常:{}",e.getErrMassage());        return CommonResponse.error(e.getErrCode(),e.getErrMassage());    }}

所以当我们在AOP 中没有拦截到的异常可以在这里记录日志。

package com.zhoust.fastdome.exception;import com.alibaba.fastjson.JSONObject;import com.zhoust.fastdome.business.entity.CommonLog;import com.zhoust.fastdome.business.service.CommonLogService;import com.zhoust.fastdome.common.CommonResponse;import com.zhoust.fastdome.utils.IPUtils;import com.zhoust.fastdome.utils.SnowflakeIdWorker;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;import java.util.Date;/** * @author zhoust * @Date * @Desc */@Slf4j@ControllerAdvicepublic class ExceptionHandle {    private final HttpServletRequest request;    private final CommonLogService commonLogService;    public ExceptionHandle(HttpServletRequest request,CommonLogService commonLogService) {        this.request = request;        this.commonLogService = commonLogService;    }    /**     * 处理未知异常     * @param e     * @return     */    @ExceptionHandler(Exception.class)    @ResponseBody    public CommonResponse handleException(Exception e){        e.printStackTrace();        log.error("系统异常:{}",e);        CommonLog commonLog = commonLogService.getCommonLog(request, "");        CommonResponse error = CommonResponse.error("操作异常,日志流水:"+commonLog.getLogId());        error.setLogId(commonLog.getLogId());        // 失败原因可以不返回前台,根据自己业务需求        error.setDate(e.getMessage());        commonLog.setResponseBody(JSONObject.toJSONString(error));        saveLog(commonLog);        return error;    }    /**     * 处理主动抛出的自定义异常     * @param e     * @return     */    @ExceptionHandler(BusinessException.class)    @ResponseBody    public CommonResponse handleBusinessException(BusinessException e){        log.error("自定义异常:{}",e.getErrMassage());        CommonLog commonLog = commonLogService.getCommonLog(request, "");        CommonResponse error = CommonResponse.error(e.getErrCode(), e.getErrMassage()+",日志流水:"+commonLog.getLogId());        error.setLogId(commonLog.getLogId());        error.setDate(e);        commonLog.setResponseBody(JSONObject.toJSONString(error));        saveLog(commonLog);        return error;    }    private void saveLog(CommonLog commonLog){        commonLogService.save(commonLog);    }}

getCommonLog方法

@Override    public CommonLog getCommonLog(HttpServletRequest request,String requestBody){        CommonLog commonLog = new CommonLog();        String logId  = "";        Object logId1 = request.getAttribute("logId");        if(StringUtils.isEmpty(logId1)){            logId = String.valueOf(SnowflakeIdWorker.generateId());        }else{            logId = (String) logId;        }        // 获取 工号信息 (一般在session获取,根据自己业务)        String operaType = "common";        String operaName = "测试";        String operaNo = "6666";        String requestIp = IPUtils.getIpAddr(request);        String requestURI = request.getRequestURI();        commonLog.setLogId(logId);        commonLog.setOperaType(operaType);        commonLog.setOperaNam(operaName);        commonLog.setOperaNo(operaNo);        commonLog.setRemoteIp(requestIp);        commonLog.setRequestUri(requestURI);        commonLog.setRequestBody(requestBody);        commonLog.setCreateTime(new Date());        return commonLog;    }

源码地址