Spring BootでAOP(アスペクト指向)を使うとコードが奇麗になる
オブジェクト指向にアスペクト指向も取り入れるとソースが奇麗になります。
アスペクト指向というのは私が新人の時の社内勉強会で勉強したのですが当時はチンプンカンプンでよくわかりませんでした。
「横断的」という言葉が特に意味が分かりませんでしたが、すごく簡単に言うとプログラムの至る所でしている本来あるべきでない処理を全て纏める。だから横断的(アスペクト)という言葉を使うんだと思います。
一番わかりやすいのは、メソッドの開始終了のログなどは本来メソッドにあるべき処理ではありません。
こういうのを削除して、AOP指向を取り入れてログの処理は一つのクラスだけに任せます。
Spring BootにはAOPが用意されており、build.gradleのdependenciesに以下を追記します。
implementation 'org.springframework.boot:spring-boot-starter-aop'
空のコントローラを作成します。何もしていないメソッドを2つ用意しただけです。
package jp.co.confrage; import org.springframework.stereotype.Controller; @Controller public class DemoController { public void uploadFile() { } public void downloadFile() { } }
AOPコンポーネントを作成します。
package jp.co.confrage; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; @Component @Aspect @Slf4j public class AopComponent { @Before("execution(* jp.co.confrage.*.*(..))") public void aa(JoinPoint jp) { // メソッド名は何でもよい log.info(jp.getSignature().getDeclaringType().getSimpleName() + "クラスの" + jp.getSignature().getName() + "メソッドを開始します"); } @After("execution(* jp.co.confrage.*.*(..))") public void bb(JoinPoint jp) { // メソッド名は何でもよい log.info(jp.getSignature().getDeclaringType().getSimpleName() + "クラスの" + jp.getSignature().getName() + "メソッドを終了します"); } }
AOPのクラスには@Aspectアノテーションを付与します。各アノテーションの意味です。
アノテーション | 意味 |
---|---|
@Before | メソッド前に実行する |
@After | メソッド後に実行する |
@Arround | アノテーションで指定したクラスの前後の処理を記述することが出来る |
これでメソッドの開始終了ログが出力されるようになります。
メインクラスは以下のようにしておきます。
package jp.co.confrage; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import lombok.AllArgsConstructor; @SpringBootApplication @AllArgsConstructor public class SpringAopApplication implements CommandLineRunner{ private final DemoController controller; public static void main(String[] args) { SpringApplication.run(SpringAopApplication.class, args); } @Override public void run(String... args) throws Exception { controller.uploadFile(); // 呼び出ししているだけ controller.downloadFile(); // 呼び出ししているだけ } }
このSpring Bootアプリケーションを実行すると以下のようにログ出力されます。
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.5.RELEASE) 2019-05-23 20:08:36.126 INFO 13396 --- [ main] jp.co.confrage.SpringAopApplication : Starting SpringAopApplication on DESKTOP-DNSQQ62 with PID 13396 (C:\Users\takahshi\Documents\workspace-sts\SpringAop\bin\main started by takahashi in C:\Users\takahashi\Documents\workspace-sts\SpringAop) 2019-05-23 20:08:36.129 INFO 13396 --- [ main] jp.co.confrage.SpringAopApplication : No active profile set, falling back to default profiles: default 2019-05-23 20:08:36.636 INFO 13396 --- [ main] jp.co.confrage.SpringAopApplication : Started SpringAopApplication in 0.74 seconds (JVM running for 1.262) 2019-05-23 20:08:36.667 INFO 13396 --- [ main] jp.co.confrage.AopComponent : SpringAopApplicationクラスのrunメソッドを開始します 2019-05-23 20:08:36.673 INFO 13396 --- [ main] jp.co.confrage.AopComponent : DemoControllerクラスのuploadFileメソッドを開始します 2019-05-23 20:08:36.679 INFO 13396 --- [ main] jp.co.confrage.AopComponent : DemoControllerクラスのuploadFileメソッドを終了します 2019-05-23 20:08:36.680 INFO 13396 --- [ main] jp.co.confrage.AopComponent : DemoControllerクラスのdownloadFileメソッドを開始します 2019-05-23 20:08:36.680 INFO 13396 --- [ main] jp.co.confrage.AopComponent : DemoControllerクラスのdownloadFileメソッドを終了します 2019-05-23 20:08:36.680 INFO 13396 --- [ main] jp.co.confrage.AopComponent : SpringAopApplicationクラスのrunメソッドを終了します
コントローラにあるメソッドは本来あるべき処理を記述するだけで、ログ出力はAOPコンポーネントに任せるというように実装します。
これによって本来あるべきコーディングと本来ないべきコーディングを分離することができます。
JoinPointクラス
@Beforeや@Afterがついたメソッドは引数にJoinPointクラスを持ちます。
色々出来るのですが簡単に以下にいくつか紹介しておきます。jpはJoinPointのインスタンスです。
取得値 | メソッド |
---|---|
クラス名を取得 | jp.getSignature().getDeclaringType().getSimpleName() |
メソッド名を取得 | jp.getSignature().getName() |
引数名を取得 | jp.getSignature()).getParameterNames() |
引数の値を取得 | jp.getArgs() |
ServletUriComponentsBuilderからURI取得
org.springframework.web.servlet.support.ServletUriComponentsBuilderクラスからURIを取得することが出来ます。
log.info(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString());
RequestContextHolderからHTTPメソッドを取得
org.springframework.web.context.request.RequestContextHolderクラスからHTTPメソッドを取得することが出来ます。
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); var httpMethod = httpServletRequest.getMethod(); log.info("{}",httpMethod);
ヘッダ情報を取得
ヘッダ情報も取得することが出来ます。
String[] argsNames = ((CodeSignature) jp.getSignature()).getParameterNames();
String配列に@RequestHeader HttpHeadersクラスの変数名が入ります。
@RequestMapping(path = "/test/{hoge}", method = RequestMethod.GET) public void test(@PathVariable("hoge") String hoge, @RequestHeader HttpHeaders headers) {
このRestControllerの例ですと、argsNames = [hoge, headers]
という順で取得できます。
同様にjp.getArgs()
にも同じ順でObject[]として値が入ります。HttpHeadersは各ヘッダがLinkedHashMapのkey-valueで保持されています。
API Gateway経由でRestControllerを実行する際にContent-Typeがapplication/jsonではない場合、JSONスキーマでのバリデーションが効きません。その為、RestController(java)側でContent-TypeやHTTPメソッドを判断し、application/jsonではない場合はBAD REQUEST(400)にするなど、エラーハンドリングが必要になります。
@Arroundアノテーション
@Arroundアノテーションを付与すれば、特定のクラスの前後の処理を記述することが出来ます。
package jp.co.confrage.presentation.aop; import java.util.Optional; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.CodeSignature; import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; @Component @Aspect @Slf4j public class AopPjpComponent { @Around("within(jp.co.confrage.presentation.controller.*)") public Object arround(final ProceedingJoinPoint pjp) throws Throwable { final String[] argsNames = ((CodeSignature) pjp.getSignature()).getParameterNames(); final Object[] argsValues = pjp.getArgs(); log.info("argsNames: {}", (Object) argsNames); log.info("argsValues: {}", argsValues); Object result = Optional.empty(); // 前処理 result = pjp.proceed(); // コントローラ呼び出し // 後処理 return result; } }
jp.co.confrage.controller配下のクラスのメソッドが呼ばれたときにarroundメソッドが呼び出されます。
proceedメソッドの箇所でクラスのメソッドが呼び出されますので、前後処理を記述することが出来ます。
@Validate,@Validが効かない
RestControllerでクエリパラメータの正規表現チェックをしようとしたのですが、@ArroundアノテーションでAOPしていると、@RestControllerAdviceアノテーションのエラーハンドリングが効きません。
KHI入社して退社。今はCONFRAGEで正社員です。関西で140-170/80~120万から受け付けております^^
得意技はJS(ES6),Java,AWSの大体のリソースです
コメントはやさしくお願いいたします^^
座右の銘は、「狭き門より入れ」「願わくは、我に七難八苦を与えたまえ」です^^
コメント