• springboot AOP记录操作日志(包含遇到的问题)


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

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

    第一次

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

    1. ServletInputStream inputStream = httpRequest.getInputStream();
    2. InputStreamReader reader = new InputStreamReader(inputStream,StandardCharsets.UTF_8);
    3. BufferedReader bfReader = new BufferedReader(reader);
    4. StringBuilder sb = new StringBuilder();
    5. String line;
    6. while ((line = bfReader.readLine()) != null){
    7. sb.append(line);
    8. }
    9. System.out.println(sb.toString());

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

    第二次

    1. public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {
    2. // 将流中的内容保存
    3. private final byte[] buff;
    4. public BodyReaderRequestWrapper(HttpServletRequest request) throws IOException {
    5. super(request);
    6. InputStream is = request.getInputStream();
    7. ByteArrayOutputStream baos = new ByteArrayOutputStream();
    8. byte[] b = new byte[1024];
    9. int len;
    10. while ((len = is.read(b)) != -1) {
    11. baos.write(b, 0, len);
    12. }
    13. buff = baos.toByteArray();
    14. }
    15. @Override
    16. public ServletInputStream getInputStream() throws IOException {
    17. final ByteArrayInputStream bais = new ByteArrayInputStream(buff);
    18. return new ServletInputStream() {
    19. @Override
    20. public boolean isFinished() {
    21. return false;
    22. }
    23. @Override
    24. public boolean isReady() {
    25. return false;
    26. }
    27. @Override
    28. public void setReadListener(ReadListener listener) {
    29. }
    30. @Override
    31. public int read() throws IOException {
    32. return bais.read();
    33. }
    34. };
    35. }
    36. @Override
    37. public BufferedReader getReader() throws IOException {
    38. return new BufferedReader(new InputStreamReader(getInputStream()));
    39. }
    40. public String getRequestBody() {
    41. return new String(buff);
    42. }
    43. }
    1. public class AuthFilter implements Filter {
    2. @Override
    3. public void init(FilterConfig filterConfig) throws ServletException {
    4. }
    5. @Override
    6. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    7. // 防止流读取一次后就没有了, 所以需要将流继续写出去
    8. HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    9. // 这里将原始request传入,读出流并存储
    10. ServletRequest requestWrapper = new BodyReaderRequestWrapper(httpServletRequest);
    11. // 这里将原始request替换为包装后的request,此后所有进入controller的request均为包装后的
    12. filterChain.doFilter(requestWrapper, servletResponse);//
    13. }
    14. @Override
    15. public void destroy() {
    16. }
    17. }
    1. @Configuration
    2. public class FilterOrderConfig {
    3. @Bean
    4. public FilterRegistrationBean filterRegistrationBean1(){
    5. FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
    6. filterRegistrationBean.setFilter(new AuthFilter());
    7. filterRegistrationBean.addUrlPatterns("/*");
    8. //order的数值越小 则优先级越高,这里直接使用的最高优先级
    9. filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    10. return filterRegistrationBean;
    11. }
    12. }

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

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

    第三次

    直接贴上代码

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

    1. @Data
    2. @Builder
    3. @NoArgsConstructor
    4. @AllArgsConstructor
    5. @Document(collection = "op_log")
    6. public class OpLog {
    7. @Id
    8. private String id;
    9. /**
    10. * 用户名
    11. */
    12. private String username;
    13. /**
    14. * 方法名
    15. */
    16. private String method;
    17. /**
    18. * 参数
    19. */
    20. private String params;
    21. /**
    22. * ip地址
    23. */
    24. private String ip;
    25. /**
    26. * 请求url
    27. */
    28. private String url;
    29. /**
    30. * //操作类型 :新增、删除等等
    31. */
    32. private String type;
    33. /**
    34. * 模块
    35. */
    36. private String model;
    37. /**
    38. * 操作时间
    39. */
    40. private Long createTime;
    41. /**
    42. * 操作结果
    43. */
    44. private String result;
    45. /**
    46. * 描述
    47. */
    48. private String description;
    49. }

    注解定义

    1. import java.lang.annotation.Documented;
    2. import java.lang.annotation.ElementType;
    3. import java.lang.annotation.Retention;
    4. import java.lang.annotation.RetentionPolicy;
    5. import java.lang.annotation.Target;
    6. /**
    7. * 自定义操作日志记录注解
    8. */
    9. @Target({ ElementType.PARAMETER, ElementType.METHOD })
    10. @Retention(RetentionPolicy.RUNTIME)
    11. @Documented
    12. public @interface OperationLog
    13. {
    14. /**
    15. * 模块
    16. */
    17. String module() default "";
    18. /**
    19. * 功能
    20. * @return
    21. */
    22. String function() default "";
    23. /**
    24. * 操作类型
    25. */
    26. BusinessType type() default BusinessType.QUERY;
    27. }

    操作日志处理

    1. import cn.hutool.core.date.DateUtil;
    2. import cn.hutool.json.JSONUtil;
    3. import com.alibaba.fastjson.JSON;
    4. import lombok.extern.slf4j.Slf4j;
    5. import org.aspectj.lang.JoinPoint;
    6. import org.aspectj.lang.Signature;
    7. import org.aspectj.lang.annotation.AfterReturning;
    8. import org.aspectj.lang.annotation.AfterThrowing;
    9. import org.aspectj.lang.annotation.Aspect;
    10. import org.aspectj.lang.annotation.Pointcut;
    11. import org.aspectj.lang.reflect.MethodSignature;
    12. import org.springframework.beans.factory.annotation.Autowired;
    13. import org.springframework.security.core.context.SecurityContextHolder;
    14. import org.springframework.stereotype.Component;
    15. import org.springframework.validation.BindingResult;
    16. import org.springframework.web.multipart.MultipartFile;
    17. import javax.servlet.http.HttpServletRequest;
    18. import javax.servlet.http.HttpServletResponse;
    19. import java.lang.reflect.Method;
    20. import java.util.Collection;
    21. import java.util.Iterator;
    22. import java.util.Map;
    23. /**
    24. * 操作日志记录处理
    25. */
    26. @Aspect
    27. @Component
    28. @Slf4j
    29. public class LogAspect {
    30. @Autowired
    31. private OpLogService opLogService;
    32. // 配置织入点
    33. @Pointcut("@annotation(com.cq.mysmsmanagerback.annotation.OperationLog)")
    34. public void logPointCut() {
    35. }
    36. /**
    37. * 处理完请求后执行
    38. *
    39. * @param joinPoint 切点
    40. */
    41. @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    42. public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
    43. handleLog(joinPoint, null, jsonResult);
    44. }
    45. /**
    46. * 拦截异常操作
    47. *
    48. * @param joinPoint 切点
    49. * @param e 异常
    50. */
    51. @AfterThrowing(value = "logPointCut()", throwing = "e")
    52. public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
    53. handleLog(joinPoint, e, null);
    54. }
    55. protected void handleLog(final JoinPoint joinPoint, final Exception e, Object result) {
    56. try {
    57. // 获得注解
    58. OperationLog controllerOperationLog = getAnnotationLog(joinPoint);
    59. if (controllerOperationLog == null) {
    60. return;
    61. }
    62. OpLog opLog = new OpLog();
    63. // 从切面织入点处通过反射机制获取织入点处的方法
    64. MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    65. //获取切入点所在的方法
    66. Method method = signature.getMethod();
    67. //获取操作
    68. OperationLog annotation = method.getAnnotation(OperationLog.class);
    69. if (annotation != null) {
    70. opLog.setModel(annotation.module());
    71. opLog.setDescription(annotation.function());
    72. opLog.setType(annotation.type().name());
    73. }
    74. // 获取请求的类名
    75. String className = joinPoint.getTarget().getClass().getName();
    76. // 获取请求的方法名
    77. String methodName = method.getName();
    78. methodName = className + "." + methodName;
    79. opLog.setMethod(methodName);
    80. opLog.setCreateTime(DateUtil.date().getTime());
    81. //操作用户 --登录时有把用户的信息保存在session中,可以直接取出
    82. String userName = SecurityContextHolder.getContext().getAuthentication().getName();
    83. opLog.setUsername(userName);
    84. String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
    85. opLog.setIp(ip);
    86. opLog.setUrl(ServletUtils.getRequest().getRequestURI());
    87. // 请求参数
    88. String params = argsArrayToString(joinPoint.getArgs());
    89. opLog.setParams(params.length() > 2000 ? params.substring(0, 2000) : params);
    90. opLog.setResult(JSONUtil.toJsonStr(result));
    91. // 插入到mongo
    92. log.info("opLog: {}", JSONUtil.toJsonStr(opLog));
    93. opLogService.insertOpLog(opLog);
    94. // 保存数据库
    95. // AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
    96. } catch (Exception exp) {
    97. // 记录本地异常日志
    98. log.error("==前置通知异常==");
    99. log.error("异常信息:{}", exp.getMessage());
    100. exp.printStackTrace();
    101. }
    102. }
    103. /**
    104. * 是否存在注解,如果存在就获取
    105. */
    106. private OperationLog getAnnotationLog(JoinPoint joinPoint) throws Exception {
    107. Signature signature = joinPoint.getSignature();
    108. MethodSignature methodSignature = (MethodSignature) signature;
    109. Method method = methodSignature.getMethod();
    110. if (method != null) {
    111. return method.getAnnotation(OperationLog.class);
    112. }
    113. return null;
    114. }
    115. /**
    116. * 参数拼装
    117. */
    118. private String argsArrayToString(Object[] paramsArray) {
    119. String params = "";
    120. if (paramsArray != null && paramsArray.length > 0) {
    121. for (int i = 0; i < paramsArray.length; i++) {
    122. if (paramsArray[i] != null && !isFilterObject(paramsArray[i])) {
    123. Object jsonObj = JSON.toJSON(paramsArray[i]);
    124. params += jsonObj.toString() + " ";
    125. }
    126. }
    127. }
    128. return params.trim();
    129. }
    130. /**
    131. * 判断是否需要过滤的对象。
    132. *
    133. * @param o 对象信息。
    134. * @return 如果是需要过滤的对象,则返回true;否则返回false。
    135. */
    136. @SuppressWarnings("rawtypes")
    137. public boolean isFilterObject(final Object o) {
    138. Class clazz = o.getClass();
    139. if (clazz.isArray()) {
    140. return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
    141. } else if (Collection.class.isAssignableFrom(clazz)) {
    142. Collection collection = (Collection) o;
    143. for (Iterator iter = collection.iterator(); iter.hasNext(); ) {
    144. return iter.next() instanceof MultipartFile;
    145. }
    146. } else if (Map.class.isAssignableFrom(clazz)) {
    147. Map map = (Map) o;
    148. for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) {
    149. Map.Entry entry = (Map.Entry) iter.next();
    150. return entry.getValue() instanceof MultipartFile;
    151. }
    152. }
    153. return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
    154. || o instanceof BindingResult;
    155. }
    156. }

    业务操作类型

    1. public enum BusinessType
    2. {
    3. /**
    4. * 新增
    5. */
    6. ADD,
    7. /**
    8. * 修改
    9. */
    10. UPDATE,
    11. /**
    12. * 删除
    13. */
    14. DELETE,
    15. /**
    16. * 删除查询
    17. */
    18. QUERY,
    19. }

    操作日志存储

    1. @Service
    2. @Slf4j
    3. public class OpLogServiceImpl implements OpLogService {
    4. @Resource
    5. private MongoTemplate mongoTemplate;
    6. /**
    7. * 插入操作日志到mongo
    8. *
    9. * @param opLog
    10. * @return
    11. */
    12. @Override
    13. public Result insertOpLog(OpLog opLog) {
    14. mongoTemplate.insert(opLog);
    15. return Result.buildSucc();
    16. }
    17. }

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

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

    总结

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

  • 相关阅读:
    P1278 单词游戏 简单搜索+玄学优化
    论文解读(S^3-CL)《Structural and Semantic Contrastive Learning for Self-supervised Node Representation Learning》
    面对工作中的失误:从错误中学习与成长
    Linux之yum安装MySQL
    WPF 单击移动窗口 MouseLeftButtonDown 事件
    设计模式2、抽象工厂模式 Abstract Factory
    100天精通Golang(基础入门篇)——第20天:Golang 接口 深度解析☞从基础到高级
    wpf 命令概述
    RT-Thread内核快速入门,内核实现与应用开发学习随笔记
    审计智能合约的成本是多少?如何审计智能合约?
  • 原文地址:https://blog.csdn.net/maxi1234/article/details/127641722