«Шаблоны проектирования» — шаблон агента

1. Какова модель агентства?

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

Основная функция режима прокси — расширение функциональности целевого объекта. Например, вы можете добавить некоторые пользовательские операции до и после выполнения метода целевого объекта.

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

[Не удалось загрузить изображение...(image-e75331-1650446995215)]

Режим прокси имеет два метода реализации: статический прокси и динамический прокси.Давайте сначала посмотрим на реализацию режима статического прокси.

2. Статический прокси

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

Выше мы говорили о статических прокси с точки зрения реализации и применения.С уровня JVM статические прокси превращают интерфейсы, классы реализации и прокси-классы в реальные файлы классов во время компиляции.

2.1. Этапы реализации статического прокси:

  1. Определите интерфейс и класс его реализации;
  2. Создайте прокси-класс, который также реализует этот интерфейс.
  3. Внедрите целевой объект в прокси-класс, а затем вызовите соответствующий метод целевого класса в соответствующем методе прокси-класса. В этом случае мы можем защитить доступ к целевому объекту через прокси-класс и делать то, что хотим, до и после выполнения целевого метода.

Показано ниже через код! 

2.2 Случай 1

1. Определите интерфейс для отправки текстовых сообщений.

public interface SmsService {
    String send(String message);
}

2. Реализовать интерфейс отправки текстовых сообщений.

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

3. Создайте прокси-класс, а также реализуйте интерфейс для отправки текстовых сообщений.

public class SmsProxy implements SmsService {

    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String send(String message) {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method send()");
        smsService.send(message);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method send()");
        return null;
    }
}

4. Фактическое использование

public class Main {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsProxy smsProxy = new SmsProxy(smsService);
        smsProxy.send("java");
    }
}

После запуска приведенного выше кода консоль выводит:

перед методом send()
отправить сообщение: Java
после метода send()

Из вывода видно, что мы добавили  SmsServiceImpl методы send()

2.2 Случай 2

package com.company.proxy.v07;


import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 * v05:使用代理
 * v06:代理有各种类型
 * 问题:如何实现代理的各种组合?继承?Decorator?
 * v07:代理的对象改成Movable类型-越来越像decorator了
 *
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        Tank t = new Tank();
        TankTimeProxy ttp = new TankTimeProxy(t);
        TankLogProxy tlp = new TankLogProxy(ttp);
        tlp.move();
    }
}

class TankTimeProxy implements Movable {
    Movable m;

    public TankTimeProxy(Movable m) {
        this.m = m;
    }

    @Override
    public void move() {
        long start = System.currentTimeMillis();
        m.move();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

class TankLogProxy implements Movable {
    Movable m;

    public TankLogProxy(Movable m) {
        this.m = m;
    }

    @Override
    public void move() {
        System.out.println("start moving...");
        m.move();
        long end = System.currentTimeMillis();
        System.out.println("stopped!");
    }
}

interface Movable {
    void move();
}

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

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

По сравнению со статическими прокси, динамические более гибкие. Нам не нужно создавать отдельный прокси-класс для каждого целевого класса, и нам не нужно реализовывать интерфейс.Мы можем напрямую проксировать класс реализации (  механизм динамического прокси CGLIB ).

С точки зрения JVM, динамический прокси динамически генерирует байт-код класса во время выполнения.

Говоря о динамических прокси, следует упомянуть фреймворки Spring AOP и RPC, реализация которых основана на динамических прокси.

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

Что касается Java, существует множество способов реализации динамического прокси, например  динамический прокси JDK , динамический прокси CGLIB и так далее.

guide-rpc-framework
использует динамический прокси JDK.Давайте сначала рассмотрим использование динамического прокси JDK.

Кроме того, хотя руководство-rpc-framework

Динамический прокси-сервер CGLIB не используется.Здесь   мы кратко представим его использование и сравнение с динамическим прокси-сервером JDK .

3.1. Механизм динамического прокси JDK

3.1.1. Введение

InvocationHandler Интерфейсы и  классы являются основой механизма динамического прокси Java  Proxy .

Proxy Наиболее часто используемый метод в классе: newProxyInstance() , этот метод в основном используется для создания прокси-объекта.

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        ......
    }

Этот метод имеет всего 3 параметра:

  1. loader  : загрузчик классов, используемый для загрузки прокси-объектов.
  2. интерфейсы  : некоторые интерфейсы, реализованные прокси-классом;
  3. h  : объект, реализующий  InvocationHandler интерфейс;

Чтобы реализовать динамический прокси, необходимо также реализовать InvocationHandler собственную логику обработки. Когда наш динамический прокси-объект вызывает метод, вызов этого метода будет перенаправлен методу, реализующему InvocationHandler класс интерфейса  invoke .

public interface InvocationHandler {

    /**
     * 当你使用代理对象调用方法的时候实际会调用到这个方法
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

invoke() Метод имеет следующие три параметра:

  1. прокси  : динамически генерируемый прокси-класс
  2. метод  : соответствует методу, вызываемому объектом прокси-класса
  3. args  : параметры текущего метода

То есть:  когда прокси-объект, который вы создаете через Proxy класс,  вызывает метод, он фактически вызывает метод  класса, реализующего интерфейс  . newProxyInstance()InvocationHandlerinvoke() Вы можете  invoke() настроить логику обработки метода, например, что делать до и после выполнения метода. 

3.1.2. Шаги по использованию динамического прокси-класса JDK

  1. Определите интерфейс и класс его реализации;
  2. Настройте InvokeHandler и перепишите метод вызова. В методе вызова мы будем вызывать собственный метод (метод прокси-класса) и настраивать некоторую логику обработки;
  3. Создайте прокси-объект с помощью метода Proxy.newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InfectionHandler h); 

3.1.3 Пример кода 1

Возможно, это немного пусто и сложно для понимания. Позвольте мне привести вам пример. Давайте попробуем!

1. Определите интерфейс для отправки текстовых сообщений.

public interface SmsService {
    String send(String message);
}

2. Реализовать интерфейс отправки текстовых сообщений.

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

3. Определите динамический прокси-класс JDK.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 11:23:00
 */
public class DebugInvocationHandler implements InvocationHandler {
    /**
     * 代理类中的真实对象
     */
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return result;
    }
}

invoke() Метод: когда наш динамический прокси-объект вызывает собственный метод, на самом деле вызывается метод  invoke() , а затем  invoke() метод вызывает собственный метод прокси-объекта от нашего имени.

4. Получите фабричный класс прокси-объекта.

public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 目标类的类加载
                target.getClass().getInterfaces(),  // 代理需要实现的接口,可指定多个
                new DebugInvocationHandler(target)   // 代理对象对应的自定义 InvocationHandler
        );
    }
}

getProxy() : В основном Proxy.newProxyInstance()получить прокси-объект определенного класса с помощью методов.

5. Фактическое использование

SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("java");

После запуска приведенного выше кода консоль выводит:

перед отправкой метода отправить
сообщение: Java
после отправки метода

3.1.4. Пример кода 2

Отображение кода

package com.company.proxy.v08;


import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 * v05:使用代理
 * v06:代理有各种类型
 * 问题:如何实现代理的各种组合?继承?Decorator?
 * v07:代理的对象改成Movable类型-越来越像decorator了
 * v08:如果有stop方法需要代理...
 * 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型 Object
 * (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
 * 分离代理行为与被代理对象
 * 使用jdk的动态代理
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Tank tank = new Tank();

        //reflection 通过二进制字节码分析类的属性和方法

        Movable m = (Movable)Proxy.newProxyInstance(Tank.class.getClassLoader(),
                new Class[]{Movable.class}, //tank.class.getInterfaces()
                new LogHander(tank)
        );

        m.move();
    }
}

class LogHander implements InvocationHandler {

    Tank tank;

    public LogHander(Tank tank) {
        this.tank = tank;
    }
    //getClass.getMethods[]
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("method " + method.getName() + " start..");
        Object o = method.invoke(tank, args);
        System.out.println("method " + method.getName() + " end!");
        return o;
    }
}


interface Movable {
    void move();
}

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

3.2. Механизм динамического прокси CGLIB 

3.2.1. Введение

Одна из наиболее серьезных проблем динамического прокси JDK заключается в том, что он может проксировать только те классы, которые реализуют интерфейсы.

Чтобы решить эту проблему, мы можем использовать механизм динамического прокси CGLIB, чтобы избежать ее.

CGLIB
(библиотека генерации кода) — это библиотека генерации байт-кода на основе ASM, которая позволяет нам изменять и динамически генерировать байт-код во время выполнения. CGLIB реализует прокси посредством наследования. Многие известные платформы с открытым исходным кодом используют CGLIB, например модуль AOP в Spring: если целевой объект реализует интерфейс, по умолчанию используется динамический прокси-сервер JDK, в противном случае используется динамический прокси-сервер CGLIB.

MethodInterceptor Интерфейсы и  классы являются ядром механизма динамического прокси CGLIB  Enhancer .

Вам необходимо настроить  MethodInterceptor и переопределить  intercept методы intercept , чтобы перехватывать методы, расширяющие прокси-класс.

public interface MethodInterceptor
extends Callback{
    // 拦截被代理类中的方法
    public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                               MethodProxy proxy) throws Throwable;
}

  1. obj  : проксируемый объект (объект, который необходимо улучшить)
  2. метод  : перехваченный метод (метод, который необходимо улучшить)
  3. args  : входные параметры метода
  4. прокси  : используется для вызова исходного метода

Вы можете  Enhancerдинамически получить прокси-класс через класс.Когда прокси-класс вызывает метод,  фактически MethodInterceptor вызывается  метод intercept . 

3.2.2. Шаги по использованию динамического прокси-класса CGLIB

  1. определить класс;
  2. Настраивайте  MethodInterceptor и переписывайте  intercept методы intercept для перехвата методов, расширяющих класс прокси,  invoke аналогично методам в динамическом прокси JDK;
  3. Создание прокси-классов через  Enhancer классы  ;create()

3.2.3 Примеры кода 

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

( Библиотека генерации кода ) на самом деле является проектом с открытым исходным кодом. Если вы хотите использовать его, вам необходимо вручную добавить связанные зависимости.

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

1. Реализуйте класс, который использует Alibaba Cloud для отправки текстовых сообщений.

package github.javaguide.dynamicProxy.cglibDynamicProxy;

public class AliSmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

2. Кастомизация (перехватчик метода)

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * 自定义MethodInterceptor
 */
public class DebugMethodInterceptor implements MethodInterceptor {

    /**
     * @param o           代理对象(增强的对象)
     * @param method      被拦截的方法(需要增强的方法)
     * @param args        方法入参
     * @param methodProxy 用于调用原始方法
     */
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object object = methodProxy.invokeSuper(o, args);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return object;
    }

}

3. Получите прокси-класс

import net.sf.cglib.proxy.Enhancer;

public class CglibProxyFactory {

    public static Object getProxy(Class<?> clazz) {
        // 创建动态代理增强类
        Enhancer enhancer = new Enhancer();
        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());
        // 设置被代理类
        enhancer.setSuperclass(clazz);
        // 设置方法拦截器
        enhancer.setCallback(new DebugMethodInterceptor());
        // 创建代理类
        return enhancer.create();
    }
}

 4. Фактическое использование

AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("java");

После запуска приведенного выше кода консоль выводит:

перед отправкой метода отправить
сообщение: Java
после отправки метода

3.3 Сравнение динамического прокси JDK и динамического прокси CGLIB 

  • Динамический прокси-сервер JDK может проксировать только классы, реализующие интерфейсы, или непосредственно прокси-интерфейсы, тогда как CGLIB может проксировать классы, которые не реализуют никаких интерфейсов. Кроме того, динамический прокси-сервер CGLIB перехватывает вызовы методов прокси-класса, создавая подкласс проксируемого класса, поэтому он не может проксировать классы и методы, объявленные как конечные типы.
  • Что касается эффективности этих двух методов, в большинстве случаев динамический прокси JDK лучше.С обновлением версии JDK это преимущество становится более очевидным. 

4. Сравнение статического и динамического прокси 

  • Гибкость: динамические прокси более гибки. Им не нужно реализовывать интерфейсы. Они могут напрямую проксировать классы реализации и не требуют создания прокси-класса для каждого целевого класса. Кроме того, в статическом прокси-сервере после добавления нового метода в интерфейс необходимо изменить и целевой объект, и прокси-объект, что очень хлопотно!
  • Уровень JVM: статический прокси превращает интерфейс, класс реализации и прокси-класс в реальные файлы классов во время компиляции. Динамический прокси динамически генерирует байт-код класса во время выполнения и загружает его в JVM.

5. Резюме

В этой статье в основном представлены две реализации режима прокси: статический прокси и динамический прокси. Охватывает фактическую борьбу между статическим и динамическим прокси-сервером, разницу между статическим и динамическим прокси-сервером, разницу между динамическим прокси-сервером JDK и динамическим прокси-сервером Cglib и т. д.

Справочная статья:  Подробное объяснение режима прокси-сервера Java_Блог Джеральда Ньютона - блог CSDN_Подробное объяснение режима прокси-сервера Java

Je suppose que tu aimes

Origine blog.csdn.net/m0_50370837/article/details/126323005
conseillé
Classement