Saturday, August 20, 2016

[DevNote] Uploading Multipart File with Spring MVC

[DevNote] Uploading Multipart File with Spring MVC


In this article we are going to examine how to setup multipart file uploading and handling in Spring MVC.

It is common to submit images within a form submission, Spring MVC provides MultipartFile to represent submitted multiple part content.

Keywords: Spring MVC, MultipartFile

Here's the Maven dependency for some modules we need for MultipartFile handling:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>${springframework.version}</version>
  </dependency>

  <!-- Servlet -->
  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>javax.servlet-api</artifactId>
   <version>3.1.0</version>
  </dependency>

  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>jstl</artifactId>
   <version>1.2</version>
  </dependency>
  
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>1.1.0.Final</version>
        </dependency>

Most of needed dependencies are already in the core Spring MVC artifact and the only thing that is extra is javax.validation, we are going to use the @Valid annotation to validate the data user uploaded.


Configuration


To resolve the multiple part content in HTTP request, we need a resolver for MultipartFile, Spring MVC provides MultipartResolver interface which is the strategy for resolving MultipartFile and we can use the CommonMultipartResolver as the implementation we what to use. So the first thing we need is to create a bean method for it in our application configuration.

1
2
3
4
5
6
    @Bean
    public CommonsMultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver=new CommonsMultipartResolver();
        resolver.setDefaultEncoding("utf-8");
        return resolver;
    }

The next we need to configure our MultipartFile with our servlet, so that the servlet would know what to expect from the request. There are two ways of doing this configuration, one is by @MultipartConfig annotation and the other is creating a MultipartConfigElement then pass it to servlet registration. 

I used the second option:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        registration.setMultipartConfig(getMultipartConfigElement());
    }
 
 private MultipartConfigElement getMultipartConfigElement() {
  MultipartConfigElement multipartConfigElement = new MultipartConfigElement(FileDataConfig.TEMPROOT,
    FileDataConfig.MAX_FILE_SIZE, FileDataConfig.MAX_REQUEST_SIZE, FileDataConfig.FILE_SIZE_THRESHOLD);
  return multipartConfigElement;
 }

Final Paths, Temp paths


In the MultipartConfig, we configured four things:

Temporary upload location
Maximum file size allowed

Maximum request size
File size threshold

One thing in this registration we need to pay attention is the first argument of the constructor of MultipartConfigElement, which is the temporary directory the uploaded file will ended locating, so one should not mix this with the final upload location. 

Final path is never configured with the resolver, it's the business logic who determine the final upload location.

File Validation


For the content in the incoming HTTP request, usually we will be putting constraint on the content. This can be achieved by implementing a container for the MultipartFile and a a Validator then bind the content with validator to perform validation.

A file container can be defined as simple as follows:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class FileContainer {

 private MultipartFile file;
 
 public MultipartFile getFile() {
  return file;
 }
 
 public void setFile(MultipartFile file) {
  this.file = file;
 }
}

Then our file validator will be:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class FileValidator implements Validator {

 @Override
 public boolean supports(Class<?> obj) {
  return FileContainer.class.isAssignableFrom(obj);
 }

 @Override
 public void validate(Object obj, Errors error) {
  FileContainer file = (FileContainer) obj;
  if (file.getFile() != null) {
   if (file.getFile().getSize() == 0) {
    error.reject("file", "missing.file");
   } else if (!file.getFile().getContentType().equals("application/pdf")) {
    /* Allowed only PDF, which has MIME type of application/pdf,
     * application/x-pdf is experimental type not the official standard
     * */
    error.reject("file", "unsupported type");
   }
  }
 }

}

An instance of FileValidator will be bound to the FileContainer, so we need to make sure the FileValidator is simple enough and has no internal state, because this will be injected as a Singleton.

Binding of the two looks like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Controller
public class FileController {

 @Autowired
 private FileValidator fileValidator;
 
 @InitBinder("fileContainer")
 protected void initFileContainerBinder(WebDataBinder binder) {
  binder.setValidator(fileValidator);
 }
}

Controller


Let's examine how we use them altogether in the controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 @RequestMapping(value = "/uploadFile", method = RequestMethod.POST)
 public String DemoUploadSingleFile(HttpServletRequest request, @Valid FileContainer fileContainer,
   BindingResult binding) {
  
  if (binding.hasErrors()) {
   return "hasError";
  } else {
   MultipartFile file = fileContainer.getFile();
   String filename = file.getOriginalFilename();
   try {
    Files.copy(file.getInputStream(), Paths.get(FileDataConfig.STASHROOT, filename));
    return "success";
   } catch (IOException | RuntimeException e) {
    return "hasError";
   }
  }
 }

Since we've already coded most of the validation and content handling in other places, the controller just simply need to check if the MultipartFile is valid and then save the file to the final location.

No comments:

Post a Comment