Удаление по значению в ArrayList
Операция remove(Object o) в ArrayList сочетает в себе линейный поиск (аналогично contains) и структурную реорганизацию (аналогично удалению по индексу).
Детальный процесс выполнения remove(element)
Фаза поиска:
Выполняется последовательный обход массива от начала до конца для поиска первого вхождения элемента. Поиск использует семантику equals() и специально обрабатывает null значения.
Фаза удаления:
Если элемент найден на позиции i:
Сохраняется значение для возврата
Выполняется сдвиг элементов от i+1 до size-1 на одну позицию влево
Последняя позиция обнуляется
Размер уменьшается, modCount увеличивается
Особенности:
Удаляется только первое вхождение элемента
Если элемент не найден, возвращается false
Если элемент найден и удален, возвращается true
Временная сложность
Операция имеет сложность O(n) в худшем случае:
Поиск: O(n) в худшем случае
Удаление: O(n) в худшем случае (при удалении из начала)
Удаление по значению в LinkedList
Процесс аналогичен удалению по индексу, но с предварительным поиском по значению.
Детальный процесс
Фаза поиска:
Последовательный обход узлов от головы до хвоста с использованием equals() для сравнения.
Фаза удаления:
При нахождении узла с совпадающим значением выполняется операция "вырезания" аналогично удалению по индексу.
Особенности:
Удаляется только первый найденный узел с совпадающим значением
Возвращает true при успешном удалении, false при отсутствии элемента
Сравнительный анализ операций удаления
Временная сложность
Удаление по индексу:
ArrayList: O(n) в худшем случае (удаление из начала)
LinkedList: O(n) в худшем случае (удаление из середины)
Удаление по значению:
ArrayList: O(n) поиск + O(n) удаление = O(n)
LinkedList: O(n) поиск + O(1) удаление = O(n)
Потребление памяти
ArrayList:
Не требует дополнительной памяти (кроме временных переменных)
Может оставлять "пустоты" в массиве (обнуленные ссылки)
LinkedList:
Освобождает память узла (24-32 байта на узел)
Требует времени garbage collector для очистки
Многопоточные аспекты
Проблемы конкурентного доступа
Несинхронизированные реализации:
Обе операции не являются thread-safe и могут привести к:
Состояниям гонки при одновременных модификациях
Повреждению внутренних структур данных
Непредсказуемому поведению итераторов
Стратегии синхронизации
Явная синхронизация:
Thread-safe альтернативы:
CopyOnWriteArrayList для сценариев "частое чтение, редкая запись"
Collections.synchronizedList() с external locking
Concurrent коллекции для специализированных use cases
Memory consistency
Операции remove и contains требуют proper memory barriers для обеспечения visibility изменений между потоками. Без синхронизации нет гарантий, что один поток увидит изменения, сделанные другим потоком.
#Java #для_новичков #beginner #List #ArrayList #LinkedList #remove #contains
Операция remove(Object o) в ArrayList сочетает в себе линейный поиск (аналогично contains) и структурную реорганизацию (аналогично удалению по индексу).
Детальный процесс выполнения remove(element)
Фаза поиска:
Выполняется последовательный обход массива от начала до конца для поиска первого вхождения элемента. Поиск использует семантику equals() и специально обрабатывает null значения.
Фаза удаления:
Если элемент найден на позиции i:
Сохраняется значение для возврата
Выполняется сдвиг элементов от i+1 до size-1 на одну позицию влево
Последняя позиция обнуляется
Размер уменьшается, modCount увеличивается
Особенности:
Удаляется только первое вхождение элемента
Если элемент не найден, возвращается false
Если элемент найден и удален, возвращается true
Временная сложность
Операция имеет сложность O(n) в худшем случае:
Поиск: O(n) в худшем случае
Удаление: O(n) в худшем случае (при удалении из начала)
Удаление по значению в LinkedList
Процесс аналогичен удалению по индексу, но с предварительным поиском по значению.
Детальный процесс
Фаза поиска:
Последовательный обход узлов от головы до хвоста с использованием equals() для сравнения.
Фаза удаления:
При нахождении узла с совпадающим значением выполняется операция "вырезания" аналогично удалению по индексу.
Особенности:
Удаляется только первый найденный узел с совпадающим значением
Возвращает true при успешном удалении, false при отсутствии элемента
Сравнительный анализ операций удаления
Временная сложность
Удаление по индексу:
ArrayList: O(n) в худшем случае (удаление из начала)
LinkedList: O(n) в худшем случае (удаление из середины)
Удаление по значению:
ArrayList: O(n) поиск + O(n) удаление = O(n)
LinkedList: O(n) поиск + O(1) удаление = O(n)
Потребление памяти
ArrayList:
Не требует дополнительной памяти (кроме временных переменных)
Может оставлять "пустоты" в массиве (обнуленные ссылки)
LinkedList:
Освобождает память узла (24-32 байта на узел)
Требует времени garbage collector для очистки
Многопоточные аспекты
Проблемы конкурентного доступа
Несинхронизированные реализации:
Обе операции не являются thread-safe и могут привести к:
Состояниям гонки при одновременных модификациях
Повреждению внутренних структур данных
Непредсказуемому поведению итераторов
Стратегии синхронизации
Явная синхронизация:
synchronized(list) {
if (list.contains(element)) {
list.remove(element);
}
}Thread-safe альтернативы:
CopyOnWriteArrayList для сценариев "частое чтение, редкая запись"
Collections.synchronizedList() с external locking
Concurrent коллекции для специализированных use cases
Memory consistency
Операции remove и contains требуют proper memory barriers для обеспечения visibility изменений между потоками. Без синхронизации нет гарантий, что один поток увидит изменения, сделанные другим потоком.
#Java #для_новичков #beginner #List #ArrayList #LinkedList #remove #contains
👍2
Оптимизации и специализированные сценарии
Эффективные паттерны использования
Для ArrayList:
Удаление с конца более эффективно, чем с начала
Пакетное удаление может быть оптимизировано через создание нового списка
Использование removeAll(Collection) для массовых операций
Для LinkedList:
Удаление из начала/конца значительно эффективнее, чем из середины
Использование removeFirst()/removeLast() для операций на концах
Итераторные методы более эффективны для последовательного удаления
Избегание неэффективных паттернов
Антипаттерны:
Частые удаления из начала ArrayList
Использование contains перед remove без необходимости (двойной обход)
Игнорирование возможности использования Iterator.remove()
Оптимизированные подходы:
Влияние на итераторы
Fail-fast семантика
Обе операции изменяют modCount, что влияет на поведение итераторов:
Любое изменение списка invalidates все активные итераторы
Последующие операции итератора вызывают ConcurrentModificationException
Только Iterator.remove() может безопасно удалять элементы во время итерации
Безопасное удаление во время итерации
Правильный подход:
Неправильный подход:
Производительность в реальных сценариях
Сравнение операций contains:
ArrayList: 5-100 нс в зависимости от позиции элемента
LinkedList: 10-200 нс в зависимости от позиции и размера списка
Сравнение операций remove:
ArrayList (удаление из конца): 10-20 нс
ArrayList (удаление из начала): 100-5000 нс для 1000 элементов
LinkedList (удаление из конца): 10-30 нс
LinkedList (удаление из начала): 10-30 нс
LinkedList (удаление из середины): 50-5000 нс для 1000 элементов
Влияние размера коллекции
Малые коллекции (до 100 элементов):
Различия между ArrayList и LinkedList минимальны, часто доминируют другие факторы.
Средние коллекции (100-10,000 элементов):
ArrayList обычно превосходит LinkedList для большинства операций, кроме вставки/удаления в начале.
Большие коллекции (более 10,000 элементов):
ArrayList значительно превосходит LinkedList для операций доступа и поиска, но страдает при частых вставках/удалениях в начале.
Практические рекомендации
Критерии выбора реализации
Выбор ArrayList когда:
Преобладают операции случайного доступа и поиска
Редкие вставки/удаления в начале списка
Известен или может быть оценен конечный размер
Критически важна memory locality и кэширование
Выбор LinkedList когда:
Частые вставки/удаления в начале списка
Последовательный доступ является доминирующим паттерном
Размер списка сильно варьируется
Память не является основным ограничением
Оптимизация алгоритмов
Для частых операций contains/remove:
Рассмотреть использование HashSet для операций проверки существования
Использовать специализированные структуры данных для specific use cases
Кэшировать результаты частых проверок
Для пакетных операций:
Использовать removeAll() вместо цикла с индивидуальными удалениями
Рассмотреть создание новых коллекций вместо модификации существующих
Использовать stream API для декларативных операций
#Java #для_новичков #beginner #List #ArrayList #LinkedList #remove #contains
Эффективные паттерны использования
Для ArrayList:
Удаление с конца более эффективно, чем с начала
Пакетное удаление может быть оптимизировано через создание нового списка
Использование removeAll(Collection) для массовых операций
Для LinkedList:
Удаление из начала/конца значительно эффективнее, чем из середины
Использование removeFirst()/removeLast() для операций на концах
Итераторные методы более эффективны для последовательного удаления
Избегание неэффективных паттернов
Антипаттерны:
Частые удаления из начала ArrayList
Использование contains перед remove без необходимости (двойной обход)
Игнорирование возможности использования Iterator.remove()
Оптимизированные подходы:
// Вместо:
if (list.contains(element)) {
list.remove(element); // Двойной обход
}
// Использовать:
boolean removed = list.remove(element); // Один обход с ранним выходом
Влияние на итераторы
Fail-fast семантика
Обе операции изменяют modCount, что влияет на поведение итераторов:
Любое изменение списка invalidates все активные итераторы
Последующие операции итератора вызывают ConcurrentModificationException
Только Iterator.remove() может безопасно удалять элементы во время итерации
Безопасное удаление во время итерации
Правильный подход:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (shouldRemove(element)) {
iterator.remove(); // Безопасное удаление
}
}
Неправильный подход:
for (String element : list) {
if (shouldRemove(element)) {
list.remove(element); // ConcurrentModificationException
}
}Производительность в реальных сценариях
Сравнение операций contains:
ArrayList: 5-100 нс в зависимости от позиции элемента
LinkedList: 10-200 нс в зависимости от позиции и размера списка
Сравнение операций remove:
ArrayList (удаление из конца): 10-20 нс
ArrayList (удаление из начала): 100-5000 нс для 1000 элементов
LinkedList (удаление из конца): 10-30 нс
LinkedList (удаление из начала): 10-30 нс
LinkedList (удаление из середины): 50-5000 нс для 1000 элементов
Влияние размера коллекции
Малые коллекции (до 100 элементов):
Различия между ArrayList и LinkedList минимальны, часто доминируют другие факторы.
Средние коллекции (100-10,000 элементов):
ArrayList обычно превосходит LinkedList для большинства операций, кроме вставки/удаления в начале.
Большие коллекции (более 10,000 элементов):
ArrayList значительно превосходит LinkedList для операций доступа и поиска, но страдает при частых вставках/удалениях в начале.
Практические рекомендации
Критерии выбора реализации
Выбор ArrayList когда:
Преобладают операции случайного доступа и поиска
Редкие вставки/удаления в начале списка
Известен или может быть оценен конечный размер
Критически важна memory locality и кэширование
Выбор LinkedList когда:
Частые вставки/удаления в начале списка
Последовательный доступ является доминирующим паттерном
Размер списка сильно варьируется
Память не является основным ограничением
Оптимизация алгоритмов
Для частых операций contains/remove:
Рассмотреть использование HashSet для операций проверки существования
Использовать специализированные структуры данных для specific use cases
Кэшировать результаты частых проверок
Для пакетных операций:
Использовать removeAll() вместо цикла с индивидуальными удалениями
Рассмотреть создание новых коллекций вместо модификации существующих
Использовать stream API для декларативных операций
#Java #для_новичков #beginner #List #ArrayList #LinkedList #remove #contains
👍4
Что выведет код?
#Tasks
import java.util.ArrayList;
import java.util.List;
public class Task041225 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);
list.add(30);
list.add(1);
list.remove(1);
list.remove(Integer.valueOf(30));
list.remove(2);
System.out.println(list);
}
}
#Tasks
👍1
Варианты ответа:
Anonymous Quiz
0%
[10, 20, 1]
12%
[10, 30, 1]
29%
[10, 1]
59%
IndexOutOfBoundsException
Вопрос с собеседований
Что такое volatile в Java?🤓
Ответ:
volatile гарантирует видимость изменений переменной между потоками и запрещает определённые оптимизации.
Он не обеспечивает атомарности сложных операций, но полезен для флагов и простых статусов.
Работает быстрее, чем полная синхронизация, но ограниченно.
#собеседование
Что такое volatile в Java?
Ответ:
Он не обеспечивает атомарности сложных операций, но полезен для флагов и простых статусов.
Работает быстрее, чем полная синхронизация, но ограниченно.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
История IT-технологий сегодня — 05 декабря
ℹ️ Кто родился в этот день
Не нашел((
🌐 Знаковые события
1957 — в СССР спущен на воду первый в мире атомный ледокол «Ленин».
2014 — Exploration Flight Test 1 — первый тестовый непилотируемый запуск многоразового космического корабля «Орион».
#Biography #Birth_Date #Events #05Декабря
Не нашел((
1957 — в СССР спущен на воду первый в мире атомный ледокол «Ленин».
2014 — Exploration Flight Test 1 — первый тестовый непилотируемый запуск многоразового космического корабля «Орион».
#Biography #Birth_Date #Events #05Декабря
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Профессиональный квиз 🤓
Найдите потенциальную ошибку в коде на скрине.
Кейс интересный, поймал случайно☺️ 🤫
#Quiz
Найдите потенциальную ошибку в коде на скрине.
Кейс интересный, поймал случайно
#Quiz
Please open Telegram to view this post
VIEW IN TELEGRAM
Архитектура Spring Cloud Gateway: Внутренние компоненты и жизненный цикл запроса
В основе Spring Cloud Gateway лежат три ключевые абстракции, которые формируют декларативную модель конфигурации: Route, Predicate и Filter. Эти сущности организованы в строгую иерархию, где каждая выполняет свою роль в обработке входящего запроса.
Route (Маршрут)
Является центральной конфигурационной единицей. Он определяет полный путь обработки конкретного запроса от получения до возврата ответа. Маршрут инкапсулирует три основных элемента: уникальный идентификатор, целевой URI (куда будет перенаправлен запрос после обработки) и упорядоченные коллекции предикатов и фильтров. Внутренне маршрут представлен классом org.springframework.cloud.gateway.route.Route, который является иммутабельным объектом. Иммутабельность критически важна, поскольку маршруты могут динамически обновляться в runtime (например, через обновление конфигурации из Spring Cloud Config), и необходимо гарантировать согласованное состояние во время обработки запроса.
Конфигурация маршрута в YAML демонстрирует эту структуру:
Predicate (Предикат)
Условие, которое определяет, должен ли данный маршрут быть применён к входящему запросу. Предикаты реализуют функциональный интерфейс Predicate<ServerWebExchange>, где ServerWebExchange является контейнером для HTTP-запроса и ответа, а также дополнительных атрибутов, накопленных в процессе обработки. Предикаты оцениваются в определённом порядке, и первый маршрут, чьи предикаты возвращают true для данного ServerWebExchange, выбирается для дальнейшей обработки. Типичные предикаты включают проверку пути (Path=/api/**), метода HTTP (Method=GET,POST), наличия заголовков (Header=X-Request-Id, \\d+), параметров запроса, хоста, кук и даже сложные временные условия (After=2023-01-20T17:42:47.789-07:00[America/Denver]). Механизм предикатов позволяет реализовать сложную логику маршрутизации без написания imperative-кода.
Filter (Фильтр)
Компонент, который модифицирует ServerWebExchange до или после вызова целевого сервиса. Фильтры организованы в цепочку (Gateway Filter Chain) и выполняются в строгом порядке. Они реализуют интерфейс GatewayFilter, который содержит единственный метод Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain). Фильтры разделяются на две категории по моменту выполнения: "pre-filters" выполняются до передачи запроса целевому сервису (модификация запроса, аутентификация, логирование), а "post-filters" — после получения ответа от целевого сервиса (модификация ответа, добавление заголовков, метрик). Порядок выполнения фильтров внутри цепочки определяется их конфигурацией в маршруте.
Reactor Netty как HTTP-сервер: архитектурный фундамент
Spring Cloud Gateway построен на реактивном стеке Spring WebFlux, который в свою очередь использует Reactor Netty в качестве HTTP-сервера. Это фундаментальный архитектурный выбор, отличающий SCG от традиционных сервлетных контейнеров.
#Java #middle #Spring_Cloud_Gateway
В основе Spring Cloud Gateway лежат три ключевые абстракции, которые формируют декларативную модель конфигурации: Route, Predicate и Filter. Эти сущности организованы в строгую иерархию, где каждая выполняет свою роль в обработке входящего запроса.
Route (Маршрут)
Является центральной конфигурационной единицей. Он определяет полный путь обработки конкретного запроса от получения до возврата ответа. Маршрут инкапсулирует три основных элемента: уникальный идентификатор, целевой URI (куда будет перенаправлен запрос после обработки) и упорядоченные коллекции предикатов и фильтров. Внутренне маршрут представлен классом org.springframework.cloud.gateway.route.Route, который является иммутабельным объектом. Иммутабельность критически важна, поскольку маршруты могут динамически обновляться в runtime (например, через обновление конфигурации из Spring Cloud Config), и необходимо гарантировать согласованное состояние во время обработки запроса.
Конфигурация маршрута в YAML демонстрирует эту структуру:
spring:
cloud:
gateway:
routes:
- id: user_service_route
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- AddRequestHeader=X-Gateway-Tag, processed
Predicate (Предикат)
Условие, которое определяет, должен ли данный маршрут быть применён к входящему запросу. Предикаты реализуют функциональный интерфейс Predicate<ServerWebExchange>, где ServerWebExchange является контейнером для HTTP-запроса и ответа, а также дополнительных атрибутов, накопленных в процессе обработки. Предикаты оцениваются в определённом порядке, и первый маршрут, чьи предикаты возвращают true для данного ServerWebExchange, выбирается для дальнейшей обработки. Типичные предикаты включают проверку пути (Path=/api/**), метода HTTP (Method=GET,POST), наличия заголовков (Header=X-Request-Id, \\d+), параметров запроса, хоста, кук и даже сложные временные условия (After=2023-01-20T17:42:47.789-07:00[America/Denver]). Механизм предикатов позволяет реализовать сложную логику маршрутизации без написания imperative-кода.
Filter (Фильтр)
Компонент, который модифицирует ServerWebExchange до или после вызова целевого сервиса. Фильтры организованы в цепочку (Gateway Filter Chain) и выполняются в строгом порядке. Они реализуют интерфейс GatewayFilter, который содержит единственный метод Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain). Фильтры разделяются на две категории по моменту выполнения: "pre-filters" выполняются до передачи запроса целевому сервису (модификация запроса, аутентификация, логирование), а "post-filters" — после получения ответа от целевого сервиса (модификация ответа, добавление заголовков, метрик). Порядок выполнения фильтров внутри цепочки определяется их конфигурацией в маршруте.
Reactor Netty как HTTP-сервер: архитектурный фундамент
Spring Cloud Gateway построен на реактивном стеке Spring WebFlux, который в свою очередь использует Reactor Netty в качестве HTTP-сервера. Это фундаментальный архитектурный выбор, отличающий SCG от традиционных сервлетных контейнеров.
#Java #middle #Spring_Cloud_Gateway
👍2
Netty — это асинхронный event-driven фреймворк сетевого программирования, работающий на уровне NIO (Non-blocking I/O). В контексте JVM это означает, что вместо использования blocking socket operations и пула потоков (thread-per-connection модель), Netty использует небольшое количество потоков-селекторов (event loop threads), которые обрабатывают события на множестве соединений. Каждое соединение ассоциировано с каналом (Channel), а события (чтение данных, запись данных, изменение состояния соединения) диспетчеризуются через цепочки обработчиков (ChannelPipeline).
Когда Spring Boot приложение со SCG стартует, автоконфигурация ReactorNettyAutoConfiguration создаёт и настраивает экземпляр HttpServer Netty. Конфигурация по умолчанию устанавливает количество потоков event loop group равным количеству доступных процессорных ядер (Runtime.getRuntime().availableProcessors()), что является оптимальным для CPU-bound операций. Каждый поток event loop обслуживает множество соединений, переключаясь между ними по мере появления событий ввода-вывода.
В памяти JVM это приводит к совершенно иной структуре по сравнению с традиционными сервлетными контейнерами. Вместо большого пула потоков (200-500 объектов Thread в heap), каждый со своим стеком (1MB по умолчанию), создаётся небольшое количество долгоживущих потоков Netty. Основные структуры данных в heap — это буферы ByteBuf (которые Netty эффективно пуллит через ByteBufAllocator), объекты Channel и их контексты, а также реактивные потоки (Mono, Flux) и их операторы, представляющие pipeline обработки запроса.
Жизненный цикл запроса: от байтов в сокете до ответа
Фаза 1: Обработка в Netty и преобразование в ServerWebExchange
Когда клиент устанавливает TCP-соединение и отправляет HTTP-запрос, Netty event loop thread получает событие channelRead. Сырые байты из сокета декодируются в объект HttpRequest Netty. Затем через адаптер ReactorServerHttpRequest этот запрос оборачивается в реактивный тип ServerHttpRequest Spring WebFlux. Создаётся контейнер ServerWebExchange, который будет нести состояние запроса через весь pipeline обработки. Критически важно, что на этом этапе тело запроса ещё не читается полностью — оно представлено как реактивный поток Flux<DataBuffer>, что позволяет обрабатывать запросы потоково, без буферизации всего тела в памяти.
Фаза 2: Сопоставление маршрута через HandlerMapping
Обработка передаётся в DispatcherHandler Spring WebFlux, который ищет подходящий обработчик для запроса. В контексте SCG ключевым является RoutePredicateHandlerMapping — специализированная реализация HandlerMapping. Его задача — найти подходящий маршрут для текущего запроса.
Процесс сопоставления начинается с получения всех доступных маршрутов через RouteLocator. RouteLocator — это абстракция, которая предоставляет поток маршрутов. Реализация по умолчанию CachingRouteLocator кэширует маршруты для производительности, но поддерживает механизмы инвалидации при динамическом обновлении конфигурации. Каждый маршрут проверяется последовательно: для каждого предиката маршрута вызывается метод test(ServerWebExchange). Проверка предикатов выполняется синхронно (хотя сами предикаты могут выполнять асинхронные операции) до первого совпадения.
Сложность предиката Path демонстрирует детали реализации: при конфигурации Path=/api/users/** создаётся PathRoutePredicateFactory. Внутри он использует PathPatternParser из Spring WebFlux для компиляции строки шаблона в оптимизированную структуру данных PathPattern. При сопоставлении выполняется не простое строковое сравнение, а эффективный алгоритм сопоставления с извлечением переменных пути (например, /api/users/{id}). Это существенно быстрее, чем регулярные выражения, и не создает издержек при большом количестве маршрутов.
Когда маршрут найден, он сохраняется в атрибутах ServerWebExchange под ключом Route.class.getName(), и управление передаётся соответствующему обработчику — FilteringWebHandler.
#Java #middle #Spring_Cloud_Gateway
Когда Spring Boot приложение со SCG стартует, автоконфигурация ReactorNettyAutoConfiguration создаёт и настраивает экземпляр HttpServer Netty. Конфигурация по умолчанию устанавливает количество потоков event loop group равным количеству доступных процессорных ядер (Runtime.getRuntime().availableProcessors()), что является оптимальным для CPU-bound операций. Каждый поток event loop обслуживает множество соединений, переключаясь между ними по мере появления событий ввода-вывода.
В памяти JVM это приводит к совершенно иной структуре по сравнению с традиционными сервлетными контейнерами. Вместо большого пула потоков (200-500 объектов Thread в heap), каждый со своим стеком (1MB по умолчанию), создаётся небольшое количество долгоживущих потоков Netty. Основные структуры данных в heap — это буферы ByteBuf (которые Netty эффективно пуллит через ByteBufAllocator), объекты Channel и их контексты, а также реактивные потоки (Mono, Flux) и их операторы, представляющие pipeline обработки запроса.
Жизненный цикл запроса: от байтов в сокете до ответа
Фаза 1: Обработка в Netty и преобразование в ServerWebExchange
Когда клиент устанавливает TCP-соединение и отправляет HTTP-запрос, Netty event loop thread получает событие channelRead. Сырые байты из сокета декодируются в объект HttpRequest Netty. Затем через адаптер ReactorServerHttpRequest этот запрос оборачивается в реактивный тип ServerHttpRequest Spring WebFlux. Создаётся контейнер ServerWebExchange, который будет нести состояние запроса через весь pipeline обработки. Критически важно, что на этом этапе тело запроса ещё не читается полностью — оно представлено как реактивный поток Flux<DataBuffer>, что позволяет обрабатывать запросы потоково, без буферизации всего тела в памяти.
Фаза 2: Сопоставление маршрута через HandlerMapping
Обработка передаётся в DispatcherHandler Spring WebFlux, который ищет подходящий обработчик для запроса. В контексте SCG ключевым является RoutePredicateHandlerMapping — специализированная реализация HandlerMapping. Его задача — найти подходящий маршрут для текущего запроса.
Процесс сопоставления начинается с получения всех доступных маршрутов через RouteLocator. RouteLocator — это абстракция, которая предоставляет поток маршрутов. Реализация по умолчанию CachingRouteLocator кэширует маршруты для производительности, но поддерживает механизмы инвалидации при динамическом обновлении конфигурации. Каждый маршрут проверяется последовательно: для каждого предиката маршрута вызывается метод test(ServerWebExchange). Проверка предикатов выполняется синхронно (хотя сами предикаты могут выполнять асинхронные операции) до первого совпадения.
Сложность предиката Path демонстрирует детали реализации: при конфигурации Path=/api/users/** создаётся PathRoutePredicateFactory. Внутри он использует PathPatternParser из Spring WebFlux для компиляции строки шаблона в оптимизированную структуру данных PathPattern. При сопоставлении выполняется не простое строковое сравнение, а эффективный алгоритм сопоставления с извлечением переменных пути (например, /api/users/{id}). Это существенно быстрее, чем регулярные выражения, и не создает издержек при большом количестве маршрутов.
Когда маршрут найден, он сохраняется в атрибутах ServerWebExchange под ключом Route.class.getName(), и управление передаётся соответствующему обработчику — FilteringWebHandler.
#Java #middle #Spring_Cloud_Gateway
👍2
Фаза 3: Выполнение цепочки фильтров
FilteringWebHandler — это сердце логики преобразования запроса. Он получает выбранный маршрут и строит цепочку фильтров, упорядочивая их согласно конфигурации. Цепочка представляет собой реактивный pipeline, где каждый фильтр — это оператор, трансформирующий ServerWebExchange.
Порядок выполнения фильтров строго определён:
Сначала выполняются все GlobalFilter (глобальные фильтры), зарегистрированные в контексте приложения. Глобальные фильтры выполняются в порядке, определённом их значением getOrder().
Затем выполняются фильтры, специфичные для маршрута, в том порядке, в котором они объявлены в конфигурации маршрута.
Каждый фильтр получает контроль над ServerWebExchange и может либо модифицировать его, либо передать управление следующему фильтру в цепочке через вызов chain.filter(exchange), либо завершить обработку, вернув ответ непосредственно из шлюза. Последний сценарий используется, например, когда фильтр аутентификации обнаруживает невалидный токен и возвращает 401 Unauthorized.
Пример кастомного pre-фильтра для логирования:
#Java #middle #Spring_Cloud_Gateway
FilteringWebHandler — это сердце логики преобразования запроса. Он получает выбранный маршрут и строит цепочку фильтров, упорядочивая их согласно конфигурации. Цепочка представляет собой реактивный pipeline, где каждый фильтр — это оператор, трансформирующий ServerWebExchange.
Порядок выполнения фильтров строго определён:
Сначала выполняются все GlobalFilter (глобальные фильтры), зарегистрированные в контексте приложения. Глобальные фильтры выполняются в порядке, определённом их значением getOrder().
Затем выполняются фильтры, специфичные для маршрута, в том порядке, в котором они объявлены в конфигурации маршрута.
Каждый фильтр получает контроль над ServerWebExchange и может либо модифицировать его, либо передать управление следующему фильтру в цепочке через вызов chain.filter(exchange), либо завершить обработку, вернув ответ непосредственно из шлюза. Последний сценарий используется, например, когда фильтр аутентификации обнаруживает невалидный токен и возвращает 401 Unauthorized.
Пример кастомного pre-фильтра для логирования:
@Component
public class LoggingGatewayFilterFactory extends AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {
private static final Logger log = LoggerFactory.getLogger(LoggingGatewayFilterFactory.class);
public LoggingGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
long startTime = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();
log.info("Incoming request: {} {} from {}",
request.getMethod(),
request.getURI().getPath(),
request.getRemoteAddress());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
ServerHttpResponse response = exchange.getResponse();
log.info("Completed request: {} {} -> {} in {}ms",
request.getMethod(),
request.getURI().getPath(),
response.getStatusCode(),
duration);
}));
};
}
public static class Config {
// Конфигурационные свойства фильтра
}
}
Этот фильтр демонстрирует важный паттерн: логирование в pre-фильтре, а измерение времени и логирование результата — в post-части, реализованной через then(Mono.fromRunnable(...)). Обратите внимание, что весь фильтр — это функция, возвращающая Mono<Void>, и логирование выполняется в реактивном стиле без блокирования потоков.
#Java #middle #Spring_Cloud_Gateway
👍2
Фаза 4: Проксирование запроса к целевому сервису
После выполнения всех pre-фильтров управление доходит до ключевого фильтра — NettyRoutingFilter (или WebClientHttpRoutingFilter в альтернативной реализации). Именно этот фильтр выполняет фактическое проксирование запроса к целевому сервису.
Процесс проксирования включает несколько шагов:
Преобразование URI назначения: Если URI маршрута использует схему lb:// (например, lb://USER-SERVICE), вызывается LoadBalancerClientFilter (или реактивный ReactorLoadBalancer), который преобразует логическое имя сервиса в физический адрес, выбирая конкретный экземпляр с учётом алгоритма балансировки нагрузки.
Подготовка запроса прокси: NettyRoutingFilter создаёт новый HTTP-запрос Netty, копируя метод, заголовки и тело из оригинального запроса. При этом он может применять трансформации, определённые фильтрами (например, перезапись пути, добавление заголовков).
Асинхронное выполнение запроса: Запрос отправляется через реактивный HTTP-клиент Netty (HttpClient). Клиент Netty работает в неблокирующем режиме — он ставит запрос в очередь на отправку и немедленно возвращает Mono<HttpClientResponse> без блокировки потока. Event loop thread освобождается для обработки других соединений.
Обработка ответа: Когда от целевого сервиса приходит ответ, Netty генерирует событие, которое обрабатывается реактивным pipeline. Тело ответа также остаётся в реактивном представлении (Flux<DataBuffer>), что позволяет streamingly передавать большие ответы без буферизации в памяти.
Конфигурация для балансировки нагрузки с помощью Spring Cloud LoadBalancer:
Фаза 5: Пост-обработка ответа и завершение
После получения ответа от целевого сервиса выполняется оставшаяся часть цепочки фильтров — post-фильтры. Эти фильтры получают доступ как к оригинальному запросу, так и к ответу от целевого сервиса. Они могут модифицировать статус-код, заголовки, тело ответа.
Пример post-фильтра для добавления стандартных заголовков безопасности:
После выполнения всех post-фильтров финальный ответ записывается обратно в исходный канал Netty к клиенту. Netty берёт на себя эффективную отправку данных, включая chunked encoding для stream-ответов.
#Java #middle #Spring_Cloud_Gateway
После выполнения всех pre-фильтров управление доходит до ключевого фильтра — NettyRoutingFilter (или WebClientHttpRoutingFilter в альтернативной реализации). Именно этот фильтр выполняет фактическое проксирование запроса к целевому сервису.
Процесс проксирования включает несколько шагов:
Преобразование URI назначения: Если URI маршрута использует схему lb:// (например, lb://USER-SERVICE), вызывается LoadBalancerClientFilter (или реактивный ReactorLoadBalancer), который преобразует логическое имя сервиса в физический адрес, выбирая конкретный экземпляр с учётом алгоритма балансировки нагрузки.
Подготовка запроса прокси: NettyRoutingFilter создаёт новый HTTP-запрос Netty, копируя метод, заголовки и тело из оригинального запроса. При этом он может применять трансформации, определённые фильтрами (например, перезапись пути, добавление заголовков).
Асинхронное выполнение запроса: Запрос отправляется через реактивный HTTP-клиент Netty (HttpClient). Клиент Netty работает в неблокирующем режиме — он ставит запрос в очередь на отправку и немедленно возвращает Mono<HttpClientResponse> без блокировки потока. Event loop thread освобождается для обработки других соединений.
Обработка ответа: Когда от целевого сервиса приходит ответ, Netty генерирует событие, которое обрабатывается реактивным pipeline. Тело ответа также остаётся в реактивном представлении (Flux<DataBuffer>), что позволяет streamingly передавать большие ответы без буферизации в памяти.
Конфигурация для балансировки нагрузки с помощью Spring Cloud LoadBalancer:
spring:
cloud:
gateway:
routes:
- id: user_service_lb
uri: lb://user-service
predicates:
- Path=/users/**
loadbalancer:
configurations: default
# Конфигурация для кэширования resolved адресов
discovery:
locator:
enabled: true
lower-case-service-id: true
При использовании lb:// схема автоматически активирует интеграцию с Service Discovery (Eureka, Consul) через ReactiveLoadBalancer. Выбор экземпляра выполняется с учётом состояния здоровья и выбранного алгоритма (round-robin по умолчанию).
Фаза 5: Пост-обработка ответа и завершение
После получения ответа от целевого сервиса выполняется оставшаяся часть цепочки фильтров — post-фильтры. Эти фильтры получают доступ как к оригинальному запросу, так и к ответу от целевого сервиса. Они могут модифицировать статус-код, заголовки, тело ответа.
Пример post-фильтра для добавления стандартных заголовков безопасности:
@Component
public class SecurityHeadersGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
if (!response.getHeaders().containsKey("X-Content-Type-Options")) {
response.getHeaders().add("X-Content-Type-Options", "nosniff");
}
if (!response.getHeaders().containsKey("X-Frame-Options")) {
response.getHeaders().add("X-Frame-Options", "DENY");
}
if (!response.getHeaders().containsKey("Content-Security-Policy")) {
response.getHeaders().add("Content-Security-Policy",
"default-src 'self'; frame-ancestors 'none'");
}
}));
}
}
Важно отметить, что этот фильтр реализует интерфейс GlobalFilter и будет применён ко всем маршрутам автоматически. Глобальные фильтры выполняются до фильтров, специфичных для маршрута, если только их порядок (через аннотацию @Order или реализацию Ordered) не указывает иное.
После выполнения всех post-фильтров финальный ответ записывается обратно в исходный канал Netty к клиенту. Netty берёт на себя эффективную отправку данных, включая chunked encoding для stream-ответов.
#Java #middle #Spring_Cloud_Gateway
👍2
Типы фильтров и их специализация
Pre-filters / Post-filters — это логическая группировка, определяемая моментом выполнения относительно вызова целевого сервиса. Технически большинство фильтров могут работать и как pre, и как post, в зависимости от их реализации. Паттерн разделения на pre/post часто реализуется через вызов chain.filter(exchange).then(...) для post-логики.
Сетевые фильтры (Netty layer) работают на более низком уровне, ближе к транспорту. NettyRoutingFilter и NettyWriteResponseFilter являются примерами таких фильтров. Они манипулируют непосредственно объектами Netty (HttpClientRequest, HttpClientResponse) и работают с реактивными потоками байтов (ByteBuf). Эти фильтры критически важны для производительности, так как обеспечивают эффективную передачу данных без лишних копирований.
Global filters применяются ко всем маршрутам автоматически. Они регистрируются как Spring Beans и могут быть упорядочены. Типичные use cases: централизованное логирование, сбор метрик, добавление стандартных заголовков, кэширование. Глобальные фильтры выполняются до фильтров маршрута, если только их порядок не указывает иное.
Per-route filters конфигурируются для конкретных маршрутов и применяются только к ним. Они декларативно задаются в конфигурации маршрута (YAML, properties) или через Java DSL.
Пример сложного per-route фильтра с кастомной конфигурацией:
Управление памятью и производительностью в JVM
Архитектура SCG на Reactor Netty имеет глубокие последствия для управления памятью в JVM.
Вместо пулов потоков с фиксированным размером стека, основное потребление памяти связано с:
Буферами Netty (ByteBuf): Netty использует пул буферов через ByteBufAllocator. Это позволяет эффективно переиспользовать буферы для чтения/записи данных, минимизируя аллокации и сборку мусора. Буферы могут быть off-heap (direct buffers), что уменьшает нагрузку на GC, но требует явного управления памятью.
Реактивные потоки и лямбда-выражения: Каждый запрос создаёт цепочку реактивных операторов (Mono, Flux), которые представляются как объекты в heap. Лямбда-выражения в фильтрах также становятся объектами. Хотя эти объекты обычно недолгоживущие (short-lived) и эффективно обрабатываются молодым поколением сборщика мусора (young generation GC), при высокой нагрузке может создаваться значительное давление на GC.
Кэши маршрутов и предикатов: CachingRouteLocator и компилированные PathPattern кэшируются, что увеличивает постоянное потребление памяти (old generation), но значительно ускоряет обработку запросов.
Для оптимизации производительности важно:
Настроить размеры пулов буферов Netty согласно ожидаемому размеру запросов/ответов
Использовать профилирование для выявления memory leaks в кастомных фильтрах (невыполненные подписки на реактивные потоки)
Настроить сборщик мусора для низких пауз (G1GC или Shenandoah для больших heap-ов)
Мониторить количество аллокаций и pressure на young generation через JMX или Micrometer
#Java #middle #Spring_Cloud_Gateway
Pre-filters / Post-filters — это логическая группировка, определяемая моментом выполнения относительно вызова целевого сервиса. Технически большинство фильтров могут работать и как pre, и как post, в зависимости от их реализации. Паттерн разделения на pre/post часто реализуется через вызов chain.filter(exchange).then(...) для post-логики.
Сетевые фильтры (Netty layer) работают на более низком уровне, ближе к транспорту. NettyRoutingFilter и NettyWriteResponseFilter являются примерами таких фильтров. Они манипулируют непосредственно объектами Netty (HttpClientRequest, HttpClientResponse) и работают с реактивными потоками байтов (ByteBuf). Эти фильтры критически важны для производительности, так как обеспечивают эффективную передачу данных без лишних копирований.
Global filters применяются ко всем маршрутам автоматически. Они регистрируются как Spring Beans и могут быть упорядочены. Типичные use cases: централизованное логирование, сбор метрик, добавление стандартных заголовков, кэширование. Глобальные фильтры выполняются до фильтров маршрута, если только их порядок не указывает иное.
Per-route filters конфигурируются для конкретных маршрутов и применяются только к ним. Они декларативно задаются в конфигурации маршрута (YAML, properties) или через Java DSL.
Пример сложного per-route фильтра с кастомной конфигурацией:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("custom_rewrite", r -> r
.path("/legacy/api/**")
.filters(f -> f
.rewritePath("/legacy/api/(?<segment>.*)", "/modern/${segment}")
.addRequestParameter("source", "gateway")
.circuitBreaker(config -> config
.setName("myCircuitBreaker")
.setFallbackUri("forward:/fallback/default"))
)
.uri("lb://backend-service"))
.build();
}
В этой конфигурации DSL демонстрируется несколько фильтров в цепочке: rewritePath изменяет путь запроса с помощью регулярного выражения, addRequestParameter добавляет параметр, а circuitBreaker оборачивает вызов в контур устойчивости с fallback.
Управление памятью и производительностью в JVM
Архитектура SCG на Reactor Netty имеет глубокие последствия для управления памятью в JVM.
Вместо пулов потоков с фиксированным размером стека, основное потребление памяти связано с:
Буферами Netty (ByteBuf): Netty использует пул буферов через ByteBufAllocator. Это позволяет эффективно переиспользовать буферы для чтения/записи данных, минимизируя аллокации и сборку мусора. Буферы могут быть off-heap (direct buffers), что уменьшает нагрузку на GC, но требует явного управления памятью.
Реактивные потоки и лямбда-выражения: Каждый запрос создаёт цепочку реактивных операторов (Mono, Flux), которые представляются как объекты в heap. Лямбда-выражения в фильтрах также становятся объектами. Хотя эти объекты обычно недолгоживущие (short-lived) и эффективно обрабатываются молодым поколением сборщика мусора (young generation GC), при высокой нагрузке может создаваться значительное давление на GC.
Кэши маршрутов и предикатов: CachingRouteLocator и компилированные PathPattern кэшируются, что увеличивает постоянное потребление памяти (old generation), но значительно ускоряет обработку запросов.
Для оптимизации производительности важно:
Настроить размеры пулов буферов Netty согласно ожидаемому размеру запросов/ответов
Использовать профилирование для выявления memory leaks в кастомных фильтрах (невыполненные подписки на реактивные потоки)
Настроить сборщик мусора для низких пауз (G1GC или Shenandoah для больших heap-ов)
Мониторить количество аллокаций и pressure на young generation через JMX или Micrometer
#Java #middle #Spring_Cloud_Gateway
👍2
Что выведет код?
#Tasks
import java.util.Map;
import java.util.TreeMap;
public class Task051225 {
public static void main(String[] args) {
Map<String, Integer> map = new TreeMap<>();
map.put("x", 5);
map.put("y", null);
map.merge("x", 3, Integer::sum);
map.merge("y", 7, Integer::sum);
map.merge("z", 2, Integer::sum);
map.merge("x", 1, Integer::sum);
System.out.println(map.get("x"));
System.out.println(map.get("y"));
System.out.println(map.get("z"));
System.out.println(map.size());
}
}
#Tasks
👍2
👍2
Вопрос с собеседований
Что такое ABA-проблема?🤓
Ответ:
ABA возникает, когда значение меняется A→B→A, и алгоритм CAS считает, что ничего не изменилось.
Это приводит к ошибкам в lock-free структурах.
Решают её с помощью версионных меток или атомарных ссылок с пометкой, обеспечивая надежность обновлений.
#собеседование
Что такое ABA-проблема?
Ответ:
Это приводит к ошибкам в lock-free структурах.
Решают её с помощью версионных меток или атомарных ссылок с пометкой, обеспечивая надежность обновлений.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
История IT-технологий сегодня — 06 декабря
ℹ️ Кто родился в этот день
Джеффри Эверест Хинтон, CC FRS FRSC (родился 6 декабря 1947 года в Уимблдоне, Великобритания) — британско-канадский учёный, один из «крёстных отцов» современного глубокого обучения; фундаментальные работы по нейронным сетям заложили основу современных систем ИИ.
🌐 Знаковые события
1946 — выдан патент на первую в мире микроволновую печь.
#Biography #Birth_Date #Events #06Декабря
Джеффри Эверест Хинтон, CC FRS FRSC (родился 6 декабря 1947 года в Уимблдоне, Великобритания) — британско-канадский учёный, один из «крёстных отцов» современного глубокого обучения; фундаментальные работы по нейронным сетям заложили основу современных систем ИИ.
1946 — выдан патент на первую в мире микроволновую печь.
#Biography #Birth_Date #Events #06Декабря
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
С 29.11 по 05.12
Предыдущий пост(с 22.11 по 28.11)
Воскресный мотивационный пост:
Не было мотивации
Запись встреч/видео:
не было
Обучающие статьи:
Технический разбор: Spring WebFlux Gateway с JWT аутентификацией
Java:
Коллекции в Java
Глава 2. List — списки в Java
Метод set
Методы remove, contains
GraphQL
Advanced GraphQL: реактивность и Federation
Spring Cloud Gateway
Архитектурный страж микросервисов
Внутренние компоненты и жизненный цикл запроса
Полезные статьи и видео:
Kafka без боли: моя шпаргалка для собесов в Java
Вредные советы Java: просто используй Parallel Stream
Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
Предыдущий пост(с 22.11 по 28.11)
Воскресный мотивационный пост:
Не было мотивации
Запись встреч/видео:
не было
Обучающие статьи:
Технический разбор: Spring WebFlux Gateway с JWT аутентификацией
Java:
Коллекции в Java
Глава 2. List — списки в Java
Метод set
Методы remove, contains
GraphQL
Advanced GraphQL: реактивность и Federation
Spring Cloud Gateway
Архитектурный страж микросервисов
Внутренние компоненты и жизненный цикл запроса
Полезные статьи и видео:
Kafka без боли: моя шпаргалка для собесов в Java
Вредные советы Java: просто используй Parallel Stream
Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
👍4
История IT-технологий сегодня — 07 декабря
ℹ️ Кто родился в этот день
Франческа Росси (родилась 7 декабря 1962 года) — итальянский учёный в области искусственного интеллекта, IBM Fellow, эксперт по этике ИИ и многопараметрическим методам оптимизации для интеллектуальных агентов.
Авраам Ноам Хомский (родился 7 декабря 1928 года) — лингвист и философ, чьи формальные работы по грамматикам (иерархия Хомского) фундаментально повлияли на теорию формальных языков и компиляторы — важная теоретическая база для информатики.
🌐 Знаковые события
1972 — запуск космического корабля «Аполлон-17», совершившего последнюю в программе «Аполлон» посадку на Луну.
#Biography #Birth_Date #Events #07Декабря
Франческа Росси (родилась 7 декабря 1962 года) — итальянский учёный в области искусственного интеллекта, IBM Fellow, эксперт по этике ИИ и многопараметрическим методам оптимизации для интеллектуальных агентов.
Авраам Ноам Хомский (родился 7 декабря 1928 года) — лингвист и философ, чьи формальные работы по грамматикам (иерархия Хомского) фундаментально повлияли на теорию формальных языков и компиляторы — важная теоретическая база для информатики.
1972 — запуск космического корабля «Аполлон-17», совершившего последнюю в программе «Аполлон» посадку на Луну.
#Biography #Birth_Date #Events #07Декабря
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Привет! 👋
Сегодня я обращаюсь к тебе - подписчик этого канала.
Мы, возможно, никогда не встречались. Но раз ты здесь, раз ты видел мои посты или видео — значит, у нас точно есть что-то общее.
Скорее всего, Java, я угадал?😉
Ранее я неоднократно спрашивал мнение подписчиков о контенте и канале, но откликались единицы (и я им благодарен❤️ ).
Поэтому сегодня я вынужден спросить лично у тебя - зачем ты тут?
Не в глобальном смысле, а буквально - зачем ты остаёшься на этом канале?
- Ведь подобных каналов много тысяч и всегда есть из чего выбрать.
- Ведь судя по просмотрам, только 10% из всех подписчиков, вообще смотрят, что за очередную фигню я там написал.
- Ведь иногда кажется, что даже лайк поставить бывает сложнее, чем выучить Stream API.😄
Но я никого не выгоняю.
Те кому неинтересно или сложно, или слишком легко, или слишком смешно, уйдут сами.
И меня снедает любопытство, почему ты еще здесь?
Почему остаешься в виде безмолвного наблюдателя? Когда-то подписался на канал, поставил на беззвучку и забыл выйти?😄
Почему не хочешь высказать свое мнение? Или предложить свою тему?
(Хотя знаю, что и на это сообщение большинство ничего не ответит🤷♀️ )
Как я писал раньше - канал для меня уже давно не хобби.
Это способ прокачки, способ найти друзей, способ доказать что любой может стать достойным программистом и получить свой оффер.
Поэтому у меня лично к тебе просьба - помоги мне сделать канал лучше?
Скажи, почему ты здесь.
Что хочешь от Java.
Что хочешь от контента.
А я - если это в моих силах - попробую реализовать☺️
Для меня любой отклик, комментарий, предложение или вопрос - важны.
Заранее спасибо.🙂
#мои_мысли
Сегодня я обращаюсь к тебе - подписчик этого канала.
Мы, возможно, никогда не встречались. Но раз ты здесь, раз ты видел мои посты или видео — значит, у нас точно есть что-то общее.
Скорее всего, Java, я угадал?
Ранее я неоднократно спрашивал мнение подписчиков о контенте и канале, но откликались единицы (и я им благодарен
Поэтому сегодня я вынужден спросить лично у тебя - зачем ты тут?
Не в глобальном смысле, а буквально - зачем ты остаёшься на этом канале?
- Ведь подобных каналов много тысяч и всегда есть из чего выбрать.
- Ведь судя по просмотрам, только 10% из всех подписчиков, вообще смотрят, что за очередную фигню я там написал.
- Ведь иногда кажется, что даже лайк поставить бывает сложнее, чем выучить Stream API.
Но я никого не выгоняю.
Те кому неинтересно или сложно, или слишком легко, или слишком смешно, уйдут сами.
И меня снедает любопытство, почему ты еще здесь?
Почему остаешься в виде безмолвного наблюдателя? Когда-то подписался на канал, поставил на беззвучку и забыл выйти?
Почему не хочешь высказать свое мнение? Или предложить свою тему?
(Хотя знаю, что и на это сообщение большинство ничего не ответит
Как я писал раньше - канал для меня уже давно не хобби.
Это способ прокачки, способ найти друзей, способ доказать что любой может стать достойным программистом и получить свой оффер.
Поэтому у меня лично к тебе просьба - помоги мне сделать канал лучше?
Скажи, почему ты здесь.
Что хочешь от Java.
Что хочешь от контента.
А я - если это в моих силах - попробую реализовать
Для меня любой отклик, комментарий, предложение или вопрос - важны.
Заранее спасибо.
#мои_мысли
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥3🆒1 1