Docker is an open source platform that lets you build application container images, which developers can then use to create, manage, and run containers. Docker enhances the efficiency of the development lifecycle while also making it more predictable, maximizing your productivity.
Meanwhile, Spring Boot is a popular Java framework for creating production-grade applications. Dockerizing Spring Boot apps has become the norm for modern application development, simplifying the process of packaging and deploying Spring Boot applications into containers that can be executed on any Docker-enabled machine. This makes it easy to move your application from one environment to another (ie from development to production).
In addition, Docker allows you to easily scale your application by running multiple instances of your containerized application. This can be done using tools such as Kubernetes, which provides a powerful platform for managing containerized applications.
In this tutorial, you will create a sample microservice application with Spring Boot and run it with Docker. You will learn that containerization can help increase scalability and portability in a microservices architecture. In addition, you’ll develop unit tests of the microservice application to better understand the importance of code coverage.
All the code for this tutorial is available in this GitHub repo.
Setting Up Your Spring Boot Project
Before you begin this tutorial, you need to install the following:
- Java JDK 17
- Apache Maven 3.8.7
- An IDE of your choice, such as IntelliJ, for Java project development
In this tutorial, you’ll be setting up a Spring Boot project from scratch using the Spring Boot command line tool (CLI). This CLI helps you bootstrap a new Spring Boot project in no time. Refer to the official documentation to install the CLI for your operating system.
Please note:
The Spring Boot CLI version 3.0.5 on Windows was used here.
Once installed, you can verify the installation by executing the command from the bin
directory of Spring Boot CLI’s installation path:
.\spring --version
Creating a New Spring Boot Project
Now that you’ve installed the Spring Boot CLI, it’s time to create a new Spring Boot project for your microservice application. From the bin
directory of Spring Boot CLI’s installation path, execute the following command:
.\spring init --boot-version=2.7.11 --build=maven --java-version=17 --dependencies=web,lombok,postgresql,data-jpa --packaging=jar getting-started-with-springboot-and-docker.zip
This command tells your system to create a Java 17–based Spring Boot application with a Maven-compatible build definition and other required dependencies.
You should see a new ZIP file named getting-started-with-springboot-and-docker.zip
created in the same directory. This ZIP file contains the essential files with a folder structure for the microservice application that you’re going to build.
Next, you need to create a new directory named getting-started-with-springboot-and-docker
in any location of your choice on the host machine. Extract and copy the contents in the ZIP file to the newly created getting-started-with-springboot-and-docker
directory.
Configuring Basic Properties in application.properties
Now that your basic project files are ready, open the project in IntelliJ (or your favorite Java IDE) and create the application.properties
file in the Spring Boot project path .\getting-started-with-springboot-and-docker\src\main\resources\
. Use this file to configure the basic properties of your application by pasting the following code in application.properties
:
server.port=8080
spring.datasource.url=jdbc:postgresql://host.docker.internal:5432/product
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=create-drop
Building a Microservice with Spring Boot
Before we continue to develop this application, take a minute to understand what a microservices architecture is and why that matters.
Microservices architecture is a software design approach where a large monolithic application is broken down into smaller, independent services that can be developed, deployed, and scaled independently of each other. Each microservice is designed to perform a specific function and communicates with other microservices through well-defined APIs. This approach enables faster development, deployment, and testing, as well as better scalability and resilience. However, it also requires careful management of interservice communication and data consistency.
Now that you know microservices communicate through APIs, move on to building REST APIs with Spring Boot.
Create a Controller Package
In this example, you are working with a product microservices application that holds data about products in a retail store. Here, you create REST APIs to fetch the details about the products stored in the microservice application’s database, which we use PostgreSQL for.
Start by creating a Java package named controller
in your project’s path, src/main/java/com/example/gettingstartedwithspringbootanddocker
. Then create a class file named ProductsController.java
and paste the following code into it:
package com.example.gettingstartedwithspringbootanddocker.controller;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.example.gettingstartedwithspringbootanddocker.model.Products;
import com.example.gettingstartedwithspringbootanddocker.service.ProductsService;
@RestController
public class ProductsController {
@Autowired
ProductsService productsService;
@GetMapping("/all/products")
List<Products> getAllProducts() {
return productsService.getAllProducts();
}
@GetMapping("/product/{productId}")
ResponseEntity<Optional<Products>> getProductById(@PathVariable Long productId) {
Optional<Products> product = productsService.findByProductId(productId);
if (product.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(product);
}
}
A Controller
class is responsible for handling incoming HTTP requests and returning an HTTP response. The Controller
class typically receives input from the user or from other systems, performs any necessary validation or processing, interacts with the service layer to perform business logic, and returns an appropriate response to the client.
The ProductsController
class has two REST endpoints: one to get all the products and another to get a product based on its ID. These endpoints make use of the service class methods to perform business logic. In the previous code snippet, you can see that the @Autowired
annotation is applied to a field named productsService
and a ProductsService
type.
The @Autowired
annotation is used for automatic dependency injection in Spring and allows Spring to automatically resolve and inject the dependencies required by a class. Next, you’ll learn more about the service class as you create one.
Create a Service Class
Your ProductsController
class depends on the service class named ProductsService
to handle the backend business logic. For this purpose, you can create a ProductsService.java
class in a package named service
in the project path src/main/java/com/example/gettingstartedwithspringbootanddocker/
. Paste the following code in the ProductsService
class file:
package com.example.gettingstartedwithspringbootanddocker.service;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.gettingstartedwithspringbootanddocker.model.Products;
import com.example.gettingstartedwithspringbootanddocker.repository.ProductsRepository;
@Service
public class ProductsService {
@Autowired
ProductsRepository productsRepository;
public List<Products> getAllProducts() {
return productsRepository.findAll();
}
public Optional<Products> findByProductId(Long productId) {
return productsRepository.findById(productId);
}
}
The Service
class uses a Repository
class to interact with the database. You’ll define the Repository
class next.
Create a Repository Class
Create the ProductsRepository.java
class in a package named repository
in the project path src/main/java/com/example/gettingstartedwithspringbootanddocker/
. A repository class provides an interface to store and retrieve data from a database. Edit the ProductsRepository.java
class by pasting the following code into it:
package com.example.gettingstartedwithspringbootanddocker.repository;
import com.example.gettingstartedwithspringbootanddocker.model.Products;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductsRepository extends JpaRepository<Products, Long> {
}
In Spring, the @Repository
annotation indicates that a class is a repository that is responsible for data access and persistence operations. It’s typically applied to classes that interact with a database or other data sources.
In the previous code, the ProductsRepository
interface extends the JpaRepository
interface, which is a generic interface provided by Spring Data JPA. The Spring Data JPA is an abstraction over the Java Persistence API (JPA). By extending JpaRepository
, the ProductsRepository
interface inherits several CRUD (Create, Read, Update, Delete) methods, as well as additional querying capabilities provided by Spring Data JPA. The generic types used in JpaRepository<Products, Long>
specify the entity type (Products) and the type of the entity’s primary key (Long). In this case, it indicates that the ProductsRepository
interface is specifically designed to handle persistence operations for the Products entity, where the primary key is of type Long.
Create a Products Model Class
The ProductsRepository
class acts on the Products
table in the database. You need to create a model class for the Products
table to define the relevant table and field mapping on the Java application side. Create the Products.java
class in a package named model
in the project path src/main/java/com/example/gettingstartedwithspringbootanddocker/
. Paste the following code in the Products
class file:
package com.example.gettingstartedwithspringbootanddocker.model;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name="products")
@Getter
@Setter
public class Products {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id")
private Long id;
@Column(name="name")
private String name;
@Column(name="category")
private String category;
@Column(name="price")
private float price;
}
This code snippet makes use of several annotations including the following:
@Entity
marks the class as an entity, representing a table in the database.@Table(name="products")
specifies the table name (ie products) to which the entity is mapped.@Getter
generates the getter methods for the class’s fields.@Setter
generates the setter methods for the class’s fields.@Id
marks the field as the primary key of the entity.@GeneratedValue(strategy = GenerationType.IDENTITY)
specifies that the primary key values are automatically generated by the database using an identity strategy.@Column
maps the field to a column in the table
Create Database Table Script Files
Now that you’ve created the Java application code for your microservice application, you need to create database script files in the resources location (src\main\resources
) where the application.properties
of your Java project exists. These scripts are used to create the Products
table structure in the database and insert data into it when the application gets initialized.
Create a file named schema.sql
and paste the following data definition language (DDL) code into it:
CREATE TABLE products (
id NOT NULL NUMBER(19) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
category TEXT,
price DECIMAL(10,2) NOT NULL
);
Then create another file named import.sql
and paste the following code into it:
INSERT INTO products (id, name, category, price) VALUES (1, 'Closeup Toothpaste', 'Toothpaste', 78) ;
INSERT INTO products (id, name, category, price) VALUES (2, 'Colgate Toothpaste', 'Toothpaste', 80) ;
INSERT INTO products (id, name, category, price) VALUES (3, 'Pepsodent Toothpaste', 'Toothpaste', 82) ;
Spring Boot takes care of automatically reading these script files and loading them into the database configured in the application.properties
file.
And now, you’ve finished creating the sample microservices application with Spring Boot.
Building the Spring Boot Microservice Application
To build the JAR of the Spring Boot microservice application, run the following command in your terminal:
cd getting-started-with-springboot-and-docker
mvnw clean install
After successful execution, you should be able to see the application JAR created in a directory named target
.
Running the Spring Boot Application with Docker
To run the Spring Boot application with Docker, download and install the Docker Desktop software. Installing Docker Desktop gets you both Docker and Docker Compose.
Docker Compose is a tool that developers use to specify and deploy multicontainer Docker applications on a single machine. With Docker Compose, you can define the services, networks, and volumes required for your application in a single docker-compose.yml
file. This file specifies how each container image should be built, configured, and linked together.
Once Docker Desktop is up and running, you can verify that it’s working by opening a terminal or command prompt and running the following command:
docker version
docker compose version
Creating a Dockerfile for your Spring Boot Project
After installing Docker, you need to create a Dockerfile for your Spring Boot application. A Dockerfile is a text file that contains a set of instructions that are used to build a Docker image.
Create a file named Dockerfile
in the project directory getting-started-with-springboot-and-docker
and paste the following code into it:
# Use an official OpenJDK runtime as a parent image
FROM openjdk:17-jdk-alpine
# Set the working directory to /app
WORKDIR /app
# Copy the executable jar file and the application.properties file to the container
COPY target/getting-started-with-springboot-and-docker-0.0.1-SNAPSHOT.jar /app/
# Set the command to run the Spring Boot application
CMD ["java", "-jar", "getting-started-with-springboot-and-docker-0.0.1-SNAPSHOT.jar"]
This Dockerfile contains instructions, including the following:
- The base image
openjdk:18-jdk-alpine
to use for the container - The process to set the working directory in the container
- The instructions to copy the application JAR into the container
- The command to start the application
Creating a Docker Compose YAML File
Since your microservice application requires a database to persist the Products
data, you need to work with two services: a PostgreSQL database and the microservice application. You can create a docker-compose.yaml
file in the project directory getting-started-with-springboot-and-docker
to hold the instructions to start and manage both these services.
Paste the following code in the docker-compose.yaml
file:
version: '3.5'
services:
postgres:
container_name: postgres
image: postgres:15.1
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: product
ports:
- "5432:5432"
product-service:
container_name: product-service
image: product-service
ports:
- "8080:8080"
restart: "always"
The previous docker-compose
file defines two services: a PostgreSQL database service (postgres) and a custom service (product-service). It sets up the necessary environment variables and port mappings for each service and specifies the Docker images to use. For more information on these specifications check out the official documentation of Docker Compose
Building and Running a Docker Container for Your Microservice
Run the following command to build the Docker container image in your terminal:
cd getting-started-with-springboot-and-docker
docker build -t product-service.
Once the image is built, you can run the following command to start the containers:
docker compose -f docker-compose.yaml up -d
Access the following URLs in a browser or REST client to test if the REST APIs you created are working:
http://localhost:8080/all/products
You should see a response like this:
[{
"id": 1,
"name": "Closeup Toothpaste",
"category": "Toothpaste",
"price": 78.0
}, {
"id": 2,
"name": "Colgate Toothpaste",
"category": "Toothpaste",
"price": 80.0
}, {
"id": 3,
"name": "Pepsodent Toothpaste",
"category": "Toothpaste",
"price": 82.0
}]
Unit Testing Microservices in Spring Boot
Now that your entire sample microservice is ready, it’s time to develop some unit tests. Unit tests ensure individual classes or components work as intended in isolation. They’re usually automated tests designed to test the smallest piece of code possible.
In the context of microservices, unit tests are used to test the individual microservices in isolation, without relying on other microservices or external dependencies, such as databases.
In the case of your microservice, the backend logic is present in the service class ProductsService
, and you need to unit test it. To do so, create a test package named service
in the project’s test package path, src/test/java/com/example/gettingstartedwithspringbootanddocker
. Then create a class file named ProductsServiceTest.java
and paste the following code into it:
package com.example.gettingstartedwithspringbootanddocker.service;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.example.gettingstartedwithspringbootanddocker.model.Products;
import com.example.gettingstartedwithspringbootanddocker.repository.ProductsRepository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
@SpringBootTest
class ProductsServiceTest {
@MockBean
private ProductsRepository productsRepository;
@Autowired
private ProductsService productsService;
/**
* Method under test: {@link ProductsService#getAllProducts()}
*/
@Test
void testGetAllProducts() {
// Arrange
ArrayList<Products> productsList = new ArrayList<>();
when(productsRepository.findAll()).thenReturn(productsList);
// Act
List<Products> actualAllProducts = productsService.getAllProducts();
// Assert
assertSame(productsList, actualAllProducts);
assertTrue(actualAllProducts.isEmpty());
verify(productsRepository).findAll();
}
/**
* Method under test: {@link ProductsService#findByProductId(Long)}
*/
@Test
void testFindByProductId() {
// Arrange
Products products = new Products();
products.setCategory("Toothpaste");
products.setId(1L);
products.setName("Closeup Toothpaste");
products.setPrice(78.0f);
Optional<Products> ofResult = Optional.of(products);
when(productsRepository.findById((Long) any())).thenReturn(ofResult);
// Act
Optional<Products> actualFindByProductIdResult = productsService.findByProductId(1L);
// Assert
assertSame(ofResult, actualFindByProductIdResult);
assertTrue(actualFindByProductIdResult.isPresent());
verify(productsRepository).findById((Long) any());
}
}
You can see that the getAllProducts
and findByProductId
methods of the actual ProductsService
class are being tested. The ProductsRepository
class is mocked to simulate the database interaction without connecting to the real database.
Understanding Code Coverage
Code coverage is a measure of how much of the source code of a software application is executed during testing. It’s an important metric to ensure that the application has been thoroughly tested and that all parts of the code have been exercised. High code coverage is desirable, as it increases confidence in the quality of the software and reduces the risk of bugs and issues. So how can you incorporate code coverage into this Spring Boot project?
The answer is simple. Add the following code to your project’s pom.xml
file:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
The jacoco
plugin addition to the project allows you to autogenerate code coverage reports when the tests are executed.
Tests Execution and Code Coverage Report Generation
To execute your tests and generate code coverage reports, execute the following commands:
docker compose -f docker-compose.yaml up -d postgres
mvnw clean install
This docker compose
command starts the PostgreSQL database for your initialized application. The mvnw clean install
command clears the target directory if it exists and builds the application JAR by executing the tests. As part of the execution, it also uses the jacoco maven plugin
to generate code coverage reports in the target/site/jacoco/
directory.
You can access the index.html
file in that jacoco
directory to see your project’s code coverage:
In a simple microservice application like the one where you only have a few classes to deal with, this process is relatively easy. However, in an enterprise-grade microservice application, you have to deal with developing more classes and, subsequently, more unit tests for all those classes. This can easily become overwhelming and take a lot of your team’s resources.
To reduce your time spent on test class development and increase code coverage, it’s a good idea to use an AI-powered technology platform like Diffblue that can help automatically generate unit tests for your application.
Diffblue Cover writes Java unit tests for you completely autonomously, with zero effort on your part. They always compile and reflect the behavior of your code today, to help you spot regressions when you make changes. A free IntelliJ plugin can be used to write tests while you’re working on specific code; a more powerful CLI version lets you write and update tests across an entire Java project, and make the process an integrated part of your CI pipeline.
Diffblue Cover Reports expands on the code coverage reporting provided by JaCoCo to give you more insight into where risks might lie and why some code is tested.
Conclusion
Spring Boot is an amazing open source framework to develop product-grade Java applications. Bundling it as Docker images and running it in containers will help you gain flexibility and scalability in any environment.
In this tutorial, you developed a sample microservice with Spring Boot and executed the application using Docker technology. In addition, you learned about the importance of code coverage and how to implement it in your application.
We also looked at how important unit tests are when you’re getting started with Spring Boot and Docker (just like when you’re writing any other code!) to ensure quality and avoid regressions as you make changes. But writing them is slow, repetitive and tedious compared to the more interesting work of getting your Docker-based microservice up and running. That’s where Diffblue Cover comes in.
Try Cover today to see what it can do for your Java projects.