Библиотека Java разработчика
📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate. По всем вопросам @evgenycarter РКН clck.ru/3KoGeP
Mostrar más📈 Análisis del canal de Telegram Библиотека Java разработчика
El canal Библиотека Java разработчика (@bookjava) en el segmento lingüístico de Ruso es un actor destacado. Actualmente la comunidad reúne a 10 280 suscriptores, ocupando la posición 12 030 en la categoría Tecnologías y Aplicaciones y el puesto 63 913 en la región Rusia.
📊 Métricas de audiencia y dinámica
Desde su creación el невідомо, el proyecto ha mostrado un crecimiento acelerado, reuniendo a 10 280 suscriptores.
Según los últimos datos del 05 junio, 2026, el canal mantiene una actividad estable. En los últimos 30 días la variación de miembros fue de 20, y en las últimas 24 horas de 0, conservando un alto alcance.
- Estado de verificación: No verificado
- Tasa de interacción (ER): El promedio de interacción de la audiencia es 8.29%. Durante las primeras 24 horas tras publicar, el contenido suele obtener 3.77% de reacciones respecto al total de suscriptores.
- Alcance de las publicaciones: Cada publicación recibe en promedio 852 visualizaciones. En el primer día suele acumular 388 visualizaciones.
- Reacciones e interacción: La audiencia responde de forma activa: el promedio de reacciones por publicación es 6.
- Intereses temáticos: El contenido se centra en temas clave como string, интерфейс, строка, boot, api.
📝 Descripción y política de contenido
El autor describe el recurso como un espacio para expresar opiniones subjetivas:
“📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate.
По всем вопросам @evgenycarter
РКН clck.ru/3KoGeP”
Gracias a la alta frecuencia de actualizaciones (últimos datos recibidos el 06 junio, 2026), el canal mantiene la vigencia y un amplio alcance. La analítica demuestra que la audiencia interactúa activamente con el contenido, lo que lo convierte en un punto de referencia dentro de la categoría Tecnologías y Aplicaciones.
@Value vs @ConfigurationProperties — не выбирай наобум
Оба способа хороши для конфигурации, но используют их по-разному. И если ты всё ещё везде пихаешь @Value, держи краткий гайд, когда лучше что:
📌 @Value — просто, но не гибко:
@Value("${my.prop}")
private String value;
✅ Хорошо для единичных значений
❌ Плохо для сложных структур, списков, валидации
❌ Трудно покрыть тестами (без TestPropertySource)
❌ Нет биндинга по префиксу → нет группировки
📌 @ConfigurationProperties — сила и масштаб:
@ConfigurationProperties(prefix = "app.feature")
public class FeatureProperties {
private boolean enabled;
private List<String> items;
}
💡 Используй с @EnableConfigurationProperties или аннотируй как @Component
✅ Удобно группировать и документировать
✅ Работает с вложенными структурами, коллекциями
✅ Поддерживает JSR-303 валидацию (@Validated)
✅ Легче мокать в тестах
✅ Интеграция с Spring Boot Actuator (/actuator/configprops)
⚠️ Не смешивай: не нужно тянуть @Value внутрь @ConfigurationProperties — это антипаттерн.
💬 Если конфигурация простая — @Value норм. Но как только появляется структура, коллекции, логика — всегда используй @ConfigurationProperties.
👉@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'а
⚠️ Минус: чуть больше кода, но понятнее поведение.
👉@BookJavaLazyInitializationException в 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
¡Ya disponible! Investigación de Telegram 2025 — los principales insights del año 
