Spring/SpringBoot @RequestParam注解无法读取application/json格式数据问题解决
作者:小菜鸡cccc 时间:2023-11-26 11:26:29
前言
Emmmm…最近在做项目的途中,有遇到一个方法需要接收的参数只有一个或者较少的时候就懒得写实体类去接收,使用spring框架都知道,接收单个参数就使用@RequestParam注解就好了,但是前端对应的Content-type是需要改成application/x-www-form-urlencoded,所以在接口文档上面特地标记了。但是…不知道前端是格式改了但是参数还是用的json格式没有改成键值对的方式传递还是什么原因,就一直说参数传不过来,叫我改回json格式的。。我也实在是懒,另外一个也觉得没必要,就一两个参数就新建一个实体,太浪费,但是这个问题让我觉得不灵活蛮久了,也一直没找到办法,所以借这个机会,打开了我的开发神器,www.baidu.com…输入我的问题,找了好久也没找到有解决的方案,然后就想着看下Spring内部是怎么处理的吧,就稍微跟了下源码,下面就说下我解决的方案。
一、RequestMappingHandlerAdapter
RequestMappingHandlerAdapter实现了HandlerAdapter接口,顾名思义,表示handler的adapter,这里的handler指的是Spring处理具体请求的某个Controller的方法,也就是说HandlerAdapter指的是将当前请求适配到某个Handler的处理器。
RequestMappingHandlerAdapter是HandlerAdapter的一个具体实现,主要用于将某个请求适配给@RequestMapping类型的Handler处理,这里面就包含着请求数据和响应数据的处理。
// 这里可以获取到处理程序方法参数解析器的一个列表
List<HandlerMethodArgumentResolver> argumentResolvers =
requestMappingHandlerAdapter.getArgumentResolvers()
如果是想处理响应参数的话就使用
//这里可以获取到处理程序方法返回值的处理器
List<HandlerMethodReturnValueHandler> originalHandlers =
requestMappingHandlerAdapter.getReturnValueHandlers();
能获取到这个列表了,那需要加入我们自己定义的处理器应该不太麻烦了吧?(这里不讲返回数据的自定义策略处理,网上也有其他文章,如果需要可以找下)
二、HandlerMethodArgumentResolver
策略接口解决方法参数代入参数值在给定请求的上下文(翻译的源码注释)
简单的理解为:它负责处理你Handler方法里的所有入参:包括自动封装、自动赋值、校验等等。
——————————————————————————————————————————
那么这个时候我已经知道了第一步获取到的那个列表中存放的类型是什么了,简而言之,我们只需要实现这个策略类,编写我们自己的算法或逻辑就行了
这个接口里面有两个方法需要实现:
第一个方法的作用:是否与给定方法的参数是由该解析器的支持。(如果返回true,那么就使用该类进行参数转换,如果返回false,那么继续找下一个策略类)
第二个方法的作用:解决方法参数成从给定请求的自变量值。 由WebDataBinderFactory提供了一个方法来创建一个WebDataBinder所需数据绑定和类型转换目的时实例。(简单来讲,就是转换参数值的,返回的就是解析的参数值)
三、RequestParamMethodArgumentResolver
这个类就是用来处理Controller的方法上有加@RequestParam注解的具体处理器。
首先会调用这个方法来确定是否使用这个处理器解析参数,那么我们也看到了,如果参数有RequestParam注解,那么则会使用该类进行处理,那么我们能不能效仿呢?
四、MyHandlerMethodArgumentResolver
这个没啥好说,就自己定义的参数解析器。
直接上代码吧
/**
* @BelongsProject:
* @BelongsPackage:
* @Author: hef
* @CreateTime: 2020-06-20 18:49
* @Description: 描述
*/
public class MyHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 这个是处理@RequestParam注解的原本策略类
*/
private RequestParamMethodArgumentResolver requestParamMethodArgumentResolver;
/**
* 全参构造
*/
public MyHandlerMethodArgumentResolver(RequestParamMethodArgumentResolver requestParamMethodArgumentResolver) {
this.requestParamMethodArgumentResolver = requestParamMethodArgumentResolver;
}
/**
* 当参数前有@RequestParam注解时,会使用此 解析器
* <p>
* 注:此方法的返回值将决定:是否使用此解析器解析该参数
*/
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
//很明显,就是判断是否有这个注解
return methodParameter.hasParameterAnnotation(RequestParam.class);
}
/**
* 解析参数
*/
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory)
throws Exception {
final String applicationJson = "application/json";
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
throw new RuntimeException(" request must not be null!");
}
//获取到内容类型
String contentType = request.getContentType();
//如果类型是属于json 那么则跑自己解析的方法
if (null != contentType && contentType.contains(applicationJson )) {
//获取参数名称
String parameterName = methodParameter.getParameterName();
//获取参数类型
Class<?> parameterType = methodParameter.getParameterType();
//因为json数据是放在流里面,所以要去读取流,
//但是ServletRequest的getReader()和getInputStream()两个方法只能被调用一次,而且不能两个都调用。
//所以这里是需要写个自定义的HttpServletRequestWrapper,主要功能就是需要重复读取流数据
String read = getRead(request.getReader());
//转换json
JSONObject jsonObject = JSON.parseObject(read);
Object o1;
if (jsonObject == null) {
//这里有一个可能性就是比如get请求,参数是拼接在URL后面,但是如果我们还是去读流里面的数据就会读取不到
Map<String, String[]> parameterMap = request.getParameterMap();
o1 = parameterMap.get(parameterName);
}else {
o1 = jsonObject.get(parameterName);
}
Object arg = null;
//如果已经获取到了值的话那么再做类型转换
if (o1 != null) {
WebDataBinder binder = webDataBinderFactory.createBinder(nativeWebRequest, null, parameterName);
arg = binder.convertIfNecessary(o1, parameterType, methodParameter);
}
return arg;
}
//否则跑原本的策略类.
Object o = requestParamMethodArgumentResolver.resolveArgument(methodParameter,
modelAndViewContainer, nativeWebRequest, webDataBinderFactory);
return o;
}
/**
* 流转字符串
*
* @param bf
* @return
*/
private static String getRead(BufferedReader bf) {
StringBuilder sb = new StringBuilder();
try {
char[] buff = new char[1024];
int len;
while ((len = bf.read(buff)) != -1) {
sb.append(buff, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
}
四、ConfigArgumentResolvers
自己的策略类已经写好了,那么怎么加入到配置中去呢?
/**
* @BelongsProject:
* @BelongsPackage:
* @Author: hef
* @CreateTime: 2020-06-20 18:49
* @Description: 描述
*/
@Configuration
public class ConfigArgumentResolvers {
private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;
public ConfigArgumentResolvers(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
this.requestMappingHandlerAdapter = requestMappingHandlerAdapter;
}
//springBoot启动的时候执行
@PostConstruct
private void addArgumentResolvers() {
// 获取到框架定义好的参数解析集合
List<HandlerMethodArgumentResolver> argumentResolvers =
requestMappingHandlerAdapter.getArgumentResolvers();
MyHandlerMethodArgumentResolver myHandlerMethodArgumentResolver = getMyHandlerMethodArgumentResolver(argumentResolvers);
// ha.getArgumentResolvers()获取到的是不可变的集合,所以我们需要新建一个集合来放置参数解析器
List<HandlerMethodArgumentResolver> myArgumentResolvers =
new ArrayList<>(argumentResolvers.size() + 1);
//这里有一个注意点就是自定义的处理器需要放在RequestParamMethodArgumentResolver前面
//为什么呢?因为如果放在它后面的话,那么它已经处理掉了,就到不了我们自己定义的策略里面去了
//所以直接把自定义的策略放在第一个,稳妥!
// 将自定义的解析器,放置在第一个; 并保留原来的解析器
myArgumentResolvers.add(myHandlerMethodArgumentResolver);
myArgumentResolvers.addAll(argumentResolvers);
//再把新的集合设置进去
requestMappingHandlerAdapter.setArgumentResolvers(myArgumentResolvers);
}
/**
* 获取MyHandlerMethodArgumentResolver实例
*/
private MyHandlerMethodArgumentResolver getMyHandlerMethodArgumentResolver(
List<HandlerMethodArgumentResolver> argumentResolversList) {
// 原本处理RequestParam的类
RequestParamMethodArgumentResolver requestParamMethodArgumentResolver = null;
if (argumentResolversList == null) {
throw new RuntimeException("argumentResolverList must not be null!");
}
for (HandlerMethodArgumentResolver argumentResolver : argumentResolversList) {
if (requestParamMethodArgumentResolver != null) {
break;
}
if (argumentResolver instanceof RequestParamMethodArgumentResolver) {
// 因为在我们自己策略里面是还需要用到这个原本的类的,所以需要得到这个对象实例
requestParamMethodArgumentResolver = (RequestParamMethodArgumentResolver) argumentResolver;
}
}
if (requestParamMethodArgumentResolver == null) {
throw new RuntimeException("RequestParamMethodArgumentResolver not be null!");
}
//实例化自定义参数解析器
return new MyHandlerMethodArgumentResolver(requestParamMethodArgumentResolver);
}
}
五、MyHttpServletRequestWrapper
这个就是自定义的HttpServletRequest,保证可以重复获取到流数据
/**
* @BelongsProject:
* @BelongsPackage:
* @Author: hef
* @CreateTime: 2020-06-22 16:29
* @Description: 描述
*/
public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public MyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
//在读取流之前获取一次这个parameterMap,否则读取流后无法再解析出数据,
// 原因是org.apache.catalina.connector.Request里面有usingInputStream 和 usingReader两个全局变量记录流是否被读取过
//org.apache.catalina.connector.Request里面的parseParameters方法就是用来解析请求参数(Parse request parameters.)
//在解析参数之前会有一个判断,如果流被读取过 则不再解析请求参数 //
// if (usingInputStream || usingReader) { 这是源码里面的判断
// success = true;
// return;
// }
//如果先请求过一次后,那么org.apache.catalina.util.ParameterMap里面会有一个locked状态,如果读过一次之后 会变成锁定状态 那么后面再读都是读取解析过后的map
// /**
// * The current lock state of this parameter map.
// */
// private boolean locked = false;
request.getParameterMap();
body = ReadAsChars(request).getBytes(Charset.forName("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 readListener) {
}
};
}
/**
* 解析流
* @param request
* @return
*/
public static String ReadAsChars(ServletRequest request)
{
InputStream is = null;
StringBuilder sb = new StringBuilder();
try
{
is = request.getInputStream();
byte[] b = new byte[4096];
for (int n; (n = is.read(b)) != -1;)
{
sb.append(new String(b, 0, n));
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
if (null != is)
{
try
{
is.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
return sb.toString();
}
}
六、HttpServletRequestReplacedFilter
替换掉原本的Request对象,使用自定义的
/**
* @BelongsProject:
* @BelongsPackage:
* @Author: hef
* @CreateTime: 2020-06-22 16:47
* @Description: 描述
*/
@Component
public class HttpServletRequestReplacedFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(request instanceof HttpServletRequest) {
requestWrapper = new MyHttpServletRequestWrapper((HttpServletRequest) request);
}
if(null == requestWrapper) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
}
七、总结
如果是想@RequestBody接收表单形式的参数也可以用此方法,处理起来更简单 ,只需要实例化自定义处理器的时候传入另外两个个处理器就可以了
/**
* 解析Content-Type为application/json的默认解析器是RequestResponseBodyMethodProcessor
*/
private RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor;
/**
* 解析Content-Type为application/x-www-form-urlencoded的默认解析器是ServletModelAttributeMethodProcessor
*/
private ServletModelAttributeMethodProcessor servletModelAttributeMethodProcessor;
到这一步就已经实现了RequestParam注解也可以接受Json格式数据了,我也没进行更多的测试,具体还会出现什么关联性的问题暂时是没发现,后续如果 * 友出现了什么问题可以留言一起讨论,本人小菜鸡一枚,希望写的不好的地方大神多多指教,不胜感激!
来源:https://blog.csdn.net/weixin_42536015/article/details/106906055