项目上需要对一些重要的接口记录操作日志,便于历史问题追踪、排查。主要记录的字段有操作人、请求ip、操作时间、模块、功能、请求参数、请求结果等。

记录操作日志基本上都是用AOP,当然我也不例外,需要记录的字段,大部分都很容易获取到,比较难获取的一个字段是请求参数,因为不同的接口参数请求方式不同,有的接口使用@RequestBody传json字符串,有的接口使用@RequestParam传的form参数,有的接口有文件上传@RequestParam("file") MultipartFile file。我们项目中大部分是使用@RequestBody传json字符串,少部分文件上传接口使用@RequestParam("file") MultipartFile file。

第一次

使用httpRequest.getInputStream()的方式提取@RequestBody中的json参数,大致代码如下:

ServletInputStream inputStream = httpRequest.getInputStream();
InputStreamReader reader = new InputStreamReader(inputStream,StandardCharsets.UTF_8);
BufferedReader bfReader = new BufferedReader(reader);
StringBuilder sb = new StringBuilder();
String line;
while ((line = bfReader.readLine()) != null){
sb.append(line);
}
System.out.println(sb.toString());

问题:发现AOP拦截后,controller那里获取到的参数为空。在网上查到问题原因是,“在拦截器中读取请求的JSON数据需要,获取请求中的输入流InputStream is = request.getInputStream();当我们拦截器执行完成后,进入其他拦截器或者控制层参数解析时,也需要获取,当因为我们之前的拦截器已经获取过一次,之后的都获取不到内容,因此报出此错误!”。解决思路就是“通过过滤器,将原始的 HttpServletRequest替换成我们自己的请求包装类,在其中重写 getInputStream()方法”

第二次

public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {
    // 将流中的内容保存
    private final byte[] buff;
    public BodyReaderRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream is = request.getInputStream();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] b = new byte[1024];
        int len;
        while ((len = is.read(b)) != -1) {
            baos.write(b, 0, len);
        }
        buff = baos.toByteArray();
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(buff);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener listener) {
            }
            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
    public String getRequestBody() {
        return new String(buff);
    }
}
public class AuthFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 防止流读取一次后就没有了, 所以需要将流继续写出去
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        // 这里将原始request传入,读出流并存储
        ServletRequest requestWrapper = new  BodyReaderRequestWrapper(httpServletRequest);
        // 这里将原始request替换为包装后的request,此后所有进入controller的request均为包装后的
        filterChain.doFilter(requestWrapper, servletResponse);//
    }
    @Override
    public void destroy() {
    }
}
@Configuration
public class FilterOrderConfig {
    @Bean
    public FilterRegistrationBean filterRegistrationBean1(){
        FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new AuthFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        //order的数值越小 则优先级越高,这里直接使用的最高优先级
        filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return filterRegistrationBean;
    }
}

这种方式确实解决了第一次遇到的问题,测了几个普通接口也没问题,后来发现包含@RequestParam("file") MultipartFile file文件上传的接口提示Required request part 'file' is not present

spring boot 记录类字段改变 springboot记录每一次请求_spring boot 记录类字段改变

 在网上查了半天也没有查到合适的解决方法。于是准备参考一下一些成熟的springboot项目脚手架怎么处理的,发现若依项目没有出现这个问题,于是参考若依项目,结合自己项目特点,完成了这记录操作日志的功能,目前没发现问题。

第三次

直接贴上代码

操作日志实体类,存储在mongo

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "op_log")
public class OpLog {

    @Id
    private String id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 方法名
     */
    private String method;

    /**
     * 参数
     */
    private String params;

    /**
     * ip地址
     */
    private String ip;

    /**
     * 请求url
     */
    private String url;

    /**
     * //操作类型 :新增、删除等等
     */
    private String type;

    /**
     * 模块
     */
    private String model;

    /**
     * 操作时间
     */
    private Long createTime;

    /**
     * 操作结果
     */
    private String result;

    /**
     * 描述
     */
    private String description;
}

注解定义

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义操作日志记录注解
 */
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog
{
    /**
     * 模块
     */
    String module() default "";

    /**
     * 功能
     * @return
     */
    String function() default "";

    /**
     * 操作类型
     */
    BusinessType type() default BusinessType.QUERY;

}

操作日志处理

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

/**
 * 操作日志记录处理
 */
@Aspect
@Component
@Slf4j
public class LogAspect {

    @Autowired
    private OpLogService opLogService;

    // 配置织入点
    @Pointcut("@annotation(com.cq.mysmsmanagerback.annotation.OperationLog)")
    public void logPointCut() {
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object result) {
        try {
            // 获得注解
            OperationLog controllerOperationLog = getAnnotationLog(joinPoint);
            if (controllerOperationLog == null) {
                return;
            }

            OpLog opLog = new OpLog();
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //获取切入点所在的方法
            Method method = signature.getMethod();
            //获取操作
            OperationLog annotation = method.getAnnotation(OperationLog.class);
            if (annotation != null) {
                opLog.setModel(annotation.module());
                opLog.setDescription(annotation.function());
                opLog.setType(annotation.type().name());
            }

            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            // 获取请求的方法名
            String methodName = method.getName();
            methodName = className + "." + methodName;
            opLog.setMethod(methodName);

            opLog.setCreateTime(DateUtil.date().getTime());
            //操作用户 --登录时有把用户的信息保存在session中,可以直接取出
            String userName = SecurityContextHolder.getContext().getAuthentication().getName();
            opLog.setUsername(userName);

            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
            opLog.setIp(ip);
            opLog.setUrl(ServletUtils.getRequest().getRequestURI());

            // 请求参数
            String params = argsArrayToString(joinPoint.getArgs());
            opLog.setParams(params.length() > 2000 ? params.substring(0, 2000) : params);

            opLog.setResult(JSONUtil.toJsonStr(result));

            // 插入到mongo
            log.info("opLog: {}", JSONUtil.toJsonStr(opLog));
            opLogService.insertOpLog(opLog);
            // 保存数据库
//            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }


    /**
     * 是否存在注解,如果存在就获取
     */
    private OperationLog getAnnotationLog(JoinPoint joinPoint) throws Exception {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(OperationLog.class);
        }
        return null;
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray) {
        String params = "";
        if (paramsArray != null && paramsArray.length > 0) {
            for (int i = 0; i < paramsArray.length; i++) {
                if (paramsArray[i] != null && !isFilterObject(paramsArray[i])) {
                    Object jsonObj = JSON.toJSON(paramsArray[i]);
                    params += jsonObj.toString() + " ";
                }
            }
        }
        return params.trim();
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象,则返回true;否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Iterator iter = collection.iterator(); iter.hasNext(); ) {
                return iter.next() instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry) iter.next();
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}

业务操作类型

public enum BusinessType
{
    /**
     * 新增
     */
    ADD,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 删除查询
     */
    QUERY,
}

操作日志存储

@Service
@Slf4j
public class OpLogServiceImpl implements OpLogService {

    @Resource
    private MongoTemplate mongoTemplate;

    /**
     * 插入操作日志到mongo
     *
     * @param opLog
     * @return
     */
    @Override
    public Result insertOpLog(OpLog opLog) {

        mongoTemplate.insert(opLog);

        return Result.buildSucc();
    }

}

使用方法,在controller方法上加上下面类似注解就好

@OperationLog(module = "客户管理", function = "创建客户", type = BusinessType.ADD)

总结

一些用得比较多的脚手架,里面还是有很多值得我们学习的地方,一些常用功能,可以多参考参考脚手架中怎么实现的,包括功能界面设计和代码实现。