【问题标题】:multipart/form-data binding for ModelAttribute fails if multipart file param is null如果多部分文件参数为空,则 ModelAttribute 的多部分/表单数据绑定失败
【发布时间】:2015-02-27 04:29:40
【问题描述】:

我有一个控制器映射用于处理上传的文件

控制器

  @RequestMapping(value = "/careers/pursue", method = RequestMethod.POST)
  public Callable<String> pursue(
      final @RequestParam("g-recaptcha-response") String captchaResponse,
      final @RequestParam("file") MultipartFile file,
      final @ModelAttribute("jobapplication") @Valid JobApplication application, final BindingResult bindingResult,
      final Model model)  

表格

<form name="jobsForm" id="jobsForm" novalidate="novalidate" action="#" th:action="@{/careers/pursue}"
                    th:object="${jobapplication}" method="post" enctype="multipart/form-data">
    <div class="control-group form-group">
        <div class="controls">
            <label>First Name:</label>
            <input type="text" class="form-control" id="firstName" th:field="*{firstName}" required="required" data-validation-required-message="Please enter your name." />
            <p class="help-block"></p>
        </div>
    </div>
    <div class="control-group form-group">
        <div class="controls">
            <label>Last Name:</label>
            <input type="text" class="form-control" id="lastName" th:field="*{lastName}" required="required" data-validation-required-message="Please enter your name." />
            <p class="help-block"></p>
        </div>
    </div>                    
    <div class="control-group form-group">
        <div class="controls">
            <label>Phone Number:</label>
            <input type="tel" class="form-control" id="phone" th:field="*{phone}" required="required" data-validation-required-message="Please enter your phone number." />
        </div>
    </div>
    <div class="control-group form-group">
        <div class="controls">
            <label>Email Address:</label>
            <input type="email" class="form-control" id="email" th:field="*{email}" required="required" data-validation-required-message="Please enter your email address."/>
        </div>
    </div>
    <div class="control-group form-group">
        <div class="controls">
            <label>Role:</label>
            <input type="email" class="form-control" id="role" th:field="*{role}" required="required" data-validation-required-message="Please enter your email address."/>
        </div>
    </div>                    
    <div class=" control-group form-group">
                            <div class="g-recaptcha" data-sitekey="ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"></div>                    
    </div>
    <div class=" control-group form-group">
         <span class="btn btn-primary btn-file">
                                Add your Resumé <input type="file" name="file" id="file" required="required"/>
                         </span>
    </div>
    <div id="success"></div>
    <!-- For success/fail messages -->
    <button type="submit" class="btn btn-primary">Apply!</button>
</form>

现在,如果有人在提交之前错过了将文件附加到表单,

-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="firstName"

Anadi
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="lastName"

Misra
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="phone"

9845420420
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="email"

foo@bar.com
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="role"

open.project
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="g-recaptcha-response"

03AHJ_Vuv9i7WQ_4zCipfnyrLNl6467l_cZgGIhkdpLjS1M0YmWvwQMOWQeRcrAHFh8s3-jO13NQs7019lzI7UobwNeHKIhBmcLMiVGPk38Iy8BjrEi2glI4QGjE4VTvRhV_-WWYsmlzV_7PRPE5Y8L0NboPXYoG9JSabMOL8V958w74pOzkxabsoR4wouCSa0gzo0EbOsLiCWjd0MAvZiCcKJGdwIlMp0WIjxcufB-RfG2F0rwv65yrgL-By0bdMewkWULY_aRvC-FRSqOEM9X5Qg4gviA-cvc5IY2XnRtaUALOPlR_QbwjgUKl2mJEFNab6Pks3MlsivuEZFkba4isDFlrJ4jXwBBQ
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="file"; filename=""
Content-Type: application/octet-stream


-----------------------------749526091303082321866336941--

或尝试在未验证验证码的情况下提交,我收到此异常

Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'org.thymeleaf.spring4.processor.attr.SpringInputGeneralFieldAttrProcessor' (jobs:91)
....
....
Caused by: java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'jobapplication' available as request attribute
at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:144)
at org.thymeleaf.spring4.util.FieldUtils.getBindStatusFromParsedExpression(FieldUtils.java:396)
at org.thymeleaf.spring4.util.FieldUtils.getBindStatus(FieldUtils.java:323)
at org.thymeleaf.spring4.util.FieldUtils.getBindStatus(FieldUtils.java:289)
at org.thymeleaf.spring4.processor.attr.AbstractSpringFieldAttrProcessor.processAttribute(AbstractSpringFieldAttrProcessor.java:98)
at org.thymeleaf.processor.attr.AbstractAttrProcessor.doProcess(AbstractAttrProcessor.java:87)
at org.thymeleaf.processor.AbstractProcessor.process(AbstractProcessor.java:212)
... 66 common frames omitted

我希望我得到验证码响应和文件的空值,然后我的控制器方法应该能够处理它,并将用户发送回带有特定错误消息的表单。它在没有多部分数据的表单上工作,即我没有得到绑定错误,但控制器参数中的值为空。我仅在使用 Multipart Form data 时才看到此问题,如果填充了所有数据,即用户验证验证码并附加文件,则绑定一切正常。

将这些参数设为可选或使用 RequestPart 也无济于事(我承认我真的不明白 RequestPartannotation 的目的是什么)因此,将控制器更改为此(膝跳实验 ;-))

  @RequestMapping(value = "/careers/pursue", method = RequestMethod.POST)
  public Callable<String> pursue(
      final @RequestPart(value = "g-recaptcha-response", required = false) String captchaResponse,
      final @RequestPart(value = "file", required = false) MultipartFile file,
      final @ModelAttribute("jobapplication") @Valid JobApplication application, final BindingResult bindingResult,
      final Model model) 

也无济于事。我是否必须扩展 StandardServletMultipartResolver 或者在 SpringInputGeneralFieldAttrProcessor 中进行更改/修复,还是我在这里遗漏了一些小细节?

更新

添加控制器方法

  @RequestMapping(value = "/careers/pursue", method = RequestMethod.POST)
  public Callable<String> pursue(final @ModelAttribute("jobapplication") @Valid JobApplication application,
      final BindingResult bindingResult, final Model model,
      final @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
      final @RequestPart(value = "file", required = false) MultipartFile file) {
    return new Callable<String>() {
      @Override
      public String call() throws Exception {
        try {
          model.asMap().clear();
          GoogleCaptchaResponseData response = captchaVerifier.isCaptchaResponseValid(captchaResponse).get();
          model.addAttribute("recaptcha", response.isSuccess());
          model.addAttribute("recaptchamessage", response.getErrorCodes());

          if (response.isSuccess() && !file.isEmpty()) {
            byte[] bytes = file.getBytes();

            LOGGER.info("Found file of type {}", file.getOriginalFilename());
            ByteArrayInputStream inputBytes = new ByteArrayInputStream(bytes);
            mailApi.sendMail(mailApi.buildJobApplicationEmail(application, new BufferedInputStream(inputBytes)));
            model.asMap().clear();
            model.addAttribute("uploadsuccess", true);
            model.addAttribute("resource_host", resourceHost);
            model.addAttribute("jobapplication", new JobApplication());
          }
        } catch (InterruptedException | ExecutionException e) {
          LOGGER.error(e.getMessage(), e);
          model.asMap().clear();
          model.addAttribute("jobapplication", application);
          model.addAttribute("resource_host", resourceHost);
          model.addAttribute("uploadsuccess", false);
        }
        return "jobs";
      }
    };
  }

【问题讨论】:

    标签: spring spring-mvc spring-boot recaptcha thymeleaf


    【解决方案1】:

    @RequestPart 依赖 HttpMessageConvertors 和 content-type 将多部分数据绑定到方法参数,而 @RequestParam 依赖注册的转换器进行转换。 Spring mvc 默认提供了某些转换器。您可以使用@RequestParam 或@RequestPart 来绑定文件数据。大部分应用都使用commons file upload来上传文件和注册

    org.springframework.web.multipart.commons.CommonsMultipartResolver

    用于多部分解析。注册后,spring 检查多部分数据的每个请求,并使用它将其解析为方法 arg。看这里

    http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-multipart

    您可以尝试几个项目。确保您的验证码和文件参数都是可选的,如下所示在您的控制器中。我切换到@RequestParam 进行验证码。

    @RequestMapping(value = "/careers/pursue", method = RequestMethod.POST)
      public Callable<String> pursue(
          final @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
          final @RequestPart(value = "file", required = false) MultipartFile file,
          final @ModelAttribute("jobapplication") @Valid JobApplication application, final BindingResult bindingResult,
          final Model model) 
    

    希望这会有所帮助。

    【讨论】:

    • StandardServletMultipartResolver 的行为与CommonsMultipartResolver 有什么不同吗?
    • 它也可以提供帮助。它的配置不同。我主要使用公地。你在用什么?
    • 我正在使用 Spring Boot,它显然使用 StandardServletMultipartResolver 作为默认的多部分解析器。
    • 改成@minion建议的语法,并为CommonsMultipartResolver添加了配置,没用,还是报同样的错误
    • @minion 我也有同样的问题。如果你们找到任何解决方法,请告诉我。
    【解决方案2】:
    1. 您的控制器 @RequestMapping 未映射到与表单相同的路径
    2. 在您的表单中确保您输入的文件名称和验证码与@RequestParam 匹配
    3. @RequestParam 没有传递空通知我正在寻找 length() == 0file.isEmpty()

    你也可以看看spring guide for file upload

    控制器

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
        public String pursue(
                final @RequestParam("g-recaptcha-response") String captchaResponse,
                final @RequestParam("file") MultipartFile file,
                final @ModelAttribute("jobapplication") @Valid JobApplication application, final BindingResult bindingResult,
                final Model model)
        {
            if (bindingResult.hasErrors() || captchaResponse.length() == 0 || file.isEmpty())
            {
                return "form";
            }
    
            return "redirect:/";
        }
    

    表格

    <form name="jobsForm" id="jobsForm" novalidate="novalidate" action="#" th:action="@{/upload}"
          th:object="${jobapplication}" method="post" enctype="multipart/form-data">
        <span>First Name: </span>
        <input id="firstname" th:field="*{firstName}" type="text"/><br/>
        <span>Last Name: </span>
        <input id="lastname" th:field="*{lastName}" type="text"/><br/>
        <span>Phone: </span>
        <input id="phone" th:field="*{phone}" type="text"/><br/>
        <span>Email: </span>
        <input id="email" th:field="*{email}" type="text"/><br/>
        <span>Role: </span>
        <input id="role" th:field="*{role}" type="text"/><br/>
        <span></span>
        <input type="text" name="g-recaptcha-response"/><br/>
        <span>File: </span>
        <input type="file" name="file"/><br/>
        <input id="submit" type="submit"/>
    </form>
    

    【讨论】:

    • 如果任何参数为空,为什么在控制器方法可以执行之前我得到异常?表单操作 url 没有问题,我在编辑问题时忘记更新它
    • 您发布的错误是关于 jobapplication bean 而不是文件。 jobapplication bean 是否为空?验证码和文件是 jobapplication 的一部分吗?
    • 如果您查看我发布的多部分数据,如果fileg-response-captcha 为空,我会收到jobapplication bean 的此错误。这让我很困惑。验证码参数附加到谷歌验证码的请求,文件和验证码不是jobapplication bean的一部分
    • @AnadiMisra 我将文件作为 bean 的一部分。当我尝试提交表单而不选择文件时,“modelattribute”映射失败。你找到解决办法了吗?
    猜你喜欢
    • 2021-04-27
    • 1970-01-01
    • 1970-01-01
    • 2018-07-02
    • 2013-04-27
    • 1970-01-01
    • 2014-09-21
    • 1970-01-01
    • 2022-07-07
    相关资源
    最近更新 更多