Подробное объяснение режима прокси-сервера Java и динамического прокси-сервера

Java

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

прокси-режим

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

Сначала взгляните на определение энциклопедии Baidu:

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

Это может быть трудно понять непосредственно, глядя на определение, поэтому давайте проиллюстрируем его конкретными примерами из жизни.

Мы все были в супермаркете, чтобы купить товары.Супермаркет покупает товары у производителя и продает их нам.Мы обычно не знаем, откуда берутся товары и сколько процессов проходит в супермаркете.

В этом процессе это равносильно тому, что производитель «поручает» супермаркету продавать товар, для нас производитель (реальный объект) невидим. А супермаркет (прокси-объект) взаимодействует с нами как «агент» производителя.

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

В режиме прокси мы можем делать две вещи:

1. Скрыть конкретную реализацию класса делегата.

2. Реализовать развязку клиента и класса делегата, а также добавить некоторые дополнительные функции (логи, разрешения) и т.п. без изменения кода класса делегата.

Определение роли режима агента

В приведенном выше процессе мы можем определить три типа объектов в процессе программирования:

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

Диаграммы классов, соответствующие трем вышеперечисленным ролям, следующие:

Java代理及动态代理详解

Статический прокси-сервер

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

Давайте возьмем конкретный пример, чтобы продемонстрировать статический прокси.

Сначала определите набор интерфейсов Sell, используемых для предоставления таких функций, как реклама и продажи. Затем предоставьте класс Vendor (производитель, прокси-объект) и Shop (супермаркет, прокси-класс), которые соответственно реализуют интерфейс Sell.

Интерфейс Sell определяется следующим образом:

/**
 * 委托类和代理类都实现了Sell接口
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public interface Sell {

    /**
     * 出售
     */
    void sell();

    /**
     * 广告
     */
    void ad();
}

Класс Vendor определяется следующим образом:

/**
 * 供应商
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public class Vendor implements Sell{

    @Override
    public void sell() {
        System.out.println("Shop sell goods");
    }

    @Override
    public void ad() {
        System.out.println("Shop advert goods");
    }
}

Класс Shop определяется следующим образом:

/**
 * 超市,代理类
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public class Shop implements Sell{

    private Sell sell;

    public Shop(Sell sell){
        this.sell = sell;
    }

    @Override
    public void sell() {
        System.out.println("代理类Shop,处理sell");
        sell.sell();
    }

    @Override
    public void ad() {
        System.out.println("代理类Shop,处理ad");
        sell.ad();
    }
}

Прокси-класс Shop содержит ссылку на прокси-класс Vendor посредством агрегации и вызывает соответствующий метод Vendor в соответствующем методе. В классе Shop мы можем добавить некоторые дополнительные обработки, такие как фильтрация пользователей покупки, запись журналов и другие операции.

Давайте посмотрим, как прокси-класс используется в клиенте.

/**
 * 静态代理类测试方法
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:33 AM
 **/
public class StaticProxy {

    public static void main(String[] args) {

        // 供应商---被代理类
        Vendor vendor = new Vendor();

        // 创建供应商的代理类Shop
        Sell sell = new Shop(vendor);

        // 客户端使用时面向的是代理类Shop。
        sell.ad();
        sell.sell();
    }
}

В приведенном выше коде клиент видит, что интерфейс Sell предоставляет функции, а функции предоставляются Shop. Мы можем изменить или добавить некоторый контент в магазине, не затрагивая прокси-класс Vendor.

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

Реализация статического прокси проста и не затрагивает исходный код, но когда сцена сложная, статический прокси будет иметь следующие недостатки:

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

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

Итак, динамические прокси пригодятся.

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

Динамический прокси относится к методу прокси, в котором класс прокси создается во время работы программы. В этом случае прокси-класс не определен в коде Java, а динамически генерируется во время выполнения на основе «инструкций» в коде Java.

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

На основе встроенной реализации динамического прокси JDK.

Обычно существует два способа реализации динамических прокси: собственные динамические прокси JDK и динамические прокси CGLIB. Здесь мы берем собственный динамический прокси-сервер JDK в качестве примера для объяснения.

Динамический прокси-сервер JDK в основном включает два класса: java.lang.reflect.Proxy и java.lang.reflect.InvocationHandler.

Интерфейс InvocationHandler определяет следующие методы:

/**
 * 调用处理程序
 */
public interface InvocationHandler { 
    Object invoke(Object proxy, Method method, Object[] args); 
}

Как следует из названия, промежуточный класс, реализующий этот интерфейс, действует как «обработчик вызовов». При вызове метода объекта прокси-класса «вызов» будет переадресован методу вызова. Объект прокси-класса передается в качестве параметра прокси. Метод параметра определяет, какой именно метод прокси-класса вызывается, а аргументы является параметром метода. Таким образом, вызовы всех методов в прокси-классе станут вызовами для вызова, и к методу вызова можно добавить унифицированную логику обработки (разные методы прокси-классов также могут обрабатываться по-разному в соответствии с параметром метода).

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

Ниже приведен пример добавления журнала для демонстрации динамического прокси.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

public class LogHandler implements InvocationHandler {
    Object target;  // 被代理的对象,实际的方法执行者

    public LogHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);  // 调用 target 的 method 方法
        after();
        return result;  // 返回方法的执行结果
    }
    // 调用invoke方法之前执行
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    // 调用invoke方法之后执行
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

Модуль записи на стороне клиента использует динамический прокси-код следующим образом:

import java.lang.reflect.Proxy;

/**
 * 动态代理测试
 *
 * @author sec
 * @version 1.0
 * @date 2020/3/21 10:40 AM
 **/
public class DynamicProxyMain {

    public static void main(String[] args) {
        // 创建中介类实例
        LogHandler logHandler = new LogHandler(new Vendor());
        // 设置该变量可以保存动态代理类,默认名称$Proxy0.class
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        // 获取代理类实例Sell
        Sell sell = (Sell) (Proxy.newProxyInstance(Sell.class.getClassLoader(), new Class[]{Sell.class}, logHandler));

        // 通过代理类对象调用代理类方法,实际上会转到invoke方法调用
        sell.sell();
        sell.ad();
    }
}

После выполнения журнал печати выглядит следующим образом:

调用方法sell之【前】的日志处理
Shop sell goods
调用方法sell之【后】的日志处理
调用方法ad之【前】的日志处理
Shop advert goods
调用方法ad之【后】的日志处理

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

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

// 设置该变量可以保存动态代理类,默认名称$Proxy0.class
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

Затем, после того как мы можем выполнить основной метод, также создается файл класса с именем $Proxy0.class. После декомпиляции можно увидеть следующий код:

package com.sun.proxy;

import com.choupangxia.proxy.Sell;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Sell {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

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

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void ad() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void sell() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.choupangxia.proxy.Sell").getMethod("ad");
            m3 = Class.forName("com.choupangxia.proxy.Sell").getMethod("sell");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

Вы можете видеть, что $Proxy0 (класс прокси) наследует класс Proxy и реализует все проксируемые интерфейсы, а также такие методы, как equals, hashCode и toString.

Поскольку класс динамического прокси наследует класс Proxy, каждый класс прокси связан с обработчиком вызова метода InvocationHandler.

Класс и все методы изменяются с помощью public final, поэтому можно использовать только прокси-класс, который нельзя наследовать.

Каждый метод описывается объектом Method, который создается в статическом блоке статического кода и называется в формате «m+число».

При вызове метода он вызывается через super.h.invoke(this,m1,(Object[])null);. Super.h.invoke на самом деле является объектом LogHandler, который передается в Proxy.newProxyInstance при создании прокси, наследует класс InvocationHandler и отвечает за реальную логику обработки вызовов.

резюме

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

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


--- Программа Новые Горизонты: Захватывающие и растущие нельзя пропустить

程序新视界-微信公众号