November 13, 2024

Spring Boot REST API Exception Handling Using @ControllerAdvice

In the article Spring Boot REST API, Data JPA, One-to-Many/Many-To-One Bidirectional Example we have seen how to create a Spring Boot REST API with Spring Data, JPA but one thing that is missing in that application is; exception handling. In this article we'll see how to handle exception in a Spring Boot REST application using @ControllerAdvice and @ExceptionHandler annotations.

If you don't handle exceptions that are thrown with in your application that results in stack trace being shown to the end user which is not considered a good practice. Here is an example of trying to get Customer data by passing an ID that doesn't exist.

Spring Boot Exception Handling

As you can see whole stack trace is shown to the end user which may not even make sense to the user. You should rather show a meaningful message to the user by handling the exceptions.

Exceptions in current application

The REST API which we developed throws a custom exception- ResourceNotFoundException in the methods where the resource should already exist. One such method is as given below.

@Override
public Customer getCustomerById(Long customerId) {
  Customer customer = customerRepository.findById(customerId)
                      .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given Id: " + customerId));
  return customer;
}

Application also throws RunTimeException where it converts a DB layer exception to a RunTimeException so the DB layer exception is not passed as it is to the presentation layer. One such method which is in CustomerController is as given below.

@PostMapping("/customer")
public ResponseEntity<Customer> createCustomer(@RequestBody Customer customer) {
  try {
    Customer savedCustomer = customerService.createCustomer(customer);
    final URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").build()
                .expand(savedCustomer.getCustomerId()).toUri();
    return ResponseEntity.created(location).body(savedCustomer);
  }catch(Exception e) {
    throw new RuntimeException("Error while creating customer " + e.getMessage());          
  }
}

Handling exception in Spring Boot REST application

In order to handle the application one strategy is to add method in each controller with @ExceptionHandler annotation to handle the exceptions that are thrown.

For example, in order to handle the RUnTimeException, you can add a method as given below in Customer Controller.

@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRunTimeException(Exception ex) {
  String message = "Error while processing request";
  ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNPROCESSABLE_ENTITY, message, ex.getMessage());
  return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.UNPROCESSABLE_ENTITY);
  
}

But the problem with this approach is this handling of exception is limited to the exception thrown in CustomerController. For the same type of exception in AccountController you will have to add the similar method in that controller and so on meaning having a lot of duplicate code doing the same task.

Handling exception using @ControllerAdvice

Using @ControllerAdvice annotation you can create a single component which handles exceptions globally. Which means you can write a single method, in this component, annotated with @ExceptionHandler and that will handle exception thrown in any Controller.

Creating global exception handler

In order to create a global exception handler, you need to create a class annotated with @ControllerAdvice annotation.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import com.knpcode.customer.dto.ErrorResponse;

@ControllerAdvice
public class UniversalExceptionHandler extends ResponseEntityExceptionHandler{
  
  @ExceptionHandler(value = ResourceNotFoundException.class)
  public ResponseEntity<ErrorResponse> handleResourceNotFoundException(Exception ex) {
    String message = "Error while processing request";
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND, message, ex.getMessage());
    return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.NOT_FOUND);
    
  }
  
  @ExceptionHandler(RuntimeException.class)
  public ResponseEntity<ErrorResponse> handleRunTimeException(Exception ex) {
    String message = "Error while processing request";
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNPROCESSABLE_ENTITY, message, ex.getMessage());
    return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.UNPROCESSABLE_ENTITY);
    
  }
}

Important points about this class-

  1. The component class extends ResponseEntityExceptionHandler which is an abstract class with method that handles all Spring MVC raised exceptions. You can extend it and then add extra methods to handle other exceptions.
  2. As you can see now you have to write a method only once which will work for any controller throwing the exception specified with the @ExceptionHandler annotation.
  3. You can specify more than one exception class with @ExceptionHandler annotation.
     @ExceptionHandler(value = {Exception1.class, Exception2.class})
    
    Then the method will handle all the exceptions specified with the @ExceptionHandler.
  4. In our component there are two separate methods on for handling ResourceNotFoundException and another for handling RuntimeException because the HttpStatus code which is sent is different.
  5. General practice is to create a class whose object is sent as the response body. Here that class is named ErrorResponse and it has the following fields-
    • statusCode- For HTTP status code
    • timestamp- Date and time when the exception is thrown
    • message- To store a generic user friendly message
    • exceptionMessage- To store exception message
    import java.time.LocalDateTime;
    import org.springframework.http.HttpStatus;
    import com.fasterxml.jackson.annotation.JsonFormat;
    
    public class ErrorResponse {
    	private HttpStatus statusCode;
    	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    	private LocalDateTime timestamp;
    	private String message;
    	private String exceptionMessage;
    	ErrorResponse() {}
    	public ErrorResponse(HttpStatus statusCode, String message, String exceptionMessage) {
    		this.statusCode = statusCode;
    		this.message = message;
    		this.exceptionMessage = exceptionMessage;
    		this.timestamp = LocalDateTime.now();
    	}
    	
    	public HttpStatus getStatusCode() {
    		return statusCode;
    	}
    	public void setStatus(HttpStatus statusCode) {
    		this.statusCode = statusCode;
    	}
    	public LocalDateTime getTimestamp() {
    		return timestamp;
    	}
    	public void setTimestamp(LocalDateTime timestamp) {
    		this.timestamp = timestamp;
    	}
    	public String getMessage() {
    		return message;
    	}
    	public void setMessage(String message) {
    		this.message = message;
    	}
    	public String getExceptionMessage() {
    		return exceptionMessage;
    	}
    	public void setExceptionMessage(String exceptionMessage) {
    		this.exceptionMessage = exceptionMessage;
    	}
    	
    }
    

Messages after exception handling

Trying to get customer data where ID doesn't exist.

Spring Boot @ControllerAdvice Example

Trying to insert another customer with the same mobile number. Note that mobile number column is kept unique in the DB.

Exception handling POST mapping

That's all for the topic Spring Boot REST API Exception Handling Using @ControllerAdvice. If something is missing or you have something to share about the topic please write a comment.


You may also like

November 4, 2024

Spring Boot REST API, Data JPA, One-to-Many/Many-To-One Bidirectional Example

In this tutorial we'll see how to create a Spring Boot REST API CRUD example with Spring data JPA (Hibernate) and entities having One-To-Many, Many-To-One bidirectional association. Database used is MySQL.

Database Tables

Tables used for the example are Customer and Account. Queries for creating tables are given below. A customer can have many accounts which means a One-To-Many association between Customer and Account. That is done by having the customer_id as foreign key in Account table.

I want the account number to start from 1000 so that is given as the initial value of auto_increment in Account table.

CREATE TABLE `customer` (
  `customer_id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `mobile_number` varchar(255) DEFAULT NULL,
  `address` varchar(255) DEFAULT NULL,
  `created_at` datetime(6) DEFAULT NULL,
  `created_by` varchar(255) DEFAULT NULL,
  `updated_on` datetime(6) DEFAULT NULL,
  `updated_by` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`customer_id`),
  UNIQUE KEY `UK5v8hijx47m783qo8i4sox2n5t` (`mobile_number`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `account` (
  `account_number` bigint NOT NULL AUTO_INCREMENT,
  `branch_address` varchar(255) DEFAULT NULL,
  `account_type` enum('CHECKING','SALARY','SAVINGS') DEFAULT NULL,
  `customer_id` bigint NOT NULL,
  `created_at` datetime(6) DEFAULT NULL,
  `created_by` varchar(255) DEFAULT NULL,
  `updated_on` datetime(6) DEFAULT NULL,
  `updated_by` varchar(255) DEFAULT NULL,  
  PRIMARY KEY (`account_number`),
  KEY `FKnnwpo0lfq4xai1rs6887sx02k` (`customer_id`),
  CONSTRAINT `FKnnwpo0lfq4xai1rs6887sx02k` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
DB Tables

Spring Boot REST API Example

In the Spring Boot project we'll create entity classes having fields that will map to DB Table columns. We'll create resources (Customer and Account) exposing REST endpoints to perform the following operations.

Customer

  • @PostMapping("/customer")- To create a new Customer
  • @GetMapping("/customer/{id}")- To fetch customer details for a passed customerId
  • @GetMapping("/customer")- To fetch customer details for a passed mobile number (mobile number is passed as query parameter)
  • @DeleteMapping("/customer/{id}")- To delete customer having the passed ID.
  • @PutMapping("/customer")- To update whole customer object
  • @PatchMapping("/customer")- For partial update (updating only the customer contact fields)

Account

  • @PostMapping("/account")- To create a new Account
  • @DeleteMapping("/account/{acctNo}")- To delete account having the passed ID.
  • @GetMapping("/account/{acctNo}")- Fetch account details for the passed account number.

Technologies Used

  • Spring Boot 3.x.x
  • Java 21
  • Spring Data JPA
  • Hibernate 6
  • MySQL 8
  • Maven
  • Spring Tool Suite (STS 4.x) is used as IDE.

Creating Spring Starter Project

In the STS select File - New - Spring Starter Project and provide the details for project name and packaging type. Please use the following image as reference.

Spring Starter Project

Click next and select the required starter dependencies.

  1. Spring Web- Needed to create a web, RESTful applications.
  2. Spring Data JPA- Need for Java Persistence API. With Spring Data JPA you won't have to write boiler plate code for CRUD operations, it will be generated by Spring framework. You just need to create an interface.
  3. Spring Boot Dev Tools- Provides fast application restarts, LiveReload, and configurations for enhanced development experience.

Click next and finish to create a Spring boot project.

In the generated pom.xml also add the dependency for MySQL driver.

<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <scope>runtime</scope>
</dependency>

Refer the generated pom.xml for better understanding.

<?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>3.3.5</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.knpcode</groupId>
  <artifactId>SpringBootProj</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>SpringBootProj</name>
  <description>Customer Service</description>
  <url/>
  <licenses>
    <license/>
  </licenses>
  <developers>
    <developer/>
  </developers>
  <scm>
    <connection/>
    <developerConnection/>
    <tag/>
    <url/>
  </scm>
  <properties>
    <java.version>21</java.version>
  </properties>
  <dependencies>
    <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-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

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

Configuring DB properties

Under src/main/resources create a file named application.yml. Note that application.properties file already exists once the project structure is created, this .properties file can also be used for providing configuration but these days YAML is preferred as it is more convenient to provide configuration for deployment. Note that YAML file uses indentation so always give space(s) when nesting.

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/knpcode?createDatabaseIfNotExist=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: DB_USER
    password: DB_PASSWORD

  application:
    name: customer-service

  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    hibernate:
      ddl-auto: update
    show-sql: true

Under spring.datasource DB related configurations like DB_URL, DB_user, DB_password are provided. Please change the URL, user and password as per your DB configuration.

Application name is given as customer-service.

Under jpa, dialect is given as org.hibernate.dialect.MySQLDialect so that generated SQL is optimized for MySQL DB.

ddl-auto: update is used for auto-generation of DB schema.

show-sql: true is used to show the generated SQL in the console.

Create Packages

In the generated project structure create the following packages to keep the source files in a structured way. As per my package names the packages which I have to create are-

com.knpcode.customer.controller
com.knpcode.customer.entity
com.knpcode.customer.repository
com.knpcode.customer.service
com.knpcode.customer.audit
com.knpcode.customer.dto
com.knpcode.customer.exception

Please put the classes in the appropriate package.

Entity classes

Under the com.knpcode.customer.entity package create classes Customer, Account and Base Entity. Generally in a project you do have tables with these columns created_by, created_at, updated_on, updated_by to capture user data for the same. When creating Entity classes general design is to create a BaseEntity class with these columns which can then be extended by other entity classes.

BaseEntity.java

import java.time.LocalDateTime;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
	@CreatedDate
	@Column(name = "created_at", updatable = false)
	private LocalDateTime createdAt;
	
	@CreatedBy
	@Column(name = "created_by", updatable = false)
	private String createdBy;
	
	@LastModifiedDate
	@Column(name = "updated_on", insertable = false)
	private LocalDateTime updatedOn;
	
	@LastModifiedBy
	@Column(name = "updated_by", insertable = false)
	private String updatedBy;
	
	public LocalDateTime getCreatedAt() {
		return createdAt;
	}
	public void setCreatedAt(LocalDateTime createdAt) {
		this.createdAt = createdAt;
	}
	public String getCreatedBy() {
		return createdBy;
	}
	public void setCreatedBy(String createdBy) {
		this.createdBy = createdBy;
	}
	public LocalDateTime getUpdatedOn() {
		return updatedOn;
	}
	public void setUpdatedOn(LocalDateTime updatedOn) {
		this.updatedOn = updatedOn;
	}
	public String getUpdatedBy() {
		return updatedBy;
	}
	public void setUpdatedBy(String updatedBy) {
		this.updatedBy = updatedBy;
	}
}

In the above class some of the important points are-

  1. Use of @MappedSuperclass annotation to indicate that this class would be inherited and it is not an entity class which maps to a DB table.
  2. Use of @LastModifiedDate, @LastModifiedBy, @CreatedDate, @CreatedBy annotations to configure JPA to persist values for these columns automatically. Note that with the @CreatedDate and @CreatedBy annotations updatable = false attribute is used because you don’t want to change the values of these columns at the time of update. Same way, with @LastModifiedDate and @LastModifiedBy annotations insertable = false attribute is used because these columns should have value only when row is updated not when the row is inserted.
  3. To ensure that JPA can automatically put values for these four columns you need some extra configuration. First thing is using the @EntityListeners(AuditingEntityListener.class) annotation to configure an entity listener to capture auditing information on persisting and updating entities.
  4. To provide information about the user (value that will be inserted in createdBy and updatedBy fields), you need to provide an implementation of AuditorAware interface and override getCurrentAuditor() method.

AuditorAware interface implementation

import java.util.Optional;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;

@Component
public class CustomAuditAware implements AuditorAware<String> {

  @Override
  public Optional<String> getCurrentAuditor() {
    return Optional.of("admin");
  }
}

Here the user name is hardcoded. In actual project you can get it from Security context.

You also need to use @EnableJpaAuditing annotation to enable auditing in JPA. That annotation can be used in the application class.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "customAuditAware")
public class SpringBootProjApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringBootProjApplication.class, args);
	}
}

Customer.java

import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;

@Entity
@Table(name = "customer")
public class Customer extends BaseEntity{
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "customer_id")
  private Long customerId;
  
  @Column(name = "name")
  private String name;
  
  private String email;
  
  @Column(name = "mobile_number", unique = true)
  private String mobileNumber;
  
  @Column(name = "address")
  private String address;
  
  @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)  
  private List<Account> accounts = new ArrayList<>();

  public List<Account> getAccounts() {
    return accounts;
  }

  public void setAccounts(List<Account> accounts) {
    this.accounts = accounts;
    for(Account account : accounts) {
      account.setCustomer(this);
      }
  }

  public Long getCustomerId() {
    return customerId;
  }

  public void setCustomerId(Long customerId) {
    this.customerId = customerId;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public String getMobileNumber() {
    return mobileNumber;
  }

  public void setMobileNumber(String mobileNumber) {
    this.mobileNumber = mobileNumber;
  }

  public String getAddress() {
    return address;
  }

  public void setAddress(String address) {
    this.address = address;
  }
  
}

In the above class some of the important points are-

  1. Since a customer can have many accounts, in Java class that means using a field of type List or Set. That is why List<Account> accounts field is there.
  2. Field accounts is annotated with @OneToMany annotation indicating the association that one customer may associate with many accounts. Cascade type is used as CascadeType.ALL which means all operations like merge, delete will be cascaded to associated entity. Though Fetch type is lazy by default for OnetoMany, still keeping it for readability purpose.
  3. In the setAccounts(List<Account> accounts) method customer is explicitly set for each account.
    	for(Account account : accounts) {
          account.setCustomer(this);	  
        }
    	

Account.java

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import com.fasterxml.jackson.annotation.JsonProperty;


@Entity
@Table(name = "account")
public class Account extends BaseEntity{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "account_number")	
	private Long accountNumber;
	
	@Enumerated(EnumType.STRING)
	@Column(name = "account_type")
	private AccountType accountType;
	@Column(name = "branch_address")
	private String branchAddress;
	
	@ManyToOne(fetch = FetchType.LAZY, optional = false)
	@JoinColumn(name = "customer_id")
	@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 
	private Customer customer;

	public Long getAccountNumber() {
		return accountNumber;
	}

	public void setAccountNumber(Long accountNumber) {
		this.accountNumber = accountNumber;
	}

	public AccountType getAccountType() {
		return accountType;
	}
	

	public void setAccountType(AccountType accountType) {
		this.accountType = accountType;
	}

	public String getBranchAddress() {
		return branchAddress;
	}

	public void setBranchAddress(String branchAddress) {
		this.branchAddress = branchAddress;
	}

	public Customer getCustomer() {
		return customer;
	}

	public void setCustomer(Customer customer) {
		this.customer = customer;
	}

	@Override
	public String toString() {
		return "Account [accountNumber=" + accountNumber + ", accountType=" + accountType + ", branchAddress="
				+ branchAddress + ", customer=" + customer + "]";
	}
}

In the above class some of the important points are-

  1. To make the association bi-directional @ManytoOne annotation is used here with the customer field. Use of @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) ensures that customer object is not created from JSON (deserialization process) otherwise the process may go into an infinite loop.
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "customer_id")
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private Customer customer;
    
  2. accountType field is annotated with @Enumerated(EnumType.STRING) which means this field needs an enum value and persists that value as String.

AccountType.java

This is the enum with 3 values for account types.

public enum AccountType {
	CHECKING,
	SAVINGS,
	SALARY
}

Controller Classes

Controller classes with the REST endpoints.

CustomerController.java

import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.knpcode.customer.dto.ResponseDto;
import com.knpcode.customer.entity.Customer;
import com.knpcode.customer.service.CustomerService;

@RestController
@RequestMapping("/api")
public class CustomerController {
  private final CustomerService customerService;
  
  public CustomerController(CustomerService customerService) {
    this.customerService = customerService;
  }
  
  @PostMapping("/customer")
  public ResponseEntity<Customer> createCustomer(@RequestBody Customer customer) {
      try {
          Customer savedCustomer = customerService.createCustomer(customer);
          final URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").build()
                    .expand(savedCustomer.getCustomerId()).toUri();
         
          return ResponseEntity.created(location).body(savedCustomer);
        }catch(Exception e) {
          throw new RuntimeException("Error while creating customer " + e.getMessage());          
        }
  }
  
  @GetMapping("/customer/{id}")
  public ResponseEntity<Customer> getCustomerById(@PathVariable Long id) {
    Customer customer = customerService.getCustomerById(id);
    return new ResponseEntity<>(customer, HttpStatus.OK);
  }
  
  @GetMapping("/customer")
  public ResponseEntity<Customer> getCustomerByMobileNumber(@RequestParam String mobileNumber) {
    Customer customer = customerService.getCustomerByMobileNumber(mobileNumber);
    return new ResponseEntity<>(customer, HttpStatus.OK);
    
  }
  
  @DeleteMapping("/customer/{id}")
  public ResponseEntity<ResponseDto> deleteCustomerById(@PathVariable Long id) {
    Customer customer = customerService.getCustomerById(id);
    customerService.deleteCustomerById(id);
    return ResponseEntity.ok(new ResponseDto(HttpStatus.OK, "Customer deleted successfully"));
  }
  
  @PutMapping("/customer")
  public ResponseEntity<Customer> updateCustomer(@RequestBody Customer customer) {
    Customer updatedCustomer = customerService.updateCustomer(customer);
    return new ResponseEntity<>(updatedCustomer, HttpStatus.OK);
  }
  
  @PatchMapping("/customer")
  public ResponseEntity<Customer> UpdateCustomerContactDeatails(@RequestBody Customer customer) {
    Customer updatedCustomer = customerService.updateCustomerContactDeatails(customer);
    return new ResponseEntity<>(updatedCustomer, HttpStatus.OK);
  }
}

In the above class some of the important points are-

  1. For mapping, nouns are used as per REST specification so for creating a resource, mapping is /api/customer whereas for getting or deleting a specific resource mapping is /api/customer/id where id is the customerId.
  2. When creating a resource, response which is sent back should have the location of the newly created resource sent as header and body may include the created object, along with the status code (which is 201 Created here).
  3. When deleting or fetching Customer data by passing a specific ID, you should also ensure that the ID which is sent by user actually exists. That validation is done in the Service class.
  4. There is a method getCustomerByMobileNumber() which needs mobile number to be sent as a query parameter. That means URL to access this endpoint should be something like this- localhost:8080/api/customer?mobileNumber=9888777650
  5. There is a method updateCustomer() which is annotated with PutMapping which means it will replace the whole object. For that validation is done initially to ensure that such a customer exists.
  6. In the method updateCustomerContactDeatails() which is annotated with @PatchMapping, partial update of email, address, mobileNumber fields is done.
  7. In all the methods appropriate response is sent back which contains status code and body. Body can be an object or a message of type ResponseDto.

AccountController.java

import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.knpcode.customer.dto.ResponseDto;
import com.knpcode.customer.entity.Account;
import com.knpcode.customer.entity.Customer;
import com.knpcode.customer.service.AccountService;
import com.knpcode.customer.service.CustomerService;

@RestController
@RequestMapping("/api")
public class AccountController {
  private final AccountService accountService;
  private final CustomerService customerService;
  AccountController(AccountService accountService, CustomerService customerService) {
    this.accountService = accountService;
    this.customerService = customerService;
  }
  @PostMapping("/account")
  public ResponseEntity<?> createAccount(@RequestBody Account account) {
    try {
      System.out.println(account.getCustomer().getCustomerId());
      Customer customer = customerService.getCustomerById(account.getCustomer().getCustomerId());
      // set the customer
      account.setCustomer(customer);
      Account savedAccount = accountService.createAccount(account);
      
      final URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                              .path("/{id}").build()
                              .expand(savedAccount.getAccountNumber()).toUri();      
      return ResponseEntity.created(location).body(savedAccount);
    } catch (Exception e) {
      e.printStackTrace();
      throw new RuntimeException("Error while creating Account " + e.getMessage());
    }
  }
  
  @DeleteMapping("/account/{acctNo}")
  public ResponseEntity<ResponseDto> deleteAccountByAccountNumber(@PathVariable Long acctNo) {
    accountService.deleteAccountByAccountNumber(acctNo);
    return ResponseEntity.ok(new ResponseDto(HttpStatus.OK, "Account deleted successfully"));
  }
  
  @GetMapping("/account/{acctNo}")
  public Account getAccountByAccountNumber(@PathVariable Long acctNo) {
    Account account=  accountService.getAccountByAccountNumber(acctNo);
    System.out.println(account.getCustomer().getName());
    return account;
  }
}

Service classes

Since Service layer should not be tightly coupled with Presentation layer so interfaces are created for the abstraction.

CustomerService.java

import com.knpcode.customer.entity.Customer;
public interface CustomerService {
	public Customer createCustomer(Customer customer);
	public Customer getCustomerById(Long customerId);
	public Customer getCustomerByMobileNumber(String mobileNumber);
	public void deleteCustomerById(Long customerId);
	public Customer updateCustomer(Customer customer);
	public Customer updateCustomerContactDeatails(Customer customer);
}

CustomerServiceImpl.java

import org.springframework.beans.factory.annotation.Autowired;
import com.knpcode.customer.entity.Customer;
import com.knpcode.customer.exception.ResourceNotFoundException;
import com.knpcode.customer.repository.CustomerRepository;
import org.springframework.stereotype.Service;

@Service
public class CustomerServiceImpl implements CustomerService {
  @Autowired
  CustomerRepository customerRepository;
  
  @Override
  public Customer createCustomer(Customer customer) {
    return customerRepository.save(customer);
  }

  @Override
  public Customer getCustomerById(Long customerId) {
    Customer customer = customerRepository.findById(customerId)
                        .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given Id: " + customerId));
    return customer;
  }
  
  @Override
  public Customer getCustomerByMobileNumber(String mobileNumber) {
    Customer customer = customerRepository.findByMobileNumber(mobileNumber)
          .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given mobileNumber: " + mobileNumber));
    return customer;
  }

  @Override
  public void deleteCustomerById(Long customerId) {
    customerRepository.deleteById(customerId);
    
  }
  
  @Override
  public Customer updateCustomer(Customer customer) {
    Customer dbCustomer = customerRepository.findById(customer.getCustomerId())
          .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given Id: " + customer.getCustomerId()));
    mapToCustomer(customer, dbCustomer);
    return customerRepository.save(dbCustomer);
  }
  
  private Customer mapToCustomer(Customer customer, Customer dbCustomer) {
    dbCustomer.setName(customer.getName());
    dbCustomer.setEmail(customer.getEmail());
    dbCustomer.setMobileNumber(customer.getMobileNumber());
    dbCustomer.setAddress(customer.getAddress());
    return dbCustomer;
  }
  
  /**
   * Method for partial update 
   * Update only Contact Details - email, address, mobileNumber
   */
  @Override
  public Customer updateCustomerContactDeatails(Customer customer) {
    Customer dbCustomer = customerRepository.findById(customer.getCustomerId())
          .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given Id: " + customer.getCustomerId()));
    if(customer.getMobileNumber() != null) {
      dbCustomer.setMobileNumber(customer.getMobileNumber());
    }
    if(customer.getAddress() != null) {
      dbCustomer.setAddress(customer.getAddress());
    }
    if(customer.getEmail() != null) {
      dbCustomer.setEmail(customer.getEmail());
    }
    return customerRepository.save(dbCustomer);
    
  }
}

AccountService.java

import com.knpcode.customer.entity.Account;
public interface AccountService {
	public Account createAccount(Account account);
	public Account getAccountByAccountNumber(Long acctNo);
	public void deleteAccountByAccountNumber(Long acctNo);
}

AccountServiceImpl.java

import org.springframework.stereotype.Service;
import com.knpcode.customer.entity.Account;
import com.knpcode.customer.exception.ResourceNotFoundException;
import com.knpcode.customer.repository.AccountRepository;

@Service
public class AccountServiceImpl implements AccountService {
  private final AccountRepository accountRepository;
  AccountServiceImpl(AccountRepository accountRepository){
    this.accountRepository = accountRepository;
  }
  @Override
  public Account createAccount(Account account) {
    return accountRepository.save(account);  
  }
  @Override
  public void deleteAccountByAccountNumber(Long acctNo) {
    accountRepository.findById(acctNo).orElseThrow(() -> new ResourceNotFoundException("No account associated with the given account number " + acctNo));
    accountRepository.deleteById(acctNo);
    
  }
  @Override
  public Account getAccountByAccountNumber(Long acctNo) {
    return accountRepository.findById(acctNo)
                .orElseThrow(() -> new ResourceNotFoundException("No account associated with the given account number " + acctNo));
    
  }
}

DTO classes

Data transfer object are POJOs that are used to transfer data among layers.

ResponseDto.java

import org.springframework.http.HttpStatus;

public class ResponseDto {
	private HttpStatus statusCode;
	private String message;
	
	public ResponseDto(HttpStatus statusCode, String message) {
		this.statusCode = statusCode;
		this.message = message;
	}
	
	public HttpStatus getStatusCode() {
		return statusCode;
	}
	public void setStatusCode(HttpStatus statusCode) {
		this.statusCode = statusCode;
	}
	public String getMessage() {
		return message;
	}
	public void setMessage(String message) {
		this.message = message;
	}
}

Custom Exception classes

A custom exception class is also used in the example which is used to throw an exception when resource is not found.

public class ResourceNotFoundException extends RuntimeException {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	public ResourceNotFoundException(String message) {
		super(message);
    }

}

Repository classes

Since we are using Spring Data JPA so there is no need to create classes with data access code. We just need to create interfaces by extending JpaRepository interface and passing entity for which data access code is needed and the type of the ID.

AccountRepository.java

import org.springframework.data.jpa.repository.JpaRepository;
import com.knpcode.customer.entity.Account;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
}

CustomerRepository.java

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.knpcode.customer.entity.Customer;
import org.springframework.stereotype.Repository;

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
  public Optional<Customer> findByMobileNumber(String mobileNumber);
}

In the CustomerRepository interface one method findByMobileNumber() is added, for this method also framework will create the data access code (including SQL) by parsing the method.

With that we have all the classes for the example and we can run the application class by right clicking it and choosing Run As - Spring Boot App. If there is no error then Tomcat server should start listening on the port 8080 (default port).

Creating Customer resource

Creating a customer object by passing customer and account information. Using Postman you can test the creation of Customer resource.

In the Postman, change the mapping to POST in the dropdown and select tab body - raw -JSON. URL is localhost:8080/api/customer

Paste the following as body and click Send.

{
    "name": "Ram",
    "email": "ram@ram.com",
    "mobileNumber": "9888777648",
    "address": "112, MG Road, Mumbai",
    "accounts":[{
        "accountType": "SAVINGS",
        "branchAddress": "MG Road, Mumbai"
    }, 
    {
        "accountType": "CHECKING",
        "branchAddress": "MG Road, Mumbai"
    }]
}
Spring Boot REST API

If you check in the STS console you should also be able to see the generated queries because of setting jpa.show-sql: true in the application.yml file.

Hibernate: insert into customer (address,created_at,created_by,email,mobile_number,name) values (?,?,?,?,?,?)
Hibernate: insert into account (account_type,branch_address,created_at,created_by,customer_id) values (?,?,?,?,?)
Hibernate: insert into account (account_type,branch_address,created_at,created_by,customer_id) values (?,?,?,?,?)

In the DB, you should have one entry in the Customer table and two entries with the same customer_id in Account table. Also verify the values in the created_by and created_at table which should be "admin" and current date and time. Columns updated_on and updated_by shouldn't have any value.

Creating Account resource

You can add account to the existing customer by sending Account info to the URL- localhost:8080/api/account

{
    "accountType": "SALARY",
    "branchAddress": "JP Nagar, Mumbai",
    "customer": {
        "customerId": 1
    }
}

That should add one more account to the existing Customer with ID as 1.

Get and Delete Customer

Same way you can check get and delete mapping by passing URL- localhost:8080/api/customer/1 where 1 is the ID part. Deleting customer resource should also delete the associated accounts because of the cascade property used in the entity mapping.

JPA One-to-Many/Many-To-One

Getting account

You can get specific account by passing the account number with the URL- localhost:8080/api/account/1003

One thing to note here is initially Hibernate gets only the account data (not the associated customer) because of the FetchType = lazy setting. This is the generated SQL.

select a1_0.account_number,a1_0.account_type,a1_0.branch_address,a1_0.created_at,a1_0.created_by,a1_0.customer_id,a1_0.updated_by,a1_0.updated_on from account a1_0 where a1_0.account_number=?

In the AccountController I have also tried to access the customer name, that triggers another query to get associated customer information.

@GetMapping("/account/{acctNo}")
public Account getAccountByAccountNumber(@PathVariable Long acctNo) {
  Account account=  accountService.getAccountByAccountNumber(acctNo);
  System.out.println(account.getCustomer().getName());
  return account;
}

You should see another Select query-

select c1_0.customer_id,c1_0.address,c1_0.created_at,c1_0.created_by,c1_0.email,c1_0.mobile_number,c1_0.name,c1_0.updated_by,c1_0.updated_on from customer c1_0 where c1_0.customer_id=?

Updating customer

You can update customer by using PUT mapping and the URL as- localhost:8080/api/customer

In the case of update using PUT, you need to send whole body even if there is change in mobileNumber and address fields only. That is required because PUT replaces the entire resource. If you don't send the whole customer object whatever is not sent is replaced by null.

{
    "customerId": 1,
    "name": "Ram",
    "email": "ram@ram.com",
    "mobileNumber": "9888777649",
    "address": "113, MG Road, Mumbai"
}

This is the generated query-

update customer set address=?,email=?,mobile_number=?,name=?,updated_by=?,updated_on=? where customer_id=?

If you verify in the DB table now you should see values for updated_on and updated_by columns apart from the updated values.

Partial update (Using PATCH)

Patch mapping has been used to update the contact details of the customer (email, address, mobile number). With Patch you can send only the fields that need to be updated not the whole object as required with PUT.

PATCH mapping Postman

That's all for the topic Spring Boot REST API, Data JPA, One-to-Many/Many-To-One Bidirectional Example. If something is missing or you have something to share about the topic please write a comment.


You may also like