Java Portal | Программирование
12.9K subscribers
1.01K photos
78 videos
34 files
839 links
Присоединяйтесь к нашему каналу и погрузитесь в мир для Java-разработчика

Связь: @devmangx

РКН: https://clck.ru/3H4WUg
Download Telegram
Паттерны проектирования систем - шпаргалка

🔸Webhooks и Outbox
Надёжные внешние уведомления.
Когда использовать — интеграции, сторонние колбэки.
Типичные проблемы — статусы оплат, синхронизация CRM.

🔸Blob/Object Storage
Дешёвое хранение больших файлов.
Когда использовать — медиа, бэкапы, экспорты.
Типичные проблемы — загрузки пользователей, data lake.

🔸Оркестратор задач (Airflow/Temporal)
Долгоживущие процессы с состоянием.
Когда использовать — длинные задачи, SLA.
Типичные проблемы — генерация отчётов, обработка видео.

🔸Blue-Green / Canary деплой
Постепенное переключение трафика.
Когда использовать — безопасные релизы, быстрый откат.
Типичные проблемы — деплой API, смена конфигураций.

🔸Feature Flags
Включение/отключение фич на лету.
Когда использовать — эксперименты, переключатели.
Типичные проблемы — A/B тесты, скрытые релизы.

🔸Стратегия миграции схемы
Обратная/прямая совместимость.
Когда использовать — миграция без простоя БД.
Типичные проблемы — expand-migrate-contract.

🔸Распределённые блокировки / выбор лидера
Координация одного активного воркера.
Когда использовать — уникальность cron, шардовое владение.
Типичные проблемы — один консумер, лидер партиции.

🔸Наблюдаемость (логи/метрики/трейсы)
Понимание, что делает система.
Когда использовать — SLO, отладка, планирование.
Типичные проблемы — задержка p99, error budget.

🔸Безопасность: AuthN/AuthZ
Проверка личности и прав доступа.
Когда использовать — мультиарендные продукты, внешние API.
Типичные проблемы — OAuth2/OIDC, RBAC/ABAC.

🔸Мультиарендность (pool/bridge/isolated)
Уровни изоляции данных и ресурсов.
Когда использовать — SaaS с множеством клиентов.
Типичные проблемы — отдельные БД на арендатора vs общая схема.

🔸Edge Compute/функции
Запуск логики ближе к пользователю.
Когда использовать — низкая задержка, лёгкие задачи.
Типичные проблемы — персонализация на краю, A/B тесты.

🔸Rate-Aware DB Patterns
Пакетная, очередная, троттлинг у БД.
Когда использовать — горячие партиции, блокировки.
Типичные проблемы — массовый импорт, ID hotspot.

🔸Стратегии пагинации
Keyset > Offset для больших данных.
Когда использовать — бесконечный скролл, большие таблицы.
Типичные проблемы — пагинация фидов, списки в админке.

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍53
This media is not supported in your browser
VIEW IN TELEGRAM
Кто до сих пор путается в деревьях, графах и сортировках, вот топчик:

https://visualgo.net/en

Визуалка чисто для мозга, всё анимировано: стек, очередь, DFS, BFS, сортировки, хэш-таблицы.

Как будто смотришь, как думает комп. Залипнуть можно. 😳

Сохрани

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
9
Освой планирование в Spring Boot с помощью Cron-задач и начальной задержки.

🔸Cron Job

Планировщик cron запускает задачи в определённое время, используя cron-выражение.

Это самый гибкий способ планировать задачи в Spring Boot — можно запускать их ежедневно, еженедельно, ежемесячно или по любому заданному шаблону.
Пример ниже выполняется каждый день в 9:00 утра по времени IST.

@Scheduled(cron = "0 0 9 * * ?", zone = "Asia/Kolkata")
public void runCron() {
System.out.println("Daily at 9:00 AM");
}


Формат cron: секунда минута час день месяц деньНедели

Примеры:

0 0 0 * * ? → каждый день в полночь

0 0/15 * * * ? → каждые 15 минут

Используйте cron, когда нужна точность, например для генерации отчётов в конце дня.

🔸Initial Delay

Параметр initialDelay говорит Spring Boot, сколько ждать после запуска приложения перед первым выполнением задачи.

После первого запуска задача будет работать с указанным fixedRate или fixedDelay.

Пример: код ниже ждёт 10 секунд после старта, затем выполняется каждые 5 секунд от начала предыдущего запуска.

@Scheduled(initialDelay = 10000, fixedRate = 5000)
public void runWithDelay() {
System.out.println("Starts after 10s, then every 5s");
}


Используйте initialDelay, если задача зависит от готовности других сервисов или данных — например, нужно загрузить конфигурацию из базы перед запуском фоновых задач.

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍82
This media is not supported in your browser
VIEW IN TELEGRAM
8 лучших практик при проектировании API

① Понятные названия → единообразные URL и коллекции
② Идемпотентность → безопасные повторы запросов с одинаковым результатом
③ Пагинация → ограничение количества результатов для снижения нагрузки
④ Сортировка и фильтры → возможность фильтровать результаты
⑤ Кросс-ссылки → не злоупотреблять query string
⑥ Ограничение частоты запросов → контроль количества запросов для стабильности
⑦ Версионирование → сохранение обратной совместимости
⑧ Безопасность → защита через API Keys, JWT, OAuth2

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
5
Java Logging (SLF4J, Logback) — всё, что нужно знать

1. Логирование → запись событий приложения для отладки и мониторинга
Помогает находить проблемы без запуска отладчика
Пример: запись неудачной попытки входа для целей безопасности

2. SLF4J → простой фасад для логирования в Java
Вы пишете код против API SLF4J, а дальше подключаете любую реализацию логгера
Пример: один и тот же код будет работать с Logback, Log4j или java.util.logging

3. Logback → популярный backend для логирования
Высокая производительность, гибкость, поддержка фильтров и политик ротации
Пример: запись логов в файл с ежедневной ротацией

4. Зачем использовать SLF4J + Logback
- SLF4J → возможность менять backend без переписывания кода
- Logback → функционал продакшен-уровня и высокая производительность

5. Уровни логирования (общие для большинства фреймворков)
- TRACE → подробная внутренняя информация (редко в продакшене)
- DEBUG → отладочная информация (например, значения переменных)
- INFO → общие события высокого уровня (например, «Пользователь успешно зарегистрирован»)
- WARN → неожиданные, но восстанавливаемые ситуации (например, повторный запрос)
- ERROR → критические ошибки, влияющие на функционал

6. Пример настройки

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyService {
private static final Logger logger = LoggerFactory.getLogger(MyService.class);

public void processOrder(String orderId) {
logger.info("Processing order {}", orderId);
try {
// бизнес-логика
} catch (Exception e) {
logger.error("Error processing order {}", orderId, e);
}
}
}


🔸Используйте параметризованное логирование
logger.debug("User {} logged in", userId) → избегайте конкатенации строк

🔸Выбирайте правильный уровень логов — не захламляйте прод debug-сообщениями

🔸Разделяйте логи приложения и фреймворков

🔸Применяйте политику ротации, чтобы не копились огромные файлы

🔸Добавляйте correlation ID для трейсинга запросов между сервисами

Не логируйте чувствительные данные (пароли, токены)

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍113
This media is not supported in your browser
VIEW IN TELEGRAM
Чувак сделал несколько Live Templates для intellijidea, которыми часто пользуется

👉 https://github.com/sivaprasadreddy/intellij-live-templates

С каждой новой версией IntelliJ IDEA часть этих штук появляется прямо из коробки

Например, шаблоны для логов и создания Spring-компонентов уже доступны OOTB

На примере небольшой демо, как можно быстро создавать логгер и Spring-компоненты прямо в intellijidea

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥62
Автоматический toString() в Record

record сравнительно новая фича в Java, которая позволяет описывать сущности без явного объявления полей и написания геттеров/сеттеров.

Пример:

record User(String name, int age, String city) { }


Если писать это же через класс, получится больше кода:

class User {
public String name;
public int age;
public String city;

public User(String name, int age, String city) {
this.name = name;
this.age = age;
this.city = city;
}
}


И в том и в другом случае мы получаем объект для хранения данных:

new User("Nick", 20, "New York");


Но есть отличие

У record метод toString() генерируется автоматически и выводит имена и значения всех полей в удобном формате.

В обычном классе дефолтный toString() возвращает имя класса и хэш объекта.

Пример:

record User(String name, int age, String city) { }

class SecondUser {
public String name;
public int age;
public String city;

public SecondUser(String name, int age, String city) {
this.name = name;
this.age = age;
this.city = city;
}
}

public static void main(String[] args) throws Exception {
User firstUser = new User("Nick", 20, "New York");
SecondUser secondUser = new SecondUser("Charley", 25, "New York");

System.out.println(firstUser);
System.out.println(secondUser);
}


Вывод:

User[name=Nick, age=20, city=New York]
org.example.SecondUser@68de145


То есть у record toString() сразу готов, а для обычного класса придётся переопределять вручную.

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍4
This media is not supported in your browser
VIEW IN TELEGRAM
Ресурс для разработчиков, чтобы создавать изометрические диаграммы своей инфраструктуры или софта.

Open-source: https://github.com/stan-smith/FossFLOW

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
8👍3
Java Off-Heap Memory с DirectByteBuffer

Большинство Java-разработчиков работают только с объектами в куче JVM, управляемой Garbage Collector.

Но в системах с высокими требованиями к производительности — базах данных, игровых движках или messaging-системах — Java может использовать off-heap memory (память вне кучи JVM) через DirectByteBuffer

Преимущества

- снижение пауз GC
- ускорение I/O операций
- возможность работать с большими наборами данных без раздувания кучи

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Репозиторий kdn251/interviews на GitHub — это реально крутой ресурс, собранный Кевином Нонтоном

interviews был создан, чтобы прокачать подготовку к техсобесам, собрав в одном месте алгоритмы, структуры данных, статьи и ссылки на полезные материалы

Если давно хотел подтянуться в алгоритмах и кодинге для интервью, начни с этого репозитория

Там найдёшь задачи, видео, объяснения, ссылки на LeetCode и много практики 😁

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
😁21
Советы по работе с Spring Boot Scheduler

Пул потоков

По умолчанию все задачи выполняются в одном потоке, и длинный job может блокировать остальные.

Чтобы запускать задачи параллельно, увеличь пул:

spring.task.scheduling.pool.size=4


Теперь одновременно могут работать до 4 задач.

Быстрая проверка

Во время разработки ставь короткие интервалы и добавляй логи, чтобы видеть, когда реально запускается job:

log.info("Job ran at {}", Instant.now());


Частые ошибки

- забыли @EnableScheduling → задачи не запустятся

- неверный timezone в cron

- долгие задачи блокируют другие (решается увеличением пула или оптимизацией кода)

Выбираем стратегию

fixedRate → с фиксированными интервалами

fixedDelay → ждёт после выполнения

cron → точные даты и время

Совет: выноси настройки во внешние properties и используй пул потоков для стабильной работы.

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍63
Первая статья из серии, где показывается как примеры AI-агентов из популярных Python-фреймворков можно переписать на Java и сделать гораздо лучше с помощью Embabel

Сегодняшняя цель CrewAI — читать

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
7🌭3🤣3😁1🤯1
Интеграция ChatGPT в Java

С ростом интереса к нейросетям всё больше приложений начинают использовать ИИ в своей бизнес-логике. Встраивание ChatGPT может быть реализовано по-разному

1. Пользователь вводит текст, приложение отправляет его в ChatGPT и возвращает ответ
2. Ввод отправляется в ChatGPT для дополнения или расширения и сохраняется в базу
3. ChatGPT используется как помощник для следующих действий пользователя

Механика всегда одна и та же: отправляется HTTP-запрос к API, на который приходит HTTP-ответ. Браузер делает это напрямую, Java-код делает то же самое с любым HTTP-клиентом — RestTemplate, WebClient, HttpClient, OkHttp и т д

Отличие в том, что запросы из приложения должны содержать ваш API-ключ. Его передают в заголовке Authorization

Как подготовить проект

1. Зарегистрироваться на platform.openai.com, сгенерировать API Key в разделе API Keys
2. Подключить зависимости. Например, OkHttp для запросов и org.json для разбора ответа

<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
</dependencies>


Пример простого клиента

import okhttp3.*;
import org.json.JSONObject;

public class ChatGPTClient {
private static final String API_KEY = "your-api-key-here";
private static final String API_URL = "https://api.openai.com/v1/chat/completions";

public static void main(String[] args) throws Exception {
OkHttpClient client = new OkHttpClient();

JSONObject message = new JSONObject()
.put("role", "user")
.put("content", "Привет! Объясни, как работает JVM");

JSONObject body = new JSONObject()
.put("model", "gpt-3.5-turbo")
.put("messages", new org.json.JSONArray().put(message));

Request request = new Request.Builder()
.url(API_URL)
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.post(RequestBody.create(body.toString(), MediaType.get("application/json")))
.build();

try (Response response = client.newCall(request).execute()) {
JSONObject json = new JSONObject(response.body().string());
String reply = json.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content");
System.out.println("ChatGPT ответ: " + reply);
}
}
}


Что тут происходит

> создаём HTTP-клиент
> формируем JSON с сообщением
> добавляем заголовки и ключ
> отправляем POST-запрос на API
> парсим JSON-ответ и выводим текст

Таким образом, интеграция ChatGPT в Java-приложение сводится к обычной работе с HTTP API: собрать запрос, добавить ключ и обработать результат.

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍73🤯2
7 шаблонов проектирования, которые должен знать каждый разработчик

Осознаёте вы это или нет, но большинство этих шаблонов вы уже используете.

Шаблоны проектирования — это переиспользуемые решения типовых задач в программировании. Они служат шаблонами, которые можно применять в разных языках и на разных платформах.

В 1994 году четверо разработчиков, известные как Gang of Four (Банда четырёх), написали книгу, где описали 23 шаблона проектирования. Эта книга стала классикой, и описанные в ней подходы актуальны до сих пор.

Шаблоны делятся на три группы:

🟢Порождающие — управляют созданием объектов с гибкостью.
🟣Структурные — помогают организовать объекты в более крупные структуры.
🟡Поведенческие — определяют, как объекты взаимодействуют друг с другом.

Вот 7 ключевых шаблонов, которые стоит знать:

Singleton — гарантирует, что будет только один экземпляр класса.

Builder — пошагово конструирует сложные объекты.

Factory — создаёт объекты без указания точного класса.

Facade — упрощает работу со сложной системой через единый интерфейс.

Adapter — позволяет работать вместе несовместимым интерфейсам.

Strategy — позволяет динамически подменять алгоритмы.

Observer — поддерживает зависимость между объектами (реакция на события).


Знание этих шаблонов помогает писать более поддерживаемый код и делает проекты понятнее для других разработчиков.

Но важно помнить: не злоупотребляйте ими. Подходящий шаблон в нужной ситуации — это сила. Но если применять их там, где они не нужны, это только усложнит систему.

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍53
Spring Boot с AOP:

Spring Boot позволяет использовать аспектно-ориентированное программирование , чтобы вынести сквозную логику (например, логирование, безопасность или кеширование) отдельно от бизнес-логики.
Можно даже создавать свои аннотации, которые автоматически навешивают нужное поведение, сохраняя код чистым и декларативным

1. Создаётся аннотация @LogExecutionTime

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}


Её можно навесить на любой метод, чтобы включить замер времени выполнения.

2. Аспект LoggingAspect

@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(LogExecutionTime)")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();

Object result = joinPoint.proceed(); // выполнить метод

long timeTaken = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " executed in " + timeTaken + " ms");

return result;
}
}


- Перехватывает вызов методов с аннотацией @LogExecutionTime
- Сохраняет время старта → выполняет метод → считает разницу → выводит результат в лог.

3. Контроллер TestController

@RestController
public class TestController {

@GetMapping("/hello")
@LogExecutionTime
public String hello() throws InterruptedException {
Thread.sleep(500); // имитация работы
return "Hello, World!";
}
}


- Метод hello() помечен @LogExecutionTime
- При каждом запросе на /hello метод будет логироваться с замером времени.

AOP здесь убирает дублирование логики измерения времени.
Вместо того чтобы вставлять System.currentTimeMillis() в каждый метод, всё вынесено в один аспект.

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12
ИТ-специалисты Петербурга, общий сбор 

6 и 7 сентября пройдет ИТ-фестиваль «Сезон кода» для опытных разработчиков, ML-инженеров, архитекторов, специалистов по информационной безопасности и других ИТ-специалистов.

Спикеры из Т-Банка и других компаний зовут слушать доклады, обмениваться опытом и знакомиться с единомышленниками. Развлечения и музыка тоже будут. 

В первый день:
— Разберетесь в архитектуре систем, надежности и работе с данными. 
— Узнаете, как технологии помогают решать задачи клиентов и бизнеса.
— Поймете, как идеи становятся инструментами и продуктами.

Во второй день: 
— Услышите про актуальные подходы к обеспечению информационной безопасности в разработке.
— Узнаете про backend-принципы, которые помогают работать эффективнее.
— Увидите, как работают LLM и куда все это движется.

Выбирайте один из дней или посетите оба. Встреча пройдет в новом ИТ-хабе Т-Технологий в Санкт-Петербурге.

Успейте зарегистрироваться до 5 сентября
JavaCV

Это Java-обёртка к OpenCV, FFmpeg и другим нативным библиотекам через JavaCPP Presets. Позволяет делать компьютерное зрение и обработку видео и аудио на JVM.

Умеет делать захват с камеры и из файлов, декодирование и кодирование, обработка изображений, интеграция с библиотеками вроде OpenCV, FFmpeg, Tesseract, librealsense и др. Работает на Windows, macOS, Linux и Android.

Пакет есть в Maven Central. Готовые бинари подтянутся автоматически.

Примеры кода есть в репозитории с демо

Домашняя страница проекта с деталями про JavaCPP и пресеты

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Boolean-функции в Java Stream API

При работе со Stream API часто нужно проверить, подходят ли элементы под какое-то условие. Для этого есть методы, которые возвращают true или false.

Основные такие методы:

1. anyMatch(Predicate)

Возвращает true, если в потоке есть хотя бы один элемент, который проходит проверку.

List<String> names = List.of("Alice", "Bob", "Anna");
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
// → true (подходит "Bob")


2. allMatch(Predicate)

Возвращает true, если все элементы удовлетворяют условию.

List<String> names = List.of("Alice", "Bob", "Anna");
boolean allStartWithA = names.stream()
.allMatch(name -> name.startsWith("A"));
// → false


3. noneMatch(Predicate)

Даёт true, если в потоке нет ни одного элемента, соответствующего условию.

List<String> names = List.of("Alice", "Bob", "Anna");
boolean noneEmpty = names.stream()
.noneMatch(String::isEmpty);
// → true


👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍142🔥2
Java Portal | Программирование
Первая статья из серии, где показывается как примеры AI-агентов из популярных Python-фреймворков можно переписать на Java и сделать гораздо лучше с помощью Embabel Сегодняшняя цель CrewAI — читать 👉 Java Portal
Второй пост в блоге показывает, как примеры AI-агентов из популярных Python-фреймворков можно переписать на Java и сделать лучше с помощью Embabel.

Сегодня в фокусе: Pydantic AI — читать

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍92
image_2025-08-22_08-09-28.png
587.6 KB
Наткнулся на такой пост от сеньора, про предварительные условия для System Design:

1. Понимание цели

Разберись, что именно ты строишь, кто будет пользоваться и для чего это нужно.

2. Профиль трафика (read-heavy vs write-heavy)

Если система в основном читает (много пользователей просматривают данные), делай упор на кэширование, индексацию, реплики для чтения. Пример: каталог Netflix, карточки товаров Amazon.

Если в основном пишут (много аплоадов, входящих данных), подойдут очереди, пакетная запись, eventual consistency. Пример: сообщения WhatsApp, данные IoT-датчиков.

3. Консистентность vs доступность

Строгая консистентность нужна там, где ошибка недопустима (банки, бронирование).

Eventual consistency подходит там, где можно жить с устаревшими данными (Instagram, аналитика).

4. Требования к задержке

Реалтайм — оптимизация под низкую задержку через кэш или предвычисления. Примеры: гейминг, Zoom, подтверждение платежей.

Асинхронно — очередь и фоновые воркеры. Примеры: e-mail рассылка, генерация отчётов.

5. Масштабируемость

Закладывай рост ×10. Пример: Google Docs начинался маленьким, но сейчас обрабатывает миллионы правок одновременно. Uber — тысячи поездок в минуту. Используй stateless-сервисы и горизонтальное масштабирование.

6. Паттерны доступа

Оптимизируй под то, какие данные и как часто читаются: поиск по гео/локации (geo-hash + ElasticSearch), быстрый доступ к часто используемым ID (Redis/Memcached).

7. Рост данных и партиционирование

Продумывай шардинг заранее (по пользователю, времени, гео). Пример: комментарии YouTube шардуются по ID видео.

Холодные данные сжимай или архивируй (Gmail, Google Drive).

8. Обработка отказов

Используй ретраи, фоллбэки, circuit breakers.

Если API падает по таймауту → повтор + настройка таймаута (пример: Amazon payment retry).

Если кэш недоступен → фоллбэк в БД (пример: Reddit загружает комментарии из БД).

9. Безопасность и авторизация

Авторизация: OAuth / JWT (пример: логин через Google или Spotify).

Защита от абузов и ботов: rate limiting, CAPTCHA (пример: Gmail signup, формы).

10. Нужно ли писать всё самому?

Некритичные фичи лучше вынести в SaaS или сторонние API:

Платежи — Stripe, PayPal.

Хранение медиа — Cloudinary, S3.

Уведомления — Firebase, SendGrid.


👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12😁1
Какие языки программирования самые «зелёные»?

Обычно при выборе языка мы смотрим на синтаксис, производительность или простоту обучения.

Но несколько лет назад группа португальских исследователей изучила энергопотребление 27 популярных языков программирования, измеряя время выполнения, энергозатраты и пиковое использование памяти.

Результаты показали компромиссы: быстрый код не всегда самый энергоэффективный, а работа с памятью сильно влияет на энергопотребление. Эти выводы могут помочь инженерам при выборе языка, когда важна энергоэффективность.

Выводы исследования:

Cи оказался самым энергоэффективным языком.

Python и Perl — наименее экологичные языки программирования.

Источник: читать

👉 Java Portal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5😁3👀3🏆2🤯1