Spring MVC File Upload Example + Validator

In this tutorial we show how to upload a file using Spring MVC and apache commons-fileupload. The uploaded file is validated against a custom Spring Validator. When the file exceeds the maximum allowed upload size, we correctly handle the exception by catching the exception and adding an appropriate message to the response. We’ll show how to upload a single file and also how to upload multiple files.

Maven Dependencies

We need to add the following dependencies to the pom.xml file, so maven can download and manage them automatically. Note, we added the commons-fileupload which is used to upload a MultipartFile. We also include the javax.validation API so we can annotate our controller methods with the @Valid annotation.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.memorynotfound.spring.mvc</groupId>
    <artifactId>file-upload</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>SPRING-MVC - ${project.artifactId}</name>
    <url>http://memorynotfound.com</url>
    <packaging>war</packaging>

    <properties>
        <encoding>UTF-8</encoding>
        <spring.version>4.2.6.RELEASE</spring.version>
    </properties>

    <dependencies>
        <!-- spring dependencies -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- apache file upload -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.1</version>
        </dependency>

        <!-- java bean validation -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>1.1.0.Final</version>
        </dependency>

        <!-- servlet api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Project Structure

Make sure your project looks similar to the following structure.

src
|--main
|    +--java
|        +--com
|            +--memorynotfound
|                +--config
|                    |--ServletInitializer.java
|                    |--WebConfig.java
|                +--controller
|                    |--HomeController.java
|                +--exception
|                    |--GlobalExceptionHandler.java
|                +--model
|                    |--FileBucket.java
|                    |--MultiFileBucket.java
|                +--validator
|                    |--FileValidator.java
|                    |--MultiFileValidator.java
|    +--resources
|        |--messages.properties
|    +--webapp
|        +--WEB-INF
|            +--views
|                |--multiple.jsp
|                |--single.jsp
|                |--success.jsp
pom.xml

Spring MVC File Upload Java/XML Configuration

The configuration for single or multiple file uploads is the same. The CommonsMultipartResolver saves temporary files to the servlet container’s temporary directory. You can optionally configure the following properties: maxUploadSize, maxUploadSizePerFile, maxInMemorySize, uploadDir, defaultEncoding and resolveLazily. The last one ‘resolveLazily‘ is important for handling thrown exceptions correctly, we need to set this property to true.

package com.memorynotfound.config;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@EnableWebMvc
@Configuration
@ComponentScan({"com.memorynotfound"})
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        return messageSource;
    }

    @Bean(name="multipartResolver")
    public CommonsMultipartResolver multipartResolver(){
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSizePerFile(10240); //10Kb
        resolver.setDefaultEncoding("UTF-8");
        resolver.setResolveLazily(true);
        return resolver;
    }

    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/views/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

Here, we have the equivalent Spring XML File Upload configuration.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/mvc
                           http://www.springframework.org/schema/mvc/spring-mvc.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd ">

    <mvc:annotation-driven />
    <context:component-scan base-package="com.memorynotfound" />
    
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="messages"/>
    </bean>
    
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSizePerFile" value="10240"/>
        <property name="defaultEncoding" value="UTF-8"/>
        <property name="resolveLazily" value="true"/>
    </bean>
    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/views/" />
        <property name="suffix" value=".jsp" />
    </bean>

</beans>

Handling Simple navigation

For simplicity, we put all the navigation cases in a single NavigationController. Things to notice, we initialize the multiple upload page with 3 FileBucket objects.

package com.memorynotfound.controller;

import com.memorynotfound.model.MultiFileBucket;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class NavigationController {

    @RequestMapping(value = "/single", method = RequestMethod.GET)
    public String single(Model model){
        model.addAttribute("fileBucket", new FileBucket());
        return "single";
    }

    @RequestMapping(value = "/multiple", method = RequestMethod.GET)
    public String multiple(Model model){
        model.addAttribute("multiFileBucket", new MultiFileBucket(3));
        return "multiple";
    }

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(){
        return "index";
    }

    @RequestMapping(value = "/success", method = RequestMethod.GET)
    public String success(){
        return "success";
    }

    @RequestMapping(value = "/error", method = RequestMethod.GET)
    public String error(){
        return "error";
    }

}

Validator Messages

These validation messages are used to display an appropriate message to the user. Add the messages.properties to the src/main/resources/ folder.

file.empty = Please select a file.
file.maxsize = {0} exceeds the maximum size of {1} Kb.

Spring MVC Single File Upload

The FileBucket has a MultipartFile object which will hold the uploaded file.

package com.memorynotfound.model;

import org.springframework.web.multipart.MultipartFile;

public class FileBucket {

    MultipartFile file;

    public MultipartFile getFile() {
        return file;
    }

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

The SingleFileUploadController is responsible for uploading a file to a /tmp/ directory. We inject a FileValidator, which we’ll create later. We use the @InitBinder annotation and provide a WebDataBinder as an argument in the initBinderFileBucket method to bind the FileValidator onto the controller. Finally, we create the controller method which will handle the file upload. By annotating the FileBucket with the @Valid annotation, the binded validators are automatically executed on form submission. We use the BindingResult to redirect to the current page if any errors have occurred. The RedirectAttributes is used to forward the response to a success page. With the addFlashAttribute() method, the attributes will survive a redirect.

package com.memorynotfound.controller;

import com.memorynotfound.model.FileBucket;
import com.memorynotfound.validation.FileValidator;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.validation.Valid;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

@Controller
@RequestMapping("/single")
public class SingleFileUploadController {

    @Autowired
    private FileValidator fileValidator;

    @InitBinder
    protected void initBinderFileBucket(WebDataBinder binder) {
        binder.setValidator(fileValidator);
    }

    @RequestMapping(method = RequestMethod.POST)
    public String handleFormUpload(@Valid FileBucket bucket,
                                   BindingResult result,
                                   RedirectAttributes redirectMap) throws IOException {

        if (result.hasErrors()){
            return "single";
        }

        MultipartFile file = bucket.getFile();
        InputStream in = file.getInputStream();
        File destination = new File("/some-location/" + file.getOriginalFilename());
        FileUtils.copyInputStreamToFile(in, destination);

        redirectMap.addFlashAttribute("filename", file.getOriginalFilename());
        return "redirect:success";
    }
}

This validator is used to validate if the uploaded file is not empty. If the file is empty, we reject the value and provide an appropriate error message.

package com.memorynotfound.validator;

import com.memorynotfound.model.FileBucket;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class FileValidator implements Validator {

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

    @Override
    public void validate(Object target, Errors errors) {
        FileBucket bucket = (FileBucket) target;

        if (bucket.getFile() != null && bucket.getFile().isEmpty()){
            errors.rejectValue("file", "file.empty");
        }
    }
}

To illustrate the Spring MVC File Upload example, we created a simple view to upload a single file. When the form is submitted, the method of the SingleFileUploadController is executed on an HTTP POST method.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>Spring MVC Upload Single File</title>

    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

    <h1>Spring MVC Upload Single File</h1>
    <form:form method="post" modelAttribute="fileBucket" enctype="multipart/form-data">

        Upload File: <form:input type="file" path="file" id="file"/>
        <br/>

        <form:errors path="file" cssClass="error"/>
        <br/><br/>

        <input type="submit" value="Upload">

    </form:form>

    <a href="<c:url value='/'/>">Home</a>

</body>
</html>

Spring MVC Multi File Upload

The multiFileBucket is used to upload multiple files. We reuse the previous FileBucket. The constructor takes an integer value, this value is used to create the number of upload elements.

package com.memorynotfound.model;

import java.util.ArrayList;
import java.util.List;

public class MultiFileBucket {

    List<FileBucket> files = new ArrayList<FileBucket>();

    public MultiFileBucket() {
    }

    public MultiFileBucket(int count){
        for (int i = 0; i < count; i++){
            files.add(new FileBucket());
        }
    }

    public List<FileBucket> getFiles() {
        return files;
    }

    public void setFiles(List<FileBucket> files) {
        this.files = files;
    }
}

The MultiFileUploadController handles multiple file uploads. The only difference with the previous controller is that it uses a different Validator e.g. the MultiFileValidator, which we’ll create later on. We also use the new MultiFileBucket which holds multiple files.

package com.memorynotfound.controller;

import com.memorynotfound.model.FileBucket;
import com.memorynotfound.model.MultiFileBucket;
import com.memorynotfound.validation.MultipleFileValidator;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.validation.Valid;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

@Controller
@RequestMapping("/multiple")
public class MultiFileUploadController {

    @Autowired
    private MultipleFileValidator multipleFileValidator;

    @ModelAttribute
    public MultiFileBucket multiFileBucket(){
        return new MultiFileBucket(3);
    }

    @InitBinder
    protected void initBinderFileBucket(WebDataBinder binder) {
        binder.setValidator(multipleFileValidator);
    }

    @RequestMapping(method = RequestMethod.POST)
    public String handleFormUpload(@Valid MultiFileBucket buckets,
                                   BindingResult result,
                                   RedirectAttributes redirectMap) throws IOException {

        if (result.hasErrors()){
            return "multiple";
        }

        String[] files = new String[buckets.getFiles().size()];
        int index = 0;
        for (FileBucket bucket : buckets.getFiles()){
            MultipartFile file = bucket.getFile();
            InputStream in = file.getInputStream();
            File destination = new File("/some-location/" + file.getOriginalFilename());
            FileUtils.copyInputStreamToFile(in, destination);

            files[index] = file.getOriginalFilename();
            index++;
        }

        redirectMap.addFlashAttribute("filenames", files);

        return "redirect:success";
    }

}

This validator validates multiple files. If any of the submitted files are empty, it’ll reject the request and return appropriate error messages for all the files.

package com.memorynotfound.validator;

import com.memorynotfound.model.FileBucket;
import com.memorynotfound.model.MultiFileBucket;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class MultipleFileValidator implements Validator {

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

    @Override
    public void validate(Object target, Errors errors) {
        MultiFileBucket multiBucket = (MultiFileBucket) target;

        int index = 0;
        for (FileBucket bucket : multiBucket.getFiles()){
            if (bucket.getFile() != null && bucket.getFile().isEmpty()){
                errors.rejectValue("files[" + index + "].file", "file.empty");
            }
            index++;
        }
    }
}

To illustrate the Spring MVC File Upload for multiple files, we created the following view. The amount of files is managed by the MultiFileBucket we initialized in the NavigationController.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spring MVC Upload Multiple Files</title>

    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

    <h1>Spring MVC Upload Multiple Files</h1>
    <form:form method="post" modelAttribute="multiFileBucket" enctype="multipart/form-data">

        <c:forEach var="f" varStatus="fi" items="${multiFileBucket.files}">
            <c:set var="file" value="files[${fi.index}].file"/>
            Upload file: <form:input type="file" path="${file}" id="${file}"/>
            <form:errors path="${file}" cssClass="error"/>
            <br/>
        </c:forEach>
        <br/>

        <input type="submit" value="upload"/>

    </form:form>

    <a href="<c:url value='/'/>">Home</a>

</body>
</html>

Spring MVC File Upload Exception Handling

The servlet container can throw multiple instances of the MultipartException. This exception is thrown before it reaches the controller, so we need to add a global exception handler. We register this exception handler by annotating the class with the @ControllerAdvice annotation. To handle specific exceptions we annotate the methods using the @ExceptionHandler annotation and provide the exception type that the method must handle. In the method, we inspect the thrown exception and provide an appropriate message to the user.

package com.memorynotfound.exception;

import org.apache.commons.fileupload.FileUploadBase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@ControllerAdvice
public class GlobalExceptionHandler {

    @Autowired
    private MessageSource messageSource;

    @ExceptionHandler(value = MultipartException.class)
    public RedirectView handleMultipartException(Exception ex, HttpServletRequest request){
        RedirectView model = new RedirectView("error");
        FlashMap flash = RequestContextUtils.getOutputFlashMap(request);
        if (ex instanceof MultipartException) {
            MultipartException mEx = (MultipartException)ex;

            if (ex.getCause() instanceof FileUploadBase.FileSizeLimitExceededException){
                FileUploadBase.FileSizeLimitExceededException flEx = (FileUploadBase.FileSizeLimitExceededException)mEx.getCause();
                float permittedSize = flEx.getPermittedSize() / 1024;
                String message = messageSource.getMessage(
                        "file.maxsize",
                        new Object[]{flEx.getFileName(), permittedSize},
                        LocaleContextHolder.getLocale());
                flash.put("errors", message);
            } else {
                flash.put("errors", "Please contact your administrator: " + ex.getMessage());
            }
        } else {
            flash.put("errors", "Please contact your administrator: " + ex.getMessage());
        }
        return model;
    }

    @ExceptionHandler(value = IOException.class)
    public RedirectView handleIOException(Exception ex, HttpServletRequest request){
        RedirectView model = new RedirectView("error");
        FlashMap flash = RequestContextUtils.getOutputFlashMap(request);
        flash.put("errors", "Please contact your administrator: " + ex.getMessage());
        return model;
    }
}

Success View

When the file is successfully uploaded, we display the name of the file.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spring MVC File Upload Success</title>
</head>
<body>

    <h1>Spring MVC File Upload Success</h1>
    <c:if test="${not empty filename}">
        ${filename} uploaded successfully.
        <br/><br/>
    </c:if>

    <c:if test="${not empty filenames}">
        <c:forEach var="file" items="${filenames}">
            ${file} uploaded successfully.
            <br/>
        </c:forEach>
        <br/><br/>
    </c:if>

    <a href="<c:url value='/'/>">Home</a>

</body>
</html>

Error View

If an error occurs, we display the content of the error.


<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spring MVC File Upload Error</title>

    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

    <h1>Spring MVC File Upload Error</h1>
    <span class="error">${errors}</span>
    <br/><br/>

    <a href="<c:url value='/'/>">Home</a>

</body>
</html>

Demo

Single file upload.

spring mvc file upload example singlespring mvc file upload example single no file selected

Multiple file uploads.

spring mvc file upload example multiplespring mvc file upload example multiple no files selectd

Error page.

spring mvc file upload example error file size exceedsspring mvc file upload example error io exception

Success page.

spring mvc file upload example success

References

Download

You may also like...