30 āļāļģāļāļēāļĄāļŠāļąāļĄāļ āļēāļĐāļāđ Spring Boot: āļāļđāđāļĄāļ·āļāļāļāļąāļāđāļāđāļĄāļŠāļģāļŦāļĢāļąāļāļāļąāļāļāļąāļāļāļē Java
āđāļāļĢāļĩāļĒāļĄāļāļĢāđāļāļĄāļŠāļąāļĄāļ āļēāļĐāļāđ Spring Boot āļāđāļ§āļĒ 30 āļāļģāļāļēāļĄāļŦāļĨāļąāļāļāļĢāļāļāļāļĨāļļāļĄ auto-configuration, starter, Spring Data JPA, āļāļ§āļēāļĄāļāļĨāļāļāļ āļąāļĒ āđāļĨāļ°āļāļēāļĢāļāļāļŠāļāļ

Spring Boot āļāļĨāļēāļĒāđāļāđāļāđāļāļĢāļĄāđāļ§āļīāļĢāđāļāļŦāļĨāļąāļāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļąāļāļāļē Java āđāļāļĢāļ°āļāļąāļāļāļāļāđāļāļĢ āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđāđāļāļīāļāđāļāļāļāļīāļāļāļ°āļāļĢāļ°āđāļĄāļīāļāļāļ§āļēāļĄāđāļāđāļēāđāļāļāļĨāđāļāļ āļēāļĒāđāļ āļŦāļĨāļąāļāļāļāļīāļāļąāļāļīāļāļĩāđāļāļĩ āđāļĨāļ°āļĢāļ°āļāļāļāļīāđāļ§āļĻāļāļāļ Spring āļāļđāđāļĄāļ·āļāļāļāļąāļāļāļĩāđāļĢāļ§āļĄ 30 āļāļģāļāļēāļĄāļāļĩāđāļāļāļāđāļāļĒ āļāļąāđāļāđāļāđāđāļāļ§āļāļīāļāļāļ·āđāļāļāļēāļāļāļāļāļķāļāļŦāļąāļ§āļāđāļāļāļąāđāļāļŠāļđāļ
āļāļđāđāļŠāļąāļĄāļ āļēāļĐāļāđāļāļ·āđāļāļāļāļāļāļđāđāļŠāļĄāļąāļāļĢāļāļĩāđāđāļāđāļēāđāļ "āđāļŦāļāļļāļāļĨ" āđāļāļ·āđāļāļāļŦāļĨāļąāļāļāļāļāđāļāđāļĨāļ°āļāļĩāđāļāļāļĢāđ āļāļāļāđāļŦāļāļ·āļāļāļēāļ syntax āļāļ§āļĢāļāļāļīāļāļēāļĒāļ§āđāļē Spring Boot āđāļāđāļāļąāļāļŦāļēāļāļ°āđāļĢāđāļāđ
āļāļ·āđāļāļāļēāļāļāļāļ Spring Boot
1. Spring āļāļąāļ Spring Boot āļāđāļēāļāļāļąāļāļāļĒāđāļēāļāđāļĢ?
Spring āđāļāđāļāđāļāļĢāļĄāđāļ§āļīāļĢāđāļāđāļāļāđāļĄāļāļđāļĨāļāļĩāđāđāļŦāđ dependency injection, āļāļēāļĢāļāļąāļāļāļēāļĢāļāļļāļĢāļāļĢāļĢāļĄ āđāļĨāļ°āļāļēāļĢāđāļāļ·āđāļāļĄāļāđāļāļāļąāļāđāļāļāđāļāđāļĨāļĒāļĩāļŦāļĨāļēāļĒāļāļāļīāļ āļŠāđāļ§āļ Spring Boot āļāļ·āļāđāļĨāđāļĒāļāļĢāđāļāļēāļĄāļāļĢāļĢāļĄāļāļĩāđāļāļĒāļđāđāļāļ Spring āđāļāļ·āđāļāļāļģāđāļŦāđāļāļēāļĢāļāļąāđāļāļāđāļēāđāļĨāļ°āļāļēāļĢāđāļĢāļīāđāļĄāļāđāļāđāļāļāļāļĨāļīāđāļāļāļąāļāļāđāļēāļĒāļāļķāđāļ
// 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
@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
<!-- 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 āļĒāļąāļāđāļŦāļĄāļēāļ°āļāļąāļāđāļāļĢāļāļŠāļĢāđāļēāļāđāļāļāļĢāļēāļāļĄāļēāļāļāļ§āđāļē
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.jpa.hibernate.ddl-auto=update# application.yml - clear hierarchical structure
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: admin
jpa:
hibernate:
ddl-auto: updateProfile āļāđāļ§āļĒāļāļĢāļąāļāļāļēāļĢāļāļąāđāļāļāđāļēāđāļŦāđāđāļŦāļĄāļēāļ°āļāļąāļāļŠāļ āļēāļāđāļ§āļāļĨāđāļāļĄ: application-dev.yml, application-prod.yml āđāļāļīāļāđāļāđāļāļēāļāļāđāļēāļ spring.profiles.active
5. @SpringBootApplication āļāļģāļāļēāļāļāļĒāđāļēāļāđāļĢ?
āđāļāļāđāļāđāļāļāļąāļāļāļĩāđāļĢāļ§āļĄāđāļāļāđāļāđāļāļāļąāļāļŠāļģāļāļąāļāļŠāļēāļĄāļāļąāļ§āļāļĩāđāļāļģāļŦāļāļāļāļĪāļāļīāļāļĢāļĢāļĄāļāļāļāđāļāļ Spring Boot
// @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 āļāđāļēāļāļāļąāļāļāļĒāđāļēāļāđāļĢ?
āļāļīāļāđāļāļāļĢāđāđāļāļāđāļŦāļĨāđāļēāļāļĩāđāđāļāđāļāļĨāļģāļāļąāļāļāļąāđāļāļāļĩāđāđāļŦāđāļāļ§āļēāļĄāļŠāļēāļĄāļēāļĢāļāđāļāļāļēāļĢāđāļāđāļēāļāļķāļāļāđāļāļĄāļđāļĨāđāļāļīāđāļĄāļāļķāđāļāđāļĢāļ·āđāļāļĒ āđ
// 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 āđāļĄāđāđāļāļĩāļĒāļāļāļ
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 āđāļĨāļ°āļāļēāļĢāļŠāđāļāļāđāļāđāļŦāđāļāļąāļāđāļāļĄāļąāļāļī
@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 āļāļ°āđāļŦāļĨāļāļāļąāļāļāļĩ
@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;
}@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
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();
}@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 āļāļĩāđāđāļāđāļāđāļĢāļāđāļĨāļ°āļāļĢāļ§āļāļŠāļāļāļāļīāļāļāļļāļāļāļąāļāđāļāļĄāļąāļāļī
@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));
}
}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 āļĢāļ§āļĄāļĻāļđāļāļĒāđāļāđāļ§āļĒāđāļŦāđāļĢāļđāļāđāļāļāļāļāļāļāļēāļĢāļāļāļāļāļĨāļąāļāļāđāļāļāļīāļāļāļĨāļēāļāđāļāđāļāļĄāļēāļāļĢāļāļēāļāđāļĨāļ°āļĨāļāđāļāđāļāļāđāļģ
@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);
}
}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
@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 āđāļāđāļĨāļ°āđāļāļāļĄāļĩāļāļļāļāđāļāđāļāļāđāļēāļāļāļąāļ
// 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);
}
}// 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);
}
}// 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) āļāļ§āļāļāļļāļĄāļāļģāļāļāļāļēāļāļāđāļāļāļēāļāļāđāļēāļ āđ āļĄāļĩāļŦāļĨāļēāļĒāļĢāļ°āļāļąāļāļāļēāļĢāļāļąāđāļāļāđāļēāđāļŦāđāđāļĨāļ·āļāļāđāļāđ
@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
}
}// 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+ āđāļāđāđāļāļ§āļāļēāļāđāļāļīāļāļāļąāļāļāđāļāļąāļāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļąāđāļāļāđāļēāļāļ§āļēāļĄāļāļĨāļāļāļ āļąāļĒ
@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();
}
}@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, āļŠāļīāļāļāļīāđ āļŦāļĢāļ·āļāļāđāļāļĄāļđāļĨ
@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();
}
}@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// Additional configuration if needed
}@PreAuthorize āļāļĢāļ§āļāļāđāļāļāļāļēāļĢāđāļĢāļĩāļĒāļ @PostAuthorize āļāļĢāļ§āļāļŦāļĨāļąāļāļāļēāļĢāđāļĢāļĩāļĒāļ āļŠāđāļ§āļ SpEL āļāļģāđāļŦāđāļŠāļēāļĄāļēāļĢāļāđāļāļĩāļĒāļāļāļāļāļąāļāļāđāļāļāđāļāđ
18. āļāđāļāļāļāļąāļ endpoint āļāļēāļāļāļēāļĢāđāļāļĄāļāļĩ CSRF
CSRF (Cross-Site Request Forgery) āđāļāđāļāļĢāļ°āđāļĒāļāļāđāļāļēāļāđāļāļŠāļāļąāļāļāļāļāļāļđāđāđāļāđāļāļĩāđāļĨāđāļāļāļāļīāļāļāļĒāļđāđ āļāļēāļĢāļāđāļāļāļāļąāļāļāļđāļāđāļāļīāļāđāļāđāļāļēāļāđāļāļĒāļāļĢāļīāļĒāļēāļĒāļŠāļģāļŦāļĢāļąāļāđāļāļāļāļĩāđāđāļāđāđāļāļŠāļāļąāļ
@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();
}
}@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 āļāļĩāđāđāļāđ JWT āļāļ°āļāļĨāļāļāļ āļąāļĒāļāļēāļ CSRF āđāļāļĒāļāļĢāļĢāļĄāļāļēāļāļī āđāļāļĢāļēāļ° token āļāđāļāļāļāļđāļāđāļāļāļĄāļēāļāļļāļāļāļģāļāļ āļŠāđāļ§āļāđāļāļāļāļĩāđāđāļāđāļāļļāļāļāļĩāđāđāļāļŠāļāļąāļāļāļģāđāļāđāļāļāđāļāļāđāļāļīāļ CSRF protection āđāļŠāļĄāļ
āļāļēāļĢāļāļāļŠāļāļāđāļ Spring Boot
19. @SpringBootTest āļāļąāļ @WebMvcTest āđāļāļāļāđāļēāļāļāļĒāđāļēāļāđāļĢ?
āđāļāļāđāļāđāļāļāļąāļāđāļŦāļĨāđāļēāļāļĩāđāļāļģāļŦāļāļāļāļĢāļīāļāļāļāļēāļĢāļāļāļŠāļāļāļāđāļēāļāļāļąāļāļāļēāļĄāļāļ§āļēāļĄāļāđāļāļāļāļēāļĢ
@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");
}
}@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 āļāđāļ§āļĒāļāļēāļāļāđāļāļĄāļđāļĨāļāļąāļāļāļąāļ§
@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 āļāļĩāđāļĄāļĩāļāļēāļāļāđāļāļĄāļđāļĨāļŦāļĢāļ·āļāļāļĢāļīāļāļēāļĢāļāļĢāļīāļāļĢāļ°āļŦāļ§āđāļēāļāļāļēāļĢāļāļāļŠāļāļ
@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");
}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 āļāļĩāđāļĄāļĩāļāļĢāļ°āđāļ āļāļāļĢāđāļāļĄāļāļēāļĢāļāļĢāļ§āļāļŠāļāļāļāļ§āļēāļĄāļāļđāļāļāđāļāļ
@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;
}
}
}# 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@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 āļāļĢāļąāļāļāļēāļĢāļāļąāđāļāļāđāļēāđāļŦāđāļŠāļāļāļāļĨāđāļāļāļāļąāļāļŠāļ āļēāļāđāļ§āļāļĨāđāļāļĄāļāļēāļĢāļĢāļąāļ
# 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@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"));
}
}// 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 āļŠāļģāļŦāļĢāļąāļāļĢāļ°āļāļāļĄāļāļāļīāđāļāļāļĢāđ āđāļĄāļāļĢāļīāļāļāļĩāđāļāļģāļŦāļāļāđāļāļāļāļģāđāļŦāđāļŠāļąāļāđāļāļāļāļēāļĢāļāđāļĢāļ°āļāļāđāļāđāļāļĩāļāļķāđāļ
@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();
}
}@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 āļāļāļĒāļāļīāļāļāļēāļĄāļŠāļāļēāļāļ°āļāļāļāļŠāđāļ§āļāļāļĢāļ°āļāļāļāļāļĩāđāļŠāļģāļāļąāļ
@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();
}
}
}@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 āđāļāđāļāđāļēāļĒ
@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();
}
}@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 āļĄāļĩāļŦāļĨāļēāļĒāļ§āļīāļāļĩāđāļāļāļēāļĢāļāļąāđāļāđāļ§āļĨāļēāļāļēāļāļāļĩāđāļĢāļąāļāļāđāļģ
@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;
}
}@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();
}
}@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
āļĢāļ°āļāļāļāļĩāđāļ§āļāļāđāļāđāļ§āļĒāđāļŦāđāļŠāđāļ§āļāļāļĢāļ°āļāļāļāļāļāļāđāļāļāļāļđāļāđāļāļ·āđāļāļĄāđāļĒāļāđāļāļāļŦāļĨāļ§āļĄ
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; }
}@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;
}
}@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
@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 āđāļāđāļāļāļąāļāđāļāļĄāļąāļāļīāļāļĢāđāļāļĄāļĢāļ°āļāļāđāļ§āļāļĢāđāļāļąāļāđāļĨāļ°āļŠāļēāļĄāļēāļĢāļāļāļģāļāđāļģāđāļāđ
# application.properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.validate-on-migrate=true-- 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);-- 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);-- 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);@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: āļāļēāļĢāļāļĢāļ°āļāļēāļĒāļāļļāļĢāļāļĢāļĢāļĄ
āđāļāļĩāđāļĒāļ§āļāļēāļāļāļēāļĢāļāļĢāļ°āļāļēāļĒāļāļļāļĢāļāļĢāļĢāļĄāđāļ Spring Boot: REQUIRED, REQUIRES_NEW, NESTED āđāļĨāļ°āļāļ·āđāļ āđ 12 āļāļģāļāļēāļĄāļŠāļąāļĄāļ āļēāļĐāļāđāļāļĢāđāļāļĄāđāļāđāļāđāļĨāļ°āļāļąāļāļāļąāļāļāļąāđāļ§āđāļ

Spring Boot 3.4: āļāļĩāđāļāļāļĢāđāđāļŦāļĄāđāļāļąāđāļāļŦāļĄāļāļāļāļīāļāļēāļĒāļāļĢāļāļāđāļ§āļ
Spring Boot 3.4 āļāļģāđāļŠāļāļ structured logging āđāļāļ native, virtual threads āļāļĩāđāļāļĒāļēāļĒāđāļāļīāđāļĄ, graceful shutdown āđāļāđāļāļāđāļēāđāļĢāļīāđāļĄāļāđāļ āđāļĨāļ° MockMvcTester āļāļđāđāļĄāļ·āļāļāļāļąāļāļŠāļĄāļāļđāļĢāļāđāļāļāļāļāļĩāđāļāļāļĢāđāđāļŦāļĄāđ

Spring Modulith: āļŠāļāļēāļāļąāļāļĒāļāļĢāļĢāļĄ Monolith āđāļāļāđāļĄāļāļđāļĨāļēāļĢāđ
āđāļĢāļĩāļĒāļāļĢāļđāđ Spring Modulith āđāļāļ·āđāļāļŠāļĢāđāļēāļ monolith āđāļāļāđāļĄāļāļđāļĨāļēāļĢāđāđāļ Java āļŠāļāļēāļāļąāļāļĒāļāļĢāļĢāļĄ āđāļĄāļāļđāļĨ āļāļĩāđāļ§āļāļāđāļāļ°āļāļīāļāđāļāļĢāļāļąāļŠ āđāļĨāļ°āļāļēāļĢāļāļāļŠāļāļāļāđāļ§āļĒ Spring Boot 3