Spring/SpringBoot @RequestParam注解无法读取application/json格式数据问题解决

目录
  • 前言
  • 一、RequestMappingHandlerAdapter
  • 二、HandlerMethodArgumentResolver
  • 三、RequestParamMethodArgumentResolver
  • 四、MyHandlerMethodArgumentResolver
  • 四、ConfigArgumentResolvers
  • 五、MyHttpServletRequestWrapper
  • 六、HttpServletRequestReplacedFilter
  • 七、总结
  • 总结

前言

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格式数据了,我也没进行更多的测试,具体还会出现什么关联性的问题暂时是没发现,后续如果有码友出现了什么问题可以留言一起讨论,本人小菜鸡一枚,希望写的不好的地方大神多多指教,不胜感激!

总结

到此这篇关于Spring/SpringBoot @RequestParam注解无法读取application/json格式数据问题解决的文章就介绍到这了,更多相关@RequestParam注解无法读取application/json内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring/SpringBoot @RequestParam注解无法读取application/json格式数据问题解决

    目录 前言 一.RequestMappingHandlerAdapter 二.HandlerMethodArgumentResolver 三.RequestParamMethodArgumentResolver 四.MyHandlerMethodArgumentResolver 四.ConfigArgumentResolvers 五.MyHttpServletRequestWrapper 六.HttpServletRequestReplacedFilter 七.总结 总结 前言 Emmmm…最近

  • springboot读取application.yaml文件数据的方法

    本文实例为大家分享了springboot读取application.yaml文件数据的具体代码,供大家参考,具体内容如下 提示:以下是本篇文章正文内容,下面案例可供参考 一.创建并编辑对应的文件 1.application.yaml !!!这里一定要注意,datasource一定不能写成dataSource,因为会和Spring内部的产生冲突 server:   port: 8080 contry: china user:   - name: zhangsan     age: 18   - n

  • springboot 返回json格式数据时间格式配置方式

    目录 返回json格式数据时间格式配置 返回JSON日期格式问题 返回json格式数据时间格式配置 数据库里面查出来的时间是时间错格式,前段需要处理才能展示相应的格式,自己一个个转的话太麻烦,所以可以在apllication.property加入下面配置就可以 #时间戳统一转换 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 其中time-zone是时区偏移设置,如果不指定的话时间和北京时

  • java读取文件内容,解析Json格式数据方式

    目录 java读取文件内容,解析Json格式数据 一.读取txt文件内容(Json格式数据) 二.解析处理Json格式数据 三.结果存入数据库 四.测试 java 读取txt文件中的json数据,进行导出 以下代码可直接运行 java读取文件内容,解析Json格式数据 一.读取txt文件内容(Json格式数据) public static String reader(String filePath) { try { File file = new File(filePath); if (file

  • SpringMVC环境下实现的Ajax异步请求JSON格式数据

    一 环境搭建 首先是常规的spring mvc环境搭建,不用多说,需要注意的是,这里需要引入jackson相关jar包,然后在spring配置文件"springmvc-servlet.xml"中添加json解析相关配置,我这里的完整代码如下: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schem

  • springMVC返回复杂的json格式数据方法

    一.springMVC返回json格式数据常用的写法是使用@ResponseBody注解,在每一个方法前加上这个注解,springMVC的json解析机制会自动把返回值(Object类型的对象)转换为json格式的数据,如果返回的json对象比较复杂,每一个方法都要定义很多对象,会给开发带来很大的工作量,以下介绍一种方法,直接使用json对象设置数据并放回. 二.当遇到以下json格式数据是,如果差用@ResponseBody方式需要创建多个Java对象. { "total":2, &

  • 实例详解JSON数据格式及json格式数据域字符串相互转换

    JSON(JavaScript Object Notation)是一种轻量级的数据交换格式.JSON采用完全独立于语言的文本格式,这些特性使JSON成为理想的数据交换语言.易于人阅读和编写,同时也易于机器解析和生成. 基础结构 JSON建构于两种结构: 1. "名称/值"对的集合(A collection of name/value pairs).不同的语言中,它被理解为对象(object),记录(record),结构(struct),字典(dictionary),哈希表(hash t

  • JS对象与JSON格式数据相互转换

    目前的项目数据交互几乎都用JQuery,所以处理流程是:前端页面数据->JS对象->jQuery提交->python处理,另外一种就是倒过来.python肯定不能直接处理JS对象数据,所以要把JS对象转换成为python能处理的一种数据格式(通常是字典dict),同样,python取数据反馈到前端也要把字典数据转换成JS能处理的对象,这个中间转换数据格式通常就是JSON. 一.JS对象转换成为JSON 流程:读取前端页面数据,组装成为JS对象,并通过jQuery的$.post()方法传递

  • jquery解析json格式数据的方法(对象、字符串)

    本文实例讲述了jquery解析json格式数据的方法.分享给大家供大家参考,具体如下: json数据是我们常用的一种小型的数据实时交换的一个东西,他可以利用jquery或js进行解析,下面我来介绍jquery解析json字符串方法. 一.jQuery解析Json数据格式: 使用这种方法,你必须在Ajax请求中设置参数: dataType: "json" 获取通过回调函数返回的数据并解析得到我们想要的值,看源码: jQuery.ajax({ url: full_url, dataType

  • php基于dom实现读取图书xml格式数据的方法

    本文实例讲述了php基于dom实现读取图书xml格式数据的方法.分享给大家供大家参考,具体如下: <?php $doc = new DOMDocument(); $doc->load( 'books.xml' ); $books = $doc->getElementsByTagName( "book" ); foreach( $books as $book ) { $authors = $book->getElementsByTagName( "aut

随机推荐