Validate input JSON payload with SpringBoot AOP
Validate input JSON payload with SpringBoot AOP
In this blog will demonstrate an use case to use SpringBoot AOP to perform input validation in the @Controller
or @Service
layer level.
Pre-requisites
- Basic understanding of AOP concept like Point Cut, ProceedingJointPoint, Adivces - Around, Before , After, etc.
Use case - Validate payload in controller or service layer
- When building REST based application at circumstance we might need to perform customized validation on the JSON payload before passing request to the repository layer or further next layer.
Info
Defining JSON schema, is another possible option this might requires more coding effort.
The validation can be performed using the
hibernate-validator
, with built-in annotation@NotNull
,@NotEmpty
, etc. defined in the entity class.
- Create SpringBoot project using
start.spring.io
withspring web
andlombok
dependencies. - Add the Spring starter AOP dependencies in the
pom.xml
, like below
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
In this blog have included few additional points,
- How to easily create builder pattern on POJO using
@Build
annotation from Lombok project - Extending RuntimeException to create custom exception used to handle Input validation in this case
- Custom Spring annotation creation and registering it in AOP
@Around
advice which will be invoked and used to intercept the method calls java.utils.Objects
to validate null. Objects contains few other methods which is not detailed here- Java feature to perform
instanceof
check and directly store the converted data to variable// Code where the Object instanceof Customer and the converted data stored in customers if ( requestObject.length >0 && requestObject[0] instanceof Customer customers ){ // first object in the arguments if (Objects.isNull(customers)){ throw new InvalidInputException("Input invalid - Cannot be null"); } //.....
- How to easily create builder pattern on POJO using
Info:-
SpringBoot AOP only used to intercept methods. For advanced feature we need to use AspectJ, for example to check if fields have changed.
Code
Create Spring Annotation
- Create Spring Annotation, which will be configured in the Aspect
@Around
advice class.
package com.app.demo.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateInputRequest {
}
Create Aspect to intercept the method calls
- Below code defines the Aspect using
@Aspect
annotation (note, this aspect needs to be regiestered as a bean using@Component
). - The custom annotation
@ValidateInputRequest
in the previous code snippet is registered in the@Around
advice. - The Aspect class
validateInput()
method will be invoked if the custom annotation@ValidateInputRequest
is annotated on the method defined in@Controller
or@Service
layer.
package com.app.demo.config;
import com.app.demo.InvalidInputException;
import com.app.demo.dto.Customer;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
@Aspect
@Component
@Slf4j
public class ValidationAspectConfig {
//Using the Around advice, with the Annotation created and place the annotation
//over the method will invoke this call
@Around("@annotation(ValidateInputRequest)")
public Object validateInput(ProceedingJoinPoint pjp) throws Throwable {
log.info("AOP intercept for method - "+pjp.getSignature());
Object[] requestObject = pjp.getArgs();
if ( requestObject.length >0 &&requestObject[0] instanceof Customer customers ){
// first object in the arguments
if (Objects.isNull(customers)){
throw new InvalidInputException("Input invalid - Cannot be null");
}
//Performing validation for the input request intercepted and received
ValidateInput.validateRequest(customers);
}else{
log.info("input object is not of customer instance");
}
//just obtain the obj from the ProceedingJoinPoint
Object obj = pjp.proceed();
log.info("Completed executing the AOP intercept");
return obj;
}
}
Custom validation logic to validate input JSON payload
- Class where we perform the basic validation, to demonstrate basic validation
- Where we check if the input Customer JSON payload received contains name attribute and should NOT be empty.
- Also checking if the address attributes should be available.
package com.app.demo.config;
import com.app.demo.InvalidInputException;
import com.app.demo.dto.Customer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Component
public class ValidateInput {
public static void validateRequest(Customer customer){
if( Objects.isNull(customer.getName()) || customer.getName().isEmpty()){
throw new InvalidInputException("Invalid input - name cannot be null");
}
if( Objects.isNull(customer.getAddresses())
|| (Objects.nonNull(customer.getAddresses()) && customer.getAddresses().isEmpty())){
throw new InvalidInputException("Invalid input - address cannot be null");
}
}
}
Customer and Address POJO class
- Simple POJO class defining Customer related info
package com.app.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Address {
public String addressLine1;
public String addressLine2;
public String aptNo;
public String city;
public String state;
public long zipcode;
}
package com.app.demo.dto;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Customer {
public String name;
public long id;
public String department;
public List<Address> addresses;
}
Controller class
- A simple controller object
package com.app.demo.controller;
import java.util.List;
import com.app.demo.config.ValidateInputRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.app.demo.dto.Customer;
import com.app.demo.service.CustomerService;
@RestController
@RequestMapping("/api")
public class CustomerController {
@Autowired
private CustomerService customerService;
@GetMapping("/customer")
// the annotation can be used at controller method, commented for now
//@ValidateInputRequest
public ResponseEntity<List<Customer>> getCustomers(){
return ResponseEntity.ok(customerService.getAllCustomers());
}
@PostMapping("/customer")
//@ValidateInputRequest
public ResponseEntity<Customer> addCustomer(@RequestBody Customer customer){
return ResponseEntity.ok(customerService.addCustomer(customer));
}
}
Service class
- Service layer where we have included the custom annotation
@ValidateInputRequest
- Have used List of items to load when the application is successfully started
package com.app.demo.service;
import com.app.demo.config.ValidateInputRequest;
import com.app.demo.dto.Address;
import com.app.demo.dto.Customer;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CustomerService {
static List<Customer> inMemoryDb = new ArrayList<>();
public List<Customer> getAllCustomers() {
return fetchCustomers();
}
//The annotation which will be intercepted by AOP
@ValidateInputRequest
public Customer addCustomer(Customer customer) {
inMemoryDb.add(customer);
return customer;
}
protected List<Customer> fetchCustomers() {
return inMemoryDb;
}
static {
Address address1 = Address.builder()
.addressLine1("work adddress of customer1")
.city("city1").build();
Customer customer1 = Customer.builder()
.name("user1")
.addresses(List.of(address1))
.build();
Address address20 = Address.builder()
.addressLine1("work address customer2")
.city("city1").build();
Address address21 = Address.builder()
.addressLine1("home address customer2")
.city("city1").build();
Customer customer2 = Customer.builder()
.name("user2")
.addresses(List.of(address20, address21))
.build();
inMemoryDb.add(customer1);
inMemoryDb.add(customer2);
}
}
Custom Exception
- Creating custom Exception extending RuntimeException, used to throw this exception when validating the input JSON payload.
package com.app.demo;
public class InvalidInputException extends RuntimeException{
public InvalidInputException(String message){
super(message);
}
}
- SpringBoot Application entry point
package com.app.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Project structure snapshot
Output
Curl command to send an JSON payload with empty Customer name
- The output with the below response will trigger an exception in output
curl -X POST -H "Content-Type:application/json" http://localhost:8080/api/customer -d '{"name":"","id":0,"department":null,"addresses":[{"addressLine1":"work adddress of customer3","addressLine2":null,"aptNo":null,"city":"city1","state":null,"zipcode":0}]}'
Exception message
{"timestamp":"2022-10-23T04:42:35.931+00:00","status":500,"error":"Internal Server Error","trace":"com.app.demo.demo.InvalidInputException: Invalid input - name cannot be null\r\n\tat com.app.demo.demo.config.ValidateInput.validateRequest(ValidateInput.java:19)
com.app.demo.demo.config.ValidationAspectConfig.validateInput(ValidationAspectConfig.java:38)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:568)
org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634)
org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:624)
org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:72)
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
com.app.demo.demo.service.CustomerService$$EnhancerBySpringCGLIB$$e764f31c.addCustomer(<generated>)
com.app.demo.demo.controller.CustomerController.addCustomer(CustomerController.java:29)\r\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:568)
........
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789)
org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.base/java.lang.Thread.run(Thread.java:833)\r\n","message":"Invalid input - name cannot be null","path":"/api/customer"}