Библиотека Java разработчика
10.8K subscribers
1.14K photos
564 videos
58 files
1.44K links
📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate.


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

РКН clck.ru/3KoGeP
Download Telegram
📌 Ошибка: Вызов метода с @Transactional внутри того же класса не запускает новую транзакцию.

Почему? Spring оборачивает бин в прокси, а внутренние вызовы проходят мимо прокси, значит, аннотация игнорируется.

Пример проблемы:

@Service
public class OrderService {

@Transactional
public void createOrder() {
saveOrder();
}

@Transactional
public void saveOrder() {
// Новый транзакционный контекст не создастся!
}
}


💡 Как правильно:
1. Вынести saveOrder() в отдельный бин.
2. Или получить прокси текущего бина через AopContext:


@Service
@EnableAspectJAutoProxy(exposeProxy = true) // важно!
public class OrderService {

@Transactional
public void createOrder() {
((OrderService) AopContext.currentProxy()).saveOrder();
}

@Transactional
public void saveOrder() {
// Теперь всё ок
}
}


⚠️ Важный момент: exposeProxy = true нужен на уровне конфигурации, иначе AopContext не заработает.


Понимание этой тонкости критично для корректного управления транзакциями в Spring! 🚀

👉@BookJava
👍5👎3
🧠 Сегодня покажу вам полезный трюк для Spring Boot: как элегантно валидировать параметры контроллера без лишнего кода.

Часто вижу, как в контроллерах вручную проверяют @RequestParam на пустоту или формат. Это шумит код и приводит к ошибкам.

Вместо этого используйте аннотации валидации прямо на параметрах:


@RestController
@RequestMapping("/api")
@Validated // Обязательно!
public class UserController {

@GetMapping("/users")
public List<User> getUsers(
@RequestParam @NotBlank String name,
@RequestParam @Min(18) int age
) {
// Если валидация не пройдена — автоматически вернётся 400 Bad Request
return userService.findUsers(name, age);
}
}


📌 Ключевые моменты:
- Обязательно ставим @Validated над классом контроллера.
- На параметры добавляем любые стандартные аннотации из jakarta.validation.constraints.
- Ошибки валидации Spring обработает автоматически через MethodArgumentNotValidException.

💡 Можно кастомизировать ответ на ошибку, добавив глобальный @ExceptionHandler.

⚠️ Без @Validated аннотации на контроллере валидация параметров работать не будет!

👉@BookJava
👍10
Есть фича. Есть дедлайн. Есть понимание, что тесты надо писать… но они отъедают время, которого итак впритык.

Один наш знакомый девелопер сказал: «С тех пор как поставил Explyt Test — начал писать меньше тестов… но покрытие стало лучше. Как это вообще возможно?!»

Попробуйте сами. Плагин сам предлагает тесты для вашего кода — прямо в IDE.
👉 explyt.ai — сэкономь себе пару часов уже сегодня.
👍4🔥1
В 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
Как работают 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
💡Сегодня покажу крутую фишку для оптимизации чтения больших коллекций из базы через 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👍31
🤖 А ты справишься с тестом по Kotlin?

🏆 Пройди тест из 10 вопросов, проверь свой уровень знаний и получи скидку на онлайн-курс «Kotlin Backend Developer. Professional» от OTUS!

Если успешно пройдешь тест, сможешь забронировать место в группе по выгодной цене! И еще дарим промокод Kotlin5

🎫
Курс можно приобрести в рассрочку

➡️ Пройти тест и забрать скидку: https://vk.cc/cLkRLn

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
Please open Telegram to view this post
VIEW IN TELEGRAM
🧠 Как в 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
Java. Сортировки

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

👉@BookJava
👍4
⚡️ Квиз на знание Java

Пройти тестирование — сложно! А ты справишься?
21 вопрос, 30 минут

Проверь себя - пройди квиз и оцени свой уровень навыков, а также свою готовность к обучению на курсе — «Разработчик на Spring Framework» от OTUS.

💻 За 5 месяцев обучения ты освоишь современные возможности Spring, научишься быстро проходить путь от идеи до production-grade, создавать Web-приложения на микросервисной архитектуре и решать высокоуровневые задачи по разработке.

👉 ПРОЙТИ ТЕСТ: https://vk.cc/cLshxc

Если успешно пройдешь тест, сможешь забронировать место в группе по выгодной цене! И еще дарим промокод SPRING5

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
👍1👎1
🧠 Знаешь ли ты, что @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
🧠 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
🧠 Нюанс с 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