获取错误的请求的参数和请求来源,并进行限制

背景

近期在对系统排除报错比较多的地方,并进行优化,期间发现有一个错误频繁出现,

排查过程

根据报错信息,找到接收参数的DTO类,一共有两个接口引用。原本应该是List<Long>类型的参数,却一直接收到错误的String 类型的参数,导致无法排查是哪个接口所以一开始排查的思路:是前端的请求有问题。

故而就找了前端的同事进行了初步的排查,发现果然有一个接口,当数据为空的时候,直接传了空字符串,会导致报错。发现后,前端同事着手进行了修复。

持续报错

按理来说,前端已经修复了这个bug,这个错误依然在持续输出,该接口又是需要登录的用户才能进行请求,那就只有一个可能,我们站点的用户,利用自己的token,通过api接口进行请求,而不是通过页面交互进行请求。

又由于前面提到的问题,无法定位是哪个接口报错,毕竟是在参数解析映射的时候就已经报错,错误拦截日志又没有针对性的打印请求的uri和请求的完整参数。所以需要我们在对参数进行解析映射为DTO类之前就进行拦截打印。

实现思路

spring切面AOP

一开始使用切面aop,希望能在这之前进行拦截:代码如下:

package com.example.methodtest.reqeustparamlogger.annotation;

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

/**
 * 请求前置处理日志注解
 *
 * @author AaronCgt
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface  RequestParamLogger {
}

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ParameterAspect {

    @Before("@annotation(RequestParamLogger)") // 拦截带有 @RequestParamLogger 注解的方法
    public void beforeMethod(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        
        // 假设我们想要拦截第一个参数
        if (args.length > 0 && args[0] instanceof YourParameterClass) {
            YourParameterClass param = (YourParameterClass) args[0];
            
            // 在这里可以对参数进行验证或修改
            if (param.getSomeField() == null) {
                throw new IllegalArgumentException("Missing required field in parameter");
            }

            // 可以修改参数
            param.setSomeField("modifiedValue");
        }
    }
}
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class YourController {

    @PostMapping("/your-endpoint")
    @RequestParamLogger // 使用自定义注解
    public String yourMethod(@RequestBody YourParameterClass param) {
        // 这里的 param 会被切面处理过
        return "Processed: " + param.getSomeField();
    }
}

初始尝试失败

此时发现,当参数请求符合规范,确实能在到达controller进行拦截打印,但是当请求参数的类型不对时,依然进行报错,即便是变更为环绕通知,也无法实现;

重整思路

这个思路不对,此时思考,为什么会拦截不了,是执行顺序的问题吗,至此,去重新温习了一下tomcat容器的内容,发现了这一张图
在这里插入图片描述
引用:https://blog.csdn.net/zzwpublic/article/details/111571569

该图引用自 博主 :zzwpublic
感谢他博文的解惑

过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是在servlet处理完后,返回给前端之前

过滤器实现

这也是为什么我用aop,拦截器,无法对参数进行解析映射为DTO类之前拦截的原因,在拦截器中,request就无法变更了,并且之后就已经完成了这一步映射解析,我需要在这之前进行拦截,处理reqeust才能完整获取。所以故此,思路就变成了用过滤器进行拦截,过滤器代码如下:

package com.example.methodtest.reqeustparamlogger.filter;

import com.example.methodtest.reqeustparamlogger.ReusableInputStreamWrapper;
import com.example.methodtest.reqeustparamlogger.annotation.RequestParamLogger;
import com.example.methodtest.utils.ApplicationBeanUtil;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.AnnotatedElement;
import java.util.Map;
import java.util.Set;

/**
 * 实现将带有RequestParamLogger注解的请求进行处理,将流保存下来
 *
 * @author AaronCgt
 */

@Component
public class RequestParamFilter implements Filter {


    @Override
    public void destroy() {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

         // 判断请求是否带有RequestParamLogger注解
        if (checkHasPointAnnotation(requestURI)) {
            // 处理动态配置的逻辑
            ServletRequest requestWrapper = null;
            requestWrapper = new ReusableInputStreamWrapper((HttpServletRequest) request);
            chain.doFilter(requestWrapper, response);
        }else{
            chain.doFilter(request, response);
        }

    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {}


    private boolean checkHasPointAnnotation(String uri) {

        RequestMappingHandlerMapping handlerMapping = ApplicationBeanUtil.getBean("requestMappingHandlerMapping");

        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
            RequestMappingInfo mappingInfo = entry.getKey();
            HandlerMethod handlerMethod = entry.getValue();

            PathPatternsRequestCondition patternsCondition = mappingInfo.getPathPatternsCondition();
            assert patternsCondition != null;
            Set<String> patterns = patternsCondition.getDirectPaths();

            for (String pattern : patterns) {
                if (uri.matches(pattern)) {
                    if (HasPointAnnotation(handlerMethod.getBeanType()) || HasPointAnnotation(handlerMethod.getMethod())) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    private boolean HasPointAnnotation(AnnotatedElement element) {
        return AnnotatedElementUtils.hasAnnotation(element, RequestParamLogger.class);
    }
}

Java流读取问题

上述代码可以看到,这里只是通过对拥有特定注解的方法进行拦截,并将request进行了替换,为什么要这么做,因为这里又涉及了Java的流的知识点,只能被读取一次

如果我需要获取请求参数,势必要进行流的读取,就会导致流的关闭,导致程序无法继续往下执行。
所以需要实现流的临时保存和读取,实现代码如下:

package com.example.methodtest.reqeustparamlogger;

import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 实现将流保存下来
 *
 * @author AaronCgt
 * @since 2024/7/30/030
 */
public class ReusableInputStreamWrapper extends HttpServletRequestWrapper {

    //用于将流保存下来
    private byte[] requestBody = null;

    public ReusableInputStreamWrapper(HttpServletRequest request, byte[] content) throws IOException {
        super(request);
        requestBody = content;
    }
    public ReusableInputStreamWrapper(HttpServletRequest request) throws IOException {
        super(request);
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
    }


    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
        return new ServletInputStream() {
            @Override
            public int read(){
                // 读取 requestBody 中的数据
                return bais.read();
            }
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) { }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}

拦截器实现和bean注册

至此,Java流关闭问题解决,此时再配合拦截器,就可以将参数进行打印,代码如下所示:

package com.example.methodtest.reqeustparamlogger.interceptor;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.example.methodtest.reqeustparamlogger.annotation.RequestParamLogger;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * 参数解析前拦截器
 * 用于打印请求参数,请求地址等信息,方便排查问题
 * @author AaronCgt
 * @since 2024/7/29/029
 */
@Slf4j
public class RequestParamInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            if (handlerMethod.hasMethodAnnotation(RequestParamLogger.class)) {
                StringBuilder sb = new StringBuilder();
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        sb.append(line);
                    }
                } catch (IOException e) {
                    log.error("Read request body error", e);
                }
                JSONObject jsonObject = JSON.parseObject(sb.toString());

                Map<String,Object> logMap = new HashMap<>();

                // 接口地址
                logMap.put("Request URL", request.getRequestURL().toString());
                // IP地址
                logMap.put("Request IP Address", request.getRemoteAddr());
                // 打印请求参数
                Map<String,String> paramMap = new HashMap<>();
                for (String key : request.getParameterMap().keySet()) {
                    paramMap.put(key,request.getParameter(key));
                }
                logMap.put("Request Parameters", JSON.toJSONString(paramMap));
                logMap.put("Request Parameters body", JSON.toJSONString(jsonObject));
                logMap.put("Request User", "{userId:此处可以根据需要进行检查}");
                logMap.put("Request Method", handlerMethod.getMethod().getName());
                log.info("Request Info:{}", JSON.toJSONString(logMap));
            }
        }
        return true;
    }

}

package com.example.methodtest.reqeustparamlogger.config;

import com.example.methodtest.reqeustparamlogger.filter.RequestParamFilter;
import com.example.methodtest.reqeustparamlogger.interceptor.RequestParamInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 请求前置处理自动配置
 *
 * @author AaronCgt
 */
@AutoConfiguration
public class Configuration implements WebMvcConfigurer {

    @Bean
    public RequestParamInterceptor requestParamInterceptor() {
        return new RequestParamInterceptor();
    }

    @Bean
    public RequestParamFilter requestParamFilter() {
        return new RequestParamFilter();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestParamInterceptor()).addPathPatterns("/**");
    }


}

至此,将 RequestParamLogger 注解放置到对应的controller请求方法上面,即可以对请求参数,来源进行日志打印;

当然也有人说,可以针对参数解析错误进行拦截,在报错后进行日志打印也可以,这也是一个不错的思路,后续我也可以尝试实现一下;

结果

通过上述实现,于生产中发现了有一个用户,每3秒钟请求一次,请求参数一直是错误的,导致服务器一直报错,博主将对应的ip,用户报告给主管后,主管评估后觉得影响不大,并表示可以去掉这个日志记录先。该优化工作告一段落。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值