Spring AOP のアドバイザーについての話

通常、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);

理解しやすいいくつかの方法を次に示します。

  1. setTarget メソッドは、実際のプロキシ オブジェクトを設定します。なぜこの @Lazy アノテーションが私たちの前に無限ループを破ることができるのでしょうか? この記事に登場する誰もがそれに接触したことがあります。
  2. addInterface、JDK ベースの動的プロキシにはインターフェイスが必要です。このメソッドはプロキシ オブジェクトのインターフェイスを設定します。
  3. addAdvice メソッドは、機能拡張/アドバイスを追加します。
  4. 最後に、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 つの判断が行われます。

  1. 現在のプロキシ オブジェクトにインターフェイスがない場合は、直接 true を返します。
  2. 現在のプロキシ オブジェクトにはインターフェイスがありますが、そのインターフェイスが 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 つのメソッドがあり、名前を見ればその意味が推測できるでしょう。

  1. getClassFilter: これはクラス フィルターであり、インターセプトするクラスを選択できます。
  2. 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.TRUEDefaultPointcutAdvisor が初期化されると set 、つまりすべてのクラスのすべてのメソッドがインターセプトされることがわかります。つまり、Advice は最終的に Advisor に変換されます。

おすすめ

転載: blog.csdn.net/agonie201218/article/details/132001097