CRUD from Scratch

A Step by Step guide to create CRUD RESTful APIs using Spring Boot + Spring Data JPA with H2 in-memory database

2much2learn - A Step by Step guide to create CRUD RESTful APIs using Spring Boot + Spring Data JPA with H2 in-memory database
Clone the source code of the article from crud-rest-api-using-spring-boot-spring-data-jpa

Introduction

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 NamePathResponse
Status Code
POSTCreate Catalogue Item/201
(Created)
GETGet Catalogue Items/200
(Ok)
GETGet Catalogue Item/{sku}200
(Ok)
PUTUpdate Catalogue Item/{sku}200
(Ok)
DELETEDelete Catalogue Item/{sku}204
(No Content)
POSTUpload Catalog Item Picture/{sku}/image201
(Created)

Technology stack for implementing the Restful APIs...

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

Configure project details using Spring Initializr
Configure project details using Spring Initializr

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.

Import pom.xml
Import pom.xml

Import pom.xml as Project
Import pom.xml as Project

This should show the below project structure

Intellij Project Structure
Intellij 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

pom.xml
<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

Health endpoint default response
{
    "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.

application.yml
# Catalogue Management Service Restful APIs
info:
  app:
    name: Spring Sample Application
    description: This is my first spring boot application
    version: 1.0.0
Info endpoint response
{
    "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.

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.

Available metrics
{
  "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

Response for 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

pom.xml
<?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

build.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

application.yml
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:

ColumnDatatypeNullable
IDINT PRIMARY KEYNo
SKU_NUMBERVARCHAR(16)No
ITEM_NAMEVARCHAR(255)No
DESCRIPTIONVARCHAR(500)No
CATEGORYVARCHAR(255)No
PRICEDOUBLENo
INVENTORYINTNo
CREATED_ONDATETIMENo
UPDATED_ONDATETIMEYes
CatalogueItem.java
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

CatalogueRepository.java
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.

CatalogueController.java
@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 in addCatalogueItem 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 of ResponseEntity status of the response can be included part of it.

Below is the complete implementation of the Controller class:

CatalogueControllerAPIPaths.java
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";
}
CatalogueController.java
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`.
application.yml
# 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.
CatalogueCrudService.java

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;
}
CatalogueController.java
@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.

ExceptionHandlerController.java
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.

IntelliJ Unit & Integration Tests
IntelliJ Unit & Integration Tests

  • 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)

Catalogue Integration Test
Catalogue Integration Test

  • 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.

Catalogue Controller Tests
Catalogue Controller Tests

  • 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.

Catalogue Repository Tests
Catalogue Repository Tests

  • 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.

Catalogue Service Tests
Catalogue Service Tests

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:

pom.xml
<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>
build.gradle
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.

JaCoCo Coverage Report
JaCoCo Coverage Report

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.

JaCoCo Code coverage Lombok Issue
JaCoCo Code coverage Lombok Issue

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.

maven
~:\> mvn clean package

~:\> java -jar target/catalogue-crud-0.0.1-SNAPSHOT.jar 
gradle
~:\> 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.

maven
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
  <optional>true</optional>
</dependency>
gradle
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.

application.yml
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.

application.yml
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:

application.yml
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:

application.yml
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.,

application.yml
# 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 Health
Application 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 Information
Application Information

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

Application JVM Memory Used Metric Information
Application JVM Memory Used Metric Information

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/

Request Body
{
	"sku": "CTLG-123-0001",
	"name": "The Avengers",
	"description": "Marvel's The Avengers Movie",
	"category": "Movies",
	"price": 0.0,
	"inventory": 0
}

Create Catalogue Item
Create Catalogue Item

Create Catalogue Item
Create Catalogue Item

Get Catalogue Items

Get Catalogue Items that are persisted by the requests.

Http Method: GET - Request Url: http://localhost:8080/api/v1/

Get Catalogue Items
Get Catalogue Items

Update Catalogue Item

Update one of the Catalogue Item by its SKU number.

Http Method: PUT - Request Url: http://localhost:8080/api/v1/{sku}

Request Body
{
	"sku": "CTLG-123-0001",
	"name": "The Avengers",
	"description": "Marvel's The Avengers Movie",
	"category": "Movies",
	"price": 95.99,
	"inventory": 10
}

Update Catalogue Items
Update Catalogue Items

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}

Get Catalogue Item
Get Catalogue Item

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}

Delete Catalogue Items
Delete Catalogue Items

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

Upload Catalog Item Image
Upload Catalog Item Image

Handler Not Found

Testing Handler not found exception by passing invalid url path.

Handler Not Found
Handler Not Found

Resource Not Found

Testing Resource not found exception by passing invalid SKU.

Resource Not Found
Resource Not Found

Validation Exception

Testing Validation exception by passing invalid request body.

Validation Exception
Validation Exception

Invalid Request

Testing Invalid Request when passing invalid data to price as below

Invalid Request
Invalid Request

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.

Runtime Exception
Runtime Exception

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 to https for the default maven url.
  • 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
build.gradle
id "io.freefair.lombok" version "5.0.0-rc2"
  • Add the below to build.gradle to use jUnitPlatform for executing the test cases.
build.gradle
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
build.gradle
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
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…

Clone the source code of the article from crud-rest-api-using-spring-boot-spring-data-jpa
author

Madan Narra21 Posts

Software developer, Consultant & Architect

Madan is a software developer, writer, and ex-failed-startup co-founder. He has over 10+ years of experience building scalable and distributed systems using Java, JavaScript, Node.js. He writes about software design and architecture best practices with Java and is especially passionate about Microservices, API Development, Distributed Applications and Frontend Technologies.

  • Github
  • Linkedin
  • Facebook
  • Twitter
  • Instagram

Contents

Related Posts

Get The Best Of All Hands Delivered To Your Inbox

Subscribe to our newsletter and stay updated.