Uploading File with Spring Boot

In this article we will look at uploading file with Spring Boot. Uploading and downloading files are a common task for any web application. In this article we will learn the process to upload and download files using Spring Boot uploading a file in Spring MVC. We will see how Spring Boot can help us speed up the process.

 

Introduction

If you are working in web development, chances are that you may have already worked on file upload or download feature in your application. Spring Boot provides a pluggable architecture for the file upload feature.MultipartResolver from the org.springframework.web.multipart package is a strategy for parsing multi-part requests, including file uploads. There is one implementation based on Commons FileUpload and another based on Servlet 3.0 multi-part request parsing. We will build our application in 2 parts.

  1. A web application to upload and download file using a web interface (Will use Thymeleaf to build UI). 
  2. REST APIs for uploading and downloading files.

 

1. Setting up Application.

Let’s start by creating the web application. We can use the IDE or Spring Initializr to bootstrap our application. We are using Spring Initializr for this post as it offer a fast way to pull the dependencies to build our application.

  • Go to https://start.spring.io/.
  • Select the web and Thymeleaf as dependencies.
  • Fill information for the group and artifact and click on the “Generate” button.

Spring Boot File Upload

Unzip and import the maven project in your IDE. If you like to use the Spring Boot CLI to generate the project structure, run the following command from the terminal.

$ spring init --name spring-boot-file-upload --dependencies=web,thymeleaf spring-boot-file-upload
Using service at https://start.spring.io
Project extracted to '/Users/spring-boot-file-upload'

Here is our pom.xml<code> file for the application:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.2.4.RELEASE</version>
      <relativePath />
      <!-- lookup parent from repository -->
   </parent>
   <groupId>com.javadevjournal</groupId>
   <artifactId>spring-boot-file-upload</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>spring-boot-file-upload</name>
   <description>Sample application to upload and download file using Spring Boot</description>
   <properties>
      <java.version>1.8</java.version>
   </properties>
   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
         <exclusions>
            <exclusion>
               <groupId>org.junit.vintage</groupId>
               <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
         </exclusions>
      </dependency>
   </dependencies>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>
</project>

 

2. Spring Boot Main Class

Spring Boot provides a flexible way to start our web application using the standard main method. For non boot application, we need to register a MultipartConfigElement but spring boot auto-configuration will do this automatically for us.

package com.javadevjournal;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootFileUploadApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootFileUploadApplication.class, args);
    }
}

 

3. File Uploading Configurations

Let’s fine tune the file upload properties for our application using application.properties file. Open src/main/resources/application.properties file and change the following properties as per your application need.

#Whether to enable support of multipart uploads.default is true
#spring.servlet.multipart.enabled =true

# All files uploaded through will be stored in this directory
file.upload-dir=/Users/javadevjournal/uploads

#Threshold after which files are written to disk.default is 0B
spring.servlet.multipart.file-size-threshold = 3KB

#Max file size.Default is 1MB
spring.servlet.multipart.max-file-size= 2MB

#Max request size.Default is 10MB
spring.servlet.multipart.max-request-size= 20MB

#Whether to resolve the multipart request lazily at the time of file or parameter access.Default is false
spring.servlet.multipart.resolve-lazily=true

In the first part, we will create a file upload application using Thymeleaf while the second half, we will build file upload and download using REST API.

 

4. File Upload/Download Controller

The first part of our application is to create a file upload controller. Our controller will have the functionalities to upload and download files as well store the file in temp location.

package com.javadevjournal.controller;

import com.javadevjournal.data.FileMetaData;
import com.javadevjournal.exception.FileStorageException;
import com.javadevjournal.service.FileStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.FileNotFoundException;

@Controller
public class FileUploadController extends PageController {

    @Autowired
    FileStorageService fileStorageService;

    /**
     * Controller to display the file upload form on the frontend.
     * @param model
     * @return
     */
    @GetMapping("/upload-file")
    public String uploadFile(final Model model) {
        return "uploadFile";
    }

    /**
     * POST method to accept the incoming file in the application.This method is designed to accept
     * only 1 file at a time.
     * @param file
     * @param redirectAttributes
     * @return succes page
     */
    @PostMapping("/upload-file")
    public String uploadFile(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes, Model model) {

        try {
            FileMetaData data = fileStorageService.store(file);
            data.setUrl(fileDownloadUrl(data.getFileName(), "/media/download/"));
            model.addAttribute("uploadedFile", data);

        } catch (FileStorageException e) {
            model.addAttribute("error", "Unable to store the file");
            return "uploadFile";
        }
        return "uploadFile";
    }

    /**
     * Controller to allow customer to download the file by passing the file name as the
     * request URL.
     * @param fileName
     * @param response
     * @return
     * @throws FileNotFoundException
     */
    @GetMapping("/media/download/{fileName:.+}")
    public ResponseEntity < Resource > downloadFIle(@PathVariable String fileName, final HttpServletResponse response) throws FileNotFoundException {
        FileMetaData fileData = fileStorageService.getFile(fileName);
        response.setContentType(fileData.getMime());
        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + fileName + "\"").body(fileData.getResource());
    }
}

I have added the comment to the controller for clarity. Our controller is using the FileStorageService for storing files in the file system (We will make some changes to store some data in the database in the second part of this article.). The FileStorageService service returning the FileMetaData as a response.

 

4.1. FileMetaData.

This data class contains the standard information for the file in the system. This is how our FileMetaData class look like:

package com.javadevjournal.data;

import org.springframework.core.io.Resource;

public class FileMetaData {

    private String fileName;
    private String url;
    private String mime;
    private long size;
    private Resource resource;

    public FileMetaData() {}

    public FileMetaData(String fileName, String url, String mime, long size) {
        this.fileName = fileName;
        this.url = url;
        this.mime = mime;
        this.size = size;
    }

   //getter and setter methods.
}

 

4.2. File Storage Service.

To store and retrieve the files from the file system, let’s create our FileStorageService. Our file storage service class will help us:

  1. Store the file in the file system and return the stored file metadata information.
  2. Process the stored file data for the download.
  3. Feature to delete the file.

We will keep this class short for the post, but you can extend it as per your requirement.

package com.javadevjournal.service;

import com.javadevjournal.data.FileMetaData;
import com.javadevjournal.exception.FileStorageException;
import com.javadevjournal.utils.UploadFileProperties;
import org.apache.tika.detect.Detector;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.AutoDetectParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;

@Service("fileStorageService")
public class DefaultFileStorageService implements FileStorageService {

    private static final Logger LOG = LoggerFactory.getLogger(DefaultFileStorageService.class);

    @Autowired
    UploadFileProperties uploadFileProperties;

    @Override
    public FileMetaData store(MultipartFile file) throws FileStorageException {

        //Normalize the path by suppressing sequences like "path/.." and inner simple dots.
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());
        try {
            // we can add additional file validation to discard invalid files
            Path uploadDir = getUploadDirLocation().resolve(fileName);

            //copy the file to the upload directory,it will replace any file with same name.
            Files.copy(file.getInputStream(), uploadDir, StandardCopyOption.REPLACE_EXISTING);

            FileMetaData fileData = new FileMetaData();
            fileData.setFileName(fileName);
            fileData.setSize(file.getSize());
            fileData.setMime(file.getContentType());
            return fileData;

        } catch (IOException e) {
            LOG.error("unable to cpy file to the target location {}", e);
            throw new FileStorageException("Unable to store file " + fileName);
        }
    }

    /**
     * Read all files and return as list of @FileMetaData
     * @return
     */
    @Override
    public List  getAllFiles() {
        return null;
    }

    /**
     * Method to return the file as @Resource for the download.It read the file from the file system
     * and return it as @Resource
     * @param fileName
     * @return FileMetaData
     * @throws FileNotFoundException
     */
    @Override
    public FileMetaData getFile(String fileName) throws FileNotFoundException {
        Path path = getUploadDirLocation().resolve(fileName).normalize();

        try {
            Resource resource = new UrlResource(path.toUri());

            if (resource.exists()) {
                Metadata metadata = getFileMetaDataInfo(resource);
                FileMetaData fileMetaData = new FileMetaData();
                fileMetaData.setResource(resource);
                fileMetaData.setFileName(fileName);
                fileMetaData.setSize(metadata.size());
                fileMetaData.setMime(metadata.get(Metadata.CONTENT_TYPE));
                return fileMetaData;
            } else {
                throw new FileNotFoundException("Not able to find file");
            }
        } catch (MalformedURLException e) {
            throw new FileNotFoundException("Not able to find file");
        }
    }

    /**
     * Provides the upload directory location based on the application.properties configurations
     *
     * @return Path
     */
    private Path getUploadDirLocation() {
        return Paths.get(uploadFileProperties.getUploadDir()).toAbsolutePath().normalize();
    }

    /**
     * Helper method to get the file meta-data using Apache Tikka corre library.JDK also provide
     * way to read meta-data information but it's very limited and have lot of issues.
     * @param resource
     * @return Metadata
     */
    private Metadata getFileMetaDataInfo(Resource resource) {
        AutoDetectParser parser = new AutoDetectParser();
        Detector detector = parser.getDetector();
        Metadata metadata = new Metadata();
        try {
            metadata.set(Metadata.RESOURCE_NAME_KEY, resource.getFile().getName());
            TikaInputStream stream = TikaInputStream.get(resource.getInputStream());
            MediaType mediaType = detector.detect(stream, metadata);
            metadata.set(Metadata.CONTENT_TYPE, mediaType.toString());
        } catch (IOException e) {
            e.printStackTrace();
            //fallback to default content type
            metadata.set(Metadata.CONTENT_TYPE, "application/octet-stream");

        }
        return metadata;
    }
}

We are using standard JDK feature to write the data on the file system. For file meta-data; I am using Apache Tikka core library as JDK built-in features are very limited and contains a lot of issues. To use Tikka core, add following dependency in your pom.xml file:

<dependency>
   <groupId>org.apache.tika</groupId>
   <artifactId>tika-core</artifactId>
   <version>1.23</version>
</dependency>

 

5. Excepton Classes.

We are using the mix of custom and JDK exception classes to handle exceptional scenario:

  • FileStorageException – Exception thrown by API during file storage operation.
  • FileNotFoundException – In case the requested file is not available in the file system.

 

5.1. FileStorageException

package com.javadevjournal.exception;

public class FileStorageException extends Exception {

    public FileStorageException() {
        super();
    }

    public FileStorageException(String message) {
        super(message);
    }

    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}

 

6. HTML For File upload.

Let’s build a simple HTML to allow end user to upload and download the files. Create an HTML file uploadFile.html in the src/main/resources/templates directory.

<html xmlns:th="http://www.thymeleaf.org">
   <body>
      <div th:if="${uploadedFile}">
         <h2> Uploaded File Details </h2>
         <table>
            <tr>
               <td th:text="${uploadedFile.fileName}">File Name</td>
               <td th:text="${uploadedFile.mime}">File Type:</td>
               <td th:text="${uploadedFile.url}">URL :</td>
            </tr>
         </table>
      </div>
      <div>
         <h2> Uploaded New File </h2>
         <form method="POST" enctype="multipart/form-data" action="/upload-file">
            <table>
               <tr>
                  <td>File to upload 1:</td>
                  <td><input type="file" name="file" /></td>
               </tr>
               <tr>
                  <td></td>
                  <td><input type="submit" value="Upload" /></td>
               </tr>
            </table>
         </form>
      </div>
   </body>
</html>

 

6.1. Uploading Multiple Files.

In case you face a requirement where the customer should be able to upload multiple files, everything remain the same except for two changes to the above example.

  1. We put multiple input fields inside the form.
  2. Change the MultipartFile file to MultipartFile[] files.

Let’s see these code changes:

@PostMapping(value = "/upload-files")
   public String uploadFiles(@RequestParam("files") MultipartFile[] files, Model model) {
    //Logic to store data in temp file or in DB
    model.addAttribute("uploadedFile", files);
    return "uploadForm";
}
<html xmlns:th="http://www.thymeleaf.org">
<body>

<div th:if="${uploadedFile}">
    <h2> Uploaded File Details </h2>
    <table>
        <tr th:each="file: ${uploadedFile}">
            <td th:text="${file.originalFilename}">File Name</td>
            <td th:text="${file.contentType}">File Type:</td>
        </tr>
    </table>
</div>
<div>
    <h2> Uploaded New File </h2>
    <form method="POST" enctype="multipart/form-data" action="/upload-files">
        <table>
            <tr><td>File to upload 1:</td><td><input type="file" name="files" /></td></tr>
            <tr><td>File to upload 2:</td><td><input type="file" name="files" /></td></tr>
            <tr><td></td><td><input type="submit" value="Upload" /></td></tr>
        </table>
    </form>
</div>
</body>
</html>

 

7. Testing Application

We completed the first part of our post, let’s build and run our application.To start the application from command line, use mvn spring-boot:run command.Once the application is running, open the http://localhost:8080/upload-file on the browser:

file-upload-Spring Boot

Click on the upload button, our service class will upload the file and return the file meta-data in the response:

upload-file-response-Spring Boot

Click on the URL to download the file:

 

8. Spring REST API to upload and Download Files

We have already completed the core work for our application, let’s create a REST controller for uploading file with Spring Boot.We will use Postman to test our application:

package com.javadevjournal.controller;

import com.javadevjournal.data.FileMetaData;
import com.javadevjournal.exception.FileStorageException;
import com.javadevjournal.service.FileStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.io.FileNotFoundException;

@RestController
@RequestMapping("/api/v1")
public class RestFileUploadController extends PageController {

    @Autowired
    FileStorageService fileStorageService;

    /**
     * REST controller to allow file uploading for our REST API
     * @param file
     * @param redirectAttributes
     * @param model
     * @return FileMetaData
     * @throws FileStorageException
     */
    @PostMapping("/upload-file")
    @ResponseBody
    public FileMetaData uploadFile(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes, Model model) throws FileStorageException {
        FileMetaData data = fileStorageService.store(file);
        data.setUrl(fileDownloadUrl(data.getFileName(), "/api/v1/media/download/"));
        return data;
    }

    /**
     * Rest Controller method for file download feature.
     * @param fileName
     * @return ResponseEntity
     * @throws FileNotFoundException
     */
    @GetMapping("/media/download/{fileName:.+}")
    public ResponseEntity < Resource > downloadFile(@PathVariable String fileName) throws FileNotFoundException {
        FileMetaData fileData = fileStorageService.getFile(fileName);
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + fileName + "\"")
            .contentType(MediaType.parseMediaType(fileData.getMime()))
            .body(fileData.getResource());
    }
}

Our core file service will remain the same. Start the application and open the Postman or any other REST client.

Upload File:

Spring Boot FIle Upload - REST-API

 

Download File:

Click on the URL to download the file:

File Upload Spring Boot - REST API response

 

9. Spring MVC File Upload

Most of the above concept will remain the same, even if you are not using the Spring Boot in your application. In this section, I am going to highlight the main differences or additional changes required for Spring MVC file upload.

 

9.1. Maven Dependencies.

We need to add the Apache Commons FileUpload dependency in our pom.xml file

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.3</version>
</dependency>

 

9.2. CommonsMultipartResolver Bean

The second step of our configuration involves defining CommonsMultipartResolver bean for our application. This bean is useful to customize the file upload behavior for our application.

@Bean
 public CommonsMultipartResolver multipartResolver() {
  CommonsMultipartResolver resolver = new CommonsMultipartResolver();
  resolver.setDefaultEncoding("utf-8");
  resolver.setResolveLazily(false);
  resolver.setMaxUploadSize(200000);
  return resolver;
 }

 

10. File Upload and Servlet 3.0

In case you like to use Servlet 3.0 feature, you need to configure few items before using it. Let’s see those details:

  1. Set a MultipartConfigElement on the Servlet registration.
public class WebbAppInit implements WebApplicationInitializer {

 private int MAX_UPLOAD_SIZE = 2 * 1024 * 1024;

 @Override
 public void onStartup(ServletContext servletContext) throws ServletException {

  AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
  rootContext.register(SpringMvcFileUploadApplication.class);

  // Manage the lifecycle of the root application context
  servletContext.addListener(new ContextLoaderListener(rootContext));

  // Register and map the dispatcher servlet
  ServletRegistration.Dynamic dispatcher =
   servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
  dispatcher.setLoadOnStartup(1);
  dispatcher.addMapping("/");
  MultipartConfigElement multipartConfigElement = new MultipartConfigElement("/temp",
   MAX_UPLOAD_SIZE, MAX_UPLOAD_SIZE * 2, 0);
 }
}

Read Spring WebApplicationInitializer for more details.

 

Summary

In this article, we learn the basic of uploading file with Spring Boot. We also saw the steps to upload a file in Spring MVC. We covered how you can use Spring Boot autoconfiguration to speed up the development of the file uploading features. We can find the Source code for this article on the GitHub.

Comments are closed.

Scroll to Top