Java — язык с кучей инструментов для качества кода. Проблема в том, что их слишком много. JUnit, Mockito, SpotBugs, PMD, Checkstyle, SonarQube... Голова кругом. Давайте разберёмся, что реально стоит использовать и как это настроить за один вечер, а не за неделю.
Шаг 1: Юнит-тесты — база, которую все делают неправильно
Начнём с тестирования. По моему опыту, около 60% команд пишут тесты «для галочки» — есть тест, и хорошо. Но тест без проверок граничных случаев хуже, чем отсутствие теста. Он создаёт ложное чувство безопасности.
Базовая структура теста на JUnit 5:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class PaymentServiceTest {
private PaymentService service;
@BeforeEach
void setUp() {
service = new PaymentService();
}
@Test
void processPayment_withValidAmount_returnsSuccess() {
PaymentResult result = service.processPayment(100.0);
assertTrue(result.isSuccessful());
assertEquals(100.0, result.getAmount(), 0.01);
}
@ParameterizedTest
@ValueSource(doubles = {-100.0, 0.0, -0.01})
void processPayment_withInvalidAmount_throwsException(double amount) {
assertThrows(IllegalArgumentException.class,
() -> service.processPayment(amount));
}
}
Видите @ParameterizedTest? Это вещь. Вместо того чтобы копипастить пять одинаковых тестов с разными значениями, вы пишете один параметризованный. На одном проекте мы сократили количество тест-кодов в три раза, просто переписав их на параметризованные.
Теперь моки. Mockito — стандарт де-факто, но многие используют его неправильно:
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private PaymentGateway gateway;
@Mock
private NotificationService notifier;
@InjectMocks
private OrderService orderService;
@Test
void placeOrder_whenPaymentFails_notifiesCustomer() {
when(gateway.charge(any())).thenReturn(PaymentResult.failed());
orderService.placeOrder(new Order(100.0));
verify(notifier).sendFailureNotification(any());
verify(gateway, never()).refund(any());
}
}
Ошибка номер один — переизбыток моков. Если ваш тест требует мокать десять зависимостей, проблема не в тесте. Проблема в архитектуре класса. Его нужно разбивать.
Шаг 2: Статический анализ — ловим баги до запуска
Тесты проверяют поведение. Статический анализ проверяет структуру. Это разные уровни защиты.
SpotBugs (ранее FindBugs) находит реальные баги. Не «плохой стиль», а места, где код упадёт или поведёт себя неправильно:
<!-- pom.xml -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.3.1</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<failOnError>true</failOnError>
</configuration>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
Что типичного находит SpotBugs в Java-коде? Null pointer dereference — когда вы проверяете переменную на null после того, как уже использовали её. Ресурсы, которые не закрываются. Равенство объектов через == вместо equals(). Dead code — недостижимый код.
Checkstyle — другое дело. Он следит за стилем. И вот тут важно не переборщить. Я видел конфигурации Checkstyle на 200 правил. Команда тратит больше времени на борьбу с линтером, чем на написание кода.
Рекомендую начать с минимума:
<!-- checkstyle.xml -->
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="TreeWalker">
<module name="AvoidStarImport"/>
<module name="EmptyBlock"/>
<module name="EqualsHashCode"/>
<module name="MissingSwitchDefault"/>
<module name="NullAssignment"/>
</module>
</module>
Пять правил. Этого достаточно для старта. Добавите больше, когда команда привыкнет.
PMD — третий инструмент из «большой тройки». Он находит неочевидные проблемы: дублирование кода, неэффективные конструкции, потенциальные проблемы с производительностью. Но у PMD высокий уровень false positives. Приходится тюнить.
Шаг 3: Интеграция всего этого в CI/CD
По-хорошему, анализ должен запускаться автоматически при каждом коммите. Никаких «запущу позже вручную». Позже не будет.
GitHub Actions пример:
name: Java CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Run tests
run: mvn test
- name: SpotBugs check
run: mvn spotbugs:check
- name: Checkstyle check
run: mvn checkstyle:check
Для GitLab CI:
# .gitlab-ci.yml
stages:
- test
- analyze
test:
stage: test
image: maven:3.9-eclipse-temurin-17
script:
- mvn test
artifacts:
reports:
junit: target/surefire-reports/*.xml
spotbugs:
stage: analyze
image: maven:3.9-eclipse-temurin-17
script:
- mvn spotbugs:check
allow_failure: false
checkstyle:
stage: analyze
image: maven:3.9-eclipse-temurin-17
script:
- mvn checkstyle:check
allow_failure: true
Обратите внимание: Checkstyle я разрешил падать (allow_failure: true). На первых порах это спасёт от желания всё выключить. Плавно приучите команду к чистому коду.
Шаг 4: Оптимизация производительности — где прячутся тормоза
Java-код может выглядеть идеально, но работать медленно. Типичные проблемы?
Строки. Конкатенация в цикле:
// Плохо — создаёт кучу промежуточных строк
String result = "";
for (Item item : items) {
result += item.getName();
}
// Хорошо
StringBuilder sb = new StringBuilder();
for (Item item : items) {
sb.append(item.getName());
}
String result = sb.toString();
// Ещё лучше — Java 8+
String result = items.stream()
.map(Item::getName)
.collect(Collectors.joining());
Неправильное использование коллекций. ArrayList для частых вставок в начало. LinkedList для произвольного доступа. HashMap без указания capacity, когда размер известен заранее:
// Плохо — будет реаллокация и перехэширование
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
map.put(i, "value" + i);
}
// Хорошо — сразу выделяем нужный размер
Map<Integer, String> map = new HashMap<>(16384); // с запасом на load factor
Для выявления проблем с производительностью в Java есть отличные инструменты. JProfiler, YourKit, Java Mission Control. Но самый доступный — Java VisualVM, идёт в составе JDK.
Запускаете приложение, открываете VisualVM, подключаетесь к процессу. Смотрите на вкладку Sampler. Через 30 секунд увидите, где программа проводит время. Обычно это не то, что вы думали.
Шаг 5: Code review — последний рубеж
Автоматика ловит явные проблемы. Но архитектурные решения, неочевидные баги, соответствие бизнес-логике — это уже работа людей. Code review.
Проблема в том, что ревью часто делают на бегу. «Выглядет нормально, одобряю». Знакомо?
Хороший чеклист для ревью Java-кода:
- Есть ли тесты на новый код? Покрывают ли они граничные случаи?
- Правильно ли работают с ресурсами (try-with-resources)?
- Нет ли проблем с потокобезопасностью?
- Адекватно ли используются исключения?
- Не слишком ли сложный код? Можно ли его упростить?
На одном проекте мы внедрили правило: ревьюер должен найти хотя бы один комментарий. Даже мелкий. Это заставляло людей реально читать код, а не просто кликать «Approve». Количество пропущенных багов упало на 40%.
Честно? Ручное ревью — это дорого. Сеньор тратит 2-3 часа в день на ревью. Если добавить автоматическую проверку через AI-бота, который оставляет инлайн-комментарии с замечаниями, нагрузка снижается. Ревьюер фокусируется на важном, а не ищет пропущенные null-проверки.
В Distiq как раз сделали такого бота. Он интегрируется в GitLab, GitHub или GitVerse, анализирует каждый MR и пишет конкретные замечания прямо в код. Находит баги, уязвимости, проблемы с производительностью. Побробовали на паре проектов — экономит прилично времени на ревью, плюс закрывает типичные ошибки, которые люди пропускают.
