java-recipes

ホーム デザインパターン › DP-12

DP-12: Proxy パターン

実オブジェクトへのアクセスを制御する「代理人(プロキシ)」を挟むパターンです。 代理人を経由することで、遅延ロード・アクセス制御・ログ記録などを透過的に追加できます。

Proxy パターンとは

「プロキシ(Proxy)」とは「代理」を意味します。 Proxy パターンでは、実オブジェクトと同じインターフェースを実装した代理オブジェクト(プロキシ)を用意し、 クライアント(呼び出し側)とのやり取りをプロキシが仲介します。 クライアントはプロキシと実オブジェクトを区別せずに扱えるため、透過的にアクセス制御や追加処理を実現できます。

Proxy の3種類

種類目的代表例
仮想プロキシコストの高いオブジェクト生成を必要になるまで遅らせる(遅延ロード)大きな画像ファイルのロード、DB接続
保護プロキシロールや権限を確認してからアクセスを許可する管理者のみ閲覧可能なデータ、API アクセス制御
リモートプロキシネットワーク越しのオブジェクトをローカルのように扱うRMI(Remote Method Invocation)、REST クライアント

Java 標準ライブラリの実例: java.lang.reflect.Proxy(動的プロキシ)

Java には実行時にインターフェースの実装を動的に生成するjava.lang.reflect.Proxy があります。 Spring Framework や Hibernate などの DI フレームワークは、この動的プロキシを使ってトランザクション管理や AOP(アスペクト指向プログラミング)を実現しています。

// java.lang.reflect.Proxy を使った動的プロキシの概念コード
// インターフェースに対してのみ使用可能(クラスには使えない)
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
    MyInterface.class.getClassLoader(),
    new Class[]{ MyInterface.class },
    (proxyObj, method, args) -> {
        System.out.println("メソッド呼び出し前: " + method.getName());
        Object result = method.invoke(realObject, args);
        System.out.println("メソッド呼び出し後: " + method.getName());
        return result;
    }
);

サンプルコード

ProxyPatternSample.java
public class ProxyPatternSample {

    // ---- 共通インターフェース ----
    interface ImageLoader {
        /** 画像データを読み込む */
        String load(String path);
        /** 画像を表示する */
        void display();
    }

    // ---- 実際の実装: 重い処理(コンストラクタでロードを行う)----
    static class RealImageLoader implements ImageLoader {
        private final String path;
        private String imageData;

        public RealImageLoader(String path) {
            this.path = path;
            // コンストラクタで重い処理(ディスクI/Oやネットワーク通信を想定)
            System.out.println("[RealImageLoader] ロード中: " + path);
            this.imageData = "<<" + path + " の画像データ>>";
        }

        @Override
        public String load(String path) {
            return imageData;
        }

        @Override
        public void display() {
            System.out.println("[RealImageLoader] 表示: " + imageData);
        }
    }

    // ---- 仮想プロキシ: 初回 display() まで実オブジェクトの生成を遅らせる ----
    static class LazyImageProxy implements ImageLoader {
        private final String path;
        private RealImageLoader realLoader; // null のまま保持し、必要になるまで生成しない

        public LazyImageProxy(String path) {
            this.path = path;
            // この時点では RealImageLoader を生成しない(遅延ロード)
            System.out.println("[LazyImageProxy] プロキシを作成: " + path);
        }

        @Override
        public String load(String path) {
            if (realLoader == null) {
                realLoader = new RealImageLoader(path);
            }
            return realLoader.load(path);
        }

        @Override
        public void display() {
            // 初回呼び出し時に実オブジェクトを生成する(遅延ロード)
            if (realLoader == null) {
                System.out.println("[LazyImageProxy] 初回: 実オブジェクトを生成します");
                realLoader = new RealImageLoader(path);
            }
            realLoader.display();
        }
    }

    // ---- アクセス制御プロキシ: ロールに基づいてアクセスを制限する ----
    static class AccessControlProxy implements ImageLoader {
        private final ImageLoader delegate; // 委譲先
        private final String userRole;      // ユーザーのロール

        public AccessControlProxy(ImageLoader delegate, String userRole) {
            this.delegate = delegate;
            this.userRole = userRole;
        }

        @Override
        public String load(String path) {
            checkAccess();
            return delegate.load(path);
        }

        @Override
        public void display() {
            checkAccess();
            delegate.display();
        }

        /** アクセス権を確認する。権限がなければ例外をスローする */
        private void checkAccess() {
            if (!"ADMIN".equals(userRole) && !"USER".equals(userRole)) {
                throw new SecurityException(
                        "アクセス拒否: ロール '" + userRole + "' に表示権限がありません");
            }
            System.out.println("[AccessControlProxy] アクセス許可: ロール=" + userRole);
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Proxy パターン: 仮想プロキシ(遅延ロード)===");

        // LazyImageProxy を3つ作成しても、この時点では画像データをロードしない
        ImageLoader img1 = new LazyImageProxy("/images/photo1.jpg");
        ImageLoader img2 = new LazyImageProxy("/images/photo2.jpg");
        ImageLoader img3 = new LazyImageProxy("/images/photo3.jpg");
        System.out.println("\n→ この時点ではまだロードされていません");

        System.out.println("\n--- img1 を表示(初回:ここで初めてロードされる)---");
        img1.display();

        System.out.println("\n--- img1 を再表示(2回目:ロード済みなので重い処理は走らない)---");
        img1.display();

        System.out.println("\n=== Proxy パターン: アクセス制御プロキシ ===");

        // USER ロールはアクセス可能
        ImageLoader userProxy = new AccessControlProxy(
                new LazyImageProxy("/images/secret.jpg"), "USER");
        System.out.println("\n--- USER ロールでアクセス ---");
        userProxy.display();

        // GUEST ロールはアクセス不可
        ImageLoader guestProxy = new AccessControlProxy(
                new LazyImageProxy("/images/secret.jpg"), "GUEST");
        System.out.println("\n--- GUEST ロールでアクセス(拒否される)---");
        try {
            guestProxy.display();
        } catch (SecurityException e) {
            System.out.println("例外キャッチ: " + e.getMessage());
        }
    }
}

Java 8 版では2種類のプロキシを実装しています。LazyImageProxy は初回 display() 呼び出し時だけ RealImageLoader を生成します(仮想プロキシ)。AccessControlProxy はロールを確認してからアクセスを許可します(保護プロキシ)。

よくあるミス・注意点

⚠️ Proxy パターンと Decorator パターンを混同する

両者は構造が似ていますが、目的が異なります。Proxy パターンは「アクセス制御や遅延ロードなど、オブジェクトへのアクセス自体を管理する」ことが目的です。Decorator パターンは「既存オブジェクトに機能を追加する」ことが目的です。 判断の目安: 「このクラスは何かを制御しているか(Proxy)」「機能を追加しているか(Decorator)」

⚠️ 仮想プロキシのスレッドセーフ性に注意する

複数スレッドが同時に仮想プロキシを呼び出すと、realLoader == null のチェックと生成が競合して 実オブジェクトが複数回生成されてしまう可能性があります(二重チェックロッキング問題)。 マルチスレッド環境では synchronizedAtomicReference を使ってスレッドセーフにしましょう。

⚠️ プロキシの存在を意識せず実オブジェクトにキャストしてしまう

プロキシは実オブジェクトと同じインターフェースを実装していますが、instanceof や直接キャストで実クラスを前提にしたコードを書くと、 プロキシを経由したときに ClassCastException が発生します。 常にインターフェースを通じてアクセスするよう統一しましょう。

テストする観点

  • 仮想プロキシが遅延ロードしていること: 生成直後は RealImageLoader のコンストラクタが呼ばれておらず、初回 display() 時だけ呼ばれること
  • 2回目以降の display() ではコンストラクタが呼ばれないこと(ロード済みの実オブジェクトが再利用されること)
  • USER / ADMIN ロールは display() が正常に実行されること
  • 権限のないロール(GUEST など)では SecurityException がスローされること
  • プロキシが実オブジェクトと同じインターフェースを実装しており、クライアントから区別なく使えること
  • Java 17 版: ProxyResult.allowed() と ProxyResult.denied() が正しいフィールド値を持つこと
  • Java 21 版: describeProxy() が各 ProxyType サブタイプに対して正しい説明文を返すこと

GitHub でソースコードを見る →