Библиотека Java разработчика
📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate. По всем вопросам @evgenycarter РКН clck.ru/3KoGeP
إظهار المزيد📈 نظرة تحليلية على قناة تيليجرام Библиотека Java разработчика
تُعد قناة Библиотека Java разработчика (@bookjava) في القطاع اللغوي الروسية لاعباً نشطاً. يضم المجتمع حالياً 10 280 مشتركاً، محتلاً المرتبة 12 030 في فئة التكنولوجيات والتطبيقات والمرتبة 63 913 في منطقة روسيا.
📊 مؤشرات الجمهور والحراك
منذ تأسيسه في невідомо، حقق المشروع نمواً سريعاً وجمع 10 280 مشتركاً.
بحسب آخر البيانات بتاريخ 05 يونيو, 2026، تحافظ القناة على نشاط مستقر. خلال آخر 30 يوماً تغيّر عدد الأعضاء بمقدار 20، وفي آخر 24 ساعة بمقدار 0، مع بقاء الوصول العام مرتفعاً.
- حالة التحقق: غير موثّقة
- معدل التفاعل (ER): يبلغ متوسط تفاعل الجمهور 8.29%. وخلال أول 24 ساعة من النشر يحصد المحتوى عادةً 3.77% من ردود الفعل نسبةً إلى إجمالي المشتركين.
- وصول المنشورات: يحصل كل منشور على متوسط 852 مشاهدة. وخلال اليوم الأول يجمع عادةً 388 مشاهدة.
- التفاعلات والاستجابة: يتفاعل الجمهور بانتظام؛ متوسط التفاعلات لكل منشور يبلغ 6.
- الاهتمامات الموضوعية: يركز المحتوى على مواضيع رئيسية مثل string, интерфейс, строка, boot, api.
📝 الوصف وسياسة المحتوى
يصف المؤلف القناة بأنها مساحة للتعبير عن الآراء الذاتية:
“📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate.
По всем вопросам @evgenycarter
РКН clck.ru/3KoGeP”
بفضل وتيرة التحديث المرتفعة (أحدث البيانات بتاريخ 06 يونيو, 2026) تحافظ القناة على حداثتها ومستوى وصول مرتفع. وتُظهر التحليلات تفاعلاً نشطاً من الجمهور، ما يجعلها نقطة تأثير مهمة ضمن فئة التكنولوجيات والتطبيقات.
@Configuration
public class BulkheadConfig {
@Bean("serviceAExecutor")
public ThreadPoolTaskExecutor serviceAExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("svcA-");
executor.initialize();
return executor;
}
}
В коде контроллера или сервиса указываем:
@Service
public class ServiceA {
@Autowired @Qualifier("serviceAExecutor")
private Executor executor;
public CompletableFuture<String> callExternal() {
return CompletableFuture.supplyAsync(() -> {
// долгий/ненадежный вызов
return externalClient.fetchData();
}, executor);
}
}
⚠️ Помните: если пул заполнится, новые задачи будут либо ждать (до исчерпания queueCapacity), либо бросать RejectionException. Настройте RejectedExecutionHandler по необходимости.
📌 Способ 2: Resilience4j Bulkhead (Semaphore vs ThreadPool)
resilience4j.bulkhead.instances:
myServiceBulkhead:
maxConcurrentCalls: 5
maxWaitDuration: 100ms
В сервисе:
@Service
public class MyService {
private final Bulkhead bulkhead;
public MyService(BulkheadRegistry registry) {
this.bulkhead = registry.bulkhead("myServiceBulkhead");
}
public String process() {
return Bulkhead.decorateSupplier(bulkhead, () -> {
// защищенный вызов
return externalClient.process();
}).get();
}
}
💡 Если поставить maxConcurrentCalls слишком маленьким, часть запросов будет сразу отвергаться с BulkheadFullException. Неочевидный момент: нужно мониторить реальную нагрузку и подбирать значения, а не копировать из гугла.
👉 Также есть аннотационный стиль:
@Bulkhead(name = "myServiceBulkhead", type = Bulkhead.Type.SEMAPHORE)
public String annotatedProcess() { … }
или ThreadPool-вариант:
resilience4j.bulkhead.instances:
myThreadPoolBulkhead:
maxThreadPoolSize: 10
queueCapacity: 20
@Bulkhead(name = "myThreadPoolBulkhead", type = Bulkhead.Type.THREADPOOL)
public CompletionStage<String> asyncProcess() { … }
🧠 Неочевидный момент про паттерны в целом: внедрять Bulkhead “просто потому что модно” — плохо. Паттерн не заменяет мониторинг, трассировку или грамотную архитектуру. Он лишь ограничивает повреждения, но не показывает, где именно проблема. Если вы изолировали компонент в пул, а он всё равно падает, паттерн не скажет “почему”. Всегда сочетайте паттерны с метриками (Micrometer, Prometheus, Grafana) и логированием.
💡 Совет:
▫️Используйте отдельные пулы для медленных операций (например, внешних HTTP-вызовов) и отдельно для CPU-bound задач.
▫️На уровне базы данных тоже можно “бульхедом” выделять разные пулы соединений (например, HikariCP с разными конфигурациями) для тяжелых и легких запросов.
▫️При проектировании микросервисов отдавайте предпочтение Bulkhead на уровне отдельных сервисов: в Kubernetes это можно делать через limits/requests, Horizontal Pod Autoscaling и Circuit Breaker.
⚠️ Предупреждение: перебор с изоляцией приведет к недоиспользованию ресурсов. Если у вас слишком много мелких пулов, а нагрузка неравномерна, часть ресурсов простаивает. Поэтому сначала измерьте нагрузку, а потом разбивайте.
👉@BookJavaOptional.stream() появился в Java 9 и позволяет превратить Optional<T> в Stream<T> длины 0 или 1. Зачем это нужно?
📌 Когда применять?
◾️ 🧠 Интеграция с цепочками Stream API: если у вас есть коллекция Optional<T>, можно собрать все непустые значения без дополнительных проверок:
List<Optional<User>> userOptionals = …;
List<User> users = userOptionals.stream()
.flatMap(Optional::stream) // из каждого Optional либо 1 элемент, либо пусто
.collect(Collectors.toList());
Без Optional.stream() пришлось бы делать что-то вроде filter(Optional::isPresent).map(Optional::get).
◾️ 🧠 При операциях над вложенными опционалами: когда в потоке у вас Optional<Something> и вы хотите «сливать», а не оставлять пустые обёртки.
💡 Совет:
Если вы строите конвейер обработки данных, а на каком-то шаге может не быть значения — Optional.stream() поможет аккуратно пропустить «пустышки» и не ломать последующие операции.
⚠️ Но вот в чём «подводные камни» и почему нельзя злоупотреблять:
1. Потеря явности
◾️ Когда вы где-то просто хотите проверить: есть ли значение в Optional, — использование stream() создаёт впечатление, что у вас реально коллекция элементов, хотя всего лишь 1 или 0. Для простых случаев ifPresent(), map(), orElse() читается понятнее.
// Менее канонично:
optionalValue.stream().forEach(v -> doSomething(v));
// Лучше:
optionalValue.ifPresent(v -> doSomething(v));
2. Ненужные накладные расходы
◾️ Каждый вызов Optional.stream() создаёт объект стрима и небольшую внутреннюю структуру, что на горячем участке кода (в tight loop) может сказаться на производительности. Если вместо него можно обойтись map().orElse(), задумайтесь о легковесном варианте.
3. Скрытые баги
◾️ Если вы по ошибке используете Optional.stream() в одиночном случае (не в контексте объединения множества опционалов), код может стать менее очевидным. Например:
// Что тут происходит?
Stream.of(opt1, opt2, opt3)
.flatMap(Optional::stream)
.findFirst();
Казалось бы, надо искать первый непустой, но читающий код может не сразу понять логику: а вдруг нужно просто взять любое значение, а не первой в списке? Лушче явно:
Optional<User> result = opt1.isPresent() ? opt1
: opt2.isPresent() ? opt2
: opt3;
4. Лишняя сложность
◾️ В ситуациях, когда Optional появляется из map/filter в одном стриме, а потом вы вновь оборачиваете результат в Optional, лучше сразу строить последовательность через flatMap и filter без промежуточных Optional.
💡 Пример «полезного» применения:
List<Order> orders = getOrders();
// Для каждого заказа пытаемся получить пользователя из БД,
// но он может быть не найден (Optional<User>).
List<Optional<User>> maybeUsers = orders.stream()
.map(o -> userRepository.findById(o.getUserId()))
.toList();
// Теперь формируем список уже «существующих» юзеров:
List<User> users = maybeUsers.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
Здесь Optional.stream() полностью оправдан: сразу избавляемся от «пустых» опционалов.
💡 Анти-паттерн:
Не используйте Optional.stream() внутри метода, который ожидает ровно одно значение или бросает исключение, если опционал пуст.
// Плохо:
User user = optionalUser.stream()
.findFirst()
.orElseThrow(() -> new NotFoundException("User not found"));
// Лучше так:
User user = optionalUser
.orElseThrow(() -> new NotFoundException("User not found"));
В первом случае мы заводим стрим без смысла, во втором — прямой и понятный код.
Итого:
◾️ 🧠 Используйте Optional.stream() только когда действительно нужно объединить несколько Optional-ов в один Stream и пропустить пустые.
◾️ ⚠️ В одиночных сценариях проверки и извлечения значения он избыточен и даже снижает читабельность и производительность.
👉@BookJavarecord и @ConstructorBinding
Вместо традиционных @Data + пустого конструктора можно сразу использовать Java 17 record для настройки свойств:
📌 Почему это полезно?
🔴Полная иммутабельность: поля конфигов больше нельзя случайно перезаписать.
🔴Минимум «шаблонного» кода: не нужны геттеры, сеттеры, toString(), equals() и т.д.
🔴Чёткая связь с Java 17+ и актуальными best practices.
💡 Как сделать:
1. Подключаем зависимость:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
Это нужно, чтобы IDE и Spring метаинфу подхватили.
2. Создаём record с аннотацией:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
@ConstructorBinding
@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(
String host,
int port,
String username,
String password
) {}
3. Регистрируем бин в Spring:
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class AppConfig { }
4. Конфигурируем в application.yml (или .properties):
app:
mail:
host: smtp.example.com
port: 587
username: user@example.com
password: secret123
⚠️ Обратите внимание:
🔴Без @ConstructorBinding Spring не сможет смотать значения в record’ы.
🔴Уберите все сеттеры и по умолчанию конструктор генерируется автоматически.
🔴Если вам нужна валидация свойств, добавьте @Validated и JSR-303 аннотации (@NotNull, @Min и т.д.).
🧠 Что получилось?
🔴Минимум «мусора» в коде: один блок record заменил класс с 4 полями, геттерами и конструктором.
🔴Полная типобезопасность и поддержка автокомплита при обращении к полям.
🔴Быстрый переход на Java 17+ подходы без потери функциональности.
💡 Дополнительный лайфхак:
Если вам нужно разделить конфиги по окружениям (dev/prod), просто создайте два record’а с разными префиксами или используйте @Profile. В Spring Boot 3 этот подход «из коробки» работает наилучшим образом.
👉@BookJavaSecurityContext не “переходит” автоматически в новые потоки. Для этого используют SecurityContextRepository и специальные методы в WebFlux.
⚠️ Важно: не храните SecurityContext в сессии, если у вас stateless-приложение (REST API). Вместо сессии используйте JWT или OAuth 2.0.
🧠 5. Авторизация: FilterSecurityInterceptor & AccessDecisionManager
FilterSecurityInterceptor запускается в конце цепочки фильтров и проверяет доступ к URL. Он запрашивает у SecurityMetadataSource список необходимых ролей для данного эндпоинта (Spring на основании @PreAuthorize, HttpSecurity конфигурации или XML). Затем передаёт дело в AccessDecisionManager (по умолчанию AffirmativeBased), который опрашивает список AccessDecisionVoter (например, RoleVoter для проверок ролей, WebExpressionVoter для SpEL).
FilterSecurityInterceptor
└─> SecurityMetadataSource (что нужно: ROLE_ADMIN)
└─> AccessDecisionManager.vote()
├─ RoleVoter.vote() → совпадает?
└─ WebExpressionVoter.vote() → SpEL-выражения?
Если хотя бы один голос “grant”, AffirmativeBased отпускает запрос (по умолчанию). Можно менять стратегию на Consensus или Unanimous.
💡 Трюк: чтобы локально протестировать SecurityContext, можно в тестах использовать аннотацию @WithMockUser(roles = "ADMIN") и проверять, что нужный эндпоинт доступен.
🧠 6. Аннотации & Method Security
Помимо URL-уровня, есть методная проверка:
@EnableMethodSecurity // (Spring Boot 3+) вместо @EnableGlobalMethodSecurity
public class SecurityConfig { ... }
// Где-то в сервисе:
@PreAuthorize("hasRole('ADMIN') and #id == principal.id")
public void deleteUser(Long id) { ... }
* 📌 @PreAuthorize / @PostAuthorize / @Secured / @RolesAllowed — все используют тот же механизм Voter’ов, но проверяют уже на методах сервиса.
* 💡 Совет: включайте методную безопасность только там, где действительно нужна тонкая грануляция.
🧠 7. Хранение паролей & PasswordEncoder
С Java 17+ используйте PasswordEncoder с алгоритмами Argon2 или BCrypt:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
⚠️ Никогда не храните пароли в открытом виде и не используйте MD5/SHA-1 — они считаются небезопасными.
🧠 8. Stateless vs Stateful
* Stateful (Сессии): Spring создаёт HTTP-сессию, а SecurityContextPersistenceFilter хранит контекст в сессии. Удобно для монолитов с классическим web-приложением.
* Stateless (JWT/OAuth2): убираем SessionCreationPolicy.STATELESS, используем BearerTokenAuthenticationFilter, аутентификация и авторизация проверяются по JWT в каждом запросе.
💡 Современный стек (Spring Boot 3+):
1. Настраиваем SecurityFilterChain с oauth2ResourceServer().jwt().
2. Подключаем spring-boot-starter-oauth2-resource-server.
3. Указываем issuer-uri или jwk-set-uri в application.yml.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://keycloak.example.com/realms/myrealm
🧠 9. Подпись запросов & CSRF
* По умолчанию CSRF включён для “форменных” запросов (POST, PUT, DELETE). Для stateless-API его обычно отключают:
http.csrf().disable();
* Если используете формы, не забудьте добавить в шаблон:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
⚠️ Внимание: не отключайте CSRF, если ваше приложение использует сессии и cookie на фронтенде!
💡 Совет по отладке: включите логирование фильтров:
logging.level.org.springframework.security=DEBUG
Тогда в логах вы увидите, как проходит запрос через каждый фильтр и где происходит отказ.
👉@BookJavaSecurityContext (где хранится Authentication).
* ⚙️ UsernamePasswordAuthenticationFilter – обрабатывает форму логина (если вы используете formLogin).
* ⚙️ BasicAuthenticationFilter – поддерживает HTTP Basic (для REST).
* ⚙️ BearerTokenAuthenticationFilter (Spring Boot 3+) – для JWT/OAuth2 Bearer-токенов.
* ⚙️ ExceptionTranslationFilter – перехватывает AccessDeniedException и AuthenticationException, перенаправляет на страницу логина или возвращает 401.
* ⚙️ FilterSecurityInterceptor – проверяет, есть ли у аутентифицированного пользователя разрешение (ROLE_*) для доступа к ресурсу.
Каждый фильтр решает конкретную задачу, и порядок важен: если, например, фильтр авторизации (FilterSecurityInterceptor) стоит раньше, чем фильтр аутентификации, вы получите неожиданный отказ.
💡 Современный подход (Spring Boot 3+):
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2.jwt()); // JWT из OIDC/JWK
return http.build();
}
Таким образом вы сами управляете порядком фильтров и включаете только нужные.
🧠 2. AuthenticationManager & ProviderManager
Когда UsernamePasswordAuthenticationFilter (или другой аутентификатор) получает учётные данные, он создает UsernamePasswordAuthenticationToken с неверифицированными (unauthenticated) флагом. Затем передаёт этот токен в AuthenticationManager:
UsernamePasswordAuthenticationFilter → AuthenticationManager.authenticate()
AuthenticationManager по умолчанию — это ProviderManager, который хранит список AuthenticationProvider (например, DaoAuthenticationProvider для UserDetailsService или JwtAuthenticationProvider для токенов). Каждый Provider пытается аутентифицировать токен, и если успешно, возвращает уже аутентифицированный Authentication с authorities.
📌 Совет: если нужно добавить кастомную проверку (например, MFA), реализуйте свой AuthenticationProvider и зарегистрируйте его перед DaoAuthenticationProvider.
🧠 3. UserDetailsService & UserDetails
DaoAuthenticationProvider опирается на UserDetailsService (или ReactiveUserDetailsService в WebFlux), чтобы получить UserDetails (имя, пароль, роли, статус аккаунта). В Java 17+ можно пользоваться Map.of(...) или List.of(...), но в реальных проектах лучше хранить в БД через JPA/Hibernate.
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository repo;
@Override
public UserDetails loadUserByUsername(String username) {
UserEntity user = repo.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return User.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles().toArray(new String[0]))
.accountLocked(!user.isAccountNonLocked())
.build();
}
}
🧠 4. SecurityContext & SecurityContextHolder
После успешной аутентификации фильтр устанавливает SecurityContext в SecurityContextHolder. По умолчанию используется стратегия MODE_THREADLOCAL, т.е. контекст привязан к текущему потоку.
SecurityContextHolder.getContext().setAuthentication(authenticatedToken);User и связанные с ними сущности Order. При запросе пользователей:
List<User> users = userRepository.findAll(); // Один запрос
for(User user : users) {
List<Order> orders = user.getOrders(); // +1 запрос на каждого пользователя!
}
Если пользователей 1000, то будет 1 + 1000 SQL-запросов. Это убийственно для производительности ⚠️
🛠 Как решить?
💡 Вариант 1: JOIN FETCH
Самый быстрый и простой путь — сразу подтянуть связанные сущности:
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();
✅ Всего один SQL-запрос
❌ Может привести к дублированию строк, если связей много и они сложные
💡 Вариант 2: Entity Graph (Spring Data JPA)
Entity Graph — более гибкий подход:
@EntityGraph(attributePaths = {"orders"})
List<User> findAll();
✅ Удобно и понятно, нет дублей
✅ Можно легко настраивать под конкретный запрос
💡 Вариант 3: Batch fetching (настройка Hibernate)
spring.jpa.properties.hibernate.default_batch_fetch_size=20
Hibernate будет автоматически загружать связанные сущности пачками.
✅ Просто настроить и сразу эффект
❌ Всё равно несколько запросов, хотя и пачками
🧠 Когда что использовать?
▫️ JOIN FETCH — когда связи простые, а данных немного.
▫️ Entity Graph — если нужна гибкость и удобство.
▫️ Batch fetching — когда настроить проще, чем переписывать код.
Не забывай проверять SQL-запросы в логах и профилировать приложение 🔥
👉@BookJavaXmx и Xms ты прописываешь с закрытыми глазами, самое время разобраться, что же реально происходит с памятью в Java и как новые сборщики мусора могут повлиять на производительность твоего приложения.
🗂 В этой серии — подробный разбор всех современных GC, доступных в Java HotSpot VM: от базовых до самых продвинутых. Для каждого — объяснение принципов, сценарии применения, плюсы и минусы, а также практические советы по настройке.
Список статей:
1️⃣ Введение
2️⃣ Serial GC и Parallel GC
3️⃣ CMS и G1
4️⃣ ZGC
5️⃣ Epsilon GC
6️⃣ Shenandoah GC
👉@BookJava
// НЕ рекомендуется:
@EnableTransactionManagement
public class Config {
@Bean
public JtaTransactionManager transactionManager() {
return new JtaTransactionManager();
}
}
// Лучше outbox + отдельный publisher
🧠 В 99% случаев проще и надёжнее строить архитектуру вокруг событий и простых локальных транзакций, чем бороться с XA.
👉@BookJava@Transactional) тут не подходят — нет распределённого ACID. Saga решает эту проблему через последовательность локальных транзакций с возможностью отката (compensation).
💡 Виды Saga
1. Choreography
Нет единого оркестратора. Каждый сервис реагирует на события предыдущих через очередь (Kafka, RabbitMQ).
* Простой flow
* Меньше точек отказа
2. Orchestration
Есть выделенный "оркестратор", который координирует выполнение саги, рассылая команды сервисам.
* Более явный контроль
* Лучше трассировка
⚖️ Плюсы
* Обеспечивает согласованность между сервисами
* Высокая отказоустойчивость: каждый шаг можно компенсировать
* Гибкость — легко расширять и модифицировать бизнес-логику
⚠️ Минусы
* Сложнее реализовать, чем простые транзакции
* Не моментальная консистентность — возможны временные аномалии
* Нужно проектировать compensating transactions для каждого шага
* Тяжело тестировать крайние случаи и "сорванные" шаги
🔁 Альтернативы
* Distributed Transactions (XA, 2PC) — редко используются из-за сложности и слабой поддержки в современных cloud-системах.
* Event Sourcing — другой паттерн работы с изменениями, но сложнее для чтения.
* Idempotency + Retry — иногда достаточно, если бизнес-процесс допускает.
💡 Совет: для большинства бизнес-операций с межсервисным взаимодействием, где требуется отмена — Saga остаётся самым практичным выбором. В Spring есть библиотека Axon Framework, также стоит посмотреть на Camunda.
👉@BookJavaComparator:
List<User> users = ...;
users.sort(Comparator
.comparing(User::getName, Comparator.nullsLast(String::compareToIgnoreCase))
.thenComparingInt(User::getAge)
);
📌 comparing — сравнивает по полю (например, name).
📌 nullsLast или nullsFirst — удобно обрабатывать возможные null.
📌 thenComparingInt — дополнительная сортировка (например, по возрасту).
💡 Такой подход краткий, читабельный и отлично работает с любыми полями, даже если они могут быть null.
⚠️ Никогда не забывай про null, особенно если сортируешь данные из БД или внешних источников. Ошибка NullPointerException во время сортировки может неожиданно прилететь в проде.
👉@BookJava
متاح الآن! بحث تيليغرام 2025 — أهم رؤى العام 
