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

Как воспользоваться оригинальным методом после замены Proxy

2.0 Middle🔥 201 комментариев
#Основы Java

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

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

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

# Доступ к оригинальному методу после замены Proxy

Это глубокий вопрос о том, как работает Proxy паттерн в Java и Spring AOP. Я разберу разные подходы в зависимости от сценария.

Контекст: Proxy паттерн

Когда используется Proxy (например, в Spring AOP), оригинальный объект оборачивается в proxy:

Запрос → Proxy → Дополнительная логика (logging, caching) → Оригинальный объект

Основная проблема

@Service
public class UserService {
    @Cacheable("users")
    public User getUser(UUID id) {
        // Оригинальный метод
        return userRepository.findById(id);
    }
}

// При вызове:
UserService service = applicationContext.getBean(UserService.class);
User user = service.getUser(id); // Это вызывает PROXY, не оригинальный метод

Сценарий 1: Spring AOP и Self-invocation (Internal call)

Это наиболее частая проблема.

Проблема

@Service
public class UserService {
    @Cacheable("users")
    public User getUser(UUID id) {
        System.out.println("Fetching user " + id);
        return userRepository.findById(id);
    }
    
    public void updateUser(User user) {
        // ...
        getUser(user.getId()); // ❌ Вызывает оригинальный метод!
                               // Кэш не используется!
    }
}

Почему? Когда updateUser вызывает getUser через this, это прямой вызов оригинального объекта, минуя proxy.

Решение 1: Inject самого себя

@Service
public class UserService {
    private final UserRepository userRepository;
    private final UserService self; // Inject self
    
    public UserService(UserRepository userRepository,
                       UserService self) {
        this.userRepository = userRepository;
        this.self = self; // Это будет PROXY версия
    }
    
    @Cacheable("users")
    public User getUser(UUID id) {
        System.out.println("Fetching user " + id);
        return userRepository.findById(id);
    }
    
    public void updateUser(User user) {
        // ...
        self.getUser(user.getId()); // ✅ Используется proxy!
                                     // Кэш работает!
    }
}

Как это работает:

Spring:
1. Создает UserService объект (оригинальный)
2. Оборачивает его в Proxy (добавляет @Cacheable функционал)
3. При inject'е self, передает PROXY версию
4. updateUser → self.getUser() → Proxy обрабатывает → original method

Решение 2: ObjectFactory

@Service
public class UserService {
    private final UserRepository userRepository;
    private final ObjectFactory<UserService> userServiceFactory;
    
    public UserService(UserRepository userRepository,
                       ObjectFactory<UserService> userServiceFactory) {
        this.userRepository = userRepository;
        this.userServiceFactory = userServiceFactory;
    }
    
    @Cacheable("users")
    public User getUser(UUID id) {
        System.out.println("Fetching user " + id);
        return userRepository.findById(id);
    }
    
    public void updateUser(User user) {
        UserService proxy = userServiceFactory.getObject();
        proxy.getUser(user.getId()); // ✅ Кэш работает
    }
}

Решение 3: ApplicationContext

@Service
public class UserService implements ApplicationContextAware {
    private ApplicationContext context;
    private final UserRepository userRepository;
    
    @Override
    public void setApplicationContext(ApplicationContext context) {
        this.context = context;
    }
    
    @Cacheable("users")
    public User getUser(UUID id) {
        return userRepository.findById(id);
    }
    
    public void updateUser(User user) {
        UserService proxy = context.getBean(UserService.class);
        proxy.getUser(user.getId()); // ✅ Работает
    }
}

⚠️ Минус: Явное получение из контекста не рекомендуется (Service Locator Pattern).

Сценарий 2: Программное использование Proxy без Spring

Dynamic Proxy (Java Reflection)

public interface UserService {
    User getUser(UUID id);
    void updateUser(User user);
}

public class UserServiceImpl implements UserService {
    private final UserRepository repository;
    
    public UserServiceImpl(UserRepository repository) {
        this.repository = repository;
    }
    
    @Override
    public User getUser(UUID id) {
        System.out.println("Getting user");
        return repository.findById(id);
    }
    
    @Override
    public void updateUser(User user) {
        // ... update logic
        this.getUser(user.getId()); // ❌ Вызывает original, не proxy
    }
}

// Создание Proxy
UserService original = new UserServiceImpl(repository);

UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    new InvocationHandler() {
        private final Map<UUID, User> cache = new ConcurrentHashMap<>();
        
        @Override
        public Object invoke(Object proxy, Method method,
                           Object[] args) throws Throwable {
            
            // Кэширование для getUser
            if (method.getName().equals("getUser")) {
                UUID userId = (UUID) args[0];
                return cache.computeIfAbsent(userId,
                    id -> {
                        try {
                            return (User) method.invoke(original, args);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                );
            }
            
            // Обычный вызов для остальных методов
            return method.invoke(original, args);
        }
    }
);

// Использование proxy
User user = proxy.getUser(id); // ✅ Кэшируется

Проблема self-invocation:

proxy.updateUser(user); // Вызывает proxy
├─ InvocationHandler.invoke(updateUser)
├─ method.invoke(original, args) // Вызывает ORIGINAL updateUser
└─ original.getUser(id) // ❌ Это вызывает original getUser, не proxy!

Решение: Pass proxy to original

public class UserServiceImpl implements UserService {
    private final UserRepository repository;
    private UserService selfProxy; // Хранит proxy
    
    public void setSelfProxy(UserService proxy) {
        this.selfProxy = proxy;
    }
    
    @Override
    public void updateUser(User user) {
        // ...
        selfProxy.getUser(user.getId()); // ✅ Теперь работает!
    }
}

// При создании proxy:
UserServiceImpl original = new UserServiceImpl(repository);
UserService proxy = (UserService) Proxy.newProxyInstance(...);
original.setSelfProxy(proxy); // Передаём proxy оригиналу

Сценарий 3: CGLIB Proxy (Spring default для классов)

Spring использует CGLIB для создания subclass'а:

// Original
@Service
public class UserService {
    @Cacheable
    public User getUser(UUID id) { ... }
}

// Spring создает динамически:
public class UserService$$EnhancedBySpringCGLIB$$xyz 
    extends UserService {
    
    private UserService original;
    
    @Override
    public User getUser(UUID id) {
        // Proxy логика
        if (cache.contains(id)) return cache.get(id);
        
        User result = super.getUser(id); // Вызывает original
        cache.put(id, result);
        return result;
    }
}

Self-invocation проблема:

UserService service = new UserService$$EnhancedBySpringCGLIB$$xyz();
service.updateUser(user);
├─ UserService$$EnhancedBySpringCGLIB$$xyz.updateUser (proxy version)
├─ super.updateUser (original)
└─ this.getUser(id) // ❌ Это вызывает original getUser через super
                    // Не использует proxy!

Сценарий 4: Явный доступ к оригинальному методу

Иногда нужно обойти proxy специально:

Через AopProxyUtils

import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.AopProxyUtils;

@Service
public class OrderService {
    public void processOrder(Order order) {
        // Получить оригинальный объект (без proxy)
        OrderService original = AopProxyUtils
            .ultimateTargetClass(this);
        
        // Вызвать оригинальный метод
        // (обход @Transactional, @Cacheable и т.д.)
    }
}

⚠️ Редко используется, обычно значит что-то неправильно в дизайне.

Через Reflection

public class ProxyAccessor {
    public static <T> T getOriginal(Object proxy) {
        try {
            // CGLIB
            if (proxy.getClass().getName().contains("$$EnhancedBySpringCGLIB")) {
                Field field = proxy.getClass()
                    .getDeclaredField("CGLIB$BOUND");
                // ...
            }
            // JDK Dynamic Proxy
            if (Proxy.isProxyClass(proxy.getClass())) {
                InvocationHandler handler = Proxy.getInvocationHandler(proxy);
                Field field = handler.getClass()
                    .getDeclaredField("original");
                field.setAccessible(true);
                return (T) field.get(handler);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return (T) proxy;
    }
}

⚠️ Очень плохая практика — нарушает инкапсуляцию и зависит от реализации.

Best Practices

1. Используй Inject self для Spring

@Service
public class UserService {
    private final UserService self;
    
    public UserService(UserService self) {
        this.self = self;
    }
    
    // Используй self.method() для вызовов с proxy
}

2. Разделяй методы по логике

Вместо self-invocation:

// ❌ Плохо
@Service
public class UserService {
    @Cacheable
    public User getUser(UUID id) { ... }
    
    public void update(User user) {
        getUser(user.getId()); // Self-invocation
    }
}

// ✅ Хорошо
@Service
public class UserService {
    private final UserRepository repository;
    
    @Cacheable
    public User getUser(UUID id) {
        return repository.findById(id);
    }
    
    public void update(User user) {
        User existing = getUser(user.getId()); // Вызывает proxy
        // update logic
    }
}

// Или используй repository напрямую внутри update
@Service
public class UserService {
    private final UserRepository repository;
    
    @Cacheable
    public User getUser(UUID id) {
        return repository.findById(id);
    }
    
    public void update(User user) {
        // Используй repository напрямую, не getUser
        User existing = repository.findById(user.getId());
        // update logic
    }
}

3. Будь осторожен с аннотациями

// ❌ Self-invocation проблема
@Service
public class TransactionService {
    @Transactional
    public void methodA() { ... }
    
    @Transactional
    public void methodB() {
        methodA(); // ❌ Использует ORIGINAL, не proxy
                   // Новая транзакция не создается
    }
}

// ✅ Решение: Inject self
@Service
public class TransactionService {
    private final TransactionService self;
    
    @Transactional
    public void methodA() { ... }
    
    @Transactional
    public void methodB() {
        self.methodA(); // ✅ Вызывает proxy, новая транзакция
    }
}

Заключение

Доступ к оригинальному методу через proxy:

  • Spring AOP: Используй inject self для вызовов с proxy
  • Dynamic Proxy: Передай proxy оригиналу через setter
  • Не обходи proxy без необходимости — это признак неправильного дизайна
  • Лучший подход: Разделяй логику так, чтобы не было self-invocation проблем

Это advanced тема, показывающая глубокое понимание того, как работает Spring AOP и proxy паттерны.