In this article, I will show how to upload files with Spring Boot using MultipartFile. I’m gonna also explain the best option to store the images, explaining what’s a CDN and what’s an NFS.
Content:
- The MultipartFile object
- Where to store the images
- Advantages of a CDN or NFS
You can find more details with this video.
All the code used in the article is available in this repository.
The MultipartFile object
First, I have to prepare the backend to accept images, then I have to store those images linked to a user, and allow the request to return the stored images.
I’m gonna start with the controller to upload an image to the backend. For that, I need the MultipartFile. This object contains the binary content of the image and some metadata, as the name of the file, extension, the size and some other information.
@PostMapping("/images")
public ResponseEntity<ImageDto> postImage(
@AuthenticationPrincipal UserDto userDto,
@RequestParam <strong>MultipartFile</strong> file,
@RequestParam(value = "title") String title
) throws IOException {
return ResponseEntity.created(URI.create("/community/images"))
.body(communityService.postImage(userDto, file, title));
}
Where to store the images
But where will I store the image? An option is to store the image in the database. But that’s not a good idea. The images tend to be big. And the database is more performant when using small data. What I will is store the data on the hard drive. In fact, a better option is to store them in an NFS or upload them to a CDN.
Why store them in an NFS, a Network File System, or to a CDN, a Content Delivery Network?
This way, the images are stored in a separated machine than the one which is running the application. And separated from the database too. If a problem occurs in some machine, as
the content is separated in multiple machines, you have less probability to lose the data.
@Value("${app.nfs.path:/tmp}")
private String nfsPath;
public ImageDto postImage(UserDto userDto, MultipartFile file, String title) throws IOException {
User user = getUser(userDto);
String path = this.nfsPath
+ "/" + user.getId()
+ "/" + UUID.randomUUID()
+ "-" + file.getOriginalFilename();
Image image = Image.builder()
.title(title)
.user(user)
.path(path)
.build();
File storedImage = new File(path);
if (!storedImage.getParentFile().exists()) {
storedImage.getParentFile().mkdir();
}
storedImage.createNewFile();
file.transferTo(storedImage);
if (user.getImages() == null) {
user.setImages(new ArrayList<>());
}
user.getImages().add(image);
userRepository.save(user);
return userMapper.imageToImageDto(image);
}
I’ve created the nfsPath value in the configuration file. And then, in the service, the @Value annotation will inject the value to the variable or take the default value /tmp if there is no value in the configuration file.
I’m storing the path of the image only. This way, I leave the database with lightweight data, just a pointer to the image. And when retrieving it, as the image is in a shared resource, a public shared resource, I return the URL, which points to the image itself.
Advantages of a CDN or NFS
Ok, until now, I only store the images which I receive from the controller. How to return them when asked? As I said, I only return a URL. Why? The images are static resources, they won’t change upon a user request. And they are big resources, heavyweight resources. I may use a CDN to return them. What’s a CDN? With a CDN i have the images duplicated in several servers all around the world to let the user download them from the nearest server. Otherwise, if I have to download the images always through the backend, I add an additional
load to the backend. This will increase the response time of the backend. And as they are heavyweight, this will consume a lot of CPU and memory from the backend server. If the images are stored in a separate server, in multiple separated servers, in multiple CDNs, I won’t need any resource from the backend server, and the images will be near to the user who makes the request. If they are nearest, they will be downloaded faster. So, to return them, I will start with the controller.
@GetMapping("/images")
public ResponseEntity<List<ImageDto>> getCommunityImages(
@AuthenticationPrincipal UserDto userDto,
@RequestParam(value = "page", defaultValue = "0") int page
) {
return ResponseEntity.ok(communityService.getCommunityImages(userDto, page));
}
And now, in the service return the information with the path of the image. The complete URL will be built at the frontend part to let the frontend download the image.
public List<ImageDto> getCommunityImages(UserDto userDto, int page) {
User user = getUser(userDto);
List<Long> friendIds = Optional.of(user.getFriends())
.map(friends -> friends.stream().map(User::getId).collect(Collectors.toList()))
.orElse(Collections.emptyList());
friendIds.add(user.getId());
List<Image> images = imageRepository.findCommunityImages(friendIds,
PageRequest.of(page, PAGE_SIZE));
return userMapper.imagesToImageDtos(images);
}
The main point of this architecture is to separate the content from the running application.
If something wrong happens to the server where the application is running, I won’t lose all the images, as for the database, it should be in a separated server. And then, to return the images, I will only use a network server, a CDN, to optimize the download time of the images.
A CDN is a service that must be purchased with the provider of your internet resources.
All the code used in the article is available in this repository.



Leave a comment