ProxyFactoryBean を使用して AOP プロキシを作成する

ビジネスオブジェクトに Spring IoC コンテナーApplicationContext または BeanFactory)を使用している場合(そしてそうあるべきです! )、Spring の AOP FactoryBean 実装のいずれかを使用する必要があります。(ファクトリ Bean は間接化のレイヤーを導入し、異なる型のオブジェクトを作成できることを思い出してください。)

Spring AOP サポートは、カバーにあるファクトリ Bean も使用します。

Spring で AOP プロキシを作成する基本的な方法は、org.springframework.aop.framework.ProxyFactoryBean を使用することです。これにより、ポイントカット、適用されるアドバイス、それらの順序を完全に制御できます。ただし、このような制御が必要ない場合は、より簡単なオプションがあります。

基本

ProxyFactoryBean は、他の Spring FactoryBean 実装と同様に、間接的なレベルを導入します。foo という名前の ProxyFactoryBean を定義すると、foo を参照するオブジェクトには ProxyFactoryBean インスタンス自体は表示されませんが、ProxyFactoryBean の getObject() メソッドの実装によって作成されたオブジェクトは表示されます。このメソッドは、ターゲットオブジェクトをラップする AOP プロキシを作成します。

ProxyFactoryBean または別の IoC 対応クラスを使用して AOP プロキシを作成することの最も重要な利点の 1 つは、アドバイスとポイントカットも IoC で管理できることです。これは強力な機能であり、他の AOP フレームワークでは実現が難しい特定のアプローチを可能にします。例: アドバイス自体がアプリケーションオブジェクト (任意の AOP フレームワークで利用可能である必要があるターゲット以外) を参照する場合があり、依存性注入によって提供されるすべてのプラグ可能性の恩恵を受けます。

JavaBean のプロパティ

Spring で提供されるほとんどの FactoryBean 実装と共通して、ProxyFactoryBean クラス自体は JavaBean です。そのプロパティは次の目的で使用されます。

一部の主要なプロパティは、org.springframework.aop.framework.ProxyConfig (Spring のすべての AOP プロキシファクトリのスーパークラス)から継承されます。これらの主要なプロパティには次のものが含まれます。

  • proxyTargetClasstrue は、ターゲットクラスのインターフェースではなく、ターゲットクラスをプロキシする場合。このプロパティ値が true に設定されている場合、CGLIB プロキシが作成されます(ただし、JDK および CGLIB ベースのプロキシも参照してください)。

  • optimize: CGLIB を介して作成されたプロキシに積極的な最適化を適用するかどうかを制御します。関連する AOP プロキシが最適化を処理する方法を完全に理解していない限り、この設定を気軽に使用しないでください。これは現在、CGLIB プロキシにのみ使用されます。JDK 動的プロキシでは効果がありません。

  • frozen: プロキシ構成が frozen の場合、構成の変更は許可されなくなりました。これは、わずかな最適化としても、プロキシの作成後に発信者が(Advised インターフェースを介して)プロキシを操作できないようにする場合にも役立ちます。このプロパティのデフォルト値は false であるため、変更(アドバイスの追加など)が許可されます。

  • exposeProxy: ターゲットがアクセスできるように、現在のプロキシを ThreadLocal で公開するかどうかを決定します。ターゲットがプロキシを取得する必要があり、exposeProxy プロパティが true に設定されている場合、ターゲットは AopContext.currentProxy() メソッドを使用できます。

ProxyFactoryBean に固有のその他のプロパティには、次のものがあります。

  • proxyInterfacesString インターフェース名の配列。これが提供されない場合、ターゲットクラスの CGLIB プロキシが使用されます(ただし、JDK および CGLIB ベースのプロキシも参照してください)。

  • interceptorNamesAdvisor の String 配列、インターセプター、適用する他のアドバイス名。先着順でオーダーは重要です。つまり、リスト内の最初のインターセプターが呼び出しをインターセプトできる最初のインターセプターです。

    名前は、祖先ファクトリからの Bean 名を含む、現在のファクトリの Bean 名です。ここで Bean 参照についてメンションすることはできません。そのようにすると、ProxyFactoryBean はアドバイスのシングルトン設定を無視することになります。

    インターセプター名にアスタリスク(*)を追加できます。これにより、適用されるアスタリスクの前の部分で始まる名前を持つすべてのアドバイザ Bean が適用されます。この機能の使用例は「グローバル」アドバイザの使用にあります。

  • シングルトン: getObject() メソッドが呼び出される頻度に関係なく、ファクトリが単一のオブジェクトを返す必要があるかどうか。いくつかの FactoryBean 実装では、このような方法が提供されています。デフォルト値は true です。ステートフルアドバイスを使用する場合 (たとえば、ステートフルミックスインの場合) は、プロトタイプアドバイスをシングルトン値 false と共に使用します。

JDK および CGLIB ベースのプロキシ

このセクションは、ProxyFactoryBean が特定のターゲットオブジェクト(プロキシされる)に対して JDK ベースのプロキシまたは CGLIB ベースのプロキシを作成する方法の決定的なドキュメントとして機能します。

JDK または CGLIB ベースのプロキシの作成に関する ProxyFactoryBean の動作は、Spring のバージョン 1.2.x および 2.0 の間で変更されました。ProxyFactoryBean は、TransactionProxyFactoryBean クラスのインターフェースと同様に、インターフェースの自動検出に関して同様のセマンティクスを示します。

プロキシされるターゲットオブジェクトのクラス(以降、単にターゲットクラスと呼びます)がインターフェースを実装しない場合、CGLIB ベースのプロキシが作成されます。これは最も簡単なシナリオです。JDK プロキシはインターフェースベースであり、インターフェースがないということは、JDK プロキシが不可能であることを意味するためです。ターゲット Bean をプラグインし、interceptorNames プロパティを設定してインターセプターのリストを指定できます。ProxyFactoryBean の proxyTargetClass プロパティが false に設定されている場合でも、CGLIB ベースのプロキシが作成されることに注意してください。(そうすることは意味がなく、Bean 定義から削除するのが最善です。これは、せいぜい冗長であり、最悪の場合混乱を招くからです。)

ターゲットクラスが 1 つ(または複数)のインターフェースを実装する場合、作成されるプロキシの型は ProxyFactoryBean の構成に依存します。

ProxyFactoryBean の proxyTargetClass プロパティが true に設定されている場合、CGLIB ベースのプロキシが作成されます。これは理にかなっており、最小限の驚きの原則に沿っています。ProxyFactoryBean の proxyInterfaces プロパティが 1 つ以上の完全修飾インターフェース名に設定されている場合でも、proxyTargetClass プロパティが true に設定されているという事実により、CGLIB ベースのプロキシが有効になります。

ProxyFactoryBean の proxyInterfaces プロパティが 1 つ以上の完全修飾インターフェース名に設定されている場合、JDK ベースのプロキシが作成されます。作成されたプロキシは、proxyInterfaces プロパティで指定されたすべてのインターフェースを実装します。ターゲットクラスがたまたま proxyInterfaces プロパティで指定されたインターフェースよりもはるかに多くのインターフェースを実装している場合、すべて良好ですが、それらの追加インターフェースは返されたプロキシによって実装されません。

ProxyFactoryBean の proxyInterfaces プロパティが設定されていないが、ターゲットクラスが 1 つ(または複数)のインターフェースを実装している場合、ProxyFactoryBean はターゲットクラスが実際に少なくとも 1 つのインターフェースと JDK ベースのプロキシを実装するという事実を自動検出します。創造された。実際にプロキシされるインターフェースは、ターゲットクラスが実装するすべてのインターフェースです。実際、これは、ターゲットクラスが proxyInterfaces プロパティに実装するすべてのインターフェースのリストを提供することと同じです。ただし、作業が大幅に少なくなり、誤植が発生しにくくなります。

プロキシインターフェース

ProxyFactoryBean の実際の簡単な例を考えてみましょう。この例には以下が含まれます。

  • プロキシされるターゲット Bean。これは、この例の personTarget Bean 定義です。

  • アドバイスを提供するために使用される Advisor および Interceptor

  • ターゲットオブジェクト (personTarget Bean)、プロキシへのインターフェース、適用するアドバイスを指定するための AOP プロキシ Bean 定義。

次のリストに例を示します。

<bean id="personTarget" class="com.mycompany.PersonImpl">
	<property name="name" value="Tony"/>
	<property name="age" value="51"/>
</bean>

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
	<property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>

<bean id="person"
	class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces" value="com.mycompany.Person"/>

	<property name="target" ref="personTarget"/>
	<property name="interceptorNames">
		<list>
			<value>myAdvisor</value>
			<value>debugInterceptor</value>
		</list>
	</property>
</bean>

interceptorNames プロパティは String のリストを取得することに注意してください。String は、現在のファクトリのインターセプターまたはアドバイザーの Bean 名を保持しています。アドバイザ、インターセプタを使用して、戻る前、後、アドバイスオブジェクトをスローできます。アドバイザーの順序は重要です。

リストに Bean 参照が含まれていない理由を疑問に思うかもしれません。これは、ProxyFactoryBean のシングルトンプロパティが false に設定されている場合、独立したプロキシインスタンスを返すことができる必要があるためです。いずれかのアドバイザ自体がプロトタイプである場合、独立したインスタンスを返す必要があるため、ファクトリからプロトタイプのインスタンスを取得できる必要があります。参照を保持するだけでは不十分です。

前述の person Bean 定義は、次のように Person 実装の代わりに使用できます。

  • Java

  • Kotlin

Person person = (Person) factory.getBean("person");
val person = factory.getBean("person") as Person

同じ IoC コンテキスト内の他の Bean は、通常の Java オブジェクトと同様に、強く型付けされた依存関係を表現できます。次の例は、その方法を示しています。

<bean id="personUser" class="com.mycompany.PersonUser">
	<property name="person"><ref bean="person"/></property>
</bean>

この例の PersonUser クラスは、型 Person のプロパティを公開します。懸念される限り、AOP プロキシは「実際の」人の実装の代わりに透過的に使用できます。ただし、そのクラスは動的プロキシクラスになります。Advised インターフェースにキャストすることは可能です(後で説明します)。

匿名の内部 Bean を使用して、ターゲットとプロキシの区別を隠すことができます。ProxyFactoryBean 定義のみが異なります。アドバイスは完全を期すためにのみ含まれています。次の例は、匿名の内部 Bean の使用方法を示しています。

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
	<property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces" value="com.mycompany.Person"/>
	<!-- Use inner bean, not local reference to target -->
	<property name="target">
		<bean class="com.mycompany.PersonImpl">
			<property name="name" value="Tony"/>
			<property name="age" value="51"/>
		</bean>
	</property>
	<property name="interceptorNames">
		<list>
			<value>myAdvisor</value>
			<value>debugInterceptor</value>
		</list>
	</property>
</bean>

匿名の内部 Bean を使用すると、型 Person のオブジェクトが 1 つしかないという利点があります。これは、アプリケーションコンテキストのユーザーが非推奨オブジェクトへの参照を取得できないようにする場合、または Spring IoC オートワイヤーのあいまいさを回避する必要がある場合に役立ちます。また、ほぼ間違いなく、ProxyFactoryBean 定義が自己完結しているという利点もあります。ただし、ファクトリから推奨されていないターゲットを取得できることが実際に有利な場合があります(たとえば、特定のテストシナリオ)。

プロキシクラス

1 つ以上のインターフェースではなく、クラスをプロキシする必要がある場合はどうなるでしょうか?

前の例では、Person インターフェースがなかったと想像してください。ビジネスインターフェースを実装していない Person というクラスをアドバイスする必要がありました。この場合、動的プロキシではなく CGLIB プロキシを使用するように Spring を構成できます。これを行うには、前述の ProxyFactoryBean の proxyTargetClass プロパティを true に設定します。クラスではなくインターフェースにプログラミングするのが最善ですが、インターフェースを実装しないクラスにアドバイスする機能は、レガシーコードを使用する場合に役立ちます。(一般的に、Spring は規範的ではありません。優れたプラクティスを簡単に適用できますが、特定のアプローチを強制することは避けます。)

必要に応じて、インターフェースがある場合でも、CGLIB の使用を強制できます。

CGLIB プロキシは、実行時にターゲットクラスのサブクラスを生成することにより機能します。Spring は、この生成されたサブクラスを構成して、メソッド呼び出しを元のターゲットに委譲します。サブクラスは、アドバイスに織り込むデコレーターパターンを実装するために使用されます。

CGLIB プロキシは通常、ユーザーに対して透過的である必要があります。ただし、考慮すべき課題がいくつかあります。

  • final クラスは拡張できないため、プロキシできません。

  • final メソッドはオーバーライドできないため、アドバイスできません。

  • private メソッドはオーバーライドできないため、アドバイスできません。

  • 表示されないメソッド (通常、別のパッケージから親クラスにプライベートメソッドをパッケージ化) は、事実上プライベートであるため、推奨できません。

クラスパスに CGLIB を追加する必要はありません。CGLIB は再パッケージ化され、spring-core JAR に含まれています。つまり、CGLIB ベースの AOP は、JDK 動的プロキシと同様に、「すぐに」動作します。

CGLIB プロキシと動的プロキシのパフォーマンスの違いはほとんどありません。この場合、パフォーマンスは決定的な考慮事項ではありません。

「グローバル」アドバイザの使用

インターセプター名にアスタリスクを追加すると、アスタリスクの前の部分に一致する Bean 名を持つすべてのアドバイザーがアドバイザーチェーンに追加されます。これは、「グローバル」アドバイザの標準セットを追加する必要がある場合に役立ちます。次の例では、2 つのグローバルアドバイザーを定義しています。

<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="target" ref="service"/>
	<property name="interceptorNames">
		<list>
			<value>global*</value>
		</list>
	</property>
</bean>

<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>