spring 如果多部分文件参数为空,则 ModelAttribute 的多部分/表单数据绑定失败

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/28757551/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 00:24:28  来源:igfitidea点击:

multipart/form-data binding for ModelAttribute fails if multipart file param is null

springspring-mvcspring-bootrecaptchathymeleaf

提问by Anadi Misra

I have a controller mapped for processing uploaded files

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

Controller

控制器

  @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

形式

<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>

Now, if a person misses attaching a file to the form before submit,

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

-----------------------------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"

[email protected]
-----------------------------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--

or tries to submit without validating captcha, I get this exception

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

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

What I expect is that I get empty values for captcha response and file and then my controller method should be able handle it, and send user back to the form with specific error message. It works like so on forms without multipart data, i.e. I do not get binding errors but null values in the controller arguments. I see this issue only when I use Multipart Form data, the binding goes all fine if all the data is populated, i.e. a user verifies captcha, and attaches a file.

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

Making these params optional or using RequestPart hasn't also helped (I admit I really do not get what is the purpose of RequestPartannotation) So, changing the controller to this (knee jerk experimentations ;-))

使这些参数可选或使用 RequestPart 也没有帮助(我承认我真的不明白RequestPart注释的目的是什么)所以,将控制器更改为此(膝跳实验;-))

  @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) 

does not help either. Do I have to extend StandardServletMultipartResolveror is it something to change/fix in SpringInputGeneralFieldAttrProcessor, or am I missing some minor detail here?

也没有帮助。我是否必须扩展StandardServletMultipartResolver或者它是否需要在SpringInputGeneralFieldAttrProcessor 中更改/修复,或者我在这里遗漏了一些小细节?

Update

更新

Adding Controller method

添加控制器方法

  @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";
      }
    };
  }

回答by minion

@RequestPart relies on HttpMessageConvertors and content-type to bind the multipart data to method param, whereas @RequestParam relies on registered convertors to do the conversion. Spring mvc provides certain convertors by default. You can use either @RequestParam or @RequestPart to bind the file data. Most of the applications use commons file upload for uploading file and register

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

org.springframework.web.multipart.commons.CommonsMultipartResolver

org.springframework.web.multipart.commons.CommonsMultipartResolver

for multi part resolving. When this is registered, spring check every request for multi part data and use it to resolve this to method arg. check here

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

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

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

There are couple of items you can try. Make sure both of your captcha and file params are optional like below in your controller. I switched to @RequestParam for captcha.

您可以尝试几个项目。确保您的验证码和文件参数都是可选的,如下所示在您的控制器中。我切换到 @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) 

Hope this helps.

希望这可以帮助。

回答by ndrone

  1. Your Controller @RequestMappingisn't mapped to the same path as for form
  2. In your form make sure your input name for the file and captcha match the @RequestParam
  3. @RequestParamdoesn't pass a null notice I'm looking for length() == 0and file.isEmpty()
  1. 您的控制器@RequestMapping未映射到与表单相同的路径
  2. 在您的表单中,确保您的文件输入名称和验证码匹配 @RequestParam
  3. @RequestParam没有通过我正在寻找的空通知length() == 0并且file.isEmpty()

Also you may want to take a look at the spring guide for file upload

你也可能想看看文件上传弹簧指南

Controller

控制器

@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

形式

<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>