Đầu tiên, chúng ta sẽ xây dựng dự án REST APIs cho việc uploading và downloading files, sau đó thực hiện test các APIs đó bằng Postman. Chúng ta sẽ viết javascript cho phần front-end để upload files.
1. Tạo ứng dụng
Tôi sẽ khởi tạo project bằng cách sử dụng công cụ web Spring Initializr, hãy làm theo hướng dẫn bên dưới:
Đầu tiên, hãy truy cập vào http://start.spring.io
- Nhập "file-demo" vào Artifact field.
- Add "Web" vào phần Dependencies.
- Click Generate Project để tải về project.
Bây giờ bạn có thể giải nén project đã tải xuống và import nó vào IDE yêu thích của bạn.
2. Cấu hình server và thông tin lưu trữ file
Đầu tiên, hãy cấu hình ứng dụng Spring Boot của chúng ta để enable Multipart file uploads, và xác định kích thước tối đa của file được upload lên. Chúng ta cũng sẽ cấu hình thư mục mà tất cả các tệp đã tải lên sẽ được lưu trữ.
Mở file src/main/resources/application.properties , và thêm các thông tin sau:
## MULTIPART (MultipartProperties)
# Enable multipart uploads
spring.servlet.multipart.enabled=true
# Threshold after which files are written to disk.
spring.servlet.multipart.file-size-threshold=2KB
# Max file size.
spring.servlet.multipart.max-file-size=200MB
# Max Request Size
spring.servlet.multipart.max-request-size=215MB
## File Storage Properties
# All files uploaded through the REST API will be stored in this directory
file.upload-dir=/Users/maiphuoctung/uploads
3. Tự động binding các thuộc tính với một class POJO
Spring Boot có một tính năng tuyệt vời được gọi là
@ConfigurationProperties , dùng để tự động liên kết các thuộc tính được xác định trong file application.properties với class POJO.
Hãy định nghĩa một class POJO được gọi là FileStorageProperties bên trong package com.mpt.filedemo.property để liên kết tất cả các thuộc tính lưu trữ file.
package com.mpt.filedemo.property;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "file")
public class FileStorageProperties {
private String uploadDir;
public String getUploadDir() {
return uploadDir;
}
public void setUploadDir(String uploadDir) {
this.uploadDir = uploadDir;
}
}
Annotation @ConfigurationProperties(prefix = "file") sẽ làm công việc của mình khi khởi động ứng dụng và liên kết với tất cả các thuộc tính với tiền tố là file đến các trường tương ứng class POJO.
Lưu ý: bạn sẽ gặp lỗi khi sử dụng annotation @ConfigurationProperties như: Not registered via @EnableConfigurationProperties or marked as Spring component. Đây là lỗi xảy ra khi ta chưa Enable Configuration Properties.
Bây giờ, hãy enable tính năng ConfigurationProperties , bạn cần thêm annotation
@EnableConfigurationProperties vào bất kỳ class cấu hình nào.
Mở class main src/main/java/com/mpt/filedemo/FileDemoApplication.java và thêm annotation @EnableConfigurationProperties cho nó
package com.mpt.filedemo;
import com.mpt.filedemo.property.FileStorageProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties({
FileStorageProperties.class
})
public class FileDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FileDemoApplication.class, args);
}
}
4. Viết APIs để upload và download file
Bây giờ hãy viết các REST APIs cho việc uploading và downloading files. Tạo mới 1 class controller với tên là FileController nằm bên trong package com.mpt.filedemo.controller
Đây là mã hoàn chỉnh cho FileController
package com.mpt.filedemo.controller;
import com.mpt.filedemo.payload.UploadFileResponse;
import com.mpt.filedemo.service.FileStorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@RestController
public class FileController {
private static final Logger logger = LoggerFactory.getLogger(FileController.class);
@Autowired
private FileStorageService fileStorageService;
@PostMapping("/uploadFile")
public UploadFileResponse uploadFile(@RequestParam("file") MultipartFile file) {
String fileName = fileStorageService.storeFile(file);
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/downloadFile/")
.path(fileName)
.toUriString();
return new UploadFileResponse(fileName, fileDownloadUri,
file.getContentType(), file.getSize());
}
@PostMapping("/uploadMultipleFiles")
public List<UploadFileResponse> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
return Arrays.asList(files)
.stream()
.map(file -> uploadFile(file))
.collect(Collectors.toList());
}
@GetMapping("/downloadFile/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {
// Load file as Resource
Resource resource = fileStorageService.loadFileAsResource(fileName);
// Try to determine file's content type
String contentType = null;
try {
contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
} catch (IOException ex) {
logger.info("Could not determine file type.");
}
// Fallback to the default content type if type could not be determined
if(contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
}
Class FileController có sử dụng class FileStorageService để xử lý lưu trữ các tập tin trong hệ thống và lấy ra chúng. Nó trả về một payload loại UploadFileResponse sau khi tải lên hoàn tất. Hãy xác định từng class một.
5. UploadFileResponse
Như tên cho thấy, class này được sử dụng để trả về phản hồi từ các API /uploadFile và /uploadMultipleFiles .
Tạo class UploadFileResponse bên trong package com.example.filedemo.payload với nội dung sau
package com.mpt.filedemo.payload;
public class UploadFileResponse {
private String fileName;
private String fileDownloadUri;
private String fileType;
private long size;
public UploadFileResponse(String fileName, String fileDownloadUri, String fileType, long size) {
this.fileName = fileName;
this.fileDownloadUri = fileDownloadUri;
this.fileType = fileType;
this.size = size;
}
// Getters and Setters (Omitted for brevity)
}
6. Service xử lý lưu trữ files trong FileSystem và lấy ra chúng.
Tạo một class với tên FileStorageService.java bên trong package com.mpt.filedemo.service và điền các nội dung sau
package com.mpt.filedemo.service;
import com.mpt.filedemo.exception.FileStorageException;
import com.mpt.filedemo.exception.MyFileNotFoundException;
import com.mpt.filedemo.property.FileStorageProperties;
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.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;
@Service
public class FileStorageService {
private final Path fileStorageLocation;
@Autowired
public FileStorageService(FileStorageProperties fileStorageProperties) {
this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir())
.toAbsolutePath().normalize();
try {
Files.createDirectories(this.fileStorageLocation);
} catch (Exception ex) {
throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex);
}
}
public String storeFile(MultipartFile file) {
// Normalize file name
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
try {
// Check if the file's name contains invalid characters
if(fileName.contains("..")) {
throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
}
// Copy file to the target location (Replacing existing file with the same name)
Path targetLocation = this.fileStorageLocation.resolve(fileName);
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
return fileName;
} catch (IOException ex) {
throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
}
}
public Resource loadFileAsResource(String fileName) {
try {
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if(resource.exists()) {
return resource;
} else {
throw new MyFileNotFoundException("File not found " + fileName);
}
} catch (MalformedURLException ex) {
throw new MyFileNotFoundException("File not found " + fileName, ex);
}
}
}
7. Exception Classes
Class FileStorageService sẽ throws ra một số exception trong trường hợp có tình huống ngoại lệ xảy ra. Sau đây là các định nghĩa của class ngoại lệ đó ( Tất cả các class ngoại lên nằm trong package com.mpt.filedemo.exception ).
7.1 FileStorageException
Nó bị ném ra khi có tình huống bất ngờ xả ra trong khi lưu trữ File trong FileSystem
package com.mpt.filedemo.exception;
public class FileStorageException extends RuntimeException {
public FileStorageException(String message) {
super(message);
}
public FileStorageException(String message, Throwable cause) {
super(message, cause);
}
}
7.2 MyFileNotFoundException
Nó bị ném ra khi không tìm thấy file mà người dùng đang cố tải xuống.
package com.mpt.filedemo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyFileNotFoundException extends RuntimeException {
public MyFileNotFoundException(String message) {
super(message);
}
public MyFileNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
Lưu ý rằng, tôi đã chú thích class ngoại lệ trên với annotation @ResponseStatus(HttpStatus.NOT_FOUND) . Điều này đảm bảo rằng Spring Boot sẽ phản hồi với trạng thái 404 Not Found khi ngoại lệ này được ném.
8. Chạy ứng dụng và kiểm tra các APIs thông qua Postman
Chúng ta đã hoàn thành việc phát triển các APIs. Hãy chạy ứng dụng và kiểm tra các APIs đấy thông qua Postman.
8.1. Upload File
8.2. Upload Multiple Files
8.3. Download File
9. Phát triển phần Front End
Phần APIs backend của chúng ta đang làm việc tốt. Bây giờ hãy viết code cho phần front end để cho users có thể upload và download filse từ ứng dụng web.
Tất cả các files source code front end sẽ nằm bên trong folder src/main/resources/static . Dưới đây là cấu trúc thư mục code cho phần front-end
static
└── css
└── main.css
└── js
└── main.js
└── index.html
9.1 HTML
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>Spring Boot File Upload / Download Rest API Example</title>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<noscript>
<h2>Sorry! Your browser doesn't support Javascript</h2>
</noscript>
<div class="upload-container">
<div class="upload-header">
<h2>Spring Boot File Upload / Download Rest API Example</h2>
</div>
<div class="upload-content">
<div class="single-upload">
<h3>Upload Single File</h3>
<form id="singleUploadForm" name="singleUploadForm">
<input id="singleFileUploadInput" type="file" name="file" class="file-input" required />
<button type="submit" class="primary submit-btn">Submit</button>
</form>
<div class="upload-response">
<div id="singleFileUploadError"></div>
<div id="singleFileUploadSuccess"></div>
</div>
</div>
<div class="multiple-upload">
<h3>Upload Multiple Files</h3>
<form id="multipleUploadForm" name="multipleUploadForm">
<input id="multipleFileUploadInput" type="file" name="files" class="file-input" multiple required />
<button type="submit" class="primary submit-btn">Submit</button>
</form>
<div class="upload-response">
<div id="multipleFileUploadError"></div>
<div id="multipleFileUploadSuccess"></div>
</div>
</div>
</div>
</div>
<script src="/js/main.js" ></script>
</body>
</html>
9.2 Javascript
'use strict';
var singleUploadForm = document.querySelector('#singleUploadForm');
var singleFileUploadInput = document.querySelector('#singleFileUploadInput');
var singleFileUploadError = document.querySelector('#singleFileUploadError');
var singleFileUploadSuccess = document.querySelector('#singleFileUploadSuccess');
var multipleUploadForm = document.querySelector('#multipleUploadForm');
var multipleFileUploadInput = document.querySelector('#multipleFileUploadInput');
var multipleFileUploadError = document.querySelector('#multipleFileUploadError');
var multipleFileUploadSuccess = document.querySelector('#multipleFileUploadSuccess');
function uploadSingleFile(file) {
var formData = new FormData();
formData.append("file", file);
var xhr = new XMLHttpRequest();
xhr.open("POST", "/uploadFile");
xhr.onload = function() {
console.log(xhr.responseText);
var response = JSON.parse(xhr.responseText);
if(xhr.status == 200) {
singleFileUploadError.style.display = "none";
singleFileUploadSuccess.innerHTML = "<p>File Uploaded Successfully.</p><p>DownloadUrl : <a href='" + response.fileDownloadUri + "' target='_blank'>" + response.fileDownloadUri + "</a></p>";
singleFileUploadSuccess.style.display = "block";
} else {
singleFileUploadSuccess.style.display = "none";
singleFileUploadError.innerHTML = (response && response.message) || "Some Error Occurred";
}
}
xhr.send(formData);
}
function uploadMultipleFiles(files) {
var formData = new FormData();
for(var index = 0; index < files.length; index++) {
formData.append("files", files[index]);
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "/uploadMultipleFiles");
xhr.onload = function() {
console.log(xhr.responseText);
var response = JSON.parse(xhr.responseText);
if(xhr.status == 200) {
multipleFileUploadError.style.display = "none";
var content = "<p>All Files Uploaded Successfully</p>";
for(var i = 0; i < response.length; i++) {
content += "<p>DownloadUrl : <a href='" + response[i].fileDownloadUri + "' target='_blank'>" + response[i].fileDownloadUri + "</a></p>";
}
multipleFileUploadSuccess.innerHTML = content;
multipleFileUploadSuccess.style.display = "block";
} else {
multipleFileUploadSuccess.style.display = "none";
multipleFileUploadError.innerHTML = (response && response.message) || "Some Error Occurred";
}
}
xhr.send(formData);
}
singleUploadForm.addEventListener('submit', function(event){
var files = singleFileUploadInput.files;
if(files.length === 0) {
singleFileUploadError.innerHTML = "Please select a file";
singleFileUploadError.style.display = "block";
}
uploadSingleFile(files[0]);
event.preventDefault();
}, true);
multipleUploadForm.addEventListener('submit', function(event){
var files = multipleFileUploadInput.files;
if(files.length === 0) {
multipleFileUploadError.innerHTML = "Please select at least one file";
multipleFileUploadError.style.display = "block";
}
uploadMultipleFiles(files);
event.preventDefault();
}, true);
9.3 CSS
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-weight: 400;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
line-height: 1.58;
color: #333;
background-color: #f4f4f4;
}
body:before {
height: 50%;
width: 100%;
position: absolute;
top: 0;
left: 0;
background: #128ff2;
content: "";
z-index: 0;
}
.clearfix:after {
display: block;
content: "";
clear: both;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 20px;
margin-bottom: 20px;
}
h1 {
font-size: 1.7em;
}
a {
color: #128ff2;
}
button {
box-shadow: none;
border: 1px solid transparent;
font-size: 14px;
outline: none;
line-height: 100%;
white-space: nowrap;
vertical-align: middle;
padding: 0.6rem 1rem;
border-radius: 2px;
transition: all 0.2s ease-in-out;
cursor: pointer;
min-height: 38px;
}
button.primary {
background-color: #128ff2;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
color: #fff;
}
input {
font-size: 1rem;
}
input[type="file"] {
border: 1px solid #128ff2;
padding: 6px;
max-width: 100%;
}
.file-input {
width: 100%;
}
.submit-btn {
display: block;
margin-top: 15px;
min-width: 100px;
}
@media screen and (min-width: 500px) {
.file-input {
width: calc(100% - 115px);
}
.submit-btn {
display: inline-block;
margin-top: 0;
margin-left: 10px;
}
}
.upload-container {
max-width: 700px;
margin-left: auto;
margin-right: auto;
background-color: #fff;
box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
margin-top: 60px;
min-height: 400px;
position: relative;
padding: 20px;
}
.upload-header {
border-bottom: 1px solid #ececec;
}
.upload-header h2 {
font-weight: 500;
}
.single-upload {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid #e8e8e8;
}
.upload-response {
overflow-x: hidden;
word-break: break-all;
}
Truy cập vào http://localhost:8080 và ứng dụng sau cùng của bạn sẽ trông như thế này
OK, done! Cảm ơn các bạn đã đọc! Hẹn gặp lại trong bài tiếp theo!





