TGViewer
Channel Public Channel
Библиотека Java разработчика

Библиотека Java разработчика

@bookjava

📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate.


По всем вопросам @evgenycarter

РКН clck.ru/3KoGeP
Subscribers
10.1K
Photos
1.1K
Videos
603
Links
1.5K

Showing posts older than #3861 · Back to latest

Older Posts 16 shown
Post #3859 1.32K
🧠 Осторожно с @Transactional на private-методах!

Очень частый анти-паттерн, который легко упустить 👇

@Service
public class UserService {

@Transactional
private void saveUser(User user) {
userRepository.save(user);
}

public void create() {
saveUser(new User());
}
}


Кажется, всё ок. Но ❌ транзакция НЕ работает.

📌 Почему?
Spring AOP использует прокси, а прокси не “видит” вызовы private-методов внутри класса. Такие вызовы происходят напрямую, мимо прокси-обёртки — и аннотация @Transactional просто игнорируется.

💡 Решение:
1. Сделай метод public и вызывай его извне (или из другого бина).
2. Или выдели этот метод в отдельный бин-сервис.

Пример:

@Service
public class UserTransactionalHelper {

@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
}


И в UserService:

public void create() {
helper.saveUser(new User());
}


⚠️ Так же не работают protected, private, final, static, и @PostConstruct-методы.

Нужно помнить: Spring AOP = прокси, а значит, работают только публичные методы, вызываемые ИЗВНЕ.

👉@BookJava
  • 👍 9
  • ❤ 3
Post #3858 1.21K
🧠 Коллекторы и toList() в Java 16+: можно ли заменить collect(Collectors.toList()) на просто .toList()?

Да, но с нюансами.

📌 Короткий ответ:
Если ты используешь Java 16+, можешь заменить:


List<String> list = stream.collect(Collectors.toList());


на:


List<String> list = stream.toList();


💡 Но будь осторожен. Вот 3 ключевых отличия:


1️⃣ Немодифицируемость

* .toList() возвращает немодифицируемый список (immutable).
* Collectors.toList() возвращает modifiable ArrayList.


var list1 = List.of("a", "b");
var list2 = list1.stream().toList();
list2.add("c"); // 💥 UnsupportedOperationException

var list3 = list1.stream().collect(Collectors.toList());
list3.add("c"); // ✅ OK



2️⃣ Тип возвращаемого списка

* Collectors.toList() — это ArrayList (или его сабкласс).
* .toList() — это неопределённый тип внутри JDK (часто List.of под капотом).

Если ты делаешь что-то вроде:


if (list instanceof ArrayList) ...


то поведение может измениться.


3️⃣ Параллельные стримы

.toList() оптимизирован для параллельных стримов: может работать быстрее, но также может повлиять на порядок, если ты этого явно не контролируешь.


⚠️ Когда НЕ стоит заменять:

* Если ты мутируешь список после получения.
* Если ты полагаешься на конкретный тип (например, ArrayList).
* Если ты используешь Java < 16.


✅ Когда заменить можно:

* Если тебе нужен read-only список.
* Если ты не изменяешь коллекцию.
* Если важна сжатость и выразительность.


📌 Резюме:

Заменяй collect(Collectors.toList()) на .toList(), только если тебе действительно не нужен изменяемый список. Это безопасно при соблюдении условий, но может привести к неожиданным багам в тестах и проде, если забыть про неизменяемость.


👉@BookJava
  • 👍 5
  • ❤ 1
Post #3856 1.52K
🧠 Record (Java 16+) + pattern matching для instanceof (Java 14+) в Java 17+ позволяют писать лаконичный и безопасный код:

📌 Запись DTO с валидацией через компактный конструктор:


public record User(String name, String email) {
public User {
Objects.requireNonNull(name, "name не должен быть null");
if (!email.contains("@")) {
throw new IllegalArgumentException("Неверный email: " + email);
}
}
}


– автоматические toString(), equals(), hashCode() без лишнего кода.

🧠 Проверка и приведение типов в одном выражении:


Object obj = …;
if (obj instanceof User u) {
System.out.println("Привет, " + u.name());
}


– нет лишних кастов, код чище и безопаснее.

💡 Совет: для полей — коллекций или массивов — избегайте поверхностной мутабельности:


public record Team(String name, List<String> members) {
public Team {
members = List.copyOf(members);
}
}


– таким образом members нельзя изменить извне.

⚠️ Антипаттерн: не используйте public record X(List<String> list) без копирования — рискуете нарушить неизменяемость!

👉@BookJava
  • 👍 5
Post #3855 1.44K
🧠 JPA Batch Insert: ускоряем и защищаем от OOM

📌 Настройка Hibernate
Добавьте в application.yml или properties:


spring:
jpa:
properties:
hibernate.jdbc.batch_size: 50 # размер пакета
hibernate.order_inserts: true # группировка INSERT’ов
hibernate.order_updates: true # группировка UPDATE’ов


Это позволит драйверу посылать пачками, а Hibernate — сортировать операции для максимальной эффективности.

💡 Сниппет для batch-пакетов


@Service
@RequiredArgsConstructor
public class OrderService {
private final EntityManager em;
private static final int BATCH_SIZE = 50;

@Transactional
public void saveAll(List<Order> orders) {
for (int i = 0; i < orders.size(); i++) {
em.persist(orders.get(i));
if (i > 0 && i % BATCH_SIZE == 0) {
em.flush();
em.clear(); // освобождаем persistence-context
}
}
em.flush();
em.clear();
}
}


– flush() выталкивает пакеты в БД,
– clear() освобождает ОЗУ от управляемых сущностей.

⚠️ Важные моменты

* GenerationType.IDENTITY отключает batching. Используйте @SequenceGenerator с allocationSize.
* При двусторонних связях (OneToMany) избегайте каскадного сохранения огромных графов — лучше сохранять “плоско” и затем связывать.
* Следите за JDBC-драйвером: не все поддерживают batch-вставки одинаково хорошо.

💡 Совет по мониторингу
Запустите приложение с -Dorg.hibernate.SQL=DEBUG и -Dhibernate.format_sql=true — вы увидите групповые INSERT вместо множества одиночных.

📌 Результат

* Скорость записи растёт в 5–10× (в зависимости от нагрузки).
* Память на стороне приложения остаётся стабильной, без роста Persistence Context.

👉@BookJava
  • 👍 7
Post #3853 1.34K
📌 CRaC (Coordinated Restore at Checkpoint) — горячая JVM-фишка для сверхбыстрого cold-start: сохраняем состояние приложения после инициализации и моментально «восстанавливаем» при рестарте.

🧠 Как это работает

1. JVM создаёт снимок (checkpoint) всего heap- и native-состояния сразу после bootstrap и bean-инициализации.
2. При рестарте JVM грузит этот снимок вместо полной загрузки классов и прогрева JIT.

💡 Подключение в Java 21+

1. Включите экспериментальный модуль:


--add-modules jdk.crac
--enable-preview

2. Реализуйте CheckpointListener для чистки и восстановления ресурсов:


import jdk.crac.Core;
import jdk.crac.Control;
import jdk.crac.CheckpointListener;
import jdk.crac.Context;
import org.springframework.stereotype.Component;

@Component
public class CracHandler implements CheckpointListener {
@Override
public void beforeCheckpoint(Context<?> ctx) {
// 📌 Закрываем пулы, Flush в БД, отписываемся от очередей
}
@Override
public void afterRestore(Context<?> ctx) {
// 💡 Реинициализируем пулы, повторная регистрация listeners
}
}
// Регистрация слушателя
Core.getGlobalContext().register(new CracHandler());

3. Сборка и запуск:


# Сохраняем checkpoint
java \
--add-modules jdk.crac \
--enable-preview \
-XX:CRaCCheckpointToDir=crac-checkpoint \
-jar app.jar

# Восстанавливаем из него
java \
--add-modules jdk.crac \
--enable-preview \
-XX:CRaCRestoreFrom=crac-checkpoint \
-jar app.jar


⚠️ Ограничения и нюансы

* Не все native-библиотеки безопасны для снапшота.
* Тяжёлые background-потоки: до checkpoint лучше останавливать.
* Проверяйте на staging-окружении — subtle bugs могут всплыть только после restore.

📌 Зачем это нужно?

* 🚀 Ускоренный cold-start для Spring Boot 3+ сервисов (лучшая DevOps-интеграция в контейнерах и serverless).
* 💰 Экономия ресурсов в автоскейлируемых кластерах.

Простой CRaC-proof-of-concept позволит вам измерить прирост старта ваших микросервисов уже сегодня!

👉@BookJava
  • 👍 5
  • ❤ 1
Post #3851 1.23K
📌 Stream.toList() vs Collectors.toList() — безопасная замена?

🧠 В Java 16+ появился метод Stream.toList(), который собирает элементы потока в список. Раньше мы писали:


List<String> list = stream.collect(Collectors.toList());


Теперь можно укоротить до:


List<String> list = stream.toList();


💡 Главные отличия:

1️⃣ Неизменяемость
toList() возвращает unmodifiable List — любые add()/remove() вылетят UnsupportedOperationException.
Если нужен изменяемый список, продолжайте использовать collect(Collectors.toList()) или


stream.collect(Collectors.toCollection(ArrayList::new));


2️⃣ Null-элементы
toList() не допускает null и бросит NPE при встрече null в потоке. Collectors.toList() сохранит null без ошибок.

3️⃣ Спецификация
Stream.toList() гарантированно создаёт новый список с точным размером, а Collectors.toList() лишь «может» вернуть любой List (часто ArrayList, но без чётких гарантий).

⚠️ Если вам важна мутабельность или поддержка null — не меняйте на toList().

✅ Если же нужен чистый readonly-список и вы уверены в отсутствии null — смело переходите на toList() для более лаконичного и потенциально более эффективного кода.

Я перехожу на toList() везде, где нужен только чтение — получилось короче и понятнее.

👉@BookJava
  • 👍 9
Post #3850 1.24K
⚡️ Быстрые альтернативы HashMap: EnumMap, массивы и примитивные коллекции


🧠 EnumMap вместо HashMap (enum-ключи)
📌 EnumMap<K, V> хранит данные в массиве → нет хеширования и boxing’а.


EnumMap<Status, String> map = new EnumMap<>(Status.class);
map.put(Status.STARTED, "Запущен");
String status = map.get(Status.STARTED);


⚠️ Работает только для enum-ключей.


🧠 Прямой массив для плотных int-ключей
📌 Используйте массив вместо Map, если ключи — диапазон [0…MAX] и известны заранее.


int MAX = 1000;
var cache = new String[MAX + 1];
cache[42] = "ответ";
String result = cache[42];


💡 O(1), без коллизий и аллокаций объектов.
⚠️ Память линейно зависит от MAX.


🧠 Специализированные коллекции для примитивов
📌 Библиотеки fastutil, HPPC, Trove и др.


var fastMap = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<String>();
fastMap.put(42, "ответ");
String res = fastMap.get(42);


💡 Избегаете boxing/unboxing и получаете быстрее операции.


🧠 Switch-expression / tableswitch
📌 Для фиксированного набора целых или enum-ключей генерируйте switch, а не Map.


String handle(int code) {
return switch (code) {
case 100 -> "OK";
case 200 -> "Created";
default -> "Other";
};
}


💡 JIT компилирует switch в tableswitch или lookupswitch — молниеносно.


💡 Выбирайте стратегию под конкретную задачу:

- EnumMap — enum-ключи
- Массив — плотные int-диапазоны
- fastutil/HPPC — примитивы с большим диапазоном
- Switch — фиксированный набор значений

👉@BookJava
  • 👍 5
  • ❤ 1
Post #3845 1.9K
Test Driven Development (TDD) in Java

Creating a Queue Abstract Data Type class
Introduction to Test Doubles Dummies and Stubs
Introduction to Test Doubles Spies
Introduction to Test Doubles Mocks
Introduction to Test Doubles - Fakes

источник

👉@BookJava
  • 👍 6
Post #3844 1.83K
🚀 Spring WebFlux с Server-Sent Events 🚀

Улучшите свои приложения в режиме реального времени с помощью #SpringWebFlux и Server-Sent Events! 🔥

👉@BookJava
  • ❤ 5
  • 👍 2
Post #3842 1.33K
🎯 Java-хаки: динамический вывод с помощью printf()

Статья посвящена методу printf() в Java, который используется для создания форматированной строки вывода. В ней рассматривается синтаксис метода, различные спецификаторы формата (например, %d, %f, %s и т.д.) и то, как с их помощью управлять отображением чисел, строк и других типов данных в консоли.

Приведены примеры использования System.out.printf() с пояснениями по флагам, ширинe и точности форматирования, а также показано, как легко создавать динамический и читаемый вывод в приложениях на Java.

https://springframework.guru/java-output-printf-method/

👉@BookJava
  • 👍 1
Post #3841 1.28K
Структурированное логирование в Spring Boot 3.5.5

Spring Boot 3.5.5 приносит улучшенное структурированное логирование.
Чтобы его включить, добавьте следующее в ваш application.yml:

Это обеспечивает более чистые, структурированные логи, что делает их проще для разбора инструментами вроде ELK, Grafana или Datadog.

👉@BookJava
  • ❤ 6
  • 👍 2
Post #3839 1.28K
Bulkhead — это паттерн из мира устойчивых систем, цель которого — изолировать сбои в одном компоненте, чтобы они не “затопили” всю систему. Сейчас я покажу вам несколько способов его реализации и подсвечу неочевидный момент при работе с любыми паттернами.

🧠 Концепция: представьте корабль с отсековыми переборками (bulkheads). Если вода просачивается в один отсек, остальные остаются сухими, и судно всё ещё может плыть. В мире Java/Spring это означает: ограничивать ресурсы (пулы потоков, соединения, очереди) для каждого сервиса/метода, чтобы при пике нагрузки или ошибках нагрузка не разошлась по всей системе.

📌 Способ 1: отдельные пул-экзекьюторы


@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.

⚠️ Предупреждение: перебор с изоляцией приведет к недоиспользованию ресурсов. Если у вас слишком много мелких пулов, а нагрузка неравномерна, часть ресурсов простаивает. Поэтому сначала измерьте нагрузку, а потом разбивайте.

👉@BookJava
  • 👍 6
  • ❤ 3
  • 💩 1
Post #3837 1.22K
30 лет Java: от провалившегося гаджета до фундамента разработки ПО

Некоторые языки программирования, например Rust, Go и TypeScript, считаются крутыми. Другие, в том числе Cobol и Java, «скучны». Однако пусть Java, которому 23 мая этого года исполнилось тридцать лет, может, и не самый захватывающий язык, он остаётся одним из самых важных.

Путь Java начался 23 мая 1995 года, когда его выпустила компания Sun Microsystems. За прошедшее время благодаря удачному видению разработчиков и адаптивности он превратился из нишевого проекта для потребительской электроники в мощный фундамент энтерпрайз-, облачной и веб-разработки.

Хоть Java исполнилось тридцать, его история гораздо дольше. Корнями этот язык уходит в 1991 год, когда инженеры Sun Джеймс Гослинг, Майк Шеридан и Патрик Ноутон приступили к созданию языка для интерактивного телевидения и встроенных устройств. Этот проект назвали Green Project. Его цель заключалась не столько в написании нового языка, сколько в создании того, что бы мы сегодня назвали контроллером Интернета вещей. Ещё один разработчик Java Тим Линдхольм, описал его как «своего рода гибрид между КПК и универсальным пультом дистанционного управления».

https://habr.com/ru/articles/914970/

original https://www.zdnet.com/article/java-at-30-how-a-language-designed-for-a-failed-gadget-became-a-global-powerhouse/

👉@BookJava
  • 👍 4
Post #3836 1.11K
Optional.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 и пропустить пустые.
◾️ ⚠️ В одиночных сценариях проверки и извлечения значения он избыточен и даже снижает читабельность и производительность.

👉@BookJava
  • 👍 4
  • 🤡 1
Post #3835 1.23K
🔍Тестовое собеседование с Java-разработчиком из Т1 Иннотех уже завтра

4 июня(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Java-разработчика.

Как это будет:
📂 Илья Аров, старший разработчик в Т1, будет задавать реальные вопросы и задачи разработчику-добровольцу
📂 Илья будет комментировать каждый ответ респондента, чтобы дать понять чего от вас ожидает собеседующий на интервью
📂 В конце можно будет задать любой вопрос Илье

Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Java-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.

Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_sh_bot

Реклама. ООО "ШОРТКАТ", ИНН: 9731139396, erid: 2VtzqvL6bDw
  • 🥱 2
Post #3834 1.21K
🧠 Конфигурация Spring Boot 3 через record и @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 этот подход «из коробки» работает наилучшим образом.

👉@BookJava
  • ❤ 7
  • 👍 2
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →