前言
我們開發(fā)的 Web 系統(tǒng)都會有日志模塊,用來記錄對數(shù)據(jù)有進行變更的操作。一般都會記錄請求的 URL,請求的 IP,執(zhí)行的方法,操作人員等等。其目的可能是為了保留操作痕跡,防抵賴,或是記錄系統(tǒng)運行情況,再有就是審計要求。
一、AOP是什么?
360百科:在軟件業(yè),AOP 為 Aspect Oriented Programming 的縮寫,意為:面向切面編程,通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術(shù)。AOP 是 OOP 的延續(xù),是軟件開發(fā)中的一個熱點,也是 Spring 框架中的一個重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用 AOP 可以對業(yè)務(wù)邏輯的各個部分進行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率。
將一些功能從業(yè)務(wù)邏輯代碼中劃分出來,通過對這些行為的分離,我們希望可以將它們獨立到非指導(dǎo)業(yè)務(wù)邏輯的方法中,進而改變這些行為的時候不影響業(yè)務(wù)邏輯的代碼。比如最常見的日志記錄、性能統(tǒng)計、安全控制、事務(wù)處理、異常處理等等。
二、Spring AOP
這里主要為 Spring Framework 5.2.5 中 AOP 的介紹。
1.使用Spring進行切面的編程

2.AOP基本概念

譯文:
切面(Aspect):切面是一個關(guān)注點的模塊化。事務(wù)管理是企業(yè) Java 應(yīng)用程序中橫切關(guān)注點的一個很好的例子。在 Spring AOP 中,切面是通過使用常規(guī)類(基于模式的方法)或使用
@Aspect注解的常規(guī)類來實現(xiàn)的。連接點(Join Point):程序執(zhí)行過程中的一個點,如方法的執(zhí)行或異常的處理。在 Spring AOP 中,連接點總是表示方法執(zhí)行。
通知(Advice):切面在特定連接點上采取的操作。不同類型的通知包括
around、before、after。許多 AOP 框架,包括 Spring,將通知建模為攔截器,并維護圍繞連接點的攔截器鏈。
- 前置通知(Before advice):在連接點之前運行但不能阻止執(zhí)行流繼續(xù)到連接點的通知(除非它拋出異常)。
- 后置通知(After returning advice):通知在連接點正常完成后運行(例如,如果一個方法沒有拋出異常而返回)。
- 異常通知(After throwing advice):如果一個方法通過拋出異常退出,則要執(zhí)行的通知。
- 最終通知(After (finally) advice):無論連接點以何種方式退出(正?;虍惓7祷?,都將執(zhí)行通知。
- 環(huán)繞通知(Around advice):圍繞連接點(如方法調(diào)用)的通知。這是最有力的通知。環(huán)繞通知可以在方法調(diào)用之前和之后執(zhí)行自定義行為。它還負責(zé)選擇是繼續(xù)到連接點,還是通過返回它自己的返回值或拋出異常來簡化通知的方法執(zhí)行。切點(Pointcut):匹配連接點的術(shù)語。通知與切入點表達式相關(guān)聯(lián),并在與切入點匹配的任何連接點上運行(例如,執(zhí)行具有特定名稱的方法)。連接點由切入點表達式匹配的概念是 AOP 的核心,Spring 默認使用 AspectJ 切入點表達式語言。
引入(Introduction):代表類型聲明其他方法或字段。Spring AOP 允許您將新的接口(和相應(yīng)的實現(xiàn))引入任何被建議的對象。例如,您可以使用一個引入來讓一個 bean 實現(xiàn)一個
IsModified接口,以簡化緩存。(引入在 AspectJ 社區(qū)中稱為類型間聲明。)目標對象(Target object):被一個或多個切面告知的對象。也稱為“被通知對象”。因為 Spring AOP 是通過使用運行時代理來實現(xiàn)的,所以這個對象總是一個代理對象。
AOP代理(AOP proxy):為了實現(xiàn)切面契約(通知方法執(zhí)行等)而由 AOP 框架創(chuàng)建的對象。在 Spring 框架中,AOP 代理是 JDK 動態(tài)代理或 CGLIB 代理。
織入(Weaving):將切面與其他應(yīng)用程序類型或?qū)ο箧溄右詣?chuàng)建通知的對象。這可以在編譯時(例如,使用 AspectJ 編譯器)、加載時或運行時完成。與其他純 Java AOP 框架一樣,Spring AOP 在運行時執(zhí)行編織。
說明:
環(huán)繞通知是最普遍的通知。因為 Spring AOP 和 AspectJ 一樣,提供了各種各樣的通知類型,所建議使用最不強大的通知類型來實現(xiàn)所需的行為。使用最特定的通知類型可以提供更簡單的編程模型,減少出錯的可能性。
所有的通知參數(shù)都是靜態(tài)類型的,這樣就可以使用適當類型的通知參數(shù)(例如:方法執(zhí)行返回值的類型),而不是對象數(shù)組。
由切入點匹配的連接點的概念是 AOP 的關(guān)鍵,它將 AOP 與只提供攔截的舊技術(shù)區(qū)分開來。切入點使通知能夠獨立于面向?qū)ο蟮膶哟谓Y(jié)構(gòu)。例如,可以將提供聲明性事務(wù)管理的環(huán)繞通知應(yīng)用于一組跨多個對象的方法(例如:服務(wù)層中的所有業(yè)務(wù)操作)。
3.AOP的功能和目標
Spring AOP 是在純 Java 中實現(xiàn)的。不需要特殊的編譯過程。Spring AOP 不需要控制類裝入器層次結(jié)構(gòu),因此適合在 servlet 容器或應(yīng)用程序服務(wù)器中使用。
Spring AOP 目前只支持方法執(zhí)行連接點(建議在 Spring bean 上執(zhí)行方法)。雖然可以在不破壞核心 Spring AOP api 的情況下添加對字段攔截的支持,但是沒有實現(xiàn)字段攔截。如果需要通知字段訪問和更新連接點,請考慮 AspectJ 之類的語言。
Spring 框架的 AOP 功能通常與 Spring IoC 容器一起使用。切面是通過使用普通的 bean 定義語法來配置的(盡管這允許強大的“自動代理”功能)。這是與其他 AOP 實現(xiàn)的一個重要區(qū)別。
4.AOP代理
Spirng 的 AOP 的動態(tài)代理實現(xiàn)機制有兩種,分別是:JDK 動態(tài)代理和 CGLib 動態(tài)代理。簡單介紹下兩種代理機制:
- JDK 動態(tài)代理
JDK 動態(tài)代理是面向接口的代理模式,如果被代理目標沒有接口那么 Spring 也無能為力,Spring 通過 Java 的反射機制生產(chǎn)被代理接口的新的匿名實現(xiàn)類,重寫了其中 AOP 的增強方法。 - CGLib 動態(tài)代理
CGLib 是一個強大、高性能的 Code 生產(chǎn)類庫,可以實現(xiàn)運行期動態(tài)擴展 Java 類,Spring 在運行期間通過 CGlib 繼承要被動態(tài)代理的類,重寫父類的方法,實現(xiàn) AOP 面向切面編程。
兩者對比:
- JDK 動態(tài)代理是面向接口,在創(chuàng)建代理實現(xiàn)類時比 CGLib 要快,創(chuàng)建代理速度快。而且 JDK 動態(tài)代理只能對實現(xiàn)了接口的類生成代理,而不能針對類。
- CGLib 動態(tài)代理是通過字節(jié)碼底層繼承要代理類來實現(xiàn)(如果被代理類被
final關(guān)鍵字所修飾,那么抱歉會失?。趧?chuàng)建代理這一塊沒有 JDK 動態(tài)代理快,但是運行速度比 JDK 動態(tài)代理要快。
5.@AspectJ 支持
@AspectJ 指的是將切面聲明為用注解注釋的常規(guī) Java 類的樣式。@AspectJ 樣式是由 AspectJ 項目作為 AspectJ 5 發(fā)行版的一部分引入的。Spring 使用 AspectJ 提供的用于切入點解析和匹配的庫來解釋與 AspectJ 5 相同的注釋。但是 AOP 運行時仍然是純 Spring AOP,并且不依賴于 AspectJ 編譯器或編織器。
(1)聲明一個切面
- 第一個示例展示應(yīng)用程序上下文中的一個常規(guī) bean 定義,它指向一個具有
@Aspect注釋的 bean 類。
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
- 第二個示例展示NotVeryUsefulAspect類定義,它是由org.aspectj.lang.annotation.Aspect 注解。
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面(使用 @Aspect 注解的類)可以具有與任何其他類相同的方法和字段。它們還可以包含切入點、通知和引入(類型間)聲明。
通過組件掃描自動檢測切面:可以將切面類注冊為 Spring XML 配置中的常規(guī) bean,或者通過類路徑掃描自動檢測它們——與任何其他 Spring 管理的 bean 相同。但是,請注意 @Aspect 注解對于類路徑中的自動檢測是不夠的。為此,您需要添加一個單獨的 @Component 注解(或者,根據(jù) Spring 的組件掃描器的規(guī)則,一個定制的原型注釋)。
(2)聲明一個切入點
切入點確定感興趣的連接點,從而使我們能夠控制何時執(zhí)行通知。Spring AOP 只支持 Spring bean 的方法執(zhí)行連接點,因此可以將切入點看作是與 Spring bean 上的方法執(zhí)行相匹配的。切入點聲明有兩部分:一個簽名,它包含名稱和任何參數(shù);一個是切入點表達式,它確定我們對哪個方法執(zhí)行感興趣。在 AOP 的 @AspectJ 注解風(fēng)格中,切入點簽名由一個常規(guī)方法定義提供,切入點表達式通過使用 @Pointcut 注解來表示(作為切入點簽名的方法必須有一個 void 返回類型)。
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
切入點指示器
Spring AOP 支持以下用于切入點表達式的 AspectJ 切入點指示器(PCD):
- execution:用于匹配方法執(zhí)行連接點。這是使用 Spring AOP 時要使用的主要切入點指示器。
- within:限制對某些類型中的連接點的匹配(使用 Spring AOP 時在匹配類型中聲明的方法的執(zhí)行)。
- this:限制連接點(使用 Spring AOP 時方法的執(zhí)行)的匹配,其中 bean 引用(Spring AOP 代理)是給定類型的實例。
- target:限制對連接點(使用 Spring AOP 時方法的執(zhí)行)的匹配,其中目標對象(代理的應(yīng)用程序?qū)ο?是給定類型的實例。
- args:限制連接點的匹配(使用 Spring AOP 時方法的執(zhí)行),其中的參數(shù)是給定類型的實例。
- @target:限制連接點的匹配(使用 Spring AOP 時方法的執(zhí)行),其中執(zhí)行對象的類具有給定類型的注解。
- @args:限制連接點的匹配(使用 Spring AOP 時方法的執(zhí)行),其中實際傳遞的參數(shù)的運行時類型具有給定類型的注解。
- @within:限制對具有給定注解的類型中的連接點的匹配(在使用 Spring AOP 時,使用給定注解在類型中聲明的方法的執(zhí)行)。
- @annotation:限制對連接點的匹配,連接點的主體(在 Spring AOP 中執(zhí)行的方法)具有給定的注解。
切入點表達式
可以使用 &&、|| 和 ! 組合切入點表達式。您還可以通過名稱引用切入點表達式。
// 如果方法執(zhí)行連接點表示任何公共方法的執(zhí)行,則匹配。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
// 如果方法執(zhí)行在交易模塊中,則匹配。
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
// 如果方法執(zhí)行代表交易模塊中的任何公共方法,則匹配。
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
最佳實踐是從較小的命名組件構(gòu)建更復(fù)雜的切入點表達式,如前面所示。當通過名稱引用切入點時,應(yīng)用普通的Java可見性規(guī)則(您可以在相同的類型中看到私有切入點,層次結(jié)構(gòu)中受保護的切入點,任何地方的公共切入點,等等)??梢娦圆挥绊懬腥朦c匹配。
(3)聲明一個通知
通知與切入點表達式相關(guān)聯(lián),并在切入點匹配的方法執(zhí)行之前、之后或前后運行。切入點表達式可以是對指定切入點的簡單引用,也可以是在適當位置聲明的切入點表達式。
- 前置通知:使用
@Before注解在切面中聲明前置通知。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
- 后置通知:返回后,當匹配的方法執(zhí)行正常返回時,將運行通知。你可以使用
@AfterReturning注解來聲明它。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
需要在通知主體中訪問返回的實際值。您可以使用 @AfterReturning 的形式綁定返回值來獲得訪問權(quán)限。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
- 異常通知:拋出通知后,當匹配的方法執(zhí)行通過拋出異常退出時運行。您可以使用
@AfterThrowing注解來聲明它。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,您希望僅在拋出給定類型的異常時才運行通知,并且常常需要在通知正文中訪問拋出的異常。您可以使用 throwing 屬性來限制匹配(如果需要,可以使用 Throwable 作為異常類型),并將拋出的異常綁定到一個通知參數(shù)。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
- 最終通知:當匹配的方法執(zhí)行退出時,將運行最終通知。它是使用
@After注解聲明的。最終通知必須準備好處理正常和異常返回條件。它通常用于釋放資源和類似的目的。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
- 環(huán)繞通知:環(huán)繞 通知是通過使用
@Around注解來聲明的。通知方法的第一個參數(shù)必須是類型為ProceedingJoinPoint。在通知的主體中,對過程ProceedingJoinPoint調(diào)用proceed()會導(dǎo)致底層方法執(zhí)行。proceed方法也可以傳遞一個Object[]。當方法執(zhí)行時,數(shù)組中的值用作方法執(zhí)行的參數(shù)。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
三、AOP全局統(tǒng)一日志管理
1.環(huán)境說明
開發(fā)工具:IDEA 2019.3.1
框架版本:SpringBoot 2.2.6
2.具體實現(xiàn)
- pom.xml 中加入
Spring AOP依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
-
啟用
@AspectJ支持,這里默認已經(jīng)開啟
@AspectJ支持 自定義日志注解類
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER, ElementType.METHOD })
public @interface WebLog {
/**
* 渠道
* @return 渠道標識
*/
String channel() default "web";
/**
* 功能名稱
* @return 功能名稱
*/
String name() default "";
/**
* 方法名稱
* @return 方法名稱
*/
String action() default "";
/**
* 是否保存(默認不保存)
* @return 是否保存
*/
boolean saveFlag() default false;
}
@Retention 注解保留策略
RetentionPolicy 策略枚舉類(RUNTIME 注解將由編譯器記錄在類文件中,并在運行時由VM保留,因此可以反射性地讀取它們。)
@Target 注解目標位置(也就是該注解要用在什么地方)
ElementType 目標元素類型枚舉類(PARAMETER:參數(shù),METHOD:方法)
- 日志切面類
@Component
@Aspect
public class WebLogAspect {
private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
private final SysUserLogService sysUserLogService;
public WebLogAspect(SysUserLogService sysUserLogService) {
this.sysUserLogService = sysUserLogService;
}
/**
* 連接點(切入點)
* 切入點表達式:匹配 web 包及子包 Controller 類的任何公共方法
*/
@Pointcut("execution(public * com.skillset.web..*Controller.*(..))")
public void webLog() {
}
/**
* 通知:前置通知(Before advice),在連接點之前運行但不能阻止執(zhí)行流繼續(xù)到連接點的通知(除非它拋出異常)。
* 在日志文件或控制臺輸出請求信息
*
* @param joinPoint
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
// 利用RequestContextHolder獲取HttpServletRequest對象
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
// 重組請求信息
StringBuffer sb = new StringBuffer();
sb.append("收到請求:");
sb.append("\r\n訪問URI :" + httpServletRequest.getRequestURI().toString());
sb.append("\r\nSession :" + httpServletRequest.getSession().getId());
sb.append("\r\n訪問IP :" + RequestUtil.getIP(httpServletRequest));
sb.append("\r\n響應(yīng)類 :" + joinPoint.getSignature().getDeclaringTypeName());
sb.append("\r\n方法 :" + joinPoint.getSignature().getName());
Object[] objects = joinPoint.getArgs();
for (Object arg : objects) {
if (arg != null) {
sb.append("\r\n參數(shù) :" + arg.toString());
}
}
// 打印請求信息
logger.info(sb.toString());
}
/**
* 通知:后置通知(After returning advice),通知在連接點正常完成后運行
* 處理請求日志信息
*
* @param joinPoint
*/
@AfterReturning(pointcut = "webLog()", returning = "rvt")
public void doAfterReturning(JoinPoint joinPoint, Object rvt) {
// 處理日志信息
handleLog(joinPoint, null);
}
/**
* 通知:異常通知(After throwing advice),方法通過拋出異常退出,則要執(zhí)行的通知
* 處理請求異常日志信息
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "webLog()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
// 處理日志信息
handleLog(joinPoint, e);
}
/**
* 日志處理
*
* @param joinPoint
* @param e
*/
private void handleLog(JoinPoint joinPoint, Exception e) {
// 利用RequestContextHolder獲取HttpServletRequest對象
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
// 獲取執(zhí)行的方法
Signature signature = joinPoint.getSignature();
if(!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("暫不支持非方法注解");
}
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
// 獲取注解
WebLog controllerLog = method.getAnnotation(WebLog.class);
if (controllerLog != null) {
// 保存日志到數(shù)據(jù)庫
if (controllerLog.saveFlag()) {
SysUserLog sysUserLog = new SysUserLog();
// SessionId
sysUserLog.setAccount(httpServletRequest.getRequestedSessionId());
// 渠道
sysUserLog.setChannel(controllerLog.channel());
// 功能名稱
sysUserLog.setName(controllerLog.name());
// 響應(yīng)類.方法
sysUserLog.setAction(signature.getDeclaringTypeName() + "." + method.getName());
// URI
sysUserLog.setUrl(httpServletRequest.getRequestURI());
// 參數(shù)
sysUserLog.setParams(JSONObject.toJSONString(httpServletRequest.getParameterMap()).replace("\"", ""));
// 請求IP
sysUserLog.setIp(RequestUtil.getIP(httpServletRequest));
// 操作時間
sysUserLog.setLogTime(new Date());
// 異常信息
if (e != null) {
sysUserLog.setErrMsg(e.getMessage());
}
sysUserLogService.insert(sysUserLog);
}
}
}
// 發(fā)生異常時打印錯誤信息
if (e != null) {
StringBuffer sb = new StringBuffer();
sb.append("時間:");
sb.append(DateFormat.getDateTimeInstance().format(new Date()));
sb.append("方法:");
sb.append(joinPoint.getSignature() + "\n");
sb.append("異常信息:" + e.getMessage());
logger.error(sb.toString());
}
}
}
- Controller請求處理層添加注解
/**
* 系統(tǒng)用戶 列表頁
*
* @param sysUserCriteria
* @param model
* @return
*/
@WebLog(channel = "web", name = "系統(tǒng)用戶列表", action = "/sysUser", saveFlag = true)
@GetMapping("")
public String list(SysUserCriteria sysUserCriteria, Model model) {
model.addAttribute("sysUserCriteria", sysUserCriteria);
return "sysUser/list";
}
- 存庫日志記錄

四、總結(jié)說明
拋開雜念,靜下心來,只看當下;
充電片刻,日積月累,步步向前。
