Spring Core

1. Основы Spring Framework

Что такое Spring Core

Spring Core — основа Spring Framework, предоставляющая:

  • IoC Container — управление зависимостями
  • Dependency Injection — внедрение зависимостей
  • AOP — аспектно-ориентированное программирование
  • Bean Management — управление жизненным циклом объектов

Maven зависимости

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.0.11</version>
</dependency>

2. IoC Container и Bean Management

ApplicationContext — основной контейнер

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // Конфигурация бинов
}

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        
        UserService userService = context.getBean(UserService.class);
        userService.performOperation();
        
        ((ConfigurableApplicationContext) context).close();
    }
}

Создание бинов

// Через @Component
@Component
@Service        // Для сервисного слоя
@Repository     // Для слоя данных
@Controller     // Для контроллеров
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

// Через @Bean
@Configuration
public class DatabaseConfig {
    
    @Bean
    @Primary  // Основной бин при нескольких кандидатах
    @Profile("production")  // Только в production
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
        return new HikariDataSource(config);
    }
}

3. Dependency Injection

Constructor Injection (рекомендуемый)

@Service
public class OrderService {
    private final UserService userService;
    private final PaymentService paymentService;
    
    // @Autowired не требуется для единственного конструктора
    public OrderService(UserService userService, PaymentService paymentService) {
        this.userService = userService;
        this.paymentService = paymentService;
    }
}

Разрешение конфликтов

// @Qualifier для выбора конкретного бина
@Service
public class UserService {
    public UserService(@Qualifier("primaryDataSource") DataSource dataSource) {
        // ...
    }
}

// @Primary для указания основного бина
@Service
@Primary
public class DatabaseUserService implements UserService {
    // Основная реализация
}

// @Conditional для условного создания
@Bean
@ConditionalOnProperty(name = "cache.enabled", havingValue = "true")
public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager();
}

4. Bean Scopes

Основные области видимости

@Component
@Scope("singleton")  // По умолчанию - один экземпляр
public class SingletonService { }

@Component
@Scope("prototype")  // Новый экземпляр при каждом запросе
public class PrototypeService { }

// Web scopes
@Component
@Scope("request")    // Один экземпляр на HTTP request
public class RequestScopedService { }

@Component
@Scope("session")    // Один экземпляр на HTTP session
public class SessionScopedService { }

Proxy для scoped бинов

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PrototypeService {
    private final String instanceId = UUID.randomUUID().toString();
    
    public String getInstanceId() {
        return instanceId;
    }
}

5. Жизненный цикл бинов

Методы инициализации и уничтожения

@Component
public class DatabaseService implements InitializingBean, DisposableBean {
    
    // 1. Конструктор
    public DatabaseService() {
        System.out.println("Constructor");
    }
    
    // 2. @PostConstruct
    @PostConstruct
    public void init() {
        System.out.println("@PostConstruct");
    }
    
    // 3. afterPropertiesSet
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterPropertiesSet");
    }
    
    // Уничтожение
    @PreDestroy
    public void cleanup() {
        System.out.println("@PreDestroy");
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean.destroy()");
    }
}

// Кастомные методы через @Bean
@Bean(initMethod = "customInit", destroyMethod = "customDestroy")
public DatabaseService databaseService() {
    return new DatabaseService();
}

BeanPostProcessor

@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
    
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        System.out.println("Before initialization: " + beanName);
        return bean;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("After initialization: " + beanName);
        return bean;
    }
}

6. Profiles и Properties

Использование профилей

@Configuration
@Profile("development")
public class DevConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }
}

@Configuration
@Profile("production")
public class ProdConfig {
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://prod-db:5432/myapp");
        return new HikariDataSource(config);
    }
}

// Активация профилей
public static void main(String[] args) {
    AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext();
    context.getEnvironment().setActiveProfiles("development");
    context.register(AppConfig.class);
    context.refresh();
}

Работа с Properties

@Configuration
@PropertySource("classpath:application.properties")
public class PropertyConfig {
    
    @Autowired
    private Environment env;
    
    @Bean
    public DatabaseConfig databaseConfig() {
        DatabaseConfig config = new DatabaseConfig();
        config.setUrl(env.getProperty("db.url"));
        config.setMaxConnections(env.getProperty("db.max-connections", Integer.class, 10));
        return config;
    }
}

// Внедрение через @Value
@Service
public class EmailService {
    
    @Value("${email.smtp.host}")
    private String smtpHost;
    
    @Value("${email.smtp.port:587}")  // Значение по умолчанию
    private int smtpPort;
    
    @Value("${email.recipients:admin@example.com,support@example.com}")
    private List<String> defaultRecipients;
}

// @ConfigurationProperties для типизированных свойств
@ConfigurationProperties(prefix = "app.database")
@Component
public class DatabaseProperties {
    private String url;
    private String username;
    private String password;
    private Pool pool = new Pool();
    
    public static class Pool {
        private int maxActive = 10;
        private int maxIdle = 5;
        // getters and setters
    }
    // getters and setters
}

7. Spring Expression Language (SpEL)

Основы SpEL

@Service
public class SpELExamples {
    
    // Литералы и операции
    @Value("#{42}")
    private int number;
    
    @Value("#{'Hello' + ' ' + 'World'}")
    private String text;
    
    @Value("#{100 > 50}")
    private boolean comparison;
    
    // Системные свойства
    @Value("#{systemProperties['java.version']}")
    private String javaVersion;
    
    @Value("#{environment['HOME']}")
    private String homeDirectory;
    
    // Обращение к бинам
    @Value("#{userService.getUserCount()}")
    private int userCount;
    
    @Value("#{@userService.findByEmail('admin@example.com')?.name}")
    private String adminName;  // Safe navigation
    
    // Коллекции
    @Value("#{{'red', 'green', 'blue'}}")
    private List<String> colors;
    
    @Value("#{{key1: 'value1', key2: 'value2'}}")
    private Map<String, String> map;
    
    // Фильтрация и проекция
    @Value("#{userService.getAllUsers().?[age > 18]}")
    private List<User> adults;
    
    @Value("#{userService.getAllUsers().![name]}")
    private List<String> userNames;
    
    // Условные выражения
    @Value("#{userService.getUserCount() > 0 ? 'Users exist' : 'No users'}")
    private String userStatus;
}

Программное использование SpEL

@Service
public class ExpressionService {
    
    private final SpelExpressionParser parser = new SpelExpressionParser();
    
    public void demonstrateSpEL() {
        // Простое выражение
        Expression exp = parser.parseExpression("'Hello World'");
        String message = (String) exp.getValue();
        
        // Выражение с контекстом
        User user = new User("John", "Doe", 30);
        Expression nameExp = parser.parseExpression("firstName + ' ' + lastName");
        String fullName = nameExp.getValue(user, String.class);
        
        // Создание контекста
        StandardEvaluationContext context = new StandardEvaluationContext(user);
        context.setVariable("greeting", "Hello");
        
        Expression complexExp = parser.parseExpression("#greeting + ' ' + firstName");
        String result = complexExp.getValue(context, String.class);
    }
}

8. События (Events)

Создание и публикация событий

// Кастомное событие
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; }
}

// Или без наследования
public class OrderCompletedEvent {
    private final Order order;
    private final Instant timestamp;
    
    public OrderCompletedEvent(Order order) {
        this.order = order;
        this.timestamp = Instant.now();
    }
    // getters
}

@Service
public class UserService {
    private final ApplicationEventPublisher eventPublisher;
    
    public UserService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    
    public User registerUser(UserRegistrationRequest request) {
        User user = createUser(request);
        
        // Публикация события
        eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
        
        return user;
    }
}

Обработка событий

@Component
public class UserEventListener {
    
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        User user = event.getUser();
        sendWelcomeEmail(user);
    }
    
    // Условная обработка
    @EventListener(condition = "#event.user.age > 18")
    public void handleAdultUserRegistered(UserRegisteredEvent event) {
        // Специальная обработка для совершеннолетних
    }
    
    // Асинхронная обработка
    @EventListener
    @Async
    public void handleUserRegisteredAsync(UserRegisteredEvent event) {
        generateUserAnalytics(event.getUser());
    }
    
    // Транзакционные события
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderCompleted(OrderCompletedEvent event) {
        notificationService.sendOrderConfirmation(event.getOrder());
    }
}

9. Aspect-Oriented Programming (AOP)

Основы AOP

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
    // Включение AOP поддержки
}

@Aspect
@Component
public class LoggingAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
    
    // Pointcut определения
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}
    
    @Pointcut("@annotation(com.example.annotation.Loggable)")
    public void loggableMethods() {}
    
    // Before advice
    @Before("serviceLayer()")
    public void logMethodEntry(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("Entering method: {}", methodName);
    }
    
    // After returning advice
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logMethodExit(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("Exiting method: {} with result: {}", methodName, result);
    }
    
    // Around advice
    @Around("loggableMethods()")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            long executionTime = System.currentTimeMillis() - start;
            logger.info("Method {} executed in {} ms", 
                       joinPoint.getSignature().getName(), executionTime);
            return result;
        } catch (Exception e) {
            logger.error("Exception in method {}: {}", 
                        joinPoint.getSignature().getName(), e.getMessage());
            throw e;
        }
    }
    
    // Exception handling
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "exception")
    public void logException(JoinPoint joinPoint, Exception exception) {
        logger.error("Exception in method {}: {}", 
                    joinPoint.getSignature().getName(), exception.getMessage());
    }
}

// Кастомная аннотация для логирования
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";
}

Практические примеры аспектов

Кэширование

@Aspect
@Component
public class CachingAspect {
    
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    
    @Around("@annotation(cacheable)")
    public Object cacheResult(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
        String key = generateKey(joinPoint);
        
        Object cached = cache.get(key);
        if (cached != null) {
            return cached;
        }
        
        Object result = joinPoint.proceed();
        cache.put(key, result);
        return result;
    }
    
    private String generateKey(ProceedingJoinPoint joinPoint) {
        return joinPoint.getSignature().toString() + 
               Arrays.toString(joinPoint.getArgs());
    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
    String value() default "";
}

Валидация

@Aspect
@Component
public class ValidationAspect {
    
    @Before("@annotation(validated)")
    public void validateParameters(JoinPoint joinPoint, Validated validated) {
        Object[] args = joinPoint.getArgs();
        
        for (Object arg : args) {
            if (arg == null) {
                throw new IllegalArgumentException("Parameter cannot be null");
            }
            
            if (arg instanceof String && ((String) arg).trim().isEmpty()) {
                throw new IllegalArgumentException("String parameter cannot be empty");
            }
        }
    }
}

10. Ресурсы и интернационализация

Работа с ресурсами

@Service
public class ResourceService {
    
    @Autowired
    private ResourceLoader resourceLoader;
    
    @Value("classpath:templates/email.html")
    private Resource emailTemplate;
    
    public void loadResources() throws IOException {
        // Различные типы ресурсов
        Resource classpathResource = resourceLoader.getResource("classpath:config.properties");
        Resource fileResource = resourceLoader.getResource("file:/path/to/file.txt");
        Resource urlResource = resourceLoader.getResource("https://example.com/data.json");
        
        if (classpathResource.exists()) {
            InputStream inputStream = classpathResource.getInputStream();
            // Работа с потоком
        }
    }
}

MessageSource для интернационализации

@Configuration
public class I18nConfig {
    
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setDefaultLocale(Locale.ENGLISH);
        return messageSource;
    }
}

@Service
public class MessageService {
    
    @Autowired
    private MessageSource messageSource;
    
    public String getMessage(String code, Locale locale, Object... args) {
        return messageSource.getMessage(code, args, locale);
    }
    
    public void sendNotification(User user, Locale locale) {
        String greeting = messageSource.getMessage("greeting", 
                                                 new Object[]{user.getName()}, 
                                                 locale);
        emailService.send(user.getEmail(), greeting);
    }
}

// messages.properties
// greeting=Hello, {0}!
// notification.welcome=Welcome to our application

// messages_ru.properties  
// greeting=Привет, {0}!
// notification.welcome=Добро пожаловать в наше приложение

11. Best Practices

Основные рекомендации

// ✅ Хорошие практики:

// 1. Используйте Constructor Injection
@Service
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {  // ✅
        this.userRepository = userRepository;
    }
}

// 2. Делайте поля final
private final UserRepository userRepository;  // ✅

// 3. Используйте @Component специализации
@Service        // Для бизнес-логики
@Repository     // Для доступа к данным  
@Controller     // Для веб-слоя

// 4. Группируйте конфигурацию по функциональности
@Configuration
public class DatabaseConfig {
    // Только конфигурация БД
}

@Configuration  
public class SecurityConfig {
    // Только конфигурация безопасности
}

// ❌ Избегайте:

// 1. Field Injection
@Autowired
private UserRepository userRepository;  // ❌

// 2. Циклических зависимостей
// A зависит от B, B зависит от A

// 3. Слишком много зависимостей в одном классе
// Признак нарушения Single Responsibility Principle

Тестирование Spring компонентов

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestConfig.class)
class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private UserRepository userRepository;
    
    @Test
    void shouldCreateUser() {
        // given
        User user = new User("John", "john@example.com");
        when(userRepository.save(any(User.class))).thenReturn(user);
        
        // when
        User result = userService.createUser("John", "john@example.com");
        
        // then
        assertThat(result.getName()).isEqualTo("John");
        verify(userRepository).save(any(User.class));
    }
}

@TestConfiguration
public class TestConfig {
    
    @Bean
    @Primary
    public UserRepository mockUserRepository() {
        return Mockito.mock(UserRepository.class);
    }
}

Конфигурация для разных сред

@Configuration
@Profile("!test")
public class ProductionConfig {
    
    @Bean
    public DataSource dataSource() {
        // Реальная БД
        return new HikariDataSource();
    }
}

@TestConfiguration
public class TestConfig {
    
    @Bean
    @Primary
    public DataSource dataSource() {
        // H2 для тестов
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }
}

Эта шпаргалка покрывает основные аспекты Spring Core и поможет в повседневной разработке приложений на Spring Framework.

Жизненный цикл Bean

1. Обзор жизненного цикла

Полная последовательность этапов

1. Instantiation          - Создание экземпляра через конструктор
2. Populate Properties    - Внедрение зависимостей (@Autowired)
3. BeanNameAware          - setBeanName()
4. BeanFactoryAware       - setBeanFactory()
5. ApplicationContextAware - setApplicationContext()
6. BeanPostProcessor      - postProcessBeforeInitialization()
7. @PostConstruct         - Аннотация инициализации
8. InitializingBean       - afterPropertiesSet()
9. init-method            - Кастомный метод инициализации
10. BeanPostProcessor     - postProcessAfterInitialization()
11. Bean готов к использованию
    ...
12. @PreDestroy           - Аннотация уничтожения
13. DisposableBean        - destroy()
14. destroy-method        - Кастомный метод уничтожения

Визуальная схема

Bean Definition → Constructor → Dependency Injection
                                        ↓
                                 Aware Interfaces
                                        ↓
                            BeanPostProcessor (Before)
                                        ↓
                              Initialization Phase
                                        ↓
                            BeanPostProcessor (After)
                                        ↓
                                   Bean Ready
                                        ↓
                                Bean in Use...
                                        ↓
                               Destruction Phase

2. Создание и внедрение зависимостей

Instantiation и Dependency Injection

@Component
public class UserService {
    
    private UserRepository userRepository;
    private EmailService emailService;
    
    // 1. Конструктор вызывается первым
    public UserService() {
        System.out.println("1. Constructor: UserService created");
    }
    
    // Constructor injection (рекомендуемый способ)
    public UserService(UserRepository userRepository) {
        System.out.println("1. Constructor with dependencies");
        this.userRepository = userRepository;
    }
    
    // 2. Setter injection (если используется)
    @Autowired
    public void setEmailService(EmailService emailService) {
        System.out.println("2. Setter injection: EmailService");
        this.emailService = emailService;
    }
    
    // Field injection
    @Autowired
    private NotificationService notificationService; // Внедряется через reflection
}

3. Aware Interfaces

Основные Aware интерфейсы

@Component("customUserService")
public class UserService implements 
    BeanNameAware, 
    BeanFactoryAware,
    ApplicationContextAware {
    
    private String beanName;
    private BeanFactory beanFactory;
    private ApplicationContext applicationContext;
    
    // 3. BeanNameAware - получение имени бина
    @Override
    public void setBeanName(String name) {
        System.out.println("3. BeanNameAware: " + name);
        this.beanName = name;
    }
    
    // 4. BeanFactoryAware - получение BeanFactory
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("4. BeanFactoryAware: BeanFactory set");
        this.beanFactory = beanFactory;
    }
    
    // 5. ApplicationContextAware - получение ApplicationContext
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) 
            throws BeansException {
        System.out.println("5. ApplicationContextAware: Context set");
        this.applicationContext = applicationContext;
    }
    
    // Использование полученных ссылок
    public void dynamicallyGetBean() {
        EmailService emailService = applicationContext.getBean(EmailService.class);
        boolean hasBean = beanFactory.containsBean("notificationService");
        
        // Публикация событий
        applicationContext.publishEvent(new UserCreatedEvent(this));
    }
}

Другие полезные Aware интерфейсы

@Component
public class ResourceAwareService implements 
    ResourceLoaderAware,
    MessageSourceAware,
    EnvironmentAware {
    
    private ResourceLoader resourceLoader;
    private MessageSource messageSource;
    private Environment environment;
    
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
    
    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
    
    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
    
    public void useInjectedResources() {
        // Загрузка ресурсов
        Resource config = resourceLoader.getResource("classpath:config.properties");
        
        // Интернационализация
        String message = messageSource.getMessage("greeting", null, Locale.ENGLISH);
        
        // Работа с окружением
        String profile = environment.getProperty("spring.profiles.active");
    }
}

4. BeanPostProcessor

Создание кастомного BeanPostProcessor

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
    
    // 6. Вызывается ДО инициализации
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) 
            throws BeansException {
        System.out.println("6. BeanPostProcessor BEFORE: " + beanName);
        
        // Можно модифицировать бин
        if (bean instanceof UserService) {
            System.out.println("   Processing UserService");
        }
        
        return bean;
    }
    
    // 10. Вызывается ПОСЛЕ инициализации
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) 
            throws BeansException {
        System.out.println("10. BeanPostProcessor AFTER: " + beanName);
        
        // Можно создать proxy
        if (bean instanceof UserService) {
            return createLoggingProxy(bean);
        }
        
        return bean;
    }
    
    private Object createLoggingProxy(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            (proxy, method, args) -> {
                System.out.println("Proxy: Calling " + method.getName());
                Object result = method.invoke(target, args);
                System.out.println("Proxy: Method completed");
                return result;
            }
        );
    }
}

5. Инициализация

Методы инициализации (порядок выполнения)

@Component
public class DatabaseService implements InitializingBean {
    
    private DataSource dataSource;
    private boolean initialized = false;
    
    @Autowired
    public DatabaseService(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    // 7. @PostConstruct - первый метод инициализации
    @PostConstruct
    public void init() {
        System.out.println("7. @PostConstruct: Initializing");
        
        // Инициализация, требующая внедренных зависимостей
        validateDependencies();
        setupConfiguration();
        
        System.out.println("   @PostConstruct completed");
    }
    
    // 8. InitializingBean - второй метод инициализации
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("8. InitializingBean: afterPropertiesSet");
        
        // Валидация конфигурации
        if (dataSource == null) {
            throw new Exception("DataSource is required");
        }
        
        // Тестирование соединения
        try (Connection conn = dataSource.getConnection()) {
            if (!conn.isValid(5)) {
                throw new Exception("Database connection invalid");
            }
        }
        
        initialized = true;
        System.out.println("   InitializingBean completed");
    }
    
    // 9. Custom init method - третий метод инициализации
    public void customInit() {
        System.out.println("9. Custom init method");
        
        // Финальная инициализация
        performFinalSetup();
        System.out.println("   Custom init completed");
    }
    
    private void validateDependencies() {
        if (dataSource == null) {
            throw new IllegalStateException("DataSource not injected");
        }
    }
    
    private void setupConfiguration() {
        System.out.println("   Setting up configuration");
    }
    
    private void performFinalSetup() {
        System.out.println("   Performing final setup");
    }
    
    public boolean isInitialized() {
        return initialized;
    }
}

// Конфигурация custom init method
@Configuration
public class AppConfig {
    
    @Bean(initMethod = "customInit")
    public DatabaseService databaseService(DataSource dataSource) {
        return new DatabaseService(dataSource);
    }
}

6. Полный пример жизненного цикла

Демонстрация всех этапов

@Component("lifecycleDemo")
public class LifecycleDemoService implements 
    BeanNameAware,
    ApplicationContextAware,
    InitializingBean,
    DisposableBean {
    
    private String beanName;
    private ApplicationContext applicationContext;
    private DependencyService dependencyService;
    
    // 1. Constructor
    public LifecycleDemoService() {
        System.out.println("1. Constructor called");
    }
    
    // 2. Dependency Injection
    @Autowired
    public void setDependencyService(DependencyService dependencyService) {
        System.out.println("2. Dependency injection");
        this.dependencyService = dependencyService;
    }
    
    // 3. BeanNameAware
    @Override
    public void setBeanName(String name) {
        System.out.println("3. BeanNameAware: " + name);
        this.beanName = name;
    }
    
    // 5. ApplicationContextAware
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        System.out.println("5. ApplicationContextAware");
        this.applicationContext = applicationContext;
    }
    
    // 7. @PostConstruct
    @PostConstruct
    public void postConstruct() {
        System.out.println("7. @PostConstruct");
    }
    
    // 8. InitializingBean
    @Override
    public void afterPropertiesSet() {
        System.out.println("8. InitializingBean: afterPropertiesSet");
    }
    
    // 9. Custom init method
    public void customInit() {
        System.out.println("9. Custom init method");
    }
    
    // Business method - бин готов к использованию
    public String businessMethod() {
        return "Bean is ready! Name: " + beanName;
    }
    
    // Destruction phase
    
    // 12. @PreDestroy
    @PreDestroy
    public void preDestroy() {
        System.out.println("12. @PreDestroy");
    }
    
    // 13. DisposableBean
    @Override
    public void destroy() {
        System.out.println("13. DisposableBean: destroy");
    }
    
    // 14. Custom destroy method
    public void customDestroy() {
        System.out.println("14. Custom destroy method");
    }
}

7. Уничтожение бинов

Методы уничтожения (порядок выполнения)

@Component
public class ResourceManager implements DisposableBean {
    
    private ExecutorService executorService;
    private Connection connection;
    private boolean isShutdown = false;
    
    @PostConstruct
    public void init() {
        executorService = Executors.newFixedThreadPool(5);
        connection = createConnection();
    }
    
    // 12. @PreDestroy - первый метод уничтожения
    @PreDestroy
    public void cleanup() {
        System.out.println("12. @PreDestroy: Starting cleanup");
        
        if (!isShutdown) {
            shutdownExecutorService();
            closeConnection();
            isShutdown = true;
        }
    }
    
    // 13. DisposableBean - второй метод уничтожения
    @Override
    public void destroy() throws Exception {
        System.out.println("13. DisposableBean: destroy");
        
        // Дополнительная очистка
        releaseResources();
    }
    
    // 14. Custom destroy method - третий метод уничтожения
    public void customDestroy() {
        System.out.println("14. Custom destroy method");
        
        // Финальная очистка
        finalCleanup();
    }
    
    private void shutdownExecutorService() {
        if (executorService != null && !executorService.isShutdown()) {
            System.out.println("   Shutting down executor");
            executorService.shutdown();
            try {
                if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                    executorService.shutdownNow();
                }
            } catch (InterruptedException e) {
                executorService.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }
    
    private void closeConnection() {
        if (connection != null) {
            try {
                System.out.println("   Closing connection");
                connection.close();
            } catch (SQLException e) {
                System.err.println("Error closing connection: " + e.getMessage());
            }
        }
    }
    
    private void releaseResources() {
        System.out.println("   Releasing resources");
    }
    
    private void finalCleanup() {
        System.out.println("   Final cleanup");
    }
}

8. Практические примеры

Управление ресурсами

@Component
public class CacheManager implements InitializingBean, DisposableBean {
    
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    private ScheduledExecutorService cleanupTask;
    private final AtomicBoolean initialized = new AtomicBoolean(false);
    
    @Value("${cache.cleanup.interval:3600}")
    private int cleanupInterval;
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("Initializing CacheManager");
        
        // Запуск фоновой задачи очистки
        cleanupTask = Executors.newScheduledThreadPool(1);
        cleanupTask.scheduleAtFixedRate(
            this::cleanupExpiredEntries,
            cleanupInterval,
            cleanupInterval,
            TimeUnit.SECONDS
        );
        
        initialized.set(true);
        System.out.println("CacheManager initialized");
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("Destroying CacheManager");
        
        // Остановка фоновых задач
        if (cleanupTask != null) {
            cleanupTask.shutdown();
            try {
                if (!cleanupTask.awaitTermination(10, TimeUnit.SECONDS)) {
                    cleanupTask.shutdownNow();
                }
            } catch (InterruptedException e) {
                cleanupTask.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
        
        // Очистка кэша
        cache.clear();
        initialized.set(false);
        
        System.out.println("CacheManager destroyed");
    }
    
    private void cleanupExpiredEntries() {
        if (initialized.get()) {
            System.out.println("Cleaning up expired cache entries");
            // Логика очистки просроченных записей
        }
    }
    
    public void put(String key, Object value) {
        if (!initialized.get()) {
            throw new IllegalStateException("CacheManager not initialized");
        }
        cache.put(key, value);
    }
    
    public Object get(String key) {
        return cache.get(key);
    }
}

Обработка ошибок инициализации

@Component
public class ExternalServiceClient implements InitializingBean {
    
    private ExternalService client;
    private boolean initialized = false;
    private final List<String> errors = new ArrayList<>();
    
    @PostConstruct
    public void init() {
        try {
            System.out.println("Initializing external service client");
            client = new ExternalService();
            
        } catch (Exception e) {
            errors.add("Failed to create client: " + e.getMessage());
            System.err.println("Client creation failed: " + e.getMessage());
        }
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        if (client == null) {
            throw new Exception("External service client not initialized");
        }
        
        try {
            // Тестирование соединения
            client.testConnection();
            initialized = true;
            System.out.println("External service client ready");
            
        } catch (Exception e) {
            errors.add("Connection test failed: " + e.getMessage());
            throw new Exception("Failed to connect to external service", e);
        }
    }
    
    public boolean isReady() {
        return initialized && errors.isEmpty();
    }
    
    public List<String> getErrors() {
        return new ArrayList<>(errors);
    }
}

9. Best Practices

Рекомендации по жизненному циклу

// ✅ Хорошие практики:

// 1. Используйте @PostConstruct для инициализации
@PostConstruct
public void init() {
    // Инициализация после внедрения зависимостей
}

// 2. Используйте @PreDestroy для очистки ресурсов
@PreDestroy
public void cleanup() {
    // Graceful shutdown ресурсов
}

// 3. Проверяйте состояние в бизнес-методах
public void businessMethod() {
    if (!initialized) {
        throw new IllegalStateException("Service not initialized");
    }
    // бизнес-логика
}

// 4. Обрабатывайте ошибки инициализации
@PostConstruct
public void init() {
    try {
        // инициализация
    } catch (Exception e) {
        logger.error("Initialization failed", e);
        // Сохранить ошибку для диагностики
    }
}

// ❌ Избегайте:

// 1. Тяжелых операций в конструкторе
public Service() {
    // ❌ Не делайте сетевые вызовы здесь
    // ❌ Не работайте с файлами
}

// 2. Обращения к зависимостям в конструкторе
public Service(Dependency dep) {
    // ❌ dep может быть не полностью инициализирован
    dep.heavyOperation();
}

// 3. Игнорирования ошибок уничтожения
@PreDestroy
public void destroy() {
    try {
        resource.close();
    } catch (Exception e) {
        // ❌ Не игнорируйте ошибки
        logger.error("Cleanup failed", e);
    }
}

Тестирование жизненного цикла

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestConfig.class)
class LifecycleTest {
    
    @Autowired
    private LifecycleDemoService service;
    
    @Autowired
    private ApplicationContext context;
    
    @Test
    void testInitialization() {
        // Проверяем инициализацию
        assertThat(service.isInitialized()).isTrue();
        assertThat(service.businessMethod()).isNotNull();
    }
    
    @Test
    void testDestruction() {
        ConfigurableApplicationContext configurableContext = 
            (ConfigurableApplicationContext) context;
        
        // Закрытие контекста вызовет методы уничтожения
        configurableContext.close();
        
        // Проверяем состояние после уничтожения
        assertThat(service.isDestroyed()).isTrue();
    }
}

Эта шпаргалка покрывает полный жизненный цикл Spring Bean с практическими примерами использования каждого этапа.

Spring AOP

1. Основы AOP

Что такое AOP

Aspect-Oriented Programming (AOP) — парадигма программирования для решения сквозных задач (cross-cutting concerns):

  • Логирование - логи методов, параметров, результатов
  • Безопасность - проверка прав доступа
  • Транзакции - управление транзакциями
  • Кэширование - кэширование результатов методов
  • Мониторинг - измерение производительности

Основные концепции AOP

Aspect     - Модуль сквозной функциональности
Advice     - Код, выполняемый в определенной точке
Pointcut   - Определение точек применения advice
Join Point - Точка выполнения программы (вызов метода)
Weaving    - Процесс применения aspects к target объектам

Maven зависимости

<dependencies>
    <!-- Spring AOP -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>6.0.11</version>
    </dependency>
    
    <!-- AspectJ -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.19</version>
    </dependency>
</dependencies>

Включение AOP

@Configuration
@EnableAspectJAutoProxy  // Включение AOP поддержки
public class AopConfig {
    // Конфигурация
}

// Или через XML
// <aop:aspectj-autoproxy />

2. Pointcut Expressions

Синтаксис Pointcut

@Aspect
@Component
public class PointcutExamples {
    
    // Execution - самый популярный designator
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}
    
    // Модификаторы доступа
    @Pointcut("execution(public * com.example.service.*.*(..))")
    public void publicServiceMethods() {}
    
    // Возвращаемые типы
    @Pointcut("execution(String com.example.service.*.*(..))")
    public void stringReturnMethods() {}
    
    @Pointcut("execution(void com.example.service.*.*(..))")
    public void voidMethods() {}
    
    // Параметры
    @Pointcut("execution(* com.example.service.*.*(String))")
    public void stringParameterMethods() {}
    
    @Pointcut("execution(* com.example.service.*.*(String, int))")
    public void stringIntParameterMethods() {}
    
    @Pointcut("execution(* com.example.service.*.*(.., String))")
    public void lastParamStringMethods() {}
    
    // Within - по типу/пакету
    @Pointcut("within(com.example.service.*)")
    public void withinServicePackage() {}
    
    @Pointcut("within(com.example.service.UserService)")
    public void withinUserService() {}
    
    // Аннотации
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalMethods() {}
    
    @Pointcut("@within(org.springframework.stereotype.Service)")
    public void serviceClasses() {}
    
    // Bean names
    @Pointcut("bean(*Service)")
    public void serviceBeans() {}
    
    @Pointcut("bean(userService)")
    public void userServiceBean() {}
    
    // Комбинирование pointcut'ов
    @Pointcut("serviceLayer() && !bean(excludedService)")
    public void serviceLayerExceptExcluded() {}
    
    @Pointcut("execution(* com.example..*(.)) && @annotation(Loggable)")
    public void loggableMethodsInPackage() {}
}

Сложные Pointcut выражения

@Aspect
@Component
public class AdvancedPointcuts {
    
    // Методы с определенными аннотациями
    @Pointcut("@annotation(com.example.annotation.Cacheable)")
    public void cacheableMethods() {}
    
    // Классы с аннотациями
    @Pointcut("@target(org.springframework.stereotype.Repository)")
    public void repositoryClasses() {}
    
    // Аргументы с аннотациями
    @Pointcut("@args(com.example.annotation.Valid)")
    public void validatedArguments() {}
    
    // This и target
    @Pointcut("this(com.example.service.UserService)")
    public void userServiceProxy() {}
    
    @Pointcut("target(com.example.service.UserService)")
    public void userServiceTarget() {}
    
    // Args для получения аргументов
    @Pointcut("args(userId, ..)")
    public void methodsWithUserIdFirstParam(Long userId) {}
    
    // Комплексные условия
    @Pointcut("execution(* com.example.service.*.find*(..)) && args(id, ..)")
    public void findMethodsWithId(Object id) {}
    
    @Pointcut("(execution(* com.example.service.*.save(..)) || " +
              "execution(* com.example.service.*.update(..))) && " +
              "!@annotation(com.example.annotation.NoAudit)")
    public void auditableMethods() {}
}

3. Типы Advice

Before Advice

@Aspect
@Component
public class BeforeAdviceExample {
    
    private static final Logger logger = LoggerFactory.getLogger(BeforeAdviceExample.class);
    
    @Before("execution(* com.example.service.*.*(..))")
    public void logMethodEntry(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        Object[] args = joinPoint.getArgs();
        
        logger.info("Entering method: {}.{} with args: {}", 
                   className, methodName, Arrays.toString(args));
    }
    
    @Before("@annotation(com.example.annotation.RequireAuth)")
    public void checkAuthorization(JoinPoint joinPoint) {
        // Проверка авторизации перед выполнением метода
        String currentUser = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        
        if (currentUser == null || "anonymous".equals(currentUser)) {
            throw new SecurityException("Authorization required");
        }
        
        logger.info("User {} accessing method {}", 
                   currentUser, joinPoint.getSignature().getName());
    }
    
    // Получение аргументов через pointcut
    @Before("execution(* com.example.service.UserService.*(..)) && args(userId, ..)")
    public void validateUserId(Long userId) {
        if (userId == null || userId <= 0) {
            throw new IllegalArgumentException("Invalid userId: " + userId);
        }
    }
}

After Returning Advice

@Aspect
@Component
public class AfterReturningAdviceExample {
    
    private static final Logger logger = LoggerFactory.getLogger(AfterReturningAdviceExample.class);
    
    @AfterReturning(
        pointcut = "execution(* com.example.service.*.*(..))",
        returning = "result"
    )
    public void logMethodResult(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("Method {} returned: {}", methodName, result);
    }
    
    @AfterReturning(
        pointcut = "execution(* com.example.service.UserService.create*(..))",
        returning = "user"
    )
    public void userCreated(JoinPoint joinPoint, User user) {
        // Отправка уведомления о создании пользователя
        notificationService.sendWelcomeEmail(user);
        auditService.logUserCreation(user);
        
        logger.info("User created: {}", user.getEmail());
    }
    
    // Фильтрация по типу возвращаемого значения
    @AfterReturning(
        pointcut = "execution(List<*> com.example.service.*.*(..))",
        returning = "resultList"
    )
    public void logListSize(JoinPoint joinPoint, List<?> resultList) {
        logger.info("Method {} returned list with {} elements", 
                   joinPoint.getSignature().getName(), 
                   resultList != null ? resultList.size() : 0);
    }
}

After Throwing Advice

@Aspect
@Component
public class AfterThrowingAdviceExample {
    
    private static final Logger logger = LoggerFactory.getLogger(AfterThrowingAdviceExample.class);
    
    @AfterThrowing(
        pointcut = "execution(* com.example.service.*.*(..))",
        throwing = "exception"
    )
    public void logException(JoinPoint joinPoint, Exception exception) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        logger.error("Exception in method {}.{} with args {}: {}", 
                    joinPoint.getTarget().getClass().getSimpleName(),
                    methodName, 
                    Arrays.toString(args),
                    exception.getMessage());
    }
    
    @AfterThrowing(
        pointcut = "execution(* com.example.service.UserService.*(..))",
        throwing = "ex"
    )
    public void handleUserServiceException(JoinPoint joinPoint, DataAccessException ex) {
        // Специальная обработка для исключений доступа к данным
        String operation = joinPoint.getSignature().getName();
        alertService.sendAlert("Database error in UserService." + operation, ex);
        
        // Можно добавить retry механизм или fallback
        if (operation.startsWith("find")) {
            cacheService.markAsStale("users");
        }
    }
    
    // Различные типы исключений
    @AfterThrowing(
        pointcut = "execution(* com.example.service.*.*(..))",
        throwing = "ex"
    )
    public void handleBusinessException(JoinPoint joinPoint, BusinessException ex) {
        logger.warn("Business exception in {}: {}", 
                   joinPoint.getSignature().getName(), ex.getMessage());
        
        // Отправка уведомления бизнес-команде
        businessNotificationService.notifyException(ex);
    }
}

After (Finally) Advice

@Aspect
@Component
public class AfterAdviceExample {
    
    private static final Logger logger = LoggerFactory.getLogger(AfterAdviceExample.class);
    
    @After("execution(* com.example.service.*.*(..))")
    public void logMethodCompletion(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("Method {} completed (success or failure)", methodName);
    }
    
    @After("@annotation(com.example.annotation.CleanupRequired)")
    public void performCleanup(JoinPoint joinPoint) {
        // Очистка ресурсов независимо от результата
        String methodName = joinPoint.getSignature().getName();
        logger.debug("Performing cleanup after method: {}", methodName);
        
        // Очистка temporary файлов, кэшей и т.д.
        cleanupService.cleanup();
    }
    
    @After("execution(* com.example.service.FileService.*(..))")
    public void releaseFileResources(JoinPoint joinPoint) {
        // Освобождение файловых ресурсов
        fileResourceManager.releaseResources();
    }
}

Around Advice

@Aspect
@Component
public class AroundAdviceExample {
    
    private static final Logger logger = LoggerFactory.getLogger(AroundAdviceExample.class);
    
    @Around("@annotation(com.example.annotation.Timed)")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        
        try {
            logger.debug("Starting execution of method: {}", methodName);
            Object result = joinPoint.proceed(); // Вызов оригинального метода
            
            long executionTime = System.currentTimeMillis() - startTime;
            logger.info("Method {} executed in {} ms", methodName, executionTime);
            
            return result;
            
        } catch (Exception e) {
            long executionTime = System.currentTimeMillis() - startTime;
            logger.error("Method {} failed after {} ms: {}", 
                        methodName, executionTime, e.getMessage());
            throw e;
        }
    }
    
    @Around("@annotation(com.example.annotation.Cacheable)")
    public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable {
        String key = generateCacheKey(joinPoint);
        
        // Проверка кэша
        Object cached = cacheService.get(key);
        if (cached != null) {
            logger.debug("Cache hit for key: {}", key);
            return cached;
        }
        
        // Выполнение метода
        logger.debug("Cache miss for key: {}, executing method", key);
        Object result = joinPoint.proceed();
        
        // Сохранение в кэш
        cacheService.put(key, result);
        return result;
    }
    
    @Around("@annotation(com.example.annotation.Retry)")
    public Object retryOnFailure(ProceedingJoinPoint joinPoint) throws Throwable {
        int maxAttempts = 3;
        int attempt = 0;
        Exception lastException = null;
        
        while (attempt < maxAttempts) {
            try {
                attempt++;
                logger.debug("Attempt {} of method {}", 
                           attempt, joinPoint.getSignature().getName());
                
                return joinPoint.proceed();
                
            } catch (Exception e) {
                lastException = e;
                
                if (attempt >= maxAttempts) {
                    logger.error("Method {} failed after {} attempts", 
                                joinPoint.getSignature().getName(), maxAttempts);
                    throw e;
                }
                
                logger.warn("Method {} failed on attempt {}, retrying...", 
                           joinPoint.getSignature().getName(), attempt);
                
                // Exponential backoff
                Thread.sleep(1000 * attempt);
            }
        }
        
        throw lastException;
    }
    
    // Модификация аргументов
    @Around("execution(* com.example.service.UserService.create(..))")
    public Object preprocessUserCreation(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        
        // Модификация аргументов
        if (args.length > 0 && args[0] instanceof User) {
            User user = (User) args[0];
            
            // Валидация и нормализация данных
            user.setEmail(user.getEmail().toLowerCase().trim());
            if (user.getCreatedAt() == null) {
                user.setCreatedAt(LocalDateTime.now());
            }
            
            logger.debug("Preprocessed user data: {}", user.getEmail());
        }
        
        return joinPoint.proceed(args);
    }
    
    private String generateCacheKey(ProceedingJoinPoint joinPoint) {
        StringBuilder keyBuilder = new StringBuilder();
        keyBuilder.append(joinPoint.getTarget().getClass().getSimpleName());
        keyBuilder.append(".");
        keyBuilder.append(joinPoint.getSignature().getName());
        
        for (Object arg : joinPoint.getArgs()) {
            keyBuilder.append(":").append(arg != null ? arg.toString() : "null");
        }
        
        return keyBuilder.toString();
    }
}

4. Практические примеры аспектов

Аудит и логирование

@Aspect
@Component
public class AuditAspect {
    
    private final AuditService auditService;
    
    public AuditAspect(AuditService auditService) {
        this.auditService = auditService;
    }
    
    @AfterReturning(
        pointcut = "@annotation(auditable)",
        returning = "result"
    )
    public void auditOperation(JoinPoint joinPoint, Auditable auditable, Object result) {
        String currentUser = getCurrentUser();
        String operation = auditable.operation();
        String entity = auditable.entity();
        
        AuditEntry entry = AuditEntry.builder()
            .user(currentUser)
            .operation(operation)
            .entity(entity)
            .timestamp(LocalDateTime.now())
            .details(buildDetails(joinPoint, result))
            .build();
        
        auditService.save(entry);
    }
    
    @Around("@annotation(com.example.annotation.LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        
        try {
            Object result = joinPoint.proceed();
            stopWatch.stop();
            
            logger.info("Method {} executed in {} ms", 
                       joinPoint.getSignature().getName(),
                       stopWatch.getTotalTimeMillis());
            
            return result;
        } catch (Exception e) {
            stopWatch.stop();
            logger.error("Method {} failed after {} ms", 
                        joinPoint.getSignature().getName(),
                        stopWatch.getTotalTimeMillis());
            throw e;
        }
    }
    
    private String getCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth != null ? auth.getName() : "system";
    }
    
    private String buildDetails(JoinPoint joinPoint, Object result) {
        return String.format("Method: %s, Args: %s, Result: %s",
                           joinPoint.getSignature().getName(),
                           Arrays.toString(joinPoint.getArgs()),
                           result);
    }
}

// Кастомная аннотация для аудита
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable {
    String operation();
    String entity();
}

// Использование
@Service
public class UserService {
    
    @Auditable(operation = "CREATE", entity = "USER")
    public User createUser(User user) {
        return userRepository.save(user);
    }
    
    @Auditable(operation = "DELETE", entity = "USER")
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }
}

Валидация и безопасность

@Aspect
@Component
public class SecurityAspect {
    
    private final AuthorizationService authService;
    
    public SecurityAspect(AuthorizationService authService) {
        this.authService = authService;
    }
    
    @Before("@annotation(secured)")
    public void checkPermission(JoinPoint joinPoint, Secured secured) {
        String currentUser = getCurrentUser();
        String[] requiredRoles = secured.roles();
        
        if (!authService.hasAnyRole(currentUser, requiredRoles)) {
            throw new AccessDeniedException(
                String.format("User %s does not have required roles: %s", 
                             currentUser, Arrays.toString(requiredRoles)));
        }
    }
    
    @Around("@annotation(rateLimit)")
    public Object enforceRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) 
            throws Throwable {
        String key = generateRateLimitKey(joinPoint);
        int limit = rateLimit.value();
        TimeUnit timeUnit = rateLimit.timeUnit();
        
        if (!rateLimitService.isAllowed(key, limit, timeUnit)) {
            throw new RateLimitExceededException(
                String.format("Rate limit exceeded: %d per %s", limit, timeUnit));
        }
        
        return joinPoint.proceed();
    }
    
    @Before("execution(* com.example.service.*.*(..)) && args(input, ..)")
    public void validateInput(Object input) {
        if (input == null) {
            throw new IllegalArgumentException("Input cannot be null");
        }
        
        // Дополнительная валидация
        if (input instanceof String && ((String) input).trim().isEmpty()) {
            throw new IllegalArgumentException("Input cannot be empty");
        }
    }
    
    private String getCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth != null ? auth.getName() : "anonymous";
    }
    
    private String generateRateLimitKey(ProceedingJoinPoint joinPoint) {
        String user = getCurrentUser();
        String method = joinPoint.getSignature().getName();
        return user + ":" + method;
    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Secured {
    String[] roles();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int value();
    TimeUnit timeUnit() default TimeUnit.MINUTES;
}

5. Best Practices

Рекомендации по использованию AOP

// ✅ Хорошие практики:

// 1. Используйте четкие имена для pointcut'ов
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}  // ✅ Понятное имя

// 2. Группируйте связанные pointcut'ы в отдельном классе
@Component
public class CommonPointcuts {
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}
    
    @Pointcut("execution(* com.example.repository.*.*(..))")
    public void repositoryLayer() {}
    
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalMethods() {}
}

// 3. Обрабатывайте исключения в advice
@Around("serviceLayer()")
public Object handleServiceCalls(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
        return joinPoint.proceed();
    } catch (Exception e) {
        logger.error("Service call failed", e);
        throw e; // Не забывайте пробрасывать исключения
    }
}

// 4. Используйте аннотации для гибкости
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
    String value() default "";
}

// ❌ Избегайте:

// 1. Слишком широких pointcut'ов
@Before("execution(* *.*(..))")  // ❌ Влияет на ВСЕ методы

// 2. Тяжелых операций в advice
@Before("serviceLayer()")
public void heavyLogging(JoinPoint joinPoint) {
    // ❌ Тяжелая операция в каждом вызове
    String expensiveLog = buildDetailedLog(joinPoint);
    externalLogService.send(expensiveLog);
}

// 3. Изменения состояния в неподходящих advice
@Before("serviceLayer()")
public void modifyState(JoinPoint joinPoint) {
    // ❌ Before advice не должен изменять состояние
    someService.updateCounter();
}

Производительность и отладка

@Aspect
@Component
public class PerformanceMonitoringAspect {
    
    private final MeterRegistry meterRegistry;
    
    @Around("@annotation(com.example.annotation.MonitorPerformance)")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        
        return Timer.Sample.start(meterRegistry)
            .stop(Timer.builder("method.execution.time")
                .tag("method", methodName)
                .register(meterRegistry))
            .recordCallable(() -> {
                try {
                    return joinPoint.proceed();
                } catch (Throwable throwable) {
                    meterRegistry.counter("method.execution.error", 
                                        "method", methodName).increment();
                    throw throwable;
                }
            });
    }
    
    // Условное включение логирования
    @Around("@annotation(com.example.annotation.DebugLog)")
    public Object debugLog(ProceedingJoinPoint joinPoint) throws Throwable {
        if (!logger.isDebugEnabled()) {
            return joinPoint.proceed();
        }
        
        logger.debug("Entering method: {} with args: {}", 
                    joinPoint.getSignature().getName(),
                    Arrays.toString(joinPoint.getArgs()));
        
        try {
            Object result = joinPoint.proceed();
            logger.debug("Method completed with result: {}", result);
            return result;
        } catch (Exception e) {
            logger.debug("Method failed with exception: {}", e.getMessage());
            throw e;
        }
    }
}

Тестирование аспектов

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AopConfig.class, TestConfig.class})
@EnableAspectJAutoProxy
class AopTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private AuditService auditService;
    
    @Test
    void testAuditAspect() {
        // given
        User user = new User("John", "john@example.com");
        
        // when
        userService.createUser(user);
        
        // then
        verify(auditService).save(any(AuditEntry.class));
    }
    
    @Test
    void testExceptionHandling() {
        // given
        when(userRepository.save(any())).thenThrow(new DataAccessException("DB Error"));
        
        // when & then
        assertThatThrownBy(() -> userService.createUser(new User()))
            .isInstanceOf(DataAccessException.class);
        
        // Проверяем, что aspect обработал исключение
        verify(alertService).sendAlert(anyString(), any(Exception.class));
    }
}

Эта шпаргалка покрывает основные аспекты Spring AOP с практическими примерами использования.

@Controller vs @RestController

Основные различия

@Controller — классическая аннотация Spring MVC для создания веб-контроллеров, которые могут возвращать как views (JSP, Thymeleaf), так и данные в формате JSON/XML. Требует дополнительной аннотации @ResponseBody для возврата JSON.

@RestController — специализированная аннотация, появившаяся в Spring 4.0, которая объединяет @Controller и @ResponseBody. Предназначена специально для REST API и всегда возвращает данные в теле ответа.

// @Controller - классический подход
@Controller
public class ClassicController {
    
    // Возврат view (HTML страница)
    @GetMapping("/users")
    public String userPage(Model model) {
        model.addAttribute("users", userService.getAllUsers());
        return "users"; // возвращает имя view (users.jsp/users.html)
    }
    
    // Возврат JSON - требуется @ResponseBody
    @GetMapping("/api/users")
    @ResponseBody // Без этой аннотации Spring попытается найти view "users"
    public List<User> getUsers() {
        return userService.getAllUsers(); // Сериализация в JSON
    }
    
    // Смешанный подход - разные методы возвращают разные типы
    @PostMapping("/users")
    public String createUserForm(@ModelAttribute User user) {
        userService.save(user);
        return "redirect:/users"; // Редирект на страницу
    }
    
    @PostMapping("/api/users")
    @ResponseBody
    public ResponseEntity<User> createUserApi(@RequestBody User user) {
        User saved = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(saved);
    }
}

// @RestController - современный REST API подход
@RestController
@RequestMapping("/api")
public class RestApiController {
    
    // Все методы автоматически возвращают JSON
    @GetMapping("/users")
    public List<User> getUsers() {
        return userService.getAllUsers(); // Автоматическая сериализация в JSON
    }
    
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User saved = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(saved);
    }
    
    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .map(user -> ResponseEntity.ok(user))
            .orElse(ResponseEntity.notFound().build());
    }
}

Внутренняя реализация

Как работает @Controller

ViewResolver — компонент Spring MVC, который преобразует логическое имя view (строку) в реальный view объект (JSP, Thymeleaf template, etc.).

Model — контейнер для передачи данных между контроллером и view. Данные добавляются в model и становятся доступными в шаблоне.

@Controller
public class WebController {
    
    @Autowired
    private ViewResolver viewResolver; // InternalResourceViewResolver, ThymeleafViewResolver
    
    @GetMapping("/dashboard")
    public String dashboard(Model model, HttpServletRequest request) {
        // Добавление данных в модель
        model.addAttribute("currentUser", getCurrentUser());
        model.addAttribute("notifications", getNotifications());
        model.addAttribute("pageTitle", "Dashboard");
        
        // Spring MVC процесс:
        // 1. Возврат "dashboard" (логическое имя view)
        // 2. ViewResolver находит соответствующий template
        // 3. Данные из Model передаются в template
        // 4. Template рендерится в HTML
        // 5. HTML отправляется клиенту
        
        return "dashboard"; // /WEB-INF/views/dashboard.jsp или templates/dashboard.html
    }
    
    @GetMapping("/profile")
    public ModelAndView profile(@RequestParam Long userId) {
        // Альтернативный способ возврата view с данными
        ModelAndView mav = new ModelAndView();
        mav.setViewName("profile");
        mav.addObject("user", userService.findById(userId));
        mav.addObject("editMode", false);
        
        return mav;
    }
    
    // Обработка форм
    @PostMapping("/profile/update")
    public String updateProfile(@ModelAttribute User user, 
                              RedirectAttributes redirectAttributes) {
        try {
            userService.update(user);
            redirectAttributes.addFlashAttribute("message", "Profile updated successfully");
            return "redirect:/profile?userId=" + user.getId();
        } catch (ValidationException e) {
            redirectAttributes.addFlashAttribute("error", e.getMessage());
            return "redirect:/profile?userId=" + user.getId() + "&error=true";
        }
    }
}

Как работает @RestController

HttpMessageConverter — компонент, который автоматически преобразует Java объекты в JSON/XML и обратно. MappingJackson2HttpMessageConverter используется для JSON сериализации.

@ResponseBody — неявно применяется ко всем методам @RestController, указывает Spring MVC, что возвращаемое значение должно быть записано в HTTP response body.

@RestController
@RequestMapping("/api/v1")
public class UserRestController {
    
    @Autowired
    private ObjectMapper objectMapper; // Jackson для JSON сериализации
    
    @GetMapping("/users/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
        // Spring MVC процесс для @RestController:
        // 1. Метод возвращает UserDto объект
        // 2. @ResponseBody (неявно) указывает записать в response body
        // 3. HttpMessageConverter (Jackson) сериализует объект в JSON
        // 4. JSON отправляется клиенту с Content-Type: application/json
        
        return userService.findById(id)
            .map(user -> ResponseEntity.ok(convertToDto(user)))
            .orElse(ResponseEntity.notFound().build());
    }
    
    @PostMapping("/users")
    public ResponseEntity<UserDto> createUser(@RequestBody CreateUserRequest request) {
        // @RequestBody автоматически десериализует JSON в Java объект
        // HttpMessageConverter работает в обе стороны
        
        try {
            User user = userService.create(request);
            UserDto dto = convertToDto(user);
            
            URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(user.getId())
                .toUri();
            
            return ResponseEntity.created(location).body(dto);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().build();
        }
    }
    
    // Кастомная сериализация
    @GetMapping("/users/{id}/export")
    public ResponseEntity<String> exportUser(@PathVariable Long id) throws Exception {
        User user = userService.findById(id).orElseThrow();
        
        // Ручная сериализация с кастомными настройками
        ObjectWriter writer = objectMapper.writer()
            .withDefaultPrettyPrinter()
            .withoutFeatures(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        
        String json = writer.writeValueAsString(user);
        
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=user.json")
            .contentType(MediaType.APPLICATION_JSON)
            .body(json);
    }
}

Content Negotiation

Поддержка множественных форматов

Content Negotiation — механизм Spring MVC для автоматического выбора формата ответа (JSON, XML, HTML) на основе HTTP заголовков Accept или параметров запроса.

@Controller // Поддержка и view, и REST API
@RequestMapping("/api/users")
public class HybridController {
    
    // Возврат JSON для REST API
    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public List<UserDto> getUsersAsJson() {
        return userService.getAllUsers().stream()
            .map(this::convertToDto)
            .collect(Collectors.toList());
    }
    
    // Возврат XML для REST API
    @GetMapping(produces = MediaType.APPLICATION_XML_VALUE)
    @ResponseBody
    public UsersWrapper getUsersAsXml() {
        List<UserDto> users = userService.getAllUsers().stream()
            .map(this::convertToDto)
            .collect(Collectors.toList());
        return new UsersWrapper(users);
    }
    
    // Возврат HTML view для веб-интерфейса
    @GetMapping(produces = MediaType.TEXT_HTML_VALUE)
    public String getUsersAsHtml(Model model) {
        model.addAttribute("users", userService.getAllUsers());
        return "users/list"; // HTML template
    }
    
    // Автоматический выбор формата на основе Accept header
    @GetMapping
    @ResponseBody
    public ResponseEntity<?> getUsers(HttpServletRequest request) {
        String acceptHeader = request.getHeader(HttpHeaders.ACCEPT);
        List<User> users = userService.getAllUsers();
        
        if (acceptHeader != null && acceptHeader.contains(MediaType.APPLICATION_XML_VALUE)) {
            return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_XML)
                .body(new UsersWrapper(users.stream().map(this::convertToDto).collect(Collectors.toList())));
        } else {
            return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(users.stream().map(this::convertToDto).collect(Collectors.toList()));
        }
    }
}

// Конфигурация Content Negotiation
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorParameter(true) // Поддержка ?format=json
            .parameterName("format")
            .ignoreAcceptHeader(false) // Учитывать Accept header
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML)
            .mediaType("html", MediaType.TEXT_HTML);
    }
    
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // Кастомная настройка Jackson для JSON
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.registerModule(new JavaTimeModule());
        jsonConverter.setObjectMapper(objectMapper);
        
        converters.add(jsonConverter);
        
        // XML converter
        MappingJackson2XmlHttpMessageConverter xmlConverter = new MappingJackson2XmlHttpMessageConverter();
        converters.add(xmlConverter);
    }
}

Exception Handling

Глобальная обработка ошибок

// Для @Controller - возврат error pages
@ControllerAdvice
public class WebExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    public String handleUserNotFound(UserNotFoundException e, Model model) {
        model.addAttribute("error", e.getMessage());
        model.addAttribute("errorCode", "USER_NOT_FOUND");
        return "error/404"; // HTML error page
    }
    
    @ExceptionHandler(ValidationException.class)
    public String handleValidation(ValidationException e, Model model) {
        model.addAttribute("errors", e.getErrors());
        return "error/validation"; // HTML validation error page
    }
    
    // Для AJAX запросов можно возвращать JSON
    @ExceptionHandler(DataAccessException.class)
    @ResponseBody
    public ResponseEntity<ErrorResponse> handleDataAccess(DataAccessException e) {
        ErrorResponse error = new ErrorResponse(
            "DATABASE_ERROR",
            "Database operation failed",
            System.currentTimeMillis()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

// Для @RestController - возврат JSON/XML error responses
@RestControllerAdvice
public class RestExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
        ErrorResponse error = ErrorResponse.builder()
            .code("USER_NOT_FOUND")
            .message(e.getMessage())
            .timestamp(Instant.now())
            .path(getCurrentRequestPath())
            .build();
        
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidation(ValidationException e) {
        ValidationErrorResponse error = ValidationErrorResponse.builder()
            .code("VALIDATION_ERROR")
            .message("Validation failed")
            .errors(e.getFieldErrors())
            .timestamp(Instant.now())
            .build();
        
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleMethodArgumentNotValid(
            MethodArgumentNotValidException e) {
        
        Map<String, String> fieldErrors = e.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                FieldError::getDefaultMessage,
                (existing, replacement) -> existing
            ));
        
        ValidationErrorResponse error = ValidationErrorResponse.builder()
            .code("VALIDATION_ERROR")
            .message("Request validation failed")
            .errors(fieldErrors)
            .timestamp(Instant.now())
            .build();
        
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
}

Архитектурные паттерны

Разделение ответственности

// Hybrid подход - один контроллер для web и API
@Controller
@RequestMapping("/orders")
public class OrderController {
    
    private final OrderService orderService;
    
    // Web interface endpoints
    @GetMapping
    public String listOrders(Model model, Pageable pageable) {
        Page<Order> orders = orderService.findAll(pageable);
        model.addAttribute("orders", orders);
        model.addAttribute("currentPage", pageable.getPageNumber());
        return "orders/list";
    }
    
    @GetMapping("/create")
    public String createOrderForm(Model model) {
        model.addAttribute("order", new CreateOrderRequest());
        model.addAttribute("products", productService.findAll());
        return "orders/create";
    }
    
    @PostMapping
    public String createOrder(@ModelAttribute @Valid CreateOrderRequest request,
                            BindingResult result,
                            RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            return "orders/create";
        }
        
        Order order = orderService.create(request);
        redirectAttributes.addFlashAttribute("success", "Order created successfully");
        return "redirect:/orders/" + order.getId();
    }
    
    // REST API endpoints
    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<Page<OrderDto>> getOrdersApi(Pageable pageable) {
        Page<Order> orders = orderService.findAll(pageable);
        Page<OrderDto> dtos = orders.map(this::convertToDto);
        return ResponseEntity.ok(dtos);
    }
    
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<OrderDto> createOrderApi(@RequestBody @Valid CreateOrderRequest request) {
        Order order = orderService.create(request);
        OrderDto dto = convertToDto(order);
        
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(order.getId())
            .toUri();
        
        return ResponseEntity.created(location).body(dto);
    }
}

// Раздельный подход - разные контроллеры
@Controller
@RequestMapping("/web/orders")
public class OrderWebController {
    // Только web endpoints с возвратом views
}

@RestController
@RequestMapping("/api/v1/orders")
public class OrderRestController {
    // Только REST API endpoints с возвратом JSON
}

Версионирование API

// Версионирование через URL path
@RestController
@RequestMapping("/api/v1/users")
public class UserV1RestController {
    
    @GetMapping("/{id}")
    public ResponseEntity<UserV1Dto> getUser(@PathVariable Long id) {
        // Старая версия API
        return ResponseEntity.ok(userService.getUserV1(id));
    }
}

@RestController
@RequestMapping("/api/v2/users")
public class UserV2RestController {
    
    @GetMapping("/{id}")
    public ResponseEntity<UserV2Dto> getUser(@PathVariable Long id) {
        // Новая версия API с дополнительными полями
        return ResponseEntity.ok(userService.getUserV2(id));
    }
}

// Версионирование через headers
@RestController
@RequestMapping("/api/users")
public class UserVersionedController {
    
    @GetMapping(value = "/{id}", headers = "API-Version=1")
    public ResponseEntity<UserV1Dto> getUserV1(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUserV1(id));
    }
    
    @GetMapping(value = "/{id}", headers = "API-Version=2")
    public ResponseEntity<UserV2Dto> getUserV2(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUserV2(id));
    }
    
    // Default version
    @GetMapping("/{id}")
    public ResponseEntity<UserV2Dto> getUserLatest(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUserV2(id));
    }
}

Производительность и кэширование

Оптимизация REST API

@RestController
@RequestMapping("/api/products")
public class ProductRestController {
    
    private final ProductService productService;
    private final CacheManager cacheManager;
    
    // Кэширование ответов
    @GetMapping("/{id}")
    @Cacheable(value = "products", key = "#id")
    public ResponseEntity<ProductDto> getProduct(@PathVariable Long id) {
        return productService.findById(id)
            .map(product -> ResponseEntity
                .ok()
                .cacheControl(CacheControl.maxAge(Duration.ofMinutes(10)))
                .eTag(String.valueOf(product.getVersion()))
                .body(convertToDto(product)))
            .orElse(ResponseEntity.notFound().build());
    }
    
    // ETags для условных запросов
    @GetMapping("/{id}/details")
    public ResponseEntity<ProductDetailDto> getProductDetails(
            @PathVariable Long id,
            @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
        
        Product product = productService.findById(id).orElseThrow();
        String currentETag = String.valueOf(product.getVersion());
        
        // Если ETag совпадает, возвращаем 304 Not Modified
        if (currentETag.equals(ifNoneMatch)) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
        }
        
        ProductDetailDto dto = convertToDetailDto(product);
        return ResponseEntity.ok()
            .eTag(currentETag)
            .body(dto);
    }
    
    // Streaming для больших ответов
    @GetMapping("/export")
    public ResponseEntity<StreamingResponseBody> exportProducts() {
        StreamingResponseBody stream = outputStream -> {
            try (JsonGenerator generator = objectMapper.getFactory().createGenerator(outputStream)) {
                generator.writeStartArray();
                
                productService.findAllForExport().forEach(product -> {
                    try {
                        generator.writeObject(convertToDto(product));
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                });
                
                generator.writeEndArray();
            }
        };
        
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=products.json")
            .body(stream);
    }
    
    // Асинхронная обработка
    @PostMapping("/batch")
    public ResponseEntity<Void> processBatch(@RequestBody List<ProductUpdateRequest> requests) {
        CompletableFuture.runAsync(() -> productService.processBatch(requests));
        
        return ResponseEntity.accepted()
            .header("Location", "/api/products/batch/status")
            .build();
    }
}

Частые вопросы на собеседовании

Q: Когда использовать @Controller, а когда @RestController? A: @Controller для приложений с server-side rendering (JSP, Thymeleaf) и смешанных endpoints. @RestController для pure REST API без view layer.

Q: Можно ли смешивать @ResponseBody с @Controller? A: Да, отдельные методы в @Controller могут использовать @ResponseBody для возврата JSON/XML, остальные методы возвращают views.

Q: Как работает Content Negotiation в Spring MVC? A: Spring определяет формат ответа по Accept header, параметру format или расширению файла. HttpMessageConverter преобразует объекты в нужный формат.

Q: В чем разница между @ControllerAdvice и @RestControllerAdvice? A: @RestControllerAdvice автоматически добавляет @ResponseBody ко всем методам обработки исключений, возвращая JSON/XML вместо view names.

Q: Как обеспечить обратную совместимость API при изменении @RestController? A: Версионирование через URL path (/api/v1/, /api/v2/), headers (API-Version), параметры запроса или media types (application/vnd.api.v1+json).

Q: Какие преимущества дает использование ResponseEntity? A: Полный контроль над HTTP ответом: status code, headers, body. Позволяет возвращать разные статусы в зависимости от бизнес-логики.

Spring MVC / Web

1. Основы Spring MVC

Архитектура MVC

Client Request → DispatcherServlet → HandlerMapping → Controller
                                                           ↓
Client Response ← ViewResolver ← ModelAndView ← Controller

Maven зависимости

<dependencies>
    <!-- Spring Web MVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>6.0.11</version>
    </dependency>
    
    <!-- Servlet API -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
    </dependency>
    
    <!-- JSON обработка -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
    
    <!-- Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
        <version>3.1.2</version>
    </dependency>
</dependencies>

Конфигурация

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example")
public class WebConfig implements WebMvcConfigurer {
    
    // ViewResolver для JSP/Thymeleaf
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
    
    // Настройка статических ресурсов
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/static/");
    }
    
    // CORS настройки
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true);
    }
}

2. Controllers

Базовый Controller

@Controller
@RequestMapping("/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    // GET запрос - список пользователей
    @GetMapping
    public String getUsers(Model model) {
        List<User> users = userService.findAll();
        model.addAttribute("users", users);
        return "users/list"; // возвращает view name
    }
    
    // GET запрос с path variable
    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "users/detail";
    }
    
    // POST запрос
    @PostMapping
    public String createUser(@ModelAttribute User user, 
                           RedirectAttributes redirectAttributes) {
        User savedUser = userService.save(user);
        redirectAttributes.addFlashAttribute("message", "User created successfully");
        return "redirect:/users/" + savedUser.getId();
    }
    
    // PUT запрос
    @PutMapping("/{id}")
    public String updateUser(@PathVariable Long id, 
                           @ModelAttribute User user) {
        user.setId(id);
        userService.update(user);
        return "redirect:/users/" + id;
    }
    
    // DELETE запрос
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return "redirect:/users";
    }
}

REST Controller

@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserRestController {
    
    private final UserService userService;
    
    public UserRestController(UserService userService) {
        this.userService = userService;
    }
    
    // GET - список с пагинацией
    @GetMapping
    public ResponseEntity<Page<User>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy) {
        
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
        Page<User> users = userService.findAll(pageable);
        return ResponseEntity.ok(users);
    }
    
    // GET - один пользователь
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    // POST - создание
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
        User user = userService.create(request);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(user.getId())
            .toUri();
        
        return ResponseEntity.created(location).body(user);
    }
    
    // PUT - обновление
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, 
                                         @Valid @RequestBody UpdateUserRequest request) {
        return userService.update(id, request)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    // PATCH - частичное обновление
    @PatchMapping("/{id}")
    public ResponseEntity<User> patchUser(@PathVariable Long id, 
                                        @RequestBody Map<String, Object> updates) {
        return userService.patch(id, updates)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    // DELETE - удаление
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        if (userService.deleteById(id)) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
    
    // Поиск с query параметрами
    @GetMapping("/search")
    public ResponseEntity<List<User>> searchUsers(
            @RequestParam(required = false) String name,
            @RequestParam(required = false) String email,
            @RequestParam(required = false) Integer minAge,
            @RequestParam(required = false) Integer maxAge) {
        
        UserSearchCriteria criteria = UserSearchCriteria.builder()
            .name(name)
            .email(email)
            .minAge(minAge)
            .maxAge(maxAge)
            .build();
        
        List<User> users = userService.search(criteria);
        return ResponseEntity.ok(users);
    }
}

3. Request Mapping

Path Variables и Request Parameters

@RestController
@RequestMapping("/api")
public class RequestMappingExamples {
    
    // Простой path variable
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    // Множественные path variables
    @GetMapping("/users/{userId}/orders/{orderId}")
    public Order getUserOrder(@PathVariable Long userId, 
                            @PathVariable Long orderId) {
        return orderService.findByUserAndId(userId, orderId);
    }
    
    // Path variable с regex
    @GetMapping("/users/{id:[0-9]+}")
    public User getUserWithNumericId(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    // Optional path variable
    @GetMapping({"/products", "/products/{category}"})
    public List<Product> getProducts(@PathVariable(required = false) String category) {
        return category != null ? 
            productService.findByCategory(category) : 
            productService.findAll();
    }
    
    // Request parameters
    @GetMapping("/users")
    public List<User> getUsers(
            @RequestParam(name = "page", defaultValue = "0") int page,
            @RequestParam(name = "size", defaultValue = "10") int size,
            @RequestParam(required = false) String search) {
        
        return userService.findUsers(page, size, search);
    }
    
    // Multiple values parameter
    @GetMapping("/products/filter")
    public List<Product> filterProducts(
            @RequestParam List<String> categories,
            @RequestParam(required = false) List<String> tags) {
        
        return productService.filter(categories, tags);
    }
    
    // Request parameters as Map
    @GetMapping("/search")
    public List<Object> search(@RequestParam Map<String, String> params) {
        return searchService.search(params);
    }
}

Request Body и Content Types

@RestController
@RequestMapping("/api")
public class RequestBodyExamples {
    
    // JSON request body
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
        User user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
    
    // XML support (с соответствующими зависимостями)
    @PostMapping(value = "/users", consumes = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<User> createUserFromXml(@RequestBody CreateUserRequest request) {
        User user = userService.create(request);
        return ResponseEntity.ok(user);
    }
    
    // Multipart file upload
    @PostMapping("/users/{id}/avatar")
    public ResponseEntity<String> uploadAvatar(
            @PathVariable Long id,
            @RequestParam("file") MultipartFile file) {
        
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("File is empty");
        }
        
        String filename = fileService.saveAvatar(id, file);
        return ResponseEntity.ok(filename);
    }
    
    // Multiple files upload
    @PostMapping("/users/{id}/documents")
    public ResponseEntity<List<String>> uploadDocuments(
            @PathVariable Long id,
            @RequestParam("files") MultipartFile[] files) {
        
        List<String> filenames = new ArrayList<>();
        for (MultipartFile file : files) {
            if (!file.isEmpty()) {
                String filename = fileService.saveDocument(id, file);
                filenames.add(filename);
            }
        }
        
        return ResponseEntity.ok(filenames);
    }
    
    // Form data
    @PostMapping("/contact")
    public ResponseEntity<String> submitContactForm(
            @RequestParam String name,
            @RequestParam String email,
            @RequestParam String message) {
        
        contactService.saveMessage(name, email, message);
        return ResponseEntity.ok("Message sent successfully");
    }
}

4. Validation

Bean Validation

// DTO с валидацией
public class CreateUserRequest {
    
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    private String name;
    
    @NotBlank(message = "Email is required")
    @Email(message = "Email should be valid")
    private String email;
    
    @Min(value = 18, message = "Age must be at least 18")
    @Max(value = 120, message = "Age must be less than 120")
    private Integer age;
    
    @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number format")
    private String phone;
    
    @NotNull(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;
    
    // getters and setters
}

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping
    public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request,
                                       BindingResult bindingResult) {
        
        if (bindingResult.hasErrors()) {
            Map<String, String> errors = new HashMap<>();
            bindingResult.getFieldErrors().forEach(error -> {
                errors.put(error.getField(), error.getDefaultMessage());
            });
            return ResponseEntity.badRequest().body(errors);
        }
        
        User user = userService.create(request);
        return ResponseEntity.ok(user);
    }
}

Custom Validation

// Кастомная аннотация валидации
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
    String message() default "Email already exists";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Validator
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    
    private final UserRepository userRepository;
    
    public UniqueEmailValidator(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        if (email == null) {
            return true; // @NotNull должен обрабатывать null
        }
        return !userRepository.existsByEmail(email);
    }
}

// Использование
public class CreateUserRequest {
    
    @NotBlank
    @Email
    @UniqueEmail
    private String email;
    
    // другие поля
}

Группы валидации

// Группы валидации
public interface CreateValidation {}
public interface UpdateValidation {}

public class UserRequest {
    
    @NotNull(groups = UpdateValidation.class)
    private Long id;
    
    @NotBlank(groups = {CreateValidation.class, UpdateValidation.class})
    private String name;
    
    @NotBlank(groups = CreateValidation.class)
    @UniqueEmail(groups = CreateValidation.class)
    private String email;
    
    // getters and setters
}

@RestController
public class UserController {
    
    @PostMapping("/users")
    public ResponseEntity<User> createUser(
            @Validated(CreateValidation.class) @RequestBody UserRequest request) {
        // создание пользователя
    }
    
    @PutMapping("/users/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id,
            @Validated(UpdateValidation.class) @RequestBody UserRequest request) {
        // обновление пользователя
    }
}

5. Exception Handling

Global Exception Handler

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    // Обработка валидации
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> {
            errors.put(error.getField(), error.getDefaultMessage());
        });
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .status(HttpStatus.BAD_REQUEST.value())
            .error("Validation Failed")
            .message("Invalid input parameters")
            .details(errors)
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.badRequest().body(errorResponse);
    }
    
    // Обработка EntityNotFound
    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleEntityNotFound(
            EntityNotFoundException ex, HttpServletRequest request) {
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .status(HttpStatus.NOT_FOUND.value())
            .error("Entity Not Found")
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.notFound().build();
    }
    
    // Обработка бизнес исключений
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(
            BusinessException ex, HttpServletRequest request) {
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .status(HttpStatus.UNPROCESSABLE_ENTITY.value())
            .error("Business Logic Error")
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.unprocessableEntity().body(errorResponse);
    }
    
    // Обработка проблем с доступом
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDenied(
            AccessDeniedException ex, HttpServletRequest request) {
        
        log.warn("Access denied for request: {}", request.getRequestURI());
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .status(HttpStatus.FORBIDDEN.value())
            .error("Access Denied")
            .message("You don't have permission to access this resource")
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
    }
    
    // Общий обработчик
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception ex, HttpServletRequest request) {
        
        log.error("Unexpected error occurred", ex);
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .error("Internal Server Error")
            .message("An unexpected error occurred")
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.internalServerError().body(errorResponse);
    }
}

@Data
@Builder
public class ErrorResponse {
    private int status;
    private String error;
    private String message;
    private String path;
    private LocalDateTime timestamp;
    private Object details;
}

Локальная обработка исключений

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    // Локальный обработчик исключений
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
        return ResponseEntity.notFound().build();
    }
    
    @ExceptionHandler(DuplicateEmailException.class)
    public ResponseEntity<ErrorResponse> handleDuplicateEmail(DuplicateEmailException ex) {
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.CONFLICT.value())
            .error("Duplicate Email")
            .message(ex.getMessage())
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
    }
}

6. Interceptors и Filters

HandlerInterceptor

@Component
@Slf4j
public class LoggingInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        String requestUri = request.getRequestURI();
        String method = request.getMethod();
        
        log.info("Incoming request: {} {}", method, requestUri);
        
        // Добавляем timestamp в request attributes
        request.setAttribute("startTime", System.currentTimeMillis());
        
        return true; // продолжить обработку
    }
    
    @Override
    public void postHandle(HttpServletRequest request, 
                          HttpServletResponse response, 
                          Object handler, 
                          ModelAndView modelAndView) throws Exception {
        
        log.info("Handler completed for: {} {}", 
                request.getMethod(), request.getRequestURI());
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Object handler, 
                               Exception ex) throws Exception {
        
        Long startTime = (Long) request.getAttribute("startTime");
        long duration = System.currentTimeMillis() - startTime;
        
        log.info("Request completed: {} {} - Status: {} - Duration: {}ms",
                request.getMethod(), 
                request.getRequestURI(),
                response.getStatus(),
                duration);
        
        if (ex != null) {
            log.error("Request completed with exception", ex);
        }
    }
}

// Регистрация interceptor
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private LoggingInterceptor loggingInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/health", "/api/metrics");
    }
}

Servlet Filter

@Component
@Order(1)
public class CorsFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, 
                        ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // CORS headers
        httpResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpResponse.setHeader("Access-Control-Allow-Methods", 
                              "GET, POST, PUT, DELETE, OPTIONS");
        httpResponse.setHeader("Access-Control-Allow-Headers", 
                              "Content-Type, Authorization, X-Requested-With");
        
        // Handle preflight requests
        if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
            httpResponse.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        
        chain.doFilter(request, response);
    }
}

7. Content Negotiation и Response

Multiple Response Formats

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    // Поддержка JSON и XML
    @GetMapping(value = "/{id}", 
                produces = {MediaType.APPLICATION_JSON_VALUE, 
                           MediaType.APPLICATION_XML_VALUE})
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    // Условный response на основе Accept header
    @GetMapping("/export")
    public ResponseEntity<?> exportUsers(
            @RequestHeader("Accept") String acceptHeader) {
        
        List<User> users = userService.findAll();
        
        if (acceptHeader.contains("application/pdf")) {
            byte[] pdf = pdfService.generateUsersPdf(users);
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_PDF)
                    .body(pdf);
        } else if (acceptHeader.contains("text/csv")) {
            String csv = csvService.generateUsersCsv(users);
            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType("text/csv"))
                    .body(csv);
        } else {
            return ResponseEntity.ok(users);
        }
    }
    
    // File download
    @GetMapping("/{id}/avatar")
    public ResponseEntity<Resource> downloadAvatar(@PathVariable Long id) {
        Resource file = fileService.loadAvatar(id);
        
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                       "attachment; filename=\"" + file.getFilename() + "\"")
                .body(file);
    }
    
    // Streaming response
    @GetMapping(value = "/stream", produces = MediaType.TEXT_PLAIN_VALUE)
    public ResponseEntity<StreamingResponseBody> streamData() {
        
        StreamingResponseBody stream = out -> {
            for (int i = 0; i < 1000; i++) {
                out.write(("Data chunk " + i + "\n").getBytes());
                out.flush();
                Thread.sleep(100); // Имитация обработки
            }
        };
        
        return ResponseEntity.ok()
                .header("Content-Type", "text/plain")
                .body(stream);
    }
}

8. Testing

Integration Tests

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class UserControllerIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @Test
    void shouldCreateUser() {
        // given
        CreateUserRequest request = CreateUserRequest.builder()
                .name("John Doe")
                .email("john@example.com")
                .age(30)
                .build();
        
        // when
        ResponseEntity<User> response = restTemplate.postForEntity(
                "/api/users", request, User.class);
        
        // then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getEmail()).isEqualTo("john@example.com");
        
        // Verify in database
        Optional<User> savedUser = userRepository.findByEmail("john@example.com");
        assertThat(savedUser).isPresent();
    }
    
    @Test
    void shouldReturn404WhenUserNotFound() {
        // when
        ResponseEntity<String> response = restTemplate.getForEntity(
                "/api/users/999", String.class);
        
        // then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }
}

Mock MVC Tests

@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void shouldReturnUserById() throws Exception {
        // given
        User user = User.builder()
                .id(1L)
                .name("John Doe")
                .email("john@example.com")
                .build();
        
        when(userService.findById(1L)).thenReturn(Optional.of(user));
        
        // when & then
        mockMvc.perform(get("/api/users/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("John Doe"))
                .andExpect(jsonPath("$.email").value("john@example.com"));
    }
    
    @Test
    void shouldCreateUser() throws Exception {
        // given
        CreateUserRequest request = CreateUserRequest.builder()
                .name("Jane Doe")
                .email("jane@example.com")
                .age(25)
                .build();
        
        User savedUser = User.builder()
                .id(1L)
                .name("Jane Doe")
                .email("jane@example.com")
                .age(25)
                .build();
        
        when(userService.create(any(CreateUserRequest.class))).thenReturn(savedUser);
        
        // when & then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("Jane Doe"));
    }
    
    @Test
    void shouldValidateUserInput() throws Exception {
        // given
        CreateUserRequest invalidRequest = CreateUserRequest.builder()
                .name("") // Invalid - empty name
                .email("invalid-email") // Invalid email format
                .age(15) // Invalid - too young
                .build();
        
        // when & then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(invalidRequest)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.status").value(400))
                .andExpect(jsonPath("$.details.name").exists())
                .andExpect(jsonPath("$.details.email").exists())
                .andExpect(jsonPath("$.details.age").exists());
    }
}

Эта шпаргалка покрывает основные аспекты Spring MVC/Web с практическими примерами использования.

Spring Boot

1. Основы Spring Boot

Что такое Spring Boot

Spring Boot — фреймворк для быстрого создания production-ready приложений на основе Spring Framework:

  • Auto-configuration — автоматическая конфигурация
  • Starter dependencies — готовые наборы зависимостей
  • Embedded servers — встроенные веб-серверы
  • Production features — метрики, health checks, externalized configuration

Создание проекта

<!-- pom.xml -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.2</version>
    <relativePath/>
</parent>

<dependencies>
    <!-- Web starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- JPA starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- Test starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Главный класс приложения

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

// @SpringBootApplication эквивалентно:
// @Configuration + @EnableAutoConfiguration + @ComponentScan

2. Популярные Starters

Web Starters

<!-- Web MVC -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- WebFlux (Reactive) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<!-- REST API с документацией -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.1.0</version>
</dependency>

Data Starters

<!-- JPA with Hibernate -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- MongoDB -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Elasticsearch -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

Security и Messaging

<!-- Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- RabbitMQ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<!-- Kafka -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

3. Auto-Configuration

Как работает Auto-Configuration

// Spring Boot автоматически настраивает компоненты на основе classpath
// Примеры условий автоконфигурации:

@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
    
    @Bean
    @Primary
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}

// Отключение автоконфигурации
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {
    // ...
}

// Или через properties
// spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

Создание кастомной Auto-Configuration

@Configuration
@ConditionalOnClass(MyService.class)
@ConditionalOnProperty(name = "myservice.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    public MyService myService(MyServiceProperties properties) {
        return new MyService(properties);
    }
    
    @Bean
    @ConditionalOnBean(MyService.class)
    public MyServiceHealthIndicator myServiceHealthIndicator(MyService myService) {
        return new MyServiceHealthIndicator(myService);
    }
}

// META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
// com.example.autoconfigure.MyServiceAutoConfiguration

@ConfigurationProperties(prefix = "myservice")
@Data
public class MyServiceProperties {
    private boolean enabled = true;
    private String url;
    private int timeout = 5000;
    private int retryAttempts = 3;
}

4. Configuration Properties

application.properties / application.yml

# application.properties
server.port=8080
server.servlet.context-path=/api

# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Logging
logging.level.com.example=DEBUG
logging.level.org.springframework.web=INFO
logging.file.name=application.log

# Custom properties
app.name=My Application
app.version=1.0.0
app.features.cache-enabled=true
app.features.notifications-enabled=false
# application.yml
server:
  port: 8080
  servlet:
    context-path: /api

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: user
    password: password
    driver-class-name: org.postgresql.Driver
  
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: true
    properties:
      hibernate:
        format_sql: true

logging:
  level:
    com.example: DEBUG
    org.springframework.web: INFO
  file:
    name: application.log

app:
  name: My Application
  version: 1.0.0
  features:
    cache-enabled: true
    notifications-enabled: false

@ConfigurationProperties

@ConfigurationProperties(prefix = "app")
@Component
@Data
@Validated
public class AppProperties {
    
    @NotBlank
    private String name;
    
    @NotBlank
    private String version;
    
    private Features features = new Features();
    
    private Database database = new Database();
    
    @Data
    public static class Features {
        private boolean cacheEnabled = true;
        private boolean notificationsEnabled = false;
        private int maxRetries = 3;
    }
    
    @Data
    @Validated
    public static class Database {
        @Min(1)
        @Max(100)
        private int maxPoolSize = 10;
        
        @Min(1000)
        private int connectionTimeout = 30000;
        
        private List<String> allowedHosts = new ArrayList<>();
    }
}

// Использование
@Service
public class AppService {
    private final AppProperties appProperties;
    
    public AppService(AppProperties appProperties) {
        this.appProperties = appProperties;
    }
    
    public void doSomething() {
        if (appProperties.getFeatures().isCacheEnabled()) {
            // use cache
        }
        
        String appName = appProperties.getName();
        // ...
    }
}

Profile-specific Properties

# application.properties (default)
app.environment=default

# application-dev.properties
spring.datasource.url=jdbc:h2:mem:devdb
spring.jpa.hibernate.ddl-auto=create-drop
logging.level.org.springframework.web=DEBUG

# application-prod.properties
spring.datasource.url=jdbc:postgresql://prod-db:5432/proddb
spring.jpa.hibernate.ddl-auto=validate
logging.level.org.springframework.web=WARN

# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
// Активация профилей
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        System.setProperty("spring.profiles.active", "dev");
        SpringApplication.run(Application.class, args);
    }
}

// Или через аргументы командной строки:
// java -jar app.jar --spring.profiles.active=prod

// Или через переменную окружения:
// SPRING_PROFILES_ACTIVE=prod java -jar app.jar

// Profile-specific компоненты
@Component
@Profile("!prod")
public class DevEmailService implements EmailService {
    // Development implementation
}

@Component
@Profile("prod")
public class ProdEmailService implements EmailService {
    // Production implementation
}

5. Actuator

Включение Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Конфигурация Actuator

# Включение всех endpoints
management.endpoints.web.exposure.include=*

# Или конкретные endpoints
management.endpoints.web.exposure.include=health,info,metrics,prometheus

# Детальная информация о health
management.endpoint.health.show-details=always

# Информация приложения
info.app.name=@project.name@
info.app.version=@project.version@
info.app.description=@project.description@

# Custom info
info.team=Backend Team
info.contact=backend@example.com

Основные Endpoints

# Health check
GET /actuator/health

# Application info
GET /actuator/info

# Metrics
GET /actuator/metrics
GET /actuator/metrics/jvm.memory.used

# Environment
GET /actuator/env
GET /actuator/env/spring.datasource.url

# Configuration properties
GET /actuator/configprops

# Beans
GET /actuator/beans

# Mappings
GET /actuator/mappings

# Shutdown (требует POST и отдельной активации)
POST /actuator/shutdown

Custom Health Indicators

@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    private final DataSource dataSource;
    
    public DatabaseHealthIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    @Override
    public Health health() {
        try (Connection connection = dataSource.getConnection()) {
            if (connection.isValid(1)) {
                return Health.up()
                    .withDetail("database", "Available")
                    .withDetail("validationQuery", "SELECT 1")
                    .build();
            } else {
                return Health.down()
                    .withDetail("database", "Connection invalid")
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withDetail("database", "Connection failed")
                .withException(e)
                .build();
        }
    }
}

@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
    
    private final ExternalServiceClient client;
    
    @Override
    public Health health() {
        try {
            ExternalServiceStatus status = client.getStatus();
            
            if (status.isHealthy()) {
                return Health.up()
                    .withDetail("service", "External Service")
                    .withDetail("status", status.getStatus())
                    .withDetail("responseTime", status.getResponseTime() + "ms")
                    .build();
            } else {
                return Health.down()
                    .withDetail("service", "External Service")
                    .withDetail("status", status.getStatus())
                    .withDetail("error", status.getErrorMessage())
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withDetail("service", "External Service")
                .withDetail("error", "Service unavailable")
                .withException(e)
                .build();
        }
    }
}

Custom Metrics

@Service
public class UserService {
    
    private final MeterRegistry meterRegistry;
    private final Counter userCreationCounter;
    private final Timer userSearchTimer;
    
    public UserService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.userCreationCounter = Counter.builder("user.creation.count")
            .description("Number of users created")
            .register(meterRegistry);
        this.userSearchTimer = Timer.builder("user.search.duration")
            .description("User search duration")
            .register(meterRegistry);
    }
    
    public User createUser(CreateUserRequest request) {
        User user = userRepository.save(new User(request));
        
        // Increment counter
        userCreationCounter.increment();
        
        // Gauge for active users
        Gauge.builder("user.active.count")
            .description("Number of active users")
            .register(meterRegistry, this, UserService::getActiveUserCount);
        
        return user;
    }
    
    public List<User> searchUsers(String query) {
        return userSearchTimer.recordCallable(() -> {
            return userRepository.findByNameContaining(query);
        });
    }
    
    private int getActiveUserCount() {
        return (int) userRepository.countByStatus("ACTIVE");
    }
}

6. Testing

Test Slices

// @SpringBootTest - полный интеграционный тест
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:testdb",
    "spring.jpa.hibernate.ddl-auto=create-drop"
})
class ApplicationIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void contextLoads() {
        // Тест загрузки контекста
    }
    
    @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);
    }
}

// @WebMvcTest - тестирование web layer
@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void shouldReturnUser() throws Exception {
        User user = new User(1L, "John", "john@example.com");
        when(userService.findById(1L)).thenReturn(user);
        
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"));
    }
}

// @DataJpaTest - тестирование JPA layer
@DataJpaTest
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldFindByEmail() {
        User user = new User("John", "john@example.com");
        entityManager.persistAndFlush(user);
        
        Optional<User> found = userRepository.findByEmail("john@example.com");
        
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }
}

// @JsonTest - тестирование JSON serialization
@JsonTest
class UserJsonTest {
    
    @Autowired
    private JacksonTester<User> json;
    
    @Test
    void shouldSerializeUser() throws Exception {
        User user = new User(1L, "John", "john@example.com");
        
        assertThat(json.write(user)).extractingJsonPathValue("$.name")
            .isEqualTo("John");
        assertThat(json.write(user)).extractingJsonPathValue("$.email")
            .isEqualTo("john@example.com");
    }
    
    @Test
    void shouldDeserializeUser() throws Exception {
        String content = """
            {
                "id": 1,
                "name": "John",
                "email": "john@example.com"
            }
            """;
        
        assertThat(json.parse(content))
            .usingRecursiveComparison()
            .isEqualTo(new User(1L, "John", "john@example.com"));
    }
}

TestContainers

@SpringBootTest
@Testcontainers
class DatabaseIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldSaveUser() {
        User user = new User("John", "john@example.com");
        User saved = userRepository.save(user);
        
        assertThat(saved.getId()).isNotNull();
        assertThat(userRepository.findById(saved.getId())).isPresent();
    }
}

7. Production Features

Logging

// Конфигурация логирования
@Slf4j
@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        log.info("Fetching user with id: {}", id);
        
        try {
            User user = userService.findById(id);
            log.debug("Found user: {}", user);
            return ResponseEntity.ok(user);
        } catch (UserNotFoundException e) {
            log.warn("User not found: {}", id);
            return ResponseEntity.notFound().build();
        } catch (Exception e) {
            log.error("Error fetching user: {}", id, e);
            return ResponseEntity.internalServerError().build();
        }
    }
}
<!-- logback-spring.xml -->
<configuration>
    <springProfile name="!prod">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
        <root level="INFO">
            <appender-ref ref="STDOUT"/>
        </root>
    </springProfile>
    
    <springProfile name="prod">
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>logs/application.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
            </rollingPolicy>
            <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
                <providers>
                    <timestamp/>
                    <logLevel/>
                    <loggerName/>
                    <message/>
                    <mdc/>
                    <stackTrace/>
                </providers>
            </encoder>
        </appender>
        <root level="WARN">
            <appender-ref ref="FILE"/>
        </root>
    </springProfile>
</configuration>

Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        
        return http.build();
    }
}

Environment-specific Configuration

# application-prod.properties
# Server configuration
server.port=8080
server.compression.enabled=true
server.http2.enabled=true

# Database connection pooling
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=20000

# JPA optimizations
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.jdbc.batch_size=25

# Actuator security
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=when-authorized
management.security.enabled=true

# Graceful shutdown
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s

Docker Configuration

# Dockerfile
FROM openjdk:17-jdk-slim

WORKDIR /app

COPY target/myapp-*.jar app.jar

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown appuser:appuser app.jar
USER appuser

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:

      - "8080:8080"
    environment:

      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myapp
      - SPRING_DATASOURCE_USERNAME=myapp
      - SPRING_DATASOURCE_PASSWORD=password
    depends_on:

      - db
    restart: unless-stopped

  db:
    image: postgres:13
    environment:

      - POSTGRES_DB=myapp
      - POSTGRES_USER=myapp
      - POSTGRES_PASSWORD=password
    volumes:

      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:

Эта шпаргалка покрывает основные аспекты Spring Boot от создания проекта до production deployment.

Spring Data JPA / JDBC

1. Основы и настройка

Maven зависимости

<!-- Spring Data JPA включает Hibernate, Spring JDBC, Connection Pool -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Драйвер БД -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

Конфигурация

# Подключение к БД
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=password

# JPA/Hibernate
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Connection Pool (HikariCP по умолчанию)
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.connection-timeout=20000

2. Entity Mapping

Базовая Entity

@Entity
@Table(name = "users")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 50)
    private String name;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @Enumerated(EnumType.STRING)
    private Status status;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp  
    private LocalDateTime updatedAt;
    
    @Version  // Оптимистичные блокировки
    private Long version;
}

enum Status { ACTIVE, INACTIVE }

Relationships (Связи)

// One-to-Many
@Entity
public class User {
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();
}

@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

// Many-to-Many
@Entity
public class Product {
    @ManyToMany
    @JoinTable(
        name = "product_categories",
        joinColumns = @JoinColumn(name = "product_id"),
        inverseJoinColumns = @JoinColumn(name = "category_id")
    )
    private Set<Category> categories = new HashSet<>();
}

Fetch Types:

  • LAZY - загружается при обращении (по умолчанию для коллекций)
  • EAGER - загружается сразу (по умолчанию для @ManyToOne, @OneToOne)

Cascade Types:

  • PERSIST - сохранение связанных entities
  • MERGE - обновление связанных entities
  • REMOVE - удаление связанных entities
  • ALL - все операции

3. Repository Interfaces

JpaRepository методы

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // Query Methods - Spring автоматически генерирует SQL
    List<User> findByName(String name);
    List<User> findByNameContainingIgnoreCase(String name);
    Optional<User> findByEmail(String email);
    List<User> findByStatus(Status status);
    List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
    
    // Sorting и Pagination
    List<User> findByStatusOrderByCreatedAtDesc(Status status);
    Page<User> findByNameContaining(String name, Pageable pageable);
    
    // Count и Exists
    long countByStatus(Status status);
    boolean existsByEmail(String email);
    
    // Delete
    void deleteByStatus(Status status);
    
    // Top/First
    List<User> findTop10ByStatusOrderByCreatedAtDesc(Status status);
}

Ключевые слова Query Methods:

  • findBy, getBy, queryBy - поиск
  • countBy - подсчет
  • deleteBy, removeBy - удаление
  • existsBy - проверка существования
  • And, Or - логические операторы
  • Like, NotLike - текстовый поиск
  • GreaterThan, LessThan, Between - сравнения
  • OrderBy - сортировка
  • Top, First - ограничение результатов

Custom Queries

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // JPQL - объектно-ориентированный SQL
    @Query("SELECT u FROM User u WHERE u.email = :email")
    Optional<User> findByEmailJpql(@Param("email") String email);
    
    @Query("SELECT u FROM User u WHERE u.name LIKE %:name% ORDER BY u.createdAt")
    List<User> searchByName(@Param("name") String name);
    
    // Native SQL - обычный SQL
    @Query(value = "SELECT * FROM users WHERE email ILIKE %:pattern%", nativeQuery = true)
    List<User> findByEmailPattern(@Param("pattern") String pattern);
    
    // Modifying queries - UPDATE/DELETE
    @Modifying
    @Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
    int updateUserStatus(@Param("id") Long id, @Param("status") Status status);
    
    // DTO Projection
    @Query("SELECT new com.example.UserDto(u.id, u.name, u.email) FROM User u")
    List<UserDto> findAllAsDto();
}

Projections (Проекции)

// Interface-based Projection
public interface UserProjection {
    String getName();
    String getEmail();
    
    @Value("#{target.name + ' (' + target.email + ')'}")
    String getDisplayName();
}

// Class-based Projection (DTO)
@Value
public class UserDto {
    Long id;
    String name;
    String email;
}

// Dynamic Projections
public interface UserRepository extends JpaRepository<User, Long> {
    <T> List<T> findByStatus(Status status, Class<T> type);
}

// Использование:
// List<UserProjection> projections = userRepository.findByStatus(ACTIVE, UserProjection.class);
// List<UserDto> dtos = userRepository.findByStatus(ACTIVE, UserDto.class);

4. Custom Repository Implementation

Расширение функциональности Repository

// Кастомный интерфейс
public interface UserRepositoryCustom {
    List<User> findWithComplexCriteria(UserSearchCriteria criteria);
    Page<User> findWithDynamicQuery(UserSearchCriteria criteria, Pageable pageable);
}

// Реализация
@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public List<User> findWithComplexCriteria(UserSearchCriteria criteria) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);
        
        List<Predicate> predicates = new ArrayList<>();
        
        if (criteria.getName() != null) {
            predicates.add(cb.like(cb.lower(root.get("name")), 
                "%" + criteria.getName().toLowerCase() + "%"));
        }
        
        if (criteria.getStatus() != null) {
            predicates.add(cb.equal(root.get("status"), criteria.getStatus()));
        }
        
        if (!predicates.isEmpty()) {
            query.where(cb.and(predicates.toArray(new Predicate[0])));
        }
        
        return entityManager.createQuery(query).getResultList();
    }
}

// Основной интерфейс
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    // Стандартные методы + кастомные
}

Criteria API - программное построение запросов:

  • CriteriaBuilder - фабрика для создания запросов
  • CriteriaQuery - сам запрос
  • Root - корневая entity
  • Predicate - условия WHERE

5. Spring JDBC

JdbcTemplate - низкоуровневый доступ к БД

@Repository
public class UserJdbcRepository {
    
    private final JdbcTemplate jdbcTemplate;
    private final NamedParameterJdbcTemplate namedJdbcTemplate;
    
    public UserJdbcRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.namedJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
    }
    
    // Простые запросы
    public List<User> findAll() {
        return jdbcTemplate.query(
            "SELECT id, name, email FROM users", 
            new UserRowMapper()
        );
    }
    
    public Optional<User> findById(Long id) {
        try {
            User user = jdbcTemplate.queryForObject(
                "SELECT id, name, email FROM users WHERE id = ?",
                new UserRowMapper(), id
            );
            return Optional.of(user);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }
    
    // Named parameters
    public List<User> findByStatus(Status status) {
        String sql = "SELECT id, name, email FROM users WHERE status = :status";
        Map<String, Object> params = Map.of("status", status.name());
        return namedJdbcTemplate.query(sql, params, new UserRowMapper());
    }
    
    // Insert с получением ID
    public User save(User user) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        String sql = "INSERT INTO users (name, email, status) VALUES (?, ?, ?)";
        
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
            ps.setString(1, user.getName());
            ps.setString(2, user.getEmail());
            ps.setString(3, user.getStatus().name());
            return ps;
        }, keyHolder);
        
        user.setId(keyHolder.getKey().longValue());
        return user;
    }
    
    // Batch операции
    public void saveAll(List<User> users) {
        String sql = "INSERT INTO users (name, email, status) VALUES (?, ?, ?)";
        
        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                User user = users.get(i);
                ps.setString(1, user.getName());
                ps.setString(2, user.getEmail());
                ps.setString(3, user.getStatus().name());
            }
            
            @Override
            public int getBatchSize() {
                return users.size();
            }
        });
    }
    
    // RowMapper для маппинга ResultSet в объект
    private static class UserRowMapper implements RowMapper<User> {
        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getLong("id"));
            user.setName(rs.getString("name"));
            user.setEmail(rs.getString("email"));
            return user;
        }
    }
}

Когда использовать JDBC вместо JPA:

  • Сложные SQL запросы
  • Высокая производительность
  • Batch операции
  • Работа с хранимыми процедурами
  • Legacy системы

6. Transactions (Транзакции)

Declarative Transactions

@Service
@Transactional  // Все методы класса транзакционные
public class UserService {
    
    // Read-only транзакция (оптимизация)
    @Transactional(readOnly = true)
    public List<User> findActiveUsers() {
        return userRepository.findByStatus(Status.ACTIVE);
    }
    
    // Настройка транзакции
    @Transactional(
        isolation = Isolation.READ_COMMITTED,    // Уровень изоляции
        propagation = Propagation.REQUIRES_NEW,  // Новая транзакция
        timeout = 30,                            // Таймаут в секундах
        rollbackFor = BusinessException.class,   // Откат при этих исключениях
        noRollbackFor = LoggingException.class   // Не откатывать при этих
    )
    public User createUser(CreateUserRequest request) {
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        
        return userRepository.save(user);
    }
    
    // Ручное управление транзакциями
    @Autowired
    private TransactionTemplate transactionTemplate;
    
    public void processInBatches(List<User> users) {
        int batchSize = 100;
        for (int i = 0; i < users.size(); i += batchSize) {
            List<User> batch = users.subList(i, Math.min(i + batchSize, users.size()));
            
            transactionTemplate.executeWithoutResult(status -> {
                userRepository.saveAll(batch);
            });
        }
    }
}

Propagation типы:

  • REQUIRED - использует существующую или создает новую (по умолчанию)
  • REQUIRES_NEW - всегда создает новую транзакцию
  • SUPPORTS - использует существующую, если есть
  • NOT_SUPPORTED - выполняется без транзакции
  • NEVER - выбрасывает исключение, если есть транзакция
  • MANDATORY - требует существующую транзакцию

Isolation levels:

  • READ_UNCOMMITTED - грязное чтение
  • READ_COMMITTED - фиксированное чтение (по умолчанию)
  • REPEATABLE_READ - повторяемое чтение
  • SERIALIZABLE - последовательное выполнение

7. Performance и Best Practices

N+1 Problem (проблема N+1 запросов)

// ПРОБЛЕМА: 1 запрос для пользователей + N запросов для заказов каждого
List<User> users = userRepository.findAll();  // 1 запрос
for (User user : users) {
    user.getOrders().size();  // N запросов!
}

// РЕШЕНИЕ 1: Fetch Join
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.status = :status")
List<User> findByStatusWithOrders(@Param("status") Status status);

// РЕШЕНИЕ 2: Entity Graph
@EntityGraph(attributePaths = {"orders"})
List<User> findByStatus(Status status);

// РЕШЕНИЕ 3: Named Entity Graph
@NamedEntityGraph(name = "User.withOrders", attributeNodes = @NamedAttributeNode("orders"))
@Entity
public class User { ... }

@EntityGraph("User.withOrders")
List<User> findByStatus(Status status);

Pagination и Sorting

@RestController
public class UserController {
    
    @GetMapping("/users")
    public Page<User> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDir) {
        
        Sort sort = Sort.by(Sort.Direction.fromString(sortDir), sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);
        
        return userRepository.findAll(pageable);
    }
}

// Slice vs Page
// Page - знает общее количество записей (делает дополнительный COUNT запрос)
// Slice - не знает общее количество (быстрее для больших данных)
Slice<User> findTop20ByOrderByCreatedAtDesc(Pageable pageable);

Batch Processing

@Service
public class BatchService {
    
    @PersistenceContext
    private EntityManager em;
    
    private final int BATCH_SIZE = 25;  // spring.jpa.properties.hibernate.jdbc.batch_size
    
    @Transactional
    public void batchInsert(List<User> users) {
        for (int i = 0; i < users.size(); i++) {
            em.persist(users.get(i));
            
            // Flush каждые BATCH_SIZE записей
            if (i % BATCH_SIZE == 0 && i > 0) {
                em.flush();
                em.clear();  // Очистка кэша для освобождения памяти
            }
        }
        em.flush();
        em.clear();
    }
}

Caching

// Entity-level кэширование
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User { ... }

// Query-level кэширование
@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
@Query("SELECT u FROM User u WHERE u.status = :status")
List<User> findCachedByStatus(@Param("status") Status status);

// Spring Cache
@Service
public class UserService {
    
    @Cacheable("users")
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    @CacheEvict(value = "users", key = "#user.id")
    public User update(User user) {
        return userRepository.save(user);
    }
}

8. Testing

Repository Testing

@DataJpaTest  // Настраивает только JPA компоненты
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;  // Для setup тестовых данных
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldFindByEmail() {
        // given
        User user = new User();
        user.setName("John");
        user.setEmail("john@example.com");
        entityManager.persistAndFlush(user);
        
        // when
        Optional<User> found = userRepository.findByEmail("john@example.com");
        
        // then
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }
    
    @Test
    void shouldCountByStatus() {
        // given
        User active = new User("John", "john@example.com", Status.ACTIVE);
        User inactive = new User("Jane", "jane@example.com", Status.INACTIVE);
        entityManager.persist(active);
        entityManager.persist(inactive);
        entityManager.flush();
        
        // when
        long count = userRepository.countByStatus(Status.ACTIVE);
        
        // then
        assertThat(count).isEqualTo(1);
    }
}

Integration Testing с TestContainers

@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Autowired
    private UserService userService;
    
    @Test
    void shouldCreateAndFindUser() {
        // given
        CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
        
        // when
        User created = userService.createUser(request);
        Optional<User> found = userService.findById(created.getId());
        
        // then
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }
}

9. Полезные советы

Конфигурация для Production

# JPA Optimization
spring.jpa.open-in-view=false  # Отключить для API
spring.jpa.properties.hibernate.jdbc.batch_size=25
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true

# Connection Pool
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=20000
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=1200000
spring.datasource.hikari.leak-detection-threshold=60000

# Logging
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

Общие ошибки и решения

1. LazyInitializationException

// ПРОБЛЕМА: доступ к lazy коллекции вне транзакции
@Transactional(readOnly = true)  // РЕШЕНИЕ: добавить @Transactional
public List<Order> getUserOrders(Long userId) {
    User user = userRepository.findById(userId).get();
    return user.getOrders();  // LazyInitializationException без @Transactional
}

2. N+1 Problem

// РЕШЕНИЕ: использовать fetch join или entity graph
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();

3. Слишком большие транзакции

// ПРОБЛЕМА: обработка миллионов записей в одной транзакции
// РЕШЕНИЕ: batch processing
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void processLargeDataSet(List<Data> data) {
    // Обработка по частям в отдельных транзакциях
}

Эта шпаргалка покрывает основные аспекты Spring Data JPA и JDBC с акцентом на практическое применение и типичные проблемы.

Hibernate

Что такое Hibernate?

Hibernate — это Object-Relational Mapping (ORM) фреймворк для Java, который автоматизирует маппинг между объектной моделью приложения и реляционной базой данных. Hibernate является реализацией JPA (Java Persistence API) спецификации.

Основные возможности:

  • Автоматический маппинг — преобразование Java объектов в SQL и обратно
  • Lazy Loading — загрузка данных по требованию для оптимизации производительности
  • Кэширование — первый и второй уровни кэша для минимизации обращений к БД
  • HQL/JPQL — объектно-ориентированный язык запросов
  • Automatic dirty checking — автоматическое отслеживание изменений в entities
  • Connection pooling — управление пулом соединений с БД

Архитектура Hibernate

Основные компоненты

SessionFactory — тяжеловесный объект, создается один раз при старте приложения. Содержит метаданные о маппинге и конфигурации. Thread-safe и является фабрикой для создания Session.

Session — основной интерфейс для работы с БД. Представляет единицу работы (Unit of Work) и управляет жизненным циклом entities. НЕ thread-safe.

Transaction — представляет транзакцию базы данных. Hibernate может работать с JTA и JDBC транзакциями.

EntityManager — JPA эквивалент Session. Стандартный API для работы с persistence context.

// Основные компоненты Hibernate
@Configuration
public class HibernateConfig {
    
    @Bean
    public SessionFactory sessionFactory() {
        return new Configuration()
            .configure("hibernate.cfg.xml")
            .addAnnotatedClass(User.class)
            .buildSessionFactory();
    }
    
    @Bean
    public EntityManagerFactory entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setDataSource(dataSource());
        factory.setPackagesToScan("com.example.entity");
        factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        return factory.getObject();
    }
}

Entity Lifecycle и состояния

Состояния Entity

Transient (Временное) — объект создан, но не связан с Session. Hibernate не знает о его существовании.

Persistent (Управляемое) — объект связан с активной Session и имеет соответствующую запись в БД. Автоматическое отслеживание изменений.

Detached (Отсоединенное) — объект был persistent, но Session закрыта. Содержит данные из БД, но изменения не отслеживаются.

Removed (Удаляемое) — объект помечен для удаления из БД при следующем flush.

@Service
@Transactional
public class UserService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void demonstrateEntityStates() {
        // 1. Transient - объект создан, но не управляется Hibernate
        User user = new User("John", "john@example.com");
        
        // 2. Persistent - объект становится управляемым
        entityManager.persist(user); // INSERT в БД при flush
        user.setEmail("newemail@example.com"); // Изменение будет автоматически сохранено
        
        // 3. Detached - после закрытия EntityManager
        entityManager.detach(user);
        user.setName("Jane"); // Это изменение НЕ будет сохранено
        
        // 4. Merge - возвращение detached объекта в persistent состояние
        User managed = entityManager.merge(user); // UPDATE в БД
        
        // 5. Removed - пометка для удаления
        entityManager.remove(managed); // DELETE при flush
    }
    
    public void showLazyLoading() {
        // Lazy loading - данные загружаются при первом обращении
        User user = entityManager.find(User.class, 1L);
        
        // Proxy объект - реальные данные еще не загружены
        List<Order> orders = user.getOrders(); // Здесь происходит загрузка из БД
        
        // Проверка инициализации
        boolean isInitialized = Hibernate.isInitialized(orders);
        
        // Принудительная инициализация
        Hibernate.initialize(user.getOrders());
    }
}

Маппинг аннотации

Основные аннотации маппинга

@Entity
@Table(name = "users", 
       indexes = @Index(name = "idx_email", columnList = "email"),
       uniqueConstraints = @UniqueConstraint(columnNames = "email"))
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // Second-level cache
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "user_name", nullable = false, length = 100)
    private String name;
    
    @Column(unique = true)
    @Email
    private String email;
    
    @Temporal(TemporalType.TIMESTAMP)
    @CreationTimestamp // Автоматическая установка времени создания
    private Date createdAt;
    
    @UpdateTimestamp // Автоматическое обновление времени изменения
    private LocalDateTime updatedAt;
    
    @Enumerated(EnumType.STRING) // Хранение enum как строка
    private UserStatus status;
    
    @Embedded // Встраиваемый объект
    private Address address;
    
    // One-to-Many с lazy loading по умолчанию
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    @OrderBy("createdAt DESC")
    private List<Order> orders = new ArrayList<>();
    
    // Many-to-One с eager loading
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "department_id")
    private Department department;
    
    // Many-to-Many с join table
    @ManyToMany
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
}

@Embeddable // Встраиваемый класс
public class Address {
    private String street;
    private String city;
    
    @Column(name = "zip_code")
    private String zipCode;
}

@Entity — помечает класс как JPA entity @Table — конфигурация таблицы БД (имя, индексы, ограничения) @GeneratedValue — стратегии генерации первичного ключа @Temporal — маппинг Date/Calendar типов @Embedded/@Embeddable — композиция объектов в одной таблице

Стратегии наследования

// 1. SINGLE_TABLE - все классы в одной таблице с discriminator
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "account_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Account {
    @Id
    @GeneratedValue
    private Long id;
    
    private BigDecimal balance;
}

@Entity
@DiscriminatorValue("SAVINGS")
public class SavingsAccount extends Account {
    private Double interestRate;
}

@Entity
@DiscriminatorValue("CHECKING")
public class CheckingAccount extends Account {
    private BigDecimal overdraftLimit;
}

// 2. JOINED - отдельные таблицы с внешними ключами
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Vehicle {
    @Id
    @GeneratedValue
    private Long id;
    private String manufacturer;
}

@Entity
@PrimaryKeyJoinColumn(name = "vehicle_id")
public class Car extends Vehicle {
    private Integer numberOfDoors;
}

// 3. TABLE_PER_CLASS - отдельная таблица для каждого конкретного класса
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment {
    @Id
    @GeneratedValue
    private Long id;
    private BigDecimal amount;
}

Queries: HQL, JPQL, Criteria API

HQL (Hibernate Query Language)

@Repository
public class UserRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<User> findUsersByDepartment(String departmentName) {
        // HQL с именованными параметрами
        String hql = "SELECT u FROM User u JOIN u.department d WHERE d.name = :deptName";
        return entityManager.createQuery(hql, User.class)
            .setParameter("deptName", departmentName)
            .getResultList();
    }
    
    public List<UserProjection> getUserProjections() {
        // HQL с проекцией (constructor expression)
        String hql = "SELECT new com.example.dto.UserProjection(u.name, u.email, d.name) " +
                    "FROM User u LEFT JOIN u.department d";
        return entityManager.createQuery(hql, UserProjection.class)
            .getResultList();
    }
    
    @Query("SELECT u FROM User u WHERE u.createdAt > :since ORDER BY u.createdAt DESC")
    public List<User> findRecentUsers(@Param("since") LocalDateTime since);
    
    // Именованный запрос, определенный в Entity
    @NamedQuery(
        name = "User.findByStatus",
        query = "SELECT u FROM User u WHERE u.status = :status"
    )
    public List<User> findByStatus(UserStatus status) {
        return entityManager.createNamedQuery("User.findByStatus", User.class)
            .setParameter("status", status)
            .getResultList();
    }
}

HQL особенности:

  • Объектно-ориентированный язык запросов
  • Работа с entity классами и свойствами, а не таблицами
  • Автоматическое разрешение ассоциаций
  • Поддержка полиморфных запросов

Criteria API

@Service
public class UserSearchService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<User> dynamicSearch(UserSearchCriteria criteria) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);
        
        List<Predicate> predicates = new ArrayList<>();
        
        // Динамическое построение условий
        if (criteria.getName() != null) {
            predicates.add(cb.like(user.get("name"), "%" + criteria.getName() + "%"));
        }
        
        if (criteria.getMinAge() != null) {
            predicates.add(cb.greaterThanOrEqualTo(user.get("age"), criteria.getMinAge()));
        }
        
        if (criteria.getDepartmentId() != null) {
            Join<User, Department> deptJoin = user.join("department");
            predicates.add(cb.equal(deptJoin.get("id"), criteria.getDepartmentId()));
        }
        
        // Объединение условий
        query.where(cb.and(predicates.toArray(new Predicate[0])));
        
        // Сортировка
        if (criteria.getSortBy() != null) {
            Order order = criteria.getSortDirection() == SortDirection.DESC 
                ? cb.desc(user.get(criteria.getSortBy()))
                : cb.asc(user.get(criteria.getSortBy()));
            query.orderBy(order);
        }
        
        TypedQuery<User> typedQuery = entityManager.createQuery(query);
        
        // Пагинация
        if (criteria.getPageSize() != null) {
            typedQuery.setMaxResults(criteria.getPageSize());
            if (criteria.getOffset() != null) {
                typedQuery.setFirstResult(criteria.getOffset());
            }
        }
        
        return typedQuery.getResultList();
    }
    
    // Criteria API для агрегации
    public Map<String, Long> getUserCountByDepartment() {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Object[]> query = cb.createQuery(Object[].class);
        Root<User> user = query.from(User.class);
        Join<User, Department> department = user.join("department");
        
        query.multiselect(
            department.get("name"),
            cb.count(user)
        );
        query.groupBy(department.get("name"));
        
        List<Object[]> results = entityManager.createQuery(query).getResultList();
        
        return results.stream()
            .collect(Collectors.toMap(
                row -> (String) row[0],
                row -> (Long) row[1]
            ));
    }
}

Criteria API преимущества:

  • Type-safe запросы
  • Динамическое построение запросов
  • Компиляционная проверка
  • Интеграция с метамоделью JPA

Кэширование

Первый уровень кэша (Session Cache)

First Level Cache — автоматический кэш на уровне Session/EntityManager. Гарантирует, что в рамках одной сессии объект с одним ID будет загружен только один раз.

@Service
@Transactional
public class CacheExampleService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void demonstrateFirstLevelCache() {
        // Первый вызов - загрузка из БД
        User user1 = entityManager.find(User.class, 1L);
        
        // Второй вызов - возврат того же объекта из кэша Session
        User user2 = entityManager.find(User.class, 1L);
        
        // user1 == user2 (один и тот же объект)
        assert user1 == user2;
        
        // Очистка кэша первого уровня
        entityManager.clear();
        
        // Теперь будет новый запрос к БД
        User user3 = entityManager.find(User.class, 1L);
        assert user1 != user3;
    }
}

Второй уровень кэша (SessionFactory Cache)

Second Level Cache — опциональный кэш на уровне SessionFactory, разделяемый между всеми сессиями. Кэширует entity данные, коллекции и результаты запросов.

// Конфигурация второго уровня кэша
@Configuration
public class HibernateCacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        return CacheManagerBuilder.newCacheManagerBuilder()
            .withCache("userCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    Long.class, User.class,
                    ResourcePoolsBuilder.heap(1000).offheap(10, MemoryUnit.MB))
                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(30)))
            )
            .build();
    }
}

// Entity с включенным кэшированием
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "userCache")
public class User {
    // ... поля
    
    @OneToMany(mappedBy = "user")
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // Кэширование коллекции
    private List<Order> orders;
}

@Service
public class CacheService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void demonstrateSecondLevelCache() {
        // Первый запрос - загрузка в кэш второго уровня
        User user = entityManager.find(User.class, 1L);
        
        // Закрытие EntityManager
        entityManager.close();
        
        // Новый EntityManager
        EntityManager newEm = entityManagerFactory.createEntityManager();
        
        // Загрузка из кэша второго уровня (без запроса к БД)
        User cachedUser = newEm.find(User.class, 1L);
        
        // Статистика кэша
        SessionFactory sessionFactory = entityManager.getEntityManagerFactory()
            .unwrap(SessionFactory.class);
        Statistics stats = sessionFactory.getStatistics();
        
        long hitCount = stats.getSecondLevelCacheHitCount();
        long missCount = stats.getSecondLevelCacheMissCount();
        double hitRatio = (double) hitCount / (hitCount + missCount);
    }
    
    // Кэширование запросов
    @Cacheable("usersByDepartment")
    public List<User> findUsersByDepartment(String departmentName) {
        return entityManager.createQuery(
            "SELECT u FROM User u JOIN u.department d WHERE d.name = :dept", User.class)
            .setParameter("dept", departmentName)
            .setHint("org.hibernate.cacheable", true) // Включение кэширования запроса
            .getResultList();
    }
}

Стратегии кэширования:

  • READ_ONLY — только чтение, для неизменяемых данных
  • READ_WRITE — чтение и запись с поддержкой транзакций
  • NONSTRICT_READ_WRITE — без строгой консистентности
  • TRANSACTIONAL — полная транзакционная поддержка

Lazy Loading и N+1 проблема

Стратегии загрузки

@Entity
public class Department {
    @Id
    private Long id;
    
    // Lazy loading (по умолчанию для @OneToMany)
    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
    private List<User> users;
    
    // Eager loading
    @ManyToOne(fetch = FetchType.EAGER)
    private Company company;
}

@Service
public class LoadingStrategiesService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    // Проблема N+1 запросов
    public void demonstrateNPlusOneProblem() {
        // 1 запрос для загрузки департаментов
        List<Department> departments = entityManager
            .createQuery("SELECT d FROM Department d", Department.class)
            .getResultList();
        
        // N запросов для загрузки пользователей каждого департамента
        for (Department dept : departments) {
            List<User> users = dept.getUsers(); // Lazy loading здесь
            System.out.println("Department " + dept.getName() + " has " + users.size() + " users");
        }
    }
    
    // Решение через JOIN FETCH
    public void solutionWithJoinFetch() {
        List<Department> departments = entityManager
            .createQuery("""
                SELECT DISTINCT d FROM Department d 
                LEFT JOIN FETCH d.users 
                LEFT JOIN FETCH d.company
                """, Department.class)
            .getResultList();
        
        // Все данные загружены одним запросом
        for (Department dept : departments) {
            List<User> users = dept.getUsers(); // Нет дополнительных запросов
            System.out.println("Department " + dept.getName() + " has " + users.size() + " users");
        }
    }
    
    // Решение через @EntityGraph
    @EntityGraph(attributePaths = {"users", "company"})
    public List<Department> findDepartmentsWithUsersAndCompany() {
        return entityManager
            .createQuery("SELECT d FROM Department d", Department.class)
            .setHint("javax.persistence.fetchgraph", 
                    entityManager.getEntityGraph("departmentWithDetails"))
            .getResultList();
    }
    
    // Batch fetching для оптимизации
    @BatchSize(size = 10) // Загрузка по 10 коллекций за раз
    @OneToMany(mappedBy = "department")
    private List<User> users;
}

// Named Entity Graph
@Entity
@NamedEntityGraph(
    name = "departmentWithDetails",
    attributeNodes = {
        @NamedAttributeNode("users"),
        @NamedAttributeNode("company")
    }
)
public class Department {
    // ... поля
}

Способы решения N+1:

  • JOIN FETCH — явная загрузка связанных данных
  • @EntityGraph — декларативное указание графа загрузки
  • @BatchSize — пакетная загрузка
  • Subselect fetching — подзапрос для загрузки коллекций

Performance Tuning

Оптимизация запросов

@Service
public class PerformanceTuningService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    // Пакетное сохранение
    @Transactional
    public void batchInsert(List<User> users) {
        int batchSize = 50;
        
        for (int i = 0; i < users.size(); i++) {
            entityManager.persist(users.get(i));
            
            if (i % batchSize == 0 && i > 0) {
                // Принудительная отправка в БД и очистка Session
                entityManager.flush();
                entityManager.clear();
            }
        }
    }
    
    // Пакетное обновление через bulk operations
    @Transactional
    public int updateUserStatusBulk(UserStatus oldStatus, UserStatus newStatus) {
        return entityManager.createQuery(
            "UPDATE User u SET u.status = :newStatus WHERE u.status = :oldStatus")
            .setParameter("newStatus", newStatus)
            .setParameter("oldStatus", oldStatus)
            .executeUpdate();
    }
    
    // Пагинация для больших результатов
    public Page<User> findUsersWithPagination(Pageable pageable) {
        // Запрос для подсчета общего количества
        Long total = entityManager.createQuery(
            "SELECT COUNT(u) FROM User u", Long.class)
            .getSingleResult();
        
        // Запрос для получения данных страницы
        List<User> content = entityManager.createQuery(
            "SELECT u FROM User u ORDER BY u.createdAt DESC", User.class)
            .setFirstResult((int) pageable.getOffset())
            .setMaxResults(pageable.getPageSize())
            .getResultList();
        
        return new PageImpl<>(content, pageable, total);
    }
    
    // Оптимизация с проекциями
    public List<UserSummaryDto> getUserSummaries() {
        // Загрузка только необходимых полей
        return entityManager.createQuery("""
            SELECT new com.example.dto.UserSummaryDto(
                u.id, u.name, u.email, d.name
            )
            FROM User u LEFT JOIN u.department d
            """, UserSummaryDto.class)
            .getResultList();
    }
    
    // Мониторинг производительности
    public void monitorHibernateStatistics() {
        SessionFactory sessionFactory = entityManager.getEntityManagerFactory()
            .unwrap(SessionFactory.class);
        Statistics stats = sessionFactory.getStatistics();
        
        // Статистика запросов
        long queryCount = stats.getQueryExecutionCount();
        long queryTime = stats.getQueryExecutionMaxTime();
        String slowestQuery = stats.getQueryExecutionMaxTimeQueryString();
        
        // Статистика кэша
        long secondLevelCacheHitCount = stats.getSecondLevelCacheHitCount();
        long secondLevelCacheMissCount = stats.getSecondLevelCacheMissCount();
        
        // Статистика сессий
        long sessionOpenCount = stats.getSessionOpenCount();
        long sessionCloseCount = stats.getSessionCloseCount();
        
        log.info("Query count: {}, Max query time: {}ms, Slowest query: {}", 
                queryCount, queryTime, slowestQuery);
    }
}

Hibernate Configuration для производительности

spring:
  jpa:
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        # JDBC и connection pooling
        jdbc:
          batch_size: 50
          batch_versioned_data: true
        order_inserts: true
        order_updates: true
        
        # Статистика и логирование
        generate_statistics: true
        show_sql: false
        format_sql: true
        
        # Второй уровень кэша
        cache:
          use_second_level_cache: true
          use_query_cache: true
          region:
            factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory
        
        # Оптимизации
        default_batch_fetch_size: 16
        max_fetch_depth: 3
        
        # Connection pool (HikariCP)
        hikari:
          maximum-pool-size: 20
          minimum-idle: 5
          idle-timeout: 300000
          max-lifetime: 600000

Транзакции и Concurrency

Уровни изоляции и блокировки

@Service
public class TransactionService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    // Pessimistic locking - блокировка на уровне БД
    @Transactional
    public User updateUserWithLock(Long userId, String newName) {
        User user = entityManager.find(User.class, userId, LockModeType.PESSIMISTIC_WRITE);
        user.setName(newName);
        return user;
    }
    
    // Optimistic locking - версионирование
    @Entity
    public class User {
        @Version
        private Long version; // Автоматическое управление версиями
        
        // При обновлении Hibernate проверит version
        // и выбросит OptimisticLockException при конфликте
    }
    
    @Transactional
    public void handleOptimisticLockException(User user) {
        try {
            user.setName("Updated Name");
            entityManager.merge(user);
            entityManager.flush();
        } catch (OptimisticLockException e) {
            // Конфликт версий - нужно обновить данные
            entityManager.clear();
            User freshUser = entityManager.find(User.class, user.getId());
            // Повторить операцию или показать пользователю конфликт
        }
    }
    
    // Различные уровни изоляции
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public List<User> readCommittedExample() {
        return entityManager.createQuery("SELECT u FROM User u", User.class)
            .getResultList();
    }
    
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void serializableExample() {
        // Самый строгий уровень изоляции
        // Предотвращает phantom reads, но может вызвать deadlocks
    }
    
    // Read-only транзакции для оптимизации
    @Transactional(readOnly = true)
    public List<User> findAllUsersReadOnly() {
        // Hibernate может оптимизировать read-only транзакции
        // - отключить dirty checking
        // - использовать read-only connection
        return entityManager.createQuery("SELECT u FROM User u", User.class)
            .getResultList();
    }
}

Частые вопросы на собеседовании

Q: В чем разница между Session и EntityManager? A: Session — Hibernate-специфичный API, EntityManager — стандартный JPA API. EntityManager является оберткой над Session. В Spring Data JPA используется EntityManager.

Q: Что такое N+1 проблема и как ее решить? A: N+1 — это когда для загрузки N связанных объектов выполняется N+1 запросов (1 основной + N для каждой связи). Решения: JOIN FETCH, @EntityGraph, @BatchSize, eager loading.

Q: В чем разница между persist() и merge()? A: persist() работает только с transient entities, создает новую запись. merge() работает с detached entities, обновляет существующую запись или создает новую.

Q: Как работает кэш первого и второго уровня? A: Первый уровень — автоматический кэш Session, гарантирует уникальность объектов в сессии. Второй уровень — опциональный кэш SessionFactory, разделяется между сессиями.

Q: Что такое LazyInitializationException и как его избежать? A: Исключение при обращении к lazy-загруженной коллекции вне активной сессии. Решения: eager loading, @Transactional на методе, JOIN FETCH, initialize в активной сессии.

Q: В чем разница между load() и get()? A: get() возвращает null если объект не найден, load() выбрасывает ObjectNotFoundException. load() может вернуть proxy без обращения к БД, get() всегда обращается к БД.

Spring Async & Scheduling

1. Основы асинхронного выполнения

Включение Async поддержки

@Configuration
@EnableAsync  // Включает поддержку @Async
public class AsyncConfig {
    
    // Кастомный executor для async методов
    @Bean("taskExecutor")
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);        // Базовое количество потоков
        executor.setMaxPoolSize(16);        // Максимальное количество потоков
        executor.setQueueCapacity(100);     // Размер очереди задач
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return executor;
    }
}

// Или через properties
# spring.task.execution.pool.core-size=4
# spring.task.execution.pool.max-size=16
# spring.task.execution.pool.queue-capacity=100

Зачем нужен Async:

  • Неблокирующее выполнение - основной поток не ждет завершения операции
  • Параллельная обработка - несколько задач выполняются одновременно
  • Улучшение отзывчивости - пользователь не ждет долгих операций
  • Масштабируемость - эффективное использование ресурсов

Базовое использование @Async

@Service
public class EmailService {
    
    @Async  // Метод выполняется асинхронно
    public void sendEmail(String to, String subject, String body) {
        // Имитация долгой операции отправки email
        try {
            Thread.sleep(2000);
            System.out.println("Email sent to: " + to);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    @Async("taskExecutor")  // Использует конкретный executor
    public CompletableFuture<String> sendEmailWithResult(String to) {
        try {
            Thread.sleep(1000);
            String result = "Email sent successfully to " + to;
            return CompletableFuture.completedFuture(result);
        } catch (InterruptedException e) {
            return CompletableFuture.failedFuture(e);
        }
    }
}

@RestController
public class UserController {
    
    @Autowired
    private EmailService emailService;
    
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
        User user = userService.create(request);
        
        // Email отправляется асинхронно, не блокирует ответ
        emailService.sendEmail(user.getEmail(), "Welcome", "Welcome to our app!");
        
        return ResponseEntity.ok(user);
    }
}

2. Возвращаемые типы Async методов

Void - fire-and-forget

@Async
public void processData(List<Data> data) {
    // Просто запускаем и забываем
    // Никого не интересует результат
    data.forEach(this::processItem);
}

CompletableFuture - получение результата

@Service
public class DataProcessingService {
    
    @Async
    public CompletableFuture<String> processFileAsync(String filename) {
        try {
            // Длительная обработка файла
            String result = processFile(filename);
            return CompletableFuture.completedFuture(result);
        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }
    
    @Async
    public CompletableFuture<Integer> calculateAsync(int number) {
        return CompletableFuture.supplyAsync(() -> {
            // Сложные вычисления
            return number * number;
        });
    }
}

// Использование
@Service
public class BusinessService {
    
    public void processMultipleFiles(List<String> filenames) {
        List<CompletableFuture<String>> futures = filenames.stream()
            .map(dataService::processFileAsync)
            .collect(Collectors.toList());
        
        // Ждем завершения всех задач
        CompletableFuture<Void> allTasks = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0]));
        
        allTasks.thenRun(() -> {
            // Все файлы обработаны
            System.out.println("All files processed");
        });
    }
}

ListenableFuture - альтернатива CompletableFuture

@Async
public ListenableFuture<String> processAsync(String input) {
    return new AsyncResult<>("Processed: " + input);
}

// Использование с callbacks
ListenableFuture<String> future = service.processAsync("data");
future.addCallback(
    result -> System.out.println("Success: " + result),
    failure -> System.err.println("Error: " + failure.getMessage())
);

3. Конфигурация Executors

ThreadPoolTaskExecutor - основной executor

@Configuration
public class ExecutorConfig {
    
    @Bean("emailExecutor")
    public TaskExecutor emailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);           // Минимум потоков
        executor.setMaxPoolSize(8);            // Максимум потоков
        executor.setQueueCapacity(50);         // Размер очереди
        executor.setKeepAliveSeconds(60);      // Время жизни лишних потоков
        executor.setThreadNamePrefix("Email-");
        executor.setRejectedExecutionHandler(  // Что делать при переполнении
            new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
    
    @Bean("heavyTaskExecutor")
    public TaskExecutor heavyTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("Heavy-");
        executor.initialize();
        return executor;
    }
}

Политики при переполнении (RejectedExecutionHandler):

  • CallerRunsPolicy - выполняет в вызывающем потоке
  • AbortPolicy - выбрасывает исключение (по умолчанию)
  • DiscardPolicy - молча отбрасывает задачу
  • DiscardOldestPolicy - отбрасывает самую старую задачу

Использование разных executors

@Service
public class TaskService {
    
    @Async("emailExecutor")
    public void sendNotification(String message) {
        // Легкие email задачи
    }
    
    @Async("heavyTaskExecutor")
    public CompletableFuture<String> processLargeFile(String filename) {
        // Тяжелые задачи обработки
        return CompletableFuture.completedFuture("Processed");
    }
    
    @Async  // Использует default executor
    public void simpleTask() {
        // Простые задачи
    }
}

4. Scheduling (Планировщик задач)

Включение Scheduling

@Configuration
@EnableScheduling  // Включает поддержку @Scheduled
public class SchedulingConfig {
    
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("Scheduled-");
        scheduler.initialize();
        return scheduler;
    }
}

Типы расписаний

Fixed Rate - фиксированный интервал

@Component
public class ScheduledTasks {
    
    // Выполняется каждые 5 секунд (от начала предыдущего выполнения)
    @Scheduled(fixedRate = 5000)
    public void updateStatistics() {
        System.out.println("Updating statistics at: " + LocalDateTime.now());
    }
    
    // Выполняется каждую минуту с задержкой старта 10 секунд
    @Scheduled(fixedRate = 60000, initialDelay = 10000)
    public void syncData() {
        System.out.println("Syncing data");
    }
}

Fixed Delay - фиксированная задержка

@Component
public class MaintenanceTasks {
    
    // Выполняется через 30 секунд после завершения предыдущего выполнения
    @Scheduled(fixedDelay = 30000)
    public void cleanup() {
        System.out.println("Cleaning up temporary files");
        // Даже если cleanup займет 10 секунд, следующий запуск будет через 30 сек после завершения
    }
    
    // С использованием переменных
    @Scheduled(fixedDelayString = "${app.cleanup.interval:300000}")  // 5 минут по умолчанию
    public void periodicCleanup() {
        System.out.println("Periodic cleanup");
    }
}

Cron expressions - сложные расписания

@Component
public class BusinessTasks {
    
    // Каждый день в 2:30 AM
    @Scheduled(cron = "0 30 2 * * *")
    public void dailyBackup() {
        System.out.println("Running daily backup");
    }
    
    // Каждую минуту в рабочие дни (пн-пт) с 9 до 17
    @Scheduled(cron = "0 * 9-17 * * MON-FRI")
    public void businessHoursTask() {
        System.out.println("Business hours task");
    }
    
    // Первое число каждого месяца в 0:00
    @Scheduled(cron = "0 0 0 1 * *")
    public void monthlyReport() {
        System.out.println("Generating monthly report");
    }
    
    // Настраиваемый cron через properties
    @Scheduled(cron = "${app.report.schedule:0 0 8 * * MON}")  // По умолчанию каждый понедельник в 8:00
    public void weeklyReport() {
        System.out.println("Weekly report");
    }
}

Cron expression format: секунды минуты часы день_месяца месяц день_недели

  • * - любое значение
  • ? - любое значение (только для дня месяца и дня недели)
  • - - диапазон (например, 9-17)
  • , - список значений (например, MON,WED,FRI)
  • / - интервал (например, */5 = каждые 5)

5. Практические примеры

Async обработка заказов

@Service
public class OrderProcessingService {
    
    @Async("orderExecutor")
    public CompletableFuture<Void> processOrder(Order order) {
        return CompletableFuture.runAsync(() -> {
            // Валидация заказа
            validateOrder(order);
            
            // Резервирование товаров
            reserveItems(order);
            
            // Обработка платежа
            processPayment(order);
            
            // Отправка уведомлений
            sendNotifications(order);
        });
    }
    
    @Async
    public void sendOrderConfirmation(Order order) {
        emailService.sendEmail(
            order.getCustomerEmail(),
            "Order Confirmation",
            "Your order #" + order.getId() + " has been confirmed"
        );
    }
    
    @Async
    public void updateInventory(List<OrderItem> items) {
        items.forEach(item -> {
            inventoryService.decreaseStock(item.getProductId(), item.getQuantity());
        });
    }
}

@RestController
public class OrderController {
    
    @PostMapping("/orders")
    public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
        Order order = orderService.createOrder(request);
        
        // Запускаем асинхронную обработку
        orderProcessingService.processOrder(order);
        orderProcessingService.sendOrderConfirmation(order);
        orderProcessingService.updateInventory(order.getItems());
        
        return ResponseEntity.ok(new OrderResponse(order.getId(), "Order created"));
    }
}

Scheduled задачи мониторинга

@Component
public class MonitoringTasks {
    
    private final MetricRegistry metricRegistry;
    
    // Каждые 30 секунд
    @Scheduled(fixedRate = 30000)
    public void collectSystemMetrics() {
        double cpuUsage = systemMonitor.getCpuUsage();
        long memoryUsage = systemMonitor.getMemoryUsage();
        
        metricRegistry.gauge("system.cpu.usage", () -> cpuUsage);
        metricRegistry.gauge("system.memory.usage", () -> memoryUsage);
    }
    
    // Каждые 5 минут
    @Scheduled(fixedRate = 300000)
    public void checkApplicationHealth() {
        boolean dbHealthy = healthChecker.isDatabaseHealthy();
        boolean redisHealthy = healthChecker.isRedisHealthy();
        
        if (!dbHealthy || !redisHealthy) {
            alertService.sendHealthAlert("System components unhealthy");
        }
    }
    
    // Каждый час
    @Scheduled(cron = "0 0 * * * *")
    public void cleanupExpiredSessions() {
        int cleanedSessions = sessionService.cleanupExpiredSessions();
        log.info("Cleaned up {} expired sessions", cleanedSessions);
    }
    
    // Каждый день в 3:00 AM
    @Scheduled(cron = "0 0 3 * * *")
    public void generateDailyReport() {
        DailyReport report = reportService.generateDailyReport(LocalDate.now().minusDays(1));
        reportService.saveReport(report);
        
        // Отправляем отчет асинхронно
        notificationService.sendDailyReportAsync(report);
    }
}

Batch обработка с async

@Service
public class DataMigrationService {
    
    private final int BATCH_SIZE = 1000;
    
    @Async("migrationExecutor")
    public CompletableFuture<Integer> migrateBatch(List<LegacyData> batch, int batchNumber) {
        try {
            List<NewData> convertedData = batch.stream()
                .map(this::convertData)
                .collect(Collectors.toList());
            
            newDataRepository.saveAll(convertedData);
            
            log.info("Migrated batch {} with {} records", batchNumber, batch.size());
            return CompletableFuture.completedFuture(batch.size());
            
        } catch (Exception e) {
            log.error("Failed to migrate batch {}", batchNumber, e);
            return CompletableFuture.failedFuture(e);
        }
    }
    
    public void migrateAllData() {
        List<LegacyData> allData = legacyDataRepository.findAll();
        
        // Разбиваем на batches и обрабатываем параллельно
        List<CompletableFuture<Integer>> migrationTasks = new ArrayList<>();
        
        for (int i = 0; i < allData.size(); i += BATCH_SIZE) {
            List<LegacyData> batch = allData.subList(i, Math.min(i + BATCH_SIZE, allData.size()));
            CompletableFuture<Integer> task = migrateBatch(batch, i / BATCH_SIZE + 1);
            migrationTasks.add(task);
        }
        
        // Ждем завершения всех задач
        CompletableFuture<Void> allTasks = CompletableFuture.allOf(
            migrationTasks.toArray(new CompletableFuture[0]));
        
        allTasks.thenRun(() -> {
            int totalMigrated = migrationTasks.stream()
                .mapToInt(CompletableFuture::join)
                .sum();
            log.info("Migration completed. Total records migrated: {}", totalMigrated);
        });
    }
}

6. Error Handling и Exception Management

Обработка ошибок в Async методах

@Component
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    
    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        log.error("Async method {} failed with parameters {}", 
                 method.getName(), Arrays.toString(params), ex);
        
        // Отправляем alert о проблеме
        alertService.sendAsyncError(method.getName(), ex.getMessage());
    }
}

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncExceptionHandler();
    }
}

Retry механизм для Scheduled задач

@Component
public class ResilientScheduledTasks {
    
    @Retryable(
        value = {TransientException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2)
    )
    @Scheduled(fixedRate = 60000)
    public void unreliableTask() {
        try {
            externalService.syncData();
        } catch (Exception e) {
            log.error("Failed to sync data", e);
            throw new TransientException("Sync failed", e);
        }
    }
    
    @Recover
    public void recoverFromSyncFailure(TransientException ex) {
        log.error("All retry attempts failed for data sync", ex);
        alertService.sendAlert("Data sync failed after all retries");
    }
}

7. Monitoring и Best Practices

Мониторинг Async задач

@Component
public class AsyncMonitoringService {
    
    private final MeterRegistry meterRegistry;
    private final Counter asyncTaskCounter;
    private final Timer asyncTaskTimer;
    
    public AsyncMonitoringService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.asyncTaskCounter = Counter.builder("async.tasks.total")
            .description("Total async tasks executed")
            .register(meterRegistry);
        this.asyncTaskTimer = Timer.builder("async.tasks.duration")
            .description("Async task execution time")
            .register(meterRegistry);
    }
    
    @EventListener
    public void handleAsyncTaskStart(AsyncTaskStartEvent event) {
        asyncTaskCounter.increment();
    }
    
    @EventListener
    public void handleAsyncTaskComplete(AsyncTaskCompleteEvent event) {
        asyncTaskTimer.record(event.getDuration(), TimeUnit.MILLISECONDS);
    }
}

// Мониторинг через Actuator
@Component
public class ThreadPoolMetrics {
    
    @Scheduled(fixedRate = 30000)
    public void reportThreadPoolStats() {
        ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) applicationContext.getBean("taskExecutor");
        
        Gauge.builder("threadpool.active.threads")
            .register(meterRegistry, executor, ThreadPoolTaskExecutor::getActiveCount);
            
        Gauge.builder("threadpool.pool.size")
            .register(meterRegistry, executor, ThreadPoolTaskExecutor::getPoolSize);
            
        Gauge.builder("threadpool.queue.size")
            .register(meterRegistry, executor, e -> e.getThreadPoolExecutor().getQueue().size());
    }
}

Configuration Properties для настройки

@ConfigurationProperties(prefix = "app.async")
@Data
public class AsyncProperties {
    
    private ThreadPool threadPool = new ThreadPool();
    private Scheduling scheduling = new Scheduling();
    
    @Data
    public static class ThreadPool {
        private int coreSize = 4;
        private int maxSize = 16;
        private int queueCapacity = 100;
        private int keepAliveSeconds = 60;
        private String threadNamePrefix = "Async-";
    }
    
    @Data
    public static class Scheduling {
        private int poolSize = 10;
        private String threadNamePrefix = "Scheduled-";
    }
}

# application.yml
app:
  async:
    thread-pool:
      core-size: 8
      max-size: 32
      queue-capacity: 200
    scheduling:
      pool-size: 15

Best Practices

1. Правильная настройка Thread Pool:

// Для CPU-intensive задач: количество потоков ≈ количество ядер
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());

// Для I/O-intensive задач: больше потоков
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);

2. Избегание Self-Invocation:

@Service
public class TaskService {
    
    @Autowired
    private TaskService self;  // Инжектим себя
    
    public void publicMethod() {
        // НЕ РАБОТАЕТ: this.asyncMethod() - self-invocation
        // РАБОТАЕТ: через proxy
        self.asyncMethod();
    }
    
    @Async
    public void asyncMethod() {
        // async logic
    }
}

3. Graceful Shutdown:

@Configuration
public class AsyncConfig {
    
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // ... настройки ...
        executor.setWaitForTasksToCompleteOnShutdown(true);  // Ждать завершения задач
        executor.setAwaitTerminationSeconds(30);             // Максимум 30 секунд
        return executor;
    }
}

4. Управление ресурсами:

@Component
public class ResourceManagementTasks {
    
    @Scheduled(fixedRate = 300000)  // Каждые 5 минут
    @SchedulerLock(name = "cleanupTask", lockAtMostFor = "4m", lockAtLeastFor = "1m")
    public void cleanupResources() {
        // ShedLock предотвращает одновременное выполнение в кластере
        tempFileService.cleanupOldFiles();
    }
    
    @PreDestroy
    public void shutdown() {
        // Корректная остановка при shutdown приложения
        log.info("Shutting down async services");
    }
}

Эта шпаргалка поможет эффективно использовать асинхронное выполнение и планировщик задач в Spring приложениях.

Spring Security

Что такое Spring Security

Spring Security — это мощный и настраиваемый фреймворк для аутентификации и авторизации в Spring-приложениях. Обеспечивает защиту от основных уязвимостей: CSRF, Session Fixation, Clickjacking и других.

Основные концепции:

  • Authentication — кто пользователь (логин/пароль, JWT, OAuth2)
  • Authorization — что пользователь может делать (роли, права доступа)
  • Principal — текущий аутентифицированный пользователь
  • Authority/Role — права доступа пользователя
  • SecurityContext — контекст безопасности текущего запроса

Подключение к проекту

Maven зависимости

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Для JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>

<!-- Для OAuth2 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

Пояснение: При добавлении spring-boot-starter-security автоматически включается базовая HTTP Basic аутентификация. По умолчанию создается пользователь user с сгенерированным паролем в логах.


Базовая конфигурация

Основной конфигурационный класс

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/login", "/register").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/login?error")
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)
                .clearAuthentication(true)
            )
            .csrf(csrf -> csrf.disable()); // Только для API!
        
        return http.build();
    }
}

Пояснение:

  • requestMatchers() — определяет URL-паттерны и права доступа
  • formLogin() — включает форму входа
  • logout() — настройка выхода из системы
  • csrf().disable() — отключение CSRF (только для REST API)

UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
        
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword()) // Должен быть закодирован!
            .authorities(user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                .collect(Collectors.toList()))
            .accountExpired(!user.isAccountNonExpired())
            .accountLocked(!user.isAccountNonLocked())
            .credentialsExpired(!user.isCredentialsNonExpired())
            .disabled(!user.isEnabled())
            .build();
    }
}

Пояснение: UserDetailsService загружает информацию о пользователе из базы данных. Spring Security автоматически вызывает этот сервис при аутентификации. Важно: пароли должны быть захешированы!

Кодирование паролей

@Configuration
public class PasswordConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // Strength 12
    }
    
    @Bean
    public AuthenticationManager authManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
@Service
public class UserService {
    
    private final PasswordEncoder passwordEncoder;
    
    public void createUser(String username, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        // Сохранение пользователя с закодированным паролем
        userRepository.save(new User(username, encodedPassword));
    }
}

Пояснение: BCryptPasswordEncoder — рекомендуемый способ хеширования паролей. Каждый раз генерирует уникальный hash, защищая от rainbow table атак.


Аутентификация

Form-based аутентификация

@Configuration
public class FormLoginConfig {
    
    @Bean
    public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/custom-login")
                .loginProcessingUrl("/perform-login") // URL для обработки формы
                .usernameParameter("email") // Кастомное имя поля
                .passwordParameter("password")
                .defaultSuccessUrl("/home", true) // true = всегда редирект
                .failureHandler(customAuthenticationFailureHandler())
                .successHandler(customAuthenticationSuccessHandler())
            );
        return http.build();
    }
    
    @Bean
    public AuthenticationFailureHandler customAuthenticationFailureHandler() {
        return (request, response, exception) -> {
            request.getSession().setAttribute("error", "Invalid credentials");
            response.sendRedirect("/custom-login?error");
        };
    }
}

Кастомная форма входа

<!-- custom-login.html -->
<form th:action="@{/perform-login}" method="post">
    <input type="email" name="email" placeholder="Email" required>
    <input type="password" name="password" placeholder="Password" required>
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    <button type="submit">Login</button>
</form>

Пояснение: Кастомная форма позволяет полностью контролировать внешний вид. Обязательно включать CSRF-токен для защиты от атак.

HTTP Basic аутентификация

@Bean
public SecurityFilterChain basicAuthFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .requestMatchers("/api/**").authenticated()
        )
        .httpBasic(basic -> basic
            .realmName("My API")
            .authenticationEntryPoint(customBasicAuthEntryPoint())
        )
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );
    return http.build();
}

Пояснение: HTTP Basic подходит для API, где клиент отправляет Authorization: Basic base64(username:password) заголовок. STATELESS означает, что сессии не создаются.


JWT Authentication

JWT конфигурация

@Component
public class JwtUtils {
    
    private final String jwtSecret = "mySecretKey";
    private final int jwtExpirationMs = 86400000; // 24 часа
    
    public String generateJwtToken(Authentication authentication) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        
        return Jwts.builder()
            .setSubject(userPrincipal.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(new Date().getTime() + jwtExpirationMs))
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }
    
    public String getUserNameFromJwtToken(String token) {
        return Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
    
    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException | MalformedJwtException | ExpiredJwtException | 
                 UnsupportedJwtException | IllegalArgumentException e) {
            logger.error("Invalid JWT token: {}", e.getMessage());
        }
        return false;
    }
}

JWT фильтр

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtUtils jwtUtils;
    private final UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
        
        String jwt = parseJwt(request);
        if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
            String username = jwtUtils.getUserNameFromJwtToken(jwt);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");
        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
            return headerAuth.substring(7);
        }
        return null;
    }
}

JWT конфигурация Security

@Configuration
public class JwtSecurityConfig {
    
    @Bean
    public SecurityFilterChain jwtFilterChain(HttpSecurity http, 
                                            JwtAuthenticationFilter jwtAuthFilter) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
            
        return http.build();
    }
}

Пояснение: JWT токены содержат всю необходимую информацию, поэтому сессии не нужны (STATELESS). Фильтр проверяет токен в каждом запросе и устанавливает аутентификацию.

Контроллер аутентификации

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    private final AuthenticationManager authManager;
    private final JwtUtils jwtUtils;
    
    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
        Authentication authentication = authManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(), 
                loginRequest.getPassword())
        );
        
        String jwt = jwtUtils.generateJwtToken(authentication);
        UserPrincipal userDetails = (UserPrincipal) authentication.getPrincipal();
        
        return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getUsername()));
    }
}

OAuth2 и Social Login

OAuth2 конфигурация

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: profile, email
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: user:email
        provider:
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/auth
            token-uri: https://oauth2.googleapis.com/token
            user-info-uri: https://www.googleapis.com/oauth2/v2/userinfo

OAuth2 Security конфигурация

@Configuration
public class OAuth2Config {
    
    @Bean
    public SecurityFilterChain oauth2FilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login", "/error").permitAll()
                .anyRequest().authenticated())
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService())
                )
                .successHandler(oauth2SuccessHandler())
            );
        return http.build();
    }
    
    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
        return new CustomOAuth2UserService();
    }
}

Кастомный OAuth2 User Service

@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    
    private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
    
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = delegate.loadUser(userRequest);
        
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String email = oAuth2User.getAttribute("email");
        String name = oAuth2User.getAttribute("name");
        
        // Создание или обновление пользователя в БД
        User user = userService.processOAuth2User(email, name, registrationId);
        
        return new CustomOAuth2User(oAuth2User, user);
    }
}

Пояснение: OAuth2 позволяет пользователям входить через Google, GitHub и другие провайдеры. Spring Security автоматически обрабатывает OAuth2 flow, вам нужно только настроить клиентские данные.


Авторизация

Аннотации безопасности

@RestController
@PreAuthorize("hasRole('USER')") // На уровне класса
public class UserController {
    
    @GetMapping("/profile")
    @PreAuthorize("hasRole('USER')") // Базовая роль
    public UserProfile getProfile() {
        return userService.getCurrentUserProfile();
    }
    
    @PostMapping("/admin/users")
    @PreAuthorize("hasRole('ADMIN')") // Только админы
    public User createUser(@RequestBody CreateUserRequest request) {
        return userService.createUser(request);
    }
    
    @GetMapping("/posts/{id}")
    @PreAuthorize("@postService.isOwner(#id, authentication.name) or hasRole('ADMIN')")
    public Post getPost(@PathVariable Long id) {
        return postService.getPost(id);
    }
    
    @PutMapping("/users/{id}")
    @PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
    public User updateUser(@PathVariable Long id, @RequestBody UpdateUserRequest request) {
        return userService.updateUser(id, request);
    }
}

Пояснение:

  • @PreAuthorize — проверка до выполнения метода
  • @PostAuthorize — проверка после выполнения
  • authentication.name — имя текущего пользователя
  • Можно вызывать методы бинов для сложной логики

Включение аннотаций

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
    
    @Bean
    public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
        DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(new CustomPermissionEvaluator());
        return handler;
    }
}

Кастомный Permission Evaluator

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    
    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        if (authentication == null || targetDomainObject == null || !(permission instanceof String)) {
            return false;
        }
        
        String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
        return hasPrivilege(authentication, targetType, permission.toString().toUpperCase());
    }
    
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        if (authentication == null || targetType == null || !(permission instanceof String)) {
            return false;
        }
        return hasPrivilege(authentication, targetType.toUpperCase(), permission.toString().toUpperCase());
    }
    
    private boolean hasPrivilege(Authentication auth, String targetType, String permission) {
        return auth.getAuthorities().stream()
            .anyMatch(grantedAuth -> 
                grantedAuth.getAuthority().equals(targetType + "_" + permission) ||
                grantedAuth.getAuthority().equals("ROLE_ADMIN"));
    }
}

Использование hasPermission

@PreAuthorize("hasPermission(#post, 'EDIT')")
public Post updatePost(@PathVariable Long id, @RequestBody Post post) {
    return postService.updatePost(id, post);
}

@PreAuthorize("hasPermission(#id, 'POST', 'DELETE')")
public void deletePost(@PathVariable Long id) {
    postService.deletePost(id);
}

CSRF Protection

Настройка CSRF

@Bean
public SecurityFilterChain csrfFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .ignoringRequestMatchers("/api/public/**") // API без CSRF
        )
        .authorizeHttpRequests(auth -> auth
            .anyRequest().authenticated()
        );
    return http.build();
}

CSRF в формах

<form method="post" action="/transfer">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    <input type="text" name="amount" placeholder="Amount">
    <button type="submit">Transfer</button>
</form>

CSRF для AJAX

// Получение токена из meta-тега
const token = document.querySelector("meta[name='_csrf']").getAttribute("content");
const header = document.querySelector("meta[name='_csrf_header']").getAttribute("content");

fetch('/api/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        [header]: token
    },
    body: JSON.stringify(data)
});

Пояснение: CSRF защищает от атак, когда злоумышленник заставляет пользователя выполнить нежелательное действие. Для REST API CSRF обычно отключается, так как используются токены.


Session Management

Конфигурация сессий

@Bean
public SecurityFilterChain sessionFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .maximumSessions(1) // Одна сессия на пользователя
            .maxSessionsPreventsLogin(false) // Новая сессия вытесняет старую
            .sessionRegistry(sessionRegistry())
            .and()
            .invalidSessionUrl("/login?expired")
            .sessionFixation().migrateSession() // Защита от Session Fixation
        );
    return http.build();
}

@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

Управление сессиями

@RestController
public class SessionController {
    
    private final SessionRegistry sessionRegistry;
    
    @PostMapping("/admin/sessions/invalidate/{username}")
    @PreAuthorize("hasRole('ADMIN')")
    public void invalidateUserSessions(@PathVariable String username) {
        sessionRegistry.getAllPrincipals().stream()
            .filter(principal -> principal.toString().equals(username))
            .forEach(principal -> {
                sessionRegistry.getAllSessions(principal, false)
                    .forEach(SessionInformation::expireNow);
            });
    }
    
    @GetMapping("/admin/active-sessions")
    public List<SessionInfo> getActiveSessions() {
        return sessionRegistry.getAllPrincipals().stream()
            .flatMap(principal -> sessionRegistry.getAllSessions(principal, false).stream())
            .map(session -> new SessionInfo(
                session.getPrincipal().toString(),
                session.getLastRequest(),
                session.isExpired()
            ))
            .collect(Collectors.toList());
    }
}

Пояснение: Session Management контролирует количество одновременных сессий пользователя. migrateSession() создает новую сессию при входе, защищая от Session Fixation атак.


Безопасность заголовков

Настройка заголовков

@Bean
public SecurityFilterChain headersFilterChain(HttpSecurity http) throws Exception {
    http
        .headers(headers -> headers
            .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny) // X-Frame-Options
            .contentTypeOptions(HeadersConfigurer.ContentTypeOptionsConfig::and) // X-Content-Type-Options
            .httpStrictTransportSecurity(hstsConfig -> hstsConfig
                .maxAgeInSeconds(31536000) // 1 год
                .includeSubdomains(true)
                .preload(true)
            )
            .contentSecurityPolicy("default-src 'self'; script-src 'self' 'unsafe-inline'")
        );
    return http.build();
}

Пояснение: Заголовки безопасности защищают от различных атак:

  • X-Frame-Options — защита от Clickjacking
  • HSTS — принудительное использование HTTPS
  • CSP — защита от XSS атак

Получение информации о пользователе

В контроллерах

@RestController
public class UserInfoController {
    
    @GetMapping("/current-user")
    public UserInfo getCurrentUser(Authentication authentication) {
        return UserInfo.from(authentication);
    }
    
    @GetMapping("/current-user-principal")
    public UserInfo getCurrentUser(Principal principal) {
        return userService.findByUsername(principal.getName());
    }
    
    @GetMapping("/current-user-security-context")
    public UserInfo getCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return userService.findByUsername(auth.getName());
    }
    
    @GetMapping("/user-details")
    public UserDetails getUserDetails(@AuthenticationPrincipal UserDetails userDetails) {
        return userDetails;
    }
}

В сервисах

@Service
public class PostService {
    
    public List<Post> getCurrentUserPosts() {
        String username = SecurityContextHolder.getContext()
            .getAuthentication()
            .getName();
        return postRepository.findByUsername(username);
    }
    
    public boolean isOwner(Long postId, String username) {
        return postRepository.findById(postId)
            .map(post -> post.getAuthor().getUsername().equals(username))
            .orElse(false);
    }
}

Обработка ошибок безопасности

Кастомные страницы ошибок

@Configuration
public class SecurityErrorConfig {
    
    @Bean
    public SecurityFilterChain errorHandlingFilterChain(HttpSecurity http) throws Exception {
        http
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(customAuthenticationEntryPoint())
                .accessDeniedHandler(customAccessDeniedHandler())
            );
        return http.build();
    }
    
    @Bean
    public AuthenticationEntryPoint customAuthenticationEntryPoint() {
        return (request, response, authException) -> {
            if (request.getRequestURI().startsWith("/api/")) {
                response.setContentType("application/json");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("{\"error\":\"Unauthorized\",\"message\":\"" + 
                    authException.getMessage() + "\"}");
            } else {
                response.sendRedirect("/login");
            }
        };
    }
    
    @Bean
    public AccessDeniedHandler customAccessDeniedHandler() {
        return (request, response, accessDeniedException) -> {
            if (request.getRequestURI().startsWith("/api/")) {
                response.setContentType("application/json");
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.getWriter().write("{\"error\":\"Access Denied\"}");
            } else {
                response.sendRedirect("/access-denied");
            }
        };
    }
}

Лучшие практики

1. Конфигурация для разных профилей

# application-dev.yml
spring:
  security:
    user:
      name: admin
      password: admin
      roles: ADMIN
logging:
  level:
    org.springframework.security: DEBUG

# application-prod.yml
spring:
  security:
    require-ssl: true
server:
  ssl:
    enabled: true

2. Безопасное хранение секретов

@Component
public class SecurityProperties {
    
    @Value("${app.jwt.secret}")
    private String jwtSecret;
    
    @Value("${app.jwt.expiration:86400000}")
    private int jwtExpiration;
    
    // Getters
}
# Используйте переменные окружения
app:
  jwt:
    secret: ${JWT_SECRET:defaultSecretForDev}
    expiration: ${JWT_EXPIRATION:86400000}

3. Аудит безопасности

@Component
public class SecurityAuditListener {
    
    @EventListener
    public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        String ip = getClientIP();
        auditService.logSuccessfulLogin(username, ip);
    }
    
    @EventListener
    public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        String username = event.getAuthentication().getName();
        String ip = getClientIP();
        auditService.logFailedLogin(username, ip, event.getException().getMessage());
    }
}

4. Rate Limiting для аутентификации

@Component
public class LoginAttemptService {
    
    private final int MAX_ATTEMPTS = 5;
    private final LoadingCache<String, Integer> attemptsCache;
    
    public LoginAttemptService() {
        attemptsCache = CacheBuilder.newBuilder()
            .expireAfterWrite(15, TimeUnit.MINUTES)
            .build(new CacheLoader<String, Integer>() {
                public Integer load(String key) {
                    return 0;
                }
            });
    }
    
    public void loginFailed(String key) {
        int attempts = attemptsCache.getUnchecked(key);
        attempts++;
        attemptsCache.put(key, attempts);
    }
    
    public boolean isBlocked(String key) {
        return attemptsCache.getUnchecked(key) >= MAX_ATTEMPTS;
    }
}

Заключение

Spring Security предоставляет мощные инструменты для защиты приложений. Ключевые принципы:

  1. Начинайте с простой конфигурации — добавляйте сложность постепенно
  2. Всегда хешируйте пароли — используйте BCryptPasswordEncoder
  3. Правильно настраивайте CSRF — включайте для форм, отключайте для API
  4. Используйте HTTPS в продакшене — особенно для аутентификации
  5. Логируйте события безопасности — для мониторинга и аудита
  6. Применяйте принцип минимальных привилегий — давайте только необходимые права

Правильная настройка Spring Security критически важна для безопасности приложения. Регулярно обновляйте зависимости и следите за уязвимостями.

Spring Integration / Messaging

1. Основы Spring Integration

Что такое Spring Integration

Spring Integration - фреймворк для построения интеграционных решений на основе Enterprise Integration Patterns (EIP):

  • Message-driven архитектура - обмен сообщениями между компонентами
  • Channels - каналы для передачи сообщений
  • Endpoints - точки входа/выхода сообщений
  • Transformers - преобразование сообщений
  • Filters - фильтрация сообщений

Maven зависимости

<!-- Core Spring Integration -->
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-core</artifactId>
</dependency>

<!-- File operations -->
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-file</artifactId>
</dependency>

<!-- HTTP operations -->
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-http</artifactId>
</dependency>

<!-- JMS messaging -->
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-jms</artifactId>
</dependency>

Включение поддержки

@Configuration
@EnableIntegration  // Включает Spring Integration
public class IntegrationConfig {
}

2. Channels (Каналы)

Типы каналов

@Configuration
public class ChannelConfig {
    
    // Direct Channel - синхронное выполнение в том же потоке
    @Bean
    public MessageChannel directChannel() {
        return MessageChannels.direct().get();
    }
    
    // Queue Channel - асинхронное выполнение с очередью
    @Bean
    public MessageChannel queueChannel() {
        return MessageChannels.queue(100).get();  // Размер очереди 100
    }
    
    // PublishSubscribe Channel - отправка всем подписчикам
    @Bean
    public MessageChannel pubSubChannel() {
        return MessageChannels.publishSubscribe().get();
    }
    
    // Executor Channel - асинхронное выполнение с executor
    @Bean
    public MessageChannel executorChannel() {
        return MessageChannels.executor(taskExecutor()).get();
    }
}

Когда использовать каждый тип:

  • Direct - простая синхронная обработка
  • Queue - асинхронная обработка с буферизацией
  • PublishSubscribe - рассылка одного сообщения нескольким получателям
  • Executor - асинхронная обработка с кастомным thread pool

Interceptors для каналов

@Component
public class LoggingChannelInterceptor implements ChannelInterceptor {
    
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        log.info("Sending message: {} to channel: {}", message.getPayload(), channel);
        return message;
    }
    
    @Override
    public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
        log.info("Message sent: {} success: {}", message.getPayload(), sent);
    }
}

@Bean
public MessageChannel monitoredChannel() {
    return MessageChannels.direct()
        .interceptor(new LoggingChannelInterceptor())
        .get();
}

3. Message Endpoints

Service Activator - обработка сообщений

@Component
public class OrderProcessor {
    
    // Простая обработка
    @ServiceActivator(inputChannel = "orderChannel")
    public void processOrder(Order order) {
        log.info("Processing order: {}", order.getId());
        // Бизнес-логика обработки заказа
    }
    
    // Обработка с возвратом результата
    @ServiceActivator(inputChannel = "calculationChannel", outputChannel = "resultChannel")
    public CalculationResult calculate(CalculationRequest request) {
        return calculationService.calculate(request);
    }
    
    // Обработка с Message wrapper
    @ServiceActivator(inputChannel = "messageChannel")
    public void handleMessage(Message<String> message) {
        String payload = message.getPayload();
        MessageHeaders headers = message.getHeaders();
        
        log.info("Received: {} with headers: {}", payload, headers);
    }
}

Gateway - точка входа в integration flow

// Интерфейс для отправки сообщений
@MessagingGateway
public interface OrderGateway {
    
    @Gateway(requestChannel = "orderProcessingChannel")
    void submitOrder(Order order);
    
    @Gateway(requestChannel = "orderStatusChannel", replyChannel = "orderReplyChannel")
    OrderStatus getOrderStatus(Long orderId);
    
    @Gateway(requestChannel = "asyncOrderChannel")
    Future<OrderResult> processOrderAsync(Order order);
}

// Использование
@RestController
public class OrderController {
    
    @Autowired
    private OrderGateway orderGateway;
    
    @PostMapping("/orders")
    public ResponseEntity<String> createOrder(@RequestBody Order order) {
        orderGateway.submitOrder(order);
        return ResponseEntity.ok("Order submitted");
    }
}

Inbound/Outbound Adapters

@Configuration
public class FileIntegrationConfig {
    
    // Inbound - чтение файлов
    @Bean
    @InboundChannelAdapter(value = "fileInputChannel", poller = @Poller(fixedDelay = "5000"))
    public MessageSource<File> fileReadingMessageSource() {
        FileReadingMessageSource source = new FileReadingMessageSource();
        source.setDirectory(new File("/input"));
        source.setFilter(new SimplePatternFileListFilter("*.txt"));
        return source;
    }
    
    // Outbound - запись файлов
    @Bean
    @ServiceActivator(inputChannel = "fileOutputChannel")
    public MessageHandler fileWritingMessageHandler() {
        FileWritingMessageHandler handler = new FileWritingMessageHandler(new File("/output"));
        handler.setFileExistsMode(FileExistsMode.REPLACE);
        handler.setExpectReply(false);
        return handler;
    }
}

4. Message Transformation

Transformer - преобразование сообщений

@Component
public class MessageTransformers {
    
    // Простое преобразование
    @Transformer(inputChannel = "xmlInputChannel", outputChannel = "objectChannel")
    public Order xmlToOrder(String xmlString) {
        return xmlMapper.readValue(xmlString, Order.class);
    }
    
    // Преобразование с заголовками
    @Transformer(inputChannel = "enrichChannel", outputChannel = "enrichedChannel")
    public Message<Order> enrichOrder(Order order) {
        // Обогащение данными
        CustomerInfo customerInfo = customerService.getCustomerInfo(order.getCustomerId());
        
        return MessageBuilder.withPayload(order)
            .setHeader("customerType", customerInfo.getType())
            .setHeader("discountRate", customerInfo.getDiscountRate())
            .build();
    }
    
    // Header Enricher
    @Transformer(inputChannel = "headerEnrichChannel")
    public Message<?> addHeaders(Message<?> message) {
        return MessageBuilder.fromMessage(message)
            .setHeader("timestamp", System.currentTimeMillis())
            .setHeader("source", "integration-service")
            .build();
    }
}

Filter - фильтрация сообщений

@Component
public class MessageFilters {
    
    // Простой фильтр
    @Filter(inputChannel = "orderFilterChannel", outputChannel = "validOrderChannel")
    public boolean isValidOrder(Order order) {
        return order.getTotal() > 0 && order.getCustomerId() != null;
    }
    
    // Фильтр с отклоненными сообщениями
    @Filter(inputChannel = "filterChannel", outputChannel = "acceptedChannel", 
            discardChannel = "rejectedChannel")
    public boolean filterByStatus(Message<Order> message) {
        Order order = message.getPayload();
        return order.getStatus() == OrderStatus.PENDING;
    }
}

Router - маршрутизация сообщений

@Component
public class MessageRouters {
    
    // Простая маршрутизация
    @Router(inputChannel = "orderRouterChannel")
    public String routeOrder(Order order) {
        switch (order.getType()) {
            case PRIORITY:
                return "priorityOrderChannel";
            case STANDARD:
                return "standardOrderChannel";
            default:
                return "defaultOrderChannel";
        }
    }
    
    // Маршрутизация с несколькими каналами
    @Router(inputChannel = "multiRouterChannel")
    public List<String> routeToMultipleChannels(Order order) {
        List<String> channels = new ArrayList<>();
        channels.add("auditChannel");
        
        if (order.getTotal() > 1000) {
            channels.add("highValueChannel");
        }
        
        if (order.isInternational()) {
            channels.add("internationalChannel");
        }
        
        return channels;
    }
}

5. Integration Flows (DSL)

Programmatic configuration

@Configuration
public class IntegrationFlowConfig {
    
    // Простой flow
    @Bean
    public IntegrationFlow orderProcessingFlow() {
        return IntegrationFlows
            .from("orderInputChannel")
            .filter(Order.class, order -> order.getTotal() > 0)
            .transform(Order.class, this::enrichOrder)
            .handle("orderService", "processOrder")
            .get();
    }
    
    // Flow с разветвлением
    @Bean
    public IntegrationFlow fileProcessingFlow() {
        return IntegrationFlows
            .from(Files.inboundAdapter(new File("/input"))
                .patternFilter("*.csv")
                .preventDuplicates(false),
                e -> e.poller(Pollers.fixedDelay(5000)))
            .transform(File.class, file -> fileProcessor.parseFile(file))
            .split()  // Разбиваем на отдельные записи
            .route(Record.class, record -> record.getType(),
                router -> router
                    .subFlowMapping("CUSTOMER", customerFlow())
                    .subFlowMapping("ORDER", orderFlow())
                    .defaultOutputToParentFlow())
            .aggregate()  // Собираем результаты
            .handle("resultService", "saveResults")
            .get();
    }
    
    // HTTP integration flow
    @Bean
    public IntegrationFlow httpRequestFlow() {
        return IntegrationFlows
            .from(Http.inboundGateway("/api/webhook")
                .requestMapping(m -> m.methods(HttpMethod.POST))
                .requestPayloadType(WebhookPayload.class))
            .log(LoggingHandler.Level.INFO, "webhook.received")
            .transform("payload.data")
            .route(String.class, data -> data.contains("order") ? "orderChannel" : "eventChannel")
            .get();
    }
    
    private IntegrationFlow customerFlow() {
        return flow -> flow
            .transform("customerProcessor", "processCustomer")
            .handle("customerService", "saveCustomer");
    }
    
    private IntegrationFlow orderFlow() {
        return flow -> flow
            .transform("orderProcessor", "processOrder")
            .handle("orderService", "saveOrder");
    }
}

Error Handling в flows

@Bean
public IntegrationFlow errorHandlingFlow() {
    return IntegrationFlows
        .from("inputChannel")
        .handle("processingService", "processMessage",
            e -> e.advice(retryAdvice()))  // Retry механизм
        .transform("responseProcessor", "processResponse")
        .<String, String>transform(String::toUpperCase,
            e -> e.advice(exceptionHandlingAdvice()))  // Обработка исключений
        .channel("outputChannel")
        .get();
}

@Bean
public RequestHandlerRetryAdvice retryAdvice() {
    RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
    advice.setRetryTemplate(RetryTemplate.builder()
        .maxAttempts(3)
        .exponentialBackoff(1000, 2, 10000)
        .build());
    return advice;
}

6. Messaging с JMS

JMS Configuration

@Configuration
@EnableJms
public class JmsConfig {
    
    @Bean
    public ConnectionFactory connectionFactory() {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory();
        factory.setBrokerURL("tcp://localhost:61616");
        return factory;
    }
    
    @Bean
    public JmsTemplate jmsTemplate() {
        JmsTemplate template = new JmsTemplate(connectionFactory());
        template.setDefaultDestinationName("orderQueue");
        return template;
    }
}

JMS Integration

@Component
public class JmsIntegration {
    
    // Outbound - отправка в JMS
    @ServiceActivator(inputChannel = "jmsOutboundChannel")
    @Bean
    public MessageHandler jmsOutbound() {
        JmsSendingMessageHandler handler = new JmsSendingMessageHandler(jmsTemplate());
        handler.setDestinationName("orders.queue");
        return handler;
    }
    
    // Inbound - получение из JMS
    @Bean
    public MessageProducer jmsInbound() {
        JmsMessageDrivenEndpoint endpoint = new JmsMessageDrivenEndpoint(
            jmsListenerContainer(), jmsMessageListener());
        endpoint.setOutputChannel(jmsInputChannel());
        return endpoint;
    }
    
    // Message-driven bean
    @JmsListener(destination = "orders.queue")
    public void processOrder(Order order, @Header Map<String, Object> headers) {
        log.info("Processing JMS order: {} with headers: {}", order.getId(), headers);
        orderService.processOrder(order);
    }
}

7. File Integration

File Operations

@Configuration
public class FileIntegrationConfig {
    
    // Мониторинг директории
    @Bean
    public IntegrationFlow fileProcessingFlow() {
        return IntegrationFlows
            .from(Files.inboundAdapter(new File("/watch-dir"))
                .patternFilter("*.json")
                .preventDuplicates(true)
                .useWatchService(true),  // Использует WatchService для мониторинга
                e -> e.poller(Pollers.fixedDelay(1000)))
            .transform(File.class, this::readFileContent)
            .transform(String.class, this::parseJson)
            .route("payload.type", 
                router -> router
                    .subFlowMapping("order", orderProcessingSubflow())
                    .subFlowMapping("customer", customerProcessingSubflow()))
            .get();
    }
    
    // Архивирование обработанных файлов
    @Bean
    public IntegrationFlow fileArchivingFlow() {
        return IntegrationFlows
            .from("processedFileChannel")
            .enrichHeaders(h -> h.header(FileHeaders.FILENAME, "processed_" + 
                new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".json"))
            .handle(Files.outboundAdapter(new File("/archive"))
                .fileExistsMode(FileExistsMode.REPLACE)
                .deleteSourceFiles(true))  // Удаляем оригинальные файлы
            .get();
    }
    
    // FTP Integration
    @Bean
    public IntegrationFlow ftpInboundFlow() {
        return IntegrationFlows
            .from(Ftp.inboundAdapter(ftpSessionFactory())
                .deleteRemoteFiles(true)
                .remoteDirectory("/remote/input")
                .localDirectory(new File("/local/input")),
                e -> e.poller(Pollers.fixedDelay(30000)))
            .transform(File.class, this::processFile)
            .channel("fileProcessingChannel")
            .get();
    }
}

8. HTTP Integration

REST API Integration

@Configuration
public class HttpIntegrationConfig {
    
    // HTTP Outbound Gateway
    @Bean
    public IntegrationFlow httpOutboundFlow() {
        return IntegrationFlows
            .from("httpRequestChannel")
            .enrichHeaders(h -> h
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer " + tokenService.getToken()))
            .handle(Http.outboundGateway("http://api.example.com/orders")
                .httpMethod(HttpMethod.POST)
                .expectedResponseType(OrderResponse.class)
                .charset("UTF-8"))
            .transform("payload.orderId")
            .channel("httpResponseChannel")
            .get();
    }
    
    // Webhook endpoint
    @Bean
    public IntegrationFlow webhookFlow() {
        return IntegrationFlows
            .from(Http.inboundGateway("/webhook/orders")
                .requestMapping(m -> m.methods(HttpMethod.POST))
                .requestPayloadType(WebhookData.class)
                .headerMapper(headerMapper())
                .crossOrigin(origins -> origins.origin("*")))
            .log()
            .transform("payload.data")
            .filter("payload.type == 'order'")
            .handle("webhookProcessor", "processWebhook")
            .transform(payload -> ResponseEntity.ok("Processed"))
            .get();
    }
    
    // Polling external API
    @Bean
    public IntegrationFlow apiPollingFlow() {
        return IntegrationFlows
            .from(() -> "trigger", e -> e.poller(Pollers.fixedRate(60000)))  // Каждую минуту
            .handle(Http.outboundGateway("http://api.example.com/status")
                .httpMethod(HttpMethod.GET)
                .expectedResponseType(ApiStatus.class))
            .filter("payload.hasUpdates")
            .handle("statusProcessor", "processStatusUpdate")
            .get();
    }
}

9. Aggregation и Splitting

Message Aggregation

@Configuration
public class AggregationConfig {
    
    @Bean
    public IntegrationFlow orderAggregationFlow() {
        return IntegrationFlows
            .from("orderItemsChannel")
            .aggregate(aggregator -> aggregator
                .correlationStrategy(message -> 
                    message.getHeaders().get("orderId"))  // Группируем по orderId
                .releaseStrategy(new MessageCountReleaseStrategy(5))  // Когда 5 элементов
                .expireGroupsUponCompletion(true)
                .sendPartialResultOnExpiry(true)
                .groupTimeout(30000))  // Таймаут 30 секунд
            .transform("payload.?[payload != null]")  // Убираем null элементы
            .handle("orderService", "createCompleteOrder")
            .get();
    }
    
    // Custom aggregator
    @Aggregator(inputChannel = "customAggregatorChannel", 
                outputChannel = "aggregatedChannel")
    public Order aggregateOrderItems(List<OrderItem> items) {
        Order order = new Order();
        order.setItems(items);
        order.setTotal(items.stream()
            .mapToDouble(OrderItem::getPrice)
            .sum());
        return order;
    }
}

Message Splitting

@Configuration
public class SplittingConfig {
    
    @Bean
    public IntegrationFlow batchSplittingFlow() {
        return IntegrationFlows
            .from("batchInputChannel")
            .split(s -> s.delimiters(","))  // Разбиваем по запятой
            .transform(String.class, String::trim)
            .filter(String.class, str -> !str.isEmpty())
            .channel("individualItemChannel")
            .get();
    }
    
    // Custom splitter
    @Splitter(inputChannel = "orderSplitterChannel", outputChannel = "orderItemChannel")
    public List<OrderItem> splitOrder(Order order) {
        return order.getItems();
    }
    
    // File line splitter
    @Bean
    public IntegrationFlow fileSplittingFlow() {
        return IntegrationFlows
            .from("csvFileChannel")
            .split(new FileSplitter(true, true))  // По строкам, с заголовками
            .transform(String.class, this::parseCsvLine)
            .aggregate(a -> a.correlationStrategy(m -> "batch")
                .releaseStrategy(new MessageCountReleaseStrategy(100)))
            .handle("batchProcessor", "processBatch")
            .get();
    }
}

10. Monitoring и Management

JMX Management

@Configuration
@EnableIntegrationManagement  // Включает JMX для Integration
public class IntegrationManagementConfig {
    
    @Bean
    public IntegrationMBeanExporter integrationMBeanExporter() {
        IntegrationMBeanExporter exporter = new IntegrationMBeanExporter();
        exporter.setDefaultDomain("spring.integration");
        return exporter;
    }
}

Metrics и Monitoring

@Component
public class IntegrationMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Counter messageCounter;
    private final Timer processingTimer;
    
    public IntegrationMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.messageCounter = Counter.builder("integration.messages.processed")
            .description("Number of processed messages")
            .register(meterRegistry);
        this.processingTimer = Timer.builder("integration.processing.time")
            .description("Message processing time")
            .register(meterRegistry);
    }
    
    @EventListener
    public void handleMessageProcessed(MessageProcessedEvent event) {
        messageCounter.increment(
            Tags.of("channel", event.getChannelName(), 
                   "status", event.getStatus()));
    }
    
    @ServiceActivator(inputChannel = "metricsChannel")
    public void recordProcessingTime(Message<?> message) {
        Timer.Sample sample = Timer.Sample.start(meterRegistry);
        sample.stop(processingTimer);
    }
}

Error Channels и Dead Letter Queues

@Configuration
public class ErrorHandlingConfig {
    
    // Global error channel
    @ServiceActivator(inputChannel = "errorChannel")
    public void handleError(ErrorMessage errorMessage) {
        Throwable exception = errorMessage.getPayload();
        Message<?> failedMessage = errorMessage.getOriginalMessage();
        
        log.error("Integration error: {}", exception.getMessage(), exception);
        
        // Отправляем в Dead Letter Queue
        deadLetterService.sendToDeadLetter(failedMessage, exception);
    }
    
    // Retry and Dead Letter Flow
    @Bean
    public IntegrationFlow retryAndDeadLetterFlow() {
        return IntegrationFlows
            .from("processingChannel")
            .handle("messageProcessor", "process",
                e -> e.advice(retryAdvice()))
            .routeToRecipients(r -> r
                .recipientFlow("successChannel")
                .recipientFlow(subFlow -> subFlow
                    .filter("headers.errorChannel != null")
                    .channel("deadLetterChannel")))
            .get();
    }
    
    @Bean
    public RequestHandlerRetryAdvice retryAdvice() {
        RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
        
        RetryTemplate retryTemplate = RetryTemplate.builder()
            .maxAttempts(3)
            .exponentialBackoff(1000, 2, 10000)
            .retryOn(TransientException.class)
            .build();
            
        advice.setRetryTemplate(retryTemplate);
        
        RecoveryCallback<Object> recoveryCallback = context -> {
            // Отправляем в Dead Letter после исчерпания попыток
            deadLetterService.sendToDeadLetter(
                (Message<?>) context.getAttribute("message"),
                context.getLastThrowable());
            return null;
        };
        
        advice.setRecoveryCallback(recoveryCallback);
        return advice;
    }
}

Configuration Properties

@ConfigurationProperties(prefix = "app.integration")
@Data
public class IntegrationProperties {
    
    private File fileProcessing = new File();
    private Http http = new Http();
    private Jms jms = new Jms();
    
    @Data
    public static class File {
        private String inputDirectory = "/input";
        private String outputDirectory = "/output";
        private int pollingInterval = 5000;
        private boolean useWatchService = true;
    }
    
    @Data
    public static class Http {
        private String baseUrl = "http://localhost:8080";
        private int connectionTimeout = 30000;
        private int readTimeout = 60000;
        private int retryAttempts = 3;
    }
    
    @Data
    public static class Jms {
        private String brokerUrl = "tcp://localhost:61616";
        private int concurrentConsumers = 5;
        private int maxConcurrentConsumers = 10;
    }
}

# application.yml
app:
  integration:
    file-processing:
      input-directory: /data/input
      output-directory: /data/output
      polling-interval: 10000
    http:
      base-url: https://api.external.com
      connection-timeout: 30000
      retry-attempts: 5

Spring Integration предоставляет мощные возможности для построения enterprise-уровня интеграционных решений с минимальным количеством кода благодаря декларативному подходу и богатой библиотеке готовых компонентов.

Spring Cloud

Что такое Spring Cloud

Spring Cloud — это набор инструментов для создания распределенных систем и микросервисов на основе Spring Boot. Решает основные проблемы микросервисной архитектуры: service discovery, load balancing, circuit breakers, distributed configuration.

Основные компоненты:

  • Spring Cloud Config — централизованное управление конфигурацией
  • Spring Cloud Gateway — API Gateway для маршрутизации
  • Spring Cloud OpenFeign — декларативный HTTP-клиент
  • Spring Cloud Circuit Breaker — паттерн Circuit Breaker
  • Spring Cloud LoadBalancer — клиентский балансировщик нагрузки
  • Spring Cloud Sleuth — распределенная трассировка

Spring Cloud Config

Конфигурация Config Server

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
# application.yml
server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/username/config-repo
          # Локальная папка
          # native:
          #   search-locations: classpath:/config

Пояснение: Config Server централизует конфигурацию всех микросервисов в одном месте. Можно использовать Git-репозиторий или локальные файлы. Все изменения конфигурации можно делать без перезапуска сервисов.

Конфигурация Config Client

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
# bootstrap.yml (важно именно bootstrap!)
spring:
  application:
    name: user-service
  cloud:
    config:
      uri: http://localhost:8888
      fail-fast: true
      retry:
        max-attempts: 6
@RestController
@RefreshScope  // Позволяет обновлять конфигурацию без перезапуска
public class ConfigController {
    
    @Value("${app.message:Default message}")
    private String message;
    
    @GetMapping("/message")
    public String getMessage() {
        return message;
    }
}

Пояснение: @RefreshScope позволяет обновлять конфигурацию через POST-запрос на /actuator/refresh. fail-fast=true означает, что приложение не запустится, если не сможет подключиться к Config Server.


Spring Cloud Gateway

Базовая конфигурация

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
spring:
  cloud:
    gateway:
      routes:

        - id: user-service
          uri: http://localhost:8081
          predicates:

            - Path=/users/**
          filters:

            - StripPrefix=1
        - id: order-service
          uri: http://localhost:8082
          predicates:

            - Path=/orders/**
          filters:

            - AddRequestHeader=X-Request-Source, Gateway

Пояснение: Gateway маршрутизирует запросы к микросервисам. StripPrefix=1 убирает первый сегмент пути (/users/123 становится /123). Можно добавлять заголовки, проверять аутентификацию, логировать запросы.

Программная конфигурация маршрутов

@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r -> r.path("/api/users/**")
                .filters(f -> f.stripPrefix(2)
                    .addRequestHeader("X-Gateway", "Spring-Cloud"))
                .uri("http://localhost:8081"))
            .route("order-service", r -> r.path("/api/orders/**")
                .and().method("GET")
                .filters(f -> f.stripPrefix(2))
                .uri("http://localhost:8082"))
            .build();
    }
}

Кастомные фильтры

@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomGatewayFilterFactory.Config> {
    
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            System.out.println("Custom filter: " + request.getPath());
            return chain.filter(exchange);
        };
    }
    
    public static class Config {
        // Параметры фильтра
    }
}

Пояснение: Кастомные фильтры позволяют добавлять логику обработки запросов: аутентификацию, логирование, модификацию заголовков. Фильтры выполняются в цепочке до и после обращения к микросервису.


Spring Cloud OpenFeign

Основное использование

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@EnableFeignClients
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
@FeignClient(name = "user-service", url = "http://localhost:8081")
public interface UserClient {
    
    @GetMapping("/users/{id}")
    User getUser(@PathVariable Long id);
    
    @PostMapping("/users")
    User createUser(@RequestBody User user);
    
    @GetMapping("/users")
    List<User> getUsers(@RequestParam String status);
}
@RestController
public class OrderController {
    
    private final UserClient userClient;
    
    public OrderController(UserClient userClient) {
        this.userClient = userClient;
    }
    
    @GetMapping("/orders/{id}/user")
    public User getOrderUser(@PathVariable Long id) {
        // Вызов другого микросервиса через Feign
        return userClient.getUser(id);
    }
}

Пояснение: OpenFeign создает HTTP-клиент из интерфейса. Это упрощает вызовы между микросервисами — вместо RestTemplate или WebClient пишем простой интерфейс. Feign автоматически сериализует/десериализует JSON.

Конфигурация Feign

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
      user-service:
        connectTimeout: 10000
        readTimeout: 10000
@Configuration
public class FeignConfig {
    
    @Bean
    public RequestInterceptor requestInterceptor() {
        return template -> {
            template.header("Authorization", "Bearer " + getToken());
        };
    }
    
    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder();
    }
}

Пояснение: Можно настроить таймауты, логирование, добавить заголовки ко всем запросам. ErrorDecoder позволяет кастомизировать обработку ошибок HTTP.


Spring Cloud Circuit Breaker

Resilience4j (рекомендуемый)

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
@RestController
public class UserController {
    
    private final CircuitBreakerFactory circuitBreakerFactory;
    private final UserService userService;
    
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        CircuitBreaker circuitBreaker = circuitBreakerFactory.create("user-service");
        
        return circuitBreaker.run(
            () -> userService.getUser(id),
            throwable -> User.builder()
                .id(id)
                .name("Unknown User")
                .build()
        );
    }
}

Пояснение: Circuit Breaker защищает от каскадных отказов. Если сервис недоступен, вместо ошибки возвращается fallback-значение. После нескольких неудачных попыток "размыкает цепь" и сразу возвращает fallback.

Конфигурация Circuit Breaker

resilience4j:
  circuitbreaker:
    configs:
      default:
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s
        sliding-window-size: 10
        minimum-number-of-calls: 5
    instances:
      user-service:
        base-config: default
        failure-rate-threshold: 60

Использование с аннотациями

@Component
public class UserService {
    
    @CircuitBreaker(name = "user-service", fallbackMethod = "fallbackUser")
    @Retry(name = "user-service")
    @TimeLimiter(name = "user-service")
    public CompletableFuture<User> getUser(Long id) {
        // Вызов внешнего сервиса
        return CompletableFuture.supplyAsync(() -> {
            // Симуляция HTTP-вызова
            return userClient.getUser(id);
        });
    }
    
    public CompletableFuture<User> fallbackUser(Long id, Exception ex) {
        return CompletableFuture.completedFuture(
            User.builder().id(id).name("Fallback User").build()
        );
    }
}

Пояснение: Аннотации упрощают использование. @Retry повторяет неудачные вызовы. @TimeLimiter ограничивает время выполнения. @CircuitBreaker включает circuit breaker паттерн.


Spring Cloud LoadBalancer

Конфигурация

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
@Configuration
public class LoadBalancerConfig {
    
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}
@RestController
public class OrderController {
    
    private final RestTemplate restTemplate;
    
    @GetMapping("/orders/{id}/user")
    public User getOrderUser(@PathVariable Long id) {
        // user-service - логическое имя сервиса
        return restTemplate.getForObject(
            "http://user-service/users/" + id, 
            User.class
        );
    }
}

Пояснение: @LoadBalanced позволяет использовать логические имена сервисов вместо IP-адресов. LoadBalancer автоматически выбирает один из доступных экземпляров сервиса.

Кастомный алгоритм балансировки

@Configuration
public class CustomLoadBalancerConfig {
    
    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
            Environment environment, 
            LoadBalancerClientFactory loadBalancerClientFactory) {
        
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(
            loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
            name
        );
    }
}

Пояснение: Можно создавать собственные алгоритмы балансировки нагрузки: случайный выбор, взвешенный round-robin, выбор по географии.


Spring Cloud Sleuth (Tracing)

Конфигурация

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- Для отправки в Zipkin -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
spring:
  sleuth:
    sampler:
      probability: 1.0  # Трассировать все запросы (для разработки)
  zipkin:
    base-url: http://localhost:9411
@RestController
public class OrderController {
    
    private final Tracer tracer;
    
    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        Span span = tracer.nextSpan().name("get-order").start();
        try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
            span.tag("order.id", String.valueOf(id));
            // Бизнес-логика
            return orderService.getOrder(id);
        } finally {
            span.end();
        }
    }
}

Пояснение: Sleuth автоматически добавляет trace ID и span ID в логи. Это помогает отслеживать запросы через всю цепочку микросервисов. Zipkin визуализирует трассировку.


Практические паттерны

1. Saga Pattern для распределенных транзакций

@Service
public class OrderSagaService {
    
    @SagaOrchestrationStart(sagaType = "order-saga")
    public void processOrder(Order order) {
        // Шаг 1: Резервирование товара
        sagaManager.choreography()
            .step("reserve-inventory")
            .localTxn(this::reserveInventory)
            .withCompensation(this::cancelReservation)
            .step("process-payment")
            .localTxn(this::processPayment)
            .withCompensation(this::refundPayment)
            .step("ship-order")
            .localTxn(this::shipOrder)
            .withCompensation(this::cancelShipment)
            .execute();
    }
}

Пояснение: Saga обеспечивает согласованность данных без распределенных транзакций. Если один из шагов fails, выполняются компенсирующие действия.

2. Event-Driven Architecture

@Component
public class OrderEventHandler {
    
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Отправка в очередь сообщений
        messageProducer.send("order-notifications", event);
    }
    
    @RabbitListener(queues = "order-processing")
    public void processOrder(OrderCreatedEvent event) {
        // Обработка заказа
        orderService.processOrder(event.getOrderId());
    }
}

3. CQRS Pattern

// Command стороны
@RestController
public class OrderCommandController {
    
    @PostMapping("/orders")
    public void createOrder(@RequestBody CreateOrderCommand command) {
        commandHandler.handle(command);
    }
}

// Query стороны
@RestController
public class OrderQueryController {
    
    @GetMapping("/orders/{id}")
    public OrderView getOrder(@PathVariable Long id) {
        return orderQueryService.getOrder(id);
    }
}

Пояснение: CQRS разделяет операции чтения и записи. Это позволяет оптимизировать каждую сторону независимо.


Мониторинг и обслуживание

Spring Boot Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always

Кастомные health checks

@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    @Override
    public Health health() {
        try {
            // Проверка подключения к БД
            if (isDatabaseUp()) {
                return Health.up()
                    .withDetail("database", "PostgreSQL")
                    .withDetail("status", "Available")
                    .build();
            } else {
                return Health.down()
                    .withDetail("database", "PostgreSQL")
                    .withDetail("error", "Connection failed")
                    .build();
            }
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

Пояснение: Health checks позволяют мониторить состояние микросервисов. Kubernetes может использовать эти endpoints для проверки жизнеспособности подов.


Лучшие практики

1. Конфигурация профилей

# application.yml
spring:
  profiles:
    active: dev
    
---
spring:
  profiles: dev
  datasource:
    url: jdbc:h2:mem:testdb
    
---
spring:
  profiles: prod
  datasource:
    url: jdbc:postgresql://prod-db:5432/myapp

2. Graceful Shutdown

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 20s

3. Логирование

logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n"
  level:
    org.springframework.cloud: DEBUG
    com.mycompany: INFO

4. Безопасность

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtDecoder(jwtDecoder()))
            );
        return http.build();
    }
}

Пояснение: Важно защищать микросервисы. Обычно используется OAuth2 + JWT для аутентификации между сервисами.


Заключение

Spring Cloud предоставляет готовые решения для основных проблем микросервисной архитектуры. Начните с простых компонентов (Config, Gateway, OpenFeign), затем добавляйте более сложные (Circuit Breaker, Sleuth). Важно правильно настроить мониторинг и логирование с самого начала.

Resilience4j в Spring

Что такое Resilience4j

Resilience4j — это легковесная библиотека для создания отказоустойчивых приложений, вдохновленная Netflix Hystrix. Предоставляет паттерны устойчивости к сбоям: Circuit Breaker, Retry, Rate Limiter, Time Limiter, Bulkhead.

Основные компоненты:

  • Circuit Breaker — защита от каскадных отказов
  • Retry — повторные попытки при сбоях
  • Rate Limiter — ограничение частоты запросов
  • Time Limiter — ограничение времени выполнения
  • Bulkhead — изоляция ресурсов
  • Cache — кеширование результатов

Подключение к проекту

Maven зависимости

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

<!-- Для Spring Boot 2.x -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
</dependency>

<!-- Для реактивного программирования -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-reactor</artifactId>
</dependency>

Активация в Spring Boot

@SpringBootApplication
@EnableCircuitBreaker  // Для Spring Cloud
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Circuit Breaker

Основные состояния

Circuit Breaker имеет три состояния:

  • CLOSED — нормальная работа, запросы проходят
  • OPEN — сервис недоступен, запросы отклоняются с fallback
  • HALF_OPEN — тестирование доступности сервиса

Использование с аннотациями

@Service
public class UserService {
    
    @CircuitBreaker(name = "userService", fallbackMethod = "fallbackUser")
    public User getUser(Long id) {
        // Вызов внешнего API
        return userClient.getUser(id);
    }
    
    // Fallback метод должен иметь ту же сигнатуру + Exception
    public User fallbackUser(Long id, Exception ex) {
        return User.builder()
            .id(id)
            .name("Default User")
            .email("default@example.com")
            .build();
    }
}

Пояснение: Когда процент неудачных вызовов превышает порог, Circuit Breaker "размыкается" и начинает сразу возвращать fallback-значение, не вызывая реальный сервис. Это защищает от каскадных отказов.

Конфигурация в application.yml

resilience4j:
  circuitbreaker:
    configs:
      default:
        # Порог неудачных вызовов для размыкания (в %)
        failure-rate-threshold: 50
        # Время ожидания в открытом состоянии
        wait-duration-in-open-state: 10s
        # Размер скользящего окна
        sliding-window-size: 10
        # Минимальное количество вызовов для расчета статистики
        minimum-number-of-calls: 5
        # Количество разрешенных вызовов в HALF_OPEN
        permitted-number-of-calls-in-half-open-state: 3
        # Тип окна: COUNT_BASED или TIME_BASED
        sliding-window-type: COUNT_BASED
        # Автоматический переход из OPEN в HALF_OPEN
        automatic-transition-from-open-to-half-open-enabled: true
        # Исключения, которые считаются неудачами
        record-exceptions:

          - java.net.ConnectException
          - java.util.concurrent.TimeoutException
        # Исключения, которые игнорируются
        ignore-exceptions:

          - java.lang.IllegalArgumentException
    instances:
      userService:
        base-config: default
        failure-rate-threshold: 60
      paymentService:
        base-config: default
        wait-duration-in-open-state: 20s

Пояснение:

  • failure-rate-threshold — при превышении этого процента неудач Circuit Breaker размыкается
  • sliding-window-size — количество последних вызовов для анализа
  • minimum-number-of-calls — минимум вызовов перед началом анализа статистики

Программная конфигурация

@Configuration
public class CircuitBreakerConfig {
    
    @Bean
    public CircuitBreaker userServiceCircuitBreaker() {
        return CircuitBreaker.of("userService", CircuitBreakerConfig.custom()
            .failureRateThreshold(50)
            .waitDurationInOpenState(Duration.ofSeconds(10))
            .slidingWindowSize(10)
            .minimumNumberOfCalls(5)
            .build());
    }
    
    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        return CircuitBreakerRegistry.of(
            Map.of("userService", 
                CircuitBreakerConfig.custom()
                    .failureRateThreshold(60)
                    .build())
        );
    }
}

Использование без аннотаций

@Service
public class OrderService {
    
    private final CircuitBreaker circuitBreaker;
    
    public OrderService(CircuitBreakerRegistry registry) {
        this.circuitBreaker = registry.circuitBreaker("orderService");
    }
    
    public Order getOrder(Long id) {
        Supplier<Order> decoratedSupplier = CircuitBreaker
            .decorateSupplier(circuitBreaker, () -> orderClient.getOrder(id));
            
        return Try.ofSupplier(decoratedSupplier)
            .recover(throwable -> Order.builder()
                .id(id)
                .status("UNKNOWN")
                .build())
            .get();
    }
}

Retry

Использование с аннотациями

@Service
public class PaymentService {
    
    @Retry(name = "paymentService", fallbackMethod = "fallbackPayment")
    public PaymentResult processPayment(PaymentRequest request) {
        // Вызов платежного API
        return paymentClient.processPayment(request);
    }
    
    public PaymentResult fallbackPayment(PaymentRequest request, Exception ex) {
        return PaymentResult.builder()
            .status("FAILED")
            .message("Payment service temporarily unavailable")
            .build();
    }
}

Пояснение: Retry автоматически повторяет неудачные вызовы с настраиваемыми интервалами. Полезно для временных сбоев сети или кратковременной недоступности сервиса.

Конфигурация Retry

resilience4j:
  retry:
    configs:
      default:
        # Максимальное количество попыток
        max-attempts: 3
        # Интервал между попытками
        wait-duration: 1s
        # Исключения для повтора
        retry-exceptions:

          - java.net.ConnectException
          - java.util.concurrent.TimeoutException
        # Исключения для игнорирования
        ignore-exceptions:

          - java.lang.IllegalArgumentException
        # Экспоненциальный backoff
        exponential-backoff-multiplier: 2
        # Случайность в интервалах
        randomized-wait-factor: 0.5
    instances:
      paymentService:
        base-config: default
        max-attempts: 5
        wait-duration: 2s

Пояснение:

  • exponential-backoff-multiplier — каждая попытка ждет в 2 раза дольше
  • randomized-wait-factor — добавляет случайность, чтобы избежать "эффекта стада"

Программное использование

@Service
public class NotificationService {
    
    private final Retry retry;
    
    public NotificationService(RetryRegistry retryRegistry) {
        this.retry = retryRegistry.retry("notificationService");
    }
    
    public void sendNotification(String message) {
        Runnable decoratedRunnable = Retry.decorateRunnable(retry, 
            () -> emailService.sendEmail(message));
            
        Try.run(decoratedRunnable)
            .onFailure(throwable -> 
                log.error("Failed to send notification after retries", throwable));
    }
}

Rate Limiter

Использование с аннотациями

@RestController
public class ApiController {
    
    @RateLimiter(name = "apiRateLimit")
    @GetMapping("/api/data")
    public ResponseEntity<String> getData() {
        // Обработка запроса
        return ResponseEntity.ok("Data retrieved successfully");
    }
}

Пояснение: Rate Limiter ограничивает количество запросов в единицу времени. Защищает от перегрузки и DDoS-атак.

Конфигурация Rate Limiter

resilience4j:
  ratelimiter:
    configs:
      default:
        # Лимит запросов за период
        limit-for-period: 10
        # Период обновления лимита
        limit-refresh-period: 1s
        # Время ожидания разрешения
        timeout-duration: 0s
        # Регистрация событий
        register-health-indicator: true
    instances:
      apiRateLimit:
        base-config: default
        limit-for-period: 5
        limit-refresh-period: 1s
      adminApiRateLimit:
        limit-for-period: 100
        limit-refresh-period: 1m

Пояснение:

  • limit-for-period — максимум запросов за период
  • limit-refresh-period — период обновления счетчика
  • timeout-duration — время ожидания освобождения слота (0 = немедленный отказ)

Программное использование

@Service
public class ExternalApiService {
    
    private final RateLimiter rateLimiter;
    
    public ExternalApiService(RateLimiterRegistry registry) {
        this.rateLimiter = registry.rateLimiter("externalApi");
    }
    
    public String callExternalApi() {
        return RateLimiter.decorateSupplier(rateLimiter, 
            () -> externalApiClient.getData())
            .get();
    }
}

Time Limiter

Использование с аннотациями

@Service
public class SlowService {
    
    @TimeLimiter(name = "slowService", fallbackMethod = "fallbackMethod")
    public CompletableFuture<String> slowOperation() {
        return CompletableFuture.supplyAsync(() -> {
            // Медленная операция
            try {
                Thread.sleep(5000);
                return "Operation completed";
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
    }
    
    public CompletableFuture<String> fallbackMethod(Exception ex) {
        return CompletableFuture.completedFuture("Operation timed out");
    }
}

Пояснение: Time Limiter ограничивает время выполнения операций. Важно: работает только с асинхронными методами (CompletableFuture).

Конфигурация Time Limiter

resilience4j:
  timelimiter:
    configs:
      default:
        # Максимальное время выполнения
        timeout-duration: 3s
        # Отмена операции при таймауте
        cancel-running-future: true
    instances:
      slowService:
        base-config: default
        timeout-duration: 5s

Bulkhead

Thread Pool Bulkhead

@Service
public class ResourceService {
    
    @Bulkhead(name = "resourceService", type = Bulkhead.Type.THREADPOOL)
    public CompletableFuture<String> heavyOperation() {
        return CompletableFuture.supplyAsync(() -> {
            // Тяжелая операция
            return processHeavyTask();
        });
    }
}

Пояснение: Bulkhead изолирует ресурсы, предотвращая влияние одного сервиса на другой. Thread Pool Bulkhead выделяет отдельный пул потоков.

Конфигурация Bulkhead

resilience4j:
  bulkhead:
    configs:
      default:
        # Максимальное количество одновременных вызовов
        max-concurrent-calls: 10
        # Максимальное время ожидания
        max-wait-duration: 1s
    instances:
      resourceService:
        base-config: default
        max-concurrent-calls: 5
        
  thread-pool-bulkhead:
    configs:
      default:
        # Размер пула потоков
        core-thread-pool-size: 5
        max-thread-pool-size: 10
        # Размер очереди
        queue-capacity: 20
        # Время жизни потока
        keep-alive-duration: 20s
    instances:
      resourceService:
        base-config: default

Комбинирование паттернов

Использование нескольких аннотаций

@Service
public class CriticalService {
    
    @CircuitBreaker(name = "criticalService", fallbackMethod = "fallbackMethod")
    @Retry(name = "criticalService")
    @TimeLimiter(name = "criticalService")
    @RateLimiter(name = "criticalService")
    public CompletableFuture<String> criticalOperation() {
        return CompletableFuture.supplyAsync(() -> {
            // Критически важная операция
            return performCriticalTask();
        });
    }
    
    public CompletableFuture<String> fallbackMethod(Exception ex) {
        return CompletableFuture.completedFuture("Fallback result");
    }
}

Пояснение: Паттерны можно комбинировать. Порядок выполнения: RateLimiter → TimeLimiter → CircuitBreaker → Retry → Bulkhead.

Программное комбинирование

@Service
public class ComplexService {
    
    public String complexOperation() {
        CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("complexService");
        Retry retry = Retry.ofDefaults("complexService");
        RateLimiter rateLimiter = RateLimiter.ofDefaults("complexService");
        
        Supplier<String> decoratedSupplier = Decorators.ofSupplier(() -> externalService.call())
            .withRateLimiter(rateLimiter)
            .withCircuitBreaker(circuitBreaker)
            .withRetry(retry)
            .decorate();
            
        return decoratedSupplier.get();
    }
}

Мониторинг и метрики

Интеграция с Micrometer

management:
  endpoints:
    web:
      exposure:
        include: health, metrics, circuitbreakers, retries, ratelimiters
  endpoint:
    health:
      show-details: always
  metrics:
    distribution:
      percentiles-histogram:
        resilience4j.circuitbreaker.calls: true
        resilience4j.retry.calls: true

Кастомные метрики

@Component
public class MetricsConfig {
    
    @EventListener
    public void onCircuitBreakerStateTransition(CircuitBreakerOnStateTransitionEvent event) {
        log.info("Circuit breaker {} transition from {} to {}", 
            event.getCircuitBreakerName(),
            event.getStateTransition().getFromState(),
            event.getStateTransition().getToState());
    }
    
    @EventListener
    public void onRetryEvent(RetryOnRetryEvent event) {
        log.warn("Retry attempt {} for {}", 
            event.getNumberOfRetryAttempts(),
            event.getName());
    }
}

Пояснение: Resilience4j публикует события, которые можно использовать для мониторинга. Метрики автоматически экспортируются в Prometheus.


Реактивное программирование

Использование с WebFlux

@Service
public class ReactiveService {
    
    private final CircuitBreaker circuitBreaker;
    private final Retry retry;
    
    public Mono<String> reactiveOperation() {
        return webClient.get()
            .uri("/api/data")
            .retrieve()
            .bodyToMono(String.class)
            .transformDeferred(CircuitBreakerOperator.of(circuitBreaker))
            .transformDeferred(RetryOperator.of(retry))
            .onErrorReturn("Fallback value");
    }
}

Конфигурация для реактивных потоков

resilience4j:
  circuitbreaker:
    instances:
      reactiveService:
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s
        sliding-window-size: 10
        minimum-number-of-calls: 5
        # Специфично для реактивных потоков
        slow-call-duration-threshold: 2s
        slow-call-rate-threshold: 60

Практические примеры

1. Микросервисная архитектура

@Service
public class OrderService {
    
    @CircuitBreaker(name = "userService", fallbackMethod = "defaultUser")
    @Retry(name = "userService")
    public User getUserInfo(Long userId) {
        return userServiceClient.getUser(userId);
    }
    
    @CircuitBreaker(name = "inventoryService", fallbackMethod = "defaultInventory")
    @Retry(name = "inventoryService")
    public boolean checkInventory(Long productId, int quantity) {
        return inventoryServiceClient.checkAvailability(productId, quantity);
    }
    
    public User defaultUser(Long userId, Exception ex) {
        return User.builder()
            .id(userId)
            .name("Guest User")
            .build();
    }
    
    public boolean defaultInventory(Long productId, int quantity, Exception ex) {
        // Консервативный подход - считаем товар недоступным
        return false;
    }
}

2. Внешние API

@Service
public class WeatherService {
    
    @RateLimiter(name = "weatherApi")
    @CircuitBreaker(name = "weatherApi", fallbackMethod = "cachedWeather")
    @Retry(name = "weatherApi")
    public WeatherData getWeather(String city) {
        return weatherApiClient.getCurrentWeather(city);
    }
    
    public WeatherData cachedWeather(String city, Exception ex) {
        // Возвращаем кешированные данные
        return weatherCache.get(city)
            .orElse(WeatherData.builder()
                .city(city)
                .temperature(20.0)
                .description("Data unavailable")
                .build());
    }
}

3. Критически важные операции

@Service
public class PaymentService {
    
    @CircuitBreaker(name = "paymentGateway", fallbackMethod = "fallbackPayment")
    @Retry(name = "paymentGateway")
    @TimeLimiter(name = "paymentGateway")
    @Bulkhead(name = "paymentGateway", type = Bulkhead.Type.THREADPOOL)
    public CompletableFuture<PaymentResult> processPayment(PaymentRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            // Критически важная операция платежа
            return paymentGateway.charge(request);
        });
    }
    
    public CompletableFuture<PaymentResult> fallbackPayment(PaymentRequest request, Exception ex) {
        // Логируем ошибку и возвращаем статус для повторной попытки
        log.error("Payment failed for request {}", request.getId(), ex);
        return CompletableFuture.completedFuture(
            PaymentResult.builder()
                .status("RETRY_LATER")
                .message("Payment gateway temporarily unavailable")
                .build()
        );
    }
}

Лучшие практики

1. Правильная конфигурация

# Разные настройки для разных сервисов
resilience4j:
  circuitbreaker:
    instances:
      # Быстрый сервис - строгие настройки
      fastService:
        failure-rate-threshold: 30
        wait-duration-in-open-state: 5s
        sliding-window-size: 20
      # Медленный сервис - мягкие настройки
      slowService:
        failure-rate-threshold: 70
        wait-duration-in-open-state: 30s
        sliding-window-size: 10

2. Логирование и мониторинг

@Component
public class ResilienceMetrics {
    
    private final MeterRegistry meterRegistry;
    
    @EventListener
    public void onCircuitBreakerStateTransition(CircuitBreakerOnStateTransitionEvent event) {
        Counter.builder("circuit_breaker_state_transitions")
            .tag("name", event.getCircuitBreakerName())
            .tag("from_state", event.getStateTransition().getFromState().name())
            .tag("to_state", event.getStateTransition().getToState().name())
            .register(meterRegistry)
            .increment();
    }
}

3. Graceful Degradation

@Service
public class RecommendationService {
    
    @CircuitBreaker(name = "mlService", fallbackMethod = "simpleRecommendations")
    public List<Product> getPersonalizedRecommendations(Long userId) {
        return mlService.getRecommendations(userId);
    }
    
    public List<Product> simpleRecommendations(Long userId, Exception ex) {
        // Возвращаем популярные товары вместо персонализированных
        return productService.getPopularProducts(10);
    }
}

4. Конфигурация для окружений

# application-prod.yml
resilience4j:
  circuitbreaker:
    configs:
      default:
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        
# application-dev.yml
resilience4j:
  circuitbreaker:
    configs:
      default:
        failure-rate-threshold: 80
        wait-duration-in-open-state: 5s

Заключение

Resilience4j предоставляет мощные инструменты для создания отказоустойчивых приложений. Ключевые принципы:

  1. Начинайте с Circuit Breaker — самый важный паттерн для микросервисов
  2. Добавляйте Retry осторожно — может усугубить проблемы при неправильной настройке
  3. Используйте Rate Limiter — для защиты от перегрузки
  4. Мониторьте метрики — для понимания поведения системы
  5. Настраивайте под конкретные сервисы — универсальных настроек не существует

Правильная конфигурация Resilience4j может значительно повысить надежность вашего приложения и улучшить пользовательский опыт при сбоях.

Spring Cache

Что такое Spring Cache?

Spring Cache — это абстракция кэширования в Spring Framework, которая позволяет декларативно добавлять кэширование к методам через аннотации. Использует AOP (Aspect-Oriented Programming) для прозрачного кэширования без изменения бизнес-логики.

Основные принципы:

  • Декларативное кэширование — управление через аннотации
  • Абстракция провайдеров — единый API для разных cache providers (Redis, Ehcache, Caffeine)
  • Key-based кэширование — данные хранятся по ключам
  • Автоматическое управление — Spring автоматически проверяет кэш перед вызовом метода

Конфигурация Spring Cache

Включение кэширования

@Configuration
@EnableCaching // Включает поддержку кэширования
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        // Простой in-memory cache manager
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(
            new ConcurrentMapCache("users"),
            new ConcurrentMapCache("products"),
            new ConcurrentMapCache("orders")
        ));
        return cacheManager;
    }
    
    // Caffeine - высокопроизводительный Java cache
    @Bean
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofMinutes(10))
            .recordStats()); // Включение статистики
        return cacheManager;
    }
    
    // Redis как распределенный кэш
    @Bean
    @Primary
    public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
    }
}

CacheManager — центральный компонент для управления кэшами. @EnableCaching активирует AOP proxy для методов с cache аннотациями.

Основные Cache аннотации

@Cacheable - кэширование результата

@Service
public class UserService {
    
    // Базовое кэширование по ID
    @Cacheable(value = "users", key = "#id")
    public User findById(Long id) {
        System.out.println("Loading user from database: " + id);
        return userRepository.findById(id).orElse(null);
        // При первом вызове - загрузка из БД и сохранение в кэш
        // При повторном вызове - возврат из кэша
    }
    
    // Кэширование с составным ключом
    @Cacheable(value = "usersByDepartment", key = "#departmentId + '_' + #active")
    public List<User> findByDepartmentAndStatus(Long departmentId, boolean active) {
        return userRepository.findByDepartmentIdAndActive(departmentId, active);
    }
    
    // Условное кэширование
    @Cacheable(value = "users", 
               key = "#id", 
               condition = "#id > 0", // Кэшировать только если ID > 0
               unless = "#result == null") // Не кэшировать null результаты
    public User findUserConditional(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    // SpEL выражения для сложных ключей
    @Cacheable(value = "userProfiles", 
               key = "#user.id + '_' + #includePrivate.toString()")
    public UserProfile getUserProfile(User user, boolean includePrivate) {
        return profileService.buildProfile(user, includePrivate);
    }
    
    // Sync = true для предотвращения cache stampede
    @Cacheable(value = "expensiveCalculations", 
               key = "#input", 
               sync = true) // Только один поток будет выполнять вычисления
    public Result expensiveCalculation(String input) {
        // Длительная операция
        return performComplexCalculation(input);
    }
}

Cache Stampede — проблема, когда множество потоков одновременно пытаются вычислить один и тот же результат при истечении кэша. sync = true решает эту проблему.

@CacheEvict - удаление из кэша

@Service
public class UserService {
    
    // Удаление конкретного элемента
    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
        // После удаления пользователя очищаем его из кэша
    }
    
    // Очистка всего кэша
    @CacheEvict(value = "users", allEntries = true)
    public void deleteAllUsers() {
        userRepository.deleteAll();
        // Полная очистка кэша users
    }
    
    // Множественная очистка
    @CacheEvict(value = {"users", "userProfiles"}, key = "#user.id")
    public void updateUser(User user) {
        userRepository.save(user);
        // Очищаем пользователя из всех связанных кэшей
    }
    
    // beforeInvocation = true - очистка ДО выполнения метода
    @CacheEvict(value = "users", 
                key = "#id", 
                beforeInvocation = true) // Очистка даже если метод выбросит исключение
    public void dangerousUserOperation(Long id) throws Exception {
        // Рискованная операция, которая может упасть
        riskyRepository.performOperation(id);
    }
    
    // Условная очистка
    @CacheEvict(value = "users", 
                key = "#user.id", 
                condition = "#user.active == false") // Очищать только неактивных
    public void deactivateUser(User user) {
        user.setActive(false);
        userRepository.save(user);
    }
}

@CachePut - обновление кэша

@Service
public class UserService {
    
    // Всегда выполняет метод и обновляет кэш
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        User savedUser = userRepository.save(user);
        // Метод ВСЕГДА выполняется, результат сохраняется в кэш
        return savedUser;
    }
    
    // Обновление нескольких кэшей
    @CachePut(value = "users", key = "#result.id")
    @CacheEvict(value = "usersByDepartment", allEntries = true)
    public User createUser(CreateUserRequest request) {
        User user = new User(request);
        User saved = userRepository.save(user);
        // Новый пользователь добавляется в кэш users
        // Очищается кэш usersByDepartment (т.к. списки изменились)
        return saved;
    }
    
    // Условное обновление
    @CachePut(value = "users", 
              key = "#user.id", 
              condition = "#user.active == true") // Кэшировать только активных
    public User saveUser(User user) {
        return userRepository.save(user);
    }
}

Разница между @Cacheable и @CachePut:

  • @Cacheable — проверяет кэш, если есть данные — возвращает их, иначе выполняет метод
  • @CachePut — всегда выполняет метод и обновляет кэш результатом

Сложные сценарии кэширования

Составные аннотации

@Service
public class ProductService {
    
    // Группировка нескольких cache операций
    @Caching(
        cacheable = {
            @Cacheable(value = "products", key = "#id"),
            @Cacheable(value = "productSummaries", key = "#id + '_summary'")
        },
        evict = {
            @CacheEvict(value = "productCategories", allEntries = true)
        }
    )
    public Product getProductWithSideEffects(Long id) {
        Product product = productRepository.findById(id).orElse(null);
        // Сложная логика с несколькими кэш операциями
        return product;
    }
    
    // Условная группировка
    @Caching(
        cacheable = @Cacheable(value = "products", key = "#id", condition = "#useCache"),
        evict = @CacheEvict(value = "productStats", allEntries = true, condition = "#updateStats")
    )
    public Product getProduct(Long id, boolean useCache, boolean updateStats) {
        return productRepository.findById(id).orElse(null);
    }
}

Кастомные Cache Resolver и Key Generator

@Configuration
public class CustomCacheConfig {
    
    // Кастомный генератор ключей
    @Bean
    public KeyGenerator customKeyGenerator() {
        return (target, method, params) -> {
            StringBuilder key = new StringBuilder();
            key.append(target.getClass().getSimpleName());
            key.append("_").append(method.getName());
            
            for (Object param : params) {
                if (param != null) {
                    key.append("_").append(param.toString());
                }
            }
            
            return key.toString();
        };
    }
    
    // Кастомный cache resolver
    @Bean
    public CacheResolver customCacheResolver(CacheManager cacheManager) {
        return new SimpleCacheResolver(cacheManager) {
            @Override
            protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) {
                // Динамическое определение имени кэша
                String methodName = context.getMethod().getName();
                if (methodName.startsWith("find")) {
                    return Collections.singleton("reads");
                } else if (methodName.startsWith("save") || methodName.startsWith("update")) {
                    return Collections.singleton("writes");
                }
                return super.getCacheNames(context);
            }
        };
    }
}

@Service
public class CustomCacheService {
    
    // Использование кастомного key generator
    @Cacheable(value = "customCache", keyGenerator = "customKeyGenerator")
    public String customCachedMethod(String param1, Integer param2) {
        return "Result for " + param1 + " and " + param2;
    }
    
    // Использование кастомного cache resolver
    @Cacheable(cacheResolver = "customCacheResolver")
    public User findUserCustom(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

Интеграция с Redis

Конфигурация Redis Cache

@Configuration
@EnableCaching
public class RedisCacheConfig {
    
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory factory = new LettuceConnectionFactory(
            new RedisStandaloneConfiguration("localhost", 6379)
        );
        return factory;
    }
    
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
        // Конфигурация для разных кэшей
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        
        // Долгоживущий кэш для пользователей
        cacheConfigurations.put("users", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer())));
        
        // Короткоживущий кэш для сессий
        cacheConfigurations.put("sessions", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .disableCachingNullValues());
        
        // Кэш без TTL для справочников
        cacheConfigurations.put("references", RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer())));
        
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)))
            .withInitialCacheConfigurations(cacheConfigurations)
            .build();
    }
    
    // Для ручной работы с Redis
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

@Service
public class RedisAdvancedService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private CacheManager cacheManager;
    
    // Ручное управление кэшем
    public void manualCacheOperations() {
        Cache userCache = cacheManager.getCache("users");
        
        // Ручное добавление в кэш
        User user = new User(1L, "John");
        userCache.put(1L, user);
        
        // Проверка наличия в кэше
        Cache.ValueWrapper wrapper = userCache.get(1L);
        if (wrapper != null) {
            User cachedUser = (User) wrapper.get();
        }
        
        // Удаление из кэша
        userCache.evict(1L);
        
        // Очистка всего кэша
        userCache.clear();
    }
    
    // Работа с Redis операциями
    public void redisSpecificOperations() {
        // Atomic операции
        redisTemplate.opsForValue().setIfAbsent("lock:user:1", "locked", Duration.ofMinutes(5));
        
        // Списки для кэширования очередей
        redisTemplate.opsForList().rightPush("queue:notifications", "New notification");
        
        // Sets для уникальных значений
        redisTemplate.opsForSet().add("active_users", "user1", "user2", "user3");
        
        // Sorted Sets для рейтингов
        redisTemplate.opsForZSet().add("user_scores", "user1", 100.0);
        
        // Hash для сложных объектов
        redisTemplate.opsForHash().put("user:1", "name", "John");
        redisTemplate.opsForHash().put("user:1", "email", "john@example.com");
    }
}

Кэш статистика и мониторинг

Мониторинг производительности

@Component
public class CacheMonitoringService {
    
    @Autowired
    private CacheManager cacheManager;
    
    @EventListener
    public void handleCacheHit(CacheHitEvent event) {
        log.info("Cache HIT: cache={}, key={}", event.getCacheName(), event.getKey());
        meterRegistry.counter("cache.hit", "cache", event.getCacheName()).increment();
    }
    
    @EventListener
    public void handleCacheMiss(CacheMissEvent event) {
        log.info("Cache MISS: cache={}, key={}", event.getCacheName(), event.getKey());
        meterRegistry.counter("cache.miss", "cache", event.getCacheName()).increment();
    }
    
    @Scheduled(fixedRate = 60000) // Каждую минуту
    public void reportCacheStatistics() {
        for (String cacheName : cacheManager.getCacheNames()) {
            Cache cache = cacheManager.getCache(cacheName);
            
            if (cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
                com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache = 
                    (com.github.benmanes.caffeine.cache.Cache<Object, Object>) cache.getNativeCache();
                
                CacheStats stats = caffeineCache.stats();
                
                // Метрики для Micrometer
                meterRegistry.gauge("cache.size", Tags.of("cache", cacheName), caffeineCache.estimatedSize());
                meterRegistry.gauge("cache.hit.ratio", Tags.of("cache", cacheName), stats.hitRate());
                meterRegistry.gauge("cache.eviction.count", Tags.of("cache", cacheName), stats.evictionCount());
                meterRegistry.gauge("cache.load.average.time", Tags.of("cache", cacheName), stats.averageLoadPenalty());
                
                log.info("Cache {} stats: size={}, hitRate={:.2f}, evictions={}", 
                    cacheName, caffeineCache.estimatedSize(), stats.hitRate(), stats.evictionCount());
            }
        }
    }
    
    // Health check для кэшей
    @Component
    public class CacheHealthIndicator implements HealthIndicator {
        
        @Override
        public Health health() {
            try {
                for (String cacheName : cacheManager.getCacheNames()) {
                    Cache cache = cacheManager.getCache(cacheName);
                    // Простая проверка доступности кэша
                    cache.get("health-check");
                }
                return Health.up()
                    .withDetail("caches", cacheManager.getCacheNames())
                    .build();
            } catch (Exception e) {
                return Health.down()
                    .withDetail("error", e.getMessage())
                    .build();
            }
        }
    }
}

Лучшие практики и паттерны

Cache-Aside Pattern

@Service
public class CacheAsideService {
    
    @Autowired
    private CacheManager cacheManager;
    
    @Autowired
    private UserRepository userRepository;
    
    // Реализация Cache-Aside вручную для полного контроля
    public User findUserCacheAside(Long id) {
        Cache cache = cacheManager.getCache("users");
        
        // 1. Проверка кэша
        Cache.ValueWrapper wrapper = cache.get(id);
        if (wrapper != null) {
            return (User) wrapper.get();
        }
        
        // 2. Загрузка из БД
        User user = userRepository.findById(id).orElse(null);
        
        // 3. Сохранение в кэш
        if (user != null) {
            cache.put(id, user);
        }
        
        return user;
    }
    
    // Write-Through паттерн
    public User saveUserWriteThrough(User user) {
        // 1. Сохранение в БД
        User savedUser = userRepository.save(user);
        
        // 2. Обновление кэша
        Cache cache = cacheManager.getCache("users");
        cache.put(savedUser.getId(), savedUser);
        
        return savedUser;
    }
    
    // Write-Behind (асинхронная запись)
    @Async
    public CompletableFuture<Void> saveUserWriteBehind(User user) {
        // Немедленное обновление кэша
        Cache cache = cacheManager.getCache("users");
        cache.put(user.getId(), user);
        
        // Асинхронное сохранение в БД
        return CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(100); // Имитация batch операции
                userRepository.save(user);
            } catch (Exception e) {
                // Откат изменений в кэше при ошибке
                cache.evict(user.getId());
                throw new RuntimeException(e);
            }
        });
    }
}

Многоуровневое кэширование

@Configuration
public class MultiLevelCacheConfig {
    
    @Bean
    @Primary
    public CacheManager multiLevelCacheManager() {
        // L1 Cache - быстрый локальный кэш (Caffeine)
        CaffeineCacheManager l1CacheManager = new CaffeineCacheManager();
        l1CacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(Duration.ofMinutes(5)));
        
        // L2 Cache - распределенный кэш (Redis)
        RedisCacheManager l2CacheManager = RedisCacheManager.builder(redisConnectionFactory())
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1)))
            .build();
        
        return new MultiLevelCacheManager(l1CacheManager, l2CacheManager);
    }
}

public class MultiLevelCacheManager implements CacheManager {
    
    private final CacheManager l1CacheManager; // Локальный кэш
    private final CacheManager l2CacheManager; // Распределенный кэш
    
    public MultiLevelCacheManager(CacheManager l1, CacheManager l2) {
        this.l1CacheManager = l1;
        this.l2CacheManager = l2;
    }
    
    @Override
    public Cache getCache(String name) {
        Cache l1Cache = l1CacheManager.getCache(name);
        Cache l2Cache = l2CacheManager.getCache(name);
        return new MultiLevelCache(name, l1Cache, l2Cache);
    }
    
    @Override
    public Collection<String> getCacheNames() {
        Set<String> names = new HashSet<>();
        names.addAll(l1CacheManager.getCacheNames());
        names.addAll(l2CacheManager.getCacheNames());
        return names;
    }
}

public class MultiLevelCache implements Cache {
    
    private final String name;
    private final Cache l1Cache;
    private final Cache l2Cache;
    
    @Override
    public ValueWrapper get(Object key) {
        // 1. Проверка L1 кэша
        ValueWrapper l1Value = l1Cache.get(key);
        if (l1Value != null) {
            return l1Value;
        }
        
        // 2. Проверка L2 кэша
        ValueWrapper l2Value = l2Cache.get(key);
        if (l2Value != null) {
            // Обратная загрузка в L1
            l1Cache.put(key, l2Value.get());
            return l2Value;
        }
        
        return null;
    }
    
    @Override
    public void put(Object key, Object value) {
        // Сохранение в оба уровня
        l1Cache.put(key, value);
        l2Cache.put(key, value);
    }
    
    @Override
    public void evict(Object key) {
        // Удаление из обоих уровней
        l1Cache.evict(key);
        l2Cache.evict(key);
    }
}

Частые вопросы на собеседовании

Q: В чем разница между @Cacheable и @CachePut? A: @Cacheable проверяет кэш и пропускает выполнение метода если данные есть. @CachePut всегда выполняет метод и обновляет кэш результатом.

Q: Как решить проблему cache stampede? A: Использовать sync = true в @Cacheable для блокировки параллельных вычислений одного ключа или distributed locks в Redis.

Q: Когда использовать beforeInvocation = true в @CacheEvict? A: Когда нужно очистить кэш даже если метод выбросит исключение, например при критических операциях изменения данных.

Q: Как выбрать между локальным кэшем (Caffeine) и распределенным (Redis)? A: Локальный для часто используемых данных в single-instance приложениях. Распределенный для shared данных между instances или когда нужна персистентность кэша.

Q: Что такое cache warm-up и как его реализовать? A: Предварительная загрузка критических данных в кэш при старте приложения через @PostConstruct методы или scheduled tasks.

Q: Как обеспечить консистентность между кэшем и базой данных? A: Write-Through для критических данных, cache invalidation при изменениях, TTL для автоматического истечения, event-driven cache updates.