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
- сохранение связанных entitiesMERGE
- обновление связанных entitiesREMOVE
- удаление связанных entitiesALL
- все операции
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
- корневая entityPredicate
- условия 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
— защита от ClickjackingHSTS
— принудительное использование HTTPSCSP
— защита от 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 предоставляет мощные инструменты для защиты приложений. Ключевые принципы:
- Начинайте с простой конфигурации — добавляйте сложность постепенно
- Всегда хешируйте пароли — используйте BCryptPasswordEncoder
- Правильно настраивайте CSRF — включайте для форм, отключайте для API
- Используйте HTTPS в продакшене — особенно для аутентификации
- Логируйте события безопасности — для мониторинга и аудита
- Применяйте принцип минимальных привилегий — давайте только необходимые права
Правильная настройка 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 предоставляет мощные инструменты для создания отказоустойчивых приложений. Ключевые принципы:
- Начинайте с Circuit Breaker — самый важный паттерн для микросервисов
- Добавляйте Retry осторожно — может усугубить проблемы при неправильной настройке
- Используйте Rate Limiter — для защиты от перегрузки
- Мониторьте метрики — для понимания поведения системы
- Настраивайте под конкретные сервисы — универсальных настроек не существует
Правильная конфигурация 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.