Что такое прокси в Spring и как они работают?

Ответ

Spring-прокси — это объекты-заместители, которые Spring создаёт для поддержки сквозной функциональности (AOP), такой как транзакции, кэширование и безопасность.

Принцип работы: Когда вы вызываете метод бина, управление сначала передаётся прокси, который применяет дополнительное поведение (например, открывает транзакцию), а затем делегирует вызов целевому объекту.

Два основных типа прокси в Spring:

  1. JDK Dynamic Proxy (по умолчанию для бинов с интерфейсами).
    • Создаётся во время выполнения на основе интерфейсов бина.
    • Требует, чтобы бин реализовывал хотя бы один интерфейс.
  2. CGLIB Proxy (используется, если интерфейсов нет или задано proxyTargetClass=true).
    • Создаёт прокси через наследование от класса бина.
    • Не работает для final классов и методов.

Пример с транзакционным прокси:

@Service
public class UserService {
    @Transactional // Spring создаст прокси для управления транзакцией
    public User createUser(String name) {
        // Логика сохранения в БД
        return userRepository.save(new User(name));
    }
}

Важные нюансы:

  • Самовызов (this.createUser()) внутри класса не проходит через прокси, так как вызывается метод напрямую, а не у прокси-объекта. Для обхода используют инъекцию самого бина (@Autowired private UserService self) или AopContext.currentProxy().
  • Проверить, является ли объект прокси: AopUtils.isAopProxy(bean).

Ответ 18+ 🔞

Давай разберём эту вашу прокси-махинацию, а то как будто про шпионов речь, а не про код. Весь этот цирк с заместителями, который Spring вытворяет у нас под капотом, пока мы чай пьём.

Вот представь: у тебя есть обычный бин, простой работяга, который должен юзеров в базу пихать. А тут приходит менеджер-прокси, такой хитрожопый посредник, и говорит: «Стоять! Нельзя просто так взять и сохранить. Сначала я транзакцию открою, потом безопасность проверю, может, ещё логи напишу, а уж потом, если всё чики-пуки, ты своё дело делай». И управление через него идёт. Весь этот AOP — это и есть навешивание таких вот надзирателей на твои методы.

А создаёт он этих надзирателей двумя основными способами, тут уже на выбор, как повезёт:

  1. JDK Dynamic Proxy. Это когда твой бин — хороший мальчик и реализует интерфейс. Тогда Spring, этот хитрый ёж, во время работы приложения на лету генерит прокси-класс, который этот интерфейс и реализует. Вызываешь ты userService.save(), а на самом деле дергаешь метод у этой сгенерированной мартышлюшки, которая уже потом дергает твой настоящий бин.
  2. CGLIB Proxy. А это уже тяжёлая артиллерия, когда твой класс — упрямый мудак и интерфейсов не признаёт. Тогда Spring наследуется от твоего же класса и создаёт его подкласс-прокси. По сути, делает его незаконнорожденного ребёнка, который все папины методы переопределяет, чтобы свою дичь вставить. Но если у папы (твоего класса) методы final — всё, пиши пропало, наследоваться не получится, ебаный рот этого CGLIB!

Вот смотри, как это выглядит в коде, когда всё прилично:

@Service
public class UserService {
    @Transactional // Вот эта аннотация — сигнал для Spring: «Эй, надень на этот метод наручники с транзакцией!»
    public User createUser(String name) {
        // Какая-то логика
        return userRepository.save(new User(name));
    }
}

Вызываешь ты userService.createUser("Вася"). А на самом деле дергаешь не самого UserService, а его прокси-клона. Клон сначала говорит: «Так, ща транзакцию откроем...», делает свои дела, потом вызывает настоящий метод createUser у оригинального бина, и только потом коммитит или откатывает всё. Красота!

Но есть один пиздецкий подводный камень, о который все бьются! Самовызов.

Представь внутри этого же класса другой метод:

public void someInternalMethod() {
    this.createUser("Петя"); // ВНУТРЕННИЙ вызов!
}

Вот тут-то и жопа. Потому что this — это ссылка на настоящий, оригинальный объект, а не на его прокси-обёртку! И когда ты зовёшь this.createUser(), ты обходишь всю эту охрану с транзакциями стороной, вызываешь метод напрямую. Прокси остаётся в стороне и плачет. Транзакция не откроется, и будет тебе боль и страдание.

Как с этим бороться, спросишь? Есть два грязных хакерских способа, на которые идёт Spring-сообщество:

  1. Инжектнуть самого себя (звучит как шизофрения, да): @Autowired private UserService self. И вызывать через self.createUser() — это уже вызов через прокси.
  2. Использовать AopContext.currentProxy() — это как крикнуть: «Эй, а где мой прокси-двойник?», и он тебе его подаст. Способ так себе, требует дополнительных настроек.

А если хочешь на глазок определить, имеешь дело с оригиналом или уже с подставной уткой, есть утилитка: AopUtils.isAopProxy(bean). Вернёт true — поздравляю, ты общаешься с прокси. Вернёт false — перед тобой голый, ничем не прикрытый бин, со всеми вытекающими.

Вот такая, блядь, матрешка получается. Один бин в другом, поверх ещё прокси, и всё это крутится, чтобы твои транзакции не разъебались. Ёперный театр!