10986
📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate. По всем вопросам @evgenycarter РКН clck.ru/3KoGeP
☸️ Kubernetes: Оркестратор вашего зоопарка
Представьте, что у вас 50 микросервисов, каждый запущен в 3 экземплярах (для надежности). Итого 150 контейнеров.
Вдруг сервер №2 сгорает. 50 контейнеров умирают.
Без K8s: Админ просыпается в 3 ночи и руками запускает их на сервере №3.
С K8s: Вы спите. K8s замечает, что "кого-то не хватает", и сам перезапускает умершие контейнеры на живых серверах за секунды. Это называется Self-healing (Самовосстановление).
Чтобы понять K8s, нужно выучить всего три главных слова: Pod, Deployment, Service.
📦 1. Pod - Атом системы
В Docker мы запускаем Контейнеры. В Kubernetes мы запускаем Поды.
• Pod - это минимальная единица. Это обертка вокруг одного (или нескольких) контейнеров.
• У Пода есть свой IP-адрес.
• Обычно: 1 Pod = 1 Контейнер с вашим app.jar.
• Редко: 1 Pod = Java App + Sidecar (например, агент логирования). Они живут вместе, как сиамские близнецы, и делят сеть.
👮♂️ 2. Deployment (Развертывание) - Начальник
Вы никогда не создаете Поды вручную. Потому что Поды смертны. Если Под умер - он умер.
Вместо этого вы создаете Deployment. Это инструкция для K8s:
"Я хочу, чтобы у меня ВСЕГДА было 3 копии моего приложения OrderService".
http://order-service и пересылает его на один из живых Подов. Ему все равно, 3 их или 30.
# 1. Описываем Deployment (Что запускать?)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-java-app
spec:
replicas: 3 # Хочу 3 экземпляра!
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: java-app
image: my-docker-hub/app:v1 # Берем этот образ
ports:
- containerPort: 8080
---
# 2. Описываем Service (Как достучаться?)
apiVersion: v1
kind: Service
metadata:
name: my-backend-service
spec:
selector:
app: backend # Ищи Поды с меткой 'backend'
ports:
- protocol: TCP
port: 80 # Внешний порт
targetPort: 8080 # Порт контейнера
kubectl apply -f app.yaml, и магия случается.
🚀 CI/CD: Роботы делают рутину за вас
Аббревиатура состоит из двух частей, и они решают разные проблемы.
🛠 1. CI (Continuous Integration / Непрерывная интеграция)
Суть: Разработчики постоянно (несколько раз в день) сливают свой код в общую ветку (например, main или develop).
Каждый раз, когда вы делаете git push, специальный сервер (GitLab CI, GitHub Actions, Jenkins) автоматически:
1. Скачивает ваш свежий код.
2. Собирает проект (mvn clean compile).
3. Запускает все Unit и Integration тесты.
Если хоть один тест упал - сборка помечается красным крестиком (Build Failed). Код не пройдет дальше.
Итог: Ваша главная ветка в Git всегда находится в рабочем состоянии.
📦 2. CD (Continuous Delivery & Deployment)
Здесь две буквы "D", и они немного отличаются:
🔴Continuous Delivery (Доставка): Код автоматически собирается в готовый артефакт (например, Docker-образ) и кладется в хранилище. Нажать кнопку "Опубликовать на Production" должен человек (например, тимлид).
🔴Continuous Deployment (Развертывание): Полная автоматизация. Прошли тесты? Собрался образ? Он сразу же автоматически загружается на боевой сервер и заменяет старую версию. (Так делает Amazon, выкатывая обновления тысячи раз в день).
⚙️ Анатомия Пайплайна (Pipeline)
Пайплайн - это скрипт, состоящий из шагов (Stages), которые выполняются строго друг за другом. Упал предыдущий - следующий не запустится.
Типичный пайплайн для Spring Boot + Docker:
1. Lint (Проверка стиля кода, нет ли неиспользуемых импортов).
2. Test (Запуск JUnit тестов).
3. Build (Сборка `app.jar`).
4. Dockerize (Сборка Docker-образа и отправка его в Docker Registry).
5. Deploy (Команда серверу: "Скачай новый образ и перезапустись").
💻 Как это выглядит в коде? (GitHub Actions)
Вам не нужно кликать мышкой в интерфейсах. Пайплайн описывается кодом (YAML) и лежит прямо в вашем репозитории (подход Infrastructure as Code).
Вот пример простого .github/workflows/build.yml для Java-проекта:
name: Spring Boot CI/CD
on:
push:
branches: [ "main" ] # Запускать только при пуше в main
jobs:
build-and-test:
runs-on: ubuntu-latest # Выделяем виртуальную машину Linux
steps:
- uses: actions/checkout@v3 # 1. Скачиваем код из Git
- name: Установка Java 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Сборка и Тесты (Maven) # 2. Запускаем тесты и сборку
run: ./mvnw clean package
# Дальше могут быть шаги для сборки Docker и деплоя...
git push, и GitHub сам поднимет сервер, выполнит эти команды и пришлет вам письмо, если что-то сломалось.
❗️Микросервисы часто обещают масштабируемость, но на практике превращаются в сложную и хрупкую систему.
Причина почти всегда одна — неверная декомпозиция. Слишком мелкие сервисы, плотные связи, распределённые транзакции и постоянные проблемы с данными.
На открытом уроке:
- разберём, как правильно делить систему на сервисы - поговорим о границах микросервисов, ключевых шаблонах декомпозиции, подходах к взаимодействию и управлению согласованностью данных
- вы увидите, как принимать архитектурные решения не по шаблону, а с учётом бизнес-контекста, нагрузки и эволюции системы
- разберём типовые ошибки, из-за которых микросервисы теряют автономность и становятся сложнее монолита
📌 Встречаемся 18 февраля в преддверии старта курса «Highload Architect».
Зарегистрируйтесь, чтобы не пропустить: https://vk.cc/cUudMfРеклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
🧅 Архитектура: От Слоев к Луковице
❌ Проблема Слоенки (Database Driven Design)
В классическом Spring-приложении зависимости идут сверху вниз:
1. Контроллер зависит от Сервиса.
2. Сервис зависит от Репозитория (Базы данных).
В чем подвох?
Ваша бизнес-логика (Сервис) намертво привязана к деталям хранения данных (БД).
🔴Хотите поменять SQL на NoSQL? Переписывайте сервис.
🔴Хотите протестировать логику? Придется мокать базу данных.
🔴Главный грех: База данных диктует, как писать бизнес-логику. А должно быть наоборот!
✅ Решение: Clean / Hexagonal / Onion
Дядя Боб, Алистер Кокберн и другие умные дядьки придумали, как перевернуть игру.
Главная идея: Зависимости должны быть направлены ТОЛЬКО внутрь, к центру.
Представьте приложение как Луковицу.
1. Ядро (Core / Domain) - Центр Вселенной
Здесь живет ваша Бизнес-логика.
🔴Сущности (User, Order).
🔴Правила (User не может быть моложе 18 лет).
🔴Правило: Здесь НЕТ фреймворков. Никакого Spring, никакого Hibernate, никакого SQL. Только чистая Java.
🔴Этот слой ничего не знает о внешнем мире.
2. Порты (Ports / Use Cases) - Граница
Ядро определяет интерфейсы (Порты), которые ему нужны для работы.
🔴Например: interface UserRepository (найти пользователя, сохранить пользователя).
🔴Заметьте: интерфейс лежит внутри домена!
3. Адаптеры (Adapters / Infrastructure) - Внешний мир
Здесь живут детали реализации.
🔴PostgresUserRepository реализует интерфейс UserRepository.
🔴RestController вызывает методы Ядра.
🔴Здесь подключается Spring, Hibernate, Kafka и всё остальное.
🔄 Инверсия Зависимостей (DIP)
Следите за руками:
1. В слоеной архитектуре: Service зависит от PostgresDao.
2. В чистой архитектуре: Service зависит от Интерфейса. А PostgresDao зависит от Интерфейса.
Оба зависят от абстракции. БД стала просто плагином. Вы можете выкинуть Postgres и поставить заглушку (In-Memory Map) - и бизнес-логика даже не заметит подмены!
⚖️ Когда что использовать?
1. Layered (Controller-Service-Repo)
• ✅ Простые CRUD-приложения.
• ✅ Админки, прототипы.
• ✅ Когда логики почти нет, просто перекладываем данные.
2. Hexagonal (Ports & Adapters)
• ✅ Сложная бизнес-логика (Банкинг, Финтех, Логистика).
• ✅ Приложение живет долго (5+ лет).
• ✅ Нужно писать много Unit-тестов для ядра, не поднимая контекст Spring.
🔥 Итог
🔴Layered: Быстро писать, сложно поддерживать. БД - главная.
🔴Clean: Дольше писать (много маппингов DTO <-> Entity), легко поддерживать. Логика - главная.
#Architecture #CleanArchitecture #Hexagonal #Spring #Java
📲 Мы в MAX
👉@BookJava
🎥 Открытый урок «Class Data Sharing и его перспективы».
🗓 17 февраля в 20:00 МСК
🆓 Бесплатно. Урок в рамках старта курса «Java Developer. Advanced».
Быстрый, лёгкий старт Java-сервисов — конкурентное преимущество. Разберём, чем поможет Class Data Sharing и где он уместен.
🔴 Завтра тестовое собеседование с Java-разработчиком
11 февраля(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Java-разработчика.
Как это будет:
📂 Сергей Чамкин, старший разработчик из Uzum, ex-WildBerries, будет задавать реальные вопросы и задачи разработчику-добровольцу
📂 Cергей будет комментировать каждый ответ респондента, чтобы дать понять чего от вас ожидает собеседующий на интервью
📂 В конце можно будет задать любой вопрос Сергею
Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Java-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.
Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_sh_bot
Реклама.
О рекламодателе.
🏗 SOLID - Пять заповедей программиста
Почему один проект живет 10 лет и его легко дорабатывать, а другой через полгода превращается в "Legacy", к которому страшно подходить?
Разница в соблюдении принципов SOLID.
Это аббревиатура из 5 правил, сформулированных Робертом Мартином (Дядя Боб). Если вы нарушаете их - ваш код "гниет".
Давайте разберем каждую букву.
1️⃣ S - Single Responsibility Principle (Единственная ответственность)
"У класса должна быть только одна причина для изменения."
OrderService делает всё:OrderCalculator (считает).OrderRepository (сохраняет).EmailNotificationService (шлет письма).PdfGenerator (печатает).OrderService теперь просто дирижер, который вызывает эти компоненты."Программные сущности должны быть открыты для расширения, но закрыты для модификации."
if (deliveryType == "DHL") { ... }
else if (deliveryType == "Post") { ... }
// Пришла задача добавить FedEx? Придется лезть сюда и добавлять else if!
interface DeliveryStrategy { void deliver(); }
class DhlDelivery implements DeliveryStrategy { ... }
class PostDelivery implements DeliveryStrategy { ... }
// Нужен FedEx? Просто создаем НОВЫЙ класс, не трогая старые!
class FedExDelivery implements DeliveryStrategy { ... }
"Наследники должны без проблем заменять родителей."
Bird с методом fly(), а вы создали наследника Penguin (Пингвин), который при вызове fly() бросает ошибку (потому что пингвины не летают) - вы нарушили LSP."Много маленьких интерфейсов лучше, чем один огромный."
Worker имеет методы work() и eat().Robot. Роботы работают, но не едят.eat() и оставить его пустым или кинуть ошибку. Это мусор.Workable и Eatable.Workable."Зависьте от абстракций, а не от конкретики."
Service не должен зависеть от PostgresRepository. Он должен зависеть от интерфейса Repository.
👮♂️ Spring Security: Фейсконтроль для вашего API
Spring Security - это не просто библиотека, это мощный фреймворк, который встает стеной между интернетом и вашим контроллером.
Его работа строится на концепции Filter Chain (Цепочка фильтров). Каждый запрос проходит через серию проверок: "Есть ли токен?", "Валиден ли он?", "Есть ли права?".
🔑 Authentication vs Authorization
Два слова, которые путают все джуниоры.
1. Authentication (Аутентификация): "Кто ты?"
🔴Ввод логина/пароля.
🔴Проверка отпечатка пальца.
🔴Ответ: 401 Unauthorized (если не знаем, кто это).
2. Authorization (Авторизация): "А что тебе можно?"
🔴Ты Вася (мы тебя узнали), но ты хочешь удалить базу данных. Тебе нельзя.
🔴Ответ: 403 Forbidden (знаем кто ты, но не пустим).
🎫 JWT (JSON Web Token) - Паспорт туриста
В микросервисах мы не храним состояние пользователя на сервере (Stateless). Вместо этого, при логине мы выдаем пользователю Токен.
JWT - это строка из трех частей, разделенных точками: Header.Payload.Signature.
🔴Payload: Полезные данные (User ID, Role, Email).
🔴Signature: Цифровая подпись сервера. Гарантирует, что хитрый хакер не поменял в токене роль USER на ADMIN.
Как это работает:
1. Клиент шлет Логин/Пароль -> Сервер проверяет и отдает JWT.
2. Клиент сохраняет JWT (обычно в LocalStorage браузера).
3. При каждом запросе клиент прикрепляет JWT в заголовок:Authorization: Bearer <token>
4. Сервер видит токен, проверяет подпись и пускает (не ходя в базу данных!).
🛡 Настройка (Spring Boot 3.x)
Раньше мы наследовались от WebSecurityConfigurerAdapter. Забудьте, этот класс Deprecated.
Сейчас мы просто объявляем бин SecurityFilterChain.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // Для REST API отключаем
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Никаких сессий!
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // Логин доступен всем
.requestMatchers("/admin/**").hasRole("ADMIN") // Админка только админам
.anyRequest().authenticated() // Всё остальное - только с токеном
)
// Добавляем наш кастомный фильтр для проверки JWT
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
application.yaml, но под капотом там огромная машина стандартов.
🎥 Открытый урок «Eclipse Memory Analyzer (MAT): помощь в работе с heap».
🗓 04 февраля в 20:00 МСК
🆓 Бесплатно. Урок в рамках старта курса «Java Developer. Advanced».
Без MAT сложно найти причины OutOfMemoryError и утечек. Разберём базу и полезные приёмы анализа heap dump.
🧩 Микросервисы: Укрощение хаоса (Spring Cloud)
Когда у вас один сервис, всё просто. Но когда их становится 10, 20 или 50, возникают вопросы:
1. Как сервису А узнать IP-адрес сервиса Б? (А если он меняется динамически?)
2. Как не писать тонны кода для HTTP-запросов?
3. Как клиенту (фронтенду) обращаться ко всей этой куче сервисов?
Для этого есть три главных инструмента.
1️⃣ Eureka: Телефонная книга (Service Discovery)
В облаке сервисы постоянно перезапускаются, меняют IP-адреса и порты. Хардкодить http://localhost:8081 в коде - самоубийство.
Eureka Server - это реестр.
🔴Когда сервис (например, OrderService) запускается, он звонит в Eureka: "Привет, я OrderService, мой IP такой-то".
🔴Когда UserService хочет найти заказы, он спрашивает Eureka: "Где сейчас живет OrderService?".
В коде:
Вам нужно просто добавить аннотацию @EnableDiscoveryClient, и магия произойдет сама. Сервисы будут находить друг друга по имени, а не по IP.
2️⃣ OpenFeign: Магия общения
Окей, адрес мы нашли. Теперь нужно отправить запрос.
Раньше мы использовали RestTemplate - это было громоздко и некрасиво.
Feign позволяет вызывать удаленный REST-сервис так, будто это обычный метод интерфейса в вашем коде.
Было (RestTemplate):
String url = "http://order-service/orders/" + userId;
List<Order> orders = restTemplate.getForObject(url, List.class); // Фу, нетипизированно
FeignClient):
@FeignClient(name = "order-service") // Имя сервиса в Eureka
public interface OrderClient {
@GetMapping("/orders/{userId}")
List<Order> getUserOrders(@PathVariable Long userId);
}
// Использование в сервисе:
// List<Order> orders = orderClient.getUserOrders(123L);
api.com:8081, api.com:8082...). Это небезопасно и сложно (CORS error, привет 👋)./users/** -> лети в UserService, /orders/** -> лети в OrderService.application.yaml):
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE # lb = Load Balancer (через Eureka)
predicates:
- Path=/users/**
🧪 Тестирование в Spring Boot: Спите спокойно
Глобально тесты делятся на два лагеря:
1. Unit-тесты (Модульные): Быстрые, изолированные. Проверяем только логику одного класса (обычно Service). Никаких баз данных и поднятия Spring Context.
2. Integration-тесты (Интеграционные): Проверяем, как компоненты работают вместе (Controller + Service + DB). Здесь поднимается Spring Context.
⚡ 1. Unit-тесты: Изоляция и Скорость
Когда вы тестируете UserService, вам не нужно, чтобы он реально лез в базу данных. Вам нужно проверить: "Если репозиторий вернет null, выбросит ли сервис ошибку?"
Для этого мы используем Mockito - библиотеку, которая создает "фейковые" объекты (моки).
Ключевые аннотации:
🔴@ExtendWith(MockitoExtension.class) - включаем Mockito.
🔴@Mock - "Создай фейковый объект" (например, UserRepository).
🔴@InjectMocks - "Создай тестируемый объект (UserService) и вставь в него моки".
💻 Пример Unit-теста:
@ExtendWith(MockitoExtension.class) // 1. Включаем Mockito
class UserServiceTest {
@Mock
private UserRepository userRepository; // Фейк
@InjectMocks
private UserService userService; // Реальный сервис с фейком внутри
@Test
void shouldReturnUser_WhenExists() {
// GIVEN (Дано) - Настраиваем поведение мока
User mockUser = new User("Alex");
// "Если кто-то вызовет findById(1), верни mockUser"
Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// WHEN (Когда) - Вызываем метод сервиса
User result = userService.findById(1L);
// THEN (Тогда) - Проверяем результат
Assertions.assertEquals("Alex", result.getName());
// Проверяем, что метод репозитория действительно вызывался 1 раз
Mockito.verify(userRepository, times(1)).findById(1L);
}
}
@SpringBootTest - Поднимает весь контекст приложения (тяжело и медленно, но честно).@WebMvcTest - Поднимает только веб-слой (Контроллеры). Сервисы и БД не грузятся.@MockBean - Главная магия Spring Test. Это как @Mock, только этот мок кладется прямо в Spring Context, заменяя собой настоящий бин.@WebMvcTest):
@WebMvcTest(UserController.class) // Грузим только контроллер
class UserControllerTest {
@Autowired
private MockMvc mockMvc; // Инструмент для имитации HTTP-запросов
@MockBean // Заменяем настоящий сервис заглушкой в контексте Spring
private UserService userService;
@Test
void shouldReturnStatus200() throws Exception {
Mockito.when(userService.findById(1L)).thenReturn(new User("Alex"));
mockMvc.perform(get("/users/1")) // Делаем GET запрос
.andExpect(status().isOk()) // Ждем 200 OK
.andExpect(jsonPath("$.name").value("Alex")); // Проверяем JSON
}
}
@Mock vs @MockBean - Не путать!@Mock (из org.mockito): Используется в Unit-тестах. Быстро. Spring про него ничего не знает.@MockBean (из spring-boot-test): Используется в Integration-тестах. Spring находит этот бин в контексте и подменяет его на мок. Медленнее.Mockito.when(...).MockMvc.
🎛 Конфигурация Spring Boot: YAML, Профили и Секреты
Хардкодить настройки (порты, пароли, URL-ы) в Java-коде - это моветон. Если вам нужно поменять порт сервера, вы не должны перекомпилировать приложение.
Spring Boot следует принципу: "Код отдельно, настройки отдельно".
1️⃣ Properties vs YAML
По умолчанию Spring создает application.properties. Это старый формат (ключ=значение).
Весь мир переходит на YAML (.yaml или .yml). Он читабельнее и поддерживает иерархию.
Было (.properties):
spring.datasource.url=jdbc:postgresql://localhost/db
spring.datasource.username=admin
server.port=8080
.yaml):
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost/db
username: admin
@Value)
@Value("${server.port}")
private int port;
@ConfigurationProperties)
@ConfigurationProperties(prefix = "mail")
public record MailConfig(String host, int port, String username) {}
MailConfig в свои сервисы, как обычный бин.application.yaml:application-dev.yaml (для разработки)application-prod.yaml (для боевого сервера)
spring:
profiles:
active: dev # По умолчанию грузим dev-настройки
java -jar app.jar -Dspring.profiles.active=prodapplication-prod.yaml с паролем от боевой БД в репозиторий. ☠️spring.datasource.password=secretSPRING_DATASOURCE_PASSWORD=SuperSecurePass123@ConfigurationProperties.dev, test, prod).
💾 Spring Data JPA: SQL больше не нужен?
Spring Data JPA это абстракция над Hibernate (который, в свою очередь, является реализацией JPA).
Его главная киллер-фича: Генерация запросов из названий методов.
🏗 1. Сущность (@Entity)
Сначала мы объясняем Java, как выглядит наша таблица. Обычный класс превращается в таблицу с помощью пары аннотаций.
@Entity // Это таблица в БД
@Table(name = "users")
public class User {
@Id // Это Primary Key
@GeneratedValue(strategy = GenerationType.IDENTITY) // Авто-инкремент
private Long id;
private String email;
private int age;
private boolean active;
// Геттеры, сеттеры...
}
UserDao, мы просто создаем интерфейс.
public interface UserRepository extends JpaRepository<User, Long> {
// Здесь пусто! Но методы уже есть.
}
JpaRepository, вы сразу получаете готовые методы:.save(user) - сохранить/обновить..findById(id) - найти по ID (возвращает Optional)..findAll() - найти всех..deleteById(id) - удалить.
public interface UserRepository extends JpaRepository<User, Long> {
// SQL: SELECT * FROM users WHERE email = ?
Optional<User> findByEmail(String email);
// SQL: SELECT * FROM users WHERE active = true AND age > ?
List<User> findByActiveTrueAndAgeGreaterThan(int age);
// SQL: EXISTS (SELECT 1 FROM users WHERE email = ?)
boolean existsByEmail(String email);
}
find + By + ИмяПоля + Условие (если нужно).@Query)findByNameAndAgeAndActiveAnd...). Или нужен сложный JOIN.
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain%")
List<User> findUsersByEmailDomain(@Param("domain") String domain);
@Transactional)@Transactional над методом сервиса.
@Service
public class PaymentService {
@Transactional // Если упадет ошибка, все изменения откатятся
public void transferMoney() {
repo.withdraw(...);
repo.deposit(...);
}
}
@Entity.extends JpaRepository.findByField.@Query.
🎮 Анатомия REST Controller: Входящие и Исходящие
Раньше, чтобы вернуть JSON, нужно было танцевать с бубном. В Spring Boot это делается "из коробки" благодаря библиотеке Jackson, которая тихо работает в фоне.
1️⃣ @RestController vs @Controller
Это первый вопрос на собеседовании.
🔴@Controller: Олдскул. Используется, когда мы возвращаем HTML-страницы (Thymeleaf, JSP). Чтобы вернуть JSON, нужно над каждым методом вешать @ResponseBody.
🔴@RestController: Современный стандарт для REST API.
🔴Это просто @Controller + @ResponseBody над всеми методами.
🔴Всё, что возвращает метод, автоматически превращается в JSON.
2️⃣ Принимаем данные (3 главных способа)
Как вытащить информацию из запроса?
А. Из пути URL (@PathVariable)
Используем, когда параметр - это часть адреса ресурса.
🔴URL: GET /users/42
🔴Код:
@GetMapping("/users/{id}")
public User getById(@PathVariable Long id) { ... }
@RequestParam)GET /users?role=ADMIN&age=25
@GetMapping("/users")
public List<User> search(
@RequestParam String role,
@RequestParam(required = false) Integer age // Опционально
) { ... }
@RequestBody){ "name": "Alex", "email": "a@b.com" }
@PostMapping("/users")
public User create(@RequestBody UserDto userDto) { ... }
ResponseEntity)User, это хорошо (статус будет 200 OK). Но что, если мы хотим вернуть 404 (Not Found) или 201 (Created)?ResponseEntity<T>.
@RestController
@RequestMapping("/api/v1/users") // Общий префикс для всех методов
public class UserController {
private final UserService service; // Внедряем через конструктор
public UserController(UserService service) {
this.service = service;
}
// 1. Получить всех (GET 200 OK)
@GetMapping
public List<User> getAll() {
return service.findAll();
}
// 2. Найти одного (с управлением статусом)
@GetMapping("/{id}")
public ResponseEntity<User> getOne(@PathVariable Long id) {
return service.findById(id)
.map(user -> ResponseEntity.ok(user)) // 200 OK
.orElse(ResponseEntity.notFound().build()); // 404 Not Found
}
// 3. Создать (POST 201 Created)
@PostMapping
public ResponseEntity<User> create(@RequestBody UserDto dto) {
User created = service.save(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
}
@JsonIgnore - поле не попадет в JSON.@JsonProperty("full_name") - поле fullName в Java станет full_name в JSON.@RestController для API.@PathVariable - для ID (/users/1).@RequestParam - для фильтров (/users?sort=name).@RequestBody - для больших данных (JSON).ResponseEntity, чтобы контролировать HTTP-статусы.
⌨️ Открытый урок «Неожиданное введение в Spring Context».
🗓 22 января в 20:00 МСК
🆓 Бесплатно. Урок в рамках старта курса «Разработчик на Spring Framework».
На очередном открытом уроке курса "Разработчик на Spring Framework", мы на примере своего приложения попробуем реализовать свой IoC-контейнер, которой отдаленно будет напоминать Spring Context
Кому будет интересно:
Начинающим Java-бэкенд-разработчикам.
Результаты после вебинара:
Поймете общую идею IoC/Spring IoC.
🔗 Ссылка на регистрацию: https://vk.cc/cTwZTO
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
⚡️ Хотите освоить Backend-разработку с нуля, но не знаете, с чего начать? Java — это лучший старт!
Курс «Java-разработчик» от OTUS — это 15 месяцев обучения, которое откроет вам двери в мир Java-разработки. Программа включает живые лекции от опытных наставников и актуальные инструменты, такие как IntelliJ IDEA, Spring, Hibernate, Docker и Kubernetes. Мы разбираем всё: от синтаксиса Java до создания серверных приложений и работы с базами данных.
После курса вы сможете:
✔ создавать серверные приложения на Java;
✔ работать с реляционными и NoSQL базами данных;
✔ осваивать и применять фреймворки Spring, Hibernate, Docker, Kafka, RabbitMQ;
✔ претендовать на позицию Middle Java Developer в крупных компаниях.
Оставьте заявку и получите скидку на большое обучение «Java-разработчик»: https://vk.cc/cUAK9u
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
🧹 Гигиена кода: Рефакторинг и Технический долг
Почему проекты умирают? Не из-за плохой идеи, а из-за того, что стоимость добавления новой фичи становится выше, чем прибыль от неё.
Разработчики тратят 90% времени на чтение и распутывание старого кода, и только 10% - на написание нового.
💳 1. Technical Debt (Технический долг)
Это метафора, придуманная Уордом Каннингемом.
🔴Суть: Вы берете "кредит" у качества кода, чтобы выпустить фичу быстрее ("Сделаем костыль, потом поправим").
🔴Проценты: Каждая минута, потраченная на борьбу с этим костылем в будущем.
🔴Банкротство: Момент, когда код настолько запутан, что проще всё переписать с нуля, чем добавить кнопку.
Главное правило: Долги надо возвращать. Выделяйте 20% времени в спринте на рефакторинг.
2. Code Smells (Запахи кода)
Как понять, что код "тухнет"? По запаху.
Это не баги (код работает), это признаки плохого дизайна.
Топ-5 самых вонючих мест:
1. God Object (Божественный объект): Класс Manager или Utils, который знает всё и делает всё. Нарушает SRP (Single Responsibility Principle).
2. Long Method (Длинный метод): Если метод не помещается на один экран монитора - это запах.
3. Magic Numbers (Магические числа):
• Плохо: if (status == 7) ... (Что такое 7? Счастливое число?)
• Хорошо: if (status == STATUS_ACTIVE) ...
4. Duplicated Code (Дублирование): Copy-Paste это зло. Если вы нашли ошибку в одном месте, вам придется искать её копии по всему проекту. Принцип DRY (Don't Repeat Yourself).
5. Feature Envy (Зависть к чужим данным): Метод класса А постоянно обращается к полям класса Б. Скорее всего, этот метод должен жить в классе Б.
🛠 3. Refactoring (Рефакторинг)
Это процесс изменения структуры кода без изменения его поведения.
Золотое правило Бойскаута:
"Оставь место стоянки чище, чем оно было до твоего прихода."
public void process(Order o) {
// Magic Number + Long Method + Feature Envy
if (o.getItems().size() > 0 && o.getStatus() == 1) {
double total = 0;
for (Item i : o.getItems()) {
total += i.getPrice() * 1.2; // Что такое 1.2? НДС? Наценка?
}
System.out.println("Total: " + total); // Hardcoded output
}
}
private static final double TAX_RATE = 1.2;
private static final int STATUS_ACTIVE = 1;
public void printOrderTotal(Order order) {
if (isEligibleForProcessing(order)) {
double total = calculateTotal(order);
outputResult(total);
}
}
private boolean isEligibleForProcessing(Order order) {
return !order.isEmpty() && order.getStatus() == STATUS_ACTIVE;
}
private double calculateTotal(Order order) {
return order.getItems().stream()
.mapToDouble(item -> item.getPrice() * TAX_RATE)
.sum();
}
🧠 Domain Driven Design: Программируем на языке бизнеса
Главная проблема IT: Разработчики говорят на языке таблиц (INSERT, Foreign Key, DTO), а бизнес на языке денег и процессов («Провести проводку», «Списать остаток»).
DDD (Предметно-ориентированное проектирование) - это попытка убрать переводчика.
🗣 1. Ubiquitous Language (Единый язык)
Это фундамент. Код должен звучать так же, как речь эксперта.
🔴Плохо (CRUD-мышление):
• Бизнес: "Клиент сменил адрес доставки".
• Код: user.setAddress("New York"); userRepo.save(user);
🔴Хорошо (DDD):
• Код: user.relocateTo(new Address("New York"));
Методы должны называться глаголами бизнеса, а не сеттерами.
📦 2. Bounded Context (Ограниченный контекст)
Самая большая ошибка новичка - создать один класс Product на всё приложение.
🔴Для Продаж: Товар - это цена, описание, картинки.
🔴Для Склада: Товар - это вес, габариты, номер полки.
🔴Для Бухгалтерии: Товар - это актив, амортизация, инвентарный номер.
В DDD мы не делаем монстра. Мы создаем разные модели для разных контекстов.
🔴SalesContext.Product
🔴WarehouseContext.StockItem
Эти модели могут даже иметь разные ID и общаться друг с другом только через события (Kafka).
🏗 3. Tactical DDD (Строительные блоки)
Как писать код внутри контекста?
A. Entity (Сущность)
Объект, у которого есть Identity (ID).
Если у двух людей одинаковое имя, это все равно два разных человека (разные ID).
🔴Пример: User, Order.
🔴Они живут долго, меняют свое состояние, но остаются собой.
B. Value Object (Объект-значение)
Объект, который определяется только своими данными. У него нет ID. Он неизменяем (Immutable).
🔴Пример: Money, Address, Color.
🔴Если у меня есть 100 рублей и у вас 100 рублей - это одни и те же 100 рублей. Нам не важно, какая именно это купюра.
🔴Правило: Не используйте примитивы! Вместо String email используйте класс EmailAddress. Там можно спрятать валидацию.
C. Aggregate (Агрегат)
Это кластер объектов, которые живут и умирают вместе.
🔴Пример: Заказ (Order) + Позиции заказа (OrderItems).
🔴Aggregate Root (Корень): Это главный объект (Order).
🔴Правило: Извне можно обращаться только к Корню. Нельзя получить ссылку на OrderItem и изменить его цену напрямую. Вы должны сказать: order.changeItemPrice(...). Это гарантирует целостность данных.
🩸 Anemic vs Rich Model (Анемичная vs Богатая модель)
❌ Анемичная (Стандартный Spring):
Класс — это просто мешок с геттерами и сеттерами. Вся логика лежит в Service.
// Service
public void completeOrder(Long id) {
Order order = repo.findById(id);
if (order.getStatus() != PAID) throw ...
order.setStatus(COMPLETED); // Кто угодно может поменять статус!
repo.save(order);
}
// Entity
public void complete() {
if (this.status != PAID) throw new DomainException("Не оплачено!");
this.status = COMPLETED;
// Можно даже вернуть событие OrderCompletedEvent
}
// Service
public void completeOrder(Long id) {
Order order = repo.findById(id);
order.complete(); // Вся бизнес-логика внутри
repo.save(order);
}
🧠 Поведенческие паттерны: Стратегия, Наблюдатель, Цепочка
Эти паттерны помогают избежать спагетти-кода, где один класс управляет всем миром через гигантские if-else.
🗺 1. Strategy (Стратегия)
Суть: Позволяет менять алгоритм поведения объекта прямо "на лету", во время выполнения программы.
Это убийца бесконечных if (type == "CARD") { ... } else if (type == "PAYPAL") { ... }.
Аналогия: Навигатор. Вы строите маршрут из точки А в точку Б. Стратегия - это способ передвижения:
🔴Пешком (один алгоритм).
🔴На машине (другой алгоритм).
🔴На автобусе (третий алгоритм).
Цель одна, пути реализации разные.
Код:
// Общий интерфейс
interface RouteStrategy {
void buildRoute(String a, String b);
}
// Конкретные стратегии
class RoadStrategy implements RouteStrategy { ... }
class WalkingStrategy implements RouteStrategy { ... }
// Контекст (Навигатор)
class Navigator {
private RouteStrategy strategy;
public void setStrategy(RouteStrategy strategy) {
this.strategy = strategy; // Меняем на лету!
}
public void buildRoute(String a, String b) {
strategy.buildRoute(a, b);
}
}
class NewsAgency {
private List<Channel> channels = new ArrayList<>();
public void subscribe(Channel channel) {
channels.add(channel);
}
public void broadcast(String news) {
for (Channel channel : channels) {
channel.update(news); // Уведомляем всех!
}
}
}
CorsFilter (проверяет домен) -> JwtFilter (проверяет токен) -> UsernamePasswordFilter (проверяет логин).
abstract class SupportHandler {
protected SupportHandler next;
public void setNext(SupportHandler next) { this.next = next; }
public void handleRequest(String issue) {
if (canHandle(issue)) {
solve();
} else if (next != null) {
next.handleRequest(issue); // Передаем следующему
}
}
}
🏗 Структурные паттерны: Адаптер, Декоратор, Прокси
Когда вы пытаетесь соединить старую библиотеку с новым кодом или добавить логирование, не переписывая половину проекта - вы используете эти паттерны.
🔌 1. Adapter (Адаптер)
Суть: Делает несовместимые интерфейсы совместимыми.
Это как переходник для розетки. У вас вилка американская, а розетка европейская. Адаптер позволяет им работать вместе.
Где нужен: Когда есть старый класс (Legacy), который нельзя менять, но его нужно использовать в новом коде.
Пример: У нас есть система, которая понимает только KM/H (километры), а внешняя библиотека выдает скорость в MPH (мили).
// 1. Наш интерфейс (чего мы ждем)
interface Movable { double getSpeed(); } // км/ч
// 2. Чужой класс (что есть)
class Bugatti {
double getSpeedMph() { return 268; }
}
// 3. Адаптер (Переводчик)
class MovableAdapter implements Movable {
private Bugatti bugatti;
public MovableAdapter(Bugatti bugatti) {
this.bugatti = bugatti;
}
@Override
public double getSpeed() {
return convertMphToKmph(bugatti.getSpeedMph());
}
}
CoffeeWithMilkAndSugar, мы берем Coffee и заворачиваем его в Milk, а потом в Sugar.
// Базовый кофе
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getCost()); // 10$
// Добавили молоко (Обернули)
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getCost()); // 12$
// Добавили сахар (Обернули еще раз)
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getCost()); // 13$
java.io это используется повсюду: new BufferedReader(new FileReader(file)).
interface Image { void display(); }
class RealImage implements Image {
public RealImage(String file) { loadFromDisk(file); } // Долгая операция!
public void display() { System.out.println("Displaying..."); }
}
class ProxyImage implements Image {
private RealImage realImage;
private String file;
public ProxyImage(String file) { this.file = file; }
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(file); // Грузим только сейчас!
}
realImage.display();
}
}
@Transactional, Spring создает прокси вокруг вашего сервиса, открывает транзакцию, вызывает ваш метод, а потом закрывает транзакцию.
🏗 Порождающие паттерны: Как рождаются объекты?
Создать объект просто: User u = new User().
А если у объекта 20 полей? А если нам нужен только один экземпляр на всё приложение? А если мы не знаем заранее, какой именно класс нам нужен?
Тут на сцену выходят паттерны.
1️⃣ Singleton (Одиночка)
Суть: Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.
Где нужен: Логгеры, Конфигурация, Пул соединений с БД.
Как реализовать:
1. Скрываем конструктор (private).
2. Создаем статическое поле с экземпляром.
3. Возвращаем его через статический метод.
public class Database {
// Единственный экземпляр
private static Database instance;
private Database() {} // Никто не создаст объект извне
public static synchronized Database getInstance() {
if (instance == null) {
instance = new Database();
}
return instance;
}
}
age, а где height).new User("Alex", null, true, "admin", 25, null);
User user = User.builder()
.name("Alex")
.age(25)
.role("ADMIN")
.active(true)
.build();
@Builder над классом.
// 1. Интерфейс
interface Notification { void send(String msg); }
// 2. Реализации
class EmailNotification implements Notification { ... }
class SmsNotification implements Notification { ... }
// 3. Фабрика (Решает, что создать)
class NotificationFactory {
public static Notification create(String type) {
return switch (type) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
default -> throw new IllegalArgumentException("Unknown type");
};
}
}
// Клиентский код (не знает про классы Email/Sms, знает только интерфейс)
Notification notification = NotificationFactory.create("SMS");
notification.send("Hello!");
А вы справитесь с тестом по HighLoad?
Как пройти путь от разработчика до архитектора высоконагруженных систем для работы с крупными проектами?
Пройдите тест, проверьте свои знания для обучения на курсе «Highload Architect» от OTUS. А так же и получите скидку 🎁 до 15.02.2026 - подробности у менеджера.
➡️ Пройти Тест https://vk.cc/cU6fLb
На курсе вы освоите проектирование масштабируемых и отказоустойчивых систем, оптимизацию производительности, работу с современными инструментами для создания высоконагруженных решений и лучшие практики разработки серверных приложений.
❗️Практическое обучение проводится в прямом эфире — вебинары не являются предзаписанными.
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
🚀 Redis + Spring Cache: Турбо-наддув для бэкенда
Самая медленная часть любого приложения это ввод-вывод (I/O). Поход в базу данных (Postgres/MySQL) это "долго" (миллисекунды). Поход в оперативную память это "мгновенно" (наносекунды).
Redis - это база данных, которая хранит всё в оперативной памяти (In-Memory). Она идеально подходит на роль кэша.
🧠 Как работает Spring Cache?
Spring предоставляет крутую абстракцию. Вам не нужно писать код для подключения к Redis в каждом методе. Вы просто вешаете аннотации, а Spring сам перехватывает вызов метода.
Алгоритм @Cacheable:
1. Вызывается метод getUser(1).
2. Spring лезет в кэш (Redis) по ключу user::1.
3. Если данные есть (Cache Hit): Spring НЕ выполняет код метода, а сразу возвращает данные из кэша.
4. Если данных нет (Cache Miss): Spring выполняет метод (идет в БД), берет результат, кладет его в кэш и отдает вам.
🛠 Настройка (2 шага)
1. Зависимости
В pom.xml добавляем стартер для кэша и драйвер Redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
@EnableCaching.@Cacheable — "Запомни меня"get, find).
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// Имитация долгого запроса в БД (3 секунды)
simulateSlowService();
return userRepository.findById(id).orElseThrow();
}
}
@CacheEvict - "Забудь меня"
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
getUserById(id) Spring увидит, что кэш пуст, и снова сходит в БД за свежими данными.@CachePut - "Обнови меня"ConcurrentHashMap прямо в Java?"HashMap съест всю память JVM, и приложение упадет (OutOfMemory). Redis живет отдельно.HashMap. Кэш будет рассинхронизирован. Redis это общий кэш для всех инстансов.HashMap очищается. Redis (обычно) работает на отдельном сервере и хранит данные даже при рестарте вашего кода.Serializable, либо (что правильнее) нужно настроить Jackson Serializer, чтобы хранить данные в Redis в читаемом JSON-формате.@Cacheable.@CacheEvict.
📨 Apache Kafka: Нервная система микросервисов
Представьте, что вы заказали пиццу.
🔴REST подход: Вы стоите у прилавка и смотрите на повара, пока он не закончит. Вы не можете отойти. Если повар уснул - вы застряли.
🔴Kafka подход: Вы бросаете чек в коробку "Заказы" и уходите по своим делам. Повар возьмет заказ, когда освободится. Когда пицца будет готова, он положит её на стол выдачи, и вы получите уведомление.
Kafka - это распределенный лог событий. Это не просто очередь, это история всего, что произошло в системе.
🧩 Основные понятия
1. Topic (Топик) - Это "папка" или канал, куда падают сообщения. Например: orders-topic, email-topic.
2. Producer (Продюсер) - Тот, кто пишет сообщения в топик (например, сервис Заказов).
3. Consumer (Консьюмер) - Тот, кто читает сообщения (например, сервис Уведомлений или Склад).
4. Broker (Брокер) - Сервер Kafka, который хранит эти данные.
🚀 Главная фишка: Fire and Forget
Когда OrderService создает заказ, ему плевать, работает ли сейчас сервис отправки SMS или сервис начисления бонусов.
Он просто кидает событие OrderCreated в Kafka и мгновенно возвращает ответ пользователю "Заказ принят".
Сервисы-подписчики (Consumers) разгребут эти сообщения в своем темпе. Если сервис SMS упал, он поднимется через час, прочитает топик с того места, где остановился, и дошлет все смски. Данные не пропадут.
💻 Spring Kafka: Как писать код?
В Spring Boot работа с Kafka максимально упрощена.
1. Настройка (application.yaml)
spring:
kafka:
bootstrap-servers: localhost:9092 # Адрес брокера
consumer:
group-id: my-group # Важно для масштабирования
KafkaTemplate.
@Service
@RequiredArgsConstructor
public class OrderProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public void sendOrderEvent(String orderId) {
// Отправляем в топик "orders"
kafkaTemplate.send("orders", orderId);
System.out.println("Сообщение отправлено: " + orderId);
}
}
@KafkaListener.
@Service
public class NotificationConsumer {
@KafkaListener(topics = "orders", groupId = "notification_group")
public void listen(String orderId) {
System.out.println("Получено событие для заказа: " + orderId);
// Тут логика отправки email/sms
sendEmail(orderId);
}
}
📦 От Кода к Продакшену: JAR и Docker
В старые времена (Java EE) процесс деплоя был адом: нужно было установить на сервер Tomcat, настроить его, скомпилировать .war файл, закинуть его в папку... 🤯
Spring Boot принес концепцию Fat JAR (Жирный JAR).
🍔 1. Fat JAR - "Всё своё ношу с собой"
Spring Boot упаковывает ваше приложение, все библиотеки (зависимости) и даже сам веб-сервер (Tomcat) в один единственный файл .jar.
Этот файл работает как .exe в Windows. Ему ничего не нужно, кроме установленной Java.
Как собрать?
В терминале (в папке проекта):
# Для Maven
./mvnw clean package
# Для Gradle
./gradlew build
target (или build/libs) появится файл myapp-0.0.1-SNAPSHOT.jar.
java -jar myapp.0.0.1-SNAPSHOT.jar
DockerfileDockerfile в корне проекта.
# 1. Берем базовый образ с Java 17 (легковесный Alpine Linux)
FROM eclipse-temurin:17-jre-alpine
# 2. Копируем наш JAR внутрь образа
# (Предварительно нужно сделать mvn package руками!)
COPY target/*.jar app.jar
# 3. Говорим, какую команду запустить при старте контейнера
ENTRYPOINT ["java", "-jar", "/app.jar"]
# 1. Собираем образ (Image)
docker build -t my-spring-app .
# 2. Запускаем контейнер
# -p 8080:8080 пробрасывает порт наружу
docker run -p 8080:8080 my-spring-app
# --- Этап 1: Сборка (Build) ---
FROM maven:3.8.5-openjdk-17 AS builder
WORKDIR /app
COPY . .
# Собираем JAR, пропуская тесты (для скорости)
RUN mvn clean package -DskipTests
# --- Этап 2: Запуск (Run) ---
# Берем чистый, маленький образ для запуска
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Копируем ТОЛЬКО готовый jar из первого этапа
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
docker build, даже если на компьютере вообще не установлена Java!
🎥 Открытый урок «Apache Camel: масштабируемые интеграции для Highload».
🗓 28 января в 20:00 МСК
🆓 Бесплатно. Урок в рамках старта курса «Java Developer. Advanced».
Как сделать так, чтобы ваши интеграции на Camel не стали узким местом при росте нагрузки?
🚑 Global Exception Handling: Красиво падаем
Представьте: пользователь запрашивает ID, которого нет.
🔴Плохой сценарий: Сервер выплевывает стэктрейс на 500 строк, раскрывая внутренности БД. Статус 500 Internal Server Error. Клиент в шоке.
🔴Хороший сценарий: Клиент получает аккуратный JSON: {"status": 404, "message": "User not found"}.
Раньше, чтобы добиться хорошего сценария, приходилось писать try-catch в каждом методе контроллера. Это ужасно.
В Spring Boot есть элегантное решение - @ControllerAdvice.
🛡 Что это такое?
Это перехватчик (Interceptor), который работает по принципу Аспектно-Ориентированного Программирования (AOP). Он "сидит" над всеми вашими контроллерами и ловит исключения, которые вылетели из них, прежде чем они дойдут до пользователя.
🛠 Как настроить? (3 шага)
1. Создаем DTO для ошибки
Нам нужен красивый формат ответа, чтобы фронтенд всегда знал, чего ожидать.
public record ErrorResponse(int statusCode, String message, LocalDateTime timestamp) {}
@RestControllerAdvice. Это тот же @ControllerAdvice, но он автоматически добавляет @ResponseBody ко всем ответам (мы же пишем REST API).@ExceptionHandler.
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1. Ловим конкретную ошибку (например, сущность не найдена)
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(404, ex.getMessage(), LocalDateTime.now()));
}
// 2. Ловим ошибки валидации (если передали кривой JSON)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
String errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(400, errors, LocalDateTime.now()));
}
// 3. Ловим всё остальное (Fallback)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(500, "Произошла внутренняя ошибка", LocalDateTime.now()));
}
}
public User getUser(Long id) {
return repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User with id " + id + " not found"));
}
try-catch блоков. Только "счастливый путь" (happy path).try-catch в контроллерах.@RestControllerAdvice.EntityNotFoundException) на HTTP-статусы (404 Not Found).
🔴 Завтра тестовое собеседование с Java-разработчиком
21 января(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Java-разработчика.
Как это будет:
📂 Сергей Чамкин, старший разработчик из Uzum, ex-WildBerries, будет задавать реальные вопросы и задачи разработчику-добровольцу
📂 Cергей будет комментировать каждый ответ респондента, чтобы дать понять чего от вас ожидает собеседующий на интервью
📂 В конце можно будет задать любой вопрос Сергею
Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Java-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.
Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_sh_bot
Реклама.
О рекламодателе.
🧙♂️ Spring Boot: Магия под капотом (Starters & AutoConfig)
Вы когда-нибудь задумывались: почему вы просто добавляете одну строчку в pom.xml, пишете main метод, и у вас волшебным образом поднимается Tomcat, настраивается JSON-конвертер и подключается логирование?
За этим стоят два кита Spring Boot: Starters и AutoConfiguration.
1️⃣ Starters (Стартеры) - "Всё включено"
В старом Spring, чтобы сделать веб-приложение, нужно было вручную найти версии для Spring MVC, Tomcat, Jackson, Validation API... и молиться, чтобы они были совместимы.
Starter — это готовый набор зависимостей (dependencies), собранный в один пакет. Это как "Комбо-обед" в ресторане.
🔴Хотите Веб? Добавляете spring-boot-starter-web.
* Внутри: Tomcat + Spring MVC + Jackson + Logback.
🔴Хотите Тесты? Добавляете spring-boot-starter-test.
*Внутри: JUnit + Mockito + AssertJ + Hamcrest.
Вам больше не нужно думать о версиях библиотек. Spring Boot следит за "BOM" (Bill of Materials) и гарантирует, что все версии внутри стартера дружат друг с другом.
2️⃣ AutoConfiguration - "Умный детектив"
Это мозг фреймворка. Когда приложение запускается, Spring Boot начинает сканировать ваш classpath (все подключенные библиотеки jar).
Он рассуждает примерно так:
1. "Так, я вижу, что в зависимостях есть класс H2Driver?" "Значит, программист хочет базу данных. Создам-ка я ему бин DataSource с настройками для H2!"
2. "Я вижу классы Tomcat и Spring MVC?" "Значит, нужно поднять встроенный веб-сервер на порту 8080 и настроить DispatcherServlet."
3. "О, программист сам создал свой бин DataSource?" "Окей, тогда я отступаю и свою автоконфигурацию не применяю."
Вся эта логика держится на аннотациях @Conditional...:
🔴@ConditionalOnClass: Создать бин, если найден класс X.
🔴@ConditionalOnMissingBean: Создать бин, ТОЛЬКО если программист не создал такой же сам.
⚙️ Главная кнопка: @SpringBootApplication
Вы вешаете эту аннотацию над main классом. На самом деле это "матрешка", внутри которой спрятаны 3 другие аннотации:
@SpringBootConfiguration // Говорит: "Это конфигурационный класс"
@EnableAutoConfiguration // Говорит: "ВКЛЮЧИ МАГИЮ!" (запусти сканирование classpath)
@ComponentScan // Говорит: "Ищи пользовательские бины в этом пакете"
public @interface SpringBootApplication { ... }
application.properties одну строчку:
debug=true
🍃 Spring Boot: Магия или Логика? (IoC & Beans)
Когда вы запускаете Spring-приложение, происходит магия: все нужные объекты создаются сами, базы данных подключаются, сервер стартует.
Но за этой магией стоит четкий механизм - IoC Container (Inversion of Control / Инверсия управления).
📦 Что такое Application Context?
Представьте Spring как огромный завод.
🔴Context (Контейнер) - это сам завод. Он управляет жизненным циклом объектов.
🔴Bean (Бин) - это любая деталь (объект), которую этот завод создал и хранит у себя на складе.
Суть IoC:
🔴Обычный подход: Вы сами управляете объектами (Service s = new Service()). Вы - главный.
🔴Spring подход: Вы отдаете управление фреймворку. "Спринг, создай мне сервис и дай его, когда он понадобится". Spring - главный.
🏷 Как сделать Бин? (Аннотации)
Чтобы Spring узнал про ваши классы, их нужно пометить.
1. @Component — Самая базовая аннотация. "Эй, Спринг, это бин, управляй им!".
2. @Service - Тот же @Component, но семантически говорит: "Здесь бизнес-логика".
3. @Repository - Тот же @Component, но для работы с БД (ловит специфичные ошибки баз данных).
4. @Controller / @RestController - Для обработки HTTP-запросов.
5. @Configuration + @Bean - Используется, когда нужно создать бин из чужого класса (библиотеки), код которого вы не можете пометить аннотацией @Component.
💉 Dependency Injection (DI)
Главная фишка. Как один бин попадает внутрь другого?
Например, UserService нуждается в UserRepository.
❌ Способ 1: Через поле (Field Injection)
@Service
public class UserService {
@Autowired // ⚠️ Не рекомендуется!
private UserRepository repository;
}
NullPointerException.
@Service
public class UserService {
private final UserRepository repository;
// @Autowired здесь не обязателен (в новых версиях Spring)
public UserService(UserRepository repository) {
this.repository = repository;
}
}
final (неизменяемое), легко тестировать (можно передать любой репозиторий в конструктор), сразу видно все зависимости класса.
@Service
@RequiredArgsConstructor // Генерирует конструктор для final полей
public class UserService {
private final UserRepository repository; // Всё внедрится само!
}
new Service().@Service, @Repository).