Spring AOP の内部動作を明らかにする

王林
リリース: 2024-09-07 06:34:36
オリジナル
611 人が閲覧しました

Unveiling the Inner Workings of Spring AOP

この投稿では、Spring のアスペクト指向プログラミング (AOP) の内部メカニズムをわかりやすく説明します。多くの場合「魔法」の一種と考えられるロギングなどの機能を AOP がどのように実現するかを理解することに焦点を当てます。コア Java 実装を実際に見てみると、本当に魔法のようなものではなく、Java のリフレクション、プロキシ パターン、アノテーションがすべてであることがわかります。

前提条件

  • Java コア プロキシ API
  • リフレクション API
  • アノテーション API

これらはすべて、java.lang.reflect、java.lang.annotation、および javassist.util.proxy パッケージの一部です。

コアメカニズム

Spring AOP の中心には、プロキシ オブジェクト、メソッド インターセプタ、およびリフレクションの概念があります。このパターンの中心となるのは、MethodHandler (または呼び出しハンドラー) です。このハンドラーは、メソッド呼び出しをインターセプトすることによって、プロキシ オブジェクトの動作を制御します。メソッドがプロキシ上で呼び出されると、そのメソッドはハンドラーを介して渡され、リフレクションを介してアノテーションをイントロスペクトできます。適用されたアノテーションに基づいて、必要なロジック (ログなど) を例外の前、後、または例外時に実行できます。

分解する

  1. プロキシ オブジェクト: これらは、実際のビジネス オブジェクトの代わりに動的に作成されるオブジェクトであり、メソッド ハンドラーを通じてメソッド呼び出しをルーティングします。
  2. 呼び出しハンドラー: ここでインターセプトの魔法が起こります。リフレクションを使用すると、ハンドラーはターゲット メソッドに存在するアノテーションを調べ、それに応じて動作を変更できます。
  3. カスタム アノテーション: ロギング、セキュリティ チェック、トランザクション管理などの追加機能をトリガーするマーカーとして機能するカスタム アノテーションを定義できます。

例: 特定のメソッドの実行の前後にログを追加するとします。あらゆる場所でログをハードコーディングする代わりに、メソッドに @BeforeMethod と @AfterMethod の注釈を付けることができます。私たちのハンドラーは、このアノテーションのメソッドを検査し、適切なロギング ロジックを動的に追加します。

以下は、この例のコントローラーとサービスがどのように見えるかを示すクラスです。

WorkerController.java

package edu.pk.poc.aop.controller;

import edu.pk.poc.aop.annotation.AfterMethod;
import edu.pk.poc.aop.annotation.All;
import edu.pk.poc.aop.annotation.BeforeMethod;
import edu.pk.poc.aop.helper.ProxyFactory;
import edu.pk.poc.aop.service.Worker;
import edu.pk.poc.aop.service.WorkerService;
import edu.pk.poc.aop.service.WorkerServiceImpl;

public class WorkerController {
    WorkerService workerService = ProxyFactory.createProxy(WorkerServiceImpl.class);
    /**
     * This Method 1s annotated with @BeforeMethod and @AfterMethod, So the log statements
     * will be generated before and after method call.
     */
    @BeforeMethod
    @AfterMethod
    public void engageFullTimeWorker() throws Exception {
        Worker fullTimeWorker = new Worker();
        fullTimeWorker.setName("FullTime-Worker");
        fullTimeWorker.setPartTime(false);
        fullTimeWorker.setDuration(9);
        workerService.doWork(fullTimeWorker);
    }
    /**
     * This Method is annotated with @All, So the log statements will be generated before and after method call
     * along with exception if raised.
     */
    @All
    public void engagePartTimeWorker() throws Exception {
        Worker partTimeWorker = new Worker();
        partTimeWorker.setName("PartTime-Worker");
        partTimeWorker.setPartTime(true);
        partTimeWorker.setDuration(4);
        workerService.doWork(partTimeWorker);
    }
}
ログイン後にコピー

WorkerServiceImpl.java

package edu.pk.poc.aop.service;

import edu.pk.poc.aop.annotation.AfterMethod;

public class WorkerServiceImpl implements WorkerService {
    /**
     * Here this method is annotated with only @AfterMethod, So only log statement
     * will be generated after method call
     */
    @AfterMethod
    @Override
    public void doWork(Worker worker) throws Exception {
        if (worker.isPartTime()) {
            throw new Exception("Part time workers are not permitted to work.");
        }
        System.out.print("A full time worker is working for " + worker.getDuration() + " hours :: ");
        for (int i = 1; i < worker.getDuration(); i++) {
            System.out.print("* ");
        }
        System.out.println();
    }
}
ログイン後にコピー

Main.java テスト クラス

package edu.pk.poc.aop.test;

import edu.pk.poc.aop.controller.WorkerController;
import edu.pk.poc.aop.helper.ProxyFactory;
import edu.pk.util.Logger;

public class Main {
    public static void main(String[] args) {
        WorkerController controller = ProxyFactory.createProxy(WorkerController.class);
        Logger logger = new Logger();
        try {
            System.out.println("Testing @BeforeMethod and @AfterMethod");
            System.out.println("-----------------------------------------");
            controller.engageFullTimeWorker();
            System.out.println("Testing @All");
            System.out.println("-----------------------------------------");
            controller.engagePartTimeWorker();
        } catch (Exception e) {
            logger.error("Exception caught in Main class");
        }
    }
}
ログイン後にコピー

出力

Testing @BeforeMethod and @AfterMethod
-----------------------------------------
>>> Entering into edu.pk.poc.aop.controller.WorkerController.engageFullTimeWorker()
A full time worker is working for 9 hours :: * * * * * * * * 
>>> Exiting from edu.pk.poc.aop.service.WorkerServiceImpl.doWork()
>>> Exiting from edu.pk.poc.aop.controller.WorkerController.engageFullTimeWorker()
Testing @All
-----------------------------------------
>>> Entering into edu.pk.poc.aop.controller.WorkerController.engagePartTimeWorker()
>>> Exception in edu.pk.poc.aop.controller.WorkerController.engagePartTimeWorker()
Exception caught in Main class
ログイン後にコピー

仕組み

プロキシ オブジェクトでメソッドが呼び出されると、その呼び出しはハンドラーによってインターセプトされ、ハンドラーはリフレクションを使用してターゲット メソッドのすべてのアノテーションを検査します。これらのアノテーションに基づいて、ハンドラーはメソッドの出入りをログに記録するか、例外をログに記録するか、またはログを完全にスキップするかを決定します。

これを視覚化する方法は次のとおりです:

  • 実行前: メソッドのエントリをログに記録します。
  • 実行後: メソッドの終了または成功をログに記録します。
  • すべて: メソッド エントリ、メソッド エントリ、および例外が発生した場合はログに記録します。 この動的な動作は、Spring AOP が魔法のトリックを使用するのではなく、コア Java API を活用していることを示しています。

注釈の定義

package edu.pk.poc.aop.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterMethod {

}
ログイン後にコピー
package edu.pk.poc.aop.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BeforeMethod {

}
ログイン後にコピー
package edu.pk.poc.aop.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface All {

}
ログイン後にコピー

プロキシ ファクトリを定義します

package edu.pk.poc.aop.helper;

/**
 * The {@code ProxyFactory} class is responsible for creating proxy objects using the Javassist library.
 * It allows for dynamic generation of proxies for classes or interfaces, with support for method interception.
 */
public class ProxyFactory {

    /**
     * A Javassist ProxyFactory instance used to generate proxy classes.
     */
    private static final javassist.util.proxy.ProxyFactory factory = new javassist.util.proxy.ProxyFactory();

    /**
     * Creates a proxy object for the given class or interface.
     * If the class is an interface, the proxy implements the interface.
     * If it's a concrete class, the proxy extends the class.
     *
     * @param <T>   the type of the class or interface for which the proxy is to be created
     * @param klass the {@code Class} object representing the class or interface to proxy
     * @return a proxy instance of the specified class or interface, or {@code null} if proxy creation fails
     */
    public static <T> T createProxy(Class<T> klass) {
        if (klass.isInterface())
            factory.setInterfaces(new Class[]{klass});
        else
            factory.setSuperclass(klass);
        try {
            return (T) factory.create(new Class<?>[0], new Object[0], new AOPLoggingMethodHandler());
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
        return null;
    }
}
ログイン後にコピー

MethodHandler を定義します

package edu.pk.poc.aop.helper;

import edu.pk.poc.aop.annotation.AfterMethod;
import edu.pk.poc.aop.annotation.All;
import edu.pk.poc.aop.annotation.BeforeMethod;
import edu.pk.poc.aop.annotation.OnException;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import edu.pk.util.Logger;
import javassist.util.proxy.MethodHandler;

public class AOPLoggingMethodHandler implements MethodHandler {

    private static final Logger logger = new Logger();

    public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
        if (proceed != null) { // Concrete Method
            Object result = null;
            String className = resolveClassName(self);
            try {
                if (isAnnotationPresent(thisMethod, BeforeMethod.class) || isAnnotationPresent(thisMethod, All.class)) {
                    logger.info(">>> Entering into " + className + "." + thisMethod.getName() + "()");
                }
                result = proceed.invoke(self, args);
                if (isAnnotationPresent(thisMethod, AfterMethod.class) || isAnnotationPresent(thisMethod, All.class)) {
                    logger.info(">>> Exiting from " + className + "." + thisMethod.getName() + "()");
                }
            } catch (Throwable t) {
                if (isAnnotationPresent(thisMethod, OnException.class) || isAnnotationPresent(thisMethod, All.class)) {
                    logger.error(">>> Exception in " + className + "." + thisMethod.getName() + "()");
                }
                throw t;
            }
            return result;
        }
        throw new RuntimeException("Method is Abstract");
    }

    private boolean isAnnotationPresent(Method method, Class klass) {
        Annotation[] declaredAnnotationsByType = method.getAnnotationsByType(klass);
        return declaredAnnotationsByType != null && declaredAnnotationsByType.length > 0;
    }

    private String resolveClassName(Object self) {
        String className = self.getClass().getName();
        if (className.contains("_$$")) {
            className = className.substring(0, className.indexOf("_$$"));
        }
        return className;
    }
}
ログイン後にコピー

結論

Spring AOP は横断的な問題に対応する強力なツールですが、何も革新的なことはしていません。これは、言語自体で使用できるリフレクションやプロキシなどの Java のコア概念に基づいて構築されています。これを理解することで、Spring が開発者の便宜のためにこれらの下位レベルの仕組みをどのように簡素化しているかをよりよく理解できるようになります。

以上がSpring AOP の内部動作を明らかにするの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート