← Назад к вопросам

Что инструментируется при инструментации в Spring

2.0 Middle🔥 111 комментариев
#Spring Boot и Spring Data#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Инструментация в Spring: что меняется и как это работает

Инструментация (Instrumentation) в Spring — это механизм внедрения кода в существующие классы во время выполнения (runtime). Spring использует несколько техник для этого, и это критически важно для работы AOP, транзакций и других функций.

Типы инструментации в Spring

1. Bytecode Instrumentation (динамическое изменение классов)

Spring может изменять bytecode классов на лету. Это делается через прокси-объекты и динамическое создание классов.

// Оригинальный класс:
@Service
public class UserService {
    public void createUser(String name) {
        System.out.println("Creating user: " + name);
    }
}

// Spring создаёт прокси (примерно это):
public class UserServiceProxy extends UserService {
    private final UserService target;
    private final TransactionManager transactionManager;
    
    public UserServiceProxy(UserService target, TransactionManager tm) {
        this.target = target;
        this.transactionManager = tm;
    }
    
    @Override
    public void createUser(String name) {
        transactionManager.begin();
        try {
            target.createUser(name); // Вызов оригинального
        } catch (Exception e) {
            transactionManager.rollback();
            throw e;
        }
        transactionManager.commit();
    }
}

// Клиент получает прокси, а не оригинальный класс:
UserService service = context.getBean(UserService.class);
// Это на самом деле UserServiceProxy!
service.createUser("Alice"); // Сначала открывается транзакция, потом выполняется

Что инструментируется

1. Транзакции (@Transactional)

Все методы с @Transactional оборачиваются в управление транзакциями:

@Service
public class OrderService {
    @Autowired
    private OrderRepository repository;
    
    @Transactional // ← Это получит instrumentation
    public Order createOrder(Order order) {
        return repository.save(order);
        // Spring автоматически:
        // 1. Открывает транзакцию
        // 2. Выполняет метод
        // 3. Коммитит или откатывает в зависимости от результата
    }
    
    @Transactional(readOnly = true) // ← Специальная инструментация
    public List<Order> getOrders() {
        return repository.findAll();
        // Оптимизация БД для только-чтения
    }
}

// Что происходит внутри:
public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
    TransactionStatus status = transactionManager.getTransaction(...);
    try {
        Object result = pjp.proceed(); // Вызов оригинального метода
        transactionManager.commit(status);
        return result;
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw e;
    }
}

2. AOP (Aspect Oriented Programming) - @Around, @Before, @After

Все методы, подходящие под pointcut, оборачиваются:

@Aspect
@Component
public class LoggingAspect {
    @Around("execution(public * com.example.service.*.*(...))")
    public Object logMethodExecution(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        
        System.out.println("Before: " + methodName);
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = pjp.proceed(); // Выполняем оригинальный метод
            return result;
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            System.out.println("After: " + methodName + " took " + duration + "ms");
        }
    }
}

// Это работает для ВСЕХ public методов в пакете service
public class UserService {
    public void createUser(String name) {
        // Автоматически оборачивается aspect!
    }
}

3. Caching (@Cacheable, @CacheEvict)

Методы с кешированием получают дополнительную логику:

@Service
public class ProductService {
    @Cacheable("products") // ← Инструментируется
    public Product getProduct(Long id) {
        System.out.println("Fetching product from DB...");
        return productRepository.findById(id);
    }
    
    @CacheEvict("products", allEntries = true) // ← Инструментируется
    public void updateProduct(Product product) {
        productRepository.save(product);
    }
}

// Что происходит:
// getProduct(1) → проверка кеша → не найдено → выполнить метод → сохранить в кеш
// getProduct(1) → проверка кеша → НАЙДЕНО → вернуть из кеша (не выполняем метод!)
// updateProduct(...) → выполнить метод → очистить весь кеш "products"

4. Security (@PreAuthorize, @PostAuthorize)

Методы с проверками безопасности оборачиваются в auth-логику:

@Service
public class AdminService {
    @PreAuthorize("hasRole(ADMIN)") // ← Инструментируется
    public void deleteUser(Long userId) {
        // Перед выполнением: проверка роли ADMIN
        // Если нет ADMIN → AccessDeniedException
    }
    
    @PostAuthorize("returnObject.owner == authentication.principal.username") // ← Инструментируется
    public Document getDocument(Long docId) {
        return documentRepository.findById(docId);
        // После выполнения: проверка что owner == текущий юзер
    }
}

5. Async (@Async)

Методы выполняются в отдельном потоке:

@Service
public class EmailService {
    @Async // ← Инструментируется
    public void sendEmail(String to, String subject, String body) {
        // Этот метод выполнится в отдельном thread pool
        // Вызывающий код продолжит сразу
    }
}

// Использование:
UserService userService; // autowired

void registerUser(User user) {
    userRepository.save(user);
    emailService.sendEmail(user.getEmail(), "Welcome!", "...");
    // Не ждём, пока отправится email!
    return "User registered"; // Сразу возвращаем
}

6. Retry Logic (@Retry, @Retryable)

Методы автоматически перевызываются при ошибке:

@Service
public class ExternalApiService {
    @Retryable(
        value = {ConnectionException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 2000)
    ) // ← Инструментируется
    public String fetchData(String url) {
        return restTemplate.getForObject(url, String.class);
        // При ConnectionException автоматически повторяется 3 раза
        // Между попытками ждёт 2 секунды
    }
}

Как работает инструментация: JDK Proxy vs CGLIB

JDK Dynamic Proxy (по умолчанию для интерфейсов)

public interface UserService {
    void createUser(String name);
}

public class UserServiceImpl implements UserService {
    public void createUser(String name) { /* ... */ }
}

// Spring создаёт прокси:
UserService proxy = Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    new InvocationHandler() {
        @Override
        public Object invoke(Object o, Method method, Object[] args) throws Throwable {
            System.out.println("Before: " + method.getName());
            Object result = method.invoke(userService, args);
            System.out.println("After: " + method.getName());
            return result;
        }
    }
);

CGLIB Proxy (для классов)

// Если класс не реализует интерфейс:
public class UserService { // Нет interface!
    public void createUser(String name) { /* ... */ }
}

// Spring использует CGLIB для создания подкласса:
public class UserServiceEnhancerBySpringCGLIB extends UserService {
    private MethodInterceptor interceptor;
    
    public void createUser(String name) {
        interceptor.intercept(this, ...); // ← Внедрённый код
        super.createUser(name); // Вызов оригинального
    }
}

Практический пример: полная инструментация

@Service
public class TransferService {
    @Autowired
    private AccountRepository accountRepository;
    
    @Transactional // Инструментация 1: управление транзакциями
    @Cacheable("transfers") // Инструментация 2: кеширование
    @PreAuthorize("hasRole(USER)") // Инструментация 3: проверка безопасности
    @Async // Инструментация 4: асинхронное выполнение
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId);
        Account to = accountRepository.findById(toId);
        
        from.debit(amount);
        to.credit(amount);
        
        accountRepository.save(from);
        accountRepository.save(to);
    }
}

// Порядок инструментации (слой за слоем):
// 1. Security check (@PreAuthorize)
// 2. Cache check (@Cacheable)
// 3. Async wrapper (@Async)
// 4. Transaction wrapper (@Transactional)
// 5. Выполнение оригинального метода

Проблемы с инструментацией

Проблема 1: Self-invocation (вызов из того же класса)

@Service
public class UserService {
    @Transactional
    public void createUserWithProfile(String name) {
        createUser(name); // ← Это НЕ будет обёрнуто! (прямой вызов)
        createProfile(name);
    }
    
    @Transactional
    private void createUser(String name) {
        // Это НЕ сработает, потому что вызов идёт из того же класса
    }
    
    @Transactional
    private void createProfile(String name) {
        // Это тоже НЕ сработает
    }
}

// ✅ Решение: вызывать через injection
@Service
public class UserService {
    @Autowired
    private UserService self; // Inject self
    
    public void createUserWithProfile(String name) {
        self.createUser(name); // ✅ Теперь работает через прокси!
        self.createProfile(name);
    }
}

Проблема 2: Private методы

@Service
public class UserService {
    @Transactional // ✅ Сработает
    public void createUser(String name) { /* ... */ }
    
    @Transactional // ❌ НЕ сработает на private методе!
    private void validateUser(User user) { /* ... */ }
}

// Решение: сделать protected или создать отдельный сервис

Вывод

Инструментация в Spring — это:

  1. Динамическое создание прокси-объектов
  2. Внедрение функционала (транзакции, кеш, security, async)
  3. Работает через AOP механизм (JDK Proxy или CGLIB)
  4. Требует дизайна через интерфейсы для лучшей совместимости
  5. Имеет ограничения (self-invocation, private методы)

Понимание инструментации критично для:

  • Отладки проблем с @Transactional
  • Кеширования (@Cacheable)
  • Безопасности (@PreAuthorize)
  • Асинхронных операций (@Async)
Что инструментируется при инструментации в Spring | PrepBro