Spring Boot Advanced Features

1. Auto Configuration

1.1 Custom Auto Configuration

@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DatabaseProperties.class)
public class DatabaseAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource(DatabaseProperties properties) {
        return DataSourceBuilder
            .create()
            .url(properties.getUrl())
            .username(properties.getUsername())
            .password(properties.getPassword())
            .build();
    }
}

@ConfigurationProperties(prefix = "app.database")
public class DatabaseProperties {
    private String url;
    private String username;
    private String password;

    // Getters and setters
}

// META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.DatabaseAutoConfiguration

1.2 Conditional Configuration

// Custom Condition
public class OnCustomCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, 
                         AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("custom.property");
    }
}

@Conditional(OnCustomCondition.class)
@Configuration
public class CustomConfiguration {
    // Configuration based on condition
}

// Built-in Conditions
@ConditionalOnProperty(prefix = "app", name = "feature")
@ConditionalOnBean(DataSource.class)
@ConditionalOnMissingBean
@ConditionalOnClass(name = "org.springframework.data.redis.RedisClient")
@ConditionalOnExpression("${app.feature.enabled:true}")
@Profile("dev")

2. Custom Starter

2.1 Starter Structure

my-spring-boot-starter/
├── pom.xml
├── src/main/java/
│   └── com/example/starter/
│       ├── MyAutoConfiguration.java
│       ├── MyProperties.java
│       └── MyService.java
└── src/main/resources/
    └── META-INF/
        └── spring.factories

2.2 Implementation

// Auto Configuration
@Configuration
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MyService myService(MyProperties properties) {
        return new MyService(properties);
    }
}

// Properties
@ConfigurationProperties(prefix = "my.service")
public class MyProperties {
    private boolean enabled = true;
    private String prefix = "default";

    // Getters and setters
}

// Service
public class MyService {
    private final MyProperties properties;

    public MyService(MyProperties properties) {
        this.properties = properties;
    }

    public String doSomething() {
        if (!properties.isEnabled()) {
            return null;
        }
        return properties.getPrefix() + ": Done";
    }
}

3. Event Handling

3.1 Application Events

// Custom Event
public class UserRegisteredEvent extends ApplicationEvent {
    private final User user;

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

    public User getUser() {
        return user;
    }
}

// Event Publisher
@Service
public class UserService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void registerUser(User user) {
        // Business logic
        eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
    }
}

// Event Listeners
@Component
public class UserEventListener {
    @EventListener
    public void handleUserRegistration(UserRegisteredEvent event) {
        // Handle event
    }

    @EventListener
    @Async
    public void handleAsyncEvent(UserRegisteredEvent event) {
        // Handle event asynchronously
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleTransactionalEvent(UserRegisteredEvent event) {
        // Handle event after transaction commits
    }
}

4. Custom Actuator Endpoints

4.1 Custom Endpoint

@Component
@Endpoint(id = "custom")
public class CustomEndpoint {

    @ReadOperation
    public Map<String, Object> getCustomInfo() {
        Map<String, Object> info = new HashMap<>();
        info.put("timestamp", System.currentTimeMillis());
        info.put("status", "UP");
        return info;
    }

    @WriteOperation
    public void setCustomInfo(@Selector String name, 
                            @Selector String value) {
        // Update operation
    }

    @DeleteOperation
    public void deleteCustomInfo(@Selector String name) {
        // Delete operation
    }
}

4.2 Health Indicator

@Component
public class CustomHealthIndicator extends AbstractHealthIndicator {

    @Override
    protected void doHealthCheck(Builder builder) throws Exception {
        try {
            // Perform health check
            builder.up()
                .withDetail("app", "Healthy")
                .withDetail("time", System.currentTimeMillis());
        } catch (Exception e) {
            builder.down()
                .withDetail("error", e.getMessage());
        }
    }
}

5. Testing

5.1 Unit Testing

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void whenFindById_thenReturnUser() {
        // Given
        User user = new User();
        when(userRepository.findById(1L))
            .thenReturn(Optional.of(user));

        // When
        User found = userService.findById(1L);

        // Then
        assertNotNull(found);
        verify(userRepository).findById(1L);
    }
}

5.2 Integration Testing

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void whenCreateUser_thenStatus201() throws Exception {
        User user = new User("test@test.com");
        String json = objectMapper.writeValueAsString(user);

        mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content(json))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.email").value("test@test.com"));
    }
}

6. Security

6.1 JWT Authentication

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(
                new JwtAuthenticationFilter(tokenProvider),
                UsernamePasswordAuthenticationFilter.class
            );
    }
}

@Component
public class JwtTokenProvider {
    @Value("${app.jwtSecret}")
    private String jwtSecret;

    @Value("${app.jwtExpirationInMs}")
    private int jwtExpirationInMs;

    public String generateToken(Authentication authentication) {
        UserPrincipal userPrincipal = 
            (UserPrincipal) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);

        return Jwts.builder()
            .setSubject(Long.toString(userPrincipal.getId()))
            .setIssuedAt(new Date())
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }

    public Long getUserIdFromJWT(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();

        return Long.parseLong(claims.getSubject());
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(authToken);
            return true;
        } catch (SignatureException |
                MalformedJwtException |
                ExpiredJwtException |
                UnsupportedJwtException |
                IllegalArgumentException ex) {
            return false;
        }
    }
}

6.2 OAuth2

@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
            .authorizationEndpoint()
            .baseUri("/oauth2/authorize")
            .and()
            .redirectionEndpoint()
            .baseUri("/oauth2/callback/*")
            .and()
            .userInfoEndpoint()
            .userService(customOAuth2UserService)
            .and()
            .successHandler(oAuth2AuthenticationSuccessHandler)
            .failureHandler(oAuth2AuthenticationFailureHandler);
    }
}

@Service
public class CustomOAuth2UserService 
    extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) 
        throws OAuth2AuthenticationException {

        OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);

        try {
            return processOAuth2User(
                oAuth2UserRequest, 
                oAuth2User
            );
        } catch (Exception ex) {
            throw new OAuth2AuthenticationProcessingException(ex.getMessage());
        }
    }

    private OAuth2User processOAuth2User(
        OAuth2UserRequest oAuth2UserRequest,
        OAuth2User oAuth2User) {
        // Process OAuth2 user information
        return CustomUserPrincipal.create(user, oAuth2User.getAttributes());
    }
}

7. Spring Boot Caching

Cache Configuration

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(
            new ConcurrentMapCache("users"),
            new ConcurrentMapCache("roles")
        ));
        return cacheManager;
    }
}

Using Cache

@Service
public class UserService {

    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        // This will be cached
        return userRepository.findById(id).orElse(null);
    }

    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        return userRepository.save(user);
    }

    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

8. Spring Boot Messaging

RabbitMQ Configuration

@Configuration
public class RabbitConfig {

    @Bean
    public Queue queue() {
        return new Queue("messageQueue", false);
    }

    @Bean
    public TopicExchange exchange() {
        return new TopicExchange("messageExchange");
    }

    @Bean
    public Binding binding(Queue queue, TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("routing.key");
    }
}

Message Producer and Consumer

@Service
public class MessageProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMessage(String message) {
        rabbitTemplate.convertAndSend("messageExchange", "routing.key", message);
    }
}

@Service
public class MessageConsumer {

    @RabbitListener(queues = "messageQueue")
    public void receiveMessage(String message) {
        // Process message
        System.out.println("Received message: " + message);
    }
}

9. Spring Boot Microservices

Service Discovery với Eureka

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

@SpringBootApplication
@EnableEurekaClient
public class ServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceApplication.class, args);
    }
}

Circuit Breaker với Resilience4j

@RestController
public class ServiceController {

    @CircuitBreaker(name = "backendA", fallbackMethod = "fallback")
    @GetMapping("/backend")
    public String backendA() {
        return restTemplate.getForObject("http://backend-a", String.class);
    }

    public String fallback(Exception e) {
        return "Fallback response";
    }
}

10. Best Practices

1. Configuration Management

  • Sử dụng application.yml thay vì application.properties
  • Sử dụng profiles cho các môi trường khác nhau
  • Externalize configuration

2. Exception Handling

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> handleResourceNotFoundException(
            ResourceNotFoundException ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(
            new Date(),
            ex.getMessage(),
            request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }
}

3. Logging

@Slf4j
@Service
public class UserService {

    public User createUser(User user) {
        log.info("Creating user: {}", user.getEmail());
        try {
            return userRepository.save(user);
        } catch (Exception e) {
            log.error("Error creating user: {}", e.getMessage());
            throw e;
        }
    }
}

4. Security Best Practices

  • Sử dụng HTTPS
  • Implement rate limiting
  • Validate input data
  • Sử dụng secure headers
  • Implement CORS policy