Media is too big
VIEW IN TELEGRAM
Разработчик выложил проект ShadowStream, систему отслеживания изменений в базе данных (CDC), построенную на PostgreSQL logical replication.
Цель проекта — ловить любые изменения в реальном времени и передавать их в потоковую инфраструктуру.
Как это работает:
- изменения в базе (INSERT, UPDATE, DELETE) сразу перехватываются через logical replication
- события сериализуются в Protobuf и отправляются в Redis Streams для быстрого доступа
- параллельно те же данные архивируются в Kafka для надежного хранения
- Kafka использует grouped consumer'ов: два обработчика работают параллельно, плюс резервная группа с отдельным offset
- поверх всего в Django Admin добавлена визуализация gRPC-вызовов
Исходники открыты на GitHub
Проект может пригодиться тем, кто работает с потоковой обработкой данных, аналитикой, репликацией или интеграцией микросервисов.🙂
👉 Java Portal
Цель проекта — ловить любые изменения в реальном времени и передавать их в потоковую инфраструктуру.
Как это работает:
- изменения в базе (INSERT, UPDATE, DELETE) сразу перехватываются через logical replication
- события сериализуются в Protobuf и отправляются в Redis Streams для быстрого доступа
- параллельно те же данные архивируются в Kafka для надежного хранения
- Kafka использует grouped consumer'ов: два обработчика работают параллельно, плюс резервная группа с отдельным offset
- поверх всего в Django Admin добавлена визуализация gRPC-вызовов
Исходники открыты на GitHub
Проект может пригодиться тем, кто работает с потоковой обработкой данных, аналитикой, репликацией или интеграцией микросервисов.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11❤4
Java даёт много вариантов по части синтаксиса, но функциональные интерфейсы — одна из самых аккуратных и приятных фишек языка ☕️
Сегодня разберём 4 штуки, которые встречаются чаще всего. Если поймёшь их, писать код станет современнее, местами чище
Идея простая👇
Функциональный интерфейс — это интерфейс с одним абстрактным методом. Благодаря этому его можно реализовать через лямбды.
В Java их много, но вот четыре, которые ты будешь видеть постоянно:
👉 Consumer — делает что-то
Consumer принимает значение и ничего не возвращает.
Отлично подходит для побочных эффектов: логирование, вывод в консоль, сохранение, отправка и так далее.
Проще говоря:
"получи это и сделай с этим что-то".
👉 Supplier — дай что-то
Supplier ничего не принимает и возвращает значение.
Часто используется для получения конфигураций, генерации ID, ленивого создания объектов и прочего.
То есть:
"выдай нужную штуку, когда я попрошу".
👉 Function<T, R> — преобразуй что-то
Принимает значение типа T и возвращает значение типа R.
На практике эта штука — самая распространённая.
По смыслу:
"получаю T, возвращаю R".
👉 Predicate — реши что-то (true/false)
Принимает значение и возвращает boolean.
Часто нужен для фильтрации списков, простых проверок, валидаций, правил.
То есть:
"подходит или не подходит под условие".
Важно:
Эти интерфейсы существуют не ради компактного кода.
Они нужны, чтобы ты думал через операции, а не через классы.
Они идеально заходят в Streams, в коллбеки, в валидации, в композицию логики — везде, где есть простая операция, для которой не нужна отдельная сущность.
Это не замена всему на свете. Речь не про то, чтобы переписать всю систему в функциональном стиле.
Но они реально помогают во множестве сценариев.
Если научишься читать Function, Consumer, Supplier и Predicate, то спокойно разберёшь и напишешь современный Java-код без лишних страданий. И это уже хороший шаг вперёд.😁
👉 Java Portal
Сегодня разберём 4 штуки, которые встречаются чаще всего. Если поймёшь их, писать код станет современнее, местами чище
Идея простая
Функциональный интерфейс — это интерфейс с одним абстрактным методом. Благодаря этому его можно реализовать через лямбды.
В Java их много, но вот четыре, которые ты будешь видеть постоянно:
Consumer принимает значение и ничего не возвращает.
Отлично подходит для побочных эффектов: логирование, вывод в консоль, сохранение, отправка и так далее.
Consumer<String> consumer = str -> System.out.println(str);
consumer.accept("Hola");
Проще говоря:
"получи это и сделай с этим что-то".
Supplier ничего не принимает и возвращает значение.
Часто используется для получения конфигураций, генерации ID, ленивого создания объектов и прочего.
Supplier<Double> supplier = () -> Math.random();
supplier.get();
То есть:
"выдай нужную штуку, когда я попрошу".
Принимает значение типа T и возвращает значение типа R.
На практике эта штука — самая распространённая.
Function<Integer, String> function = number -> "N° " + number;
function.apply(5);
По смыслу:
"получаю T, возвращаю R".
Принимает значение и возвращает boolean.
Часто нужен для фильтрации списков, простых проверок, валидаций, правил.
Predicate<String> predicate = s -> s.length() > 5;
predicate.test("Java");
То есть:
"подходит или не подходит под условие".
Важно:
Эти интерфейсы существуют не ради компактного кода.
Они нужны, чтобы ты думал через операции, а не через классы.
Они идеально заходят в Streams, в коллбеки, в валидации, в композицию логики — везде, где есть простая операция, для которой не нужна отдельная сущность.
Это не замена всему на свете. Речь не про то, чтобы переписать всю систему в функциональном стиле.
Но они реально помогают во множестве сценариев.
Если научишься читать Function, Consumer, Supplier и Predicate, то спокойно разберёшь и напишешь современный Java-код без лишних страданий. И это уже хороший шаг вперёд.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍18❤4🔥2🤣1
Кандидаты часто путаются, когда спрашивают:
На самом деле всё очень просто.
* Rate limiting
Задаёт фиксированный максимум запросов.
Если превысил лимит — лишние запросы просто блокируются.
Пример: 100 запросов в минуту разрешено. 101-й — отклоняется.
* Throttling
Не блокирует. Он замедляет обработку запросов, когда ты начинаешь спамить.
Пример: после 100 запросов каждый следующий обрабатывается с задержкой, но всё равно проходит.
Запомнить легко:
Rate limiting — стоп сверхлимита.
Throttling — замедление сверхлимита.
👉 Java Portal
в чем разница между rate limiting и throttling?
На самом деле всё очень просто.
* Rate limiting
Задаёт фиксированный максимум запросов.
Если превысил лимит — лишние запросы просто блокируются.
Пример: 100 запросов в минуту разрешено. 101-й — отклоняется.
* Throttling
Не блокирует. Он замедляет обработку запросов, когда ты начинаешь спамить.
Пример: после 100 запросов каждый следующий обрабатывается с задержкой, но всё равно проходит.
Запомнить легко:
Rate limiting — стоп сверхлимита.
Throttling — замедление сверхлимита.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7❤3🤔1
Совет по Java: в JPA можно использовать native query не только для SELECT, но и для UPDATE, DELETE и INSERT. Учти, что в этом случае всё проходит мимо EntityManager и PersistenceContext.
👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4🔥2👍1
Одна из самых опасных проблем в распределенных системах это двойная запись:
сохранить что-то в своей базе и параллельно опубликовать событие в другой системе, рассчитывая что оба шага пройдут вместе.
А что если нет?
Представь классический поток:
1️⃣ Сохраняешь заказ в базе.
2️⃣ Отправляешь событие в Kafka или делаешь запрос в другую API, чтобы сообщить что заказ создан.
Где проблема?
👉 Если шаг 1 прошел, а шаг 2 упал:
База говорит заказ создан, но внешний сервис об этом не знает.
👉 Если шаг 2 прошел, а шаг 1 упал:
Ты опубликовал фантомное событие о том, чего нет.
Это и есть двойная запись.
И если думаешь что это тебя не коснется, просто подожди пока прод покажет тебе реальность.
Тут и появляется Transactional Outbox Pattern.
Что именно делает этот паттерн?🤔
Избавляет от необходимости писать в два места одновременно.
Превращает внешнюю запись в надежный процесс.
Идея простая, вот минимальный способ это реализовать:
1. Когда сохраняешь данные в базе (INSERT/UPDATE),
ты параллельно пишешь событие в отдельную таблицу, например outbox.
2. Оба INSERT выполняются в одной транзакции.
Если что-то падает, падает все.
Так достигается гарантированное консистентное состояние.
3. Затем отдельный процесс (poller или scheduler) читает эту таблицу и публикует реальное событие в Kafka, RabbitMQ или куда нужно.
4. Если публикация упала, ничего страшного.
Событие остается в таблице пока его не получится отправить.
С этим достаточно простым потоком ты получаешь консистентность без двойной записи.
Почему это так хорошо работает?
Потому что принимает неприятную правду:
Нельзя рассчитывать что два разных системы корректно обработают одну и ту же транзакцию.
❌ База умеет в транзакции.
❌ Kafka — нет.
❌ Rabbit — нет.
❌ Webhook тем более.
Поэтому решение не в том чтобы это «продавить», а в том чтобы адаптировать архитектуру к реальности:
Единственная запись, которой реально можно доверять — запись в твою базу.
Все внешнее (то что не хранится в твоей базе) должно выполняться позже, с ретраями, логами и прочим.
Но да, это не бесплатно.
Нужно:
—> Создать таблицу outbox.
—> Настроить ретраи.
—> Удалять обработанные события.
—> Мониторить poller (или любую другую реализацию).
—> Не допустить двойные вставки в outbox чтобы избежать дублей.
И ключевая мысль:
Цена Transactional Outbox намного ниже цены ручной починки рассинхрона между сервисами и системами.
А это в проде дороже золота.
👉 Java Portal
сохранить что-то в своей базе и параллельно опубликовать событие в другой системе, рассчитывая что оба шага пройдут вместе.
А что если нет?
Представь классический поток:
Где проблема?
База говорит заказ создан, но внешний сервис об этом не знает.
Ты опубликовал фантомное событие о том, чего нет.
Это и есть двойная запись.
И если думаешь что это тебя не коснется, просто подожди пока прод покажет тебе реальность.
Тут и появляется Transactional Outbox Pattern.
Что именно делает этот паттерн?
Избавляет от необходимости писать в два места одновременно.
Превращает внешнюю запись в надежный процесс.
Идея простая, вот минимальный способ это реализовать:
1. Когда сохраняешь данные в базе (INSERT/UPDATE),
ты параллельно пишешь событие в отдельную таблицу, например outbox.
2. Оба INSERT выполняются в одной транзакции.
Если что-то падает, падает все.
Так достигается гарантированное консистентное состояние.
3. Затем отдельный процесс (poller или scheduler) читает эту таблицу и публикует реальное событие в Kafka, RabbitMQ или куда нужно.
4. Если публикация упала, ничего страшного.
Событие остается в таблице пока его не получится отправить.
С этим достаточно простым потоком ты получаешь консистентность без двойной записи.
Почему это так хорошо работает?
Потому что принимает неприятную правду:
Нельзя рассчитывать что два разных системы корректно обработают одну и ту же транзакцию.
Поэтому решение не в том чтобы это «продавить», а в том чтобы адаптировать архитектуру к реальности:
Единственная запись, которой реально можно доверять — запись в твою базу.
Все внешнее (то что не хранится в твоей базе) должно выполняться позже, с ретраями, логами и прочим.
Но да, это не бесплатно.
Нужно:
—> Создать таблицу outbox.
—> Настроить ретраи.
—> Удалять обработанные события.
—> Мониторить poller (или любую другую реализацию).
—> Не допустить двойные вставки в outbox чтобы избежать дублей.
И ключевая мысль:
Цена Transactional Outbox намного ниже цены ручной починки рассинхрона между сервисами и системами.
А это в проде дороже золота.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤2
Совет по Java: старайся не шарить данные между потоками.
Используй неизменяемые объекты
Или строй взаимодействие потоков через сообщения, без прямой модификации общего состояния
С неизменяемыми объектами:
Обмен сообщениями:
👉 Java Portal
Используй неизменяемые объекты
Или строй взаимодействие потоков через сообщения, без прямой модификации общего состояния
С неизменяемыми объектами:
record Book(String title, int price) {} // Immutable
public class BookJob implements Runnable {
private final Book book;
... // constructor
@Override
public void run() {
System.out.println(book.title() + " " + book.price());
}
}Обмен сообщениями:
...
new Thread(() -> {
try {
queue.put("mess1");
} catch (InterruptedException e) {}
}).start();
...
new Thread(() -> {
try {
String mess = queue.take();
} catch (InterruptedException e) {}
}).start();
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🤔2
image_2025-12-04_09-41-20.png
1.4 MB
Знали, что у EnableWebSecurity есть параметр debug, который включает отладку безопасности?
🤯
Это помогает разобраться, что вообще происходит во время разработки.
Но в проде такое включать нельзя.
👉 Java Portal
Это помогает разобраться, что вообще происходит во время разработки.
Но в проде такое включать нельзя.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Совет по Java: начиная с Java 19 можно потрогать фичу под названием virtual threads.
Virtual threads это лёгкие потоки, которые управляются JVM, а не операционной системой.
JVM умеет приостанавливать и возобновлять выполнение без лишней траты ресурсов ОС.
IO-вызовы не блокируют потоки ОС.
Виртуальные потоки работают поверх небольшого пула платформенных потоков.
Пример:
👉 Java Portal
Virtual threads это лёгкие потоки, которые управляются JVM, а не операционной системой.
JVM умеет приостанавливать и возобновлять выполнение без лишней траты ресурсов ОС.
IO-вызовы не блокируют потоки ОС.
Виртуальные потоки работают поверх небольшого пула платформенных потоков.
Пример:
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
Thread.sleep(1000);
});
}
}Please open Telegram to view this post
VIEW IN TELEGRAM
👍7❤4
Нужна API, чтобы попрактиковаться в программировании?
Это, пожалуй, лучший вариант для приложений с прогнозом погоды.
✓ Полностью бесплатная
✓ Без регистрации и без API-ключа
✓ Работает по HTTPS и с включённым CORS
Можно тренироваться с JavaScript, Python, Java и чем угодно ещё:
→ open-meteo.com
👉 Java Portal
Это, пожалуй, лучший вариант для приложений с прогнозом погоды.
✓ Полностью бесплатная
✓ Без регистрации и без API-ключа
✓ Работает по HTTPS и с включённым CORS
Можно тренироваться с JavaScript, Python, Java и чем угодно ещё:
→ open-meteo.com
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3👍3🔥2🤯1
Java-совет: ты можешь использовать LinkedHashMap, чтобы легко реализовать LRU-кеш (Least Recently Used).
Тебе просто нужно заинстанцировать это вот так:
👉 Java Portal
Тебе просто нужно заинстанцировать это вот так:
int capacity = 3;
Map<Integer, String> cache = new LinkedHashMap<>(capacity, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity;
}
};
cache.put(1, "one");
cache.put(2, "two");
cache.put(3, "three");
cache.get(1);
cache.put(4, "four"); // 2 удаляется (наименее недавно использованный)
System.out.println(cache);
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11❤4🤔1
freeCodeCamp выкатили бесплатный курс по Git и GitHub для новичков. За 1 час разберёшь базу: ветки, слияния, pull request’ы и базовую командную работу. Отличный быстрый вход для тех, кто откладывал Git «на потом».
Git-курс тут
👉 Java Portal
Git-курс тут
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Одна идея, на которой я люблю делать акцент, это автобоксинг и анбоксинг в Java.
Java фактически дает нам два параллельных мира:
Примитивы: int, long, double = быстрые, компактные, живут на стеке.
Обертки: Integer, Long, Double = полноценные объекты в heap.
Между этими мирами Java делает одно автоматическое преобразование:
Автобоксинг - превращение примитива в объект
Анбоксинг - обратное преобразование
Работает удобно, пока в какой-то момент не начинает мешать.
Почему это может быть проблемой?
Потому что при каждой конверсии у Java появляется лишняя работа:
Создаются новые объекты
Увеличивается нагрузка на GC
CPU делает больше операций
Срываются JIT-оптимизации
В циклах с высокой нагрузкой появляются скрытые задержки
В большинстве приложений это незаметно, но если проявится, то обычно довольно резко.
Типичный пример - коллекции:
Очевидно, что пример синтетический, но суть в том, что в этом цикле i упаковывается в Integer миллион раз.
Перенеси этот сценарий на реальные участки твоего кода.
Каждая конверсия создает объект, нагружает GC и сбивает процессорный кеш.
Из-за этого приложение начинает подвисать, когда GC активируется.
Коллекции - не единственный источник проблем. Есть и сравнения:
При работе с обертками оператор == сравнивает ссылки, а не значения.
Если происходит неявный анбоксинг, оба значения сначала превращаются в int и только потом сравниваются.
Такое смешение поведения местами непредсказуемо и приводит к трудноуловимым багам.
Есть еще и лямбды:
Если работаете с числами, лучше брать примитивные стримы:
IntStream
LongStream
DoubleStream
Они экономят память, убирают боксинг и работают быстрее.
Худший сценарий - структуры подсчета:
Каждая операция делает:
Анбоксинг существующего значения
Сложение как примитива
Обратный автобоксинг результата
Масштабируется это плохо и убивает производительность при высокой конкуренции.
Хорошие варианты:
- AtomicInteger
- LongAdder
- Хранить счетчик как примитив и конвертировать только при выдаче наружу
Значит ли это, что обертки плохие? Нет.
Проблема не в них, а в случайном использовании.
Обертки используют, когда:
Нужны null-значения
API требует объектные типы
Есть доменная логика, где важна идентичность или отсутствие значения
Примитивы используют, когда:
Есть большие циклы
Много повторных вычислений
Важно избегать лишнего GC
Нужна стабильная производительность
Автобоксинг удобен, но у него есть подводные камни.
Обычно он не мешает, но когда все-таки мешает, его эффекты сложно отследить.
Хорошая новость: исправляется это просто.
Используй примитивы в вычислениях
Используй примитивные стримы
Не применяй обертки в счетчиках
Проверь циклы, создающие лишние объекты
Пара мелких правок может убрать тысячи лишних объектов и заметно снизить нагрузку на приложение.
👉 Java Portal
Java фактически дает нам два параллельных мира:
Примитивы: int, long, double = быстрые, компактные, живут на стеке.
Обертки: Integer, Long, Double = полноценные объекты в heap.
Между этими мирами Java делает одно автоматическое преобразование:
Автобоксинг - превращение примитива в объект
Анбоксинг - обратное преобразование
Работает удобно, пока в какой-то момент не начинает мешать.
Почему это может быть проблемой?
Потому что при каждой конверсии у Java появляется лишняя работа:
Создаются новые объекты
Увеличивается нагрузка на GC
CPU делает больше операций
Срываются JIT-оптимизации
В циклах с высокой нагрузкой появляются скрытые задержки
В большинстве приложений это незаметно, но если проявится, то обычно довольно резко.
Типичный пример - коллекции:
List<Integer> numeros = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
numeros.add(i);
}
Очевидно, что пример синтетический, но суть в том, что в этом цикле i упаковывается в Integer миллион раз.
Перенеси этот сценарий на реальные участки твоего кода.
Каждая конверсия создает объект, нагружает GC и сбивает процессорный кеш.
Из-за этого приложение начинает подвисать, когда GC активируется.
Коллекции - не единственный источник проблем. Есть и сравнения:
Integer a = 1000;
Integer b = 1000;
if (a == b) { ... }
При работе с обертками оператор == сравнивает ссылки, а не значения.
Если происходит неявный анбоксинг, оба значения сначала превращаются в int и только потом сравниваются.
Такое смешение поведения местами непредсказуемо и приводит к трудноуловимым багам.
Есть еще и лямбды:
Stream<Integer> s = IntStream.range(0, 1_000_000)
.boxed(); // миллион автобоксингов
Если работаете с числами, лучше брать примитивные стримы:
IntStream
LongStream
DoubleStream
Они экономят память, убирают боксинг и работают быстрее.
Худший сценарий - структуры подсчета:
Map<String, Integer> counter = new HashMap<>();
counter.put(key, counter.getOrDefault(key, 0) + 1);
Каждая операция делает:
Анбоксинг существующего значения
Сложение как примитива
Обратный автобоксинг результата
Масштабируется это плохо и убивает производительность при высокой конкуренции.
Хорошие варианты:
- AtomicInteger
- LongAdder
- Хранить счетчик как примитив и конвертировать только при выдаче наружу
Значит ли это, что обертки плохие? Нет.
Проблема не в них, а в случайном использовании.
Обертки используют, когда:
Нужны null-значения
API требует объектные типы
Есть доменная логика, где важна идентичность или отсутствие значения
Примитивы используют, когда:
Есть большие циклы
Много повторных вычислений
Важно избегать лишнего GC
Нужна стабильная производительность
Автобоксинг удобен, но у него есть подводные камни.
Обычно он не мешает, но когда все-таки мешает, его эффекты сложно отследить.
Хорошая новость: исправляется это просто.
Используй примитивы в вычислениях
Используй примитивные стримы
Не применяй обертки в счетчиках
Проверь циклы, создающие лишние объекты
Пара мелких правок может убрать тысячи лишних объектов и заметно снизить нагрузку на приложение.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍5🔥2
This media is not supported in your browser
VIEW IN TELEGRAM
Похоже, это претендент на TUI года ⭐️
gitlogue — инструмент для «кинематографичного» воспроизведения Git-коммитов прямо в терминале.
Смотри, как коммиты развертываются с анимацией ввода, подсветкой синтаксиса и живым обновлением дерева файлов.
Буквально наблюдай, как твой репозиторий пишет себя сам.
Написан на Rust
GitHub: https://github.com/unhappychoice/gitlogue
👉 Java Portal
gitlogue — инструмент для «кинематографичного» воспроизведения Git-коммитов прямо в терминале.
Смотри, как коммиты развертываются с анимацией ввода, подсветкой синтаксиса и живым обновлением дерева файлов.
Буквально наблюдай, как твой репозиторий пишет себя сам.
Написан на Rust
GitHub: https://github.com/unhappychoice/gitlogue
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8
Часто мы пишем
Но за этим стоит довольно сложная и интересная система , а именно ClassLoader’ы.
Именно они отвечают за то, чтобы находить, проверять и загружать каждый класс, который использует твоё приложение.
Без ClassLoader’ов нет JVM, нет Spring, нет плагинов, вообще ничего нет.
Что такое ClassLoader?
Это компонент JVM, который отвечает за:
- Поиск определения класса (файлы .class, JAR’ы, модули).
- Загрузку его в память.
- Проверку байткода на валидность и безопасность.
- Определение класса в изолированном пространстве.
Проще говоря, он превращает байткод в то, что JVM может исполнять.
Иерархия держит хаос под контролем
В Java используется модель делегирования вверх {parent delegation}
Это защищает от конфликтов, дублирования и неожиданных подмен классов.
Основные загрузчики:
- Bootstrap ClassLoader
Написан на C++, загружает базовые классы языка (java.lang.*, java.util.* и так далее).
Живёт прямо внутри JVM.
- Platform / Extension ClassLoader
Загружает библиотеки самого JDK и системные модули.
- Application ClassLoader
Загружает твои классы, зависимости и весь прикладной код.
Типичный поток загрузки выглядит так:
Когда ClassLoader пытается загрузить класс, он сначала делегирует запрос родителю.
Если родитель не находит класс, тогда уже текущий загрузчик пытается загрузить его сам.
Это не даёт тебе случайно переопределить String, List, Object и другие базовые классы.
Как загружается твой код?
Когда ты запускаешь JAR:
Твой код загружает Application ClassLoader. Процесс простой:
- Проходит по classpath
- Открывает JAR’ы
- Находит .class
- Загружает их по мере необходимости
Загрузка классов происходит по требованию.
Если у тебя 200 классов, а реально используются только 10, будут загружены только эти 10.
Почему это реально важно?
Потому что всё, что мы используем поверх Java, строит свою работу на ClassLoader’ах:
Spring Boot создаёт собственный ClassLoader для загрузки fat JAR’ов.
Модульные приложения используют загрузчики для изоляции компонентов.
Веб-контейнеры вроде Tomcat создают отдельный ClassLoader на каждое приложение.
Плагины (IDEA, Jenkins, Minecraft, OSGi) работают через отдельные загрузчики.
Инструменты hot reload (Spring DevTools, JRebel) зависят от динамической перезагрузки классов.
Зачем это понимать на практике?
Рано или поздно ты столкнёшься с ошибками вида
Самые частые проблемы:
ClassNotFoundException — класс отсутствует в classpath текущего загрузчика.
NoClassDefFoundError — класс был доступен, но возникла ошибка при загрузке или верификации.
“Невозможный” ClassCastException — объект вроде бы нужного типа, но Java считает иначе. Обычно это означает, что один и тот же класс был загружен разными ClassLoader’ами.
Memory leak в веб-приложениях — Tomcat некорректно выгружает ClassLoader при redeploy.
Последнее происходит значительно чаще, чем кажется.
ClassLoader’ы это как кровеносная система Java ( их не видно, но без них ничего не работает. )
Понимание того, как Java загружает классы это не пустая теория. Это делает тебя сильнее в отладке, увереннее при разборе сложных багов и даёт чёткое понимание среды, в которой реально работает твоё приложение.
И когда в продакшене что-то падает с
👉 Java Portal
public static void main и просто предполагаем, что Java сама «делает свою работу».Но за этим стоит довольно сложная и интересная система , а именно ClassLoader’ы.
Именно они отвечают за то, чтобы находить, проверять и загружать каждый класс, который использует твоё приложение.
Без ClassLoader’ов нет JVM, нет Spring, нет плагинов, вообще ничего нет.
Что такое ClassLoader?
Это компонент JVM, который отвечает за:
- Поиск определения класса (файлы .class, JAR’ы, модули).
- Загрузку его в память.
- Проверку байткода на валидность и безопасность.
- Определение класса в изолированном пространстве.
Проще говоря, он превращает байткод в то, что JVM может исполнять.
Иерархия держит хаос под контролем
В Java используется модель делегирования вверх {parent delegation}
Это защищает от конфликтов, дублирования и неожиданных подмен классов.
Основные загрузчики:
- Bootstrap ClassLoader
Написан на C++, загружает базовые классы языка (java.lang.*, java.util.* и так далее).
Живёт прямо внутри JVM.
- Platform / Extension ClassLoader
Загружает библиотеки самого JDK и системные модули.
- Application ClassLoader
Загружает твои классы, зависимости и весь прикладной код.
Типичный поток загрузки выглядит так:
Когда ClassLoader пытается загрузить класс, он сначала делегирует запрос родителю.
Если родитель не находит класс, тогда уже текущий загрузчик пытается загрузить его сам.
Это не даёт тебе случайно переопределить String, List, Object и другие базовые классы.
Как загружается твой код?
Когда ты запускаешь JAR:
java -jar app.jar
Твой код загружает Application ClassLoader. Процесс простой:
- Проходит по classpath
- Открывает JAR’ы
- Находит .class
- Загружает их по мере необходимости
Загрузка классов происходит по требованию.
Если у тебя 200 классов, а реально используются только 10, будут загружены только эти 10.
Почему это реально важно?
Потому что всё, что мы используем поверх Java, строит свою работу на ClassLoader’ах:
Spring Boot создаёт собственный ClassLoader для загрузки fat JAR’ов.
Модульные приложения используют загрузчики для изоляции компонентов.
Веб-контейнеры вроде Tomcat создают отдельный ClassLoader на каждое приложение.
Плагины (IDEA, Jenkins, Minecraft, OSGi) работают через отдельные загрузчики.
Инструменты hot reload (Spring DevTools, JRebel) зависят от динамической перезагрузки классов.
Зачем это понимать на практике?
Рано или поздно ты столкнёшься с ошибками вида
ClassNotFoundException. Понимание ClassLoader’ов даёт отличную точку старта для отладки.Самые частые проблемы:
ClassNotFoundException — класс отсутствует в classpath текущего загрузчика.
NoClassDefFoundError — класс был доступен, но возникла ошибка при загрузке или верификации.
“Невозможный” ClassCastException — объект вроде бы нужного типа, но Java считает иначе. Обычно это означает, что один и тот же класс был загружен разными ClassLoader’ами.
Memory leak в веб-приложениях — Tomcat некорректно выгружает ClassLoader при redeploy.
Последнее происходит значительно чаще, чем кажется.
ClassLoader’ы это как кровеносная система Java ( их не видно, но без них ничего не работает. )
Понимание того, как Java загружает классы это не пустая теория. Это делает тебя сильнее в отладке, увереннее при разборе сложных багов и даёт чёткое понимание среды, в которой реально работает твоё приложение.
И когда в продакшене что-то падает с
ClassNotFoundException, ты точно будешь рад, что разобрался в этом заранее.Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤6