序文
同社は過去2か月間に新しいプロジェクトを立ち上げ、プロジェクトのスケジュールは終了しましたが、それでも学ぶ必要があります。いいえ、会社のプロジェクトを立ち上げたとき、私はSpringAOPピットに足を踏み入れました。
この記事のハイライト:
- 問題の説明
- SpringAOPの実行順序
- 間違った順序についての真実を探る
- コード検証
- 結論として
問題の説明
同社の新しいプロジェクトでは、新しい前面から背面に分離されたHTTPサービスを構築する必要があります。使い慣れたSpringBoot Webを選択して、使用可能なシステムをすばやく構築しました。
魯迅は、安定版をアップグレードするだけではないと言った。私はこの悪を信じていません。私はSpringを長い間使ってきたので、どうして急いでいられないのでしょうか。言うまでもなく、最新のSprinBoot 2.3.4.RELEASEバージョンが直接導入され、プロジェクトの構築が開始されました。
最初はほとんどのコンポーネントの導入が順調に進んでいて、もうすぐやろうと思っていたのですが、ログセクションに出くわすとは思っていませんでした。
インターフェースサービスとして、インターフェース呼び出し状況の照会と問題の特定を容易にするために、通常、要求ログが出力され、ロギングのニーズを完全に満たすアスペクトとしてSpringのAOPがサポートされます。
前のプロジェクトでは、正しいアスペクトログレコードを実行した場合の影響は次のとおりです。
図のメソッド呼び出しは、見栄えを良くするために、リクエストURL、入出力パラメータ、リクエストIPなどを出力することがわかります。分割線が追加されました。
この実装クラスを新しいプロジェクトに入れましたが、実装は次のようになります。
私は目をこすり、コピーされた古いコードを詳しく調べました。簡略化されたバージョンは次のとおりです。
/**
* 在切点之前织入
* @param joinPoint
* @throws Throwable
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 初始化traceId
initTraceId(request);
// 打印请求相关参数
LOGGER.info("========================================== Start ==========================================");
// 打印请求 url
LOGGER.info("URL : {}", request.getRequestURL().toString());
// 打印 Http method
LOGGER.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
LOGGER.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
LOGGER.info("IP : {}", IPAddressUtil.getIpAdrress(request));
// 打印请求入参
LOGGER.info("Request Args : {}", joinPoint.getArgs());
}
/**
* 在切点之后织入
* @throws Throwable
*/
@After("webLog()")
public void doAfter() throws Throwable {
LOGGER.info("=========================================== End ===========================================");
}
/**
* 环绕
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出参
LOGGER.info("Response Args : {}", result);
// 执行耗时
LOGGER.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
return result;
}
コードはまったく問題がないと感じています。新しいバージョンのSpringBootにバグがあります。
明らかに、成熟したフレームワークはこの一般的な方向で間違いを犯すことはありません。新しいバージョンのSpringBootが@Afterと@Aroundの順序を逆にしている可能性がありますか?
実際、物事はそれほど単純ではありません。
SpringAOPの実行順序
まず、SpringAOPの実行シーケンスを確認しましょう。
SpringAopの実行順序に関する情報をインターネットで検索します。ほとんどの場合、次の回答が見つかります。
通常の状況
異常な状況
複数の側面
したがって、@ Aroundは@Afterの前にある必要がありますが、SprinBoot 2.3.4.RELEASEバージョンでは、@ Aroundは実際には@Afterの後に実装されます。
2.2.5.RELEASEバージョンに戻そうとすると、実行順序が@ Around-> @Afterに戻りました。
間違った順序についての真実を探る
問題(またはシーケンスの変更)がSpringBootバージョンのアップグレードによって引き起こされていることがわかったので、どのライブラリがAOP実行の順序を変更したかを見てみましょう。結局のところ、SpringBootは単なる「形」であり、本当のコアはSpringです。
pom.xmlファイルを開き、プラグインを使用してspring-aopのバージョンを確認すると、SpringBoot2.3.4.RELEASEバージョンで使用されているAOPがspring-aop-5.2.9.RELEASEであることがわかります。
また、2.2.5.RELEASEはspring-aop-5.2.4.RELEASEに対応します。
そこで、公式サイトでドキュメンテーションを探しましたが、春が大きすぎてオフィシャルサイトのドキュメンテーションが面倒になってしまったのですが、ようやく見つけました。
https://docs.spring.io/spring-framework/docs/5.2.9.RELEASE/spring-framework-reference/core.html#aop-ataspectj-advice-ordering
Spring Framework 5.2.7以降、同じジョインポイントで実行する必要がある同じ@Aspectクラスで定義されたアドバイスメソッドには、アドバイスタイプに基づいて、優先順位の高いものから低いものの順に優先順位が割り当てられます:@ Around、@ Before 、@ After、@ AfterReturning、@ AfterThrowing。
重要なポイントを簡単に翻訳しましょう。
Spring 5.2.7以降、同じ@Aspectクラスで、通知メソッドが優先度の高いものから低いものへとタイプに従って実行されます:@ Around、@ Before、@ After、@ AfterReturning、@ AfterThrowing。
このように、比較は明らかではありません。2.2.5.RELEASEに対応するspring-aop-5.2.4.RELEASEである古いバージョンに戻りましょう。当時のドキュメントは次のように書かれていました。
複数のアドバイスがすべて同じジョインポイントで実行したい場合はどうなりますか?Spring AOPは、AspectJと同じ優先順位ルールに従って、アドバイスの実行順序を決定します。最も優先度の高いアドバイスが最初に「途中」で実行されます(したがって、2つの前のアドバイスが与えられると、最も優先度の高いアドバイスが最初に実行されます)。ジョインポイントからの「途中」では、最も優先度の高いアドバイスが最後に実行されます(したがって、2つのアフターアドバイスが与えられると、最も優先度の高いアドバイスが2番目に実行されます)。
単純な変換:同じ@Aspectクラスで、Spring AOPはAspectJと同じ優先順位ルールに従って、アドバイスの実行順序を決定します。
さらに深く掘り下げて、AspectJの優先ルールは何ですか?
AspectJのドキュメントを見つけました:
https://www.eclipse.org/aspectj/doc/next/progguide/semantics-advice.html
特定の参加ポイントでは、アドバイスは優先順位に従って並べられます。
アラウンドアドバイスの一部は、proceedを呼び出すことによって優先順位の低いアドバイスを実行するかどうかを制御します。続行するための呼び出しは、次の優先順位でアドバイスを実行するか、それ以上のアドバイスがない場合はジョインポイントの下で計算を実行します。
beforeアドバイスの一部は、例外をスローすることにより、優先順位の低いアドバイスの実行を防ぐことができます。ただし、正常に戻った場合は、次の優先順位のアドバイス、またはそれ以上のアドバイスがない場合は結合パイントの下での計算が実行されます。
アドバイスを返した後に実行すると、次の優先順位のアドバイスが実行されます。それ以上のアドバイスがない場合は、ジョインポイントの下で計算が実行されます。次に、その計算が正常に返されると、アドバイスの本文が実行されます。
アドバイスをスローした後に実行すると、次の優先順位のアドバイスが実行されます。それ以上のアドバイスがない場合は、ジョインポイントの下で計算が実行されます。次に、その計算で適切なタイプの例外がスローされた場合、アドバイスの本文が実行されます。
アドバイスの後に実行すると、次の優先順位のアドバイスが実行されます。それ以上のアドバイスがない場合は、ジョインポイントの下で計算が実行されます。次に、アドバイスの本文が実行されます。
誰もがまた話をするつもりです、ああ、見るには長すぎます!要するに、Aspectjのルールは、上記のインターネットで見つけることができるシーケンス図に示されているとおりです。これはまだ古いシーケンスです。
コード検証
コードからビジネスロジックを削除し、次のアドバイスの実行順序のみを確認しました。
package com.bj58.xfbusiness.cloudstore.system.aop;
import com.bj58.xfbusiness.cloudstore.utils.IPAddressUtil;
import com.bj58.xfbusiness.cloudstore.utils.TraceIdUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 日志切面
*/
@Aspect
@Component
public class WebLogAspect {
private final static Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);
/** 以 controller 包下定义的所有请求为切入点 */
@Pointcut("execution(public * com.xx.xxx.xxx.controller..*.*(..))")
public void webLog() {}
/**
* 在切点之前织入
* @param joinPoint
* @throws Throwable
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
LOGGER.info("-------------doBefore-------------");
}
@AfterReturning("webLog()")
public void afterReturning() {
LOGGER.info("-------------afterReturning-------------");
}
@AfterThrowing("webLog()")
public void afterThrowing() {
LOGGER.info("-------------afterThrowing-------------");
}
/**
* 在切点之后织入
* @throws Throwable
*/
@After("webLog()")
public void doAfter() throws Throwable {
LOGGER.info("-------------doAfter-------------");
}
/**
* 环绕
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
LOGGER.info("-------------doAround before proceed-------------");
Object result = proceedingJoinPoint.proceed();
LOGGER.info("-------------doAround after proceed-------------");
return result;
}
バージョンを2.2.5.RELEASEに変更し、結果を図に示します。
バージョンを2.3.4.RELEASEに変更し、結果を図に示します。
結論として
上記の文書を参照した後、私が与えることができる結論は次のとおりです。
Spring 5.2.7以降、Spring AOPは、AspectJで定義されたルールに従って厳密にアドバイスを実行するのではなく、高から低までのタイプ(@ Around、@ Before、@ After、@ AfterReturning、@ AfterThrowing)に従ってアドバイスを実行します。
今回の研究と思考は非常に急いでいます。結論が間違っている場合は、遠慮なく訂正してください。自分で試してみることもできます。結局のところ、実験室で真実をテストするための唯一の基準は根拠がありません。