Что такое паттерн Proxy и где его применяют
Что проверяют
Знаете ли вы паттерн не только по диаграмме, но и там, где он работает у вас каждый день: в Spring и Hibernate. Следом почти всегда спрашивают, чем Proxy отличается от Decorator и почему @Transactional не срабатывает при вызове из того же класса.
Как отвечать
Proxy это структурный паттерн: заместитель реализует тот же интерфейс, что и настоящий объект, держит на него ссылку и решает, когда и как пропустить вызов. Клиент разницы не видит. По назначению бывают прокси виртуальный (откладывает создание дорогого объекта), защищающий (проверяет права), удалённый (прячет сетевой вызов, как клиентская заглушка gRPC), кэширующий и логирующий.
В Java прокси делают тремя способами:
- вручную: класс с тем же интерфейсом, как учебная ленивая загрузка картинки;
- динамический JDK proxy:
Proxy.newProxyInstance()иInvocationHandler, работает только через интерфейсы; - генерация подкласса: CGLIB или Byte Buddy, подходит для классов без интерфейсов, но не умеет переопределять
finalклассы и методы.
Где это видно в проектах. Spring оборачивает бины в прокси ради @Transactional, @Cacheable, @Async и Spring Security, и Spring Boot по умолчанию делает прокси на основе классов. Hibernate возвращает прокси из getReference() и для связей @ManyToOne(fetch = FetchType.LAZY), первое обращение к полю идёт в базу.
Отличие от Decorator в намерении: декоратор добавляет поведение и собирается клиентом в цепочку, прокси управляет доступом и часто сам решает, когда создать объект. Adapter, в отличие от обоих, меняет интерфейс.
Где спотыкаются
Не связывают паттерн со Spring. Вызов this.method() внутри бина идёт мимо прокси, поэтому аннотации вызываемого метода не работают. Это прямое следствие паттерна, и его ждут услышать.
Забывают про final. Классы Kotlin по умолчанию финальные, поэтому для Spring подключают плагин kotlin-spring на основе all-open, иначе прокси не создаётся.
Сравнивают сущности Hibernate через getClass(). У прокси это сгенерированный подкласс, и equals() ломается, нужен instanceof или Hibernate.getClass(). А обращение к ленивому прокси после закрытия сессии даёт LazyInitializationException.