- Study Plan
- Java Basics
- Inversion of Control (IoC)
- Java Basics
- Java Basics
- Java Basics
- Java Basics
- Java Basics
- Java Basics
- Java Basics
- Java Basics
-
Install Java Development Kit (JDK)
- Install the latest LTS version (e.g., JDK 17+).
- Verify installation:
java --version
.
-
Install an IDE
- Recommended: IntelliJ IDEA (Community or Ultimate Edition).
-
Build Tool
- Learn and set up Maven and Gradle (focus on one initially, Maven is more common for beginners).
-
Spring Initializer
- Familiarize yourself with Spring Initializer, which generates Spring Boot project templates.
-
OOP Concepts
- Polymorphism, Encapsulation, Abstraction, Inheritance.
-
Data Structures
- Collections (List, Set, Map, Queue).
- Streams and Lambda Expressions.
-
Exception Handling
- Checked vs. Unchecked exceptions.
-
Java I/O
- File handling and Streams.
-
Annotations
- Understand how Java annotations work.
-
Inversion of Control (IoC)
- Understand how Spring manages dependencies using IoC and Dependency Injection (DI).
-
Beans and Application Context
- Learn about Spring Beans and their lifecycle.
- Learn how the ApplicationContext manages beans.
-
Spring Annotations
@Component
,@Service
,@Repository
,@Controller
.
-
Spring Configuration
- XML-based, Annotation-based, and Java-based configurations.
-
Getting Started with Spring Boot
- Understand how Spring Boot simplifies Spring applications.
- Explore auto-configuration and
application.properties
.
-
Creating RESTful APIs
- Learn
@RestController
and@RequestMapping
. - Use
@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
.
- Learn
-
Working with Data
- Integrate with JPA (Java Persistence API) using Spring Data JPA.
- Learn to use Hibernate for ORM.
- Configure data sources (
application.properties
for database connection).
-
Error Handling
- Implement global exception handling using
@ControllerAdvice
and@ExceptionHandler
.
- Implement global exception handling using
-
Logging
- Integrate with SLF4J and Logback for logging.
-
Spring Security
- Learn authentication and authorization.
- Use JWT (JSON Web Tokens) for securing APIs.
-
Spring Boot Testing
- Unit testing with JUnit and Mockito.
- Integration testing with MockMvc.
-
Spring Boot Actuator
- Monitor application health and metrics.
- Understand the endpoints provided by the Actuator.
-
Asynchronous Programming
- Use
@Async
andCompletableFuture
.
- Use
-
Caching
- Implement caching using
@Cacheable
and Ehcache/Redis.
- Implement caching using
-
Understanding Microservices
- Difference between monoliths and microservices.
- Communication between microservices (REST, gRPC, etc.).
-
Spring Cloud
- Service discovery with Eureka.
- Centralized configuration with Spring Cloud Config.
- Load balancing with Ribbon.
- API Gateway with Spring Cloud Gateway or Zuul.
-
Distributed Tracing
- Integrate with tools like Sleuth and Zipkin.
-
Containerization
- Dockerize Spring Boot applications.
-
Build and Deploy
- CI/CD pipelines using Jenkins or GitHub Actions.
- Deploy applications to AWS, Azure, or Google Cloud.
-
Kubernetes
- Learn the basics of Kubernetes and deploy Spring Boot applications on Kubernetes clusters.
-
Books
- Spring in Action by Craig Walls.
- Java Persistence with Hibernate by Bauer & Christian.
-
Courses
- Udemy: "Spring & Hibernate for Beginners" by Chad Darby.
- Pluralsight: Spring Boot and Microservices.
-
Documentation
-
YouTube Channels
- Amigoscode.
- Java Brains.
- Tech Primers.
-
Basic Projects
- Employee management system.
- Blog API.
-
Intermediate Projects
- E-commerce backend with order and inventory management.
- Social media platform backend.
-
Advanced Projects
- Multi-module microservices application (e.g., a booking system).
- Real-time event processing with Kafka.
Here’s a quick brush-up on Java basics to help you refresh your understanding:
-
Object-Oriented Programming (OOP): Java is based on OOP principles.
- Encapsulation: Wrapping data and methods inside a class.
- Inheritance: Reusing code from a parent class.
- Polymorphism: Multiple forms of a method (overloading/overriding).
- Abstraction: Hiding complex implementation details.
-
JVM, JRE, JDK:
- JVM (Java Virtual Machine): Executes Java bytecode.
- JRE (Java Runtime Environment): Includes JVM and libraries to run Java programs.
- JDK (Java Development Kit): Includes JRE and tools to develop Java programs.
Type | Size | Example Values |
---|---|---|
byte |
1 byte | -128 to 127 |
short |
2 bytes | -32,768 to 32,767 |
int |
4 bytes | -2³¹ to 2³¹-1 |
long |
8 bytes | -2⁶³ to 2⁶³-1 |
float |
4 bytes | 3.4e-038 to 3.4e+038 |
double |
8 bytes | 1.7e-308 to 1.7e+308 |
char |
2 bytes | Single character ('A') |
boolean |
1 bit | true or false |
int number = 10;
float pi = 3.14f;
char letter = 'A';
boolean isJavaFun = true;
if (condition) {
// Code
} else if (condition) {
// Code
} else {
// Code
}
switch (value) {
case 1:
// Code
break;
default:
// Code
}
- For Loop:
for (int i = 0; i < 5; i++) { System.out.println(i); }
- While Loop:
int i = 0; while (i < 5) { System.out.println(i); i++; }
- Do-While Loop:
int i = 0; do { System.out.println(i); i++; } while (i < 5);
class Animal {
String name;
void speak() {
System.out.println("Animal speaks");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Animal();
dog.name = "Dog";
dog.speak();
}
}
class Animal {
void eat() {
System.out.println("This animal eats food");
}
}
class Dog extends Animal {
void bark() {
System.out.println("The dog barks");
}
}
-
Method Overloading:
class Calculator { int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } }
-
Method Overriding:
class Animal { void sound() { System.out.println("Animal makes a sound"); } } class Dog extends Animal { @Override void sound() { System.out.println("Dog barks"); } }
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero!");
} finally {
System.out.println("Always executes");
}
void checkAge(int age) throws Exception {
if (age < 18) {
throw new Exception("Age must be 18 or above");
}
}
- List: Allows duplicates, ordered (e.g.,
ArrayList
,LinkedList
). - Set: No duplicates (e.g.,
HashSet
,TreeSet
). - Map: Key-value pairs (e.g.,
HashMap
,TreeMap
).
import java.util.ArrayList;
import java.util.HashMap;
ArrayList<String> list = new ArrayList<>();
list.add("A");
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "One");
System.out.println(map.get(1));
import java.io.*;
class FileExample {
public static void main(String[] args) {
try {
FileWriter writer = new FileWriter("output.txt");
writer.write("Hello, Java!");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.util.Arrays;
class Main {
public static void main(String[] args) {
Arrays.asList("a", "b", "c").forEach(s -> System.out.println(s));
}
}
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
Inversion of Control (IoC) is a design principle used in software development to create loosely coupled components. Instead of objects creating their dependencies, IoC transfers the responsibility of managing and injecting dependencies to a container or framework. This makes applications more modular, easier to test, and simpler to maintain.
In traditional programming, a class is responsible for creating its dependencies. For example:
class ServiceA {
private ServiceB serviceB;
public ServiceA() {
serviceB = new ServiceB(); // Dependency is created here
}
public void performTask() {
serviceB.doSomething();
}
}
Here, ServiceA
is tightly coupled with ServiceB
. If ServiceB
changes, ServiceA
must also change, making the code harder to maintain and test.
With IoC, the dependencies are provided to the class by an external container (e.g., the Spring Framework):
class ServiceA {
private ServiceB serviceB;
// Dependency is injected via the constructor
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
public void performTask() {
serviceB.doSomething();
}
}
Now, ServiceA
doesn’t create ServiceB
. Instead, it receives ServiceB
from outside, making ServiceA
independent of how ServiceB
is instantiated.
IoC is often implemented using Dependency Injection (DI). There are several ways to inject dependencies:
- Dependencies are passed through the class constructor.
public class ServiceA {
private ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
- Dependencies are set through public setter methods.
public class ServiceA {
private ServiceB serviceB;
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
- Dependencies are injected directly into class fields using annotations (e.g.,
@Autowired
in Spring).
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
In Spring, the IoC Container manages the lifecycle of objects (called beans) and their dependencies.
-
Spring Beans
Spring IoC container creates and manages beans based on configuration provided (XML, annotations, or Java-based configuration). -
Dependency Injection in Spring
Dependencies are injected into beans automatically by the IoC container.- Example with Annotations:
@Component public class ServiceB { public void doSomething() { System.out.println("ServiceB is working!"); } } @Component public class ServiceA { private final ServiceB serviceB; @Autowired public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; } public void performTask() { serviceB.doSomething(); } }
- Example with Annotations:
-
Configurations
- XML Configuration:
<bean id="serviceB" class="com.example.ServiceB" /> <bean id="serviceA" class="com.example.ServiceA"> <constructor-arg ref="serviceB" /> </bean>
- Java-based Configuration:
@Configuration public class AppConfig { @Bean public ServiceB serviceB() { return new ServiceB(); } @Bean public ServiceA serviceA(ServiceB serviceB) { return new ServiceA(serviceB); } }
- XML Configuration:
-
Loose Coupling
Components are easier to reuse and test. -
Better Testability
Dependencies can be mocked or replaced during testing. -
Improved Maintainability
Changes to dependencies don't require changes to dependent components. -
Centralized Configuration
Dependency management is handled by the container.
IoC is a cornerstone of frameworks like Spring, enabling robust, maintainable, and flexible application development.
Annotations in Java are metadata tags that provide additional information about a program's code. They do not directly affect the program's execution but can influence how it is processed by tools, frameworks, and the compiler. Annotations are heavily used in modern frameworks like Spring to reduce boilerplate code and provide clear, declarative programming models.
-
Metadata Provision
- Annotations add metadata to classes, methods, fields, and parameters.
-
Declarative Programming
- Annotations reduce boilerplate by allowing declarative definitions.
-
Processed by Tools
- Frameworks and tools like Spring, Hibernate, and JUnit process annotations to provide functionality.
Annotations are defined with an @
symbol followed by the annotation name.
public class Example {
@Override
public String toString() {
return "Example class";
}
@Deprecated
public void oldMethod() {
// Do something
}
}
- Annotations with no parameters.
Examples:
@Override
Indicates that a method overrides a method in its superclass.@Deprecated
Marks a method, class, or field as obsolete.@SuppressWarnings
Suppresses specific compiler warnings.
- Annotations can accept parameters to provide additional details.
Example:
@SuppressWarnings("unchecked")
public void method() {
// Code
}
Annotations play a crucial role in Spring by simplifying configuration and enabling declarative programming.
Used to define and identify Spring-managed components (beans).
@Component
Marks a class as a Spring-managed bean.@Service
Specialized version of@Component
for service-layer classes.@Repository
Specialized version of@Component
for persistence-layer classes.@Controller
Specialized version of@Component
for MVC controllers.
Example:
@Component
public class MyBean {
public void doSomething() {
System.out.println("Hello, Spring!");
}
}
@Autowired
Automatically wires a bean into another bean.@Qualifier
Specifies which bean to inject when multiple candidates are available.@Value
Injects values from properties or environment variables.
Example:
@Service
public class MyService {
@Autowired
private MyRepository repository;
}
@Configuration
Marks a class as a source of Spring bean definitions.@Bean
Defines a Spring bean within a@Configuration
class.@PostConstruct
Marks a method to run after the bean initialization.@PreDestroy
Marks a method to run before the bean destruction.
Example:
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
@RestController
Combines@Controller
and@ResponseBody
for REST APIs.@RequestMapping
Maps HTTP requests to handler methods or classes.@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
Specific mappings for HTTP methods.@PathVariable
Binds URI path variables to method parameters.@RequestParam
Binds query parameters to method parameters.
Example:
@RestController
public class MyController {
@GetMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
}
@Transactional
Marks methods or classes as transactional.
Example:
@Service
public class MyService {
@Transactional
public void performTransaction() {
// Transactional logic
}
}
You can create your own annotations by using the @interface
keyword.
Example:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCustomAnnotation {
String value() default "Default Value";
}
Usage:
public class Example {
@MyCustomAnnotation(value = "Custom Annotation Example")
public void myMethod() {
System.out.println("Using custom annotation");
}
}
Annotations can be processed at:
-
Compile-Time
Tools like Lombok or annotation processors handle compile-time processing. -
Runtime
Frameworks like Spring use reflection to process annotations during runtime.
Annotations simplify coding by reducing boilerplate and improving readability, making them a cornerstone of modern Java development.
Beans in the context of the Spring Framework are the core building blocks of a Spring application. A bean is an object that is instantiated, assembled, and managed by the Spring IoC (Inversion of Control) container.
- A Spring Bean is any Java object that the Spring IoC container creates, manages, and wires together.
- Beans are defined in the Spring configuration file (XML, Java, or annotations).
- The lifecycle of a bean (creation, initialization, destruction) is managed by the Spring IoC container.
Annotations are the most modern and widely used way to define beans.
-
@Component
Marks a class as a Spring-managed bean.@Component public class MyService { public void performTask() { System.out.println("Task performed!"); } }
-
Specialized Stereotype Annotations
@Service
: For service-layer beans.@Repository
: For persistence-layer beans.@Controller
: For Spring MVC controllers.
-
Example:
@Service public class TaskService { public void executeTask() { System.out.println("Executing task..."); } }
Define beans programmatically in a @Configuration
class.
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
Define beans in an XML file.
<beans xmlns="http://www.springframework.org/schema/beans">
<bean id="myService" class="com.example.MyService"/>
</beans>
When using annotations, beans are registered in the Spring context with a name. If not explicitly defined, the name defaults to the class name in camel case.
-
Default Name:
@Component public class MyService { } // Bean name: "myService"
-
Custom Name:
@Component("customBeanName") public class MyService { }
Beans can be injected into each other using annotations or configuration.
- Using @Autowired:
@Component public class MyController { private final MyService myService; @Autowired public MyController(MyService myService) { this.myService = myService; } public void handleRequest() { myService.performTask(); } }
The scope of a bean determines how many instances of the bean will be created and how they are shared.
- Singleton (default): One instance per Spring IoC container.
- Prototype: A new instance every time the bean is requested.
- Request: One instance per HTTP request (used in web applications).
- Session: One instance per HTTP session.
- Application: One instance per servlet context.
Set the scope using @Scope
or XML configuration.
Example:
@Component
@Scope("prototype")
public class PrototypeBean {
}
The lifecycle of a bean involves creation, initialization, and destruction, managed by the Spring IoC container.
-
Initialization
Beans are initialized after their dependencies are injected. -
Custom Initialization and Destruction Methods
Define custom methods to be executed at specific stages.Using
@PostConstruct
and@PreDestroy
:@Component public class MyBean { @PostConstruct public void init() { System.out.println("Bean is initialized."); } @PreDestroy public void destroy() { System.out.println("Bean is being destroyed."); } }
Using Java-based Configuration:
@Bean(initMethod = "init", destroyMethod = "destroy") public MyService myService() { return new MyService(); }
Using XML Configuration:
<bean id="myService" class="com.example.MyService" init-method="init" destroy-method="destroy"/>
-
Centralized Configuration
Beans are defined in one place and reused throughout the application. -
Dependency Management
Dependencies are automatically injected, reducing boilerplate code. -
Loose Coupling
Spring beans promote loose coupling, making applications more modular and easier to maintain. -
Lifecycle Management
The Spring container manages the entire lifecycle of beans, including initialization and destruction.
@Component
public class MyRepository {
public void saveData() {
System.out.println("Data saved!");
}
}
@Service
public class MyService {
private final MyRepository myRepository;
@Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
public void performTask() {
myRepository.saveData();
}
}
@RestController
@RequestMapping("/api")
public class MyController {
private final MyService myService;
@Autowired
public MyController(MyService myService) {
this.myService = myService;
}
@GetMapping("/task")
public String handleRequest() {
myService.performTask();
return "Task executed!";
}
}
In this example:
MyRepository
,MyService
, andMyController
are beans.- Dependencies are injected automatically by Spring IoC.
Beans are the foundation of any Spring application, enabling modular, testable, and maintainable software development.
Managing properties and configuration in a Spring Boot application is a core aspect of its flexibility. Here's a comprehensive guide on how to effectively handle application properties or configuration:
Spring Boot allows you to manage configuration using property files like application.properties
or application.yml
.
-
Place
application.properties
orapplication.yml
in thesrc/main/resources
directory. -
Example
application.properties
:server.port=8080 spring.datasource.url=jdbc:mysql://localhost:3306/mydb spring.datasource.username=root spring.datasource.password=rootpassword
-
Example
application.yml
:server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/mydb username: root password: rootpassword
Spring supports profile-specific configuration files.
application.properties
(common/default config)application-dev.properties
(for the "dev" profile)application-prod.properties
(for the "prod" profile)
Activate the desired profile using:
spring.profiles.active
inapplication.properties
:spring.profiles.active=dev
- Command-line argument:
java -Dspring.profiles.active=prod -jar myapp.jar
Spring will merge configurations, prioritizing profile-specific files.
Spring Boot supports externalized configuration for portability across environments. Configurations can be stored and prioritized from:
-
Environment Variables:
export SPRING_DATASOURCE_URL=jdbc:mysql://remotehost:3306/mydb
These override values in property files.
-
Command-Line Arguments: Pass properties at runtime:
java -jar myapp.jar --spring.datasource.url=jdbc:mysql://remotehost:3306/mydb
-
System Properties: Set JVM options:
java -Dspring.datasource.url=jdbc:mysql://remotehost:3306/mydb -jar myapp.jar
-
Application Configuration Locations: Specify custom property file locations using:
java -jar myapp.jar --spring.config.location=/path/to/application.properties
You can define custom properties in your configuration files and bind them to a class using @ConfigurationProperties
.
In application.yml
:
app:
name: MyApplication
description: This is a Spring Boot app
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String name;
private String description;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
Inject the custom configuration in your beans:
import org.springframework.stereotype.Service;
@Service
public class AppService {
private final AppProperties appProperties;
public AppService(AppProperties appProperties) {
this.appProperties = appProperties;
}
public void printAppDetails() {
System.out.println("App Name: " + appProperties.getName());
System.out.println("Description: " + appProperties.getDescription());
}
}
For quick access to individual properties, use @Value
.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class AppConfig {
@Value("${app.name}")
private String appName;
@Value("${app.description}")
private String appDescription;
public void printAppDetails() {
System.out.println("App Name: " + appName);
System.out.println("Description: " + appDescription);
}
}
Sensitive information (e.g., credentials) should not be stored in plain text in property files. Use one of the following approaches:
Define sensitive data as environment variables:
export DB_PASSWORD=secretpassword
Reference it in application.properties
:
spring.datasource.password=${DB_PASSWORD}
Centralize configuration for distributed systems using Spring Cloud Config.
- Vault (e.g., HashiCorp Vault)
- AWS Secrets Manager
Provide a default value if a property is not set:
@Value("${app.name:DefaultAppName}")
private String appName;
You can load properties from a custom location or file.
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:custom-config.properties")
public class CustomConfig {
// Configuration here
}
For security, encrypt sensitive data using libraries like Jasypt.
- Add Jasypt dependency.
- Encrypt properties using a secret key.
- Use the decrypted values at runtime.
For testing, activate a specific profile using @ActiveProfiles
:
@ExtendWith(SpringExtension.class)
@SpringBootTest
@ActiveProfiles("test")
public class AppServiceTest {
@Test
void contextLoads() {
// Test logic
}
}
- Use profiles for environment-specific configurations.
- Externalize sensitive data using environment variables or secrets management tools.
- Centralize configurations in distributed systems with Spring Cloud Config.
- Maintain clear separation between default, dev, and prod configurations.
- Avoid hardcoding values; use configuration management instead.
Would you like guidance on implementing any of these techniques?
In Spring, profiles are used to manage different configurations for different environments (e.g., development, testing, production). They allow you to control which beans are loaded into the application context based on the active profile.
Profiles can be specified for beans or configuration classes using the @Profile
annotation.
@Configuration
public class AppConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
System.out.println("Using DEV DataSource");
return new HikariDataSource(); // Example data source
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
System.out.println("Using PROD DataSource");
return new HikariDataSource(); // Example data source
}
}
Set the active profile in the configuration file:
- In
application.properties
:spring.profiles.active=dev
- In
application.yml
:spring: profiles: active: dev
You can set the profile when running the application:
java -Dspring.profiles.active=prod -jar myapp.jar
Set the SPRING_PROFILES_ACTIVE
environment variable:
export SPRING_PROFILES_ACTIVE=dev
You can create separate property files for each profile.
-
File Structure:
src/main/resources ├── application.properties ├── application-dev.properties ├── application-prod.properties
-
Spring will automatically load
application-{profile}.properties
based on the active profile.
-
application-dev.properties
server.port=8081 datasource.url=jdbc:h2:mem:devdb
-
application-prod.properties
server.port=8080 datasource.url=jdbc:mysql://prod.db.url/mydb
-
If
spring.profiles.active=dev
,application-dev.properties
will be used.
You can set the active profile programmatically using ConfigurableEnvironment
.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.ConfigurableEnvironment;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(DemoApplication.class);
ConfigurableEnvironment environment = app.run(args).getEnvironment();
// Setting profile programmatically
environment.setActiveProfiles("dev");
}
}
You can use @Profile
directly with beans annotated with @Component
or @Service
.
@Component
@Profile("dev")
public class DevService implements MyService {
@Override
public void perform() {
System.out.println("Dev Service Running");
}
}
@Component
@Profile("prod")
public class ProdService implements MyService {
@Override
public void perform() {
System.out.println("Prod Service Running");
}
}
You can set a default profile for when no profile is explicitly active.
spring.profiles.default=dev
If no profile is set (via command line or environment variable), the default profile (dev
) will be used.
You can activate profiles in test configurations using @ActiveProfiles
.
@ExtendWith(SpringExtension.class)
@SpringBootTest
@ActiveProfiles("test")
public class MyServiceTest {
@Autowired
private MyService myService;
@Test
public void testService() {
myService.perform();
// Test assertions here
}
}
- Use profiles for environment-specific configurations (e.g., database URLs, feature toggles).
- Keep sensitive production properties (e.g., credentials) in secure locations like Spring Cloud Config or environment variables.
- Use a default profile for fallback during development.
- Avoid hardcoding profiles; prefer external configurations or environment variables.
Building a REST API with Spring involves several steps, starting from setting up the project to designing the endpoints and handling data. Here's a step-by-step guide to help you build a REST API with Spring Boot:
Use Spring Initializr to create your project:
- Visit Spring Initializr.
- Choose:
- Project: Maven/Gradle
- Language: Java
- Dependencies: Spring Web, Spring Boot DevTools, Spring Data JPA, and your preferred database (e.g., H2, MySQL).
- Download the project, unzip it, and open it in your IDE (e.g., IntelliJ, Eclipse).
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-devtools</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Create a class annotated with @Entity
to represent a table in your database.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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;
}
}
Use Spring Data JPA's JpaRepository
to interact with the database.
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
The service layer handles business logic and interacts with the repository.
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
}
public User createUser(User user) {
return userRepository.save(user);
}
public User updateUser(Long id, User userDetails) {
User user = getUserById(id);
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
return userRepository.save(user);
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
The controller layer defines REST endpoints and handles HTTP requests.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return new ResponseEntity<>(userService.createUser(user), HttpStatus.CREATED);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
return ResponseEntity.ok(userService.updateUser(id, userDetails));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
Set up your database in application.properties
or application.yml
.
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
To access the H2 console, visit: http://localhost:8080/h2-console
.
Use tools like Postman, cURL, or a browser extension like Rest Client.
- GET
/api/users
- POST
/api/users
{ "name": "John Doe", "email": "[email protected]" }
- PUT
/api/users/1
{ "name": "Jane Doe", "email": "[email protected]" }
- DELETE
/api/users/1
Use javax.validation
annotations like @NotNull
, @Size
, @Email
.
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public class User {
// Fields...
@NotBlank(message = "Name is mandatory")
private String name;
@Email(message = "Email should be valid")
private String email;
}
Create a global exception handler using @ControllerAdvice
.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
}
Use Spring Data JPA's built-in pagination:
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public Page<User> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
This setup gives you a fully functional REST API. Would you like examples of securing the API (e.g., JWT authentication) or deploying it?
To set up PostgreSQL as the database for your Spring Boot application, follow these steps:
In your build.gradle
, include the PostgreSQL driver as a runtime dependency:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'
}
Run:
./gradlew build
In src/main/resources/application.properties
or application.yml
, configure the connection to your PostgreSQL database:
spring.datasource.url=jdbc:postgresql://localhost:5432/your_database
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Hibernate settings
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.url
: The JDBC URL to connect to PostgreSQL. Replacelocalhost
,5432
, andyour_database
with your PostgreSQL server and database name.spring.datasource.username
: Your PostgreSQL username.spring.datasource.password
: Your PostgreSQL password.spring.jpa.hibernate.ddl-auto
: Manages schema generation:update
: Automatically updates the database schema without data loss.validate
: Validates the schema without making changes.create
: Drops and recreates the schema every time.create-drop
: Drops the schema when the session ends.
spring.jpa.database-platform
: Specifies the Hibernate dialect for PostgreSQL.
-
Install PostgreSQL on your system:
- On Ubuntu:
sudo apt update sudo apt install postgresql postgresql-contrib
- On Windows/Mac: Download the installer from PostgreSQL official website.
- On Ubuntu:
-
Log in to the PostgreSQL shell:
sudo -u postgres psql
-
Create a new database and user:
CREATE DATABASE your_database; CREATE USER your_username WITH PASSWORD 'your_password'; GRANT ALL PRIVILEGES ON DATABASE your_database TO your_username;
-
Exit the PostgreSQL shell:
\q
Run the Spring Boot application:
./gradlew bootRun
Check the logs to confirm the connection to PostgreSQL:
- You should see messages indicating that Hibernate is initializing the database schema.
Spring Boot uses HikariCP as the default connection pool. Customize it in your application.properties
for better performance:
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=2
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=20000
If you prefer YAML format:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/your_database
username: your_username
password: your_password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
database-platform: org.hibernate.dialect.PostgreSQLDialect
Use a simple repository and entity to test database operations.
package com.example.demo;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and setters
}
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
Using a REST controller, perform CRUD operations to validate that data is being saved and retrieved from PostgreSQL.
By following these steps, your Spring Boot application should now be set up to work with a PostgreSQL database. Let me know if you need help with database migrations or testing!
Setting up relationships between tables in a Spring Boot application with PostgreSQL involves defining JPA entity relationships using Hibernate annotations. These annotations specify how entities (tables) relate to each other, such as one-to-one, one-to-many, many-to-one, and many-to-many.
Here’s a step-by-step guide:
-
Spring Boot Dependencies: Ensure your
build.gradle
orpom.xml
includes the following dependencies:implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.postgresql:postgresql'
-
PostgreSQL Configuration: Add the database connection details in
application.properties
orapplication.yml
:spring.datasource.url=jdbc:postgresql://localhost:5432/your_database spring.datasource.username=your_username spring.datasource.password=your_password spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
Use Case: A User
entity has one Profile
.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id", referencedColumnName = "id")
private Profile profile;
// Getters and Setters
}
@Entity
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
@OneToOne(mappedBy = "profile")
private User user;
// Getters and Setters
}
Use Case: A Department
has many Employees
.
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
// Getters and Setters
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
// Getters and Setters
}
Use Case: An Employee
belongs to one Department
.
This is essentially the reverse of the one-to-many relationship above.
Use Case: A Student
can enroll in many Courses
, and a Course
can have many Students
.
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
// Getters and Setters
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
// Getters and Setters
}
User user = new User();
user.setName("John");
Profile profile = new Profile();
profile.setBio("Software Engineer");
// Set the relationship
user.setProfile(profile);
// Save user (cascades to profile)
userRepository.save(user);
Department department = new Department();
department.setName("IT");
Employee employee1 = new Employee();
employee1.setName("Alice");
employee1.setDepartment(department);
Employee employee2 = new Employee();
employee2.setName("Bob");
employee2.setDepartment(department);
// Add employees to the department
department.getEmployees().add(employee1);
department.getEmployees().add(employee2);
// Save department (cascades to employees)
departmentRepository.save(department);
Student student = new Student();
student.setName("Mary");
Course course1 = new Course();
course1.setName("Mathematics");
Course course2 = new Course();
course2.setName("Physics");
// Establish relationships
student.getCourses().add(course1);
student.getCourses().add(course2);
course1.getStudents().add(student);
course2.getStudents().add(student);
// Save student (cascades to courses)
studentRepository.save(student);
By default, JPA uses lazy loading for relationships to avoid loading unnecessary data. Lazy loading can lead to exceptions like the LazyInitializationException
.
To handle this:
- Use
@Transactional
in service methods to ensure the session is active when accessing lazy-loaded data.@Transactional public User getUserWithProfile(Long id) { return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found")); }
- Switch to eager loading using
FetchType.EAGER
(not recommended for large datasets):@OneToOne(fetch = FetchType.EAGER) private Profile profile;
Create sample data during startup:
@Bean
public CommandLineRunner demoData(UserRepository userRepository, ProfileRepository profileRepository) {
return args -> {
User user = new User();
user.setName("John Doe");
Profile profile = new Profile();
profile.setBio("Full Stack Developer");
user.setProfile(profile);
userRepository.save(user);
};
}
-
Schema Generation:
- Use
spring.jpa.hibernate.ddl-auto=update
for development to auto-generate the schema. - For production, consider setting
ddl-auto
tonone
and managing the schema manually.
- Use
-
SQL Logs: Enable SQL logs for debugging:
spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
-
Entity Mappings: Ensure relationships (
mappedBy
,JoinColumn
) are properly defined. Hibernate relies on these to manage foreign keys.
Would you like additional examples or help with specific configurations?
To implement CRUD operations in a Spring Boot application connected to a PostgreSQL database, follow these steps:
The entity represents a table in the database.
package com.example.demo;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Constructors
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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;
}
}
Use Spring Data JPA to interact with the database.
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
The service layer contains the business logic.
package com.example.demo;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
}
public User createUser(User user) {
return userRepository.save(user);
}
public User updateUser(Long id, User userDetails) {
User user = getUserById(id);
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
return userRepository.save(user);
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
Expose the CRUD operations through REST endpoints.
package com.example.demo;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// Get all users
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
// Get user by ID
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
// Create a new user
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
// Update a user
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
return ResponseEntity.ok(userService.updateUser(id, userDetails));
}
// Delete a user
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
-
Create a user:
curl -X POST -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "[email protected]"}' \ http://localhost:8080/api/users
-
Get all users:
curl http://localhost:8080/api/users
-
Get a user by ID:
curl http://localhost:8080/api/users/1
-
Update a user:
curl -X PUT -H "Content-Type: application/json" \ -d '{"name": "Jane Doe", "email": "[email protected]"}' \ http://localhost:8080/api/users/1
-
Delete a user:
curl -X DELETE http://localhost:8080/api/users/1
- Open Postman and set the request method (GET, POST, PUT, DELETE).
- Set the URL (e.g.,
http://localhost:8080/api/users
). - Add JSON data in the Body tab for POST and PUT requests.
- Send the request and verify the response.
Spring Boot with JPA will automatically create the user
table in your PostgreSQL database based on the User
entity. You can verify this using a database client like pgAdmin or a terminal:
SELECT * FROM user;
By following these steps, you can implement a fully functional CRUD API with Spring Boot and PostgreSQL. Let me know if you'd like additional help or examples!
Validating data in Spring Boot ensures the correctness and integrity of data before processing it. Spring Boot leverages Bean Validation API (JSR 380) and provides built-in support for validating incoming data, such as request payloads, using annotations.
Make sure the Bean Validation API and Hibernate Validator (the reference implementation) are available in your project. If you're using Gradle, include:
implementation 'org.springframework.boot:spring-boot-starter-validation'
Add validation constraints to the fields using annotations from javax.validation.constraints
or jakarta.validation.constraints
.
Example:
import jakarta.validation.constraints.*;
public class UserRequest {
@NotNull(message = "Name cannot be null")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@Email(message = "Invalid email format")
private String email;
@NotNull(message = "Age cannot be null")
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 100, message = "Age must be at most 100")
private Integer age;
// Getters and Setters
}
Use @Valid
to validate the incoming request body, query parameters, or path variables.
Example:
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody UserRequest userRequest) {
return ResponseEntity.ok("User is valid and saved");
}
}
Spring automatically returns 400 Bad Request
if validation fails, with details about the violated constraints. However, you can customize error handling by implementing a global exception handler.
Example:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
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 ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
Annotation | Description |
---|---|
@NotNull |
Ensures the value is not null . |
@NotEmpty |
Ensures the value is not null or empty (for strings). |
@NotBlank |
Ensures the value is not null , empty, or whitespace. |
@Size(min, max) |
Validates the size of a collection or string. |
@Min(value) |
Ensures the value is greater than or equal to the specified value. |
@Max(value) |
Ensures the value is less than or equal to the specified value. |
@Email |
Validates email format. |
@Pattern(regex) |
Validates the value against a regular expression. |
@Past / @PastOrPresent |
Ensures the date is in the past or past and present. |
@Future / @FutureOrPresent |
Ensures the date is in the future or future and present. |
-
Use DTOs for Validation
- Avoid using entities for validation as they are tied to database operations. Use separate Data Transfer Objects (DTOs) for request payload validation.
-
Custom Validation
-
Create custom validators if built-in constraints are insufficient.
-
Example of a custom annotation:
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = CustomValidator.class) public @interface ValidCustom { String message() default "Invalid value"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Custom validator:
public class CustomValidator implements ConstraintValidator<ValidCustom, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && value.matches("[A-Z]+"); } }
-
-
Global Exception Handling
- Centralize exception handling to provide consistent error responses.
-
Group Validations
-
Use validation groups to apply constraints conditionally.
public interface CreateGroup {} public interface UpdateGroup {} @NotNull(groups = CreateGroup.class) private String name;
-
-
Property-Level Validation
- Use
@Validated
at the class level to validate properties (e.g., in@ConfigurationProperties
).
- Use
-
Test Validation
- Write unit tests for validation to ensure constraints are working correctly.
Here’s a simple example of a REST API with validation:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@PostMapping
public ResponseEntity<String> createProduct(@Valid @RequestBody ProductRequest request) {
// Save product logic
return ResponseEntity.status(HttpStatus.CREATED).body("Product created successfully");
}
}
public class ProductRequest {
@NotNull(message = "Name cannot be null")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
private String name;
@NotNull(message = "Price is required")
@Positive(message = "Price must be a positive number")
private BigDecimal price;
// Getters and Setters
}
- Ease of Use: Integrates seamlessly with Spring MVC.
- Extensibility: Supports custom validators and annotations.
- Consistency: Automatically returns standardized error messages.
- Separation of Concerns: Keeps validation logic separate from business logic.
Would you like to see a more specific example or explore any of the best practices in detail?
Handling errors in Spring Boot applications efficiently is crucial for providing a better user experience and maintaining application stability. Here’s a detailed guide on error handling in Spring Boot, along with best practices:
Spring Boot provides a default error-handling mechanism. When an exception is thrown, Spring returns an error response with:
- HTTP status code (e.g.,
404
,500
). - A default error body in JSON format.
Example response for a 404 Not Found
error:
{
"timestamp": "2024-12-16T12:34:56.789+00:00",
"status": 404,
"error": "Not Found",
"path": "/api/items/1"
}
However, default error handling might not be sufficient, especially for custom exceptions or specific error formats.
Define custom exceptions for specific error scenarios.
Example:
public class ItemNotFoundException extends RuntimeException {
public ItemNotFoundException(String message) {
super(message);
}
}
@ControllerAdvice
allows you to handle exceptions globally across all controllers.
Example:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ItemNotFoundException.class)
public ResponseEntity<ErrorResponse> handleItemNotFoundException(ItemNotFoundException ex) {
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"An unexpected error occurred",
System.currentTimeMillis()
);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Custom ErrorResponse class:
public class ErrorResponse {
private int status;
private String message;
private long timestamp;
public ErrorResponse(int status, String message, long timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
// Getters and Setters
}
Use the custom exception in your code:
@RestController
@RequestMapping("/api/items")
public class ItemController {
@GetMapping("/{id}")
public Item getItemById(@PathVariable Long id) {
return itemService.findById(id)
.orElseThrow(() -> new ItemNotFoundException("Item with ID " + id + " not found"));
}
}
Override the default error response structure using a @ControllerAdvice
or by customizing the ErrorAttributes
bean.
Example of overriding ErrorAttributes
:
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Map;
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("customMessage", "Something went wrong. Please try again later.");
return errorAttributes;
}
}
Use appropriate HTTP status codes in error responses:
400 Bad Request
: For invalid input or bad request payload.401 Unauthorized
: For unauthenticated access.403 Forbidden
: For unauthorized access.404 Not Found
: When a resource does not exist.500 Internal Server Error
: For unexpected server-side errors.
Do not expose sensitive details like stack traces or database queries in error messages.
- Log exceptions at appropriate levels (
INFO
,WARN
,ERROR
) using a logger like SLF4J. - Use unique identifiers (e.g., error codes) to correlate logs with user-reported issues.
Validate user input to prevent errors caused by invalid data. Use the Bean Validation API (e.g., @NotNull
, @Size
) and handle validation exceptions using @ControllerAdvice
.
Example:
@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);
}
Ensure all error responses follow a consistent format:
{
"status": 404,
"message": "Item not found",
"timestamp": 1677889012345
}
For critical errors, implement fallback mechanisms (e.g., return a default response or use a circuit breaker like Resilience4j).
Document expected errors and their formats in your API documentation (e.g., using Swagger/OpenAPI).
src/main/java/com/example/demo/
├── controller/
│ ├── ItemController.java
├── exception/
│ ├── ItemNotFoundException.java
│ ├── GlobalExceptionHandler.java
├── model/
│ ├── ErrorResponse.java
├── service/
│ ├── ItemService.java
├── repository/
│ ├── ItemRepository.java
- Use
@ControllerAdvice
and@ExceptionHandler
for centralized exception handling. - Return meaningful HTTP status codes and error messages.
- Validate inputs to prevent unnecessary errors.
- Avoid exposing sensitive information in error responses.
- Follow consistent error response formats.
Would you like assistance implementing a specific type of error handler?