通常、AOPをプロジェクトに関わる場合、基本的には宣言型の設定になりますが、XMLベースの設定であっても、Javaコードベースの設定であっても、シンプルな設定で利用することができます。宣言型構成の利点の 1 つは、ソース コードへの侵入がほとんどない、またはまったくないことです。しかし今日、ソング兄弟は友達と AOP プログラミングについて話したいと考えています。なぜこのトピックについて話したいのですか? Springのソースコードでは、このように最下層でプロキシオブジェクトを作成しているため、AOPをプログラムで開発できる方は、Springで該当のソースコードを見るとよく理解できると思います。
1. 基本的な使い方
1.1 JDK ベースの AOP
まず、JDK 動的プロキシに基づく AOP を見てみましょう。
次のような電卓インターフェイスがあるとします。
public interface ICalculator {
void add(int a, int b);
int minus(int a, int b);
}
次に、このインターフェースの実装クラスを提供します。
public class CalculatorImpl implements ICalculator {
@Override
public void add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
}
@Override
public int minus(int a, int b) {
return a - b;
}
}
ここで、プログラムによる方法を使用してプロキシ オブジェクトを生成するとします。コードは次のようになります。
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvice(new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name+" 方法开始执行了。。。");
Object proceed = invocation.proceed();
System.out.println(name+" 方法执行结束了。。。");
return proceed;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.add(3, 4);
理解しやすいいくつかの方法を次に示します。
- setTarget メソッドは、実際のプロキシ オブジェクトを設定します。なぜこの @Lazy アノテーションが私たちの前に無限ループを破ることができるのでしょうか? この記事に登場する誰もがそれに接触したことがあります。
- addInterface、JDK ベースの動的プロキシにはインターフェイスが必要です。このメソッドはプロキシ オブジェクトのインターフェイスを設定します。
- addAdvice メソッドは、機能拡張/アドバイスを追加します。
- 最後に、getProxy メソッドを通じてプロキシ オブジェクトを取得し、実行します。
最終的な印刷結果は次のようになります。
1.2 CGLIB に基づく AOP
プロキシされたオブジェクトにインターフェイスがない場合、CGLIB に基づいた動的プロキシを通じてプロキシ オブジェクトを生成できます。
次のようなクラスがあるとします。
public class UserService {
public void hello() {
System.out.println("hello javaboy");
}
}
このクラスのプロキシ オブジェクトを生成するには、次のようにします。
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new UserService());
proxyFactory.addAdvice(new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String name = invocation.getMethod().getName();
System.out.println(name+" 方法开始执行了。。。");
Object proceed = invocation.proceed();
System.out.println(name+" 方法执行结束了。。。");
return proceed;
}
});
UserService us = (UserService) proxyFactory.getProxy();
us.hello();
実際、理由は非常に単純で、インターフェイスがない場合はインターフェイスを設定しないだけです。
1.3 ソースコード分析
上記のプロキシ オブジェクトを生成する getProxy メソッドでは、最終的に createAopProxy メソッドが実行され、インターフェイスの有無に応じて JDK 動的プロキシと CGLIB 動的プロキシのどちらを使用するかを決定します。
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
このソースコードからわかるように、インターフェースがある場合は JDK ダイナミック プロキシ、インターフェースがない場合は CGLIB ダイナミック プロキシです。ただし、一番上に if 判定があり、この判定には 3 つの条件があるので、友達と話しましょう。
config.isOptimize()
最適化が必要かどうかを判断する方法です。なぜなら、従来、CGLIB ダイナミック プロキシのパフォーマンスが JDK ダイナミック プロキシよりも高いと誰もが考えているからです。しかし、JDK バージョンは近年非常に速く更新されており、現在では 2 つのダイナミック プロキシのパフォーマンスの差は大きくありません。このプロパティが true に設定されている場合、システムはインターフェイスがあるかどうかを判断し、インターフェイスがある場合は JDK によって動的にプロキシされ、インターフェイスがない場合は CGLIB によって動的にプロキシされます。
このプロパティを設定する必要がある場合は、次のコードを通じて設定できます。
proxyFactory.setOptimize(true);
config.isProxyTargetClass()
この属性の機能も同様です。AOP を使用する場合、この属性を設定することがあります。この属性を true に設定すると、if 分岐に入りますが、if 分岐内の if は満たされないはずなので、一般的には次に、このプロパティが true に設定されている場合、インターフェイスの有無に関係なく、CGLIB 動的プロキシが使用されることを意味します。このプロパティが false の場合、インターフェイスがある場合は JDK 動的プロキシが使用され、インターフェイスがない場合は CGLIB 動的プロキシが使用されます。
hasNoUserSuppliedProxyInterfaces(構成)
この方法では主に次の 2 つの判断が行われます。
- 現在のプロキシ オブジェクトにインターフェイスがない場合は、直接 true を返します。
- 現在のプロキシ オブジェクトにはインターフェイスがありますが、そのインターフェイスが SpringProxy の場合は true を返します。
基本的に true を返すと CGLIB 動的プロキシを使用することを意味し、false を返すと JDK 動的プロキシを使用することを意味します。
JDK に基づく動的プロキシの場合、最後の呼び出しは次のように JdkDynamicAopProxy#getProxy() メソッドです。
@Override
public Object getProxy() {
return getProxy(ClassUtils.getDefaultClassLoader());
}
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);
}
Proxy.newProxyInstance
これは JDK の動的プロキシであり、理解しやすいものです。
CGLIB に基づく動的プロキシの場合、最後の呼び出しは次のように CglibAopProxy#getProxy() メソッドです。
@Override
public Object getProxy() {
return buildProxy(null, false);
}
private Object buildProxy(@Nullable ClassLoader classLoader, boolean classOnly) {
try {
Class<?> rootClass = this.advised.getTargetClass();
Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");
Class<?> proxySuperClass = rootClass;
if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
proxySuperClass = rootClass.getSuperclass();
Class<?>[] additionalInterfaces = rootClass.getInterfaces();
for (Class<?> additionalInterface : additionalInterfaces) {
this.advised.addInterface(additionalInterface);
}
}
// Validate the class, writing log messages as necessary.
validateClassIfNecessary(proxySuperClass, classLoader);
// Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader smartClassLoader &&
smartClassLoader.isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(true);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);
// Generate the proxy class and create a proxy instance.
return (classOnly ? createProxyClass(enhancer) : createProxyClassAndInstance(enhancer, callbacks));
}
catch (CodeGenerationException | IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}
JDK を直接使用して動的プロキシ オブジェクトを作成するコードと、CGLIB を直接使用して動的プロキシ オブジェクトを作成するコードについては、あまり紹介しません。これらは基本的な使用法です。ブラザー ソングは、以前に記録した無料の SSM 入門チュートリアルで友人と共有しました。すでに述べたので、ここでは繰り返しません。
2.アドバイザー
2.1 アドバイザー
アドバイザー = ポイントカット + アドバイス。
前のケースでは、Advice のみを設定し、Pointcut を設定しなかったので、最終的にすべてのメソッドがインターセプトされます。
必要に応じて、アドバイザを直接設定して、どのメソッドをインターセプトする必要があるかを指定できます。
まずアドバイザーの定義を見てみましょう。
public interface Advisor {
Advice EMPTY_ADVICE = new Advice() {};
Advice getAdvice();
boolean isPerInstance();
}
ご覧のとおり、ここで重要なのは、通知/拡張機能を取得するために使用される getAdvice メソッドです。もう 1 つの isPerInstance は現在使用されておらず、デフォルトで true を返すことができます。実際には、そのサブクラスにより注意を払います。
public interface PointcutAdvisor extends Advisor {
Pointcut getPointcut();
}
このサブクラスには追加の getPointcut メソッドがあり、PointcutAdvisor のインターフェイスは、Advisor (Pointcut+Advice) の役割をよく説明しています。
2.2 ポイントカット
Pointcut には多くの実装クラスがあります。
興味深いものを 2 つ選んで話します。他のものは実際にはほとんど同じです。
2.2.1 ポイントカット
まず、このインターフェイスを見てみましょう。
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
インターフェイスには 2 つのメソッドがあり、名前を見ればその意味が推測できるでしょう。
- getClassFilter: これはクラス フィルターであり、インターセプトするクラスを選択できます。
- MethodMatcher: これはメソッド フィルターであり、インターセプトする必要があるメソッドを選択できます。
ClassFilter 自体については、実際には理解するのが簡単です。
@FunctionalInterface
public interface ClassFilter {
boolean matches(Class<?> clazz);
ClassFilter TRUE = TrueClassFilter.INSTANCE;
}
Matches メソッドの場合、Class オブジェクトを渡して比較を実行します。true を返すとインターセプトすることを意味し、false を返すとインターセプトしないことを意味します。
次のような MethodMatcher も同様です。
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method method, Class<?> targetClass, Object... args);
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}
ここには 3 つのメソッドがあり、そのうち 2 つは照合用のmatches メソッドで、isRuntime メソッドが true を返すと、args パラメータを指定した 2 つ目のmatches メソッドが実行されます。
単純な使用例として、すべてのメソッドをインターセプトしたいとします。その場合、次のように定義できます。
public class AllClassAndMethodPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return MethodMatcher.TRUE;
}
}
これらは付属の 2 つの定数であり、すべてのクラスとすべてのメソッドをインターセプトすることを意味します。
CalculatorImpl クラスの add メソッドをインターセプトしたい場合は、次のように定義できます。
public class ICalculatorAddPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return clazz.getName().equals("org.javaboy.bean.aop.CalculatorImpl");
}
};
}
@Override
public MethodMatcher getMethodMatcher() {
NameMatchMethodPointcut matcher = new NameMatchMethodPointcut();
matcher.addMethodName("add");
return matcher;
}
}
2.2.2 AspectJExpressionPointcut
通常は AOP を作成し、式を通じてアスペクトを定義することがより一般的です。そのため、ここではクラスである AspectJExpressionPointcut を使用できるため、次のように新しいクラスを継承せずに直接使用できます。
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* org.javaboy.bean.aop.ICalculator.add(..))");
上記のポイントは、ICalculator クラスの add メソッドをインターセプトすることを意味します。
2.3 アドバイス
これは、拡張/通知と言うのは簡単ですが、この記事のセクション 1.1 と 1.2 で実証済みなので、詳細は説明しません。
2.4 アドバイザーの実践
次に、ソング兄弟は事例を使用して、アドバイザーを友達に追加する方法を示します。
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
@Override
public Pointcut getPointcut() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* org.javaboy.bean.aop.ICalculator.add(..))");
return pointcut;
}
@Override
public Advice getAdvice() {
return new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name + " 方法开始执行了。。。");
Object proceed = invocation.proceed();
System.out.println(name + " 方法执行结束了。。。");
return proceed;
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.add(3, 4);
calculator.minus(3, 4);
getPointcut メソッドでは、セクション 3.2 のさまざまなカット ポイントを返すことができますが、それらはすべて OK で問題ありません。getAdvice は、前に定義した通知です。
実際、この記事のセクション 1.1 と 1.2 では、Advisor を設定せずに Advice を直接追加しました。追加した Advice は内部で自動的に Advisor に変換されました。関連するソース コードは次のとおりです。
@Override
public void addAdvice(Advice advice) throws AopConfigException {
int pos = this.advisors.size();
addAdvice(pos, advice);
}
/**
* Cannot add introductions this way unless the advice implements IntroductionInfo.
*/
@Override
public void addAdvice(int pos, Advice advice) throws AopConfigException {
if (advice instanceof IntroductionInfo introductionInfo) {
addAdvisor(pos, new DefaultIntroductionAdvisor(advice, introductionInfo));
}
else if (advice instanceof DynamicIntroductionAdvice) {
// We need an IntroductionAdvisor for this kind of introduction.
throw new AopConfigException("DynamicIntroductionAdvice may only be added as part of IntroductionAdvisor");
}
else {
addAdvisor(pos, new DefaultPointcutAdvisor(advice));
}
}
友人は、渡した Advice オブジェクトが最終的に DefaultPointcutAdvisor オブジェクトに変換され、addAdvisor メソッドが呼び出されて追加されることがわかります。
public DefaultPointcutAdvisor(Advice advice) {
this(Pointcut.TRUE, advice);
}
Pointcut.TRUE
DefaultPointcutAdvisor が初期化されると set 、つまりすべてのクラスのすべてのメソッドがインターセプトされることがわかります。つまり、Advice は最終的に Advisor に変換されます。