Thymeleaf File Upload with Spring Boot

In this tutorial, I will show you how to implement file upload with Thymeleaf and Bootstrap in a Spring Boot project. We also use Spring Web MultipartFile interface to handle HTTP multi-part requests.

Related Posts:
Spring Boot upload Image and Display with Thymeleaf
Spring Boot Thymeleaf CRUD example
Spring Boot Thymeleaf Pagination and Sorting example
Spring Boot Thymeleaf Multiple File upload
Spring Boot @ControllerAdvice & @ExceptionHandler example
@RestControllerAdvice example in Spring Boot
Spring Boot Unit Test for JPA Repositiory
Spring Boot Unit Test for Rest Controller
Deploy Spring Boot App on AWS – Elastic Beanstalk

Fullstack:
Angular + Spring Boot: File upload example
React + Spring Boot: File upload example

Associations:
Spring Boot One To One example with JPA, Hibernate
Spring Boot One To Many example with JPA, Hibernate
Spring Boot Many to Many example with JPA, Hibernate

Thymeleaf File Upload Overview

Our Spring Boot + Thymeleaf File Upload example will have following features:

  • uploading File to a static folder in the Server
  • downloading File from server with the link
  • getting list of Files’ information (file name & url)

– Here is the File Upload Form:

thymeleaf-file-upload-example

– If the File exceeds specific maximum size:

thymeleaf-file-upload-exception

– This is the static folder that stores all uploaded files:

thymeleaf-file-upload-folder

– You can see list of uploaded files with download links:

thymeleaf-file-upload-spring-boot

In this tutorial, I don’t explain way to delete File. If you want to know that, just visit:
Spring Boot Delete File example with Thymeleaf

Or adding Pagination with following tutorial:
Spring Boot Thymeleaf Pagination example

Technology

  • Java 8
  • Spring Boot 2.7 (with Spring Web MVC, Thymeleaf)
  • Maven 3.6.1
  • Bootstrap 4
  • jQuery 3.6.1

Project Structure

thymeleaf-file-upload-spring-boot-project

Let me explain it briefly.

FileInfo contains information of the uploaded file.
FilesStorageService helps us to initialize storage, save new file, load file, get list of Files’ info, delete files.
FileController uses FilesStorageService to handle file upload/download and template requests.
FileUploadExceptionAdvice handles exception when the controller processes file upload.
template stores HTML template files for the project.
application.properties contains configuration for Servlet Multipart.
uploads is the static folder for storing files.
pom.xml for Spring Boot dependency.

Create & Setup Spring Boot project

Use Spring web tool or your development tool (Spring Tool Suite, Eclipse, Intellij) to create a Spring Boot project.

Then open pom.xml and add dependencies for Spring Web, Thymeleaf, Bootstrap, Jquery:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>4.6.2</version>
</dependency>

<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>3.6.1</version>
</dependency>

<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator-core</artifactId>
</dependency>

Create Service for File Storage

First we need an interface that will be autowired in the Controller.
In service folder, create FilesStorageService interface like following code:

service/FilesStorageService.java

package com.bezkoder.spring.thymeleaf.file.upload.service;

import java.nio.file.Path;
import java.util.stream.Stream;

import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;

public interface FilesStorageService {
  public void init();

  public void save(MultipartFile file);

  public Resource load(String filename);
  
  public void deleteAll();

  public Stream<Path> loadAll();
}

Now we create implementation of the interface.

service/FilesStorageServiceImpl.java

package com.bezkoder.spring.thymeleaf.file.upload.service;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;

@Service
public class FilesStorageServiceImpl implements FilesStorageService {
  private final Path root = Paths.get("./uploads");

  @Override
  public void init() {
    try {
      Files.createDirectories(root);
    } catch (IOException e) {
      throw new RuntimeException("Could not initialize folder for upload!");
    }
  }

  @Override
  public void save(MultipartFile file) {
    try {
      Files.copy(file.getInputStream(), this.root.resolve(file.getOriginalFilename()));
    } catch (Exception e) {
      if (e instanceof FileAlreadyExistsException) {
        throw new RuntimeException("A file of that name already exists.");
      }

      throw new RuntimeException(e.getMessage());
    }
  }

  @Override
  public Resource load(String filename) {
    try {
      Path file = root.resolve(filename);
      Resource resource = new UrlResource(file.toUri());

      if (resource.exists() || resource.isReadable()) {
        return resource;
      } else {
        throw new RuntimeException("Could not read the file!");
      }
    } catch (MalformedURLException e) {
      throw new RuntimeException("Error: " + e.getMessage());
    }
  }

  @Override
  public void deleteAll() {
    FileSystemUtils.deleteRecursively(root.toFile());
  }

  @Override
  public Stream<Path> loadAll() {
    try {
      return Files.walk(this.root, 1).filter(path -> !path.equals(this.root)).map(this.root::relativize);
    } catch (IOException e) {
      throw new RuntimeException("Could not load the files!");
    }
  }
}

Create Controller for File Upload

In controller package, we create FileController.

controller/FileController.java

package com.bezkoder.spring.thymeleaf.file.upload.controller;
// ...
import com.bezkoder.spring.thymeleaf.file.upload.service.FilesStorageService;

@Controller
public class FileController {

  @Autowired
  FilesStorageService storageService;

  @GetMapping("/")
  public String homepage() {
    return "redirect:/files";
  }

  @GetMapping("/files/new")
  public String newFile(Model model) {
    return "upload_form";
  }

  @PostMapping("/files/upload")
  public String uploadFile(Model model, @RequestParam("file") MultipartFile file) {
    ...

    return "upload_form";
  }

  @GetMapping("/files")
  public String getListFiles(Model model) {
    ...

    return "files";
  }

  @GetMapping("/files/{filename:.+}")
  public ResponseEntity<Resource> getFile(@PathVariable String filename) {
    ...

    // return File
  }
}

@Controller annotation is used to define a controller.
@GetMapping and @PostMapping annotation is for mapping HTTP GET & POST requests onto specific handler methods and returning appropriate template files.
– We use @Autowired to inject implementation of FilesStorageService bean to local variable.

Setup the Template

In src/main/resources folder, create folder and file as following structure:

thymeleaf-file-upload-example-template

Header and Footer

We will use Thymeleaf Fragments (th:fragment) to reuse some common parts such as header and footer.
Let’s write HTML code for them.

fragments/footer.html

<footer class="text-center">
  Copyright © BezKoder
</footer>

And header that contains the navigation bar:
fragments/header.html

<header th:fragment="header">
  <nav class="navbar navbar-expand-md bg-dark navbar-dark mb-3">
    <a class="navbar-brand" th:href="@{/files}">
      BezKoder
    </a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="topNavbar">
      <ul class="navbar-nav">
        <li class="nav-item">
          <a class="nav-link" th:href="@{/files/new}">Upload</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" th:href="@{/files}">Files</a>
        </li>
      </ul>
    </div>
  </nav>
</header>

Now we need to create HTML files, then import Thymeleaf fragments, Bootstrap, jQuery and Font Awesome.

File Upload Form

upload_form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0" />
  <title>BezKoder - Thymeleaf File Upload example</title>
  <link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" />
  <script type="text/javascript" th:src="@{/webjars/jquery/jquery.min.js}"></script>
  <script type="text/javascript" th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
</head>

<body>
  <div th:replace="fragments/header :: header"></div>

  -- file upload form --

  <div th:replace="fragments/footer :: footer"></div>
</body>

</html>

List of Files

files.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0" />
  <title>BezKoder - Thymeleaf File Upload example</title>

  <link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
    integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
    crossorigin="anonymous" referrerpolicy="no-referrer" />
  <script type="text/javascript" th:src="@{/webjars/jquery/jquery.min.js}"></script>
  <script type="text/javascript" th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
</head>

<body>
  <div th:replace="fragments/header :: header"></div>

  -- list of files display --
  
  <div th:replace="fragments/footer :: footer"></div>
</body>

</html>

Implement File Upload function

We use @GetMapping and @PostMapping annotation is for mapping HTTP GET & POST requests onto specific handler methods:

  • GET /files/new: newFile() – return upload_form.html template
  • POST /files/upload: uploadFile() – upload a File

FileController.java

import com.bezkoder.spring.thymeleaf.file.upload.service.FilesStorageService;

@Controller
public class FileController {

  @Autowired
  FilesStorageService storageService;

  @GetMapping("/files/new")
  public String newFile(Model model) {
    return "upload_form";
  }

  @PostMapping("/files/upload")
  public String uploadFile(Model model, @RequestParam("file") MultipartFile file) {
    String message = "";

    try {
      storageService.save(file);

      message = "Uploaded the file successfully: " + file.getOriginalFilename();
      model.addAttribute("message", message);
    } catch (Exception e) {
      message = "Could not upload the file: " + file.getOriginalFilename() + ". Error: " + e.getMessage();
      model.addAttribute("message", message);
    }

    return "upload_form";
  }
}

upload_form.html

<div class="container" style="max-width: 500px">
  <h3 class="mb-3">Thymeleaf File Upload example</h3>

  <form
    id="uploadForm"
    method="post"
    th:action="@{/files/upload}"
    enctype="multipart/form-data">
    <input id="input-file" type="file" name="file" />
    <button class="btn btn-sm btn-outline-success float-right" type="submit">
      Upload
    </button>
  </form>

  <div
    th:if="${message != null}"
    class="alert alert-secondary alert-dismissible fade show text-center message mt-3"
    role="alert">
    [[${message}]]
    <button type="button" class="close btn-sm" data-dismiss="alert" aria-label="Close">
      <span aria-hidden="true">×</span>
    </button>
  </div>
</div>

Display List of Files

Firstly, we need to create FileInfo model which has fields: name & url.

model/FileInfo.java

package com.bezkoder.spring.thymeleaf.file.upload.model;

public class FileInfo {
  
  private String name;
  private String url;

  public FileInfo(String name, String url) {
    this.name = name;
    this.url = url;
  }

  public String getName() {
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getUrl() {
    return this.url;
  }

  public void setUrl(String url) {
    this.url = url;
  }
}

In the Controller, we will return List of FileInfo objects as model Attribute.
@GetMapping and @PostMapping annotation is for mapping HTTP GET & POST requests onto specific handler methods:

  • GET /files: getListFiles() – return files.html template
  • GET /files/[filename]: getFile() – download a File by filename

FileController.java

import com.bezkoder.spring.thymeleaf.file.upload.model.FileInfo;
import com.bezkoder.spring.thymeleaf.file.upload.service.FilesStorageService;

@Controller
public class FileController {

  @Autowired
  FilesStorageService storageService;

  // ...

  @GetMapping("/")
  public String homepage() {
    return "redirect:/files";
  }

  @GetMapping("/files")
  public String getListFiles(Model model) {
    List<FileInfo> fileInfos = storageService.loadAll().map(path -> {
      String filename = path.getFileName().toString();
      String url = MvcUriComponentsBuilder
          .fromMethodName(FileController.class, "getFile", path.getFileName().toString()).build().toString();

      return new FileInfo(filename, url);
    }).collect(Collectors.toList());

    model.addAttribute("files", fileInfos);

    return "files";
  }

  @GetMapping("/files/{filename:.+}")
  public ResponseEntity<Resource> getFile(@PathVariable String filename) {
    Resource file = storageService.load(filename);

    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"").body(file);
  }
}

files.html

<div class="container-fluid" style="max-width: 600px; margin: 0 auto;">
  <h2 class="text-center">List of Files</h2>

  <div th:if="${files.size() > 0}">
    <table class="table table-hover">
      <thead class="thead-light">
        <tr>
          <th scope="col">File Name</th>
          <th scope="col">Link</th>
          <th scope="col">Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr th:each="file : ${files}">
          <td>[[${file.name}]]</td>
          <td><a th:href="@{${file.url}}">Download</a></td>
          <td>
            <a th:href="@{'/files/delete/' + ${file.name}}" th:fileName="${file.name}" id="btnDelete"
              title="Delete this file" class="fa-regular fa-trash-can icon-dark btn-delete"></a>
          </td>
        </tr>
      </tbody>
    </table>
  </div>

  <div th:unless="${files.size() > 0}">
    <span>No files found!</span>
  </div>
</div>

For deleting file, kindly visit following tutorial:
Spring Boot Delete File example with Thymeleaf

Configure Multipart File for Servlet

Let’s define the maximum file size that can be uploaded in application.properties as following:

spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=1MB

spring.servlet.multipart.max-file-size: max file size for each request.
spring.servlet.multipart.max-request-size: max request size for a multipart/form-data.

Handle File Upload Exception

This is where we handle the case in that a request exceeds Max Upload Size. The system will throw MaxUploadSizeExceededException and we’re gonna use @ControllerAdvice with @ExceptionHandlerannotation for handling the exceptions.

exception/FileUploadExceptionAdvice.java

package com.bezkoder.spring.thymeleaf.file.upload.exception;

import org.springframework.web.multipart.MaxUploadSizeExceededException;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class FileUploadExceptionAdvice {

  @ExceptionHandler(MaxUploadSizeExceededException.class)
  public String handleMaxSizeException(Model model, MaxUploadSizeExceededException e) {
    model.addAttribute("message", "File is too large!");

    return "upload_form";
  }
}

Initialize Storage

We need to run init() method of FilesStorageService (and also deleteAll() if necessary). So open ThymeleafFileUploadApplication.java and implement CommandLineRunner for run() method like this:

package com.bezkoder.spring.thymeleaf.file.upload;

import javax.annotation.Resource;

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

import com.bezkoder.spring.thymeleaf.file.upload.service.FilesStorageService;

@SpringBootApplication
public class ThymeleafFileUploadApplication implements CommandLineRunner {

  @Resource
  FilesStorageService storageService;

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

  @Override
  public void run(String... arg) throws Exception {
//    storageService.deleteAll();
    storageService.init();
  }
}

Run the Spring Boot File Upload example

Run Spring Boot application with command: mvn spring-boot:run.

Source Code

You can find the complete source code for this tutorial on Github.

For deleting file:
Spring Boot Delete File example with Thymeleaf

Conclusion

Today we’ve learned how to create Spring Boot Thymeleaf File Upload Application with multipart files and get files’ information with static folder.

For upload multiple Files at once:
Spring Boot Multiple File upload with Thymeleaf

Or fullstack with frontend:
Angular + Spring Boot: File upload example
React + Spring Boot: File upload example

You can also know way to upload an Excel/CSV file and store the content in MySQL database with the post:
Spring Boot: Upload/Import Excel file data into MySQL Database
Spring Boot: Upload/Import CSV file data into MySQL Database

If you want to store files in database like this:
spring-boot-upload-files-to-database-table-files

You can find instruction at:
Spring Boot Upload/Download File to/from Database example

Happy Learning! See you again.

Further Reading

Spring Boot upload Image and Display with Thymeleaf
Spring Boot upload multiple files Rest API
Spring Boot Thymeleaf CRUD example
Spring Boot Thymeleaf Pagination and Sorting example

Exception Handling:
Spring Boot @ControllerAdvice & @ExceptionHandler example
@RestControllerAdvice example in Spring Boot

Unit Test:
Spring Boot Unit Test for JPA Repositiory
Spring Boot Unit Test for Rest Controller

Deployment:
Deploy Spring Boot App on AWS – Elastic Beanstalk

Associations:
Spring Boot One To One example with JPA, Hibernate
Spring Boot One To Many example with JPA, Hibernate
Spring Boot Many to Many example with JPA, Hibernate