Только освоив эти три режима прокси, вы сможете войти в Spring AOP!

Шаблоны проектирования

Определение шаблона прокси

Сначала давайте посмотрим на режим прокси:

image.png
Так называемый режим прокси означает, что клиент (Client) не вызывает непосредственно фактический объект (RealSubject в правом нижнем углу рисунка ниже), а косвенно вызывает фактический объект, вызывая прокси (ProxySubject).

Использование прокси-режима обычно связано с тем, что клиент не хочет напрямую обращаться к фактическому объекту или существуют технические препятствия для доступа к фактическому объекту, поэтому непрямой доступ осуществляется через прокси-объект в качестве моста.

Деловая сцена

Во-первых, есть интерфейс UserService, в котором есть метод добавления пользователей.

public interface UserService {
    void addUser();
}

Это его класс реализации

public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("添加一个用户");
    }
}

Теперь нужно вести лог при добавлении пользователя. Конечно, вы можете написать код для добавления логов прямо в addUser,

    public void addUser() {
        System.out.println("添加一个用户");
	System.out.println("拿个小本本记一下");
    }

Но Java уважаетединственная ответственностьВпринципе, если это написано с нарушением этого принципа, нам нужно отделить код добавления логов, а методу addUser() позволить сосредоточиться на написании собственной бизнес-логики.

статический прокси

Согласно диаграмме классов создайте статический прокси-класс

public class UserStaticProxy implements UserService{
    private UserService userService;
    public UserStaticProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void addUser() {
        userService.addUser();
        System.out.println("拿个小本本记录一下");
    }
}

Мы создаем тестовый класс для проверки статического прокси:

public class Test {

    public static void main(String[] args) {
        UserStaticProxy userStaticProxy = new UserStaticProxy(new UserServiceImpl());
        userStaticProxy.addUser();
    }
}

результат операции:

image.png
Таким образом создается статический прокси-класс, и мы можем сосредоточиться на написании бизнес-логики в Сервисе, добавлении журналов и другой небизнес-логики в этот статический прокси-класс для завершения.

Недостатки статических прокси

Недостаток 1: интерфейс добавляет методы, и прокси-класс необходимо поддерживать синхронно

С расширением бизнеса в классе UserService отсутствует метод addUser, а также методы updateUser, deleteUser, batchUpdateUser, batchDeleteUser и другие, и эти методы нуждаются в записи логов.

Класс UserServiceImpl выглядит следующим образом:

public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("添加一个用户");
    }

    @Override
    public void updateUser() {
        System.out.println("更新一个用户");
    }

    @Override
    public void deleteUser() {
        System.out.println("删除一个用户");
    }

    @Override
    public void batchUpdateUser() {
        System.out.println("批量更新用户");
    }

    @Override
    public void batchDeleteUser() {
        System.out.println("批量删除用户");
    }
}

Тогда соответствующий статический прокси-класс выглядит следующим образом:

public class UserStaticProxy implements UserService{
    private UserService userService;
    public UserStaticProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void addUser() {
        userService.addUser();
        System.out.println("拿个小本本记录一下");
    }

    @Override
    public void updateUser() {
        userService.updateUser();
        System.out.println("拿个小本本记录一下");
    }

    @Override
    public void deleteUser() {
        userService.deleteUser();
        System.out.println("拿个小本本记录一下");
    }

    @Override
    public void batchUpdateUser() {
        userService.batchUpdateUser();
        System.out.println("拿个小本本记录一下");
    }

    @Override
    public void batchDeleteUser() {
        userService.batchDeleteUser();
        System.out.println("拿个小本本记录一下");
    }
}

Из приведенного выше мы видим, что в прокси-классе много повторяющегося кода ведения журнала. Поскольку прокси-класс и целевой объект реализуют один и тот же интерфейс, после добавления метода в интерфейс прокси-класс также должен добавлять методы и синхронно дублировать дополнительные коды функций, что увеличивает объем кода.

Недостатки: чем больше интерфейсов, ведущих к широкому возрасту

Если вам нужно добавить бизнес-классы, такие как StudentService, TeacherService и т.д., и методы в этих классах также должны реализовать метод добавления логов, то вам необходимо синхронно создать соответствующий прокси-класс. Кроме того, статические прокси-классы не генерируются автоматически и должны быть написаны перед компиляцией.Если бизнес становится все больше и больше, создается все больше и больше прокси-классов, что увеличивает объем кода.

Как решить эти недостатки? Здесь необходим метод динамического прокси.

Динамический прокси JDK

По сути, суть динамического прокси и статического прокси одинакова: при запуске конечной программы необходимо сгенерировать экземпляр объекта прокси, и через него можно выполнить соответствующие улучшения и бизнес-логику, однако статический прокси должен быть задан жестко закодированным образом, в то время как динамический прокси поддерживает работу. Эта реализация время от времени генерируется динамически.

Сам JDK помогает нам реализовать динамический прокси, просто используйте метод newProxyInstance:

 public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

Обратите внимание, что этот метод является статическим методом в классе Proxy и получает три параметра:

  • Загрузчик ClassLoader: укажите текущий целевой объект для использования загрузчика классов.
  • Class>[] interfaces: Список интерфейсов, которые должен реализовать прокси-класс.
  • InvocationHandler h: обработчик вызова, которому отправляется метод целевого объекта.

Пример кода:

public class DynamicProxy implements InvocationHandler {

    private Object target; // 目标对象

    public Object bind(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(target, args);
        System.out.println("拿个小本本记录一下");
        return result;
    }

}

Вышеупомянутый метод вызова отвечает за расширение метода целевого объекта.Все методы класса интерфейса будут проходить через этот метод вызова. Кроме того, метод bind просто инкапсулирует прокси-метод JDK newProxyInstance, который отвечает за возврат класса интерфейса.

Тестовый класс:

 public static void main(String[] args) {
        DynamicProxy dynamicProxy = new DynamicProxy();
        UserService userService = (UserService)dynamicProxy.bind(new UserServiceImpl());
        userService.addUser();
        userService.updateUser();
    }

Результаты приведены ниже:

image.png

Как показано на рисунке, все методы в интерфейсе UserService были добавлены с логической логикой Кроме того, давайте посмотрим, что целевой атрибут в классе UserDynamicProxy имеет тип Object. Таким образом, этот метод динамического прокси может также повторно использоваться другими службами. Это можно назвать так:

DynamicProxy dynamicProxy = new DynamicProxy();
TeacherService teacherService = (TeacherService)dynamicProxy.bind(new TeacherServiceImpl());

Таким образом, динамический прокси устраняет недостатки статического прокси.

Используйте arthas для просмотра классов, созданных динамическим прокси JDK.

Динамический прокси динамически генерирует класс прокси во время выполнения, этот класс помещается в память, как мы можем увидеть этот класс?

artias — это блестящий инструмент для диагностики Java с открытым исходным кодом от Alibaba.Если вы не понимаете, вы можете прочитать эту статью http://www.dblearn.cn/article/5.Вы можете использовать его для декомпиляции кода в Интернете.

Здесь мы добавляем точку останова:

public static void main(String[] args) throws IOException {
        DynamicProxy dynamicProxy = new DynamicProxy();
        UserService userService = (UserService)dynamicProxy.bind(new UserServiceImpl());
        userService.addUser();
        userService.updateUser();
        System.in.read();
    }

беги Артас

image.png

использоватьjadДекомпиляция команды, классы прокси, сгенерированные java, находятся в каталоге com.sun.proxy. Итак, команда декомпиляции выглядит следующим образом

jad com.sun.proxy.$Proxy0

package com.sun.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.UserService;

public final class $Proxy0
extends Proxy
implements UserService {
    private static Method m1;
    private static Method m6;
    private static Method m2;
    private static Method m7;
    private static Method m0;
    private static Method m3;
    private static Method m4;
    private static Method m5;

    public final void addUser() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void updateUser() {
        try {
            this.h.invoke(this, m4, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void deleteUser() {
        try {
            this.h.invoke(this, m5, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void batchUpdateUser() {
        try {
            this.h.invoke(this, m6, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void batchDeleteUser() {
        try {
            this.h.invoke(this, m7, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m6 = Class.forName("proxy.UserService").getMethod("batchUpdateUser", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m7 = Class.forName("proxy.UserService").getMethod("batchDeleteUser", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m3 = Class.forName("proxy.UserService").getMethod("addUser", new Class[0]);
            m4 = Class.forName("proxy.UserService").getMethod("updateUser", new Class[0]);
            m5 = Class.forName("proxy.UserService").getMethod("deleteUser", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final boolean equals(Object object) {
        try {
            return (Boolean)this.h.invoke(this, m1, new Object[]{object});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString() {
        try {
            return (String)this.h.invoke(this, m2, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer)this.h.invoke(this, m0, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

Из приведенного выше кода мы видим, что наш прокси-класс был сгенерирован, и когда мы вызываем такие методы, как addUser(), он фактически отправляется в метод вызова переменной h для выполнения:

this.h.invoke(this, m3, null);

Что такое переменная h? Фактически мы реализуем класс DynamicProxy InvocationHandler.

cglib динамический прокси

Наблюдая за указанным выше режимом статического прокси и динамического прокси JDK, было обнаружено, что целевой объект требуетсяреализовать интерфейс, но иногда целевой объект — это всего лишь один объект, который не реализует никакого интерфейса. Как с этим бороться в это время? Следующее ведет к знаменитому динамическому прокси CGlib.

Прокси-сервер Cglib, также называемый прокси-сервером подкласса, создает объект подкласса в памяти для расширения функции целевого объекта.

Чтобы использовать cglib, вам нужно импортировать его пакет jar, потому что Spring интегрировал его, поэтому вы можете импортировать пакет spring.

Напишите прокси-класс:

public class CGLibProxy implements MethodInterceptor {
    private Object target; // 目标对象
    public Object bind(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(this.target.getClass());
        //设置回调函数
        enhancer.setCallback(this);
        //创建子类(代理对象)
        return enhancer.create();
    }
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object result = methodProxy.invokeSuper(obj, args);
        System.out.println("拿个小本本记录一下");
        return result;
    }
}

Среди них Enhancer должен установить целевой объект в качестве родительского класса (поскольку сгенерированный прокси-класс должен наследовать целевой объект).

Тестовый класс:

 public static void main(String[] args) throws IOException {
        CGLibProxy cgLibProxy = new CGLibProxy();
        UserServiceImpl userService = (UserServiceImpl)cgLibProxy.bind(new UserServiceImpl());
        userService.addUser();
        userService.updateUser();
        System.in.read();
    }

результат операции:

image.png

Мы видим, что он был успешно проксирован. Однако результат искажен. Здесь установлено // TODO. Я предполагаю, что это вызвано повторной инкапсуляцией CGlib в Spring. Пожалуйста, ответьте на него, если вы это знаете.

Используйте arthas для просмотра классов, сгенерированных динамическим прокси cglib.

Шаги такие же, как и для прокси-класса JDK, за исключением того, что прокси-класс cglib создается в том же пакете, что и тестовый класс.Поскольку кодов слишком много, выше приведена только часть кода.

package com.example.demo.proxy;

import com.example.demo.proxy.UserServiceImpl;
import java.lang.reflect.Method;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.cglib.core.Signature;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class UserServiceImpl?EnhancerByCGLIB?3ca8cfc3
extends UserServiceImpl
implements Factory {
    private boolean CGLIB$BOUND;
    public static Object CGLIB$FACTORY_DATA;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static Object CGLIB$CALLBACK_FILTER;
    private static final Method CGLIB$deleteUser$0$Method;
    private static final MethodProxy CGLIB$deleteUser$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$addUser$1$Method;
    private static final MethodProxy CGLIB$addUser$1$Proxy;
    private static final Method CGLIB$updateUser$2$Method;
    private static final MethodProxy CGLIB$updateUser$2$Proxy;
    private static final Method CGLIB$batchUpdateUser$3$Method;
    private static final MethodProxy CGLIB$batchUpdateUser$3$Proxy;
    private static final Method CGLIB$batchDeleteUser$4$Method;
    private static final MethodProxy CGLIB$batchDeleteUser$4$Proxy;
    private static final Method CGLIB$equals$5$Method;
    private static final MethodProxy CGLIB$equals$5$Proxy;
    private static final Method CGLIB$toString$6$Method;
    private static final MethodProxy CGLIB$toString$6$Proxy;
    private static final Method CGLIB$hashCode$7$Method;
    private static final MethodProxy CGLIB$hashCode$7$Proxy;
    private static final Method CGLIB$clone$8$Method;
    private static final MethodProxy CGLIB$clone$8$Proxy;

    public final void deleteUser() {
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            UserServiceImpl?EnhancerByCGLIB?3ca8cfc3.CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        if (methodInterceptor != null) {
            Object object = methodInterceptor.intercept(this, CGLIB$deleteUser$0$Method, CGLIB$emptyArgs, CGLIB$deleteUser$0$Proxy);
            return;
        }
        super.deleteUser();
    }

    public final void addUser() {
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            UserServiceImpl?EnhancerByCGLIB?3ca8cfc3.CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        if (methodInterceptor != null) {
            Object object = methodInterceptor.intercept(this, CGLIB$addUser$1$Method, CGLIB$emptyArgs, CGLIB$addUser$1$Proxy);
            return;
        }
        super.addUser();
    }

    public final void updateUser() {
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            UserServiceImpl?EnhancerByCGLIB?3ca8cfc3.CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        if (methodInterceptor != null) {
            Object object = methodInterceptor.intercept(this, CGLIB$updateUser$2$Method, CGLIB$emptyArgs, CGLIB$updateUser$2$Proxy);
            return;
        }
        super.updateUser();
    }

в

public class UserServiceImpl?EnhancerByCGLIB?3ca8cfc3 extends UserServiceImpl

Вы можете видеть, что сгенерированный прокси-класс наследует целевой объект, поэтому следует отметить два момента:

  1. Целевой объект не может быть изменен с помощью ключевого слова final, поскольку объект, измененный с помощью final, не наследуется.
  2. Если метод целевого объекта final/static, он не будет перехвачен, то есть дополнительные бизнес-методы целевого объекта не будут выполняться.