12 Spring Core/Spring Boot Interview Questions

MuraliMurali
20 min read

1. What is Spring Framework?

Spring Framework is an open-source application framework. We can also say that it is a lightweight inversion of control(IoC) container and aspect-oriented container framework for the Java platform. Spring handles the infrastructure so that we can focus on our application development. It was created by Rod Johnson. In 2003 Spring came into existence

product controller (UI layer) -> product service (Logic layer) -> product DAO (database code layer) -> DB, What happens if it's tightly coupled

if the ProductController, ProductService, ProductDAO, and the database are tightly coupled, it means that each component is directly dependent on the others. This can lead to several issues:

  1. Difficulty in Unit Testing: Each component cannot be tested independently as it relies on the other components. This makes unit testing challenging.

  2. Inflexibility: Changes in one component might require changes in all other components. For example, if you want to switch to a different database or change the way data is accessed, you might need to modify the ProductDAO, ProductService, and potentially even the ProductController.

  3. Hard to Maintain and Scale: As the application grows, tightly coupled architecture can become increasingly complex and hard to manage. It’s also more difficult to scale specific parts of the application independently.

  4. Less Reusability: Since the components are tightly bound to each other, reusing a component independently becomes difficult.

Dependency Injection (DI) is a technique to overcome these problems. With DI, we can inject dependencies (like ProductService into ProductController, or ProductDAO into ProductService) at runtime, making our components loosely coupled. This improves the flexibility and testability of our application. Here’s how it might look:

@Controller
public class ProductController {
    private ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
    //...
}

@Service
public class ProductService {
    private ProductDAO productDAO;

    @Autowired
    public ProductService(ProductDAO productDAO) {
        this.productDAO = productDAO;
    }
    //...
}

@Repository
public class ProductDAO {
    // DB related operations
}

In this example, ProductController doesn’t need to know about ProductDAO and the database. It only interacts with ProductService. Similarly, ProductService doesn’t need to know about ProductController, it only interacts with ProductDAO.


2. Why spring is lightweight?

Spring is considered lightweight compared to traditional Java EE applications. If we want to run a Java EE application, we can't just create a small application that will run on its own. We shall need a Java EE application server to run our application such as Glassfish, Wildfly, WebLogic, Websphere etc. Most application servers are big and complex pieces of software, that are not trivial to install or configure. Hence If we use Spring then we won't need such things.

Secondly, Spring provides various modules for different purposes. These modules are grouped into Core Container, Data Access/Integration, Web, AOP (Aspect Oriented Programming), Instrumentation, Messaging, and Test, as shown in the following diagram. To use one or part of the module we don't need to inject all the modules. For example, we can use Spring JDBC without Spring Web.


3. What is Inversion of Control (IoC)?

Inversion of Control is a principle in Software Engineering by which the control of objects or portions of a program is transferred to a container or framework.

For example, say our application has a text editor component and we want to provide spell-checking. Our standard code would look something like this:

public class TextEditor {
        private SpellChecker checker = new SpellChecker();
    }

Here TextEditor needs a SpellChecker object. This Means TextEditor is dependent on SpellChecker and we are manually instantiating the TextEditor object. This means we are managing the dependency. This means we have the control. Now look at the below code:

public class TextEditor {
       private SpellChecker checker;

       public TextEditor(SpellChecker checker) {
           this.checker = checker;
       }
    }

Here we are asking the Spring to instantiate the SpellChecker object and pass in the constructor of TextEditor i.e. Constructor Injection. This means Spring is managing the dependency. Now the control is transferred from the Programmer to Spring. This is nothing but an Inversion of Control.

Advantages of IOC:

  1. Loose Coupling:
  • Without IoC:
public class TextEditor {   
      private SpellChecker checker = new SpellChecker(); 
}
  • With IOC
public class TextEditor {    
    private SpellChecker checker;     
    public TextEditor(SpellChecker checker) {        
           this.checker = checker;     
    } 
}

The second example demonstrates IoC through constructor injection. The TextEditor class is no longer responsible for creating its own dependencies, promoting loose coupling between TextEditor and SpellChecker.

  1. Dependency Injection (DI):
  • Without IoC
public class TextEditor {     
       private SpellChecker checker = new SpellChecker(); 
       }
  • With IoC (Constructor Injection)
public class TextEditor {     
       private SpellChecker checker;      
       public TextEditor(SpellChecker checker) {                                  this.checker = checker;     
       }
    }

Dependency Injection makes components more testable and allows for easier substitution of dependencies, facilitating unit testing.

  1. Flexible Configuration:
  • Without IoC:
public class TextEditor {     
     private SpellChecker checker = new SpellChecker(); 
     }
  • With IoC (XML Configuration):
<beans>     
    <bean id="textEditor" class="com.example.TextEditor">                 <constructor-arg ref="spellChecker"/>     
    </bean>     
    <bean id="spellChecker" class="com.example.SpellChecker"/> 
</beans>

IoC containers like Spring allow centralized configuration, making it easy to modify and adapt the application without changing the code.

  1. Enhanced Testability:
  • *Without IoC:
public class TextEditor {    
        private SpellChecker checker = new SpellChecker(); 
        }
  • With IoC (Mocking for Testing):
public class TextEditorTest {    
         @Test     
         public void testTextEditor() { 
             SpellChecker mockChecker = mock(SpellChecker.class);               TextEditor textEditor = new TextEditor(mockChecker);         // Perform test using mockChecker     
             }
        }

IoC makes it easier to inject mock objects during testing, allowing for more effective unit testing.

  1. Centralized Control
  • Without IoC:
public class TextEditor {    
    private SpellChecker checker = new SpellChecker();
    }
  • With IoC (Spring Container):
// In the main application class or configuration file
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); 
TextEditor textEditor = context.getBean("textEditor", TextEditor.class);

IoC containers provide centralized control over the instantiation and management of objects, improving maintainability.


4. What is an aspect-oriented container framework?

Aspect-Oriented Programming (AOP) is a programming paradigm that addresses the modularization of cross-cutting concerns in software development. Cross-cutting concerns are aspects of a program that affect multiple modules and are often tangled or scattered throughout the codebase. AOP provides a way to modularize these concerns, making the code more modular, maintainable, and easier to understand.

Let's break down the key concepts in AOP:

  1. Core Concerns:

    • Definition: Core concerns are the essential features or functionalities that are critical to the application's purpose. They represent the primary business logic and functionality.

    • Example: In a medical records application, handling and indexing medical records would be a core concern. These are the functionalities without which the application would lose its primary purpose.

  2. Cross-Cutting Concerns:

    • Definition: Cross-cutting concerns are aspects of a program that affect multiple modules or components. They are functionalities that are commonly needed across different parts of the application.

    • Example: Logging, security, transaction management, and caching are typical cross-cutting concerns. These aspects do not belong to the core business logic but are necessary for various parts of the application.

  3. AOP's Role:

    • Separation of Concerns: AOP aims to separate cross-cutting concerns from core concerns, allowing developers to modularize and manage them independently. This separation leads to cleaner, more maintainable code.

    • Modularization: AOP achieves modularization by introducing constructs called "aspects." Aspects encapsulate cross-cutting concerns, and they can be applied to the codebase without modifying the core concerns.

    • Aspect: An aspect is a module that encapsulates a cross-cutting concern. It defines what should happen and where it should occur in the code.

  4. Advantages of AOP:

    • Code Reusability: Aspects can be reused across different modules, promoting code reusability.

    • Improved Maintainability: Separating cross-cutting concerns makes it easier to maintain and update specific functionalities without affecting the entire codebase.

    • Enhanced Readability: AOP allows developers to focus on core concerns in one place, improving the readability and understandability of the code.

    • Reduced Code Duplication: AOP helps eliminate code duplication by centralizing the implementation of cross-cutting concerns.

  5. Example Scenario:

  • Without AOP:
public void doSomething() {
            // Core concern
            // Business logic

            // Cross-cutting concern
            Logger.log("Something happened");
       }
  • With AOP:
public void doSomething() {
            // Core concern
            // Business logic
        }
@Aspect
        public class LoggingAspect {
            @Before("execution(* com.example.Service.*(..))")
            public void logBefore() {
                Logger.log("Something happened");
            }
        }

In this example, the logging concern is separated into an aspect (LoggingAspect), and it can be applied to various parts of the application without modifying their core logic.

In summary, AOP is a methodology that enhances modularity by isolating and encapsulating cross-cutting concerns, leading to more maintainable and readable code. Aspects in AOP provide a way to manage and apply these concerns in a centralized manner.


5. What is POJO class?

POJO stands for "Plain Old Java Object." It is a term used in Java development to describe a simple Java object that doesn't follow any complex framework or inheritance hierarchy, and it adheres to some basic conventions. The term "Plain Old" emphasizes that a POJO is a simple and straightforward Java class without any special restrictions or requirements imposed by a framework.

Key characteristics of a POJO class include:

  1. No Framework Dependencies: A POJO should not be tied to any specific framework or technology. It does not extend or implement any framework-specific classes or interfaces.

  2. Serializable: A POJO class often implements the Serializable interface to make instances of the class serializable, allowing them to be easily converted into byte streams for purposes like data storage or network transmission.

  3. Public Default Constructor: A POJO typically has a public default (no-argument) constructor. This is important for frameworks that instantiate objects via reflection.

  4. Getters and Setters: A POJO class often includes standard getter and setter methods for accessing and modifying its properties. This helps with encapsulation and follows JavaBeans conventions.

  5. Encapsulation: POJOs follow the principles of encapsulation, where the internal state of the object is kept private, and access to it is controlled through getter and setter methods.

Here's an example of a simple POJO class:

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

    // Public default constructor
    public Person() {
    }

    // Parameterized constructor
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter and setter methods
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

POJO stands for "Plain Old Java Object." It is a term used in Java development to describe a simple Java object that doesn't follow any complex framework or inheritance hierarchy, and it adheres to some basic conventions. The term "Plain Old" emphasizes that a POJO is a simple and straightforward Java class without any special restrictions or requirements imposed by a framework.

Key characteristics of a POJO class include:

  1. No Framework Dependencies: A POJO should not be tied to any specific framework or technology. It does not extend or implement any framework-specific classes or interfaces.

  2. Serializable: A POJO class often implements the Serializable interface to make instances of the class serializable, allowing them to be easily converted into byte streams for purposes like data storage or network transmission.

  3. Public Default Constructor: A POJO typically has a public default (no-argument) constructor. This is important for frameworks that instantiate objects via reflection.

  4. Getters and Setters: A POJO class often includes standard getter and setter methods for accessing and modifying its properties. This helps with encapsulation and follows JavaBeans conventions.

  5. Encapsulation: POJOs follow the principles of encapsulation, where the internal state of the object is kept private, and access to it is controlled through getter and setter methods.

Here's an example of a simple POJO class:

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

    // Public default constructor
    public Person() {
    }

    // Parameterized constructor
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter and setter methods
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

In this example, the Person class is a POJO. It has a default constructor, implements Serializable, includes private fields with corresponding getters and setters, and does not have dependencies on any specific framework.

6. How to disable AutoConfiguration ?

If you find that specific auto-configure classes are being applied that you don’t want, you can use the exclude attribute of @EnableAutoConfiguration to disable them.

import org.springframework.boot.autoconfigure.*;
import org.springframework.boot.autoconfigure.jdbc.*;
import org.springframework.context.annotation.*;

@Configuration@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})publicclass MyConfiguration {
}

If the class is not on the classpath, you can use the excludeName attribute of the annotation and specify the fully qualified name instead.

7. How to remove Embedded Tomcat Server in Spring Boot ?

By Default- Tomcat:-

When we add spring-boot-starter-web dependency as part of our web application(pom.xml) with spring boot. It includes tomcat along with all the dependencies. It's very convenient to use as we don't need to do anything and it's auto deployable to tomcat.

<dependency>
   <groupId>org.springframework.boot></groupId>
   <artifactId>spring-boot-starter-web></artifactId>
</dependency>

Exclude Tomcat:-

If we want to exclude tomcat from spring boot, we don't need to do much, we just need to add one additional block(<exclusions>) to the Spring Boot dependency.

<exclusions> tag is used to make us sure that given server/artifactId is being removed at the time of build.

Let's see how we can remove it:-

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artufactId>
   <exclusions>
      <exclusion>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-tomcat</artifactId>
      </exclusion>
   </exclusions>

Above approach can be used to exclude Tomcat from Spring Boot and also for other exclusions as well.

8. What is the difference between an embedded container and a WAR?

The main difference between a WAR file & an embedded system is that an embedded container allows Spring Boot applications to run as a JAR directly from the command prompt without setting up any web server. However, to run a WAR file, you need to set up a web server first like Tomcat, which has a Servlet container, then you need to deploy WAR to run Spring Boot applications.

9. What are profiles?

Profiles in Spring Boot are a way to define different sets of configurations for your application depending on the environment it is being run in.

For example, you might have one set of configurations for your development environment and another set of configurations for your production environment. These configurations might include things like database settings (I want to use a database for tests and another for dev purposes ), Bean Creation (ex: I want a bean to be created only if I’m in the development process it’s possible with profiles ), ….

Profiles can be defined using property files, YAML files, or even Java code. By default, Spring Boot will use the “default” profile if no other profile is specified. To activate a profile, you can set the “spring.profiles.active” property to the name of the profile you want to use.

Profile Activation

Profiles can be defined using property files or YAML files. For instance, you might have an application-dev.properties file for your development environment and an application-prod.properties file for your production environment. To activate a profile, set the spring.profiles.active property in your application.properties file:

propertiesCopy code
spring.profiles.active=dev

This will activate the dev profile, and Spring Boot will load the application-dev.properties file.

Note: When you define the spring.profiles.active property in your application.properties file, Spring Boot will still load both the application.properties file and the properties file specific to the active profile. This means that any properties defined in the application.properties file will be overridden by properties defined in the active profile’s properties file if they have the same key.

10. What is the difference between Authentication and Authorization

Authentication and authorization are two fundamental concepts in computer security, often used together but serving distinct purposes. Here's a detailed explanation of the differences between them:

Authentication

Definition:

Authentication is the process of verifying the identity of a user or entity. It answers the question, "Who are you?"

Purpose:

The primary purpose of authentication is to confirm that the user or entity attempting to access a system is who they claim to be.

How It Works:

Authentication typically involves the use of credentials, such as:

  • Something you know: Passwords, PINs, or answers to security questions.

  • Something you have: Security tokens, smart cards, or mobile devices.

  • Something you are: Biometric data, such as fingerprints, facial recognition, or iris scans.

Examples:

  • Logging into a website with a username and password.

  • Using a fingerprint to unlock a smartphone.

  • Scanning a badge to enter a secure building.

Outcome:

Successful authentication provides proof of identity and allows the user or entity to proceed to the next step, often authorization.

Authorization

Definition:

Authorization is the process of determining what an authenticated user or entity is allowed to do. It answers the question, "What can you do?"

Purpose:

The primary purpose of authorization is to enforce access control by defining and restricting what resources or actions an authenticated user is permitted to access or perform.

How It Works:

Authorization typically involves permissions, roles, or policies that define access levels. This can include:

  • Role-based access control (RBAC): Users are assigned roles, and each role has specific permissions.

  • Attribute-based access control (ABAC): Access decisions are based on attributes (e.g., user attributes, resource attributes, environmental conditions).

  • Policy-based access control: Access is governed by policies that define rules and conditions for access.

Examples:

  • A user can view but not edit a document.

  • An administrator can create, delete, and modify users in a system, while a regular user can only view their own profile.

  • A system that restricts access to certain files based on user roles or group membership.

Outcome:

Authorization determines the level of access or actions that an authenticated user is allowed to perform within a system. It is only relevant after authentication has been successfully completed.

Key Differences

  • Function:

    • Authentication is about verifying identity.

    • Authorization is about granting permissions and access.

  • Sequence:

    • Authentication always comes before authorization. A system must first authenticate a user before it can authorize their access to resources.
  • Information Used:

    • Authentication uses credentials (e.g., passwords, biometrics) to verify identity.

    • Authorization uses permissions, roles, or policies to control access to resources.

  • Focus:

    • Authentication focuses on who the user is.

    • Authorization focuses on what the user is allowed to do.

11. Optimizing Queries with @Query Annotation in Spring Data JPA

1. Use JPQL or Native SQL Appropriately

  • JPQL: Use JPQL for queries that are easily expressible in terms of entities and their relationships. JPQL is portable and easier to read and maintain.

  • Native SQL: Use native SQL when you need database-specific features, complex joins, or when performance requirements dictate bypassing the JPA provider.

2. Select Only Required Fields

  • Projection: Instead of fetching entire entities, select only the necessary fields. You can use JPQL projections or native SQL result mappings.

      @Query("SELECT new com.example.dto.UserDTO(u.id, u.name) FROM User u WHERE u.active = true")
      List<UserDTO> findActiveUsers();
    

3. Pagination and Limiting Results

  • Pagination: Use pagination to limit the number of results returned. This can be done by adding a Pageableparameter to the method signature.

      javaCopy code
      @Query("SELECT u FROM User u WHERE u.active = true")
      Page<User> findActiveUsers(Pageable pageable);
    
  • Limiting Results: Use LIMIT in native queries or TOP/FETCH FIRST as supported by the database.

      @Query(value = "SELECT * FROM users WHERE active = true LIMIT 10", nativeQuery = true)
      List<User> findTop10ActiveUsers();
    

4. Optimizing Conditions and Joins

  • Filter Early: Apply filters in the WHERE clause to reduce the result set as early as possible.

  • Efficient Joins: Use joins efficiently. Avoid unnecessary joins and ensure proper indexing of join columns.

      @Query("SELECT o FROM Order o JOIN o.customer c WHERE c.status = 'ACTIVE'")
      List<Order> findOrdersForActiveCustomers();
    

5. Index Awareness

  • Query with Indexes: Ensure that the fields used in WHERE clauses, joins, and ordering are indexed. This helps the database quickly locate the relevant data.

6. Avoid N+1 Query Problem

  • Fetch Strategies: Use appropriate fetch strategies to avoid the N+1 query problem, where lazy loading results in many small queries. Consider using JOIN FETCH to eagerly load associations when necessary.

      @Query("SELECT p FROM Post p JOIN FETCH p.comments WHERE p.id = :postId")
      Post findPostWithComments(@Param("postId") Long postId);
    

7. Named Queries

  • Use Named Queries: Named queries are precompiled at application startup, which can lead to faster execution times compared to dynamically defined queries.

      @NamedQuery(name = "User.findByStatus", query = "SELECT u FROM User u WHERE u.status = :status")
    

8. Parameter Binding

  • Use Parameter Binding: Always use parameter binding instead of concatenating query strings to prevent SQL injection and improve query parsing efficiency.

      @Query("SELECT u FROM User u WHERE u.name = :name")
      List<User> findByName(@Param("name") String name);
    

9. Caching

  • Second-Level Cache: If using Hibernate, consider enabling the second-level cache for entities that are frequently read but rarely modified.

  • Query Caching: Enable query caching for frequently run queries if the data does not change often.

10. Monitoring and Profiling

  • Monitor Query Performance: Use tools like Spring Boot Actuator, database logs, or APM tools to monitor the performance of your queries.

  • Profile Queries: Use database profiling tools to analyze the query execution plans and optimize them as needed.

Example of an Optimized Query with @Query Annotation

public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u WHERE u.status = :status ORDER BY u.lastLogin DESC")
    List<User> findActiveUsers(@Param("status") String status, Pageable pageable);

    @Query("SELECT new com.example.dto.UserDTO(u.id, u.name) FROM User u WHERE u.status = :status")
    List<UserDTO> findUserDTOsByStatus(@Param("status") String status);

    @Query(value = "SELECT * FROM users WHERE status = ?1 LIMIT 10", nativeQuery = true)
    List<User> findTop10UsersByStatus(String status);
}

12. Validator In SpringBoot

In Spring Boot, validators are used to enforce constraints on the data, ensuring that it meets specific criteria before being processed or persisted. Spring Boot provides robust support for validation through the use of the Bean Validation API (also known as JSR 380), with Hibernate Validator as the reference implementation.

1. Basic Concepts

  • Constraint Annotations: These are used to specify the validation rules on the fields of a class. Examples include @NotNull, @Size, @Min, @Max, and @Email.

  • Validator Interface: Custom validations can be implemented by creating classes that implement the ConstraintValidator interface.

  • @Valid and @Validated Annotations: These annotations are used to trigger validation. @Valid is used at the parameter level in method signatures, while @Validated can be used at the class level.

2. Using Built-in Validators

Example: Validating a Request Body

Suppose you have a User class that you want to validate before processing it in a REST API.

javaCopy code
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class User {

    @NotNull(message = "Name cannot be null")
    @Size(min = 2, max = 30, message = "Name must be between 2 and 30 characters")
    private String name;

    @NotNull(message = "Email cannot be null")
    @Email(message = "Email should be valid")
    private String email;

    // Getters and Setters
}

Example: Using @Valid in a Controller

In your Spring Boot controller, you can use the @Valid annotation to trigger validation.

javaCopy code
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody User user) {
        // If validation fails, a MethodArgumentNotValidException will be thrown
        return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
    }
}

If the User object does not meet the validation criteria, a MethodArgumentNotValidException will be thrown, and an appropriate error response can be returned.

3. Custom Validators

Sometimes, built-in constraints are not enough, and you may need to define custom validation logic.

Example: Custom Constraint Annotation

Define a custom annotation:

javaCopy code
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = AgeValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidAge {

    String message() default "Invalid age";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

Example: Custom Validator Implementation

Implement the validation logic:

javaCopy code
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class AgeValidator implements ConstraintValidator<ValidAge, Integer> {

    @Override
    public void initialize(ValidAge constraintAnnotation) {
    }

    @Override
    public boolean isValid(Integer age, ConstraintValidatorContext context) {
        return age != null && age >= 18 && age <= 100; // Custom validation logic
    }
}

Example: Using Custom Validator

Use the custom annotation in your data model:

javaCopy code
public class User {

    @NotNull(message = "Name cannot be null")
    private String name;

    @ValidAge
    private Integer age;

    // Getters and Setters
}

4. Global Exception Handling

To handle validation errors globally, you can use an exception handler.

Example: Global Exception Handler

javaCopy code
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

5. Group Validation

Spring Boot allows grouping validations using the @Validated annotation. This is useful when you want to apply different validation rules in different contexts.

javaCopy code
import javax.validation.GroupSequence;
import javax.validation.groups.Default;

public interface UserValidationGroups {

    interface BasicInfo {}

    interface DetailedInfo extends BasicInfo {}

    @GroupSequence({Default.class, BasicInfo.class, DetailedInfo.class})
    interface Complete {}
}
javaCopy code
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class User {

    @NotNull(message = "Name cannot be null", groups = UserValidationGroups.BasicInfo.class)
    @Size(min = 2, max = 30, message = "Name must be between 2 and 30 characters", groups = UserValidationGroups.BasicInfo.class)
    private String name;

    @ValidAge(groups = UserValidationGroups.DetailedInfo.class)
    private Integer age;

    // Getters and Setters
}

In your controller, you can specify which validation group to apply:

javaCopy code
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping("/basic")
    public ResponseEntity<String> createUserBasic(@Validated(UserValidationGroups.BasicInfo.class) @RequestBody User user) {
        return ResponseEntity.status(HttpStatus.CREATED).body("User basic info validated");
    }

    @PostMapping("/detailed")
    public ResponseEntity<String> createUserDetailed(@Validated(UserValidationGroups.DetailedInfo.class) @RequestBody User user) {
        return ResponseEntity.status(HttpStatus.CREATED).body("User detailed info validated");
    }
}

Conclusion

Spring Boot provides comprehensive support for validation, making it easy to apply both built-in and custom validation rules to your data models. By leveraging the Bean Validation API, you can ensure data integrity and enforce business rules effectively. Additionally, with global exception handling, you can provide meaningful error messages to the client, improving the overall user experience.

1
Subscribe to my newsletter

Read articles from Murali directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Murali
Murali