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


猜你喜欢
- 1 前言ATMS 即 ActivityTaskManagerService,用于管理 Activity 及其容器(任务、堆栈、显示等)。AT
- MainActivity如下: package cn.testgethandsetinfo; import android.os.Bundl
- 一、Future 接口当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用 Fu
- 以下共有4个函数分别是:1.从剪切板获得文字。2.将字符串复制到剪切板。3.从剪切板获得图片。4.复制图片到剪切板。/** * 从剪切板获得
- 在日常开发中经常遇到控件不能随着父容器大小的改变而且自动改变控件的所在位置和大小。以下是实现的代码 /// <summary>
- 一、常见场景1、ThreadLocal作为线程上下文副本,那么一种最常见的使用方式就是用来方法隐式传参,通过提供的set()和get()两个
- 1、maven是什么,为什么存在?项目结构是什么样子,怎么定位jar官方网站说了好多,整的多复杂一样,简单说:maven是一个管理包的工具。
- 长久以来统领javaee领域的脚手架以spring struts2 mybatis/hib
- Android 打开相册选择单张图片实现代码
- 众所周知Web服务器与客户端之间的通信是使用HTTP协议的。HTTP是一个客户端和服务器端请求和应答的标准(TCP)。因为HTTP协议是基于
- 什么是反射?反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动
- java "equals"和"==”异同首先简单说一下“equal”和“==”==操作对于基本数据类型比较的是
- 在开发过程中,与用户交互式免不了会用到对话框以实现更好的用户体验,所以掌握几种对话框的实现方法还是非常有必要的。在看具体实例之前先对Aler
- java * 类可以分为两种。 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存
- 本文实例讲述了C#读取csv格式文件的方法。分享给大家供大家参考。具体实现方法如下:一、CSV文件规则 1 开头是不留空,以行为单
- 前言随着敏捷开发的流行,编写单元测试已经成为业界共识。但如何来衡量单元测试的质量呢?有些管理者片面追求单元测试的数量,导致底下的开发人员投机
- 重写 equals()方法 和 hashCode()方法最近看了学习了集合的简单的知识,碰到了讲解 Set 的部分,感觉很好奇,这里对于 S
- MyBatis 本是apache的一个开源项目iBatis, 2010年这个项
- 遇到一个@ConditionalOnMissingBean失效的问题,今天花点时间来分析一下。现场回放services首先介绍下代码结构:有
- Grpc是googe开发的,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。新公司的项目服务之间的调用使用的Grpc来实现服务间