The Spring Framework is the backbone of modern Java development. From building robust web applications to creating scalable microservices, Spring empowers developers with tools that promote clean architecture, testability, and modular design.
In this complete guide, we’ll walk through everything you need to know about Spring Framework in Java — starting from core concepts like Inversion of Control (IoC) and Dependency Injection, all the way to advanced features like Spring MVC, Spring Security, AOP, and Spring Boot Actuators.
Whether you’re a beginner looking to understand the fundamentals or an experienced developer seeking to refresh and level up your skills, this guide provides real-world examples, annotations, configurations, and best practices to help you build production-ready Java applications with confidence.
Let’s get started with mastering Spring — one of the most essential frameworks in the Java ecosystem.
Contents
- Inversion of Control (IoC)
- Dependency Injection (DI)
- Essential Spring Annotations
- IoC Container & Bean Lifecycle
- Getting Started with Spring Boot
- Spring Web MVC
- Spring Validations
- WebMvcConfigurer & Message Converters
- Spring JDBC Template
- Spring Boot Actuators
- Aspect-Oriented Programming (AOP)
- Spring Security
- Conclusion
Inversion of Control (IoC)
Inversion of Control (IoC) is a core design principle where the responsibility for creating and managing objects is handed over to a framework. In Spring, this responsibility is handled by the IoC container, usually accessed through the ApplicationContext
.
Rather than having classes instantiate their own dependencies, you define these dependencies (beans) through configuration—using annotations like @Component
, @Autowired
, or XML/Java-based config. At runtime, the Spring container creates the beans and injects their required dependencies.
This approach promotes loose coupling, as classes no longer need to know how their dependencies are created or where they come from. They simply rely on abstractions (such as interfaces), making the codebase more modular and flexible.
Additionally, because dependencies are injected, it's easy to substitute mock implementations during testing—without modifying the actual business logic. This leads to better testability and cleaner unit tests.
Dependency Injection (DI)
Dependency Injection (DI) is the core mechanism that enables Inversion of Control (IoC) in the Spring Framework. Instead of classes creating their own dependencies, Spring takes care of supplying them—usually via the Spring IoC container.
In simple terms, DI allows objects to receive their required dependencies from an external source, rather than constructing them internally. This promotes separation of concerns, makes components easier to test, and encourages modular design.
Spring provides several ways to inject dependencies:
Constructor Injection: Best suited for required dependencies. Promotes immutability and is ideal for testing.
Setter Injection: Used for optional or configurable dependencies. Easier to override in certain scenarios.
Field Injection: A concise approach that uses annotations directly on class fields—but not ideal for unit testing or immutability.
Constructor vs Setter vs Field Injection
Injection Type | Use Case | Pros | Cons |
---|---|---|---|
Constructor | Required dependencies | Immutable, testable | Verbose with many dependencies |
Setter | Optional/configurable props | Easy to override | Allows partially constructed beans |
Field | Convenience | Less boilerplate | Harder to test, less flexible |
Dependency Injection is the foundation of loose coupling in Spring. By letting Spring handle dependency wiring, your code becomes more maintainable, easier to test, and flexible to change and scale.
Essential Spring Annotations
Annotation | Description |
---|---|
@Component | Generic stereotype to mark a class as a Spring-managed bean. Enables automatic detection and registration in the Spring context. |
@Service | Specialized @Component for service-layer classes (business logic). Adds semantic clarity. |
@Repository | Specialized @Component for data access layer (DAOs). Enables exception translation. |
@Controller | Declares a class as a Spring MVC controller for handling web requests. |
@RestController | Combines @Controller and @ResponseBody to simplify building RESTful web services. |
@Autowired | Automatically injects dependencies by type into constructors, setters, or fields. |
@Qualifier | Helps resolve ambiguity when multiple beans of the same type exist—used with @Autowired . |
@Primary | Marks one bean as the default to use during autowiring when multiple options exist. |
@ConditionalOnProperty | Registers a bean only if a specific property exists in the configuration file. |
@Conditional | Registers a bean based on custom logic or environment conditions. |
@Profile | Activates beans only for specific profiles. |
IoC Container & Bean Lifecycle
Spring’s IoC (Inversion of Control) container is responsible for managing the lifecycle, dependencies, and configuration of application objects known as beans. It creates and injects these beans based on metadata defined through annotations or XML.
Spring provides two main container implementations:
ApplicationContext
– commonly used and feature-rich (supports AOP, events, etc.)BeanFactory
– the basic, lightweight container (now mostly internal)
Bean lifecycle phases
- Instantiation – The container creates the bean.
- Dependency Injection – Dependencies are injected via constructor, setter, or field.
- Initialization – Custom logic via
@PostConstruct
orafterPropertiesSet()
(fromInitializingBean
). - Bean Ready for Use – The bean is now fully initialized.
- Destruction – Cleanup via
@PreDestroy
ordestroy()
(fromDisposableBean
), triggered on context shutdown.
Tip: You can hook into this lifecycle using annotations or lifecycle interfaces to execute custom logic at specific stages.
Getting Started with Spring Boot
Spring Boot makes it incredibly easy to bootstrap a new Spring application. Here's how to create a new Spring Boot project using Spring Initializr:
Steps to Create a Spring Boot App
- Go to https://start.spring.io
- Choose the following settings:
- Project: Maven or Gradle
- Language: Java
- Spring Boot version: Latest stable (e.g., 3.4.5)
- Group:
com.example
- Artifact:
myapp
- Name:
MyApp
- Packaging: Jar
- Java: Latest version (e.g., 21)
- Dependencies: Add
Spring Web
 for now. (Others as needed)Spring Data JPA
,Spring Security
.
- Click Generate to download a
.zip
file.
Unzip and Run the application directly from command line (Dev Mode):
# For Maven projects
./mvnw spring-boot:run
# For Gradle projects
./gradlew bootRun
Also, open the project in your favorite IDE (e.g., IntelliJ IDEA or Eclipse)Â and Run the main class (@SpringBootApplication
).
Additionally, you can build and run as executable jar (Production Mode):
# Maven
mvn clean package
java -jar target/myapp-0.0.1-SNAPSHOT.jar
# Gradle
./gradlew build
java -jar build/libs/myapp-0.0.1-SNAPSHOT.jar
You can verify the following logs to confirm the application started successfully:
[INFO] [TomcatWebServer] Tomcat started on port 8080 (http) with context path '/'
[INFO] [MyAppApplication] Started MyAppApplication in 0.792 seconds (process running for 0.975)
Your Spring Boot application is now up and running! You can visit http://localhost:8080
to verify.
You will get a Whitelabel Error Page as there are no request mappings yet. Press Ctrl+C
to stop the server.
Spring Boot Configuration
Spring Boot simplifies configuration via application.properties
or application.yaml
.
Add the following properties to the configuration file, then restart the application.
# application.properties
# Changes default port from 8080 to 8081
server.port=8081
# Sets the base context path
server.servlet.context-path=/myapp
# application.yaml
server:
port: 8081 # Changes default port from 8080 to 8081
servlet:
context-path: /myapp # Sets the base context path
You should see the updated logs as below:
[INFO] [TomcatWebServer] Tomcat started on port 8081 (http) with context path '/myapp'
[INFO] [MyAppApplication] Started MyAppApplication in 0.747 seconds (process running for 0.927)
You can visit http://localhost:8081/myapp
to verify the application is up and running!
Use @SpringBootConfiguration
(meta-annotated on @SpringBootApplication
) for bootstrapping custom config classes.
Spring Web MVC
Spring Web MVC is a powerful framework in the Spring ecosystem for building REST APIs and web applications using the Model-View-Controller (MVC) pattern.
It provides a clean separation of concerns between:
- Controller: Handles HTTP requests
- Model: Contains application data/business logic
- View: Renders the response (HTML or JSON)
The DispatcherServlet
acts as the front controller that routes incoming requests to the correct handler.
Core Annotations
Annotation | Purpose |
---|---|
@RestController | Marks a class as a REST API controller |
@GetMapping | Maps GET requests (e.g., read data) |
@PostMapping | Maps POST requests (e.g., create data) |
@RequestBody | Binds JSON request payload to a Java object |
@RequestParam | Binds a query parameter from the URL |
@PathVariable | Binds a dynamic path segment (e.g., /users/{id} ) |
Example
UserDTO
(data transfer object)
public class UserDTO {
private String username;
private String email;
private String password;
// Getters and Setters
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
}
UserService
(business logic)
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
// Dummy in-memory storage
private final Map<String, UserDTO> userStore = new HashMap<>();
public String registerUser(UserDTO userDTO) {
userStore.put(userDTO.getUsername(), userDTO);
logger.info("User registered: {}", userDTO.getUsername());
return "User '" + userDTO.getUsername() + "' registered successfully!";
}
public UserDTO findByUsername(String username) {
logger.info("Finding by username: {}", username);
return userStore.get(username);
}
}
UserController
(uses UserService
)
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired // Field injection
private UserService userService;
/*
@Autowired // constructor injection (recommended)
public UserController(UserService userService) {
this.userService = userService;
}
*/
/**
* Register a new user via POST
* Example: POST /api/users/register
*/
@PostMapping("/register")
public String registerUser(@RequestBody UserDTO userDTO) {
return userService.registerUser(userDTO);
}
/**
* Get user by query param
* Example: GET /api/users/find?username=srimanta
*/
@GetMapping("/find")
public UserDTO getUserByQueryParam(@RequestParam String username) {
return userService.findByUsername(username);
}
/**
* Get user by path variable
* Example: GET /api/users/srimanta
*/
@GetMapping("/{username}")
public UserDTO getUserByPath(@PathVariable String username) {
return userService.findByUsername(username);
}
}
Sample cURL Test
$ curl -X POST http://localhost:8081/myapp/api/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "srimanta",
"email": "srimanta@xmail.com",
"password": "srimanta@123"
}'
User 'srimanta' registered successfully!
$
$ curl -X GET "http://localhost:8081/myapp/api/users/find?username=srimanta"
{"username":"srimanta","email":"srimanta@xmail.com","password":"srimanta@123"}
$
$ curl http://localhost:8081/myapp/api/users/srimanta
{"username":"srimanta","email":"srimanta@xmail.com","password":"srimanta@123"}
Sample console logs
[INFO] [UserService] User registered: srimanta
[INFO] [UserService] Finding by username: srimanta
[INFO] [UserService] Finding by username: srimanta
Spring Validations
Spring Validation is used to automatically validate user input or request data in Spring applications — particularly in REST APIs and form submissions.
It integrates with JSR-303/JSR-380 Bean Validation (Jakarta Bean Validation) and allows you to apply validation rules directly on model fields using annotations.
Common Validation Annotations
Annotation | Purpose |
---|---|
@NotNull | Field must not be null |
@NotBlank | Must not be empty or whitespace |
@Email | Must be a valid email address |
@Size(min, max) | Enforces string or collection size |
@Min , @Max | Set numeric limits |
How It Works
Use @Valid
or @Validated
to trigger validation.
@Valid
→ on method parameters (e.g.,@RequestBody
)@Validated
→ on class level (supports groups and advanced validation)
If validation fails, Spring throws a MethodArgumentNotValidException
.
Example
Add dependencies to pom.xml
/ build.gradle
:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
UserDTO
(with constraints)
public class UserDTO {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
private String username;
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
// Getters and Setters
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
}
UserController
with @Validated
and @Valid
@RestController
@RequestMapping("/api/users")
@Validated // Enables method-level validation (useful for path variables, service params, etc.)
public class UserController {
@Autowired // Field injection
private UserService userService;
/**
* Register a new user via POST
* Example: POST /api/users/register
*/
@PostMapping("/register")
public String registerUser(@Valid @RequestBody UserDTO userDTO) {
return userService.registerUser(userDTO);
}
/**
* Get user by query param
* Example: GET /api/users/find?username=srimanta
*/
@GetMapping("/find")
public UserDTO getUserByQueryParam(@RequestParam String username) {
return userService.findByUsername(username);
}
/**
* Get user by path variable
* Example: GET /api/users/srimanta
*/
@GetMapping("/{username}")
public UserDTO getUserByPath(@PathVariable String username) {
return userService.findByUsername(username);
}
}
GlobalExceptionHandler
(handle validation errors gracefully)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
return errors;
}
}
Sample cURL Test
$ curl -X POST http://localhost:8081/myapp/api/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "du",
"email": "dummy@.com",
"password": ""
}'
{"username":"Username must be between 3 and 20 characters","email":"Invalid email format","password":"Password must be at least 6 characters"}
WebMvcConfigurer & Message Converters
Spring WebMvcConfigurer is an interface used to customize Spring MVC behavior without overriding the full configuration.
It's ideal for adding:
- Custom message converters
- HTTP interceptors
- Global CORS settings
- Formatters, argument resolvers, and more
Message converters in Spring handle the conversion between Java objects and HTTP request/response bodies.
They’re used internally by @RequestBody
and @ResponseBody
.
Example
Here’s an example of a REST controller that generates both JSON and XML responses using content negotiation with HttpMessageConverter
s.
Add dependencies to pom.xml
/ build.gradle
:
<!-- Maven -->
<!-- JSON (comes with Spring Boot Starter Web) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- XML support -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
// Gradle
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
UserDTO
(with Jackson annotations for better XML)
@JacksonXmlRootElement(localName = "user")
public class UserDTO {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
@JsonProperty("username")
private String username;
@Email(message = "Invalid email format")
@JsonProperty("email")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // excluded from response (serialization)
private String password;
// Constructors
public UserDTO() {
}
public UserDTO(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
// Getters and Setters
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
}
WebMvcConfigurer
Implementation
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// JSON converter
converters.add(new MappingJackson2HttpMessageConverter(new ObjectMapper()));
// XML converter
converters.add(new MappingJackson2XmlHttpMessageConverter(new XmlMapper()));
}
}
UserController
@RestController
@RequestMapping("/api/users")
@Validated // Enables method-level validation (useful for path variables, service params, etc.)
public class UserController {
@Autowired // Field injection
private UserService userService;
/**
* Register a new user via POST
* Example: POST /api/users/register
*/
@PostMapping("/register")
public String registerUser(@Valid @RequestBody UserDTO userDTO) {
return userService.registerUser(userDTO);
}
/**
* Get user by query param
* Example: GET /api/users/find?username=srimanta
*/
@GetMapping("/find")
public UserDTO getUserByQueryParam(@RequestParam String username) {
return userService.findByUsername(username);
}
/**
* Get user by path variable
* Example: GET /api/users/srimanta
*/
@GetMapping(value = "/{username}", produces = {"application/json", "application/xml"})
public UserDTO getUserByPath(@PathVariable String username) {
return userService.findByUsername(username);
}
}
Sample cURL Test
$ curl -X POST http://localhost:8081/myapp/api/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "srimanta",
"email": "srimanta@xmail.com",
"password": "srimanta@123"
}'
User 'srimanta' registered successfully!
$
$ curl http://localhost:8081/myapp/api/users/srimanta
{"username":"srimanta","email":"srimanta@xmail.com"}
$
$ curl http://localhost:8081/myapp/api/users/srimanta \
-H "Accept: application/xml"
<user><username>srimanta</username><email>srimanta@xmail.com</email></user>
Spring JDBC Template
Spring JDBC Template is a utility class in the Spring Framework that simplifies database operations like querying, inserting, updating, and deleting data using plain SQL.
It significantly reduces boilerplate code by handling:
- Connection management
- Statement preparation
- ResultSet iteration
- Exception handling
Key Features
- Simplifies common JDBC tasks
- Executes SQL queries and updates easily
- Integrates with Spring's transaction management
- Uses
RowMapper
to convertResultSet
rows into Java objects
Example
Add dependencies to pom.xml
/ build.gradle
:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId> <!-- Use H2 for testing -->
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
Add the following properties to the configuration file:
# application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=none
# application.yaml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password: "" # Empty password
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: none # Disable auto schema generation
SQL Schema (schema.sql)
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(20) NOT NULL,
email VARCHAR(100),
password VARCHAR(60) NOT NULL
);
Spring Boot will automatically pick this file from src/main/resources
.
User
Model
public class User {
private Long id;
private String username;
private String email;
private String password;
// Constructors
public User() {
}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
// Getters & Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
UserRepository
using JdbcTemplate
and NamedParameterJdbcTemplate
(named parameters to prevent SQL injection with ?
placeholders)
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public int registerUser(User user) {
String sql = "INSERT INTO users (username, email, password) VALUES (?, ?, ?)";
return jdbcTemplate.update(sql, user.getUsername(), user.getEmail(), user.getPassword());
}
public Optional findUserByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = :username";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("username", username);
List users = namedParameterJdbcTemplate.query(sql, params, new BeanPropertyRowMapper<>(User.class));
return users.stream().findFirst();
}
}
UserService
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
private UserRepository userRepository;
public String registerUser(UserDTO userDTO) {
if (findByUsername(userDTO.getUsername()) != null)
throw new RuntimeException("User already exists");
User user = new User(userDTO.getUsername(), userDTO.getEmail(), userDTO.getPassword());
int update = userRepository.registerUser(user);
String status = update == 1 ? "successful" : "failed";
logger.info("User '{}' registration {}!", userDTO.getUsername(), status);
return "User '" + userDTO.getUsername() + "' registration " + status;
}
public User findByUsername(String username) {
logger.info("Finding by username: {}", username);
Optional user = userRepository.findUserByUsername(username);
return user.orElse(null);
}
}
UserController
@RestController
@RequestMapping("/api/users")
@Validated // Enables method-level validation (useful for path variables, service params, etc.)
public class UserController {
@Autowired // Field injection
private UserService userService;
/**
* Register a new user via POST
* Example: POST /api/users/register
*/
@PostMapping("/register")
public String registerUser(@Valid @RequestBody UserDTO userDTO) {
return userService.registerUser(userDTO);
}
/**
* Get user by query param
* Example: GET /api/users/find?username=srimanta
*/
@GetMapping("/find")
public User getUserByQueryParam(@RequestParam String username) {
return userService.findByUsername(username);
}
/**
* Get user by path variable
* Example: GET /api/users/srimanta
*/
@GetMapping(value = "/{username}", produces = {"application/json", "application/xml"})
public User getUserByPath(@PathVariable String username) {
return userService.findByUsername(username);
}
}
Sample cURL Test
$ curl -X POST http://localhost:8081/myapp/api/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "srimanta",
"email": "srimanta@xmail.com",
"password": "srimanta@123"
}'
User 'srimanta' registration successful
$
$ curl -X GET "http://localhost:8081/myapp/api/users/find?username=srimanta"
{"id":1,"username":"srimanta","email":"srimanta@xmail.com","password":"srimanta@123"}
Spring Boot Actuators
Spring Boot Actuator provides production-ready monitoring and diagnostics for your application with minimal configuration. It exposes a set of built-in endpoints that give insights into the system's internals — making it easier to monitor, manage, and debug applications in real-time.
Common Endpoints
/actuator/health
– Shows application health status (e.g., database, disk, service availability)/actuator/metrics
– Exposes system and custom metrics like memory, CPU, thread usage/actuator/info
– Displays metadata (e.g., app version, description)/actuator/env
– Lists environment variables and config values/actuator/loggers
– Allows runtime management of logging levels
Actuator endpoints can be enabled, secured, and customized via application.properties|yaml
.
They're also easily integrated with observability tools like Prometheus, Grafana, and Spring Boot Admin.
Example
Add dependencies to pom.xml
/ build.gradle
:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Add the following properties to the configuration file:
# application.properties
# Expose useful actuator endpoints
management.endpoints.web.exposure.include=health, info, metrics, env, beans
# Info endpoint configuration
management.info.env.enabled=true
# Health endpoint details
management.endpoint.health.show-details=always
# Optional: Customize context path
management.endpoints.web.base-path=/actuator
# Optional: Info Metadata
info.app.name=MySpringBootApp
info.app.description=Spring Boot app with actuator endpoints
info.app.version=1.0.0
# application.yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,beans
info:
env:
enabled: true
endpoint:
health:
show-details: always
info:
app:
name: "MySpringBootApp"
description: "Spring Boot app with actuator endpoints"
version: "1.0.0"
Add the UserController
(already defined in previous code examples).
Restart the app and verify the endpoints:
http://localhost:8081/myapp/actuator/health
http://localhost:8081/myapp/actuator/info
http://localhost:8081/myapp/actuator/metrics
Create a custom health check by implementing HealthIndicator
.
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Health health() {
// perform health check logic (DB ping, dependent server ping, etc.)
try {
jdbcTemplate.execute("SELECT 1");
return Health.up().withDetail("H2 Database", "Available").build();
} catch (Exception e) {
return Health.down(e).withDetail("H2 Database", "Unavailable").build();
}
}
}
Sample cURL Test
$ curl http://localhost:8081/myapp/actuator/info
{"app":{"name":"MySpringBootApp","description":"Spring Boot app with actuator endpoints","version":"1.0.0"}}
$
$ curl http://localhost:8081/myapp/actuator/health
{"status":"UP","components":{"custom":{"status":"UP","details":{"H2 Database":"Available"}},"db":{"status":"UP","details":{"database":"H2","validationQuery":"isValid()"}},"diskSpace":{"status":"UP","details":{"total":245107195904,"free":8402948096,"threshold":10485760,"path":"/Users/srimantasahu/Repo/myapp-gradle/.","exists":true}},"ping":{"status":"UP"},"ssl":{"status":"UP","details":{"validChains":[],"invalidChains":[]}}}}
Aspect-Oriented Programming (AOP)
Aspect-Oriented Programming (AOP) in Spring allows you to separate cross-cutting concerns like logging, security, and transactions from your core business logic. Instead of scattering these concerns across multiple classes, you define them in one place as aspects.
Spring AOP uses proxy-based weaving to apply behavior at runtime without modifying the actual code.
Common Concepts & Annotations
- Aspect – A class that defines cross-cutting logic using the
@Aspect
annotation. - Advice – The actual action (e.g., code to run before or after a method).
@Before
– Run before method execution@After
– Run after method execution@Around
– Run before and after (can control execution flow)
- Join Point – A point in program execution (e.g., method call)
- Pointcut – An expression to match join points (e.g., methods in a package)
AOP makes your code cleaner and more modular by letting you apply functionality like logging or transaction management without cluttering the business logic.
Example
Add dependencies to pom.xml
/ build.gradle
:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'
LoggingAspect
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Before("execution(* com.example.myapp.controller.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
logger.info("Entering method: {}", joinPoint.getSignature().toShortString());
}
@AfterReturning(pointcut = "execution(* com.example.myapp.controller.*.*(..))", returning = "result")
public void logAfter(JoinPoint joinPoint, Object result) {
logger.info("Exiting method: {} with result = {}", joinPoint.getSignature().getName(), result);
}
}
Restart the application, then run the cURL test again — you’ll notice log entries printed to the console reflecting the aspect-based logging behavior.
[INFO] [LoggingAspect] Entering method: UserController.registerUser(..)
[INFO] [UserService] Finding by username: srimanta
[INFO] [UserService] User 'srimanta' registration successful!
[INFO] [LoggingAspect] Exiting method: registerUser with result = User 'srimanta' registration successful
[INFO] [LoggingAspect] Entering method: UserController.getUserByPath(..)
[INFO] [UserService] Finding by username: srimanta
[INFO] [LoggingAspect] Exiting method: getUserByPath with result = com.example.myapp.model.User@310648fa
Breakdown of the execution(* com.example.myapp.controller.*.*(..))
expression:
*
: Any return type.com.example.myapp.controller.*
: Any class in the controller package..*(..)
: Any method with any number/type of parameters.
Use Case: Great for logging entry into controller methods, auth checks, or input validation.
Spring Security
Spring Security is a comprehensive and customizable framework for handling authentication and authorization in Java applications. It helps protect your application from common security threats and simplifies the process of securing both web and REST-based APIs.
It supports a variety of security mechanisms including:
- Form-based and Basic Authentication
- OAuth2 and JWT-based token authentication
- Role-based access control
- Session and stateless security
Spring Security uses a filter chain to intercept and process HTTP requests before they reach controllers, allowing fine-grained access control and centralized security logic.
Example
Add dependencies to pom.xml
/ build.gradle
:
<!-- Maven -->
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
// Gradle
// Spring Boot Starter Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// JWT Dependencies
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
SecurityConfig
Configuration (Lambda DSL)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/users/register", "/api/auth/login", "/error").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(UserDetailsService uds, PasswordEncoder encoder) throws Exception {
return new ProviderManager(new DaoAuthenticationProvider() {{
setUserDetailsService(uds);
setPasswordEncoder(encoder);
}});
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JWTAuthFilter
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
jwt = authHeader.substring(7);
username = jwtService.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(token);
}
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"" + e.getMessage() + "\"}");
return;
}
}
chain.doFilter(request, response);
}
}
JWTService
@Component
public class JwtService {
@Value("${jwt.secret}")
// In Spring, @Value or @ConfigurationProperties fields aren't populated yet when the constructor runs (for field injection).
private String jwtSecret;
private SecretKey key;
@PostConstruct // Runs after dependency injection is complete (after Spring has populated all fields).
private void postConstruct() { // Clean separation: constructor → simple wiring, @PostConstruct → post-wiring logic.
this.key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSecret));
}
public String generateToken(String username) {
return Jwts.builder()
.subject(username)
.issuedAt(Date.from(Instant.now()))
.expiration(Date.from(Instant.now().plus(Duration.ofHours(1)))) // 1 hour
.signWith(key)
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
return extractUsername(token).equals(userDetails.getUsername());
}
}
UserDetailsService
Implementation
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional user = userRepository.findUserByUsername(username);
if (user.isEmpty())
throw new UsernameNotFoundException("User not found");
return new org.springframework.security.core.userdetails.User(
user.get().getUsername(), user.get().getPassword(), new ArrayList<>()
);
}
}
UserService
(store encoded password)
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
private UserRepository userRepository;
public String registerUser(UserDTO userDTO) {
if (findByUsername(userDTO.getUsername()) != null)
throw new RuntimeException("User already exists");
String encodedPassword = new BCryptPasswordEncoder().encode(userDTO.getPassword());
User user = new User(userDTO.getUsername(), userDTO.getEmail(), encodedPassword);
int update = userRepository.registerUser(user);
String status = update == 1 ? "successful" : "failed";
logger.info("User '{}' registration {}!", userDTO.getUsername(), status);
return "User '" + userDTO.getUsername() + "' registration " + status;
}
public User findByUsername(String username) {
logger.info("Finding by username: {}", username);
Optional user = userRepository.findUserByUsername(username);
return user.orElse(null);
}
}
AuthController
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtService jwtService;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody UserDTO userDTO) {
userService.registerUser(userDTO);
return ResponseEntity.ok("User registered");
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody UserDTO userDTO) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(userDTO.getUsername(), userDTO.getPassword()));
String token = jwtService.generateToken(userDTO.getUsername());
return ResponseEntity.ok(Collections.singletonMap("token", token));
}
@GetMapping("/secure-check")
public ResponseEntity secureCheck(Principal principal) {
return ResponseEntity.ok("Accessed secured endpoint by user: " + principal.getName());
}
}
Add the following properties to the configuration file, then restart the application.
# application.properties
# Secret Key (YOUR_GENERATED_BASE64_SECRET_KEY)
jwt.secret=wXRc/fUYMb2IAyXRvB4NYzyLws9oimd226c7nqNqs/fT1dbUewJGN/SEuq/5lQROLVDj62NhyR7AhbTI5gY/FA==
# application.yaml
# Secret Key (YOUR_GENERATED_BASE64_SECRET_KEY)
jwt:
secret: wXRc/fUYMb2IAyXRvB4NYzyLws9oimd226c7nqNqs/fT1dbUewJGN/SEuq/5lQROLVDj62NhyR7AhbTI5gY/FA==
Generate a secret key like:
byte[] key = new byte[64];
new SecureRandom().nextBytes(key);
System.out.println(Base64.getEncoder().encodeToString(key));
Sample cURL Test
$ curl http://localhost:8081/myapp/api/auth/secure-check
{"timestamp":"2025-05-09T04:47:06.588+00:00","status":403,"error":"Forbidden","path":"/myapp/api/auth/secure-check"}
$
$ curl -X POST http://localhost:8081/myapp/api/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "srimanta",
"email": "srimanta@xmail.com",
"password": "srimanta@123"
}'
User 'srimanta' registration successful
$
$ curl -X POST http://localhost:8081/myapp/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "srimanta",
"password": "srimanta@123"
}'
{"token":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzcmltYW50YSIsImlhdCI6MTc0Njc2NjA0OSwiZXhwIjoxNzQ2NzY5NjQ5fQ.9m0xk0PzvfnPG7kfyxjMkNZGhR0Ser7s6zTPEErYrddYBVaZmnmI4_KReEnoL7HHt0HPXGg3_x7cGc99VG7T9w"}
$
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzcmltYW50YSIsImlhdCI6MTc0Njc2NjA0OSwiZXhwIjoxNzQ2NzY5NjQ5fQ.9m0xk0PzvfnPG7kfyxjMkNZGhR0Ser7s6zTPEErYrddYBVaZmnmI4_KReEnoL7HHt0HPXGg3_x7cGc99VG7T9w" http://localhost:8081/myapp/api/auth/secure-check
Accessed secured endpoint by user: srimanta
Console Logs
[INFO] [LoggingAspect] Entering method: UserController.registerUser(..)
[INFO] [UserService] Finding by username: srimanta
[INFO] [UserService] User 'srimanta' registration successful!
[INFO] [LoggingAspect] Exiting method: registerUser with result = User 'srimanta' registration successful
[INFO] [LoggingAspect] Entering method: AuthController.login(..)
[INFO] [LoggingAspect] Exiting method: login with result = <200 OK OK,{token=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzcmltYW50YSIsImlhdCI6MTc0Njc2NjA0OSwiZXhwIjoxNzQ2NzY5NjQ5fQ.9m0xk0PzvfnPG7kfyxjMkNZGhR0Ser7s6zTPEErYrddYBVaZmnmI4_KReEnoL7HHt0HPXGg3_x7cGc99VG7T9w},[]>
[INFO] [LoggingAspect] Entering method: AuthController.secureCheck(..)
[INFO] [LoggingAspect] Exiting method: secureCheck with result = <200 OK OK,Accessed secured endpoint by user: srimanta,[]>
Conclusion
Spring empowers Java developers to build clean, maintainable, and scalable applications. With features like IoC, MVC, AOP, security, and powerful configuration management, it provides everything needed for full-stack enterprise development. Mastering these core concepts opens the door to productivity, flexibility, and production-grade performance in real-world applications.
📌 Enjoyed this post? Bookmark it and drop a comment below — your feedback helps keep the content insightful and relevant!
This guide is the perfect starting point for Java developers new to Spring. It delivers a clear, structured introduction to key concepts like dependency injection and Spring Boot, backed by real-world examples that bridge theory and practice. Both informative and practical, it’s an invaluable resource for building a strong Spring foundation.