30 āļ„āļģāļ–āļēāļĄāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot: āļ„āļđāđˆāļĄāļ·āļ­āļ‰āļšāļąāļšāđ€āļ•āđ‡āļĄāļŠāļģāļŦāļĢāļąāļšāļ™āļąāļāļžāļąāļ’āļ™āļē Java

āđ€āļ•āļĢāļĩāļĒāļĄāļžāļĢāđ‰āļ­āļĄāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot āļ”āđ‰āļ§āļĒ 30 āļ„āļģāļ–āļēāļĄāļŦāļĨāļąāļāļ„āļĢāļ­āļšāļ„āļĨāļļāļĄ auto-configuration, starter, Spring Data JPA, āļ„āļ§āļēāļĄāļ›āļĨāļ­āļ”āļ āļąāļĒ āđāļĨāļ°āļāļēāļĢāļ—āļ”āļŠāļ­āļš

30 āļ„āļģāļ–āļēāļĄāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot āļŠāļģāļŦāļĢāļąāļšāļ™āļąāļāļžāļąāļ’āļ™āļē Java

Spring Boot āļāļĨāļēāļĒāđ€āļ›āđ‡āļ™āđ€āļŸāļĢāļĄāđ€āļ§āļīāļĢāđŒāļāļŦāļĨāļąāļāļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļžāļąāļ’āļ™āļē Java āđƒāļ™āļĢāļ°āļ”āļąāļšāļ­āļ‡āļ„āđŒāļāļĢ āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđ€āļŠāļīāļ‡āđ€āļ—āļ„āļ™āļīāļ„āļˆāļ°āļ›āļĢāļ°āđ€āļĄāļīāļ™āļ„āļ§āļēāļĄāđ€āļ‚āđ‰āļēāđƒāļˆāļāļĨāđ„āļāļ āļēāļĒāđƒāļ™ āļŦāļĨāļąāļāļ›āļāļīāļšāļąāļ•āļīāļ—āļĩāđˆāļ”āļĩ āđāļĨāļ°āļĢāļ°āļšāļšāļ™āļīāđ€āļ§āļĻāļ‚āļ­āļ‡ Spring āļ„āļđāđˆāļĄāļ·āļ­āļ‰āļšāļąāļšāļ™āļĩāđ‰āļĢāļ§āļĄ 30 āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆāļžāļšāļšāđˆāļ­āļĒ āļ•āļąāđ‰āļ‡āđāļ•āđˆāđāļ™āļ§āļ„āļīāļ”āļžāļ·āđ‰āļ™āļāļēāļ™āļˆāļ™āļ–āļķāļ‡āļŦāļąāļ§āļ‚āđ‰āļ­āļ‚āļąāđ‰āļ™āļŠāļđāļ‡

āđ€āļ„āļĨāđ‡āļ”āļĨāļąāļšāđ€āļ•āļĢāļĩāļĒāļĄāļ•āļąāļ§

āļœāļđāđ‰āļŠāļąāļĄāļ āļēāļĐāļ“āđŒāļŠāļ·āđˆāļ™āļŠāļ­āļšāļœāļđāđ‰āļŠāļĄāļąāļ„āļĢāļ—āļĩāđˆāđ€āļ‚āđ‰āļēāđƒāļˆ "āđ€āļŦāļ•āļļāļœāļĨ" āđ€āļšāļ·āđ‰āļ­āļ‡āļŦāļĨāļąāļ‡āļ‚āļ­āļ‡āđāļ•āđˆāļĨāļ°āļŸāļĩāđ€āļˆāļ­āļĢāđŒ āļ™āļ­āļāđ€āļŦāļ™āļ·āļ­āļˆāļēāļ syntax āļ„āļ§āļĢāļ­āļ˜āļīāļšāļēāļĒāļ§āđˆāļē Spring Boot āđāļāđ‰āļ›āļąāļāļŦāļēāļ­āļ°āđ„āļĢāđ„āļ”āđ‰

āļžāļ·āđ‰āļ™āļāļēāļ™āļ‚āļ­āļ‡ Spring Boot

1. Spring āļāļąāļš Spring Boot āļ•āđˆāļēāļ‡āļāļąāļ™āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Spring āđ€āļ›āđ‡āļ™āđ€āļŸāļĢāļĄāđ€āļ§āļīāļĢāđŒāļāđāļšāļšāđ‚āļĄāļ”āļđāļĨāļ—āļĩāđˆāđƒāļŦāđ‰ dependency injection, āļāļēāļĢāļˆāļąāļ”āļāļēāļĢāļ˜āļļāļĢāļāļĢāļĢāļĄ āđāļĨāļ°āļāļēāļĢāđ€āļŠāļ·āđˆāļ­āļĄāļ•āđˆāļ­āļāļąāļšāđ€āļ—āļ„āđ‚āļ™āđ‚āļĨāļĒāļĩāļŦāļĨāļēāļĒāļŠāļ™āļīāļ” āļŠāđˆāļ§āļ™ Spring Boot āļ„āļ·āļ­āđ€āļĨāđ€āļĒāļ­āļĢāđŒāļ™āļēāļĄāļ˜āļĢāļĢāļĄāļ—āļĩāđˆāļ­āļĒāļđāđˆāļšāļ™ Spring āđ€āļžāļ·āđˆāļ­āļ—āļģāđƒāļŦāđ‰āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđāļĨāļ°āļāļēāļĢāđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āđāļ­āļ›āļžāļĨāļīāđ€āļ„āļŠāļąāļ™āļ‡āđˆāļēāļĒāļ‚āļķāđ‰āļ™

Application.javajava
// With Spring Boot: a single annotation to start
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        // SpringApplication configures and starts the Spring context
        SpringApplication.run(Application.class, args);
    }
}

Spring Boot āļĄāļ­āļš auto-configuration, dependency starter, āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒāļāļąāļ‡āļ•āļąāļ§ āđāļĨāļ°āđ€āļĄāļ•āļĢāļīāļāļĢāļ°āļ”āļąāļšāđ‚āļ›āļĢāļ”āļąāļāļŠāļąāļ™āļœāđˆāļēāļ™ Actuator āđ€āļ›āđ‰āļēāļŦāļĄāļēāļĒāļ„āļ·āļ­āđ„āļ›āļˆāļēāļāđ„āļ­āđ€āļ”āļĩāļĒāļŠāļđāđˆāđ‚āļ„āđ‰āļ”āļ—āļĩāđˆāļ—āļģāļ‡āļēāļ™āđ„āļ”āđ‰āļ āļēāļĒāđƒāļ™āđ„āļĄāđˆāļāļĩāđˆāļ™āļēāļ—āļĩ

2. Auto-configuration āļ—āļģāļ‡āļēāļ™āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Auto-configuration āļ§āļīāđ€āļ„āļĢāļēāļ°āļŦāđŒ classpath āđāļĨāđ‰āļ§āļ•āļąāđ‰āļ‡āļ„āđˆāļē bean āļ—āļĩāđˆāļˆāļģāđ€āļ›āđ‡āļ™āđ‚āļ”āļĒāļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī āļāļĨāđ„āļāļ™āļĩāđ‰āļ­āļēāļĻāļąāļĒāđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™ @EnableAutoConfiguration (āļĢāļ§āļĄāļ­āļĒāļđāđˆāđƒāļ™ @SpringBootApplication) āđāļĨāļ°āđ€āļ‡āļ·āđˆāļ­āļ™āđ„āļ‚āļ—āļĩāđˆāļ›āļĢāļ°āļāļēāļĻāđ„āļ§āđ‰āđƒāļ™āļ„āļĨāļēāļŠ auto-configuration

CustomAutoConfiguration.javajava
@Configuration
@ConditionalOnClass(DataSource.class) // Activates if DataSource is on classpath
@ConditionalOnMissingBean(DataSource.class) // Only acts if no DataSource exists
public class CustomAutoConfiguration {

    @Bean
    @ConditionalOnProperty(name = "app.datasource.enabled", havingValue = "true")
    public DataSource dataSource() {
        // Default DataSource configuration
        return DataSourceBuilder.create()
            .driverClassName("org.h2.Driver")
            .url("jdbc:h2:mem:testdb")
            .build();
    }
}

Spring Boot āļ•āļĢāļ§āļˆāļŠāļ­āļšāđ€āļ‡āļ·āđˆāļ­āļ™āđ„āļ‚ (@ConditionalOn*) āđ€āļžāļ·āđˆāļ­āļ•āļąāļ”āļŠāļīāļ™āđƒāļˆāļ§āđˆāļēāļˆāļ°āļŠāļĢāđ‰āļēāļ‡ bean āđƒāļ”āļšāđ‰āļēāļ‡ āļ§āļīāļ˜āļĩāļ™āļĩāđ‰āļŠāđˆāļ§āļĒāļĨāļ”āļ„āļ§āļēāļĄāļ‚āļąāļ”āđāļĒāđ‰āļ‡āđāļĨāļ°āļ—āļģāđƒāļŦāđ‰āļŠāļēāļĄāļēāļĢāļ–āđ€āļ‚āļĩāļĒāļ™āļ—āļąāļšāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āđ„āļ”āđ‰āļ‡āđˆāļēāļĒ

3. Starter āļ„āļ·āļ­āļ­āļ°āđ„āļĢāđāļĨāļ°āļŠāļĢāđ‰āļēāļ‡āđ„āļ”āđ‰āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Starter āļ„āļ·āļ­āđ‚āļĄāļ”āļđāļĨ Maven/Gradle āļ—āļĩāđˆāļĢāļ§āļšāļĢāļ§āļĄ dependency āđāļĨāļ°āļ„āđˆāļēāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ—āļĩāđˆāļˆāļģāđ€āļ›āđ‡āļ™āļ•āđˆāļ­āļŸāļĩāđ€āļˆāļ­āļĢāđŒāđƒāļ”āļŸāļĩāđ€āļˆāļ­āļĢāđŒāļŦāļ™āļķāđˆāļ‡ āļ•āļąāļ§āļ­āļĒāđˆāļēāļ‡āđ€āļŠāđˆāļ™ spring-boot-starter-web āļĢāļ§āļĄ Spring MVC, Jackson, Tomcat āļāļąāļ‡āļ•āļąāļ§ āđāļĨāļ° validation

xml
<!-- pom.xml for a custom starter -->
<project>
    <artifactId>my-company-starter</artifactId>
    <dependencies>
        <!-- Base Spring Boot dependency -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- Company-specific libraries -->
        <dependency>
            <groupId>com.mycompany</groupId>
            <artifactId>logging-utils</artifactId>
        </dependency>
    </dependencies>
</project>

āļŦāļēāļāļ•āđ‰āļ­āļ‡āļāļēāļĢāļŠāļĢāđ‰āļēāļ‡ starter āđ€āļ­āļ‡ āđƒāļŦāđ‰āļ™āļīāļĒāļēāļĄāļ„āļĨāļēāļŠ auto-configuration āđāļĨāļ°āļ­āđ‰āļēāļ‡āļ­āļīāļ‡āđ„āļ§āđ‰āđƒāļ™ META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

4. application.properties āļāļąāļš application.yml āļ•āđˆāļēāļ‡āļāļąāļ™āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āļĢāļđāļ›āđāļšāļšāļ—āļąāđ‰āļ‡āļŠāļ­āļ‡āļŠāđˆāļ§āļĒāđāļĒāļāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ­āļ­āļāļˆāļēāļāđ‚āļ„āđ‰āļ” YAML āđ€āļŦāļĄāļēāļ°āļāļąāļšāđ‚āļ„āļĢāļ‡āļŠāļĢāđ‰āļēāļ‡āļ‹āđ‰āļ­āļ™āļāļąāļ™āļ”āđ‰āļ§āļĒ syntax āļ—āļĩāđˆāļ­āđˆāļēāļ™āļ‡āđˆāļēāļĒ āļŠāđˆāļ§āļ™ properties āļĒāļąāļ‡āđ€āļŦāļĄāļēāļ°āļāļąāļšāđ‚āļ„āļĢāļ‡āļŠāļĢāđ‰āļēāļ‡āđāļšāļšāļĢāļēāļšāļĄāļēāļāļāļ§āđˆāļē

properties
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.jpa.hibernate.ddl-auto=update
yaml
# application.yml - clear hierarchical structure
server:
  port: 8080

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: admin
  jpa:
    hibernate:
      ddl-auto: update

Profile āļŠāđˆāļ§āļĒāļ›āļĢāļąāļšāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđƒāļŦāđ‰āđ€āļŦāļĄāļēāļ°āļāļąāļšāļŠāļ āļēāļžāđāļ§āļ”āļĨāđ‰āļ­āļĄ: application-dev.yml, application-prod.yml āđ€āļ›āļīāļ”āđƒāļŠāđ‰āļ‡āļēāļ™āļœāđˆāļēāļ™ spring.profiles.active

5. @SpringBootApplication āļ—āļģāļ‡āļēāļ™āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™āļ™āļĩāđ‰āļĢāļ§āļĄāđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™āļŠāļģāļ„āļąāļāļŠāļēāļĄāļ•āļąāļ§āļ—āļĩāđˆāļāļģāļŦāļ™āļ”āļžāļĪāļ•āļīāļāļĢāļĢāļĄāļ‚āļ­āļ‡āđāļ­āļ› Spring Boot

DemoApplication.javajava
// @SpringBootApplication is equivalent to these three annotations:
@SpringBootConfiguration // Equivalent to @Configuration
@EnableAutoConfiguration // Activates auto-configuration
@ComponentScan // Scans components in package and sub-packages
public class DemoApplication {

    public static void main(String[] args) {
        // Creates context, runs configurations, and starts server
        ConfigurableApplicationContext context =
            SpringApplication.run(DemoApplication.class, args);

        // The context contains all configured beans
        UserService userService = context.getBean(UserService.class);
    }
}

āļāļēāļĢāļ§āļēāļ‡āļ„āļĨāļēāļŠāļ™āļĩāđ‰āđ„āļ§āđ‰āļ—āļĩāđˆ root āļ‚āļ­āļ‡āđāļžāđ‡āļāđ€āļāļˆāļŠāļģāļ„āļąāļāļĄāļēāļ: @ComponentScan āļˆāļ°āļŠāđāļāļ™āđāļžāđ‡āļāđ€āļāļˆāļ™āļąāđ‰āļ™āļžāļĢāđ‰āļ­āļĄāđāļžāđ‡āļāđ€āļāļˆāļĒāđˆāļ­āļĒāļ—āļąāđ‰āļ‡āļŦāļĄāļ”āđ€āļžāļ·āđˆāļ­āļ„āđ‰āļ™āļŦāļēāļ„āļ­āļĄāđ‚āļžāđ€āļ™āļ™āļ•āđŒ

Spring Data āđāļĨāļ°āļāļēāļĢāļ„āļ‡āļ­āļĒāļđāđˆāļ‚āļ­āļ‡āļ‚āđ‰āļ­āļĄāļđāļĨ

6. JpaRepository, CrudRepository āđāļĨāļ° PagingAndSortingRepository āļ•āđˆāļēāļ‡āļāļąāļ™āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āļ­āļīāļ™āđ€āļ—āļ­āļĢāđŒāđ€āļŸāļ‹āđ€āļŦāļĨāđˆāļēāļ™āļĩāđ‰āđ€āļ›āđ‡āļ™āļĨāļģāļ”āļąāļšāļŠāļąāđ‰āļ™āļ—āļĩāđˆāđƒāļŦāđ‰āļ„āļ§āļēāļĄāļŠāļēāļĄāļēāļĢāļ–āđƒāļ™āļāļēāļĢāđ€āļ‚āđ‰āļēāļ–āļķāļ‡āļ‚āđ‰āļ­āļĄāļđāļĨāđ€āļžāļīāđˆāļĄāļ‚āļķāđ‰āļ™āđ€āļĢāļ·āđˆāļ­āļĒ āđ†

UserRepository.javajava
// CrudRepository: basic CRUD operations
public interface UserCrudRepository extends CrudRepository<User, Long> {
    // save(), findById(), findAll(), deleteById(), count(), existsById()
}

// PagingAndSortingRepository: adds pagination and sorting
public interface UserPagingRepository extends PagingAndSortingRepository<User, Long> {
    // Inherits from CrudRepository + findAll(Sort), findAll(Pageable)
}

// JpaRepository: complete JPA features
public interface UserRepository extends JpaRepository<User, Long> {
    // Inherits from previous + flush(), saveAndFlush(), deleteInBatch()

    // Derived query methods
    List<User> findByEmailContaining(String email);

    Optional<User> findByUsernameAndActiveTrue(String username);
}

āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāđƒāļŠāđ‰āļ‡āļēāļ™āļ—āļąāđˆāļ§āđ„āļ› JpaRepository āđ€āļ›āđ‡āļ™āļ•āļąāļ§āđ€āļĨāļ·āļ­āļāļ—āļĩāđˆāđāļ™āļ°āļ™āļģ āđ€āļžāļĢāļēāļ°āđƒāļŦāđ‰āļŸāļąāļ‡āļāđŒāļŠāļąāļ™āļ—āļĩāđˆāļˆāļģāđ€āļ›āđ‡āļ™āļ•āđˆāļ­āļāļēāļĢāļžāļąāļ’āļ™āļēāđāļ­āļ› JPA āļ­āļĒāđˆāļēāļ‡āļ„āļĢāļšāļ–āđ‰āļ§āļ™

7. āļāļģāļŦāļ™āļ” query āđ€āļ­āļ‡āļ”āđ‰āļ§āļĒ @Query āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™ @Query āļ—āļģāđƒāļŦāđ‰āļŠāļēāļĄāļēāļĢāļ–āđ€āļ‚āļĩāļĒāļ™ JPQL āļŦāļĢāļ·āļ­ SQL native āđ„āļ”āđ‰āđ€āļĄāļ·āđˆāļ­ derived method āđ„āļĄāđˆāđ€āļžāļĩāļĒāļ‡āļžāļ­

OrderRepository.javajava
public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL with named parameters
    @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status")
    List<Order> findCustomerOrdersByStatus(
        @Param("customerId") Long customerId,
        @Param("status") OrderStatus status
    );

    // Native SQL query for specific needs
    @Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'",
           nativeQuery = true)
    List<Order> findRecentOrders();

    // Modifying query with @Modifying
    @Modifying
    @Transactional
    @Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
    int updateOrderStatuses(@Param("ids") List<Long> ids, @Param("status") OrderStatus status);
}

Query āđāļšāļš JPQL āļžāļāļžāļēāđ„āļ”āđ‰āļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨ āđƒāļ™āļ‚āļ“āļ°āļ—āļĩāđˆ native query āđƒāļŦāđ‰āđ€āļ‚āđ‰āļēāļ–āļķāļ‡āļŸāļĩāđ€āļˆāļ­āļĢāđŒāđ€āļ‰āļžāļēāļ°āļ‚āļ­āļ‡ DBMS

8. āļāļēāļĢāļˆāļąāļ”āļāļēāļĢāļ˜āļļāļĢāļāļĢāļĢāļĄāļ”āđ‰āļ§āļĒ @Transactional

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™āļ™āļĩāđ‰āļĢāļ°āļšāļļāļ‚āļ­āļšāđ€āļ‚āļ•āļ˜āļļāļĢāļāļĢāļĢāļĄāļ‚āļ­āļ‡āđ€āļĄāļ˜āļ­āļ”āļŦāļĢāļ·āļ­āļ„āļĨāļēāļŠ Spring āļˆāļ°āļˆāļąāļ”āļāļēāļĢ commit, rollback āđāļĨāļ°āļāļēāļĢāļŠāđˆāļ‡āļ•āđˆāļ­āđƒāļŦāđ‰āļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī

PaymentService.javajava
@Service
public class PaymentService {

    private final AccountRepository accountRepository;
    private final TransactionLogRepository logRepository;

    // Transaction with custom configuration
    @Transactional(
        propagation = Propagation.REQUIRED,      // Creates or joins a transaction
        isolation = Isolation.READ_COMMITTED,    // Isolation level
        timeout = 30,                            // Timeout in seconds
        rollbackFor = PaymentException.class     // Rollback on this exception
    )
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId)
            .orElseThrow(() -> new AccountNotFoundException(fromId));
        Account to = accountRepository.findById(toId)
            .orElseThrow(() -> new AccountNotFoundException(toId));

        from.debit(amount);  // Throws exception if insufficient balance
        to.credit(amount);

        accountRepository.save(from);
        accountRepository.save(to);

        // Log will be rolled back with everything else if an exception occurs
        logRepository.save(new TransactionLog(fromId, toId, amount));
    }
}

āļĢāļ°āļ”āļąāļš propagation (REQUIRED, REQUIRES_NEW, NESTED āļŊāļĨāļŊ) āļāļģāļŦāļ™āļ”āļ§āļīāļ˜āļĩāļ—āļĩāđˆāļ˜āļļāļĢāļāļĢāļĢāļĄāļ‹āđ‰āļ­āļ™āļāļąāļ™āđ€āļĄāļ·āđˆāļ­āđ€āļĢāļĩāļĒāļāđ€āļĄāļ˜āļ­āļ”āļ—āļĩāđˆāđ€āļ›āđ‡āļ™ transactional āļ•āđˆāļ­āļāļąāļ™āđ€āļ›āđ‡āļ™āļ—āļ­āļ” āđ†

āļāļąāļšāļ”āļąāļāļ—āļĩāđˆāļžāļšāļšāđˆāļ­āļĒ

@Transactional āļ—āļģāļ‡āļēāļ™āđ€āļ‰āļžāļēāļ°āļāļēāļĢāđ€āļĢāļĩāļĒāļāļ—āļĩāđˆāļœāđˆāļēāļ™ proxy āļ‚āļ­āļ‡ Spring āļāļēāļĢāđ€āļĢāļĩāļĒāļāļ āļēāļĒāđƒāļ™āļ„āļĨāļēāļŠāđ€āļ”āļĩāļĒāļ§āļāļąāļ™āļˆāļ°āļ‚āđ‰āļēāļĄ proxy āđāļĨāļ°āđ„āļĄāđˆāļŠāļ™āđƒāļˆāđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™

9. āļˆāļąāļ”āļāļēāļĢāļ„āļ§āļēāļĄāļŠāļąāļĄāļžāļąāļ™āļ˜āđŒ Lazy āļāļąāļš Eager Loading āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āđ‚āļŦāļĄāļ”āļāļēāļĢāđ‚āļŦāļĨāļ”āļ„āļ§āļēāļĄāļŠāļąāļĄāļžāļąāļ™āļ˜āđŒāļŠāđˆāļ‡āļœāļĨāđ‚āļ”āļĒāļ•āļĢāļ‡āļ•āđˆāļ­āļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļž Lazy Loading āļˆāļ°āļŦāļ™āđˆāļ§āļ‡āļāļēāļĢāđ‚āļŦāļĨāļ”āļˆāļ™āļāļ§āđˆāļēāļˆāļ°āļĄāļĩāļāļēāļĢāđ€āļ‚āđ‰āļēāļ–āļķāļ‡ āļŠāđˆāļ§āļ™ Eager Loading āļˆāļ°āđ‚āļŦāļĨāļ”āļ—āļąāļ™āļ—āļĩ

User.javajava
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // Eager: roles are loaded with the user
    @ManyToMany(fetch = FetchType.EAGER)
    private Set<Role> roles;

    // Lazy: orders are only loaded on access
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;
}
UserService.javajava
@Service
public class UserService {

    @Transactional(readOnly = true)
    public UserDTO getUserWithOrders(Long userId) {
        User user = userRepository.findById(userId).orElseThrow();

        // Accessing orders triggers an additional SQL query
        // This works because the session is open (@Transactional)
        List<OrderDTO> orderDTOs = user.getOrders().stream()
            .map(this::toDTO)
            .collect(Collectors.toList());

        return new UserDTO(user, orderDTOs);
    }

    // Alternative: query with JOIN FETCH to avoid N+1
    public UserDTO getUserWithOrdersOptimized(Long userId) {
        User user = userRepository.findByIdWithOrders(userId);
        // Orders are already loaded, no additional query
        return new UserDTO(user, user.getOrders());
    }
}

āļ›āļąāļāļŦāļē N+1 āđ€āļāļīāļ”āđ€āļĄāļ·āđˆāļ­ lazy loading āļŠāļĢāđ‰āļēāļ‡ query āļŦāļ™āļķāđˆāļ‡āļ„āļģāļŠāļąāđˆāļ‡āļ•āđˆāļ­āļŠāļĄāļēāļŠāļīāļāļŦāļ™āļķāđˆāļ‡āļ•āļąāļ§āđƒāļ™āļ„āļ­āļĨāđ€āļĨāļāļŠāļąāļ™ āļāļēāļĢāđƒāļŠāđ‰ JOIN FETCH āļŦāļĢāļ·āļ­ projection āļŠāđˆāļ§āļĒāđāļāđ‰āđ„āļ”āđ‰

10. āļ›āļąāļāļŦāļē N+1 āļ„āļ·āļ­āļ­āļ°āđ„āļĢāđāļĨāļ°āđāļāđ‰āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āļ›āļąāļāļŦāļē N+1 āđ€āļāļīāļ”āđ€āļĄāļ·āđˆāļ­āļŦāļĨāļąāļ‡āļˆāļēāļ query āđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™ (1) āļ•āļēāļĄāļĄāļēāļ”āđ‰āļ§āļĒ query āđ€āļžāļīāđˆāļĄāļ­āļĩāļ N āļĢāļēāļĒāļāļēāļĢāđ€āļžāļ·āđˆāļ­āđ‚āļŦāļĨāļ”āļ„āļ§āļēāļĄāļŠāļąāļĄāļžāļąāļ™āļ˜āđŒāļ‚āļ­āļ‡āđāļ•āđˆāļĨāļ° entity

ArticleRepository.javajava
public interface ArticleRepository extends JpaRepository<Article, Long> {

    // PROBLEM: this query generates N+1 queries
    List<Article> findAll();

    // SOLUTION 1: JOIN FETCH in JPQL
    @Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments")
    List<Article> findAllWithDetails();

    // SOLUTION 2: Entity Graph
    @EntityGraph(attributePaths = {"author", "comments"})
    @Query("SELECT a FROM Article a")
    List<Article> findAllWithGraph();
}
Article.javajava
@Entity
@NamedEntityGraph(
    name = "Article.withDetails",
    attributeNodes = {
        @NamedAttributeNode("author"),
        @NamedAttributeNode("comments")
    }
)
public class Article {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Author author;

    @OneToMany(mappedBy = "article", fetch = FetchType.LAZY)
    private List<Comment> comments;
}

āļāļēāļĢāđƒāļŠāđ‰ @EntityGraph āļŦāļĢāļ·āļ­ JOIN FETCH āļŠāđˆāļ§āļĒāļĨāļ” N+1 āđƒāļŦāđ‰āđ€āļŦāļĨāļ·āļ­ query āđ€āļ”āļĩāļĒāļ§āļ—āļĩāđˆāļĄāļĩ join āļ—āļģāđƒāļŦāđ‰āļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļžāļ”āļĩāļ‚āļķāđ‰āļ™āļŠāļąāļ”āđ€āļˆāļ™

āļžāļĢāđ‰āļ­āļĄāļ—āļĩāđˆāļˆāļ°āļžāļīāļŠāļīāļ•āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot āđāļĨāđ‰āļ§āļŦāļĢāļ·āļ­āļĒāļąāļ‡āļ„āļĢāļąāļš?

āļāļķāļāļāļ™āļ”āđ‰āļ§āļĒāļ•āļąāļ§āļˆāļģāļĨāļ­āļ‡āđāļšāļšāđ‚āļ•āđ‰āļ•āļ­āļš, flashcards āđāļĨāļ°āđāļšāļšāļ—āļ”āļŠāļ­āļšāđ€āļ—āļ„āļ™āļīāļ„āļ„āļĢāļąāļš

REST API āđāļĨāļ°āđ€āļ§āđ‡āļš

11. āļŠāļĢāđ‰āļēāļ‡ REST controller āļžāļĢāđ‰āļ­āļĄ validation āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Spring Boot āļœāļŠāļēāļ™ @RestController āļāļąāļš Bean Validation āđ€āļžāļ·āđˆāļ­āļŠāļĢāđ‰āļēāļ‡ API āļ—āļĩāđˆāđāļ‚āđ‡āļ‡āđāļĢāļ‡āđāļĨāļ°āļ•āļĢāļ§āļˆāļŠāļ­āļšāļ­āļīāļ™āļžāļļāļ•āļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī

UserController.javajava
@RestController
@RequestMapping("/api/users")
@Validated // Activates validation on method parameters
public class UserController {

    private final UserService userService;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
        // Validation is performed automatically before execution
        return userService.createUser(request);
    }

    @GetMapping("/{id}")
    public UserResponse getUser(
        @PathVariable @Min(1) Long id // Validation on PathVariable
    ) {
        return userService.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }

    @GetMapping
    public Page<UserResponse> listUsers(
        @RequestParam(defaultValue = "0") @Min(0) int page,
        @RequestParam(defaultValue = "20") @Max(100) int size
    ) {
        return userService.findAll(PageRequest.of(page, size));
    }
}
CreateUserRequest.javajava
public record CreateUserRequest(
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    String name,

    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    String email,

    @NotBlank
    @Pattern(regexp = "^(?=.*[A-Z])(?=.*[0-9]).{8,}$",
             message = "Password must contain at least 8 characters, one uppercase and one digit")
    String password
) {}

āļŠāļēāļĄāļēāļĢāļ–āļ™āļģāļ‚āđ‰āļ­āļ„āļ§āļēāļĄ validation āļ­āļ­āļāđ„āļ›āļ­āļĒāļđāđˆāđƒāļ™āđ„āļŸāļĨāđŒ message āđ€āļžāļ·āđˆāļ­āļĢāļ­āļ‡āļĢāļąāļšāļŦāļĨāļēāļĒāļ āļēāļĐāļēāđ„āļ”āđ‰

12. āļˆāļąāļ”āļāļēāļĢ exception āđāļšāļšāļĢāļ§āļĄāļĻāļđāļ™āļĒāđŒāļ”āđ‰āļ§āļĒ @ControllerAdvice āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āļ•āļąāļ§āļˆāļąāļ”āļāļēāļĢ exception āļĢāļ§āļĄāļĻāļđāļ™āļĒāđŒāļŠāđˆāļ§āļĒāđƒāļŦāđ‰āļĢāļđāļ›āđāļšāļšāļ‚āļ­āļ‡āļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļšāļ‚āđ‰āļ­āļœāļīāļ”āļžāļĨāļēāļ”āđ€āļ›āđ‡āļ™āļĄāļēāļ•āļĢāļāļēāļ™āđāļĨāļ°āļĨāļ”āđ‚āļ„āđ‰āļ”āļ‹āđ‰āļģ

GlobalExceptionHandler.javajava
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // Handle validation errors
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex) {
        Map<String, String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                FieldError::getDefaultMessage,
                (existing, replacement) -> existing
            ));

        return new ErrorResponse("VALIDATION_ERROR", "Invalid data", errors);
    }

    // Handle resource not found
    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage(), null);
    }

    // Handle unexpected errors
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleUnexpectedError(Exception ex) {
        log.error("Unexpected error", ex);
        return new ErrorResponse("INTERNAL_ERROR", "An error occurred", null);
    }
}
ErrorResponse.javajava
public record ErrorResponse(
    String code,
    String message,
    Map<String, String> details,
    Instant timestamp
) {
    public ErrorResponse(String code, String message, Map<String, String> details) {
        this(code, message, details, Instant.now());
    }
}

āļ§āļīāļ˜āļĩāļ™āļĩāđ‰āļŠāđˆāļ§āļĒāđƒāļŦāđ‰āļ—āļļāļāļ‚āđ‰āļ­āļœāļīāļ”āļžāļĨāļēāļ”āļ­āļĒāļđāđˆāđƒāļ™āļĢāļđāļ›āđāļšāļšāđ€āļ”āļĩāļĒāļ§āļāļąāļ™āđāļĨāļ°āļāļąāđˆāļ‡āđ„āļ„āļĨāđ€āļ­āđ‡āļ™āļ•āđŒāļˆāļąāļ”āļāļēāļĢāļ‡āđˆāļēāļĒāļ‚āļķāđ‰āļ™

13. @RequestParam, @PathVariable āđāļĨāļ° @RequestBody āđāļ•āļāļ•āđˆāļēāļ‡āļāļąāļ™āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™āļ—āļąāđ‰āļ‡āļŠāļēāļĄāļ”āļķāļ‡āļ‚āđ‰āļ­āļĄāļđāļĨāļˆāļēāļāļŠāđˆāļ§āļ™āļ•āđˆāļēāļ‡ āđ† āļ‚āļ­āļ‡ HTTP request

ProductController.javajava
@RestController
@RequestMapping("/api/products")
public class ProductController {

    // @PathVariable: extracts values from the URL
    // GET /api/products/123
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }

    // @RequestParam: extracts query parameters
    // GET /api/products?category=electronics&minPrice=100&page=0
    @GetMapping
    public Page<Product> searchProducts(
        @RequestParam(required = false) String category,
        @RequestParam(defaultValue = "0") BigDecimal minPrice,
        @RequestParam(defaultValue = "0") int page
    ) {
        return productService.search(category, minPrice, PageRequest.of(page, 20));
    }

    // @RequestBody: deserializes the JSON request body
    // POST /api/products with {"name": "...", "price": ...}
    @PostMapping
    public Product createProduct(@Valid @RequestBody CreateProductRequest request) {
        return productService.create(request);
    }

    // Combination of all three in the same method
    // PUT /api/products/123?notify=true with JSON body
    @PutMapping("/{id}")
    public Product updateProduct(
        @PathVariable Long id,
        @RequestParam(defaultValue = "false") boolean notify,
        @Valid @RequestBody UpdateProductRequest request
    ) {
        Product updated = productService.update(id, request);
        if (notify) {
            notificationService.sendUpdateNotification(updated);
        }
        return updated;
    }
}

@PathVariable āđƒāļŠāđ‰āļĢāļ°āļšāļļ resource āđ€āļˆāļēāļ°āļˆāļ‡, @RequestParam āđƒāļŠāđ‰āļāļĢāļ­āļ‡āļŦāļĢāļ·āļ­āđāļšāđˆāļ‡āļŦāļ™āđ‰āļēāļœāļĨāļĨāļąāļžāļ˜āđŒ āđāļĨāļ° @RequestBody āđƒāļŠāđ‰āļŠāđˆāļ‡āļ‚āđ‰āļ­āļĄāļđāļĨāļ—āļĩāđˆāļ‹āļąāļšāļ‹āđ‰āļ­āļ™

14. āļ—āļģ versioning āļ‚āļ­āļ‡ API āđ„āļ”āđ‰āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āļĄāļĩāļŦāļĨāļēāļĒāļāļĨāļĒāļļāļ—āļ˜āđŒāđƒāļ™āļāļēāļĢāļāļģāļŦāļ™āļ”āđ€āļ§āļ­āļĢāđŒāļŠāļąāļ™āđƒāļŦāđ‰ REST API āđāļ•āđˆāļĨāļ°āđāļšāļšāļĄāļĩāļˆāļļāļ”āđ€āļ”āđˆāļ™āļ•āđˆāļēāļ‡āļāļąāļ™

java
// Version via URL (recommended for clarity)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
    @GetMapping("/{id}")
    public UserV1Response getUser(@PathVariable Long id) {
        return userService.findByIdV1(id);
    }
}

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
    @GetMapping("/{id}")
    public UserV2Response getUser(@PathVariable Long id) {
        // V2 includes additional fields
        return userService.findByIdV2(id);
    }
}
java
// Version via custom header
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping(value = "/{id}", headers = "X-API-Version=1")
    public UserV1Response getUserV1(@PathVariable Long id) {
        return userService.findByIdV1(id);
    }

    @GetMapping(value = "/{id}", headers = "X-API-Version=2")
    public UserV2Response getUserV2(@PathVariable Long id) {
        return userService.findByIdV2(id);
    }
}
java
// Version via media type (content negotiation)
@RestController
@RequestMapping("/api/users")
public class UserMediaTypeController {

    @GetMapping(value = "/{id}", produces = "application/vnd.myapi.v1+json")
    public UserV1Response getUserV1(@PathVariable Long id) {
        return userService.findByIdV1(id);
    }

    @GetMapping(value = "/{id}", produces = "application/vnd.myapi.v2+json")
    public UserV2Response getUserV2(@PathVariable Long id) {
        return userService.findByIdV2(id);
    }
}

āļāļēāļĢāļāļģāļŦāļ™āļ”āđ€āļ§āļ­āļĢāđŒāļŠāļąāļ™āļœāđˆāļēāļ™ URL āļĒāļąāļ‡āļ„āļ‡āđ„āļ”āđ‰āļĢāļąāļšāļ„āļ§āļēāļĄāļ™āļīāļĒāļĄāļĄāļēāļāļ—āļĩāđˆāļŠāļļāļ” āđ€āļžāļĢāļēāļ°āđ€āļĢāļĩāļĒāļšāļ‡āđˆāļēāļĒāđāļĨāļ°āđ€āļŦāđ‡āļ™āļŠāļąāļ”āđƒāļ™āļĨāđ‡āļ­āļāđāļĨāļ°āđ€āļ­āļāļŠāļēāļĢ

15. āļ•āļąāđ‰āļ‡āļ„āđˆāļē CORS āđƒāļ™ Spring Boot āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

CORS (Cross-Origin Resource Sharing) āļ„āļ§āļšāļ„āļļāļĄāļ„āļģāļ‚āļ­āļˆāļēāļāļ•āđ‰āļ™āļ—āļēāļ‡āļ•āđˆāļēāļ‡ āđ† āļĄāļĩāļŦāļĨāļēāļĒāļĢāļ°āļ”āļąāļšāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđƒāļŦāđ‰āđ€āļĨāļ·āļ­āļāđƒāļŠāđ‰

CorsConfig.java - Global configurationjava
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://frontend.example.com", "https://admin.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .exposedHeaders("X-Total-Count", "X-Page-Count")
            .allowCredentials(true)
            .maxAge(3600); // Cache preflight for 1 hour
    }
}
java
// Per-controller or method configuration
@RestController
@RequestMapping("/api/public")
@CrossOrigin(origins = "*", maxAge = 3600)
public class PublicApiController {

    @GetMapping("/data")
    public DataResponse getPublicData() {
        return dataService.getPublicData();
    }

    // Override at method level
    @CrossOrigin(origins = "https://specific-client.com")
    @PostMapping("/submit")
    public SubmitResponse submitData(@RequestBody SubmitRequest request) {
        return dataService.submit(request);
    }
}

āļšāļ™āđ‚āļ›āļĢāļ”āļąāļāļŠāļąāļ™āļ„āļ§āļĢāļˆāļģāļāļąāļ” origin āļ—āļĩāđˆāļ­āļ™āļļāļāļēāļ•āđāļĨāļ°āđƒāļŠāđ‰ HTTPS āļ­āļĒāđˆāļēāđƒāļŠāđ‰ * āļĢāđˆāļ§āļĄāļāļąāļš allowCredentials(true) āđ€āļ”āđ‡āļ”āļ‚āļēāļ”

Spring Security

16. āļ•āļąāđ‰āļ‡āļ„āđˆāļē Spring Security āļāļąāļš JWT

Spring Security 6+ āđƒāļŠāđ‰āđāļ™āļ§āļ—āļēāļ‡āđ€āļŠāļīāļ‡āļŸāļąāļ‡āļāđŒāļŠāļąāļ™āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ„āļ§āļēāļĄāļ›āļĨāļ­āļ”āļ āļąāļĒ

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable()) // Disabled for stateless API
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/**").authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
JwtAuthenticationFilter.javajava
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain chain
    ) throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        String jwt = authHeader.substring(7);
        String username = jwtService.extractUsername(jwt);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                    );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        chain.doFilter(request, response);
    }
}

āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ™āļĩāđ‰āļŠāļĢāđ‰āļēāļ‡ API āđāļšāļšāđ„āļĄāđˆāļĄāļĩāļŠāļ–āļēāļ™āļ°āļ—āļĩāđˆāļ—āļļāļāļ„āļģāļ‚āļ­āļ•āđ‰āļ­āļ‡āđāļ™āļš JWT token āļ—āļĩāđˆāļ–āļđāļāļ•āđ‰āļ­āļ‡āđƒāļ™āđ€āļŪāļ”āđ€āļ”āļ­āļĢāđŒ Authorization

17. āļ›āđ‰āļ­āļ‡āļāļąāļ™āđ€āļĄāļ˜āļ­āļ”āļ”āđ‰āļ§āļĒ @PreAuthorize āđāļĨāļ° @PostAuthorize

āļ„āļ§āļēāļĄāļ›āļĨāļ­āļ”āļ āļąāļĒāļĢāļ°āļ”āļąāļšāđ€āļĄāļ˜āļ­āļ”āđƒāļŦāđ‰āļāļēāļĢāļ„āļ§āļšāļ„āļļāļĄāļ—āļĩāđˆāļĨāļ°āđ€āļ­āļĩāļĒāļ”āļ•āļēāļĄ role, āļŠāļīāļ—āļ˜āļīāđŒ āļŦāļĢāļ·āļ­āļ‚āđ‰āļ­āļĄāļđāļĨ

UserService.javajava
@Service
@PreAuthorize("isAuthenticated()") // All methods require authentication
public class UserService {

    // Only ADMINs can list all users
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> findAll() {
        return userRepository.findAll();
    }

    // User can view their profile OR admins can view any profile
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    public User findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }

    // Post-execution check: user can only see their own data
    @PostAuthorize("returnObject.owner.id == authentication.principal.id or hasRole('ADMIN')")
    public Document getDocument(Long documentId) {
        return documentRepository.findById(documentId)
            .orElseThrow(() -> new DocumentNotFoundException(documentId));
    }

    // Collection filtering: returns only authorized elements
    @PostFilter("filterObject.owner.id == authentication.principal.id")
    public List<Project> getUserProjects() {
        return projectRepository.findAll();
    }
}
SecurityConfig.java - Activationjava
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
    // Additional configuration if needed
}

@PreAuthorize āļ•āļĢāļ§āļˆāļāđˆāļ­āļ™āļāļēāļĢāđ€āļĢāļĩāļĒāļ @PostAuthorize āļ•āļĢāļ§āļˆāļŦāļĨāļąāļ‡āļāļēāļĢāđ€āļĢāļĩāļĒāļ āļŠāđˆāļ§āļ™ SpEL āļ—āļģāđƒāļŦāđ‰āļŠāļēāļĄāļēāļĢāļ–āđ€āļ‚āļĩāļĒāļ™āļāļŽāļ‹āļąāļšāļ‹āđ‰āļ­āļ™āđ„āļ”āđ‰

18. āļ›āđ‰āļ­āļ‡āļāļąāļ™ endpoint āļˆāļēāļāļāļēāļĢāđ‚āļˆāļĄāļ•āļĩ CSRF

CSRF (Cross-Site Request Forgery) āđƒāļŠāđ‰āļ›āļĢāļ°āđ‚āļĒāļŠāļ™āđŒāļˆāļēāļāđ€āļ‹āļŠāļŠāļąāļ™āļ‚āļ­āļ‡āļœāļđāđ‰āđƒāļŠāđ‰āļ—āļĩāđˆāļĨāđ‡āļ­āļāļ­āļīāļ™āļ­āļĒāļđāđˆ āļāļēāļĢāļ›āđ‰āļ­āļ‡āļāļąāļ™āļ–āļđāļāđ€āļ›āļīāļ”āđƒāļŠāđ‰āļ‡āļēāļ™āđ‚āļ”āļĒāļ›āļĢāļīāļĒāļēāļĒāļŠāļģāļŦāļĢāļąāļšāđāļ­āļ›āļ—āļĩāđˆāđƒāļŠāđ‰āđ€āļ‹āļŠāļŠāļąāļ™

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            // CSRF configuration for session-based applications
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
                // Exclude certain endpoints from CSRF protection
                .ignoringRequestMatchers("/api/webhooks/**")
            )
            .build();
    }
}
CsrfController.java - Endpoint to retrieve token (SPA)java
@RestController
public class CsrfController {

    @GetMapping("/api/csrf")
    public CsrfToken csrf(CsrfToken token) {
        // Returns CSRF token to frontend
        return token;
    }
}

āļŠāļģāļŦāļĢāļąāļš REST API āđāļšāļš stateless āļ—āļĩāđˆāđƒāļŠāđ‰ JWT āļŠāļēāļĄāļēāļĢāļ–āļ›āļīāļ” CSRF āđ„āļ”āđ‰āđ€āļžāļĢāļēāļ°āđ„āļĄāđˆāļĄāļĩāļ„āļļāļāļāļĩāđ‰āđ€āļ‹āļŠāļŠāļąāļ™āđƒāļŦāđ‰āđāļŠāļ§āļ‡āļ›āļĢāļ°āđ‚āļĒāļŠāļ™āđŒ āļŠāđˆāļ§āļ™āđāļ­āļ›āđāļšāļšāļ”āļąāđ‰āļ‡āđ€āļ”āļīāļĄāļ—āļĩāđˆāđƒāļŠāđ‰āđ€āļ‹āļŠāļŠāļąāļ™āļ•āđ‰āļ­āļ‡āđ€āļ›āļīāļ”āļ›āđ‰āļ­āļ‡āļāļąāļ™ CSRF āđ€āļŠāļĄāļ­

API stateless āļāļąāļšāđāļ­āļ›āļ—āļĩāđˆāđƒāļŠāđ‰āđ€āļ‹āļŠāļŠāļąāļ™

API āļ—āļĩāđˆāđƒāļŠāđ‰ JWT āļˆāļ°āļ›āļĨāļ­āļ”āļ āļąāļĒāļˆāļēāļ CSRF āđ‚āļ”āļĒāļ˜āļĢāļĢāļĄāļŠāļēāļ•āļī āđ€āļžāļĢāļēāļ° token āļ•āđ‰āļ­āļ‡āļ–āļđāļāđāļ™āļšāļĄāļēāļ—āļļāļāļ„āļģāļ‚āļ­ āļŠāđˆāļ§āļ™āđāļ­āļ›āļ—āļĩāđˆāđƒāļŠāđ‰āļ„āļļāļāļāļĩāđ‰āđ€āļ‹āļŠāļŠāļąāļ™āļˆāļģāđ€āļ›āđ‡āļ™āļ•āđ‰āļ­āļ‡āđ€āļ›āļīāļ” CSRF protection āđ€āļŠāļĄāļ­

āļāļēāļĢāļ—āļ”āļŠāļ­āļšāđƒāļ™ Spring Boot

19. @SpringBootTest āļāļąāļš @WebMvcTest āđāļ•āļāļ•āđˆāļēāļ‡āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™āđ€āļŦāļĨāđˆāļēāļ™āļĩāđ‰āļāļģāļŦāļ™āļ”āļšāļĢāļīāļšāļ—āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļ•āđˆāļēāļ‡āļāļąāļ™āļ•āļēāļĄāļ„āļ§āļēāļĄāļ•āđ‰āļ­āļ‡āļāļēāļĢ

UserControllerIntegrationTest.javajava
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {

    // Loads full context: all beans, database, etc.
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldCreateUser() {
        CreateUserRequest request = new CreateUserRequest("John", "john@example.com");

        ResponseEntity<User> response = restTemplate.postForEntity(
            "/api/users", request, User.class
        );

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getName()).isEqualTo("John");
    }
}
UserControllerUnitTest.javajava
@WebMvcTest(UserController.class) // Loads only the web layer
class UserControllerUnitTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean // Replaces @MockBean (deprecated)
    private UserService userService;

    @Test
    void shouldReturnUser() throws Exception {
        // Mock configuration
        when(userService.findById(1L))
            .thenReturn(Optional.of(new User(1L, "John", "john@example.com")));

        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"))
            .andExpect(jsonPath("$.email").value("john@example.com"));
    }

    @Test
    void shouldReturn404WhenUserNotFound() throws Exception {
        when(userService.findById(999L)).thenReturn(Optional.empty());

        mockMvc.perform(get("/api/users/999"))
            .andExpect(status().isNotFound());
    }
}

@WebMvcTest āđ€āļĢāđ‡āļ§āļāļ§āđˆāļēāđ€āļžāļĢāļēāļ°āđ‚āļŦāļĨāļ”āđ€āļ‰āļžāļēāļ° controller āļ—āļĩāđˆāļĢāļ°āļšāļļāđāļĨāļ° dependency āđ‚āļ”āļĒāļ•āļĢāļ‡ āļŠāđˆāļ§āļ™ @SpringBootTest āļ•āđ‰āļ­āļ‡āđƒāļŠāđ‰āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ—āļ”āļŠāļ­āļšāđāļšāļš integration āđ€āļ•āđ‡āļĄāļĢāļđāļ›āđāļšāļš

20. āļ—āļ”āļŠāļ­āļš repository āļ”āđ‰āļ§āļĒ @DataJpaTest

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™āļ™āļĩāđ‰āļŠāļĢāđ‰āļēāļ‡āļšāļĢāļīāļšāļ—āļ‚āļąāđ‰āļ™āļ•āđˆāļģāđ€āļžāļ·āđˆāļ­āļ—āļ”āļŠāļ­āļšāđ€āļĨāđ€āļĒāļ­āļĢāđŒ persistence āļ”āđ‰āļ§āļĒāļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨāļāļąāļ‡āļ•āļąāļ§

UserRepositoryTest.javajava
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // Use Testcontainers
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldFindByEmail() {
        // Arrange: create and persist an entity
        User user = new User("John", "john@example.com");
        entityManager.persistAndFlush(user);
        entityManager.clear(); // Clear L1 cache

        // Act
        Optional<User> found = userRepository.findByEmail("john@example.com");

        // Assert
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }

    @Test
    void shouldFindActiveUsersWithOrders() {
        // Testing a complex query with joins
        User activeUser = entityManager.persistAndFlush(new User("Active", "active@test.com", true));
        User inactiveUser = entityManager.persistAndFlush(new User("Inactive", "inactive@test.com", false));

        entityManager.persistAndFlush(new Order(activeUser, BigDecimal.valueOf(100)));

        List<User> result = userRepository.findActiveUsersWithOrders();

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getEmail()).isEqualTo("active@test.com");
    }
}

TestEntityManager āļĄāļĩāđ€āļĄāļ˜āļ­āļ”āļ­āļģāļ™āļ§āļĒāļ„āļ§āļēāļĄāļŠāļ°āļ”āļ§āļāļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ—āļ”āļŠāļ­āļš JPA āđ‚āļ”āļĒāđ€āļ‰āļžāļēāļ° persistAndFlush() āļ—āļĩāđˆāļšāļąāļ‡āļ„āļąāļšāđƒāļŦāđ‰āđ€āļ‚āļĩāļĒāļ™āļĨāļ‡āļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨāļ—āļąāļ™āļ—āļĩ

21. āđƒāļŠāđ‰ Testcontainers āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ—āļ”āļŠāļ­āļšāđ€āļŠāļīāļ‡āļœāļŠāļēāļ™

Testcontainers āļˆāļ°āļĢāļąāļ™ Docker container āļ—āļĩāđˆāļĄāļĩāļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨāļŦāļĢāļ·āļ­āļšāļĢāļīāļāļēāļĢāļˆāļĢāļīāļ‡āļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļš

IntegrationTestBase.javajava
@SpringBootTest
@Testcontainers
abstract class IntegrationTestBase {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @Container
    @ServiceConnection
    static RedisContainer redis = new RedisContainer("redis:7");
}
OrderServiceIntegrationTest.javajava
class OrderServiceIntegrationTest extends IntegrationTestBase {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldProcessOrderWithRealDatabase() {
        // Create an order
        Order order = orderService.createOrder(
            new CreateOrderRequest(1L, List.of(
                new OrderItem(101L, 2),
                new OrderItem(102L, 1)
            ))
        );

        // Verify in database
        Order saved = orderRepository.findById(order.getId()).orElseThrow();
        assertThat(saved.getItems()).hasSize(2);
        assertThat(saved.getStatus()).isEqualTo(OrderStatus.PENDING);
    }

    @Test
    void shouldCacheOrderInRedis() {
        Order order = orderService.createOrder(createSampleOrderRequest());

        // First call: database
        Order fetched1 = orderService.findById(order.getId());

        // Second call: Redis cache
        Order fetched2 = orderService.findById(order.getId());

        assertThat(fetched1).isEqualTo(fetched2);
        // Verify cache metrics if needed
    }
}

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™ @ServiceConnection āļ•āļąāđ‰āļ‡āļ„āđˆāļēāļāļēāļĢāđ€āļŠāļ·āđˆāļ­āļĄāļ•āđˆāļ­āļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī āļ—āļģāđƒāļŦāđ‰āđ„āļĄāđˆāļ•āđ‰āļ­āļ‡āđƒāļŠāđ‰ @DynamicPropertySource

āļžāļĢāđ‰āļ­āļĄāļ—āļĩāđˆāļˆāļ°āļžāļīāļŠāļīāļ•āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot āđāļĨāđ‰āļ§āļŦāļĢāļ·āļ­āļĒāļąāļ‡āļ„āļĢāļąāļš?

āļāļķāļāļāļ™āļ”āđ‰āļ§āļĒāļ•āļąāļ§āļˆāļģāļĨāļ­āļ‡āđāļšāļšāđ‚āļ•āđ‰āļ•āļ­āļš, flashcards āđāļĨāļ°āđāļšāļšāļ—āļ”āļŠāļ­āļšāđ€āļ—āļ„āļ™āļīāļ„āļ„āļĢāļąāļš

āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđāļĨāļ°āļāļēāļĢāļĄāļ­āļ™āļīāđ€āļ•āļ­āļĢāđŒ

22. āđāļĒāļāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ­āļ­āļāļ”āđ‰āļ§āļĒ @ConfigurationProperties

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™āļ™āļĩāđ‰āļœāļđāļ property āđ€āļ‚āđ‰āļēāļāļąāļšāļ­āļ­āļšāđ€āļˆāđ‡āļāļ•āđŒ Java āļ—āļĩāđˆāļĄāļĩāļ›āļĢāļ°āđ€āļ āļ—āļžāļĢāđ‰āļ­āļĄāļāļēāļĢāļ•āļĢāļ§āļˆāļŠāļ­āļšāļ„āļ§āļēāļĄāļ–āļđāļāļ•āđ‰āļ­āļ‡

MailProperties.javajava
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    @NotBlank String username,
    @NotBlank String password,
    @Valid SenderConfig sender,
    @Valid RetryConfig retry
) {
    public record SenderConfig(
        @NotBlank @Email String address,
        @NotBlank String name
    ) {}

    public record RetryConfig(
        @Min(1) int maxAttempts,
        @Min(100) long delayMs
    ) {
        public RetryConfig {
            // Default values
            if (maxAttempts == 0) maxAttempts = 3;
            if (delayMs == 0) delayMs = 1000;
        }
    }
}
yaml
# application.yml
app:
  mail:
    host: smtp.example.com
    port: 587
    username: ${MAIL_USERNAME}
    password: ${MAIL_PASSWORD}
    sender:
      address: noreply@example.com
      name: MyApp Notifications
    retry:
      max-attempts: 3
      delay-ms: 1000
MailConfig.javajava
@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class MailConfig {

    @Bean
    public JavaMailSender mailSender(MailProperties props) {
        JavaMailSenderImpl sender = new JavaMailSenderImpl();
        sender.setHost(props.host());
        sender.setPort(props.port());
        sender.setUsername(props.username());
        sender.setPassword(props.password());
        return sender;
    }
}

Record āļ‚āļ­āļ‡ Java (āļ•āļąāđ‰āļ‡āđāļ•āđˆ Java 16) āđ€āļŦāļĄāļēāļ°āļĄāļēāļāļāļąāļš @ConfigurationProperties āđ€āļžāļĢāļēāļ°āđāļāđ‰āđ„āļĄāđˆāđ„āļ”āđ‰āđāļĨāļ°āļāļĢāļ°āļŠāļąāļš

23. āđƒāļŠāđ‰ profile āļ‚āļ­āļ‡ Spring āļŠāļģāļŦāļĢāļąāļšāļŠāļ āļēāļžāđāļ§āļ”āļĨāđ‰āļ­āļĄāļ•āđˆāļēāļ‡āļāļąāļ™

Profile āļ›āļĢāļąāļšāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđƒāļŦāđ‰āļŠāļ­āļ”āļ„āļĨāđ‰āļ­āļ‡āļāļąāļšāļŠāļ āļēāļžāđāļ§āļ”āļĨāđ‰āļ­āļĄāļāļēāļĢāļĢāļąāļ™

yaml
# application.yml - Default configuration
spring:
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}
  datasource:
    url: ${DATABASE_URL:jdbc:h2:mem:testdb}

---
# application-dev.yml
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:postgresql://localhost:5432/myapp_dev
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

logging:
  level:
    com.example: DEBUG

---
# application-prod.yml
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: ${DATABASE_URL}
    hikari:
      maximum-pool-size: 20
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate

logging:
  level:
    com.example: INFO
DevDataInitializer.javajava
@Component
@Profile("dev") // Only active in development
public class DevDataInitializer implements CommandLineRunner {

    private final UserRepository userRepository;

    @Override
    public void run(String... args) {
        // Create test data
        userRepository.save(new User("dev@example.com", "Dev User", "password"));
        userRepository.save(new User("test@example.com", "Test User", "password"));
    }
}
java
// Service with conditional behavior
@Service
public class NotificationService {

    @Value("${app.notifications.enabled:true}")
    private boolean notificationsEnabled;

    @Autowired(required = false) // Optional based on profile
    private EmailService emailService;

    public void sendNotification(String message) {
        if (notificationsEnabled && emailService != null) {
            emailService.send(message);
        } else {
            log.info("Notification (disabled): {}", message);
        }
    }
}

āļŠāļēāļĄāļēāļĢāļ–āļĢāļ§āļĄāļŦāļĨāļēāļĒ profile āđ„āļ”āđ‰: spring.profiles.active=prod,metrics āļˆāļ°āđ€āļ›āļīāļ”āļ—āļąāđ‰āļ‡āļŠāļ­āļ‡āļžāļĢāđ‰āļ­āļĄāļāļąāļ™

24. āļ›āļĨāđˆāļ­āļĒāđ€āļĄāļ•āļĢāļīāļāđ€āļ‰āļžāļēāļ°āļ‚āļ­āļ‡āđāļ­āļ›āļ”āđ‰āļ§āļĒ Actuator āđāļĨāļ° Micrometer

Micrometer āļ—āļģāļŦāļ™āđ‰āļēāļ—āļĩāđˆāđ€āļ›āđ‡āļ™ facade āļŠāļģāļŦāļĢāļąāļšāļĢāļ°āļšāļšāļĄāļ­āļ™āļīāđ€āļ•āļ­āļĢāđŒ āđ€āļĄāļ•āļĢāļīāļāļ—āļĩāđˆāļāļģāļŦāļ™āļ”āđ€āļ­āļ‡āļ—āļģāđƒāļŦāđ‰āļŠāļąāļ‡āđ€āļāļ•āļāļēāļĢāļ“āđŒāļĢāļ°āļšāļšāđ„āļ”āđ‰āļ”āļĩāļ‚āļķāđ‰āļ™

OrderMetrics.javajava
@Component
public class OrderMetrics {

    private final Counter ordersCreated;
    private final Counter ordersFailed;
    private final Timer orderProcessingTime;
    private final AtomicInteger activeOrders;

    public OrderMetrics(MeterRegistry registry) {
        // Counter for orders created by status
        this.ordersCreated = Counter.builder("orders.created")
            .description("Number of orders created")
            .tag("application", "order-service")
            .register(registry);

        this.ordersFailed = Counter.builder("orders.failed")
            .description("Number of failed orders")
            .register(registry);

        // Timer to measure processing time
        this.orderProcessingTime = Timer.builder("orders.processing.time")
            .description("Order processing time")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(registry);

        // Gauge for active orders count
        this.activeOrders = new AtomicInteger(0);
        Gauge.builder("orders.active", activeOrders, AtomicInteger::get)
            .description("Number of orders being processed")
            .register(registry);
    }

    public void recordOrderCreated() {
        ordersCreated.increment();
    }

    public void recordOrderFailed() {
        ordersFailed.increment();
    }

    public Timer.Sample startProcessing() {
        activeOrders.incrementAndGet();
        return Timer.start();
    }

    public void stopProcessing(Timer.Sample sample) {
        sample.stop(orderProcessingTime);
        activeOrders.decrementAndGet();
    }
}
OrderService.javajava
@Service
public class OrderService {

    private final OrderMetrics metrics;

    public Order processOrder(CreateOrderRequest request) {
        Timer.Sample sample = metrics.startProcessing();
        try {
            Order order = createOrder(request);
            metrics.recordOrderCreated();
            return order;
        } catch (Exception e) {
            metrics.recordOrderFailed();
            throw e;
        } finally {
            metrics.stopProcessing(sample);
        }
    }
}

āđ€āļĄāļ•āļĢāļīāļāđ€āļŦāļĨāđˆāļēāļ™āļĩāđ‰āđ€āļ›āļīāļ”āđƒāļŦāđ‰ Prometheus āļ—āļĩāđˆ /actuator/prometheus āļŦāļĢāļ·āļ­āļ”āļķāļ‡āđ€āļ›āđ‡āļ™ JSON āļ—āļĩāđˆ /actuator/metrics

25. āļŠāļĢāđ‰āļēāļ‡ HealthIndicator āđ€āļ‰āļžāļēāļ°āļ‚āļ­āļ‡āļĢāļ°āļšāļš

Health indicator āļ„āļ­āļĒāļ•āļīāļ”āļ•āļēāļĄāļŠāļ–āļēāļ™āļ°āļ‚āļ­āļ‡āļŠāđˆāļ§āļ™āļ›āļĢāļ°āļāļ­āļšāļ—āļĩāđˆāļŠāļģāļ„āļąāļ

ExternalApiHealthIndicator.javajava
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {

    private final RestClient restClient;
    private final ExternalApiProperties properties;

    @Override
    public Health health() {
        try {
            long startTime = System.currentTimeMillis();

            ResponseEntity<Void> response = restClient.get()
                .uri(properties.getHealthEndpoint())
                .retrieve()
                .toBodilessEntity();

            long responseTime = System.currentTimeMillis() - startTime;

            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("responseTime", responseTime + "ms")
                    .withDetail("endpoint", properties.getHealthEndpoint())
                    .build();
            } else {
                return Health.down()
                    .withDetail("status", response.getStatusCode().value())
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withException(e)
                .withDetail("endpoint", properties.getHealthEndpoint())
                .build();
        }
    }
}
DatabaseHealthContributor.java - Composite healthjava
@Component
public class DatabaseHealthContributor implements CompositeHealthContributor {

    private final Map<String, HealthIndicator> indicators;

    public DatabaseHealthContributor(
        DataSource primaryDataSource,
        @Qualifier("replicaDataSource") DataSource replicaDataSource
    ) {
        this.indicators = Map.of(
            "primary", new DataSourceHealthIndicator(primaryDataSource),
            "replica", new DataSourceHealthIndicator(replicaDataSource)
        );
    }

    @Override
    public HealthContributor getContributor(String name) {
        return indicators.get(name);
    }

    @Override
    public Iterator<NamedContributor<HealthContributor>> iterator() {
        return indicators.entrySet().stream()
            .map(e -> NamedContributor.of(e.getKey(), e.getValue()))
            .iterator();
    }
}

Endpoint /actuator/health āļĢāļ§āļĄāļ‚āđ‰āļ­āļĄāļđāļĨāļ‚āļ­āļ‡ indicator āļ—āļąāđ‰āļ‡āļŦāļĄāļ” āļ•āļąāđ‰āļ‡āļ„āđˆāļē management.endpoint.health.show-details=always āđ€āļžāļ·āđˆāļ­āđāļŠāļ”āļ‡āļĢāļēāļĒāļĨāļ°āđ€āļ­āļĩāļĒāļ”

āļŦāļąāļ§āļ‚āđ‰āļ­āļ‚āļąāđ‰āļ™āļŠāļđāļ‡

26. āļ—āļģ caching āļ”āđ‰āļ§āļĒ Spring Cache āđāļĨāļ° Redis

āļĨāļģāļ”āļąāļšāļŠāļąāđ‰āļ™āļ™āļēāļĄāļ˜āļĢāļĢāļĄāļ‚āļ­āļ‡ Spring Cache āļ—āļģāđƒāļŦāđ‰āđ€āļŠāļ·āđˆāļ­āļĄāļ•āđˆāļ­āđāļ„āļŠāđāļšāļšāļāļĢāļ°āļˆāļēāļĒ āđ€āļŠāđˆāļ™ Redis āđ„āļ”āđ‰āļ‡āđˆāļēāļĒ

CacheConfig.javajava
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        // Specific configurations per cache
        Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
            "users", config.entryTtl(Duration.ofHours(1)),
            "products", config.entryTtl(Duration.ofMinutes(30)),
            "sessions", config.entryTtl(Duration.ofMinutes(5))
        );

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(cacheConfigs)
            .build();
    }
}
ProductService.javajava
@Service
public class ProductService {

    // Automatic result caching
    @Cacheable(value = "products", key = "#id")
    public Product findById(Long id) {
        log.info("Fetching product {} from database", id);
        return productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }

    // Cache update on modification
    @CachePut(value = "products", key = "#product.id")
    public Product update(Product product) {
        return productRepository.save(product);
    }

    // Cache invalidation
    @CacheEvict(value = "products", key = "#id")
    public void delete(Long id) {
        productRepository.deleteById(id);
    }

    // Clear entire cache
    @CacheEvict(value = "products", allEntries = true)
    public void clearProductCache() {
        log.info("Product cache cleared");
    }

    // Composite key for searches
    @Cacheable(value = "productSearch", key = "#category + '-' + #minPrice + '-' + #maxPrice")
    public List<Product> search(String category, BigDecimal minPrice, BigDecimal maxPrice) {
        return productRepository.findByCategoryAndPriceBetween(category, minPrice, maxPrice);
    }
}

āđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™ cache āđ€āļ›āđ‡āļ™āđāļšāļšāđ‚āļ›āļĢāđˆāļ‡āđƒāļŠ āđ‚āļ„āđ‰āļ”āļ—āļēāļ‡āļ˜āļļāļĢāļāļīāļˆāđ„āļĄāđˆāđ€āļ›āļĨāļĩāđˆāļĒāļ™ āđāļĨāļ°āļˆāļąāļ”āļāļēāļĢāđāļ„āļŠāļ”āđ‰āļ§āļĒāļĢāļđāļ›āđāļšāļš declarative

27. āļŠāļĢāđ‰āļēāļ‡āļ‡āļēāļ™āļ•āļēāļĄāļ•āļēāļĢāļēāļ‡āđ€āļ§āļĨāļēāļ”āđ‰āļ§āļĒ @Scheduled

Spring āļĄāļĩāļŦāļĨāļēāļĒāļ§āļīāļ˜āļĩāđƒāļ™āļāļēāļĢāļ•āļąāđ‰āļ‡āđ€āļ§āļĨāļēāļ‡āļēāļ™āļ—āļĩāđˆāļĢāļąāļ™āļ‹āđ‰āļģ

SchedulerConfig.javajava
@Configuration
@EnableScheduling
public class SchedulerConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("scheduled-");
        scheduler.setErrorHandler(t -> log.error("Scheduled task error", t));
        return scheduler;
    }
}
ScheduledTasks.javajava
@Component
public class ScheduledTasks {

    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

    // Execute every 5 minutes
    @Scheduled(fixedRate = 5 * 60 * 1000)
    public void syncExternalData() {
        log.info("Starting external data sync");
        // Sync with external system
    }

    // Execute 10 seconds after the previous one ends
    @Scheduled(fixedDelay = 10000, initialDelay = 5000)
    public void processQueue() {
        // Queue processing with delay between executions
    }

    // Cron expression: every day at 2 AM
    @Scheduled(cron = "0 0 2 * * *", zone = "Europe/Paris")
    public void dailyCleanup() {
        log.info("Running daily cleanup");
        cleanupService.removeExpiredSessions();
        cleanupService.archiveOldData();
    }

    // Configurable cron via properties
    @Scheduled(cron = "${app.reports.cron:0 0 6 * * MON}")
    public void generateWeeklyReport() {
        reportService.generateAndSend();
    }
}
ConditionalScheduling.java - Conditional activationjava
@Component
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class ConditionalScheduledTasks {

    @Scheduled(fixedRate = 60000)
    public void monitorSystem() {
        // Monitoring active only if configured
    }
}

āļ‡āļēāļ™āļ•āļēāļĄāļāļģāļŦāļ™āļ”āļ—āļģāļ‡āļēāļ™āļšāļ™ thread pool āđāļĒāļāļ•āđˆāļēāļ‡āļŦāļēāļ āđ€āļžāļ·āđˆāļ­āđ„āļĄāđˆāđƒāļŦāđ‰āļ‚āļąāļ”āļ‚āļ§āļēāļ‡āļāļēāļĢāļ›āļĢāļ°āļĄāļ§āļĨāļœāļĨāļ„āļģāļ‚āļ­

28. āļˆāļąāļ”āļāļēāļĢāļ­āļĩāđ€āļ§āļ™āļ•āđŒāļ”āđ‰āļ§āļĒ ApplicationEvent

āļĢāļ°āļšāļšāļ­āļĩāđ€āļ§āļ™āļ•āđŒāļŠāđˆāļ§āļĒāđƒāļŦāđ‰āļŠāđˆāļ§āļ™āļ›āļĢāļ°āļāļ­āļšāļ‚āļ­āļ‡āđāļ­āļ›āļ–āļđāļāđ€āļŠāļ·āđˆāļ­āļĄāđ‚āļĒāļ‡āđāļšāļšāļŦāļĨāļ§āļĄ

UserRegisteredEvent.javajava
public class UserRegisteredEvent extends ApplicationEvent {

    private final User user;
    private final Instant registeredAt;

    public UserRegisteredEvent(Object source, User user) {
        super(source);
        this.user = user;
        this.registeredAt = Instant.now();
    }

    public User getUser() { return user; }
    public Instant getRegisteredAt() { return registeredAt; }
}
UserService.java - Event publishingjava
@Service
public class UserService {

    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public User registerUser(CreateUserRequest request) {
        User user = new User(request.email(), request.name());
        user = userRepository.save(user);

        // Publish event after transaction
        eventPublisher.publishEvent(new UserRegisteredEvent(this, user));

        return user;
    }
}
UserEventListeners.java - Event listenersjava
@Component
public class UserEventListeners {

    private static final Logger log = LoggerFactory.getLogger(UserEventListeners.class);

    // Synchronous listener
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        log.info("User registered: {}", event.getUser().getEmail());
    }

    // Asynchronous listener to avoid blocking the main thread
    @Async
    @EventListener
    public void sendWelcomeEmail(UserRegisteredEvent event) {
        emailService.sendWelcomeEmail(event.getUser());
    }

    // Transactional listener: executes after commit
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void notifyExternalSystem(UserRegisteredEvent event) {
        // External notification only if transaction succeeds
        externalApiClient.notifyNewUser(event.getUser().getId());
    }

    // Conditional listener with SpEL
    @EventListener(condition = "#event.user.role == 'PREMIUM'")
    public void handlePremiumUserRegistered(UserRegisteredEvent event) {
        premiumService.initializePremiumFeatures(event.getUser());
    }
}

āļ­āļĩāđ€āļ§āļ™āļ•āđŒāđāļšāļš transactional (@TransactionalEventListener) āļĢāļąāļāļĐāļēāļ„āļ§āļēāļĄāļŠāļ­āļ”āļ„āļĨāđ‰āļ­āļ‡āļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨāļāļąāļšāļœāļĨāļ‚āđ‰āļēāļ‡āđ€āļ„āļĩāļĒāļ‡

29. āđƒāļŠāđ‰ HTTP client āļ”āđ‰āļ§āļĒ RestClient (Spring Boot 3+)

RestClient āļ„āļ·āļ­ API āļ—āļĩāđˆāļ—āļąāļ™āļŠāļĄāļąāļĒāđāļĨāļ°āđ„āļŦāļĨāļĨāļ·āđˆāļ™āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāđ€āļĢāļĩāļĒāļ HTTP āđāļšāļšāļ‹āļīāļ‡āđ‚āļ„āļĢāļ™āļąāļŠ āđāļ—āļ™āļ—āļĩāđˆ RestTemplate

ExternalApiClient.javajava
@Component
public class ExternalApiClient {

    private final RestClient restClient;

    public ExternalApiClient(RestClient.Builder builder, ExternalApiProperties props) {
        this.restClient = builder
            .baseUrl(props.getBaseUrl())
            .defaultHeader("Authorization", "Bearer " + props.getApiKey())
            .defaultHeader("Accept", "application/json")
            .requestInterceptor((request, body, execution) -> {
                log.debug("Request: {} {}", request.getMethod(), request.getURI());
                return execution.execute(request, body);
            })
            .build();
    }

    public User fetchUser(Long id) {
        return restClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .body(User.class);
    }

    public List<Product> searchProducts(String query, int page) {
        return restClient.get()
            .uri(uriBuilder -> uriBuilder
                .path("/products/search")
                .queryParam("q", query)
                .queryParam("page", page)
                .build())
            .retrieve()
            .body(new ParameterizedTypeReference<List<Product>>() {});
    }

    public Order createOrder(CreateOrderRequest request) {
        return restClient.post()
            .uri("/orders")
            .contentType(MediaType.APPLICATION_JSON)
            .body(request)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
                throw new OrderValidationException("Invalid order: " + res.getStatusCode());
            })
            .body(Order.class);
    }

    // Error handling with ResponseEntity
    public Optional<User> findUser(Long id) {
        ResponseEntity<User> response = restClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .toEntity(User.class);

        return response.getStatusCode().is2xxSuccessful()
            ? Optional.ofNullable(response.getBody())
            : Optional.empty();
    }
}

RestClient āļĄāļĩ API āļ„āļĨāđ‰āļēāļĒ WebClient āđāļ•āđˆāđƒāļŠāđ‰āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāđ€āļĢāļĩāļĒāļāđāļšāļš blocking āđ€āļŦāļĄāļēāļ°āļāļąāļšāđāļ­āļ›āļ—āļĩāđˆāđ„āļĄāđˆāđƒāļŠāđ‰āļāļēāļĢāđ€āļ‚āļĩāļĒāļ™āđ‚āļ›āļĢāđāļāļĢāļĄāđ€āļŠāļīāļ‡āļ›āļāļīāļāļīāļĢāļīāļĒāļē

30. āļˆāļąāļ”āļāļēāļĢ migration āļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨāļ”āđ‰āļ§āļĒ Flyway

Flyway āļ—āļģāđƒāļŦāđ‰āļāļēāļĢ migrate schema āđ€āļ›āđ‡āļ™āļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļīāļžāļĢāđ‰āļ­āļĄāļĢāļ°āļšāļšāđ€āļ§āļ­āļĢāđŒāļŠāļąāļ™āđāļĨāļ°āļŠāļēāļĄāļēāļĢāļ–āļ—āļģāļ‹āđ‰āļģāđ„āļ”āđ‰

properties
# application.properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.validate-on-migrate=true
sql
-- db/migration/V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
sql
-- db/migration/V2__add_user_status.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'ACTIVE';

CREATE INDEX idx_users_status ON users(status);
sql
-- db/migration/V3__create_orders_table.sql
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    total_amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
FlywayConfig.java - Advanced configurationjava
@Configuration
public class FlywayConfig {

    @Bean
    public FlywayMigrationStrategy migrationStrategy() {
        return flyway -> {
            // Validation before migration
            flyway.validate();
            // Execute migrations
            flyway.migrate();
        };
    }

    // Callback for post-migration actions
    @Bean
    public Callback flywayCallback() {
        return new BaseCallback() {
            @Override
            public void handle(Event event, Context context) {
                if (event == Event.AFTER_MIGRATE) {
                    log.info("Migrations completed successfully");
                }
            }
        };
    }
}

āđ„āļŸāļĨāđŒ migration āđāļ•āđˆāļĨāļ°āđ„āļŸāļĨāđŒāļˆāļ°āļĢāļąāļ™āđ€āļžāļĩāļĒāļ‡āļ„āļĢāļąāđ‰āļ‡āđ€āļ”āļĩāļĒāļ§āđāļĨāļ°āļ•āļĢāļ§āļˆ checksum āđ€āļžāļ·āđˆāļ­āļ•āļĢāļ§āļˆāļˆāļąāļšāļāļēāļĢāđāļāđ‰āđ„āļ‚āđ‚āļ”āļĒāļšāļąāļ‡āđ€āļ­āļīāļ

āđ€āļĢāļīāđˆāļĄāļāļķāļāļ‹āđ‰āļ­āļĄāđ€āļĨāļĒ!

āļ—āļ”āļŠāļ­āļšāļ„āļ§āļēāļĄāļĢāļđāđ‰āļ‚āļ­āļ‡āļ„āļļāļ“āļ”āđ‰āļ§āļĒāļ•āļąāļ§āļˆāļģāļĨāļ­āļ‡āļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđāļĨāļ°āđāļšāļšāļ—āļ”āļŠāļ­āļšāđ€āļ—āļ„āļ™āļīāļ„āļ„āļĢāļąāļš

āļšāļ—āļŠāļĢāļļāļ›

30 āļ„āļģāļ–āļēāļĄāļ™āļĩāđ‰āļ„āļĢāļ­āļšāļ„āļĨāļļāļĄāđāļāđˆāļ™āļ‚āļ­āļ‡ Spring Boot āļ—āļĩāđˆāđƒāļŠāđ‰āļ›āļĢāļ°āđ€āļĄāļīāļ™āđƒāļ™āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđ€āļŠāļīāļ‡āđ€āļ—āļ„āļ™āļīāļ„ āļāļēāļĢāđ€āļ‚āđ‰āļēāđƒāļˆāđāļ™āļ§āļ„āļīāļ”āđ€āļŦāļĨāđˆāļēāļ™āļĩāđ‰āđāļŠāļ”āļ‡āđƒāļŦāđ‰āđ€āļŦāđ‡āļ™āļ–āļķāļ‡āļ„āļ§āļēāļĄāđ€āļŠāļĩāđˆāļĒāļ§āļŠāļēāļāđƒāļ™āđ€āļŸāļĢāļĄāđ€āļ§āļīāļĢāđŒāļāđāļĨāļ°āđāļ™āļ§āļ—āļēāļ‡āļ—āļĩāđˆāļ”āļĩāđƒāļ™āļāļēāļĢāļžāļąāļ’āļ™āļē Java

āđ€āļŠāđ‡āļāļĨāļīāļŠāļ•āđŒāđ€āļ•āļĢāļĩāļĒāļĄāļ•āļąāļ§:

  • ✅ āđ€āļ‚āđ‰āļēāđƒāļˆāļāļĨāđ„āļ auto-configuration āđāļĨāļ°āđ€āļ‡āļ·āđˆāļ­āļ™āđ„āļ‚āļ—āļĩāđˆāđ€āļāļĩāđˆāļĒāļ§āļ‚āđ‰āļ­āļ‡
  • ✅ āļŠāļģāļ™āļēāļ Spring Data JPA: query, āļ˜āļļāļĢāļāļĢāļĢāļĄ, āļ›āļąāļāļŦāļē N+1
  • ✅ āļ•āļąāđ‰āļ‡āļ„āđˆāļē Spring Security āļ”āđ‰āļ§āļĒ JWT āđāļĨāļ°āļ„āļ§āļēāļĄāļ›āļĨāļ­āļ”āļ āļąāļĒāļĢāļ°āļ”āļąāļšāđ€āļĄāļ˜āļ­āļ”āđ„āļ”āđ‰
  • ✅ āļĢāļđāđ‰āļˆāļąāļāđāļ­āļ™āđ‚āļ™āđ€āļ—āļŠāļąāļ™āļ—āļ”āļŠāļ­āļšāļ•āđˆāļēāļ‡ āđ† āđāļĨāļ°āļāļĢāļ“āļĩāļāļēāļĢāđƒāļŠāđ‰āļ‡āļēāļ™
  • ✅ āđƒāļŠāđ‰ profile āđāļĨāļ° @ConfigurationProperties āđ€āļžāļ·āđˆāļ­āđāļĒāļāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē
  • ✅ āļ—āļģ caching, scheduling āđāļĨāļ°āļˆāļąāļ”āļāļēāļĢāļ­āļĩāđ€āļ§āļ™āļ•āđŒ
  • ✅ āđ€āļœāļĒāđāļžāļĢāđˆāđ€āļĄāļ•āļĢāļīāļāđāļĨāļ° health indicator āļ—āļĩāđˆāļāļģāļŦāļ™āļ”āđ€āļ­āļ‡
  • ✅ āđƒāļŠāđ‰ RestClient āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāđ€āļĢāļĩāļĒāļ HTTP āļŠāļĄāļąāļĒāđƒāļŦāļĄāđˆ

āļāļēāļĢāļāļķāļāļāļ™āļŠāļĄāđˆāļģāđ€āļŠāļĄāļ­āļšāļ™āđ‚āļ›āļĢāđ€āļˆāļāļ•āđŒāļˆāļĢāļīāļ‡āļĒāļąāļ‡āļ„āļ‡āđ€āļ›āđ‡āļ™āļ§āļīāļ˜āļĩāļ—āļĩāđˆāļ”āļĩāļ—āļĩāđˆāļŠāļļāļ”āđƒāļ™āļāļēāļĢāļ•āļ­āļāļĒāđ‰āļģāļ„āļ§āļēāļĄāļĢāļđāđ‰āđāļĨāļ°āļ•āļ­āļšāļ„āļģāļ–āļēāļĄāļ—āļēāļ‡āđ€āļ—āļ„āļ™āļīāļ„āđ„āļ”āđ‰āļ­āļĒāđˆāļēāļ‡āļĄāļąāđˆāļ™āđƒāļˆ

āđāļ—āđ‡āļ

#spring boot
#java
#interview
#backend
#java interview

āđāļŠāļĢāđŒ

āļšāļ—āļ„āļ§āļēāļĄāļ—āļĩāđˆāđ€āļāļĩāđˆāļĒāļ§āļ‚āđ‰āļ­āļ‡

Spring Boot āļāļēāļĢāļāļĢāļ°āļˆāļēāļĒāļ˜āļļāļĢāļāļĢāļĢāļĄ: āļ„āļģāļ–āļēāļĄāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđāļĨāļ°āļ•āļąāļ§āļ­āļĒāđˆāļēāļ‡āļˆāļĢāļīāļ‡

āļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot: āļāļēāļĢāļāļĢāļ°āļˆāļēāļĒāļ˜āļļāļĢāļāļĢāļĢāļĄ

āđ€āļŠāļĩāđˆāļĒāļ§āļŠāļēāļāļāļēāļĢāļāļĢāļ°āļˆāļēāļĒāļ˜āļļāļĢāļāļĢāļĢāļĄāđƒāļ™ Spring Boot: REQUIRED, REQUIRES_NEW, NESTED āđāļĨāļ°āļ­āļ·āđˆāļ™ āđ† 12 āļ„āļģāļ–āļēāļĄāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāļžāļĢāđ‰āļ­āļĄāđ‚āļ„āđ‰āļ”āđāļĨāļ°āļāļąāļšāļ”āļąāļāļ—āļąāđˆāļ§āđ„āļ›

āļŸāļĩāđ€āļˆāļ­āļĢāđŒāđƒāļŦāļĄāđˆāđāļĨāļ°āļāļēāļĢāļ›āļĢāļąāļšāļ›āļĢāļļāļ‡āđƒāļ™ Spring Boot 3.4

Spring Boot 3.4: āļŸāļĩāđ€āļˆāļ­āļĢāđŒāđƒāļŦāļĄāđˆāļ—āļąāđ‰āļ‡āļŦāļĄāļ”āļ­āļ˜āļīāļšāļēāļĒāļ„āļĢāļšāļ–āđ‰āļ§āļ™

Spring Boot 3.4 āļ™āļģāđ€āļŠāļ™āļ­ structured logging āđāļšāļš native, virtual threads āļ—āļĩāđˆāļ‚āļĒāļēāļĒāđ€āļžāļīāđˆāļĄ, graceful shutdown āđ€āļ›āđ‡āļ™āļ„āđˆāļēāđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™ āđāļĨāļ° MockMvcTester āļ„āļđāđˆāļĄāļ·āļ­āļ‰āļšāļąāļšāļŠāļĄāļšāļđāļĢāļ“āđŒāļ‚āļ­āļ‡āļŸāļĩāđ€āļˆāļ­āļĢāđŒāđƒāļŦāļĄāđˆ

Spring Modulith: āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ monolith āđāļšāļšāđ‚āļĄāļ”āļđāļĨāļēāļĢāđŒāļ”āđ‰āļ§āļĒ Spring Boot

Spring Modulith: āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ Monolith āđāļšāļšāđ‚āļĄāļ”āļđāļĨāļēāļĢāđŒ

āđ€āļĢāļĩāļĒāļ™āļĢāļđāđ‰ Spring Modulith āđ€āļžāļ·āđˆāļ­āļŠāļĢāđ‰āļēāļ‡ monolith āđāļšāļšāđ‚āļĄāļ”āļđāļĨāļēāļĢāđŒāđƒāļ™ Java āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ āđ‚āļĄāļ”āļđāļĨ āļ­āļĩāđ€āļ§āļ™āļ•āđŒāļ­āļ°āļ‹āļīāļ‡āđ‚āļ„āļĢāļ™āļąāļŠ āđāļĨāļ°āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļ”āđ‰āļ§āļĒ Spring Boot 3