A Step by Step guide to create CRUD RESTful APIs using Spring Boot + Spring Data JPA with H2 in-memory database
Last modified: 23 Feb, 2020Introduction
Spring Boot is Spring’s convention-over-configuration solution for creating stand-alone, production-grade Spring-based Applications that you can “just run”.
Some of Spring Boot feature are:
- Create stand-alone Spring applications
- Embed Tomcat or Jetty directly (no need to deploy WAR files)
- Provide opinionated ‘starter’ Project Object Models (POMs) to simplify your Maven configuration
- Automatically configure Spring whenever possible
- Provide production-ready features such as metrics, health checks and externalized configuration
- Absolutely no code generation and no requirement for XML configuration
This article is one stop guide for implementing Restful API service using Spring Boot. We will be implementing Catalogue Management Service, which includes restful APIs to Create
, Read
, Update
and Delete
Catalogue Items by their SKU (Stock Keeping Unit). We will be using H2 Database
, which is an in-memory runtime database that get created/initialized when application boots up and get destroyed when application shuts down.
Article also includes detailed steps on
- Configuring
Maven
&Gradle
builds. - Add capability for Restart and LiveReload using
spring-boot-devtools
. - Add capability for Production-ready features using
Spring Boot Actuator
. - Implementing APIs using Spring’s
@RestController
. - Global Exception Handling using
@ControllerAdvice
. - Track API execution time using
@Aspect
. - Validate API request using
Bean Validation API
and implementing custom Enum Validator. - Implementing Unit tests & Integration Tests using
JUnit5
. - Configuring
JaCoCo
for collecting Code Coverage metrics. - Test APIs using
Postman
.
Catalogue Management System Restful APIs
We will be implementing the below CRUD Restful APIs
to manage items for a Catalogue Management System.
HTTP Method | API Name | Path | Response Status Code |
---|---|---|---|
POST | Create Catalogue Item | / | 201 (Created) |
GET | Get Catalogue Items | / | 200 (Ok) |
GET | Get Catalogue Item | /{sku} | 200 (Ok) |
PUT | Update Catalogue Item | /{sku} | 200 (Ok) |
DELETE | Delete Catalogue Item | /{sku} | 204 (No Content) |
POST | Upload Catalog Item Picture | /{sku}/image | 201 (Created) |
Technology stack for implementing the Restful APIs...
- Redhat OpenJDK 11
- Spring Boot v2.2.4
- Spring Framework v5.2.3.RELEASE
- Hibernate v5.4.11.Final
- JPA v2.0
- Maven v3.6.3
- Gradle v6.1.1
- IntelliJ Idea v2019.3.2
Why use OpenJDK?
Oracle has announced that the Oracle JDK builds released after Jan 2019 cease to be free for commercial use.
An alternative is to use OpenJDK and effort is underway to make them fully interchangeable. A number of companies who are currently using Oracle JDK in production are making the decision to switch to OpenJDK or have already done so.
Why Red Hat’s Build of OpenJDK?
- Little to No Code Changes - OracleJDK and Red Hat’s implementation of OpenJDK are functionally very similar and should require little to no changes.
- Java Compliance - Red Hat OpenJDK is baselined from the OpenJDK project and TCK compliant.
- Multi-Platform Support - Red Hat OpenJDK is optimized for containers and supported on Windows and Linux.
Bootstrapping Project with Spring Initializr
Spring Initializr generates spring boot project with just what we need to start implementing Restful services quickly. Initialize the project with appropriate details and the below dependencies.
- Spring Web
- Spring Data JPA
- H2 Database
- Lombok
- Spring Dev Tools
- Spring Actuator
Click Here to download maven
/gradle
project with the above details and dependencies which we can use to start implementing Catalogue Management System Restful APIs.
Configure IntelliJ IDEA
Extract the downloaded maven/gradle project achieve into specific location. Import the project into IntelliJ Idea by selecting pom.xml which will start downloading the dependencies.
This should show the below project structure
Post importing based on the build system being used, dependencies will be downloaded and ready for execution.
Using Project Lombok
We will be heavily relying on Project Lombok, which is a Java Library which makes our life happier and more productive by helping us to never write another getter or equals method again, constructors which are so repetitive. The way Lombok works is by plugging into our build process and autogenerating Java bytecode into our .class files as per a number of project annotations we introduce in our code.
Below is sample Lombok code for a POJO class:
@Data
@AllArgsConstructor
@RequiredArgsConstructor(staticName = "of")
public class CatalogueItem {
private Long id;
private String sku;
private String name;
}
@Data
is a convenient shortcut annotation that bundles the features of@ToString
,@EqualsAndHashCode
,@Getter
/@Setter
and@RequiredArgsConstructor
all together.@AllArgsConstructor
generates a constructor with 1 parameter for each field in your class. Fields marked with @NonNull result in null checks on those parameters.@RequiredArgsConstructor
generates a constructor with 1 parameter for each field that requires special handling.
IDE Plugins
Install Lombok plugin in IntelliJ Idea or Eclipse to start using the awesome features it provides.
Replace application.properties with application.yml
YAML
is a superset of JSON
, and as such is a very convenient format for specifying hierarchical configuration data. The SpringApplication
class will automatically support YAML
as an alternative to properties file when SnakeYAML is added as dependency in classpath.
Replace application.properties
with application.yml
under src/main/resources
.
Add SnakeYAML
as dependency in pom.xml.
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.25</version>
</dependency>
Adding additional dependencies to maven
Apart from the dependencies that are included in pom.xml, include the below ones which will be used for our implementation.
Bean Validation API & Hibernate Validator
Validating input passed in the request is very basic need in any implementation. There is a de-facto standard for this kind of validation handling defined in JSR 380
.
JSR 380 is a specification of the Java API for bean validation which ensures that the properties of a bean meet specific criteria, using annotations such as @NotNull, @Min, and @Max.
Bean Validation 2.0
is leveraging the new language features and API additions of Java 8
for the purposes of validation by supporting annotations for new types like Optional and LocalDate.
Hibernate Validator
is the reference implementation of the validation API. This should be included along side of validation-api
dependency which contains the standard validation APIs.
Below are the dependencies that need to be included
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.2.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>6.1.2.Final</version>
</dependency>
Spring Boot Actuator
Spring Boot Actuator will be included in the project archive that is generated using Spring Initializr. Actuator brings production-ready features to our application.
The main features that will be added to our API are
/health
endpoint
The health
endpoint is used to check the health or state of the application that is running. This endpoint is generally configured with some monitoring tools to notify us if the instance is running as expected or goes down or behaving unusual for any particular reasons like Connectivity issues with Database, lack of disk space, etc.,
Below is the default response that would showup upon accessing /actuator/health
endpoint if application is started successfully
{
"status" : "UP"
}
HealthIndicator
is the interface which is used to collect the health information from all the implementing beans. Custom health indicator can be implemented to expose additional information.
/info
endpoint
The info
endpoint will display the information of the API based upon the configurations defined in properties or yaml file. Below is the information that is configured for this API and the response that would showup upon accessing /actuator/info
endpoint.
# Catalogue Management Service Restful APIs
info:
app:
name: Spring Sample Application
description: This is my first spring boot application
version: 1.0.0
{
"app": {
"name": "Spring Sample Application",
"description": "This is my first spring boot application",
"version": "1.0.0"
}
}
/metrics
endpoint
The metrics
endpoint publishes information about OS, JVM as well as application level metrics.
By default, only health
and info
endpoints are enabled. For metrics to work, the below configuration should be added to application.yml.
# Spring boot actuator configurations
management:
endpoints:
web:
exposure:
include: health, info, metrics
Accessing /actuator/metrics/
will list down all the available metrics as shown below that can be queries through actuator.
{
"names": [
"jvm.threads.states",
"jdbc.connections.active",
"jvm.gc.memory.promoted",
"jvm.memory.max",
"jvm.memory.used",
"jvm.gc.max.data.size",
"jdbc.connections.max",
....
....
....
]
}
To query a specific metric, access /actuator/metrics/{metric-name}
replacing metric-name with one that is available in the list of metrics. Below is sample response retrieved when accessing /actuator/metrics/jvm.memory.used
{
"name": "jvm.memory.used",
"description": "The amount of used memory",
"baseUnit": "bytes",
"measurements": [
{
"statistic": "VALUE",
"value": 1.18750416E8
}
],
"availableTags": [
{
"tag": "area",
"values": [
"heap",
"nonheap"
]
},
{
"tag": "id",
"values": [
"G1 Old Gen",
"CodeHeap 'non-profiled nmethods'",
"G1 Survivor Space",
"Compressed Class Space",
"Metaspace",
"G1 Eden Space",
"CodeHeap 'non-nmethods'"
]
}
]
}
And many more..
Likewise to health
, info
, metrics
, there are many more endpoints when additional capabilities are added like sessions
, liquibase
, flyway
etc.,
Click here for the complete list of endpoints that provided to monitor and interact with the application.
Updating Maven and Gradle files
Below is pom.xml and build.gradle defined with all the dependencies and plugins needed for this application.
Maven
<?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.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.toomuch2learn.springboot2</groupId>
<artifactId>catalogue-crud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>crud-catalogue</name>
<description>Restful APIs for Managing Catalogue Management System</description>
<properties>
<java.version>1.8</java.version>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.25</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.2.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>6.1.2.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
Gradle
/*
* This file was generated by the Gradle 'init' task.
*/
plugins {
id 'java'
id 'maven-publish'
id "io.freefair.lombok" version "5.0.0-rc2"
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'jacoco'
}
repositories {
mavenLocal()
maven {
url = 'https://repo.maven.apache.org/maven2'
}
}
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
}
bootRun {
sourceResources sourceSets.main
}
jacocoTestReport {
reports {
html.destination file("${buildDir}/jacocoHtml")
}
}
dependencies {
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.projectlombok:lombok:1.18.12'
implementation 'org.yaml:snakeyaml:1.25'
implementation 'javax.validation:validation-api:2.0.1.Final'
implementation 'org.hibernate.validator:hibernate-validator:6.1.2.Final'
implementation 'org.hibernate.validator:hibernate-validator-annotation-processor:6.1.2.Final'
runtimeOnly 'com.h2database:h2:1.4.200'
runtimeOnly 'org.springframework.boot:spring-boot-devtools:2.2.4.RELEASE'
testImplementation('org.springframework.boot:spring-boot-starter-test:2.2.4.RELEASE') {
exclude group: 'junit', module: 'junit'
}
testImplementation 'org.junit.jupiter:junit-jupiter:5.6.0'
}
test {
useJUnitPlatform()
}
group = 'com.toomuch2learn.springboot2'
version = '0.0.1-SNAPSHOT'
description = 'crud-catalogue'
sourceCompatibility = '1.8'
publishing {
publications {
maven(MavenPublication) {
from(components.java)
}
}
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
Configure H2 database and define JPA Entity
Update application.yml with the below h2 database and JPA dialect configuration
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:file:~/cataloguedb
username: sa
password:
driverClassName: org.h2.Driver
jpa:
database-platform: org.hibernate.dialect.H2Dialect
Define JPA entity as per the below table definition for CATALOGUE_ITEMS
:
Column | Datatype | Nullable |
---|---|---|
ID | INT PRIMARY KEY | No |
SKU_NUMBER | VARCHAR(16) | No |
ITEM_NAME | VARCHAR(255) | No |
DESCRIPTION | VARCHAR(500) | No |
CATEGORY | VARCHAR(255) | No |
PRICE | DOUBLE | No |
INVENTORY | INT | No |
CREATED_ON | DATETIME | No |
UPDATED_ON | DATETIME | Yes |
package com.toomuch2learn.springboot2.crud.catalogue.model;
import com.toomuch2learn.springboot2.crud.catalogue.validation.IEnumValidator;
import lombok.*;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor(staticName = "of")
@Entity
@Table(name = "CATALOGUE_ITEMS",
uniqueConstraints = {
@UniqueConstraint(columnNames = "SKU_NUMBER")
})
public class CatalogueItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID", unique = true, nullable = false)
private Long id;
@NotEmpty(message = "SKU cannot be null or empty")
@NonNull
@Column(name = "SKU_NUMBER", unique = true, nullable = false, length = 16)
private String sku;
@NotEmpty(message = "Name cannot be null or empty")
@NonNull
@Column(name = "ITEM_NAME", unique = true, nullable = false, length = 255)
private String name;
@NotEmpty(message = "Description cannot be null or empty")
@NonNull
@Column(name = "DESCRIPTION", nullable = false, length = 500)
private String description;
@NonNull
@Column(name = "CATEGORY", nullable = false)
@IEnumValidator(
enumClazz = Category.class,
message = "Invalid category provided"
)
private String category;
@NotNull(message = "Price cannot be null or empty")
@NonNull
@Column(name = "PRICE", nullable = false, precision = 10, scale = 2)
private Double price;
@NotNull(message = "Inventory cannot be null or empty")
@NonNull
@Column(name = "INVENTORY", nullable = false)
private Integer inventory;
@NonNull
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATED_ON", nullable = false, length = 19)
private Date createdOn;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "UPDATED_ON", nullable = true, length = 19)
private Date updatedOn;
}
Spring Data JPA Repository
Java Persistence API
a.k.a JPA
handles most of the complexity of JDBC-based database access and object-relational mappings. On top of that, Spring Data JPA
reduces the amount of boilerplate code required by JPA which makes the implementation of our persistence layer easier and faster.
JPA
is a specification that defines an API for object-relational mappings and for managing persistent objects. Hibernate
and EclipseLink
are two most popular implementations of JPA specification.
Spring Data JPA
supports JPA specification allowing us to define the entities and association mappings, the entity lifecycle management, and JPA’s query capabilities. Spring Data JPA
adds an additional layer on top of JPA by providing no-code
implementation of a Repository Interface
which defines the repository with all logical read and write operations for a specific entity.
What to define in a Repository Interface?
Repository Interface should at minimum define the below 4 methods:
- Save a new or updated Entity
- Delete an entity,
- Find an entity by its Primary Key
- Find an entity by its title.
These operations are basically related to CRUD functions for managing an entity. Additional to these, we can further enhance the interface by defining methods to fetch data by pagination, sorting, count etc.,
Spring Data JpaRepository
includes all these capabilities and automatically create an implementation for them helping us to remove the DAO implementations entirely.
In this article, we will create CatalogueRepository
which extends JpaRepository
as below
package com.toomuch2learn.springboot2.crud.catalogue.repository;
import com.toomuch2learn.springboot2.crud.catalogue.model.CatalogueItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface CatalogueRepository extends JpaRepository<CatalogueItem, Long> {
Optional<CatalogueItem> findBySku(String sku);
}
As observed above, we defined additional method to fetch Catalogue Item by its SKU as per the requirement by following the syntax defined for Query methods. This provides the capability to derive the query from the method name directly.
Note:
JpaRepository
contains the full API of CrudRepository
and PagingAndSortingRepository
which provides CRUD methods and methods to do pagination and sort records. If we don’t need the full functionality provided by JpaRepository or PagingAndSortingRepository, we can just implement CrudRepository.
Spring Rest Controller
Spring provided @RestController
annotation which will simply to create Controller classes for handling Restful requests.
Spring’s @RestController` is annotated with @Controller and @ResponseBody. This will eliminates the need to annotate every request handling method in the controller class with the @ResponseBody annotation and ensuring every request handling method of the controller class automatically serialize the return objects into HttpResponse.
Below is part of the CatalogueController
portraying the class and methods with annotations to handle Restful request to fetch Catalogue Item by SKU Number.
@RestController
@RequestMapping("/api/v1")
public class CatalogueController {
@Autowired
private CatalogueCrudService catalogueCrudService;
@GetMapping("/{sku}")
@ResponseStatus(value = HttpStatus.OK)
public CatalogueItem
getCatalogueItemBySKU(@PathVariable(value = "sku") String skuNumber)
throws ResourceNotFoundException {
return catalogueCrudService.getCatalogueItem(skuNumber);
}
@PostMapping(CatalogueControllerAPIPaths.CREATE)
public ResponseEntity<ResourceIdentity> addCatalogueItem(@Valid @RequestBody CatalogueItem catalogueItem) {
Long id = catalogueCrudService.addCatalogItem(catalogueItem);
return new ResponseEntity<>(new ResourceIdentity(id), HttpStatus.CREATED) ;
}
}
As observed above,
- Additional Service layer is Introduced with class
CatalogueCrudService
which abstracts calls to data access layers from the controller class. - Handler methods are annotated with @ResponseStatus which marks a method or exception class with the status code() and reason() that should be returned.
- Paths are defined as static variables in
CatalogueControllerAPIPaths
and are used in Controller class. This will ensure all paths are defined at one place and clearly indicate what operations are available in the controller class instead of moving around the class up and down. @Valid
annotation is used inaddCatalogueItem
ensuring the request body received is validated by Bean Validation Framework before processing the request.addCatalogueItem
is defined by returning ResponseEntity instead of annotating with@ResponseStatus
. When creating instance ofResponseEntity
status of the response can be included part of it.
Below is the complete implementation of the Controller class:
package com.toomuch2learn.springboot2.crud.catalogue.controller;
public class CatalogueControllerAPIPaths {
public static final String BASE_PATH = "/api/v1";
public static final String CREATE = "/";
public static final String GET_ITEMS = "/";
public static final String GET_ITEM = "/{sku}";
public static final String UPDATE = "/{sku}";
public static final String DELETE = "/{sku}";
public static final String UPLOAD_IMAGE = "/{sku}/image";
}
package com.toomuch2learn.springboot2.crud.catalogue.controller;
import com.toomuch2learn.springboot2.crud.catalogue.exception.FileStorageException;
import com.toomuch2learn.springboot2.crud.catalogue.exception.ResourceNotFoundException;
import com.toomuch2learn.springboot2.crud.catalogue.model.CatalogueItem;
import com.toomuch2learn.springboot2.crud.catalogue.model.CatalogueItemList;
import com.toomuch2learn.springboot2.crud.catalogue.model.ResourceIdentity;
import com.toomuch2learn.springboot2.crud.catalogue.service.CatalogueCrudService;
import com.toomuch2learn.springboot2.crud.catalogue.service.FileStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping(CatalogueControllerAPIPaths.BASE_PATH)
public class CatalogueController {
@Autowired
private FileStorageService fileStorageService;
@Autowired
private CatalogueCrudService catalogueCrudService;
@GetMapping(CatalogueControllerAPIPaths.GET_ITEMS)
@ResponseStatus(value = HttpStatus.OK)
public CatalogueItemList getCatalogueItems() {
return new CatalogueItemList(catalogueCrudService.getCatalogueItems());
}
@GetMapping(CatalogueControllerAPIPaths.GET_ITEM)
public CatalogueItem
getCatalogueItemBySKU(@PathVariable(value = "sku") String skuNumber)
throws ResourceNotFoundException {
return catalogueCrudService.getCatalogueItem(skuNumber);
}
@PostMapping(CatalogueControllerAPIPaths.CREATE)
@ResponseStatus(value = HttpStatus.CREATED)
public ResponseEntity<ResourceIdentity> addCatalogueItem(@Valid @RequestBody CatalogueItem catalogueItem) {
Long id = catalogueCrudService.addCatalogItem(catalogueItem);
return new ResponseEntity<>(new ResourceIdentity(id), HttpStatus.CREATED) ;
}
@PutMapping(CatalogueControllerAPIPaths.UPDATE)
@ResponseStatus(value = HttpStatus.OK)
public void updateCatalogueItem(
@PathVariable(value = "sku") String skuNumber,
@Valid @RequestBody CatalogueItem catalogueItem) throws ResourceNotFoundException {
catalogueCrudService.updateCatalogueItem(catalogueItem);
}
@DeleteMapping(CatalogueControllerAPIPaths.DELETE)
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void removeCatalogItem(@PathVariable(value = "sku") String skuNumber)
throws ResourceNotFoundException {
catalogueCrudService.deleteCatalogueItem(catalogueCrudService.getCatalogueItem(skuNumber));
}
@PostMapping(CatalogueControllerAPIPaths.UPLOAD_IMAGE)
@ResponseStatus(value = HttpStatus.CREATED)
public void uploadCatalogueItemImage(
@PathVariable(value = "sku") String skuNumber,
@RequestParam("file") MultipartFile file)
throws ResourceNotFoundException, FileStorageException {
catalogueCrudService.getCatalogueItem(skuNumber);
fileStorageService.storeFile(file);
}
}
Handling Exceptions
Spring’s @ControllerAdvice allows us to handle exceptions across the whole application not limiting just a single controller.
@ControllerAdvice
will apply to all classes that use the @Controller
or @RestController
providing unified and centralized error handling logic reducing duplicate code and keep our code clean. The app can throw exception normally to indicate a failure of any kind which will then be handled separately following separation of concerns principals.
Apart of the exceptions thrown by Spring, Spring Data and Bean Validation Framework, Custom Exceptions are implemented for handling runtime exceptions.
Below are the exceptions which will be handled by the unified exception handler controller class:
- NoHandlerFoundException.class - When there is no controller method available for the requested handler, then this exception will be thrown when `throw-exception-if-no-handler-found` is set to `true`.
# If there is no handler found for the request path, then throw error
mvc:
throw-exception-if-no-handler-found: true
- ResourceNotFoundException.class - Exception to indicate that the requested resource is not found. This exception is thrown when there is no Catalogue Item available for the requested SKU number. Below is the service class method throwing this exception when no catalogue item can be found.
public CatalogueItem getCatalogueItem( String skuNumber) throws ResourceNotFoundException {
return getCatalogueItemBySku(skuNumber);
}
private CatalogueItem getCatalogueItemBySku(String skuNumber) throws ResourceNotFoundException {
CatalogueItem catalogueItem = catalogueRepository.findBySku(skuNumber)
.orElseThrow(() -> new ResourceNotFoundException(
String.format("Catalogue Item not found for the provided SKU :: %s" , skuNumber)));
return catalogueItem;
}
- MethodArgumentNotValidException.class - Exception to be thrown when validation on an argument annotated with `@Valid` fails.
- HttpMessageConversionException.class - Thrown by HttpMessageConverter implementations when a conversion attempt fails. This generally happens when the JSON request body is not inline to the defined `@RequestBody` class.
@PostMapping(CatalogueControllerAPIPaths.CREATE)
@ResponseStatus(value = HttpStatus.CREATED)
public ResponseEntity<ResourceIdentity> addCatalogueItem(@Valid @RequestBody CatalogueItem catalogueItem) {
Long id = catalogueCrudService.addCatalogItem(catalogueItem);
return new ResponseEntity<>(new ResourceIdentity(id), HttpStatus.CREATED) ;
}
FileStorageException.class - This is custom class to handle any wrap the runtime exceptions occurred when saving the file to storage that is uploaded.
- RuntimeException.class - Any other runtime exception occurred while processing the request.
Below is ExceptionHandlerController.java
annotated with @ControllerAdvice
for unified approach of handling the above exceptions.
package com.toomuch2learn.springboot2.crud.catalogue.exception;
import com.toomuch2learn.springboot2.crud.catalogue.utils.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
@ControllerAdvice
public class ExceptionHandlerController {
Logger log = LoggerFactory.getLogger(ExceptionHandlerController.class);
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND)
@ResponseBody
public ErrorResponse onNoHandlerFound(NoHandlerFoundException exception, WebRequest request) {
log.error(String.format("Handler %s not found", request.getDescription(false)));
ErrorResponse response = new ErrorResponse();
response.getErrors().add(
new Error(
ErrorCodes.ERR_HANDLER_NOT_FOUND,
"Handler not found",
String.format("Handler %s not found",request.getDescription(false))));
return response;
}
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND)
@ResponseBody
public ErrorResponse onResourceFound(ResourceNotFoundException exception, WebRequest request) {
log.error(String.format("No resource found exception occurred: %s ", exception.getMessage()));
ErrorResponse response = new ErrorResponse();
response.getErrors().add(
new Error(
ErrorCodes.ERR_RESOURCE_NOT_FOUND,
"Resource not found",
exception.getMessage()));
return response;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
ErrorResponse error = new ErrorResponse();
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
error.getErrors().add(
new Error(
ErrorCodes.ERR_REQUEST_PARAMS_BODY_VALIDATION_FAILED,
fieldError.getField(),
fieldError.getDefaultMessage()));
}
log.error(String.format("Validation exception occurred: %s", CommonUtils.convertObjectToJsonString(error)));
return error;
}
@ExceptionHandler(HttpMessageConversionException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorResponse onInvalidRequest(HttpMessageConversionException e) {
log.error("Invalid request received", e);
ErrorResponse error = new ErrorResponse();
error.getErrors().add(
new Error(
ErrorCodes.ERR_REQUEST_PARAMS_BODY_VALIDATION_FAILED,
"Invalid Request",
"Invalid request body. Please verify the request and try again !!"
)
);
return error;
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorResponse onConstraintValidationException(ConstraintViolationException e) {
log.error("Constraint validation exception occurred", e);
ErrorResponse error = new ErrorResponse();
for (ConstraintViolation violation : e.getConstraintViolations()) {
error.getErrors().add(
new Error(
ErrorCodes.ERR_CONSTRAINT_CHECK_FAILED,
violation.getPropertyPath().toString(),
violation.getMessage()));
}
return error;
}
@ExceptionHandler(FileStorageException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponse onFileStorageException(FileStorageException e) {
log.error("Error occurred while uploading file", e);
ErrorResponse error = new ErrorResponse();
error.getErrors().add(
new Error(
ErrorCodes.ERR_RUNTIME,
"Error occurred while uploading the file",
e.getMessage()
)
);
return error;
}
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponse onRuntimeException(RuntimeException e) {
log.error("Error occurred while handling request", e);
ErrorResponse error = new ErrorResponse();
error.getErrors().add(
new Error(
ErrorCodes.ERR_RUNTIME,
"Internal Server Error",
"Error occurred while processing your request. Please try again !!"
)
);
return error;
}
}
Implement Unit & Integration tests
Unit & Integration tests are implemented using Junit5
which is composed of several different modules from three different sub-projects.
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
spring-boot-starter-test
contains the majority of elements required for implementing the tests along side with Junit5
by adding annotations such as @SpringBootTest
, @AutoConfigureMockMvc
, @DataJpaTest
. Combined with Mockito
, dependent classes can be mocked easily.
Below are the test classes that are implemented part of this project.
- CatalogueCRUDIntegrationTest
Annotating the class with @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
will start the application on a random port and the Restful APIs will be accessed via TestRestTemplate which is convenient alternative of RestTemplate that is suitable for integration tests.
Being an Integration test and no mocking involved, order of executing the tests is necessary to test a specific sequential flow i.e., Add Catalogue Item -> Get Items -> Update Item -> Get Item -> Delete Item. To get this working we annotate the test class with @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
and annotate the test methods with @Order(INT)
- CatalogueControllerTest
Unit testing the controller class is tricky due to different annotations added to handler methods such as @GetMapping
, @PostMapping
, @RequestBody
.
Adding @SpringBootTest
annotation to the test class will bootstrap the entire container which will operate in a mock servlet environment and accessing the Restful APIs with injected MockMvc.
- CatalogueRepositoryTest
Unit testing Spring Data JPA Repository can be achieved by annotating the test class with @DataJpaTest.
@DataJpaTest
provides some standard setup needed for testing the persistence layer:
- Configuring H2, an in-memory database for all database operations.
- Setting Hibernate, Spring Data, and the DataSource
- Performing @EntityScan to load all entities defined in the application
- Turning on SQL logging
Autowiring repository class will ensure all the above setup is done and all db operations happen in H2 in-memory database.
- CatalogueCrudServiceTest
CatalogueCrudService
is a mere service class mediating requests coming to the controller to the repository. Hence, there is no need for any specific annotations added to implement test methods. We can mock the dependent classes using Mockito
and use Mocito’s when()
and given()
to stub the methods accordingly.
Collecting Code Coverage Metrics
JaCoCo is a free code coverage library for Java which is widely used to capture the code coverage metrics during tests execution.JaCoCo can be configured with Maven & Gradle builds which generate the coverage reports. Below are the configurations that should be done:
<build>
<plugins>
<plugin>
....
....
</plugin>
<!-- Add JaCoCo plugin which is prepare agent and also generate report once test phase is completed-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
plugins {
....
....
....
id 'jacoco'
}
/* Configure where the report should be generated*/
jacocoTestReport {
reports {
html.destination file("${buildDir}/jacocoHtml")
}
}
For Maven, running mvn clean package
will execute the tests and also generate the report. But for Gradle, we need to pass additional task along with build task to generate the report gradle clean build jacocoTestReport
.
Below is the report that is generated from both Maven & Gradle and it matches irrespective of the build system used.
Note:
With Lombok
used in the project, it will cause problems with coverage metrics. Jacoco
can’t distinguish between Lombok’s generated code and the normal source code. As a result, the reported coverage rate drops unrealistically low.
To fix this, we need to create a file named lombok.config
in project directory’s root and set the following flag as below. This adds the annotation lombok.@Generated to the relevant methods, classes and fields. Jacoco is aware of this annotation and will ignore that annotated code.
lombok.addLombokGeneratedAnnotation = true
Below is the report generated without this configuration file added to the project. As observed, we see the coverage result drastically decreased.
Running the Spring Boot Application
There are couple of ways to run a Spring Boot Application. During development, the ideal one would be to run the main class which is annotated with SpringBootApplication
i.e, CrudCatalogueApplication.java in this project. And the other ways are running through maven or gradle.
Run the application using Maven
Use the below command to run the Spring Boot application using Maven
~:\> mvn clean spring-boot:run
Run the application using Gradle
Use the below command to run the Spring Boot application using Gradle
~:\> gradle clean bootRun
Run the application using java -jar command
To run the application using java -jar
command, we need to generate the package. Below are the maven and gradle command to generate the jar
for the spring boot application.
~:\> mvn clean package
~:\> java -jar target/catalogue-crud-0.0.1-SNAPSHOT.jar
~:\> gradle clean build
~:\> java -jar build/libs/catalogue-crud-0.0.1-SNAPSHOT.jar
Automatic Restart and Live Reloading
Applications that use spring-boot-devtools
dependency automatically restart whenever files on the classpath change. Below are the configurations to have this capability added to maven or gradle. This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. By default, any entry on the classpath that points to a folder is monitored for changes.
This dependency is already included in the project when initialized using Spring Initializr. If not, add the below dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
bootRun {
sourceResources sourceSets.main
}
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
}
dependencies {
developmentOnly 'org.springframework.boot:spring-boot-devtools'
....
....
....
}
In Eclipse, spring-boot-devtools
will magically enable hot swapping of Java class changes and static file reload.
For IntelliJ IDE, additional steps are needed to enable it as below:
Enable check-box
Build project automatically
from File->Setting –> Build, Execution, Deployment –> Compiler.Press SHIFT+CTRL+A for Linux/Windows users or Command+SHIFT+A for Mac users, then type registry in the opened pop-up window. Scroll down to Registry using the down arrow key and hit ENTER on Registry. In the Registry window verify the option
compiler.automake.allow.when.app.running
is checked.
Note: If you start your Spring Boot app with java -jar
, the Hot Swap will not work even if we add spring-boot-devtools
dependency.
Whats under the hood with Restart?
Under the hood, Spring DevTools use two classloaders - base
and restart
. Classes which do not change are loaded by the base classloader. Classes we are working on are loaded by restart classloader. Whenever a restart is triggered, restart classloader is discarded and recreated. This way restarting your application is much faster than usual.
Disable restart If Needed
if we need to temporary disable the restart feature, we can set spring.devtools.restart.enabled
property to false in the application.properties or application.yml file in our project.
spring:
devtools:
restart:
enabled: false
Disable LiveReload if needed
if we need to temporary disable the LiveReload feature, we can set spring.devtools.livereload.enabled
property to false in the application.properties or application.yml file in our project.
spring:
devtools:
livereload:
enabled: false
Logging changes in condition evaluation
By default, each time our application restarts, a report showing the condition evaluation delta is logged. The report shows the changes to our application’s auto-configuration as we make changes such as adding or removing beans and setting configuration properties.
To disable the logging of the report, set the following property:
spring:
devtools:
restart:
log-condition-evaluation-delta: false
Excluding Resources
Certain resources do not necessarily need to trigger a restart when they are changed. For example, Thymeleaf templates can be edited in-place. By default, changing resources in /META-INF/maven, /META-INF/resources, /resources, /static, /public, or /templates does not trigger a restart but does trigger a live reload. If we want to customize these exclusions, we can use the spring.devtools.restart.exclude property.
For example, to exclude only /static and /public you would set the following property:
spring:
devtools:
restart:
exclude: static/**,public/**
Application.yml
Below is the configuration that is used in this application which includes Application information, Datasource configuration, logging etc.,
# Catalogue Management Service Restful APIs
info:
app:
name: Spring Sample Application
description: This is my first spring boot application
version: 1.0.0
# Spring boot actuator configurations
management:
endpoints:
web:
exposure:
include: health, info, metrics
# Configure Logging
logging:
level:
root: ERROR
com.toomuch2learn: DEBUG
org.springframework.web: ERROR
org.hibernate: ERROR
com.zaxxer.hikari: ERROR
org.apache.catalina: ERROR
# Configure Spring specific properties
spring:
# Enable/Disable hot swapping
devtools:
restart:
enabled: true
log-condition-evaluation-delta: false
# If there is no handler found for the request path, then throw error
mvc:
throw-exception-if-no-handler-found: true
# multipart properties for file uploads
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 10MB
# Properties for configuring jackson mapper
jackson:
mapper:
# For enums, consider case insensitive when parsing to json object
accept-case-insensitive-enums: true
resources:
add-mappings: false
# Datasource Configurations
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:file:~/cataloguedb
username: sa
password:
driverClassName: org.h2.Driver
jpa:
database-platform: org.hibernate.dialect.H2Dialect
# Custom Configurations
file:
upload-location: E:\Projects\2much2learn\2much2learn_examples\catalogue-crud
Testing APIs via Postman
API testing tool Postman is one of the most popular tools available. The ease of Accessibility, creating environments & collections to persist test cases which validate the response status & body and Automated testing with Newman which is a command-line collection runner for Postman.
Below are the tests we execute to verify the application that is started. Ensure to add header Content-Type: application/json
which is needed for most of the tests.
⭐ Download and refer to complete Postman Collection for all the below tests.
Application Health
Spring Actuator exposes /health
endpoint which will expose the status of the application.
Http Method: GET - Request Url: http://localhost:8080/actuator/health
Application Information
Info section added in application.yml will be exposed by Actuator’s ‘/info’ endpoint
Http Method: GET - Request Url: http://localhost:8080/actuator/info
Application JVM Memory Used Metric Information
Spring Actuator exposes couple of metrics for the application. Below is the sample for fetching one such metric information
Http Method: GET - Request Url: http://localhost:8080/actuator/metrics/jvm.memory.used
Add Catalogue Item
Below are two postman requests which we will use to create Catalogue Items. One of the Catalogue item will be used to update it in the later tests.
Http Method: POST - Request Url: http://localhost:8080/api/v1/
{
"sku": "CTLG-123-0001",
"name": "The Avengers",
"description": "Marvel's The Avengers Movie",
"category": "Movies",
"price": 0.0,
"inventory": 0
}
Get Catalogue Items
Get Catalogue Items that are persisted by the requests.
Http Method: GET - Request Url: http://localhost:8080/api/v1/
Update Catalogue Item
Update one of the Catalogue Item by its SKU number.
Http Method: PUT - Request Url: http://localhost:8080/api/v1/{sku}
{
"sku": "CTLG-123-0001",
"name": "The Avengers",
"description": "Marvel's The Avengers Movie",
"category": "Movies",
"price": 95.99,
"inventory": 10
}
Get Catalogue Item by SKU
Get the updated Catalogue Item by its SKU. Verify if the fields that are updated compared to the add request is reflected in thus Get Request.
Http Method: GET - Request Url: http://localhost:8080/api/v1/{sku}
Delete Catalogue Item
Delete one of the Catalogue Item persisted earlier by its SKU.
Http Method: DELETE - Request Url: http://localhost:8080/api/v1/{sku}
Upload Catalog Item Image
Upload Image for Catalogue Item as multipart file using form-data.
Http Method: POST - Request Url: http://localhost:8080/api/v1/{sku}/image
Handler Not Found
Testing Handler not found exception by passing invalid url path.
Resource Not Found
Testing Resource not found exception by passing invalid SKU.
Validation Exception
Testing Validation exception by passing invalid request body.
Invalid Request
Testing Invalid Request when passing invalid data to price as below
Runtime Exception
When any Runtime Exception occurs, it is handled by returning back the below response. Detailed log of exception is also logged by the application for debugging purpose.
Gotchas
Initialize Gradle build from Maven build
Before proceeding further, ensure Gradle is setup and configured properly. Running the below command should display the version of Gradle that is configured.
~:\> gradle -v
------------------------------------------------------------
Gradle 6.1.1
------------------------------------------------------------
Build time: 2020-01-24 22:30:24 UTC
Revision: a8c3750babb99d1894378073499d6716a1a1fa5d
Kotlin: 1.3.61
Groovy: 2.5.8
Ant: Apache Ant(TM) version 1.10.7 compiled on September 1 2019
JVM: 1.8.0_232 ( 25.232-b09)
OS: Windows 10 10.0 amd64
Running command gradle init
from the root of your project will check if there is maven build already available. If yes, it will prompt with the below message to generate Gradle build.
~:\> gradle init --stacktrace
Found a Maven build. Generate a Gradle build from this? (default: yes) [yes, no]
Upon proceeding, Gradle initialization fails with NullPointerException. To resolve this, remove <repositories/>
and <pluginRepositories/>
in pom.xml and try again. This will initialize Gradle build by successfully generating build.gradle
and settings.gradle
next to pom.xml
.
Below changes need to be applied to generated build.gradle
to get it working without any errors
- Change
http
tohttps
for the default mavenurl
. - Configure Lombok Plugin which will ensure that the lombok dependency is added to compile your source code. Add the below line to the plugins section
id "io.freefair.lombok" version "5.0.0-rc2"
- Add the below to build.gradle to use
jUnitPlatform
for executing the test cases.
test {
useJUnitPlatform()
}
- Add Spring Boot Gradle plugin which will create executable archives (jar files and war files) that contain all of an application’s dependencies and can then be run with
java -jar
id 'org.springframework.boot' version '2.2.4.RELEASE'
- Add Spring Dependency Management plugin which will automatically import the
spring-boot-dependencies
bom and use Spring Boot version for all its dependencies. Post adding the below line to plugins, you can remove:2.2.4.RELEASE
wherever it is referred in build.gradle
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
To generate executable jar of the application and start the spring boot application, run the below commands in order.
~:\> gradle clean build
~:\> java -jar build\libs\catalogue-crud-0.0.1-SNAPSHOT.jar
Conclusion
Implementing Restful APIs using Spring Boot is like a breeze as most of the uplift is done by the framework and allowing us to focus on the business logic. With the kind of support available with Spring ecosystem, there is a tool available for us to choose based upon our need.
This article is long and extensive for sure. But this is the base for my future articles
- Restful API testing with BDD Approach
- Performing Load Tests using Gatling
- Building API Container Image using Jib
- Scale up/down API services using Docker Compose
- Deploying API Service to Kubernetes
And many more…