之前项目上突然遇到个这个问题,显示后台接口报这个错:
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type
java.math.BigDecimal from String "1,122.00": not a valid representation at [Source: (PushbackInputStream); line: 23, column: 21] (through reference chain: com.ufgov.ar.modules.bill.bean.vo.ArBillVo["arBill"]->com.ufgov.ar.modules.bill.bean.ArBill["billMoney"])
大致意思就是说:ArBill实体类里的 billMoney 字段是BigDecimal类型,但参数中该值为1,122.00 ,不是数值类型的导致反序列化报错了,遇到这个情况我想着这块是不是可以通过过滤器在后端这块对金额类型的数据进行过滤处理呢?
针对上述说的问题,因为我想着采取过滤器进行数据过滤,所以本文主要讲解的是spring-boot过滤器读取body里的数据,因为我这块只针对post并且content-type为application/json的请求,不考虑其他的请求方式。
此处我们可以自定义类继承 HttpServletRequestWrapper 类 (该类继承了ServletRequestWrapper),重写 getReader() 方法进行处理,因为在controller 使用@RequestBody 在反序列化到对象数据读取就是通过该方法读取,所以可以通过在这修改数据达到最终过滤数据的目的。
此处贴两张图可以明确看到接受请求时的执行顺序:
废话不多说,直接贴过滤器读取body里内容的代码:
首先需要在启动类增加@ServletComponentScan 这样过滤器才会生效;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; @SpringBootApplication @ServletComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 复制代码
过滤器代码:
import cn.hutool.core.util.StrUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.HttpMethod; import java.io.IOException; /** * @ClassName: HttpReplaceFilter * @Description: 过滤器 * @Date: 2020/6/16 14:37 */ @WebFilter(filterName = "httpReplaceFilter", urlPatterns = "/*") @Component public class HttpReplaceFilter implements Filter { @Autowired private IArBillCharacterService arBillCharacterService; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String method = httpServletRequest.getMethod(); String contentType = httpServletRequest.getContentType(); if (StrUtil.isNotEmpty(contentType)) { contentType = contentType.toLowerCase(); } // 该方法处理 POST请求并且contentType为application/json格式的 if (HttpMethod.POST.equalsIgnoreCase(method) && StrUtil.isNotEmpty(contentType) && contentType.contains(MediaType.APPLICATION_JSON_VALUE)) { servletRequest = new BodyRequestWrapper(httpServletRequest); } filterChain.doFilter(servletRequest, servletResponse); } } 复制代码
继承 HttpServletRequestWrapper 重写getReader代码:
import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.ws.rs.HttpMethod; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; /** * @ProjectName: ar-server * @Package: com.ufgov.ar.conf.filter * @ClassName: BodyRequestWrapper * @Author: tianmengwei * @Description: 读取Request body里的内容信息 * @Date: 2020/6/16 14:42 */ public class BodyRequestWrapper extends HttpServletRequestWrapper { private byte[] body; public BodyRequestWrapper(HttpServletRequest request) throws IOException { super(request); String method = request.getMethod(); String requestURI = request.getRequestURI(); requestURI = requestURI.replaceAll("[^A-Za-z//d]", ""); boolean exist = CommonEnums.FilterUrlEnum.exist(requestURI); // 由于request并没有提供现成的获取json字符串的方法,所以我们需要将body中的流转为字符串 BufferedReader reader = request.getReader(); StringBuilder stringBuilder = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } String json = stringBuilder.toString(); if (exist && HttpMethod.POST.equalsIgnoreCase(method)) { // 数据处理 } else { body = json.getBytes(); } } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream())); } /** * @param * @description 在使用@RequestBody注解的时候,其实框架是调用了getInputStream()方法,所以我们要重写这个方法 * @date 2020/6/16 14:45 */ @Override public ServletInputStream getInputStream() { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() { return byteArrayInputStream.read(); } }; } } 复制代码
可以在上述里获取的json字符串进行数据过滤然后在返回,即可满足我现在的需求,此处代码只是涉及post请求读取body数据的操作。
功能实现一段时间后,突然有项目在实施的时候反馈说接口报错了,如下:
org.springframework.http.converter.HttpMessageConversionException: JSON conversion problem: Invalid UTF-8 start byte 0xb9; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Invalid UTF-8 start byte 0xb9
大致意思就是说:ArBill实体类里的 coName 字段值在过滤器那解析的时候报错了,我刚开始看以为是参数的汉字是乱码了,但看了入参发现都是正常的,这个时候我想可能是启动脚本设置编码有关,实施同学给我发了启动脚本,看了确实是脚本启动设置的编码是-Dfile.encoding=GBK,然后我本地设置了该编码集的参数,复现了这个问题:
idea可以通过在这里配置参数:
此处我将读取请求body里的代码注释了,spring在处理这块的编码集是根据请求头里的UTF-8编码读取,此时我看数据库存储的中文也是正常的;
在此处我设置编码集合系统设置的一致,数据存储到数据库和过滤器读取这块都正常。
那么问题来了,其实我过滤器读取这块需要主动设置编码集和系统的一致,否则就会出错,但抛开过滤器这块处理,spring在读取的时候按照UTF-8编码读取,然后在哪做了处理呢,数据存储都是正常的?但我查看源码没有找到此处设置,就感觉让我很费解?有遇到该问题的大佬请求指导!