您的当前位置:首页正文

spring项目controller层接收不到post表单提交数据的问题排查

2024-11-25 来源:个人技术集锦

问题描述

    @PostMapping("/test")
    public String test(String token, HttpServletRequest request) {
        logger.info("token:" + token);
        return "token:" + token;
    }

postman发送请求如下

原因分析及排查:

猜想问题可能产生的原因:

  • 某些拦截器特殊处理了请求
    排查所有拦截器未发现异常,甚至放行该请求也不行
  • 项目某些配置设置不正确或未开启
    检查spring的配置文件,application.yml或者application.properties等配置文件,查看servlet相关配置是否开启。查看到请求的content-type是multipart/form-data,便查看是否有如下配置,默认为true,开启表单文件上传,查看发现没有特别设置
spring.servlet.multipart.enabled=true
  • springboot版本原因,项目使用1.5.x版本
    检查请求源码,debug断点调试,从servlet分发到controller各个链路,未找到具体原因
  • 其他

解决方案:

其实上面检查某些配置的步骤就很接近答案了,最后是这样确定问题产生原因的:将项目的所在模块copy一份,去掉所有拦截器等配置等,只留着启动类和controller,pom中的依赖只留下springboot核心的依赖,删除一切特殊配置如启动类下的某些bean,发现controller能接收到表单提交数据,重新加上启动类的bean后,发现问题复现,最终确定问题为启动类下添加的bean有问题,如下:

@Bean
    public ServletRegistrationBean<CXFServlet> dispatcherServlet() {
        ServletRegistrationBean<CXFServlet> servletRegistrationBean = new ServletRegistrationBean<>(new CXFServlet(), "/services/*");
        servletRegistrationBean.setLoadOnStartup(1);
        return servletRegistrationBean;
    }

    @Bean
    public ServletRegistrationBean restServlet(){
        //注解扫描上下文
        AnnotationConfigWebApplicationContext applicationContext
                = new AnnotationConfigWebApplicationContext();
        //base package
        applicationContext.scan("com.*");
        //通过构造函数指定dispatcherServlet的上下文
        DispatcherServlet rest_dispatcherServlet
                = new DispatcherServlet(applicationContext);

        //用ServletRegistrationBean包装servlet
        ServletRegistrationBean registrationBean
                = new ServletRegistrationBean(rest_dispatcherServlet);
        registrationBean.setLoadOnStartup(1);
        //指定urlmapping
        registrationBean.addUrlMappings("/*");
        //指定name,如果不指定默认为dispatcherServlet
        registrationBean.setName("rest");
        return registrationBean;
    }

此处第一个bean注册了一个servlet,名称为dispatcherServlet,从代码可以看出,这个bean目的是给webservice服务用来分发请求的。坏就坏在这里,这个dispatcherServlet与springboot本身的dispatcherServlet重名了!!!springboot本身的dispatcherServlet是用来分发http请求到controller层的。

我们可以合理猜测,原开发者是想将项目用作了webservice服务端,但这样一来却覆盖了springboot中dispatcherServlet,发现controller层请求异常后,下面又注册了一个bean试图解决controller的请求异常问题,但这忽略了我们遇到的问题:虽然controller层接口能请求到,却接收不到表单提交的数据了。

知其然,还要知其所以然。下面还要分析下两者的差异,为什么自己创建的dispatcherServlet会获取不到表单的属性???

查看调用链

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		HttpServletRequest requestToUse = request;

		if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
			String paramValue = request.getParameter(this.methodParam);
			if (StringUtils.hasLength(paramValue)) {
				requestToUse = new HttpMethodRequestWrapper(request, paramValue);
			}
		}

		filterChain.doFilter(requestToUse, response);
	}

其中getParameter方法,会调用ApplicationHttpRequest中的getParameter方法

    @Override
    public String getParameter(String name) {

        parseParameters();

        String[] value = parameters.get(name);
        if (value == null) {
            return null;
        }
        return value[0];

    }

其中的parseParameters()方法中的逻辑,则是导致差异的根本原因在处理表单提交时,会进行parseParts操作

protected void parseParameters() {
        parametersParsed = true;
        ....
  		if ("multipart/form-data".equals(contentType)) {
            parseParts(false);
            success = true;
            return;
        }
        ....

parseParts方法如下:

具体来说,MultipartConfigElement提供了以下配置信息:

getLocation():指定存储上传文件的临时位置。
getMaxFileSize():指定单个上传文件的最大大小(以字节为单位)。
getMaxRequestSize():指定整个请求的最大大小(包括所有上传的文件)(以字节为单位)。
getFileSizeThreshold():指定在内存中存储文件数据的大小阈值。如果上传的文件大小小于这个阈值,那么文件数据会被存储在内存中,否则会被存储在磁盘上。
当Tomcat处理一个multipart/form-data类型的请求时(如文件上传),它会检查MultipartConfigElement中的配置信息来决定如何处理这个请求。例如,如果上传的文件大小超过了getMaxFileSize()或getMaxRequestSize()中指定的值,Tomcat会抛出一个异常来拒绝这个请求。

故此我们需要在创建servlet时,就需要配置MultipartConfigElement这个类

@Bean
    public ServletRegistrationBean restServlet(){
        //注解扫描上下文
        AnnotationConfigWebApplicationContext applicationContext
                = new AnnotationConfigWebApplicationContext();
        //base package
        applicationContext.scan("com.*");
        //通过构造函数指定dispatcherServlet的上下文
        DispatcherServlet rest_dispatcherServlet
                = new DispatcherServlet(applicationContext);

        //用ServletRegistrationBean包装servlet
        ServletRegistrationBean registrationBean
                = new ServletRegistrationBean(rest_dispatcherServlet);
        registrationBean.setLoadOnStartup(1);
        //指定url_mapping
        registrationBean.addUrlMappings("/*");
        //指定name,如果不指定默认为dispatcherServlet
        registrationBean.setName("rest");

        // 配置MultipartConfigElement
        MultipartConfigElement multipartConfigElement = new MultipartConfigElement(
                "D:\\temp", // 文件存储的临时位置
                2097152L, // 单个文件最大大小,单位字节,这里是2MB
                4194304L, // 请求的最大大小,单位字节,这里是4MB
                10240 // 文件大小阈值,超过这个值会存储在磁盘上,单位字节,这里是10KB
        );
        registrationBean.setMultipartConfig(multipartConfigElement);
        return registrationBean;
    }

最后,调用postman发现,成功在controller层获取到了表单中的属性值!!!

总结:

这个问题应该在一个月前就遇到了,当时没有找到根本原因,但心里会一直想着,今天总算找到原因了。
所以,能用spring默认的就用默认的配置,自己配置可能根本不知道会漏了哪些东西,导致一些小问题的产生。其实根本原因就是复制粘贴代码时,需要考虑到对项目产生的影响。

如果您对技术有兴趣,友好交流,可以加v进技术群一起沟通,v:zzs1067632338,备注csdn即可

显示全文