Как определяешь где нужно пофиксить баг
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как определяешь где нужно пофиксить баг
Отладка — это системный процесс, а не случайный поиск. Профессионально подходить к этому помогает метод научного тестирования (Scientific Method), известный как Divide-and-Conquer. Рассмотрю полный процесс.
Фаза 1: Воспроизведение проблемы
Шаг 1: Получи полную информацию о баге
// Вопросы, которые нужно задать:
/*
1. ЧТО именно произошло?
- Какое сообщение об ошибке?
- Какой результат получился?
- Какой результат ожидался?
2. КОГДА это произошло?
- При каких конкретных данных?
- В какой версии кода?
- Повторяется ли это всегда или случайно?
3. ГДЕ это произошло?
- На каком шаге процесса?
- В каком браузере/среде?
- В каких условиях (нагрузка, память, сеть)?
4. КТО это нашел?
- Какова вероятность воспроизведения?
- Есть ли пошаговые инструкции воспроизведения?
*/
Шаг 2: Создай минимальный тестовый случай (Minimal Reproducible Example)
@Test
public void reproduceNullPointerBug() {
// Минимальный код для воспроизведения
User user = new User(null, "john@example.com");
String result = user.getFullName(); // NPE здесь!
assertNotNull(result);
}
// Хорошие MRE:
// - Только необходимый код
// - Понятное ожидание vs. реальность
// - Легко запустить
// - Документированы входные данные
Фаза 2: Генерирование гипотез
Используй метод исключения (Process of Elimination)
public class BugHypotheses {
/*
Баг: OrderService.calculateTotal() возвращает неправильную сумму
Гипотеза 1: Проблема в методе calculateTotal() ✓ (начни отсюда)
Гипотеза 2: Проблема в getOrderItems()
Гипотеза 3: Проблема в getPrice() для товаров
Гипотеза 4: Проблема в базе данных (неправильные данные)
Гипотеза 5: Проблема в округлении (double precision)
Гипотеза 6: Проблема в валюте/конвертации
Приоритет: Начни с кода, работающего с самыми свежими данными
*/
}
Фаза 3: Логирование и отладка
Стратегия 1: Binary Search (Бинарный поиск)
public class OrderService {
public BigDecimal calculateTotal(Order order) {
// Добавь логирование на каждом шаге
List<OrderItem> items = order.getItems();
logger.debug("Items count: {}", items.size()); // Step 1
BigDecimal total = BigDecimal.ZERO;
for (OrderItem item : items) {
BigDecimal itemPrice = item.getPrice();
logger.debug("Item price: {}", itemPrice); // Step 2
int quantity = item.getQuantity();
logger.debug("Item quantity: {}", quantity); // Step 3
BigDecimal subtotal = itemPrice.multiply(
new BigDecimal(quantity)
);
logger.debug("Item subtotal: {}", subtotal); // Step 4
total = total.add(subtotal);
logger.debug("Running total: {}", total); // Step 5
}
// Применяем скидку
BigDecimal discount = order.getDiscount();
logger.debug("Discount: {}", discount); // Step 6
total = total.subtract(discount);
logger.debug("Final total: {}", total); // Step 7
return total;
}
}
Стратегия 2: Использование отладчика (IDE Debugger)
// В IntelliJ IDEA:
// 1. Установи Breakpoint на подозрительной строке
// 2. Запусти код в Debug режиме (Shift+F9)
// 3. Используй Step Over (F10) для пошагового выполнения
// 4. Используй Evaluate Expression (Alt+F9) для проверки значений
// 5. Смотри Variables panel для изменения переменных
public BigDecimal calculateDiscount(BigDecimal total) {
// Breakpoint здесь (Ctrl+Shift+B)
if (total.compareTo(BigDecimal.valueOf(100)) > 0) {
return total.multiply(BigDecimal.valueOf(0.1));
}
return BigDecimal.ZERO;
}
Стратегия 3: Conditional Breakpoints
// Breakpoint срабатывает только при условии
for (OrderItem item : items) {
// Правый клик на Breakpoint -> Filter:
// item.getPrice().compareTo(BigDecimal.ZERO) <= 0
// Останавливается только на нулевых/отрицательных ценах
total = total.add(item.getPrice());
}
Фаза 4: Анализ и локализация
Техника Stack Trace анализа
Exception in thread "main" java.lang.NullPointerException
at com.example.OrderService.calculateTotal(OrderService.java:45) ← Здесь!
at com.example.OrderController.getOrder(OrderController.java:12)
at java.base/java.lang.Thread.run(Thread.java:829)
Почини в обратном порядке стека:
1. OrderService.java:45 — основная ошибка
2. OrderController.java:12 — что передалось туда
3. Thread — где вызвалось
Анализ логов
# Посмотри логи приложения
grep "ERROR" app.log | head -20
# Найди тренд ошибок
grep "NullPointerException" app.log | wc -l
# Контекст ошибки
grep -B 5 -A 5 "NullPointerException" app.log
Фаза 5: Изоляция проблемы
Техника Rubber Duck Debugging
public class RubberDuckDebugging {
/*
Объясни код вслух (или напиши) линия за линией:
1. User user = getUserFromDatabase(id);
"Получаю пользователя из базы"
2. String email = user.getEmail();
"Получаю email из пользователя"
СТОП! Что если user == null?
А что если getEmail() возвращает null?
*/
}
Техника Bisecting (Git Bisect)
# Если баг появился недавно, используй бинарный поиск по коммитам
git bisect start
git bisect bad HEAD # Текущая версия — плохая
git bisect good v1.0 # Версия v1.0 — хорошая
# Git автоматически переключится на коммит в середине
# Запусти тест, скажи git bisect bad или git bisect good
# Повторяй, пока не найдёшь точный коммит с ошибкой
Фаза 6: Проверка гипотезы
Создай тест, который падает с текущей ошибкой
@Test
public void testCalculateTotalWithNullDiscount() {
Order order = new Order();
order.addItem(new OrderItem("Product", BigDecimal.TEN, 2));
order.setDiscount(null); // Баг здесь!
// Этот тест падает с NPE
BigDecimal total = orderService.calculateTotal(order);
assertEquals(new BigDecimal(20), total);
}
Запусти тест и подтверди, что он падает
$ mvn test -Dtest=OrderServiceTest#testCalculateTotalWithNullDiscount
FAILURE: testCalculateTotalWithNullDiscount
Expected: 20
Actual: NullPointerException ← Баг подтвержден!
Фаза 7: Исправление и валидация
Исправь минимально
// ❌ Не делай полный рефакторинг вместе с фиксом
public BigDecimal calculateTotal(Order order) {
BigDecimal total = BigDecimal.ZERO;
for (OrderItem item : order.getItems()) {
total = total.add(item.getPrice());
}
// FIX: Проверка null
if (order.getDiscount() != null) {
total = total.subtract(order.getDiscount());
}
return total;
}
Запусти тест снова — теперь он должен пройти
$ mvn test -Dtest=OrderServiceTest#testCalculateTotalWithNullDiscount
SUCCESS: testCalculateTotalWithNullDiscount ✓
Запусти все тесты — убедись, что ничего не сломал
$ mvn test
...
[INFO] Tests run: 156, Failures: 0, Errors: 0 ✓
Практический пример: Отладка медленного запроса
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> searchUsers(String pattern) {
long start = System.currentTimeMillis(); // Шаг 1: Замер
List<User> users = userRepository.findByNameContaining(pattern);
long dbTime = System.currentTimeMillis() - start;
logger.info("Database query took: {} ms", dbTime); // Где время теряется?
// Шаг 2: Проверим данные
logger.info("Found {} users", users.size());
// Шаг 3: Проверим обработку
start = System.currentTimeMillis();
List<User> processed = users.stream()
.filter(u -> u.isActive())
.map(this::enrichUserData) // Подозрительно!
.collect(Collectors.toList());
long processingTime = System.currentTimeMillis() - start;
logger.info("Processing took: {} ms", processingTime);
return processed;
}
private User enrichUserData(User user) {
// Шаг 4: Проверим эту функцию
// Может быть, здесь N+1 query?
user.setOrders(orderService.getUserOrders(user.getId()));
user.setNotifications(notificationService.getUnread(user.getId()));
return user;
}
}
// Вывод логов:
// Database query took: 50 ms
// Found 100 users
// Processing took: 5000 ms ← БАГО! 5 секунд на 100 пользователей = 50ms/пользователь
// Вероятная причина: N+1 query при enrichUserData
Checklist отладки
[ ] Воспроизведу проблему с минимальным кодом
[ ] Написал тест, который падает
[ ] Логирую каждый шаг выполнения
[ ] Используешь отладчик для пошагового выполнения
[ ] Проверяю граничные случаи (null, empty, negative)
[ ] Смотрю Stack Trace и логи с полным контекстом
[ ] Проверяю версии зависимостей и JVM
[ ] Изолировал проблему до одной функции/класса
[ ] Исправляю минимально (одна проблема = один фикс)
[ ] Все тесты проходят (включая новый тест для баги)
[ ] Проверяю граничные случаи исправления
[ ] Документирую причину баги в комментарии/коммите
Best Practices
- Логирование информативное, не шумное:
// ❌ Плохо
logger.info("Started");
logger.info("Processing");
logger.info("Done");
// ✅ Хорошо
logger.info("Fetching user {} from database", userId);
logger.debug("User email: {}", user.getEmail());
logger.error("Failed to update user {}: {}", userId, exception.getMessage());
-
Используй разные уровни логирования:
- ERROR — исключения и критические ошибки
- WARN — что-то неожиданное, но не критичное
- INFO — важные события (запуск, завершение)
- DEBUG — детали для отладки
-
Версионируй по зависимостям:
mvn dependency:tree | grep problematic-library
- Используй профилирование:
@Profile("debug")
@Component
public class PerformanceMonitor {
@Around("execution(* com.example..*(..))") // AOP
public Object monitorPerformance(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
Object result = pjp.proceed();
long duration = System.nanoTime() - start;
logger.debug("{} took {} ns", pjp.getSignature(), duration);
return result;
}
}
Итоги
- Воспроизводимость — первый шаг (MRE)
- Логирование — твой лучший друг
- Отладчик — мощнее, чем логирование
- Бинарный поиск — как в коде, так и в коммитах
- Тесты — валидация исправления
- Минимальный фикс — не переделывай код полностью
- Документируй — почему был баг и как ты его нашел