在自定义filter中使用自定义的HttpServletRequestWrapper后,出现中文乱码

在自定义filter中使用自定义的HttpServletRequestWrapper后,出现中文乱码

 

1、项目使用的环境

tomcat  8.5.34

Springboot  1.5.17

 

2、使用背景和问题描述

在自定义的过滤器Myfilter中,需要从request body 中获取参数,因此使用自定义的类MyHttpServletRequestWrapper继承HttpServletRequestWrapper来封装一下request,以确保Myfilter之后的处理中能从request中获取到请求参数。但是发现封装了request之后,request中获取到的中文乱码了。以下是相关代码。

Myfilter

@Order(Integer.MIN_VALUE)
public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = servletRequest;
        if (servletRequest instanceof HttpServletRequest) {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            if (!isFormDataRequest(httpServletRequest)) {
                requestWrapper = new MyHttpServletRequestWrapper(httpServletRequest);
            }

            // 从request body 中获取参数(省略)
        }
        chain.doFilter(requestWrapper, response);
    }
    
    @Override
    public void destroy() {}

    private boolean isFormDataRequest(HttpServletRequest httpServletRequest) {
        String contentType = httpServletRequest.getContentType();
        return Objects.nonNull(contentType) && (contentType.contains("application/x-www-form-urlencoded")
                || contentType.contains("multipart/form-data")) && "POST".equalsIgnoreCase(httpServletRequest.getMethod());
    }
}

MyHttpServletRequestWrapper

public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    public MyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = StreamUtil.readBytes(request.getReader(), "UTF-8");
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener arg0) {}
        };
    }

RequestUtils

@Slf4j
public class RequestUtils {

    /**
     * 获取请求体
     */
    public static String getRequestBody(HttpServletRequest request) {
        String body = null;
        try {
            body = StreamUtils.copyToString(request.getInputStream(),Charset.forName("utf-8"));
        } catch (Exception e) {
            log.error("RequestUtils getRequestBody has an error:{}", e);
        }
        return body;
    }

    public static Object getRequestParam(String body, String param) {
        if (StringUtils.isBlank(body)) {
            return null;
        }

        Object obj = null;
        try {
            JSONObject jsonObject = JSON.parseObject(body);
            obj = jsonObject.get(param);
        } catch (Exception e) {
            log.info("RequestUtils getRequestParam has an error.body:{}.error:{}", body, e);
        }
        return obj;
    }

}

 

3、原因分析

查看源码可知,springboot 有个默认的编码过滤器(OrderedCharacterEncodingFilter)被设置为最先执行,但是由于Myfilter的order也设置为了最小值,导致其执行时间早于OrderedCharacterEncodingFilter

因为目前的浏览器通常不设置字符编码,即使在HTML页面或表单中指定了字符编码,因此需要OrderedCharacterEncodingFilter为请求指定字符编码

同时,如果请求尚未指定编码,则此过滤器可以应用其设置的编码,或者在任何情况下强制使用此过滤器设置的编码(当forceEncoding =true”时(此为默认值))。在后一种情况下,该编码也将作为默认的响应编码。

而且http协议默认的编码是iso-8859-1,因此直接使用 utf-8 去读取数据将导致中文乱码。

OrderedCharacterEncodingFilter源码如下:

public class OrderedCharacterEncodingFilter extends CharacterEncodingFilter implements Ordered {
    private int order = -2147483648;

    public OrderedCharacterEncodingFilter() {
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }
}
CharacterEncodingFilter部分源码如下
public class CharacterEncodingFilter extends OncePerRequestFilter {
    private String encoding;
    private boolean forceRequestEncoding;
    private boolean forceResponseEncoding;

============= 省略 ===============

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String encoding = this.getEncoding();
        if(encoding != null) {
            if(this.isForceRequestEncoding() || request.getCharacterEncoding() == null) {
                request.setCharacterEncoding(encoding);
            }

            if(this.isForceResponseEncoding()) {
                response.setCharacterEncoding(encoding);
            }
        }
        filterChain.doFilter(request, response);
    }
}

4、解决办法

(1)将Myfilter的执行顺序设置在OrderedCharacterEncodingFilter之后执行,如设置 @Order(Integer.MIN_VALUE +1) 或 @Order(-1) 

(2)设置Myfilter中request的编码,具体如下

@Order(Integer.MIN_VALUE)
public class MyFilter implements Filter {
    private final HttpEncodingProperties properties;

    public MyFilter(HttpEncodingProperties properties) {
        this.properties = properties;
    }

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        servletRequest.setCharacterEncoding(properties.getCharset().name());
        ServletRequest requestWrapper = servletRequest;
        if (servletRequest instanceof HttpServletRequest) {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            if (!isFormDataRequest(httpServletRequest)) {
                requestWrapper = new MyHttpServletRequestWrapper(httpServletRequest);
            }

            // 从request body 中获取参数(省略)
        }
        chain.doFilter(requestWrapper, response);
    }

    @Override
    public void destroy() {}

    private boolean isFormDataRequest(HttpServletRequest httpServletRequest) {
        String contentType = httpServletRequest.getContentType();
        return Objects.nonNull(contentType) && (contentType.contains("application/x-www-form-urlencoded")
                || contentType.contains("multipart/form-data")) && "POST".equalsIgnoreCase(httpServletRequest.getMethod());
    }
}

 

<think>好的,我现在要解决的问题是如何通过继承HttpServletRequestWrapper来实现请求体的多次读取,同时确保Spring MVC中的@Valid注解参数校验不会出错。首先,我需要理解用户的需求和背景。 根据用户提供的引用内容,特别是引用[1]提到,通常的做法是通过包装HttpServletRequestWrapper,重写读取方法,将消息体内容保存到内部变量中。而用户希望简化代码,可能通过反射或其他方法实现。另外,用户提到使用@Valid注解进行参数校验时不能出错,这意味着在处理请求体时,需要确保Spring能够正确解析参数并进行校验。 首先,我需要回顾一下HttpServletRequest的工作原理。当请求体被读取后,输入流会被消费,无法再次读取。因此,包装类的目的是将请求体的内容缓存下来,以便后续多次读取。常见的做法是重写getInputStream()和getReader()方法,返回缓存的流内容。 但这里需要注意,Spring MVC在处理@RequestBody注解时,会使用getInputStream()来读取请求体。如果在其他地方已经读取过流,比如在过滤器或AOP中,会导致后续无法读取,出现错误。因此,自定义HttpServletRequestWrapper需要确保每次调用getInputStream()或getReader()时,都能返回一个重新读取缓存内容的流。 接下来,我需要考虑如何实现这个包装类。通常的做法是将请求体内容读取到一个字节数组或字符串中,然后提供一个可以多次读取的流。例如,在构造函数中读取输入流的内容并保存,之后每次调用getInputStream()时,返回一个基于缓存内容的ByteArrayInputStream。 然后,用户提到要兼容@Valid的校验逻辑。可能的问题在于,如果包装类没有正确处理流的读取,Spring在解析参数时可能无法正确获取数据,导致校验失败。例如,如果缓存的数据没有正确传递给Spring的解析器,@RequestBody映射的对象可能为空,触发校验错误。 另外,引用[3]提到,如果在Filter或AOP中提前读取了请求体,会导致后续无法读取。因此,包装类需要在第一次读取时缓存数据,后续的读取都从缓存中获取,这样就不会出现流被消费的问题。 现在,我需要具体写出这个包装类的代码。通常的结构是继承HttpServletRequestWrapper,覆盖getInputStream和getReader方法,将原始流的内容缓存下来。例如: public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { private byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); this.cachedBody = IOUtils.toByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() throws IOException { return new CachedBodyServletInputStream(this.cachedBody); } @Override public BufferedReader getReader() throws IOException { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody); return new BufferedReader(new InputStreamReader(byteArrayInputStream)); } } 然后,CachedBodyServletInputStream需要继承ServletInputStream,包装缓存的字节数组。这部分需要正确实现read等方法,并覆盖isFinished和isReady等状态方法,确保Spring可以正确读取。 接下来,需要将这个包装类应用到请求的处理流程中。通常的做法是创建一个Filter,在过滤器中替换原始的HttpServletRequest为包装后的实例。例如: public class CachingRequestBodyFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; CachedBodyHttpServletRequest wrappedRequest = new CachedBodyHttpServletRequest(httpServletRequest); filterChain.doFilter(wrappedRequest, servletResponse); } } 然后,在Spring Boot应用中注册这个Filter,确保它在其他过滤器之前执行,尤其是那些可能读取请求体的过滤器之前。例如使用@Bean或@WebFilter注解配置,但要注意引用[4]提到的路径配置问题,避免覆盖其他过滤器。 现在,问题是如何确保与@Valid的兼容性。当Spring MVC处理@RequestBody时,会从包装后的请求中读取输入流,此时流已经被缓存,可以多次读取。因此,只要包装类正确实现了缓存的流,参数绑定和校验应该正常进行。例如,当控制器方法使用@Valid @RequestBody MyRequest request时,Spring会从缓存的流中正确解析出MyRequest对象,并进行校验。如果校验失败,会抛出MethodArgumentNotValidException,可以正常处理。 另外,需要考虑字符编码的问题。在缓存请求体时,可能需要将字节数组转换为字符串时指定正确的字符编码,根据请求的Content-Type头信息。例如,使用request.getCharacterEncoding()来获取编码,避免乱码问题。 可能的错误点包括: 1. 缓存数据时未正确处理大请求体,导致内存溢出。需要评估是否处理大文件上传等情况,可能不适合使用内存缓存。 2. 包装类的流实现不完整,例如isReady或isFinished方法实现不正确,导致Spring在读取时出现异常。 3. Filter的顺序问题,如果其他过滤器在之前已经读取了请求体,那么包装类可能无法正确缓存数据。因此,需要确保CachingRequestBodyFilter是第一个执行的过滤器。 测试时,可以编写一个控制器方法,多次读取请求体,或者使用@Valid进行校验,确保参数正确绑定,校验错误能正常触发。 总结步骤: 1. 创建自定义HttpServletRequestWrapper,缓存请求体内容。 2. 创建Filter,将原始请求替换为包装后的请求。 3. 确保Filter的顺序正确,避免其他过滤器提前消费流。 4. 测试验证@Valid注解的参数校验是否正常工作。</think>为了实现支持多次读取请求体并兼容Spring MVC参数校验的自定义请求包装器,请按照以下步骤操作: ### 一、创建自定义HttpServletRequestWrapper ```java public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { private final byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); // 缓存请求体内容 this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() { return new CachedBodyServletInputStream(this.cachedBody); } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(this.getInputStream())); } // 自定义ServletInputStream实现 private static class CachedBodyServletInputStream extends ServletInputStream { private final ByteArrayInputStream buffer; public CachedBodyServletInputStream(byte[] body) { this.buffer = new ByteArrayInputStream(body); } @Override public int read() { return buffer.read(); } @Override public boolean isFinished() { return buffer.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { throw new UnsupportedOperationException("不支持异步读取"); } } } ``` ### 二、创建请求缓存过滤器 ```java @WebFilter(urlPatterns = "/*") public class CachingRequestBodyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // 包装原始请求 CachedBodyHttpServletRequest wrappedRequest = new CachedBodyHttpServletRequest(httpRequest); chain.doFilter(wrappedRequest, response); } } ``` ### 三、配置过滤器顺序(Spring Boot) ```java @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<CachingRequestBodyFilter> cachingFilterRegistration() { FilterRegistrationBean<CachingRequestBodyFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new CachingRequestBodyFilter()); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); // 设置最高优先级 return registration; } } ``` ### 四、在控制器中使用@Valid ```java @PostMapping("/validate") public ResponseEntity<?> validateRequest(@Valid @RequestBody UserDTO user) { // 参数校验通过后执行逻辑 return ResponseEntity.ok("校验成功"); } ``` ### 技术要点说明 1. **缓存实现原理** 通过`StreamUtils.copyToByteArray()`将原始请求体保存到字节数组中,后续通过自定义的`ServletInputStream`实现多次读取,解决了流只能读取一次的问题[^1]。 2. **校验兼容性保障** Spring MVC的`@RequestBody`和`@Valid`会调用`getInputStream()`获取数据,包装类返回的缓存流可以确保参数解析器正常工作。 3. **过滤器优先级控制** 设置过滤器为最高优先级,确保在参数校验前完成请求体缓存,避免其他过滤器提前消费输入流[^3][^4]。 ### 常见问题排查 - **中文乱码问题** 在包装类中通过`request.getCharacterEncoding()`获取编码格式,在构造`InputStreamReader`时显式指定编码。 - **大文件内存溢出** 该方案适用于常规请求体,若需处理大文件建议使用临时文件缓存方案。 - **流状态方法实现** `isReady()`返回true且`isFinished()`正确判断流状态,确保与Spring的兼容性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值