概要

画像の遅延ロード、API 呼び出しのキャッシュ、権限に基づくアクセス制御――これらは「実オブジェクトの手前に代理を置く」ことで実現できます。Proxy パターンは、Subject インターフェースを共有する代理オブジェクト(Proxy)が実オブジェクト(RealSubject)へのアクセスを仲介し、追加の制御を行う構造です。用途に応じて、仮想プロキシ(遅延初期化)、保護プロキシ(アクセス制御)、リモートプロキシ(ネットワーク越しの呼び出し)に分類されます。この記事では画像ローダーを題材に、仮想プロキシとアクセス制御プロキシの2つを実装し、Decorator パターンとの違い、Java 標準の java.lang.reflect.Proxy との関連を整理します。

使いどころ

画像ビューアで表示されるまでロードを遅延し、スクロールして見える範囲に入ったときだけ実体を生成する

ユーザーのロール(ADMIN / USER / GUEST)に応じてリソースへのアクセスを制御し、権限がなければ操作を拒否する

外部 API の呼び出し結果をキャッシュするプロキシを挟み、一定時間内の再呼び出しではキャッシュを返す

コード例

ProxyPatternSample.java
public class ProxyPatternSample {

    // アクセス結果を record で表現(Java 17+)
    record ProxyResult(boolean allowed, String message) {
        static ProxyResult ok(String msg) {
            return new ProxyResult(true, msg);
        }
        static ProxyResult denied(String reason) {
            return new ProxyResult(false, "拒否: " + reason);
        }
    }

    // Subject インターフェース
    interface ImageLoader {
        void display();
    }

    // RealSubject
    static class RealImageLoader implements ImageLoader {
        private final String path;
        public RealImageLoader(String path) {
            this.path = path;
            System.out.println("[Real] ロード: " + path);
        }
        @Override
        public void display() {
            System.out.println("[Real] 表示: " + path);
        }
    }

    // 仮想プロキシ: 遅延初期化
    static class LazyImageProxy implements ImageLoader {
        private final String path;
        private RealImageLoader real;

        public LazyImageProxy(String path) {
            this.path = path;
            System.out.println("[Proxy] 作成: " + path);
        }

        @Override
        public void display() {
            if (real == null) {
                real = new RealImageLoader(path);
            }
            real.display();
        }
    }

    // アクセス制御プロキシ
    static class AccessControlProxy implements ImageLoader {
        private final ImageLoader delegate;
        private final String userRole;

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

        @Override
        public void display() {
            var result = checkAccess();
            if (!result.allowed()) {
                throw new SecurityException(result.message());
            }
            System.out.println("[AccessProxy] 許可: " + result.message());
            delegate.display();
        }

        private ProxyResult checkAccess() {
            if ("ADMIN".equals(userRole) || "USER".equals(userRole)) {
                return ProxyResult.ok("ロール=" + userRole);
            }
            return ProxyResult.denied("権限なし: " + userRole);
        }
    }

    public static void main(String[] args) {
        // 仮想プロキシ
        var img = new LazyImageProxy("/images/photo.jpg");
        System.out.println("-- まだロードされていない --");
        img.display(); // ここでロード
        img.display(); // キャッシュ済み

        System.out.println();

        // アクセス制御プロキシ
        var secured = new AccessControlProxy(
            new LazyImageProxy("/images/secret.jpg"), "ADMIN");
        secured.display();

        try {
            var guest = new AccessControlProxy(
                new LazyImageProxy("/images/secret.jpg"), "GUEST");
            guest.display();
        } catch (SecurityException e) {
            System.out.println("例外: " + e.getMessage());
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でアクセス制御の結果を型安全に表現できる。var で Proxy の呼び出しコードが簡潔になる。

Java 17
// Java 17: var + record でアクセス結果を管理
var proxy = new LazyImageProxy("/images/photo.jpg");
proxy.display();

record ProxyResult(boolean allowed, String message) {
    static ProxyResult ok(String msg) {
        return new ProxyResult(true, msg);
    }
}
var result = ProxyResult.ok("ロール=ADMIN");

Library Comparison

標準 API(interface + 委譲)遅延初期化やアクセス制御の対象が限られ、静的にプロキシクラスを定義できるとき。対象クラスごとにプロキシクラスを書く必要がある。
java.lang.reflect.Proxyインターフェースの動的プロキシを実行時に生成したいとき。AOP 的な横断関心事の実装に使える。インターフェースにしか適用できない。パフォーマンスも静的プロキシに劣る。
CGLIB / ByteBuddyクラスベースの動的プロキシが必要なとき。Spring AOP の内部でも使われている。バイトコード生成を伴うため、デバッグやトレースが複雑になる。

注意点

Proxy と Decorator は構造が似ているが、Proxy はアクセス制御や遅延初期化が目的、Decorator は機能追加が目的。設計意図を明確にしないと混同しやすい

仮想プロキシの遅延初期化はスレッドセーフでない場合がある。マルチスレッド環境では synchronized や volatile を使った二重チェックが必要

アクセス制御プロキシで SecurityException を投げる場合、呼び出し側での例外処理を忘れないこと

java.lang.reflect.Proxy は動的プロキシを生成する標準 API だが、インターフェースにしか適用できない。クラスの動的プロキシには CGLIB などが必要

FAQ

Proxy パターンと Decorator パターンはどう違いますか。

Proxy はアクセス制御・遅延初期化など「実オブジェクトへのアクセスを仲介する」のが目的です。Decorator は「機能を追加する」のが目的で、構造は似ていますが設計意図が異なります。

仮想プロキシの遅延初期化はスレッドセーフですか。

単純な null チェックだけではスレッドセーフではありません。マルチスレッド環境では synchronized ブロックか volatile + double-checked locking を使ってください。

java.lang.reflect.Proxy はどのような場面で使いますか。

AOP 的な横断関心事(ログ・トランザクション・認証)を動的に適用したいときに使います。Spring AOP の内部でも同じ仕組みが使われています。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます