25698
Все самое полезное для Java-разработчика в одном канале. Список наших каналов: https://t.me/proglibrary/9197 Обратная связь: @proglibrary_feedback_bot По рекламе: @proglib_adv Прайс: @proglib_advertising
💭 Apache Kafka: как не терять данные под нагрузкой
Классическая проблема растущего проекта: сервис уведомлений начинает захлёбываться. В пике прилетает 10к запросов в секунду, а он обрабатывает 3к. Остальные просто теряются.
Можно горизонтально масштабировать, но это не решает проблему архитектурно. А Kafka решает.
Идея простая: Producer пишет, Consumer читает в своём темпе. Никто никого не ждёт. Никто никого не роняет.
1️⃣ Producer: отправляем событие
@Service
@RequiredArgsConstructor
public class OrderService {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void createOrder(Order order) {
orderRepository.save(order);
OrderEvent event = new OrderEvent(order.getId(), order.getUserId());
kafkaTemplate.send("order-created", order.getUserId().toString(), event);
// топик ключ партиции payload
}
}
userId как ключ — все события одного пользователя попадут в одну партицию и будут обработаны строго по порядку.@Component
public class NotificationConsumer {
@KafkaListener(
topics = "order-created",
groupId = "notification-group",
concurrency = "3" // 3 потока = читаем 3 партиции параллельно
)
public void handle(OrderEvent event) {
notificationService.send(event.getUserId());
}
}
order-created (3 партиции)
├── partition-0 → consumer-instance-1
├── partition-1 → consumer-instance-2
└── partition-2 → consumer-instance-3
spring:
kafka:
consumer:
auto-offset-reset: earliest # читать с начала, если offset не найден
enable-auto-commit: false # коммитим offset вручную — только после успешной обработки
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<String, Object> kafkaTemplate) {
var recoverer = new DeadLetterPublishingRecoverer(kafkaTemplate);
var backoff = new FixedBackOff(1000L, 3); // 3 попытки с паузой 1с
return new DefaultErrorHandler(recoverer, backoff);
}
order-created.DLT. Основной поток не заблокирован, разбираешься с проблемой отдельно.
🌸 Поздравляем с 8 марта
Дорогие девушки, кто выбрал этот безумный и прекрасный путь в IT. Спасибо, что вы есть в нашей профессии.
Пусть сегодня всё задуманное сбывается, улыбок будет больше, чем поводов для них, а день подарит только приятные сюрпризы!
С праздником, коллеги 💐
🐸 Библиотека джависта
🔍 Чтение csv файла
Простая команда на случай, когда надо быстро и в удобном формате прочитать CSV-файл в терминале:
$ cat inventory.csv | column -t -s,
-s указывает на использование запятых в качестве разделителей, а -t форматирует выходные данные в чистую таблицу.
💡 Collections.singletonList вместо new ArrayList<>()
Когда список точно из одного элемента — не создавай изменяемый:
// ❌ Избыточно
List<String> roles = new ArrayList<>();
roles.add("ADMIN");
someMethod(roles);
// ✅ Лаконично и без лишней аллокации
someMethod(Collections.singletonList("ADMIN"));
🕯 Memory Ordering и happens-beforevolatile – это ключевое слово, которое часто используют, но не до конца понимают. Обычно говорят, что оно гарантирует видимость изменений между потоками. На самом деле, смысл этого слова одновременно шире и уже, чем кажется.
🔹 Проблема без volatile
Современные процессоры и компиляторы переупорядочивают инструкции для оптимизации. Каждое ядро имеет store buffer — запись в память не мгновенна, сначала попадает в буфер. Другое ядро может не увидеть запись ещё долгое время.
// Поток 1
data = 42;
ready = true;
// Поток 2
if (ready) {
System.out.println(data); // может напечатать 0
}
volatile boolean ready;
int data; // не volatile!
// Поток 1
data = 42; // HB-предшествует записи в ready
ready = true; // volatile write
// Поток 2
if (ready) { // volatile read
// data гарантированно == 42
System.out.println(data);
}
volatile long counter;
counter++; // не атомарно: read → increment → write
StampedLock (Java 8+) предлагает три режима: запись, пессимистичное чтение, оптимистичное чтение. Оптимистичное чтение не берёт блокировку вообще — читает данные и потом валидирует что запись не произошла:long stamp = lock.tryOptimisticRead();
int x = point.x;
int y = point.y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
x = point.x;
y = point.y;
} finally {
lock.unlockRead(stamp);
}
}
validate — это volatile-read под капотом, устанавливающий happens-before с последней записью. Паттерн работает корректно именно из-за JMM семантики, а не "просто так".
🔧 Spring Cache + Redis: настройка которая не сломается в production@Cacheable выглядит просто. До первого падения сериализации в production или гонки при cache stampede.
🔹 Решение
▪️ Конфигурация RedisCacheManager
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // null не кешируем
Map<String, RedisCacheConfiguration> configs = Map.of(
"users", defaults.entryTtl(Duration.ofHours(1)),
"products", defaults.entryTtl(Duration.ofMinutes(5)),
"sessions", defaults.entryTtl(Duration.ofDays(1))
);
return RedisCacheManager.builder(factory)
.cacheDefaults(defaults)
.withInitialCacheConfigurations(configs)
.build();
}
}
@Cacheable(value = "users", key = "#userId", unless = "#result == null")
public User findById(Long userId) { ... }
@CacheEvict(value = "users", key = "#user.id")
public User update(User user) { ... }
// Evict всего кеша при массовых операциях
@CacheEvict(value = "users", allEntries = true)
public void importUsers(List<User> users) { ... }
@Service
public class ProductService {
private final Map<Long, Object> locks = new ConcurrentHashMap<>();
public Product getProduct(Long id) {
Product cached = cacheManager.getCache("products").get(id, Product.class);
if (cached != null) return cached;
// только один поток пересчитывает, остальные ждут
Object lock = locks.computeIfAbsent(id, k -> new Object());
synchronized (lock) {
// double-check после захвата лока
cached = cacheManager.getCache("products").get(id, Product.class);
if (cached != null) return cached;
Product product = repository.findById(id).orElseThrow();
cacheManager.getCache("products").put(id, product);
return product;
}
}
}
GenericJackson2JsonRedisSerializer сохраняет информацию о типе в JSON (@class поле). Это позволяет десериализовать полиморфные структуры, но требует чтобы все классы в графе объекта были сериализуемы Jackson'ом. Проблема обычно вылезает при кешировании Hibernate entity с lazy коллекциями — они не сериализуются и роняют приложение.
🐸 Библиотека джависта
#DevLife
⌛ Сохраняйте шпаргалку по оконным функциям sql
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#Enterprise
👑 Магия IntelliJ IDEA
Ты явно используешь Ctrl + Alt + L (форматирование кода), но мало знаешь ли это сочетание:
— Ctrl + Alt + Shift + L → Гибкое форматирование
🔹 Зачем это нужно
— Позволяет выбрать, что именно форматировать: весь файл, выделенный код или даже только измененные строки.
— Можно отключить автоформатирование аннотаций, импортов или пробелов, если не хотите, чтобы IDEA ломала ваш стиль.
— Полезно, если работаете в команде с жесткими code style правилами, можно форматировать только нужные части, не трогая остальной код.
🔹 Дополнительные трюки
— Выделите код, затем Ctrl + Alt + Shift + L, чтобы форматировать только его.
— Используйте Settings → Editor → Code Style, чтобы настроить форматирование под себя.
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava
Стек — это только инструмент. Готов ли ты стать кофаундером продукта? 🚀
Энтерпрайз даёт стабильность, но часто забирает драйв. Proglib App предлагает другое: роль технического лидера в EdTech-платформе. Мы обучаем разработчиков через курсы, квизы и ИИ-агентов. MVP готов, юзеры учатся.
Нужен сильный инженер, который перерос рамки одного языка и хочет влиять на архитектуру и бизнес-логику всего сервиса.
🛠️ Стек (современный Fullstack):
TypeScript, React 18, Express 5, PostgreSQL, Drizzle ORM.
Почему это вызов для тебя:
• Никакой бюрократии: ты и основатель (он тоже кодит) решаете всё сами. • Инструменты будущего: активное использование Claude Code и Cursor. • Масштаб: путь от MVP до лидера рынка образовательных платформ.
Ожидания:
• Крепкий бэкграунд в разработке и проектировании БД. • Готовность быстро включиться в работу с TS/React/Node.js. • Автономность и продуктовое мышление.
Удалёнка, гибкий график, отсутствие «стеклянного потолка».
Готов сменить привычный цикл обновлений на драйв кофаундера? Пиши в бота 👇
@proglibrary_feedback_bot
👾 JOOQ: Основы
Устал от того, что Hibernate «умнее» тебя и генерирует запросы, которые ты не просил? Тогда стоит взглянуть на JOOQ.
🔹 Что это такое
JOOQ (Java Object Oriented Querying) — это библиотека для построения типобезопасных SQL-запросов прямо в Java-коде. В отличие от JPA/Hibernate, здесь SQL — это основа, а не просто деталь. Пиши SQL запросы, но с поддержкой компилятора.
🔹 Проблемы, которую решает JOOQ
При работе с Hibernate часто возникают такие проблемы:
* N+1 проблема, которую сложно отловить.
* HQL/JPQL, которые не поддерживают все возможности SQL.
* Магия FetchType.LAZY, которая может неприятно удивить в production.
* Потеря контроля над тем, какие запросы выполняются на самом деле.
JOOQ возвращает контроль над SQL, но при этом не заставляет писать сырые строки.
🔹 Как это выглядит на практике
// Hibernate (непонятно, что выполнится)
List<order> orders = em.createQuery(
SELECT o FROM Order o WHERE o.status = :status, Order.class)
.setParameter(status, ACTIVE)
.getResultList();
// JOOQ (читаемо, безопасно, предсказуемо)
Result<record> result = dsl
.select(ORDER.ID, ORDER.STATUS, CUSTOMER.NAME)
.from(ORDER)
.join(CUSTOMER).on(ORDER.CUSTOMER_ID.eq(CUSTOMER.ID))
.where(ORDER.STATUS.</record>eq(ACTIVE))
.orderBy(ORDER.CREATED_AT.desc())
.limit(50)
.fetch();
<plugin>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<configuration>
<jdbc>
<url>jdbc:postgresql://localhost/mydb</url>
</jdbc>
<generator>
<database>
<inputSchema>public</inputSchema>
</database>
<target>
<packageName>com.example.generated</packageName>
</target>
</generator>
</configuration>
</plugin>
mvn generate-sources у вас появится пакет с классами, и компилятор станет вашим линтером для SQL.// CTE + оконная функция – попробуйте сделать это в Hibernate
dsl.with(ranked).as(
select(EMPLOYEE.asterisk(),
rowNumber().over()
.partitionBy(EMPLOYEE.DEPARTMENT_ID)
.orderBy(EMPLOYEE.SALARY.desc())
.as(rn))
.from(EMPLOYEE)
)
.select()
.from(table(ranked))
.where(field(rn).eq(1))
.fetch();
@Configuration
public class JooqConfig {
@Bean
public DSLContext dslContext(DataSource dataSource) {
return DSL.using(dataSource, SQLDialect.POSTGRES);
}
}
spring-boot-starter-jooq.
Офер за 2 дня в Яндекс через Weekend Offer Multitrack
Это формат быстрого найма для бэкенд-разработчиков с опытом работы от 5 лет на C++, Python, Java/Kotlin или Go.
Все этапы отбора проходят в течение двух дней:
🟢 14 марта — технические секции.
🟢 15 марта — финальная секция и офер.
Далее в рамках программы Multitrack участники выбирают три команды Яндекса, в которых последовательно работают по несколько недель, знакомясь с задачами, внутренними процессами и коллегами. После этого можно выбрать команду, которая понравится больше.
Этот подход позволяет сравнить разные проекты внутри Яндекса, оценить задачи изнутри и принять взвешенное решение.
Регистрация на участие в программе открыта до 6 марта. Подробности — по ссылке.
Реклама. ООО "Яндекс". ИНН 7736207543
✏️ Spring Proxying и self-invocation
Spring AOP использует прокси. То есть вместо настоящего бина в контейнер помещается его обёртка. Эта обёртка перехватывает вызовы и применяет к ним различные действия, такие как логирование, управление транзакциями, проверка безопасности или другие, заданные пользователем.
Способ создания такой обёртки выбирается автоматически, но у разных способов есть свои особенности и ограничения.
🔵 JDK Dynamic Proxy vs CGLIB
JDK proxy работает только с интерфейсами. Он создаёт объект, который реализует тот же интерфейс, и все вызовы идут через специальный обработчик (InvocationHandler). Внутри есть оригинальный объект, но снаружи виден только прокси.
CGLIB работает по-другому: он создаёт подкласс вашего класса прямо во время работы программы и переопределяет методы, вставляя в них перехват. Поэтому CGLIB не может работать с final-методами и классами, т.к. подкласс не может переопределить final-метод.
Начиная с Spring Boot 2.0, CGLIB используется по умолчанию даже для классов с интерфейсами (spring.aop.proxy-target-class=true). Раньше, если был интерфейс, использовался JDK proxy.
🔵 Self-invocation
Тут всё становится не так очевидно. Разберём такой код:
@Service
public class OrderService {
public void placeOrder() {
// какая-то логика
sendNotification();
}
@Async
public void sendNotification() {
// должно выполняться асинхронно
}
}
@Service
public class OrderService {
@Autowired
private ApplicationContext context;
public void placeOrder() {
context.getBean(OrderService.class).sendNotification();
}
@Async
public void sendNotification() { ... }
}
((OrderService) AopContext.currentProxy()).sendNotification();
🐸 Библиотека джависта
#DevLife
😮 Топ-вакансий для джавистов за неделю
Middle Java/Kotlin Developer — от 150 000 до 250 000 ₽ — офис (Екатеринбург)
Senior Kotlin/Java Developer (Knowledge Hub Team) — до 400 000 ₽ — удалёнка
Senior Java developer — до 6 000 $ — удалёнка
➡️ Еще больше топовых вакансий — в нашем канале Java jobs
👑 Магия IntelliJ IDEA
Если используешь Ctrl + P (подсказка параметров метода), то вот ещё один полезный хот кей: Shift + Ctrl + I → быстрый просмотр определения.
🔹 Зачем это нужно
— Позволяет посмотреть реализацию метода/класса/интерфейса без перехода в другой файл.
— Работает с любыми символами: методами, переменными, константами, даже SQL-мэпперами в MyBatis.
— Незаменимо, если не хочешь терять контекст текущего кода.
🔹 Как использовать
— Наведи курсор на метод, поле или класс, нажми Ctrl + Shift + I — появится всплывающее окно с реализацией.
— Работает и в дебаге, и при просмотре внешних библиотек (если есть исходники).
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava
💥 Открытый вебинар | ИИ-агенты в продакшене: от хайпа к деньгам
Агенты уже везде. Но мало кто признаётся, сколько денег сжёг на бесконечных циклах, галлюцинациях в RAG и отсутствии мониторинга.
Полина Полунина, руководитель AI-направления Альфа-Банка, расскажет честно:
▪️ Чем агент отличается от «просто GPT с промптом» и когда бизнесу достаточно обычного LLM
▪️ 3 реальных кейса из корпоративной среды: что взлетело, а что нет
▪️ Live-демо работающего агента
▪️ ТОП-5 граблей, на которые наступают команды при внедрении
⏱️ 10 марта в 19:00 (МСК)
🎁 Участники получат промокод на скидку на самый полный курс по ИИ-агентам
👉 Регистрируйся
✔️ Java-тест: CompletableFuture + ThreadLocal
Классическая ловушка в многопоточке👇
📦 Задание
Написали сервис для аудит-логирования действий пользователей. В проде периодически в лог пишется чужой userId — данные одного юзера попадают в запись другого. Найдите баг и исправьте:
@Component
public class UserContext {
private static final ThreadLocal<String> currentUserId = new ThreadLocal<>();
public static void set(String userId) { currentUserId.set(userId); }
public static String get() { return currentUserId.get(); }
public static void clear() { currentUserId.remove(); }
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final AuditLogger auditLogger;
public CompletableFuture<Order> createOrder(String userId, OrderDto dto) {
UserContext.set(userId);
return CompletableFuture.supplyAsync(() -> {
Order order = buildOrder(dto);
auditLogger.log("Order created by: " + UserContext.get());
return order;
});
}
}
🐸 Библиотека джависта
#DevLife
👍 На курсе по контролируемой разработке AI-агентов мы будем разбирать ровно то, о чём говорит Владислав в голосовом, но уже в формате системной практики.
📅 Старт курса — 20 апреля.
Если хотите разобраться, как строить управляемые агентные системы:
➡️ Присоединяйтесь.
P.S. С первого занятия будет практика: код и разбор реальных ошибок, а не только теория.
😮 Топ-вакансий для джавистов за неделю
Java Developer — удалёнка
Java-разработчик — от 220 000 ₽ — удалёнка
Senior Java Engineer (JavaSE, algorithms, optimization) — от 5 000 $ — удалёнка
➡️ Еще больше топовых вакансий — в нашем канале Java jobs
⚡️ Как ускорить тесты в 6 раз
Знакомая история: пушишь, идёшь за кофе, возвращаешься, а CI ещё думает.
Автор разобрал реальный backend-монолит и сократил время прогона тестов в 6 раз. Только диагностика и последовательные шаги.
Что внутри:
— Почему timeoutInSeconds = 10 — это мина замедленного действия
— HikariCP exhaustion при параллельных suite'ах — и как считать нужный pool size
— Shared Testcontainer через lazy val в object: потокобезопасно и без костылей
— Кастомный Reporter, который не даёт тестам деградировать снова
Стек Scala/SBT, но всё применимо к любому JVM-проекту.
👉 Подробнее в статье
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#Enterprise
⏳ Как работает @Async в Spring?
Spring позволяет выполнять методы асинхронно с помощью аннотации @Async. Это один из самых простых способов разгрузить основной поток и не блокировать выполнение.
🔧 Как включить
Добавь @EnableAsync на конфигурационный класс (или на @SpringBootApplication). Без этого @Async работать не будет:
@EnableAsync
@SpringBootApplication
public class App { ... }
@Async
public void sendEmail(String to) {
// выполнится в отдельном потоке
}
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
@Async("taskExecutor")
public void process() { ... }
🐸 Библиотека джависта
#DevLife
✔️ Java-тест: Transactional + EventListener
Ревью и рефактор логики для production-кода 👇
📦 Задание
Команда написала логику для отправки письма после регистрации пользователя. На проде иногда возникает ситуация, что письма приходят, а юзера в БД нет. Найдите проблему и исправьте:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void register(UserDto dto) {
User user = new User(dto.email());
userRepository.save(user);
eventPublisher.publishEvent(new UserRegisteredEvent(user));
}
}
@Component
@RequiredArgsConstructor
public class EmailListener {
private final EmailSender emailSender;
private final SomeOtherService someOtherService;
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailSender.sendWelcome(event.user().getEmail());
someOtherService.doSomething();
}
}
Последний шанс: 3 курса по цене 1 и запуск AI-агентов в продакшн
Прикручивать LLM-запросы к Spring-приложениям — весело, но кровавому энтерпрайзу нужна предсказуемость. Как маршрутизировать мультиагентные системы под большими нагрузками и строго соблюдать 152-ФЗ?
В обновлённой программе фокус смещён на жёсткий инжиниринг и вывод в прод. Вы изучите архитектуру ReAct-циклов, паттерны оркестрации агентов через LangGraph, продвинутый RAG, интеграции по стандарту MCP и AgentOps. Все ключевые навыки в одном месте: структурная генерация, time-travel дебаггинг, изоляция выполнения, human-in-the-loop и развёртывание в изолированных контурах.
Почему нельзя откладывать:
— масштабная акция «3 курса по цене 1» сгорит уже завтра;
— промокод Agent на скидку 10 000 рублей действует последние часы;
— сразу после оформления открываются материалы для подготовки — начать учиться можно прямо сейчас.
Забронировать место на курсе и забрать бонусы до 28 февраля
☕️ Java-хак: не используй Optional.get() напрямую
Классическая ошибка, которую порой делают даже опытные разработчики:
Optional<User> user = userRepository.findById(id);
return user.get(); // 💀 NoSuchElementException если пусто
// Дефолтное значение
user.orElse(User.guest())
// Ленивое создание дефолта (если создание дорогое)
user.orElseGet(() -> userService.createDefault())
// Своё исключение с контекстом
user.orElseThrow(() -> new UserNotFoundException(id))
// Выполнить действие только если есть значение
user.ifPresent(u -> eventBus.publish(new UserLoadedEvent(u)))
⚠️ Одна строка кода заблокировала 102 потока в проде
Вводные: сервис на 800 RPS, SLA соблюдается, никаких предупреждений нет. Но в это время 102 потока молча стоят в очереди к одному локу.
Причина – вызов DatatypeFactory.newInstance(). На первый взгляд безобидно, но внутри он запускает ServiceLoader, который обращается к URLClassPath.getLoader(), а этот метод синхронизирован. Каждый раз при вызове, с каждым запросом. Миллионы раз в час.
Решение – всего одна строка статической инициализации final.
Как это было обнаружено, почему ClassLoader работает именно так, и как кэш на Caffeine усугубил проблему, можно прочитать в статье на Хабре.
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava
За год мы провели три потока курса по ИИ-агентам, а теперь запускаем масштабное обновление!
В новом, четвёртом потоке мы учли все пожелания студентов, добавили большой блок про AgentOps и сместили фокус с базовых концепций на суровый инжиниринг. Прикрутить API к Java-бэкенду легко, а вот сделать систему масштабируемой, со строгим контролем затрат и интеграцией в Enterprise без галлюцинаций — задача со звёздочкой.
В программе:
— стабильные интеграции по протоколу MCP и мультиагентные паттерны;
— оркестрация в LangGraph: human-in-the-loop и runbook для диагностики;
— продвинутый RAG для промышленной эксплуатации;
— контроль экономики агентов: детальное управление ресурсами и маршрутизация;
— развёртывание опенсорс-моделей в закрытых контурах по 152-ФЗ.
В честь старта продаж действует спецпредложение: 3 курса по цене 1 (два дополнительных курса в подарок).
Доступ к материалам для предварительной подготовки откроется сразу после оплаты.
По промокоду Agent забирайте скидку 10 000 ₽ (89 000 ₽ вместо 99 000 ₽). Успейте занять место до 28 февраля!
👉 Присоединиться к четвёртому потоку и внедрить агентов в Enterprise
🔧 Caffeine: локальный кеш когда Redis избыточен
Для кеширования результатов тяжёлых вычислений или редко меняющихся справочников Redis часто избыточен. Caffeine — высокопроизводительный in-process кеш с Window TinyLFU eviction policy.
🔹 Решение
▪️ Зависимость
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
cache:
cache-names:
- currencies
- countryDictionary
- featureFlags
@Configuration
@EnableCaching
public class CaffeineConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCacheLoader(caffeineCacheLoader());
// разные настройки для разных кешей
manager.registerCustomCache("currencies",
buildCache(500, Duration.ofHours(1)));
manager.registerCustomCache("featureFlags",
buildCache(100, Duration.ofMinutes(5)));
// дефолт для остальных
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()); // метрики
return manager;
}
private Cache<Object, Object> buildCache(int size, Duration ttl) {
return Caffeine.newBuilder()
.maximumSize(size)
.expireAfterWrite(ttl)
.refreshAfterWrite(ttl.dividedBy(2)) // фоновое обновление
.recordStats()
.build();
}
}
@Bean
public CacheMetricsRegistrar cacheMetricsRegistrar(MeterRegistry registry,
CacheManager cacheManager) {
return new CacheMetricsRegistrar(registry, List.of(cacheManager));
}