Библиотека Java разработчика
📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate. По всем вопросам @evgenycarter РКН clck.ru/3KoGeP
Show more📈 Analytical overview of Telegram channel Библиотека Java разработчика
Channel Библиотека Java разработчика (@bookjava) in the Russian language segment is an active participant. Currently, the community unites 10 115 subscribers, ranking 11 697 in the Technologies & Applications category and 62 901 in the Russia region.
📊 Audience metrics and dynamics
Since its creation on невідомо, the project has demonstrated rapid growth, gathering an audience of 10 115 subscribers.
According to the latest data from 27 August, 2026, the channel demonstrates stable activity. Although there has been a change in the number of participants by -73 over the last 30 days and by -3 over the last 24 hours, overall reach remains high.
- Verification status: Not verified
- Engagement rate (ER): The average audience engagement rate is 7.20%. Within the first 24 hours after publication, content typically collects 3.71% reactions from the total number of subscribers.
- Post reach: On average, each post receives 729 views. Within the first day, a publication typically gains 375 views.
- Reactions and interaction: The audience actively supports content: the average number of reactions per post is 7.
- Thematic interests: Content is focused on key topics such as string, интерфейс, строка, boot, api.
📝 Description and content policy
The author describes the resource as a platform for expressing subjective opinions:
“📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate.
По всем вопросам @evgenycarter
РКН clck.ru/3KoGeP”
Thanks to the high frequency of updates (latest data received on 28 August, 2026), the channel maintains relevance and a high level of publication reach. Analytics show that the audience actively interacts with content, making it an important point of influence in the Technologies & Applications category.
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 — ваши лучшие друзья, если нужен контроль и производительность.
👉@BookJava@Transactional и self-invocation в Spring
Вы знали, что вызов метода с @Transactional внутри того же класса не активирует транзакцию? Это один из самых частых подводных камней.
📌 Почему так происходит?
Spring AOP работает через прокси. Когда вы вызываете метод this.someTransactionalMethod(), вы обходите прокси и вызываете метод напрямую — без обёртки, которая включает транзакцию.
Пример:
@Service
public class UserService {
public void createUser() {
// транзакция не начнётся
this.saveUser();
}
@Transactional
public void saveUser() {
// ожидаем, что тут будет транзакция, но её нет
}
}
⚠️ Итог: никакой транзакции, и вы получите баг, который сложно заметить: данные не откатываются, lazy-loading кидает LazyInitializationException, и т.д.
💡 Как правильно:
1. Вынести transactional-метод в другой бин:
@Service
public class UserSaver {
@Transactional
public void saveUser() { ... }
}
2. Или получить прокси текущего бина через AopContext:
public void createUser() {
((UserService) AopContext.currentProxy()).saveUser();
}
📌 Для второго варианта не забудьте включить:
@EnableAspectJAutoProxy(exposeProxy = true)
⚠️ Важно: Не используйте AopContext без крайней необходимости. Предпочтительнее делегировать логику в отдельный бин — это чище и легче тестируется.
👉@BookJavaObjectProvider
Иногда сервису не нужно всегда инжектить другую зависимость при старте — только иногда по ходу работы. Но @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. Это способ контролировать создание и использование бинов вручную, когда это действительно нужно.
📈 Отлично помогает:
▫️при борьбе с циклическими зависимостями;
▫️для optional-бинов;
▫️чтобы ускорить старт приложения.
👉@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 (если нужен контроль и очереди)
👉@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+ обязательно проверьте, что нужные конфигурации действительно подхватываются. Особенно, если раньше они подключались "магически".
👉@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) не как декор, а как инструмент для оптимизации.
👉@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 под каждую категорию исключений.
👉@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.
👉@BookJava@PostConstruct — особенно в проде
Сейчас расскажу, почему инициализировать важную бизнес-логику в @PostConstruct — плохая идея.
Типичный пример:
@Component
public class CacheLoader {
private final SomeService service;
public CacheLoader(SomeService service) {
this.service = service;
}
@PostConstruct
public void init() {
service.loadDataIntoCache(); // ⚠️ обращение к БД
}
}
🧨 Проблема: @PostConstruct вызывается до того, как приложение полностью поднялось.
Если внутри будет ошибка (например, БД недоступна) — приложение может упасть, или что хуже — запуститься в полурабочем состоянии.
📌 Кроме того:
* ❌ Нет контроля над порядком выполнения таких методов;
* ❌ Нельзя легко переиспользовать эту логику (например, вручную перезагрузить кеш);
* ❌ В тестах или dev-среде — такие вызовы часто мешают.
✅ Современный подход — использовать ApplicationListener:
@Component
public class CacheLoader implements ApplicationListener<ApplicationReadyEvent> {
private final SomeService service;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
service.loadDataIntoCache(); // 👍 вызывается только после старта
}
}
📌 Альтернатива — аннотация @EventListener:
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
// безопасно загружаем данные
}
📦 В Spring Boot это нативный и рекомендованный способ выполнения кода после старта.
🧠 Резюме:
🔹 @PostConstruct — только для простой инициализации бинов.
🔹 Бизнес-логику и I/O — в @EventListener(ApplicationReadyEvent.class).
👉@BookJava