Spring MVC File Upload
Introduction
File upload functionality is essential for many web applications - whether you're building a social media platform that allows users to share photos, a document management system, or simply a profile picture uploader. Spring MVC provides robust support for file uploads through its MultipartResolver implementation, making it easy to handle files submitted from HTML forms.
In this tutorial, you'll learn:
- How file uploads work in web applications
- Setting up Spring MVC for file uploads
- Creating file upload forms
- Processing uploaded files
- Implementing validation and error handling
- Best practices for file handling
How File Uploads Work
Before diving into the implementation, let's understand the basics of web-based file uploads:
- HTML forms must use the enctype="multipart/form-data"attribute for file uploads
- Form fields that accept files use <input type="file">
- When submitted, the files are sent as binary data in the HTTP request
- The server parses this multipart request to extract both regular form fields and file data
Spring MVC handles this process through its MultipartResolver interface, typically implemented by CommonsMultipartResolver or StandardServletMultipartResolver.
Setting Up Spring MVC for File Upload
Step 1: Add Dependencies
First, ensure you have the necessary dependencies in your project. If you're using Maven, add the following to your pom.xml:
<!-- For Spring MVC -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.23</version>
</dependency>
<!-- For file upload support -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>
Step 2: Configure MultipartResolver
Java Configuration (Recommended)
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSize(5242880); // 5MB
        resolver.setMaxUploadSizePerFile(1048576); // 1MB
        return resolver;
    }
}
XML Configuration (Alternative)
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!-- Maximum file size in bytes (5MB) -->
    <property name="maxUploadSize" value="5242880"/>
    <!-- Maximum size per file in bytes (1MB) -->
    <property name="maxUploadSizePerFile" value="1048576"/>
</bean>
Creating a File Upload Form
To create a form that allows users to upload files, use the following HTML:
<!DOCTYPE html>
<html>
<head>
    <title>File Upload Form</title>
</head>
<body>
    <h2>Upload a File</h2>
    
    <!-- Important: enctype must be "multipart/form-data" -->
    <form action="/upload" method="post" enctype="multipart/form-data">
        <div>
            <label for="name">Name:</label>
            <input type="text" id="name" name="name">
        </div>
        <div>
            <label for="file">Select a file:</label>
            <input type="file" id="file" name="file">
        </div>
        <button type="submit">Upload</button>
    </form>
</body>
</html>
If you're using Spring's form tags, the equivalent would be:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="post" action="/upload" enctype="multipart/form-data" modelAttribute="uploadForm">
    <div>
        <form:label path="name">Name:</form:label>
        <form:input path="name" />
    </div>
    <div>
        <form:label path="file">Select a file:</form:label>
        <form:input path="file" type="file" />
    </div>
    <button type="submit">Upload</button>
</form:form>
Processing Uploaded Files
Step 1: Create a Model Class
Create a simple model class to represent your form data:
public class FileUploadForm {
    private String name;
    private MultipartFile file;
    // Getters and setters
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public MultipartFile getFile() {
        return file;
    }
    public void setFile(MultipartFile file) {
        this.file = file;
    }
}
Step 2: Create a Controller
Now, create a controller to handle the file upload request:
@Controller
public class FileUploadController {
    
    // Display the upload form
    @GetMapping("/upload")
    public String showUploadForm(Model model) {
        model.addAttribute("uploadForm", new FileUploadForm());
        return "uploadForm";
    }
    
    // Process the uploaded file
    @PostMapping("/upload")
    public String handleFileUpload(@ModelAttribute("uploadForm") FileUploadForm form, 
                                  RedirectAttributes redirectAttributes) {
        
        MultipartFile file = form.getFile();
        
        if (file.isEmpty()) {
            redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
            return "redirect:/upload";
        }
        
        try {
            // Get the file name
            String filename = file.getOriginalFilename();
            
            // Save the file to a directory
            byte[] bytes = file.getBytes();
            Path path = Paths.get("uploads/" + filename);
            Files.write(path, bytes);
            
            redirectAttributes.addFlashAttribute("message", 
                "File uploaded successfully: " + filename);
            
        } catch (IOException e) {
            e.printStackTrace();
            redirectAttributes.addFlashAttribute("message", 
                "Failed to upload file: " + e.getMessage());
        }
        
        return "redirect:/upload";
    }
}
Implementing Validation and Error Handling
To ensure users upload valid files, you should add validation:
@PostMapping("/upload")
public String handleFileUpload(@ModelAttribute("uploadForm") FileUploadForm form, 
                              RedirectAttributes redirectAttributes) {
    
    MultipartFile file = form.getFile();
    
    // Check if file is empty
    if (file.isEmpty()) {
        redirectAttributes.addFlashAttribute("error", "Please select a file to upload");
        return "redirect:/upload";
    }
    
    // Check file size (max 2MB)
    if (file.getSize() > 2097152) {
        redirectAttributes.addFlashAttribute("error", "File size exceeds the limit (2MB)");
        return "redirect:/upload";
    }
    
    // Check file type (e.g., only allow images)
    String contentType = file.getContentType();
    if (contentType == null || !contentType.startsWith("image/")) {
        redirectAttributes.addFlashAttribute("error", "Only image files are allowed");
        return "redirect:/upload";
    }
    
    try {
        // Generate a unique filename to prevent overwriting
        String originalFilename = file.getOriginalFilename();
        String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
        String newFilename = UUID.randomUUID() + fileExtension;
        
        // Create directory if it doesn't exist
        File uploadDir = new File("uploads");
        if (!uploadDir.exists()) {
            uploadDir.mkdirs();
        }
        
        // Save the file
        Path path = Paths.get("uploads/" + newFilename);
        Files.write(path, file.getBytes());
        
        redirectAttributes.addFlashAttribute("message", 
            "File uploaded successfully: " + originalFilename);
        
    } catch (IOException e) {
        e.printStackTrace();
        redirectAttributes.addFlashAttribute("error", 
            "Failed to upload file: " + e.getMessage());
    }
    
    return "redirect:/upload";
}
Multiple File Uploads
Spring MVC also supports multiple file uploads. Here's how to implement it:
Update the Model Class
public class MultipleFileUploadForm {
    private String name;
    private List<MultipartFile> files;
    // Getters and setters
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<MultipartFile> getFiles() {
        return files;
    }
    public void setFiles(List<MultipartFile> files) {
        this.files = files;
    }
}
Update the HTML Form
<form action="/upload-multiple" method="post" enctype="multipart/form-data">
    <div>
        <label for="name">Name:</label>
        <input type="text" id="name" name="name">
    </div>
    <div>
        <label for="files">Select files:</label>
        <input type="file" id="files" name="files" multiple>
    </div>
    <button type="submit">Upload</button>
</form>
Create a Controller Method
@PostMapping("/upload-multiple")
public String handleMultipleFileUpload(@ModelAttribute MultipleFileUploadForm form, 
                                     RedirectAttributes redirectAttributes) {
    
    List<MultipartFile> files = form.getFiles();
    List<String> uploadedFiles = new ArrayList<>();
    
    if (files.isEmpty() || files.get(0).isEmpty()) {
        redirectAttributes.addFlashAttribute("error", "Please select at least one file");
        return "redirect:/upload-multiple";
    }
    
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            try {
                String filename = file.getOriginalFilename();
                byte[] bytes = file.getBytes();
                Path path = Paths.get("uploads/" + filename);
                Files.write(path, bytes);
                uploadedFiles.add(filename);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    redirectAttributes.addFlashAttribute("message", 
        "Successfully uploaded files: " + String.join(", ", uploadedFiles));
    
    return "redirect:/upload-multiple";
}
Real-World Example: Profile Picture Upload
Let's implement a practical example: a profile picture upload feature.
Model Classes
public class User {
    private Long id;
    private String username;
    private String profilePicture;
    
    // Getters and setters
}
public class ProfilePictureForm {
    private MultipartFile profileImage;
    
    // Getter and setter
    public MultipartFile getProfileImage() {
        return profileImage;
    }
    
    public void setProfileImage(MultipartFile profileImage) {
        this.profileImage = profileImage;
    }
}
Controller
@Controller
@RequestMapping("/profile")
public class ProfileController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/edit")
    public String showProfileForm(Model model, Principal principal) {
        User user = userService.findByUsername(principal.getName());
        model.addAttribute("user", user);
        model.addAttribute("profilePictureForm", new ProfilePictureForm());
        return "profile/edit";
    }
    
    @PostMapping("/update-picture")
    public String updateProfilePicture(
            @ModelAttribute ProfilePictureForm form,
            Principal principal,
            RedirectAttributes redirectAttributes) {
        
        MultipartFile file = form.getProfileImage();
        
        if (file.isEmpty()) {
            redirectAttributes.addFlashAttribute("error", "Please select an image");
            return "redirect:/profile/edit";
        }
        
        // Validate file type
        String contentType = file.getContentType();
        if (contentType == null || !contentType.startsWith("image/")) {
            redirectAttributes.addFlashAttribute("error", "Only image files are allowed");
            return "redirect:/profile/edit";
        }
        
        try {
            // Generate a unique filename
            String fileExtension = file.getOriginalFilename().substring(
                file.getOriginalFilename().lastIndexOf("."));
            String newFilename = UUID.randomUUID() + fileExtension;
            
            // Save the file to a public directory
            Path uploadPath = Paths.get("src/main/resources/static/uploads/profiles");
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }
            
            Path filePath = uploadPath.resolve(newFilename);
            Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
            
            // Update user profile picture in the database
            User user = userService.findByUsername(principal.getName());
            user.setProfilePicture("/uploads/profiles/" + newFilename);
            userService.save(user);
            
            redirectAttributes.addFlashAttribute("message", "Profile picture updated successfully");
            
        } catch (IOException e) {
            e.printStackTrace();
            redirectAttributes.addFlashAttribute("error", "Failed to upload profile picture");
        }
        
        return "redirect:/profile/edit";
    }
}
HTML Form (Thymeleaf)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Edit Profile</title>
</head>
<body>
    <h2>Edit Profile</h2>
    
    <div th:if="${message}" class="alert alert-success">
        <p th:text="${message}"></p>
    </div>
    
    <div th:if="${error}" class="alert alert-danger">
        <p th:text="${error}"></p>
    </div>
    
    <!-- Current profile picture -->
    <div class="profile-picture">
        <img th:if="${user.profilePicture}" th:src="${user.profilePicture}" 
             alt="Profile Picture" width="150">
        <img th:unless="${user.profilePicture}" src="/images/default-avatar.png" 
             alt="Default Avatar" width="150">
    </div>
    
    <!-- Profile picture upload form -->
    <form th:action="@{/profile/update-picture}" method="post" 
          enctype="multipart/form-data" th:object="${profilePictureForm}">
        <div>
            <label for="profileImage">New Profile Picture:</label>
            <input type="file" id="profileImage" name="profileImage" accept="image/*">
        </div>
        <button type="submit">Upload Picture</button>
    </form>
</body>
</html>
Best Practices for File Uploads
- Validate File Types: Always check the file's MIME type to ensure it matches what your application expects.
- Limit File Sizes: Set reasonable size limits to prevent server overload.
- Generate Unique Filenames: Avoid overwriting existing files by using UUIDs or timestamps.
- Store Files Outside the Web Root: For sensitive documents, store files where they can't be directly accessed via URL.
- Consider Using a Storage Service: For production applications, consider using Amazon S3, Azure Blob Storage, or similar services.
- Scan for Viruses: If possible, scan uploaded files for malware.
- Implement Progress Indicators: For large file uploads, provide feedback on upload progress.
- Clean Temporary Files: Implement a strategy to clean up temporary files that aren't needed.
Summary
In this tutorial, you've learned:
- How to set up Spring MVC for handling file uploads
- Creating forms that support file uploads using multipart/form-data
- Processing single and multiple file uploads
- Implementing validation and error handling
- A real-world example of profile picture uploads
- Best practices for file handling in web applications
File uploads are a common requirement in modern web applications, and Spring MVC provides a robust and flexible system for handling them. By following the patterns shown in this tutorial, you can implement secure and user-friendly file upload functionality in your Spring applications.
Additional Resources
- Spring Documentation on Multipart Resolver
- Commons FileUpload Documentation
- OWASP File Upload Security Guidelines
Exercises
- Modify the profile picture upload example to also allow users to crop their images before saving.
- Create a document management system that allows users to upload, download, and delete files.
- Implement progress tracking for large file uploads using AJAX and a progress listener.
- Add server-side validation to ensure that image dimensions are within specified limits.
- Implement a bulk file upload feature that shows a preview of each image before submission.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!