🧠 Spring Boot: как НЕ попасть в ловушку с
Когда используете
📌 Если таких задач несколько или они работают долго — остальные ждут. Это создаёт бутылочное горлышко и приводит к неожиданным задержкам.
💡 Решение: настроить кастомный executor:
Теперь все
⚠️ Не забывайте: если задача критична к ресурсам или зависит от внешних сервисов — добавьте внутреннюю защиту от повторного выполнения, например, с помощью Redis lock или базы.
✅ Подключение пула — must-have для production-проектов, где
📲 Мы в MAX
👉@BookJava
@Scheduled и многопоточностьюКогда используете
@Scheduled для периодических задач в Spring Boot, важно понимать: по умолчанию все задачи выполняются в одном потоке.
@Scheduled(fixedRate = 5000)
public void syncData() {
// долгая операция
}
📌 Если таких задач несколько или они работают долго — остальные ждут. Это создаёт бутылочное горлышко и приводит к неожиданным задержкам.
💡 Решение: настроить кастомный executor:
@Configuration
@EnableScheduling
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // количество параллельных задач
scheduler.setThreadNamePrefix("scheduled-task-");
scheduler.initialize();
registrar.setTaskScheduler(scheduler);
}
}
Теперь все
@Scheduled - методы будут использовать пул потоков, а не один.⚠️ Не забывайте: если задача критична к ресурсам или зависит от внешних сервисов — добавьте внутреннюю защиту от повторного выполнения, например, с помощью Redis lock или базы.
✅ Подключение пула — must-have для production-проектов, где
@Scheduled выполняет реальные задачи, а не просто println.📲 Мы в MAX
👉@BookJava
👍4🔥1
🧠 Spring Boot: правильно логируем
Сейчас покажу простой, но часто упускаемый момент при обработке ошибок в Spring Boot.
Если у вас есть глобальный
❌ Плохо:
✅ Хорошо:
📌
* Сохранять stacktrace для дебага;
* Не терять вложенные причины (
* Работать с лог-агрегаторами (ELK, Grafana, etc).
💡 Если вы используете Slf4j и формат
⚠️ И не забывайте: глобальные хендлеры — это хорошо, но не глушите все ошибки без разбора. Лучше создавать разные
📲 Мы в MAX
👉@BookJava
@ExceptionHandlerСейчас покажу простой, но часто упускаемый момент при обработке ошибок в Spring Boot.
Если у вас есть глобальный
@ExceptionHandler, убедитесь, что вы не теряете stacktrace при логировании.❌ Плохо:
@ExceptionHandler(MyException.class)
public ResponseEntity<String> handle(MyException ex) {
log.error("MyException occurred: {}", ex.getMessage()); // stacktrace теряется!
return ResponseEntity.status(500).body("Error");
}
✅ Хорошо:
@ExceptionHandler(MyException.class)
public ResponseEntity<String> handle(MyException ex) {
log.error("MyException occurred", ex); // stacktrace будет видно в логе
return ResponseEntity.status(500).body("Error");
}
📌
log.error(String, Throwable) — правильный способ логировать исключения. Это позволяет:* Сохранять stacktrace для дебага;
* Не терять вложенные причины (
getCause());* Работать с лог-агрегаторами (ELK, Grafana, etc).
💡 Если вы используете Slf4j и формат
log.error("message: {}", ex.getMessage()), вы теряете почти всю полезную информацию об ошибке.⚠️ И не забывайте: глобальные хендлеры — это хорошо, но не глушите все ошибки без разбора. Лучше создавать разные
@ExceptionHandler под каждую категорию исключений.📲 Мы в MAX
👉@BookJava
👍3🔥1
🧠 Неочевидный performance-трюк с
Многие используют
📌 Что делает
* Подсказывает
* Это позволяет избежать затрат на грязную проверку (dirty checking).
* Не создаётся snapshot состояний сущностей → меньше памяти и операций.
💡 Пример:
⚠️ А теперь важно: если вы случайно измените сущность в таком методе, Hibernate проигнорирует изменения — потому что readOnly намекает: "не трогай".
📉 В реальном приложении с большим количеством запросов к БД, особенно читающих, такой подход даёт ощутимый буст — меньше нагрузки на ORM, меньше GC, быстрее ответы.
📌 Где применять:
* Методы только для чтения.
* REST-эндпоинты GET.
* Сервис-методы, возвращающие DTO и не модифицирующие Entity.
⚠️ Где не надо:
* Методы с lazy-loading и последующими модификациями.
* Там, где возможно случайное изменение Entity (например, через mapper'ы).
👉 Используйте
📲 Мы в MAX
👉@BookJava
@Transactional(readOnly = true)Многие используют
@Transactional(readOnly = true) просто по инерции. Но вы знали, что в Spring это влияет не только на семантику, но и на производительность?📌 Что делает
readOnly = true:* Подсказывает
Hibernate, что внутри транзакции не будет изменений сущностей.* Это позволяет избежать затрат на грязную проверку (dirty checking).
* Не создаётся snapshot состояний сущностей → меньше памяти и операций.
💡 Пример:
@Transactional(readOnly = true)
public List<User> findActiveUsers() {
return userRepository.findByActiveTrue();
}
⚠️ А теперь важно: если вы случайно измените сущность в таком методе, Hibernate проигнорирует изменения — потому что readOnly намекает: "не трогай".
📉 В реальном приложении с большим количеством запросов к БД, особенно читающих, такой подход даёт ощутимый буст — меньше нагрузки на ORM, меньше GC, быстрее ответы.
📌 Где применять:
* Методы только для чтения.
* REST-эндпоинты GET.
* Сервис-методы, возвращающие DTO и не модифицирующие Entity.
⚠️ Где не надо:
* Методы с lazy-loading и последующими модификациями.
* Там, где возможно случайное изменение Entity (например, через mapper'ы).
👉 Используйте
@Transactional(readOnly = true) не как декор, а как инструмент для оптимизации.📲 Мы в MAX
👉@BookJava
👍2🔥1
🧠 Трюк с
Когда вы выносите конфигурацию в отдельный модуль или создаёте библиотеку с
📌 Spring Boot 3+ по умолчанию НЕ сканирует пакеты вне стартового (
Пример:
И вы такие:
🔥 Но
💡 Потому что
📌 Решения:
1. Ручной импорт конфигурации:
2. Сделать конфигурацию
3. Переместить
⚠️ Часто баг проявляется неявно: контекст стартует, но бины "теряются", и вы получаете
✅ После миграции на Spring Boot 3+ обязательно проверьте, что нужные конфигурации действительно подхватываются. Особенно, если раньше они подключались "магически".
📲 Мы в MAX
👉@BookJava
@Configuration и @ComponentScan: как не словить баг при миграции на Spring Boot 3+Когда вы выносите конфигурацию в отдельный модуль или создаёте библиотеку с
@Configuration-классами — не забывайте:📌 Spring Boot 3+ по умолчанию НЕ сканирует пакеты вне стартового (
main).Пример:
// Внутри библиотеки
@Configuration
public class MyLibConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
И вы такие:
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
🔥 Но
MyService не создаётся! Почему?💡 Потому что
@ComponentScan по умолчанию сканирует ТОЛЬКО package текущего класса и ниже.📌 Решения:
1. Ручной импорт конфигурации:
@SpringBootApplication
@Import(MyLibConfig.class)
public class App { ... }
2. Сделать конфигурацию
@AutoConfiguration и подключить через spring.factories или META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports — актуально для библиотек.3. Переместить
MyLibConfig в подпакет стартового класса (не всегда возможно или удобно).⚠️ Часто баг проявляется неявно: контекст стартует, но бины "теряются", и вы получаете
NoSuchBeanDefinitionException в рантайме.✅ После миграции на Spring Boot 3+ обязательно проверьте, что нужные конфигурации действительно подхватываются. Особенно, если раньше они подключались "магически".
📲 Мы в MAX
👉@BookJava
👍2🔥2
🔧 Maven vs. Gradle: что выбрать разработчику?
Когда речь заходит о сборке Java-проектов, выбор обычно падает на два главных инструмента: Maven и Gradle. Оба давно стали стандартом индустрии, но каждый имеет свои особенности. Разберёмся, что выбрать 👇
☕ Maven — проверенная классика
✅ Строгая структура: легче читать и сопровождать
✅ Надёжность и предсказуемость сборки
✅ Большое комьюнити и множество плагинов
⚠️ XML-конфигурация громоздкая
⚠️ Медленнее по сравнению с Gradle
⚡ Gradle — гибкость и скорость
✅ Поддержка Kotlin и Groovy DSL
✅ Инкрементальные сборки и кэширование → быстрее
✅ Более гибкий подход к настройке
⚠️ Порог входа выше
⚠️ Иногда сложно отлаживать конфигурацию
💡 Вывод:
* 🔹 Выбирай Maven, если важны стабильность, простота и читаемость.
* 🔹 Выбирай Gradle, если хочешь максимум производительности и гибкости.
🎯 В крупных проектах Gradle становится всё популярнее, особенно при использовании Kotlin. Но в enterprise-среде Maven по-прежнему правит бал.
📲 Мы в MAX
👉@BookJava
Когда речь заходит о сборке Java-проектов, выбор обычно падает на два главных инструмента: Maven и Gradle. Оба давно стали стандартом индустрии, но каждый имеет свои особенности. Разберёмся, что выбрать 👇
☕ Maven — проверенная классика
✅ Строгая структура: легче читать и сопровождать
✅ Надёжность и предсказуемость сборки
✅ Большое комьюнити и множество плагинов
⚠️ XML-конфигурация громоздкая
⚠️ Медленнее по сравнению с Gradle
⚡ Gradle — гибкость и скорость
✅ Поддержка Kotlin и Groovy DSL
✅ Инкрементальные сборки и кэширование → быстрее
✅ Более гибкий подход к настройке
⚠️ Порог входа выше
⚠️ Иногда сложно отлаживать конфигурацию
💡 Вывод:
* 🔹 Выбирай Maven, если важны стабильность, простота и читаемость.
* 🔹 Выбирай Gradle, если хочешь максимум производительности и гибкости.
🎯 В крупных проектах Gradle становится всё популярнее, особенно при использовании Kotlin. Но в enterprise-среде Maven по-прежнему правит бал.
📲 Мы в MAX
👉@BookJava
👍4🔥1
2 сентября(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Java-разработчика.
Как это будет:
📂 Сергей Чамкин, старший разработчик из Uzum, ex-WildBerries, будет задавать реальные вопросы и задачи разработчику-добровольцу
📂 Cергей будет комментировать каждый ответ респондента, чтобы дать понять чего от вас ожидает собеседующий на интервью
📂 В конце можно будет задать любой вопрос Сергею
Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Java-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.
Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_sh_bot
Реклама.
О рекламодателе.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
💡 Чем опасен
Расписание в Spring через
📌 Пример проблемы:
🧨 Каждые 10 секунд метод запускается заново. Если выполнение предыдущего ещё не закончено, начнётся второй поток, который заберёт те же
В итоге — дублирование обработки, гонки, повреждение данных.
📉 Особенно критично при долгих задачах или высокой нагрузке.
✅ Решение — обернуть метод в транзакцию + использовать блокировки:
📌 Или добавить флаг “locked”, чтобы явно помечать взятые задачи.
💡 Лучше использовать
🧠 Подумайте о том, чтобы заменить
* Spring Batch (если сложные джобы)
* Spring Integration / Flowable / Camunda (если нужны гарантии и retry)
* Quartz (если нужен контроль и очереди)
📲 Мы в MAX
👉@BookJava
@Scheduled(fixedRate) без @Transactional?Расписание в Spring через
@Scheduled — удобный способ запускать задачи по таймеру. Но часто разработчики забывают про транзакции, особенно с fixedRate, и попадают в ловушку.📌 Пример проблемы:
@Scheduled(fixedRate = 10_000)
public void cleanUp() {
List<Job> jobs = jobRepository.findAllByStatus(PENDING);
jobs.forEach(job -> {
job.setStatus(PROCESSING);
jobRepository.save(job);
});
}
🧨 Каждые 10 секунд метод запускается заново. Если выполнение предыдущего ещё не закончено, начнётся второй поток, который заберёт те же
PENDING -записи.В итоге — дублирование обработки, гонки, повреждение данных.
📉 Особенно критично при долгих задачах или высокой нагрузке.
✅ Решение — обернуть метод в транзакцию + использовать блокировки:
@Transactional
@Scheduled(fixedRate = 10_000)
public void cleanUp() {
List<Job> jobs = jobRepository.findAllByStatusForUpdate(PENDING); // SELECT ... FOR UPDATE
jobs.forEach(job -> {
job.setStatus(PROCESSING);
jobRepository.save(job);
});
}
📌 Или добавить флаг “locked”, чтобы явно помечать взятые задачи.
💡 Лучше использовать
@Scheduled(fixedDelay) — он ждёт завершения предыдущего запуска. Это безопаснее по умолчанию.🧠 Подумайте о том, чтобы заменить
@Scheduled на:* Spring Batch (если сложные джобы)
* Spring Integration / Flowable / Camunda (если нужны гарантии и retry)
* Quartz (если нужен контроль и очереди)
📲 Мы в MAX
👉@BookJava
❤2👍2🔥1
🧠 Spring Boot: ленивые зависимости через
Иногда сервису не нужно всегда инжектить другую зависимость при старте — только иногда по ходу работы. Но
💡 Решение: использовать
Пример:
📌
▫️ не создаёт бин сразу — ленивый доступ;
▫️ позволяет проверить наличие бина (
▫️ можно использовать
⚠️ Это не альтернатива DI. Это способ контролировать создание и использование бинов вручную, когда это действительно нужно.
📈 Отлично помогает:
▫️ при борьбе с циклическими зависимостями;
▫️ для optional-бинов;
▫️ чтобы ускорить старт приложения.
📲 Мы в MAX
👉@BookJava
ObjectProviderИногда сервису не нужно всегда инжектить другую зависимость при старте — только иногда по ходу работы. Но
@Autowired всё равно тянет её сразу, даже если она вам пока не нужна. Это бьёт по времени старта и может вызвать циклические зависимости.💡 Решение: использовать
ObjectProvider<T>.Пример:
@Service
public class NotificationService {
private final ObjectProvider<EmailSender> emailSenderProvider;
public NotificationService(ObjectProvider<EmailSender> emailSenderProvider) {
this.emailSenderProvider = emailSenderProvider;
}
public void sendEmailIfEnabled(String to, String body) {
if (featureEnabled()) {
EmailSender sender = emailSenderProvider.getIfAvailable();
if (sender != null) {
sender.send(to, body);
}
}
}
}
📌
ObjectProvider:getIfAvailable() / ifAvailable(...));stream() — для коллекций бинов.⚠️ Это не альтернатива DI. Это способ контролировать создание и использование бинов вручную, когда это действительно нужно.
📈 Отлично помогает:
📲 Мы в MAX
👉@BookJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🧠 Как не словить
Одна из самых частых ошибок при работе с JPA:
📌 Причина: лениво загружаемая коллекция (
💡 Как избежать?
✅ Решение 1:
Убедитесь, что вы обращаетесь к ленивым коллекциям внутри метода с
⚠️ Не используйте
✅ Решение 2: Fetch Join
Подгрузите нужные данные сразу через
📌 Плюс: 1 запрос вместо N (N+1 проблема решается).
📌 Минус: может быть избыточная загрузка, особенно с большими коллекциями.
✅ Решение 3: DTO проекция
Лучший способ в большинстве случаев - проецировать сразу в DTO:
📌 Выгружает только нужные данные. Быстро, безопасно, эффективно.
Ленивая инициализация - ок, если вы контролируете границы транзакций.
Проекции и
📲 Мы в MAX
👉@BookJava
LazyInitializationException в Spring Boot + HibernateОдна из самых частых ошибок при работе с JPA:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection
📌 Причина: лениво загружаемая коллекция (
LAZY) обращается к БД вне транзакции — например, в слое контроллера или после закрытия Session.💡 Как избежать?
✅ Решение 1:
@Transactional в сервисеУбедитесь, что вы обращаетесь к ленивым коллекциям внутри метода с
@Transactional:
@Transactional
public UserDto getUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow();
// OK: коллекция friends будет инициализирована в транзакции
return new UserDto(user.getName(), user.getFriends());
}
⚠️ Не используйте
@Transactional в контроллерах - это плохая практика.✅ Решение 2: Fetch Join
Подгрузите нужные данные сразу через
JOIN FETCH:
@Query("SELECT u FROM User u LEFT JOIN FETCH u.friends WHERE u.id = :id")
Optional<User> findByIdWithFriends(@Param("id") Long id);
📌 Плюс: 1 запрос вместо N (N+1 проблема решается).
📌 Минус: может быть избыточная загрузка, особенно с большими коллекциями.
✅ Решение 3: DTO проекция
Лучший способ в большинстве случаев - проецировать сразу в DTO:
@Query("""
SELECT new com.example.UserDto(u.name, f.name)
FROM User u
LEFT JOIN u.friends f
WHERE u.id = :id
""")
List<UserDto> findUserWithFriendNames(@Param("id") Long id);
📌 Выгружает только нужные данные. Быстро, безопасно, эффективно.
Ленивая инициализация - ок, если вы контролируете границы транзакций.
Проекции и
fetch join - ваши лучшие друзья, если нужен контроль и производительность.📲 Мы в MAX
👉@BookJava
👍2
Приглашаем на открытый урок.
🗓 21 сентября в 20:00 МСК
🆓 Бесплатно. Урок в рамках старта курса «Java разработчик. Продвинутый уровень».
Разберем, как браузер общается с сервером, и создадим работающий HTTP-сервис без Spring и сторонних библиотек.
О чем поговорим:
🔗 Ссылка на регистрацию: https://vk.cc/d1sLwn
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🧠 Трюк с
Когда тебе нужно обрабатывать события в рамках транзакции, мы часто пишем:
⚠️ Но есть нюанс:
📌 Альтернатива: обычный
💡 Сниппет:
📎 Плюсы:
— Лучше контроль: ты сам решаешь, когда обрабатывать (до/после/вне транзакции)
— Можно централизовать поведение через utility-метод
— Гибкость: логика обработки не зависит от аннотаций Spring'а
⚠️ Минус: чуть больше кода, но понятнее поведение.
📲 Мы в MAX
👉@BookJava
@EventListener в Spring — убираем лишний @TransactionalEventListenerКогда тебе нужно обрабатывать события в рамках транзакции, мы часто пишем:
@TransactionalEventListener
public void handleEvent(MyEvent event) {
// ...
}
⚠️ Но есть нюанс:
@TransactionalEventListener по умолчанию срабатывает после коммита. Иногда это не очевидно и вызывает баги, особенно если ожидаешь, что событие обработается внутри транзакции.📌 Альтернатива: обычный
@EventListener, но вместе с TransactionSynchronizationManager.💡 Сниппет:
@Component
public class MyEventHandler {
@EventListener
public void handle(MyEvent event) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// обработка события после коммита
}
}
);
} else {
// fallback: нет активной транзакции — выполняем сразу
}
}
}
📎 Плюсы:
— Лучше контроль: ты сам решаешь, когда обрабатывать (до/после/вне транзакции)
— Можно централизовать поведение через utility-метод
— Гибкость: логика обработки не зависит от аннотаций Spring'а
⚠️ Минус: чуть больше кода, но понятнее поведение.
📲 Мы в MAX
👉@BookJava
👍2🔥1