Javaデザインパターン:プロキシパターンの仕組みと実装

プロキシパターンの概要

プロキシパターン(Proxy Pattern)は、あるオブジェクトへのアクセスを制御するために、そのオブジェクトの代理(代行者)を提供する構造的デザインパターンです。直接的な参照が不適切な場合や制限がある場合に、クライアントとターゲットオブジェクトの間に仲介役として機能します。

一般的なプロキシパターンは以下の3つの役割で構成されます。

  • サブジェクト(Subject)役: リアルサブジェクトとプロキシの共通インターフェースを定義します。インターフェースまたは抽象クラスとして実装されます。
  • リアルサブジェクト(RealSubject)役: プロキシが表す実際のビジネスロジックを持つオブジェクトです。システム内の核心的な処理を実行します。
  • プロキシ(Proxy)役: リアルサブジェクトへの参照を保持し、クライアントからのリクエストを受け取ります。処理の前後に追加のロジック(権限チェック、ログ、遅延初期化など)を挿入し、最終的にリアルサブジェクトのメソッドを呼び出します。

静的プロキシの基礎実装

まず、基本的な静的プロキシの実装例を見てみましょう。ここでは、ファイルシステムへのアクセスをログに記録するシンプルなケースを想定します。

インターフェースの定義:

public interface IFileSystem {
    void accessFile(String filename);
}

実際の処理を行うクラス:

public class RealFileSystem implements IFileSystem {
    public void accessFile(String filename) {
        System.out.println("ファイル '" + filename + "' の内容を読み込みました。");
    }
}

プロキシクラス(アクセス制御とログ機能の追加):

public class SecurityProxy implements IFileSystem {

    private IFileSystem fileSystem;

    public SecurityProxy(IFileSystem fileSystem) {
        this.fileSystem = fileSystem;
    }

    public void accessFile(String filename) {
        if (!authenticate()) {
            System.out.println("アクセス拒否: 認証に失敗しました。");
            return;
        }
        System.out.println("ログ: ファイルアクセス開始 - " + filename);
        fileSystem.accessFile(filename);
        System.out.println("ログ: ファイルアクセス終了");
    }

    private boolean authenticate() {
        // 簡易的な認証ロジック
        return true;
    }
}

クライアントコード:

public class Client {
    public static void main(String[] args) {
        IFileSystem fs = new SecurityProxy(new RealFileSystem());
        fs.accessFile("data.txt");
    }
}

静的プロキシの応用:データソースルーティング

実務的なシナリオとして、データベースのシャーディング(分散データベース)におけるデータソースの動的切り替えを静的プロキシで実装する例を示します。ここでは、売上データを年単位でデータベースを分割して保存するケースを想定します。

エンティティクラス:

public class SalesRecord {
    private String id;
    private Object details;
    private long timestamp; // 作成日時
    // getter, setter 省略
}

データアクセスオブジェクト:

public class SalesRepository {
    public int save(SalesRecord record) {
        System.out.println("SalesRepository: レコードを保存しました。");
        return 1;
    }
}

サービスインターフェースと実装:

public interface ISalesService {
    int createRecord(SalesRecord record);
}

public class SalesService implements ISalesService {
    private SalesRepository repository;

    public SalesService() {
        this.repository = new SalesRepository();
    }

    public int createRecord(SalesRecord record) {
        System.out.println("SalesService: リポジトリへの保存を依頼");
        return repository.save(record);
    }
}

コンテキストホルダー(ThreadLocalを使用したデータソース切り替え):

public class DbContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    public static final String DEFAULT_DS = "DEFAULT";

    public static void setDbKey(String key) {
        contextHolder.set(key);
    }
    
    public static String getDbKey() {
        return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }
}

静的プロキシクラス(年ベースのルーティング処理):

import java.text.SimpleDateFormat;
import java.util.Date;

public class SalesServiceProxy implements ISalesService {
    private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
    private ISalesService target;

    public SalesServiceProxy(ISalesService target) {
        this.target = target;
    }

    public int createRecord(SalesRecord record) {
        before(record);
        int result = target.createRecord(record);
        after();
        return result;
    }

    private void before(SalesRecord record) {
        try {
            Date date = new Date(record.getTimestamp());
            int year = Integer.parseInt(yearFormat.format(date));
            String dsKey = "DB_" + year;
            System.out.println("プロキシ: データソースを [" + dsKey + "] に切り替え");
            DbContextHolder.setDbKey(dsKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void after() {
        DbContextHolder.clear();
        System.out.println("プロキシ: データソースをデフォルトに戻しました");
    }
}

動的プロキシによる実装

静的プロキシでは、インターフェースのメソッドが増えるたびにプロキシクラスも修正する必要があります。JDKの動的プロキシを使用すると、実行時にプロキシクラスを生成できるため、より柔軟性が高まります。先ほどのデータソースルーティングを動的プロキシで書き換えます。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DynamicRoutingProxy implements InvocationHandler {

    private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
    private Object targetObject;

    public Object bind(Object target) {
        this.targetObject = target;
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
                targetObject.getClass().getInterfaces(), this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (args != null && args.length > 0 && args[0] instanceof SalesRecord) {
            SalesRecord record = (SalesRecord) args[0];
            Date date = new Date(record.getTimestamp());
            int year = Integer.parseInt(yearFormat.format(date));
            String dsKey = "DB_" + year;
            System.out.println("動的プロキシ: データソースを [" + dsKey + "] に切り替え");
            DbContextHolder.setDbKey(dsKey);
        }

        Object result = method.invoke(targetObject, args);

        DbContextHolder.clear();
        return result;
    }
}

動的プロキシのテストコード:

public class DynamicProxyTest {
    public static void main(String[] args) {
        ISalesService service = (ISalesService) new DynamicRoutingProxy().bind(new SalesService());
        SalesRecord record = new SalesRecord();
        record.setTimestamp(System.currentTimeMillis());
        service.createRecord(record);
    }
}

JDK動的プロキシの原理

JDKの動的プロキシは、実行時にバイトコードを生成してクラスをロードします。内部的な手順は以下の通りです。

  1. ターゲットオブジェクトのクラスローダーと実装しているインターフェースの情報を取得する。
  2. すべてのインターフェースを実装する新しいプロキシクラスのバイトコードを動的に生成する。
  3. 3>生成されたクラスには、ハンドラー(InvocationHandler)への処理委譲ロジックが含まれる。
  4. 生成されたバイトコードをディスク(またはメモリ)に出力し、クラスローダーを介してJVMにロードする。

生成されたプロキシクラス(例: $Proxy0)は、メソッドが呼ばれると、ハンドラーのinvokeメソッドを呼び出す構造になっています。

自作プロキシの実装(原理の深掘り)

動的プロキシの仕組みを理解するために、簡易的なプロキシ生成クラスを自作してみます。JavaCompiler APIを使用して、ソースコード文字列からコンパイルとクラス生成を行います。

ハンドラーインターフェース:

import java.lang.reflect.Method;

public interface CustomInvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

プロキシ生成クラス:

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class CustomProxy {
    
    private static final String LINE_SEP = System.lineSeparator();

    public static Object newProxyInstance(CustomClassLoader loader, Class[] interfaces, CustomInvocationHandler h) {
        try {
            // 1. ソースコード生成
            String sourceCode = generateSourceCode(interfaces);

            // 2. ソースファイルの保存
            File root = new File(CustomProxy.class.getResource("/").getPath());
            File sourceFile = new File(root, "$Proxy0.java");
            try (FileWriter writer = new FileWriter(sourceFile)) {
                writer.write(sourceCode);
            }

            // 3. コンパイル
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
            Iterable compilationUnits = fileManager.getJavaFileObjects(sourceFile);
            compiler.getTask(null, fileManager, null, null, null, compilationUnits).call();
            fileManager.close();

            // 4. クラスロードとインスタンス化
            Class proxyClass = loader.findClass("$Proxy0");
            Constructor constructor = proxyClass.getConstructor(CustomInvocationHandler.class);
            sourceFile.delete();

            return constructor.newInstance(h);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String generateSourceCode(Class[] interfaces) {
        StringBuilder sb = new StringBuilder();
        sb.append("package " + CustomProxy.class.getPackage().getName() + ";" + LINE_SEP);
        sb.append("import java.lang.reflect.*;" + LINE_SEP);
        sb.append("public class $Proxy0 implements " + interfaces[0].getName() + " {" + LINE_SEP);
        sb.append("  CustomInvocationHandler h;" + LINE_SEP);
        sb.append("  public $Proxy0(CustomInvocationHandler h) { this.h = h; }" + LINE_SEP);

        for (Method m : interfaces[0].getMethods()) {
            sb.append("  public " + m.getReturnType().getName() + " " + m.getName() + "() {" + LINE_SEP);
            sb.append("    try {" + LINE_SEP);
            sb.append("      Method m = " + interfaces[0].getName() + ".class.getMethod(\"" + m.getName() + "\", new Class[]{});" + LINE_SEP);
            sb.append("      Object res = this.h.invoke(this, m, null);" + LINE_SEP);
            if (!m.getReturnType().equals(void.class)) {
                sb.append("      return (" + m.getReturnType().getName() + ")res;" + LINE_SEP);
            }
            sb.append("    } catch(Throwable e) { e.printStackTrace(); }" + LINE_SEP);
            if (m.getReturnType().equals(void.class)) {
                sb.append("    return;" + LINE_SEP);
            } else {
                sb.append("    return null;" + LINE_SEP);
            }
            sb.append("  }" + LINE_SEP);
        }
        sb.append("}" + LINE_SEP);
        return sb.toString();
    }
}

カスタムクラスローダー:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class CustomClassLoader extends ClassLoader {
    private String classPath;

    public CustomClassLoader() {
        String path = CustomClassLoader.class.getResource("/").getPath();
        this.classPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String className = CustomProxy.class.getPackage().getName() + "." + name;
        File file = new File(classPath, name.replaceAll("\\.", "/") + ".class");
        if (!file.exists()) return super.findClass(name);

        try (FileInputStream fis = new FileInputStream(file);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return defineClass(className, bos.toByteArray(), 0, bos.size());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

CGLibによるプロキシ実装

JDKの動的プロキシはインターフェース必須ですが、CGLibはクラスを継承してプロキシを生成するため、インターフェースがない場合でも利用できます。MethodInterceptorインターフェースを使用します。

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class AuditInterceptor implements MethodInterceptor {

    public Object getInstance(Class clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("CGLib Audit: メソッド開始前処理 " + method.getName());
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("CGLib Audit: メソッド終了後処理 " + method.getName());
        return result;
    }
}

ターゲットクラス:

public class TargetService {
    public void execute() {
        System.out.println("TargetService: 実際の処理を実行中");
    }
}

CGLibのFastClassメカニズム

CGLibの実行効率がJDKプロキシ(リフレクション使用)より高い場合がある理由は、FastClassという仕組みにあります。

FastClassは、プロキシクラスとターゲットクラスに対して、それぞれのメソッドに対応するインデックス(int型)を割り当てた補助クラスを生成します。メソッド実行時にはリフレクション(Method.invoke)を使用する代わりに、このインデックスをキーとしてメソッドを直接ディスパッチするため、オーバーヘッドが低減されます。

呼び出しフロー: プロキシオブジェクトのメソッド -> Interceptor.intercept() -> MethodProxy.invokeSuper() -> FastClass.invoke(index) -> ターゲットクラスのメソッド

プロキシ方式の比較

  • JDK動的プロキシ vs CGLib: JDKはインターフェース実装とリフレクションを使用し、CGLibは継承とバイトコード生成(ASM)、およびFastClassインデックス呼び出しを使用します。CGLibは生成時に時間がかかる場合がありますが、実行速度はFastClassにより有利な場合があります。ただし、finalメソッドやクラスにはプロキシを適用できません。
  • 静的プロキシ vs 動的プロキシ: 静的プロキシはコード量が増えやすく、メソッド追加時に修正が必要になる場合があります(開放閉鎖原則に反する可能性)。動的プロキシは実行時に柔軟にロジックを注入できるため、AOP(アスペクト指向プログラミング)などの基盤技術として適しています。

まとめ:プロキシパターンのメリットとデメリット

  • メリット:
    • 責任の分離:ビジネスロジックからアクセス制御やログなどの横断的関心を分離できる。
    • 拡張性:既存のコードを修正せずに機能を追加できる。
    • 保護:ターゲットオブジェクトへの直接アクセスを防ぎ、セキュリティやリソース管理を強化できる。
  • デメリット:
    • 複雑性の増加:クラス数が増え、処理フローが追いにくくなる場合がある。
    • パフォーマンス:プロキシを介するため、直接呼び出しに比べてわずかな遅延が発生する。

タグ: Java Proxy Pattern Design Patterns CGLIB Dynamic Proxy

8月12日 22:31 投稿