Spring Framework in Java: The Complete Guide for Developers

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.


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 TypeUse CaseProsCons
ConstructorRequired dependenciesImmutable, testableVerbose with many dependencies
SetterOptional/configurable propsEasy to overrideAllows partially constructed beans
FieldConvenienceLess boilerplateHarder 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

AnnotationDescription
@ComponentGeneric stereotype to mark a class as a Spring-managed bean. Enables automatic detection and registration in the Spring context.
@ServiceSpecialized @Component for service-layer classes (business logic). Adds semantic clarity.
@RepositorySpecialized @Component for data access layer (DAOs). Enables exception translation.
@ControllerDeclares a class as a Spring MVC controller for handling web requests.
@RestControllerCombines @Controller and @ResponseBody to simplify building RESTful web services.
@AutowiredAutomatically injects dependencies by type into constructors, setters, or fields.
@QualifierHelps resolve ambiguity when multiple beans of the same type exist—used with @Autowired.
@PrimaryMarks one bean as the default to use during autowiring when multiple options exist.
@ConditionalOnPropertyRegisters a bean only if a specific property exists in the configuration file.
@ConditionalRegisters a bean based on custom logic or environment conditions.
@ProfileActivates 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 or afterPropertiesSet() (from InitializingBean).
  • Bean Ready for Use – The bean is now fully initialized.
  • Destruction – Cleanup via @PreDestroy or destroy() (from DisposableBean), 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

AnnotationPurpose
@RestControllerMarks a class as a REST API controller
@GetMappingMaps GET requests (e.g., read data)
@PostMappingMaps POST requests (e.g., create data)
@RequestBodyBinds JSON request payload to a Java object
@RequestParamBinds a query parameter from the URL
@PathVariableBinds 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

AnnotationPurpose
@NotNullField must not be null
@NotBlankMust not be empty or whitespace
@EmailMust be a valid email address
@Size(min, max)Enforces string or collection size
@Min, @MaxSet 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 HttpMessageConverters.

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 convert ResultSet 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!

Share the Knowledge!
5 5 votes
Article Rating
Subscribe
Notify of
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
kvvssut
April 28, 2025 3:07 PM

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.