きっかけ
Spring bootを使用する開発をするにあたって従来通りのコーディングで進めるだけではなく、フレームワークを活用することを意識したいので学習した。
AOPとは
Aspect Oriented Programming、アスペクト指向プログラミングの略。
アスペクトとは日本語では「外観、様相」という意味だが、この言葉から仕組みを理解するのは難しい。
ここでは本来の処理の間に、別の場所に記載した処理を割り込ませて実行するような方法と理解した。
例
たとえば、メソッドの開始に共通のデバッグログを出力したい場合、従来の方法ではすべてのメソッドにlog出力の処理を記載するかもしくはlog出力のためのメソッドをコールする必要がある。
AOPを使用しない場合
public class ClassA {
public void method1(){
//ログの出力処理がコード内に存在する。
log.info("method1() enter");
//ビジネスロジックのみが記述される。
...
}
}
これに対してAOPでは、本来のコードは何も手を加えずに、log出力のメソッドを別の場所に記述することができる。
AOPを使用する場合
public class ClassA {
public void method1(){
//ログの出力処理はここには存在しない。
//ビジネスロジックのみが記述される。
...
}
}
public class LoggingAdvice {
//com.demoパッケージのすべてのメソッド開始前に処理を割り込ませる。
@Before("execution(* com.demo..*.*(..))")
public void methodEnter(JoinPoint joinPoint){
log.info("joinPoint.getSignature().toLongString() enter");
}
}
これによって、その処理だけで行いたいコードのみを記述し、共通で必要なものは分離して記述することができる。
これを「関心の分離」や「責務の分離」とよばれ、ひとつのクラスやメソッドの処理は一つの責務を持ち、他の責務は他の処理で定義されているようなコードのことをさす。
AOPにより、それが可能となる。
AOPを導入することの利点
- ビジネスロジックと共通処理のコードを完全に分離して設計開発できる。
- 処理ひとつの可読性が良くなる。
- コードの冗長な記述が軽減する。
AOPの用語
用語 | 説明 |
---|---|
Target | Pointcutに合致したJoinPointの具体的なクラス。 |
JoinPoint | 割り込ませたい処理の場所を表す。 |
Pointcut | 割り込ませたい処理の場所の条件。 |
Advice | 割り込ませたい処理を表す。 |
図で説明する。
メソッド1に対して、割り込み処理をAOPで実現している。
Adviceが割り込み処理、Targetが割り込み先、PointcutでどのJoinPointへ割り込ませるか指定する。
SpringでAOPを利用する
Target
このコードには、AOPに関するコードは一切存在しないことに注目してほしい。
本来のビジネスロジックのみを記述し、共通処理はクラス外から注入される。
JoinPoint
JoinPointも同様にコード中で宣言をする必要はない。
DIコンテナへ登録されたクラスのメソッドが自動的にそれにあたる。
Pointcut指定子(Pointcut Designators)
JoinPointの条件を指定する。それにはPointcut指定子(Pointcut Designators)を使用する。
Pointcut指定子には次のような種類がある。
execution
メソッドが実行されるときにJoinPointを指定する。
//com.example.demo.repository配下、
//DemoRepository.doLogicメソッド、任意の引数の数を条件に指定する場合
@Pointcut("execution(* com.example.demo.repository..DemoRepository+.doLogic(..))")
public void doLogicPointcut() {
};
within
特定のクラスに限定して実行されるときにJoinPointを指定する。
メソッドを指定することはできないため、executionより限定的。
//DemoControllerクラスを条件にする場合
@Pointcut("within(com.example.demo.controller.DemoController)")
public void withinPointcut() {
};
target
特定のInterfaceを持つメソッドのJoinPointを指定する。
withinは条件値とクラスの完全一致を条件とするが、targetでは実装したクラスも一致する。
//InterfaceDemoRepositoryインタフェースを条件にする場合
@Pointcut("target(com.example.demo.repository.InterfaceDemoRepository)")
public void targetPointcut() {
};
args
メソッドの引数の一致を条件にJoinPointを指定する。
execution指定子はコードの引数の型が静的に評価されるのに対して、args指定子では実行時の型で評価される。
コードでは型が不定であったとしても、実行時に指定された型でメソッドが呼び出されれば一致する。
//Model型を引数に持つメソッドを条件にする場合
@Pointcut("execution(* com.example.demo..*(..)) && args(org.springframework.ui.Model)")
public void argsPointcut() {
};
this
調査中。
@within
指定したアノテーションを持つメソッドを条件に指定する。
//@RequestMappingが指定されたメソッドを条件にする場合
@Pointcut("@within(org.springframework.web.bind.annotation.RequestMapping)")
public void annotationWithinPointcut() {
};
@args
指定したアノテーションを持った型を持つ引数のメソッドを条件に指定する。
//@demoAnnotationが指定された型が引数のメソッドを条件にする場合
@Pointcut("execution(* com.example.demo..*(..)) && @args(com.example.demo.annotation.demoAnnotation)")
public void annotationArgsPointcut() {
};
//この例では、method1()が@demoAnnotationを持つMyClass型を指定しているため、条件に該当する。
@demoAnnotation
public class MyClass {
//...
}
public void doLogic{
Myclass myClass = new(MyClass);
method1(myclass);
}
その他は公式ドキュメントで記載があった。
execution指定のPointcut指定子
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
Spring Framework Reference Doc. - Core Technologies - 5.4.3. Declaring a Pointcut - Examples
図ではこのような条件を指定している。
@Pointcut("execution(* com.demo.ClassA.mehod1(..))")
式 | 値 | 条件の備考 | |
---|---|---|---|
modifiers-pattern | 修飾子 | 指定なし | SpringではpublicクラスでなければDIコンテナに登録されずAOPの対象にマッチしないため、省略 |
ret-type-pattern | 戻り値の型 | * | 任意の型 |
declaring-type-pattern | クラス | com.demo.ClassA | 省略可能 |
name-pattern | メソッド | method1 | |
param-pattern | 引数 | (..) | 任意の個数の引数 |
throws-pattern | 例外クラス | 指定なし | 省略可能 |
例ではMyPointcutというクラスを作成してPointcutを指定しているが、これはMyAdviceクラスの@Beforeへ直接指定しても動作する。ただしその場合、Adviceのメソッドが複数のPointcutで使いまわすことができない。
また、ワイルドカードを指定できる。よく使われそうな書式を調べた。
* | 1個の任意のパターン |
.. | 0個以上の任意のパターン |
|| | or |
&& | and |
! | 否定 |
AspectJ execution(MethodPattern)
Pointcut指定子は正規表現に近いが正確にはAspectJというライブラリのPointcut式と呼ばれるもので、execution指定では"MethodPattern"を解釈する。
execution(MethodPattern)
Picks out each method execution join point whose signature matches MethodPattern.
The AspectJ Programming Guide - Appendix B. Language Semantics - Pointcuts
Advice
JoinPointへ注入したい処理の記述をする。
また、JoinPointのどの時点で注入するか指定することができる。
@Before | targetの実行前 |
@After | targetの実行後。公式ドキュメントではAfter finally adviceとも呼ばれており、メソッドの結果によらず必ず処理される。 |
@AfterReturning | targetが正常にreturnされたとき。 |
@AfterThrowing | targetで例外がthrowされたとき。 |
@Around | targetの処理を実行せず、Adviceを実行する。 |
Adviceの各アノテーションの引数に、PointCutで定義したメソッドを指定する。
例えば次のとおり。
//doLogicPointcut() PointCutで指定されたメソッドの実行前に注入する。
@Before("com.example.demo.aspect.MyPointcut.doLogicPointcut()")
public void methodEntry(JoinPoint joinPoint){
LOGGER.info(">>> Entry :" + joinPoint.getSignature().toLongString());
}
@AfterThrowing
例外がthorowされ、処理が終了後に実行される。
また、メソッドで発生した例外クラスはthrowing引数を指定することでAdviceでも参照できる。
特定の例外の場合のみ実行したい場合は、Adviceメソッドの引数へ例外を指定することで限定できる。逆に指定が無ければ、Exceptionクラスを指定すればよい。
//exで発生元の例外クラスを参照できる。
//DuplicateKeyExceptionクラスが発生した場合のみ注入する。
@AfterThrowing(pointcut = "com.example.demo.aspect.MyPointcut.doLogicPointcut()",
throwing = "ex")
public void duplicateException(JoinPoint joinPoint, DuplicateKeyException ex){
LOGGER.info(">>> Exception :" + joinPoint.getSignature().toLongString(), ex);
LOGGER.info("<<< Exception :");
}
@Around
この指定は他とは異なり、targetの処理の前後に注入するのではなく、処理をAdviceで上書きする。
本来のtargetの処理が実行されずAdviceが代わりに実行されるようになるが、proceed()メソッドを呼ぶことでtargetの処理を実行できる。
本来のメソッドの動作をAOPで変化させてしまうが、呼ばなければtargetの処理を実行させないこともできる。
public String doLogic(){
String str = "Return text";
return str;
}
@Pointcut("execution(* com.example.demo..doLogic(..))")
public void doLogicPointcut() {
};
//doLogicPointcut()で指定されたtargetを上書きする。
@Around(value = "com.example.demo.aspect.MyPointcut.doLogicPointcut()")
public Object aroundDoLogicPointcut(ProceedingJoinPoint pjp) throws Throwable {
//@beforeに相当する処理を実行する。
LOGGER.info("before " + pjp.getSignature().toLongString());
//targetの処理を実行する
//returnObjectには、String型の"Return text"が設定される。
Object returnObject = pjp.proceed();
//@afterに相当する処理を実行する
LOGGER.info("after " + pjp.getSignature().toLongString());
//targetの戻り値をreturnする。
//これによって、targetの呼び出し元へtargetの戻り値を渡すことができる。
return returnObject;
}
参考
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts
https://www.eclipse.org/aspectj/doc/released/progguide/language-joinPoints.html
https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html