项目上需要对一些重要的接口记录操作日志,便于历史问题追踪、排查。主要记录的字段有操作人、请求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
在网上查了半天也没有查到合适的解决方法。于是准备参考一下一些成熟的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)
一些用得比较多的脚手架,里面还是有很多值得我们学习的地方,一些常用功能,可以多参考参考脚手架中怎么实现的,包括功能界面设计和代码实现。