Что инструментируется при инструментации в Spring
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Инструментация в 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 — это:
- Динамическое создание прокси-объектов
- Внедрение функционала (транзакции, кеш, security, async)
- Работает через AOP механизм (JDK Proxy или CGLIB)
- Требует дизайна через интерфейсы для лучшей совместимости
- Имеет ограничения (self-invocation, private методы)
Понимание инструментации критично для:
- Отладки проблем с @Transactional
- Кеширования (@Cacheable)
- Безопасности (@PreAuthorize)
- Асинхронных операций (@Async)