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

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

@bookjava

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


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

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

Showing posts older than #3779 · Back to latest

Older Posts 13 shown
Post #3778 1.57K
🧠 Неочевидный performance-трюк с @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
  • 👍 9
Post #3776 1.41K
🧠 Spring Boot: правильно логируем @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
  • 👍 8
Post #3775 1.6K
🧠 Spring Boot: как НЕ попасть в ловушку с @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
  • 👍 13
Post #3772 1.5K
💡 Не делай этого в @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
  • 👍 11
  • 😁 1
Post #3771 1.28K
🧠Понимание архитектуры памяти JVM

В этой статье мы углубимся в архитектуру памяти JVM (Java Virtual Machine), исследуя, как она управляет памятью, чтобы эффективно выполнять Java-программы.

🔹Что такое JVM?

JVM — это абстрактная вычислительная машина, которая позволяет запускать Java-программы, независимо от аппаратного или операционного окружения. Она отвечает за интерпретацию байт-кода Java и выполнение его на хост-машине.

Архитектура памяти JVM

Архитектура памяти JVM делится на несколько компонентов, каждый из которых играет ключевую роль в управлении памятью во время выполнения программы. Основные области памяти JVM:

🔹1. Heap (Куча)

* Куча — это основная область памяти, используемая для хранения объектов.
* Это самая большая часть памяти и управляется сборщиком мусора (Garbage Collector).
* Объекты, созданные во время выполнения программы, размещаются в куче.

Куча делится на:

* Young Generation (Молодое поколение):

* Хранит недавно созданные объекты.
* Подразделяется на Eden Space и два Survivor Space (S0 и S1).
* Old Generation (Старое поколение):

* Хранит долго живущие объекты, которые пережили несколько сборок мусора в молодом поколении.

🔹2. Stack (Стек)

* У каждой нити (потока) есть свой собственный стек.
* Хранит фреймы методов, включая локальные переменные, операнды и возвращаемые адреса.
* Память в стеке освобождается, когда метод завершает выполнение.

🔹3. Method Area (Область методов)

* Используется для хранения метаданных классов, таких как:

* Информация о типах,
* Константы,
* Статические переменные,
* Методы и их байт-код.
* В некоторых реализациях JVM эта область называется Metaspace (начиная с Java 8).

🔹4. Program Counter (PC Register)

* Каждая нить имеет собственный регистр PC.
* Хранит адрес текущей выполняемой инструкции.

🔹5. Native Method Stack (Стек нативных методов)

* Используется для выполнения нативных (не-Java) методов, написанных на других языках, таких как C или C++.

Управление памятью и сборка мусора

JVM использует автоматическую сборку мусора для управления кучей. Различные алгоритмы (например, Mark and Sweep, Generational GC) используются для отслеживания и удаления неиспользуемых объектов. Это повышает производительность и предотвращает утечки памяти.

https://thedeveloperstory.com/2025/04/06/understanding-jvm-memory-architecture/

👉@BookJava
  • 👍 4
Post #3769 1.43K
🧠 Нюанс с Optional.map() и методами, возвращающими Optional

Многие Java-разработчики на автомате пишут что-то вроде:


Optional<User> user = findUserById(id); // возвращает Optional<User>

Optional<String> email = user.map(User::getEmail); // getEmail тоже возвращает Optional<String>


⚠️ Проблема: map() в Optional не "разворачивает" вложенные Optional. В этом примере email будет типа Optional<Optional<String>>, что почти всегда нежелательно.

📌 Правильный способ — использовать flatMap():


Optional<String> email = user.flatMap(User::getEmail);


💡 flatMap() позволяет избежать "двойной обёртки", если метод внутри map() уже возвращает Optional.


🔁 Аналогичная ситуация с Stream.map() — если внутри map() вызывается метод, возвращающий Stream, то получится Stream<Stream<T>>, и опять же нужно использовать flatMap().


🧪 Мини-памятка:

* map() — когда метод возвращает обычный тип (T → R);
* flatMap() — когда метод возвращает Optional или Stream (T → Optional<R> или T → Stream<R>).

👉@BookJava
  • 👍 16
Post #3768 1.66K
🧠 ExecutorService vs Virtual Threads: подводный камень с shutdownNow()

Java 21 принес Virtual Threads (Preview), и всё чаще появляется соблазн запускать их через ExecutorService. Но вот что важно помнить:

📌 shutdownNow() — опасная ловушка при работе с виртуальными потоками.


ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

Future<?> future = executor.submit(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("Interrupted!");
}
});

executor.shutdownNow(); // ❗️Ничего не произойдёт


💡 Почему?

Метод shutdownNow() не может прервать виртуальные потоки, если они запущены через Executors.newVirtualThreadPerTaskExecutor(). Он лишь помечает пул как завершённый и возвращает список задач, которые ещё не стартовали.
Уже запущенные виртуальные потоки продолжают выполняться — Thread.interrupt() не работает, потому что у виртуальных потоков отсутствует связь с ThreadGroup, к которому привязан shutdownNow.

⚠️ Следствие:

Если вы рассчитываете, что shutdownNow() "остановит всё" — вы можете получить утечку задач или зависания.

🔧 Что делать?

1. Контролируйте завершение через Future.cancel(true) — он вызывает interrupt() на конкретной задаче.
2. Стройте явную кооперативную модель отмены — с флагами или Thread.interrupted() внутри задачи.
3. Для массовой отмены — храните Future задач и отменяйте вручную.

👉@BookJava
  • 👍 4
Post #3767 1.85K
🧠 Знаешь ли ты, что @Transactional на private - методах не работает?

Да, Spring просто не применяет прокси к private-методам. Это частый баг, который трудно отловить: ты вызываешь приватный метод внутри бина, а транзакция… не начинается 🤷‍♂️

📌 Почему так происходит?
Spring AOP по умолчанию использует динамические прокси (JDK или CGLIB), которые перехватывают внешние вызовы. А вызов private - метода из того же класса — это внутренний вызов, который обходит прокси.

Пример, который НЕ работает:


@Service
public class UserService {

public void createUser() {
saveUser(); // Вызов мимо прокси 😞
}

@Transactional
private void saveUser() {
// Транзакция НЕ начнется!
}
}


💡 Как правильно:
1. Сделай метод public или хотя бы protected,
2. Или выноси в отдельный бин:


@Service
public class UserService {

private final TxUserSaver txUserSaver;

public UserService(TxUserSaver txUserSaver) {
this.txUserSaver = txUserSaver;
}

public void createUser() {
txUserSaver.saveUser(); // Теперь через прокси ✅
}
}

@Service
public class TxUserSaver {

@Transactional
public void saveUser() {
// Всё сработает как надо
}
}


⚠️ Проверь свои сервисы — ты можешь удивиться, сколько транзакций у тебя не работают. Особенно в проектах, где @Transactional ставят "на всякий случай".

👉@BookJava
  • 👍 4
Post #3758 2.03K
Java. Сортировки

Java. Сортировка пузырьком.
Java. О сортировке выбором.
Java. Быстрая сортировка. Объяснение на пальцах)
Java. Оценка сложности алгоритмов сортировки.
Java. Сортировка слиянием.
Java. Сортировка подсчетом.
Java. Сортировка вставками.
Java. Сортировка расческой. От пузырька до расчески.

👉@BookJava
  • 👍 4
Post #3757 1.37K
🧠 Как в Java сломать equals() и потерять данные в HashMap

Сегодня покажу, как неочевидный баг в equals()/hashCode() может привести к потере данных при работе с HashMap.

Допустим, у вас есть Entity:


public class User {
private String id;
private String name;

// equals/hashCode только по id
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}


Вроде норм. Но теперь добавим такого юзера в HashMap, а потом... изменим его id:


User user = new User();
user.setId("1");
user.setName("Alice");

Map<User, String> map = new HashMap<>();
map.put(user, "value");

user.setId("2"); // ⚠️ ключ стал "невидимым"

System.out.println(map.get(user)); // null 😱


📌 Почему так происходит?

HashMap ищет ключ по hashCode() → ищет бакет → сравнивает через equals(). А hashCode() уже другой, и объект "теряется".

💡 Совет: если вы используете объект как ключ в мапе или добавляете его в Set, не изменяйте его поля, участвующие в equals()/hashCode()!

📌 А как правильно?

- Делайте такие поля final;
- Или используйте неизменяемые типы (record);
- Или не используйте такие объекты как ключи вовсе.

Вот безопасный вариант с record:


public record User(String id, String name) {}


👉@BookJava
  • 👍 5
Post #3755 1.28K
💡Сегодня покажу крутую фишку для оптимизации чтения больших коллекций из базы через JPA.


📌 Проблема:
Когда загружаем большую коллекцию через @OneToMany, Hibernate часто делает это лениво (LAZY), но при первом доступе — забирает всю коллекцию целиком.
Это может привести к OutOfMemoryError или резкому проседанию производительности.


📌 Решение: использовать пагинацию (batch-size) или запрос коллекции порциями.

Как настроить batch-size на уровне сущности:


@Entity
public class User {

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
@BatchSize(size = 50)
private List<Order> orders;
}


🧠 Теперь Hibernate будет загружать за раз по 50 элементов, а не всю коллекцию сразу!


📌 Или можно настроить глобально через application.properties:


spring.jpa.properties.hibernate.default_batch_fetch_size=50



⚠️ Важно:

- @BatchSize работает только для LAZY-связей.
- Это не пагинация в SQL, а оптимизация внутренних запросов Hibernate.
- Если коллекция огромная (100k+ записей) — лучше делать явные paged запросы в репозитории.

💡Помните: без настройки batch-size Hibernate может сломать приложение под нагрузкой. Оптимизируйте загрузку коллекций заранее!

👉@BookJava
  • 🔥 6
  • 👍 3
  • ❤ 1
Post #3753 1.17K
Как работают instance initializer blocks.


Пример с родительским и дочерним классами:


class Parent {
int a = 5;

{
System.out.println("Parent instance initializer");
a = 10;
}

Parent() {
System.out.println("Parent constructor, a = " + a);
}
}

class Child extends Parent {
int b = 15;

{
System.out.println("Child instance initializer");
b = 25;
}

Child() {
System.out.println("Child constructor, b = " + b);
}
}

public class Test {
public static void main(String[] args) {
Child child = new Child();
}
}



Вывод программы:


Parent instance initializer
Parent constructor, a = 10
Child instance initializer
Child constructor, b = 25


Пошаговое выполнение:

1. Сначала загружается родительский класс Parent.
2. Выполняется:
- Инициализация полей родителя (a = 5),
- Потом instance initializer блока родителя (a = 10),
- Потом конструктор родителя (Parent()).
3. Далее переходим к дочернему классу Child:
- Инициализация полей дочернего класса (b = 15),
- Потом instance initializer блока дочернего класса (b = 25),
- Потом конструктор дочернего класса (Child()).


Важный порядок действий:

1. Инициализация родителя → 2. Конструктор родителя → 3. Инициализация потомка → 4. Конструктор потомка.

Блоки инициализации всегда выполняются до тела конструктора, но после вызова super().

👉@BookJava
  • 👍 6
Post #3752 1.15K
В Java instance initializer blocks (блоки инициализации экземпляра) выполняются в следующем порядке:

- Они выполняются каждый раз, когда создается новый объект класса.
- Выполнение происходит после вызова конструктора родительского класса (super()), но до тела конструктора текущего класса.

Порядок инициализации:

1. Сначала инициализируются поля в порядке их объявления.
2. Затем выполняются instance initializer blocks, в том порядке, в котором они написаны в коде.
3. После этого выполняется тело конструктора.

Пример:


class Example {
int x = 10;

{
System.out.println("Instance initializer block");
x = 20;
}

Example() {
System.out.println("Constructor");
System.out.println("x = " + x);
}

public static void main(String[] args) {
Example ex = new Example();
}
}


Вывод:

Instance initializer block
Constructor
x = 20


Ключевые моменты:
- Статические блоки (static {}) — другое дело: они выполняются один раз при загрузке класса.
- Instance initializer blocks полезны для общей инициализации, которую нужно выполнять вне зависимости от того, какой конструктор вызывается.

👉@BookJava
  • 👍 7
  • 🔥 3
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 →