「デザインパターン」 - エージェントパターン

1. 代理店モデルとは何ですか?

プロキシパターンは比較的わかりやすいデザインパターンです。簡単に言うと、プロキシ オブジェクトを使用して実際のオブジェクトへのアクセスを置き換え、追加の機能操作を提供し、元のターゲット オブジェクトを変更せずにターゲット オブジェクトの機能を拡張できます。

プロキシ モードの主な機能は、ターゲット オブジェクトの機能を拡張することです。たとえば、ターゲット オブジェクトのメソッドの実行前後にいくつかのカスタム操作を追加できます。

たとえば、Xiaohong に質問するのを手伝ってもらう場合、Xiaohong はあなたの代わりに行動するプロキシ オブジェクトと見なすことができ、エージェントの動作 (メソッド) は質問をすることです。

[画像のアップロードに失敗しました...(image-e75331-1650446995215)]

プロキシモードには静的プロキシと動的プロキシの2つの実装方法がありますが、まずは静的プロキシモードの実装を見てみましょう。

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 動的プロキシの使用方法を見てみましょう。

また、guide-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. hInvocationHandler : インターフェース を実装するオブジェクト 。

動的プロキシを実装するには、InvocationHandler カスタム処理ロジックも実装する必要があります。動的プロキシ オブジェクトがメソッドを呼び出すと、このメソッドへの呼び出しはインターフェイスInvocationHandler クラス を実装するメソッドに転送されますinvoke 。

public interface InvocationHandler {

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

invoke() このメソッドには次の 3 つのパラメータがあります。

  1. proxy  : 動的に生成されたプロキシ クラス
  2. Method  : プロキシ クラス オブジェクトによって呼び出されるメソッドに対応します
  3. args  : 現在のメソッドのパラメータ

つまり、 クラス を通じて作成した プロキシ オブジェクトがメソッドを呼び出すと、実際には そのインターフェイスを実装するクラスの メソッドが呼び出されますProxynewProxyInstance()InvocationHandlerinvoke()invoke() メソッドの実行前後に何を行うかなど、メソッド内の処理ロジックをカスタマイズ  できます 。

3.1.2. JDK 動的プロキシクラスを使用する手順

  1. インターフェースとその実装クラスを定義します。
  2. InvocationHandler をカスタマイズし、invoke メソッドを書き換えます。invoke メソッドでは、ネイティブ メソッド (プロキシ クラスのメソッド) を呼び出し、いくつかの処理ロジックをカスタマイズします。
  3. Proxy.newProxyInstance(ClassLoaderloader,Class<?>[]interfaces,InvocationHandler 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 動的プロキシの最も致命的な問題の 1 つは、インターフェイスを実装するクラスしかプロキシできないことです。

この問題を解決するには、CGLIB 動的プロキシ メカニズムを使用して回避します。

CGLIB
(コード生成ライブラリ) は、実行時にバイトコードを変更して動的に生成できる ASM ベースのバイトコード生成ライブラリです。CGLIB は継承を通じてプロキシを実装します。Spring の AOP モジュールなど、多くのよく知られたオープン ソース フレームワークは CGLIB を使用します。ターゲット オブジェクトがインターフェイスを実装している場合は、デフォルトで JDK 動的プロキシが使用され、それ以外の場合は CGLIB 動的プロキシが使用されます。

MethodInterceptor インターフェイスと クラスは、 CGLIB 動的プロキシ メカニズムの 中核ですEnhancer 。

 プロキシされたクラスを拡張するメソッドをインターセプトするには、 メソッドをMethodInterceptor カスタマイズしてオーバーライドする 必要があります 。interceptintercept

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

  1. obj  : プロキシされるオブジェクト (拡張する必要があるオブジェクト)
  2. Method  : インターセプトされたメソッド (拡張が必要な​​メソッド)
  3. args  : メソッドの入力パラメータ
  4. proxy  : 元のメソッドを呼び出すために使用されます

クラスを通じてプロキシ クラスを動的に取得できます Enhancer。プロキシ クラスがメソッドを呼び出すと、 実際にはMethodInterceptor の メソッドが呼び出されますintercept 。 

3.2.2. CGLIB 動的プロキシクラスを使用する手順

  1. クラスを定義する。
  2.  JDK 動的プロキシのメソッドと同様に 、プロキシ クラスを拡張するメソッドをインターセプト する メソッドをカスタマイズ MethodInterceptor および書き換えます 。interceptinterceptinvoke
  3. Enhancer class を通じて 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. まとめ

この記事では主に、静的プロキシと動的プロキシという 2 つのプロキシ モードの実装を紹介します。静的プロキシと動的プロキシの実際の戦闘、静的プロキシと動的プロキシの違い、JDK 動的プロキシと Cglib 動的プロキシの違いなどを取り上げます。

参考記事:  Java プロキシ モードの詳細説明_Gerald Newton のブログ - CSDN ブログ_Java プロキシ モードの詳細説明

おすすめ

転載: blog.csdn.net/m0_50370837/article/details/126323005