An easier way to upload / retrieve images using Spring boot 2.0 , Angular 8+

Ahmed Grati
8 min readMay 12, 2020

--

Problematic:

When coming to store images, we usually do the following:
1- Encode them in Base64
2- Transfer them
3- Convert them to raw binary form
4- Store them
However, such process is a complicated task for many developers. Furthermore , Base64 encoding increases the data size by 33% of the original image size, which is the largest bottleneck in computing. So, many developers avoid this method.
As an alternative solution, I am proposing another method which will save you time and resources especially when the project is scaling. In this article, I will show you how to store images without having to convert them neither to Base64 nor to binary form.

Demo:

Introduction:

Before we make our hands dirty, let me first explain briefly the solution. To start with, the client will send the image without encoding it to the server. It will be sent as a FormData. Then in the server side, the image won’t be stored in the database. Instead, it will be stored somewhere in the machine which could be our local machine or a virtual machine (in case we deploy the project) and a URL to the image will be returned to the client. Afterward, to retrieve the image, the client should only use that URL. As you can see, we don’t have to encode any image or store it in a database.

Used Technologies:

. Angular 8+

. Spring boot (Java) 2.0

1.Before we start:

We assume that you already have:
1- Front-end project
2- Back-end project
However, we will specify later in this article the dependencies used in both projects.

2.Dependencies:

2.1 Spring-boot dependencies and properties

in “pom.xml” file, insert this code which contains all the dependencies we need in this article.

<?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.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>UploadDownloadImages</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>UploadDownloadImages</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<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>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>


</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

In the same project, in “application.propreties” file insert this code:

# application configurations
server.port=90

#upload files configurations
spring.servlet.multipart.enabled=true
#It specifies the maximum size permitted for uploaded files. The default is 1MB.
spring.servlet.multipart.max-file-size=10MB
# It specifies the maximum size allowed for multipart/form-data requests. The default is 10MB.
spring.servlet.multipart.max-request-size=15MB
# Whether to enable support of multipart uploads.

2.2 Angular dependencies

In “app.module.ts” file, insert this code:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule} from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import {ImageComponent} from './components/image/image.component';

@NgModule({
declarations: [
AppComponent,
ImageComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

As you can see, a new component “image” has been generated.

3. Back-end project:

In order to have a modular project, we will set 3 packages: Configuration, Services and Controllers.

Before processing the image transfer, we should make sure that requests are sent without any CORS problems, For that reason, we will have a CORS configuration in the Configurations package.

3.1 CorsConfig.java

First of all, we should annotate the class with @Configuration and @EnableWebMVC to indicate that this class is used for the project configuration.

Then we should create a Bean that returns a WebMvcConfigurer and overriding the method addCorsMapping so we can specify the allowed Methods, Headers and Origins. In our case, we will allow all headers, origins and the methods GET, POST, PUT, DELETE.

package com.example.UploadDownloadImages.Configurations;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
//this class is a global cors configuration
public class CorsConfig {

@Bean
public WebMvcConfigurer corsConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("GET","POST","PUT","DELETE")
.allowedHeaders("*")
.allowedOrigins("*");
}
};
}


}

3.2 ImageService.java

In the class ImageService , we will implement our logic. Later, we will inject it in the controller.

3.2.1 Upload Process

Below the steps to follow in the upload processing:
1- Receive the image sent from the client-side as a FormData
2- Retrieve the name of the sent file with its extension (.JPG , .PNG , .GIF).
3- Set our path for the storage directory.
Note: This path depends on the Operating System on which our project works. In my case, I’m using windows 10.
4- Check the existence of this directory in our machine and create it if it doesn’t exist.
5- Set the image path to retrieve it later
6- Copy all the bytes from the input stream to the file located in the storage directory.
7- Return the URL which contains our image to help us retrieving it later.

3.2.2 Retrieve Process

As mentioned above, the upload process result is a URL that we will use to find our image. This URL will have the following form: https://ip:port/api/images/getImage/imageName .
Based on the image name, we can now retrieve the image from the storage directory and then return its ByteArray.

package com.example.UploadDownloadImages.Services;

import org.apache.commons.io.IOUtils;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.io.IOException;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;


@Service
public class ImageService {

public final String storageDirectoryPath = "C:\\Users\\Ahmed\\Desktop\\mediumImages";
public ResponseEntity uploadToLocalFileSystem(MultipartFile file) {
/* we will extract the file name (with extension) from the given file to store it in our local machine for now
and later in virtual machine when we'll deploy the project
*/
String fileName = StringUtils.cleanPath(file.getOriginalFilename());

/* The Path in which we will store our image . we could change it later
based on the OS of the virtual machine in which we will deploy the project.
In my case i'm using windows 10 .
*/
Path storageDirectory = Paths.get(storageDirectoryPath);
/*
* we'll do just a simple verification to check if the folder in which we will store our images exists or not
* */
if(!Files.exists(storageDirectory)){ // if the folder does not exist
try {
Files.createDirectories(storageDirectory); // we create the directory in the given storage directory path
}catch (Exception e){
e.printStackTrace();// print the exception
}
}

Path destination = Paths.get(storageDirectory.toString() + "\\" + fileName);

try {
Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);// we are Copying all bytes from an input stream to a file

} catch (IOException e) {
e.printStackTrace();
}
// the response will be the download URL of the image
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("api/images/getImage/")
.path(fileName)
.toUriString();
// return the download image url as a response entity
return ResponseEntity.ok(fileDownloadUri);
}

public byte[] getImageWithMediaType(String imageName) throws IOException {
Path destination = Paths.get(storageDirectoryPath+"\\"+imageName);// retrieve the image by its name

return IOUtils.toByteArray(destination.toUri());
}

}

3.3 ImageController.java

Before doing any process, we should inject our service “ImageService” with the annotation @Autowired so that we will be able to use its methods.
The value of the request mapping of ImageController is “api/images”

3.3.1 Upload Process

It’s a POST mapping with the value “upload”. We will just get the file as a RequestParam and then we will return the output of the upload process performed by the ImageService.

3.3.2 Retrieve Process

It’s a GET mapping with:
1- Value getImage/{imageName:.+}: contains the file name with its extension
2- Produces: It could be a JPEG, PNG or GIF.
We get the imageName with its extension using the annotation @PathVariable. Finally, we will return the output of the upload process performed by the ImageService.

package com.example.UploadDownloadImages.Controllers;

import com.example.UploadDownloadImages.Services.ImageService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@RestController
@RequestMapping(value = "api/images")
public class ImageController {

@Autowired
public ImageService imageService;
@PostMapping(value ="upload")
public ResponseEntity uploadImage(@RequestParam MultipartFile file){
return this.imageService.uploadToLocalFileSystem(file);
}
@GetMapping(
value = "getImage/{imageName:.+}",
produces = {MediaType.IMAGE_JPEG_VALUE,MediaType.IMAGE_GIF_VALUE,MediaType.IMAGE_PNG_VALUE}
)
public @ResponseBody byte[] getImageWithMediaType(@PathVariable(name = "imageName") String fileName) throws IOException {
return this.imageService.getImageWithMediaType(fileName);
}
}

4. Front-end Project:

Before diving into image upload and retrieve, we should create:
- An image component: It will contain the interface for uploading and retrieving the image
- An image service: It will have all the logic of uploading and retrieving images. Afterwards, it will be injected in the image component.

4.1 app.component.html

the app.component.html should look like this:

<app-image></app-image>

<router-outlet></router-outlet>

4.2 ImageService

In the image service, we inject the httpClient which will help us with CRUD management.
So, we have an upload image method which takes as parameter our image as FormData and we process a post method which returns an observable.
IMPORTANT: Specify the response type because if it is not JSON(by default the response type is JSON). In our case, the response type is a text which is the image retrieving URL.

import { Injectable } from '@angular/core';import {HttpClient} from '@angular/common/http';import { Observable } from 'rxjs';@Injectable({providedIn: 'root'})export class ImageService {constructor(private httpClient: HttpClient) { }public baseUrl = 'http://localhost:90/api/images';public uploadImage(formData: FormData): Observable<any> {const file = formData.get('file') as File;const url = this.baseUrl + `/upload?file=${file.name}`;return this.httpClient.post(url, formData , {responseType: 'text'});}}

4.3 ImageComponent

4.2.1 image.component.html

it’s a simple HTML form that contains:
- A file input
- A submit button
- A div which will contain the uploaded image after retrieving it
The uploading process will be triggered by changing the file input, executing the method onSelectFile and clicking the submit button.
Next, the div will be shown only if the imageSrc is not null. Consequently, the attribute src of the image tag will have the value of imageSrc
In other words, once we retrieve the image URL we will display it in our page.

<form>
<input type="file" (change)="onSelectFile($event)">
<br>
<br>
<button type="button" (click)="performUpload()">Submit your choice</button>
</form>
<div *ngIf="imageSrc">
<img src="{{imageSrc}}">
</div>

4.2.2 image.component.ts

First of all, we should inject our image service to initiate the upload process.
In this component, we will have only two methods:
- onSelectFile: A method to be executed when we select a file. Then, we need to set our variable selectedFile with the selected file value in order to send it later to the server.
- performUpload: A method to be executed when the submit button is pressed. In fact, in this method we will
* Set the field ‘file’ in our FormData to the selected file
* Execute the uploadImage of the image service
* Subscribe to the returned observable and the result will be the image URL.

import { Component, OnInit } from '@angular/core';
import { ImageService } from 'src/app/services/image.service';

@Component({
selector: 'app-image',
templateUrl: './image.component.html',
styleUrls: ['./image.component.css']
})
export class ImageComponent implements OnInit {

constructor(private imageService: ImageService) { }
public formData = new FormData();
public selectedFile: File = null;
public imageSrc: string;
ngOnInit() {

}

onSelectFile(event) {
this.selectedFile = event.target.files[event.target.files.length - 1] as File;
}

performUpload() {
this.formData.set('file', this.selectedFile, this.selectedFile.name);
this.imageService.uploadImage(this.formData).subscribe(
res => {
this.imageSrc = res;
}
);
}
}

6. Run angular and spring-boot projects

6.1 run the angular project

In your angular project command, run the following instruction:

ng serve -o

This command will run the project on port 4200 by default. So, if you want to change the used port (for example using port 5000 instead of 4200), all you need to do is running the following command:

ng serve -o --port 5000

6.2 run the spring-boot project

Just run your project depending on the IDE you are using. If you want to change the port on which your project is working (for example port 6000), you just need to update application.propreties file as follow:

server.port= 6000

I hope you find this article helpful. Feel free to post all your questions/comments below.

Front-end Source Code:

https://github.com/AhmedGrati/UploadAndRetreiveImagesClient

Back-end Source Code:

https://github.com/AhmedGrati/UploadAndRetreiveImagesServer

--

--