? ? ? ? Spring 中的切面 Aspect,這是 Spring 的一大優(yōu)勢(shì)。面向切面編程往往讓我們的開發(fā)更加低耦合,也大大減少了代碼量,同時(shí)呢讓我們更專注于業(yè)務(wù)模塊的開發(fā),把那些與業(yè)務(wù)無關(guān)的東西提取出去,便于后期的維護(hù)和迭代。
一、什么是 AOP?
????????AOP 的全稱為 Aspect Oriented Programming,譯為面向切面編程,是通過預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)核心業(yè)務(wù)邏輯之外的橫切行為的統(tǒng)一維護(hù)的一種技術(shù)。AOP 是面向?qū)ο缶幊蹋∣OP)的補(bǔ)充和擴(kuò)展。?利用 AOP 可以對(duì)業(yè)務(wù)邏輯各部分進(jìn)行隔離,從而達(dá)到降低模塊之間的耦合度,并將那些影響多個(gè)類的公共行為封裝到一個(gè)可重用模塊,從而到達(dá)提高程序的復(fù)用性,同時(shí)提高了開發(fā)效率,提高了系統(tǒng)的可操作性和可維護(hù)性。?
????????AOP 是 Spring 框架中的一個(gè)核心內(nèi)容。在 Spring 中,AOP 代理可以用 JDK 動(dòng)態(tài)代理或者 CGLIB 代理 CglibAopProxy 實(shí)現(xiàn)。Spring 中 AOP 代理由 Spring 的 IOC 容器負(fù)責(zé)生成和管理,其依賴關(guān)系也由 IOC 容器負(fù)責(zé)管理。
二、為什么要用 AOP?
????????在實(shí)際的 Web 項(xiàng)目開發(fā)中,我們常常需要對(duì)各個(gè)層面實(shí)現(xiàn)日志記錄,性能統(tǒng)計(jì),安全控制,事務(wù)處理,異常處理等等功能。如果我們對(duì)每個(gè)層面的每個(gè)類都獨(dú)立編寫這部分代碼,那久而久之代碼將變得很難維護(hù),所以我們把這些功能從業(yè)務(wù)邏輯代碼中分離出來,聚合在一起維護(hù),而且我們能靈活地選擇何處需要使用這些代碼。
三、AOP 的核心概念
名詞 | 概念 | 理解 |
切面(Aspect) | 切面類的定義,里面包含了切入點(diǎn)(Pointcut)和通知(Advice)的定義 | 首先要理解“切”字,需要把對(duì)象想象成一個(gè)立方體,每次實(shí)例化一個(gè)對(duì)象,對(duì)類定義中的成員變量賦值,就相當(dāng)于對(duì)這個(gè)立方體進(jìn)行了一個(gè)定義,定義完成之后,就等著被使用,等著被回收。 面向切面編程則是指,對(duì)于一個(gè)我們已經(jīng)封裝好的類,我們可以在編譯期間或在運(yùn)行期間,對(duì)其進(jìn)行切割,把立方體切開,在原有的方法里面添加(織入)一些新的代碼,對(duì)原有的方法代碼進(jìn)行一次增強(qiáng)處理。而那些增強(qiáng)部分的代碼,就被稱之為切面,如下面代碼實(shí)例中的通用日志處理代碼,常見的還有事務(wù)處理、權(quán)限認(rèn)證等等。 |
切入點(diǎn)(PointCut) | 對(duì)連接點(diǎn)進(jìn)行攔截的定義 | 要對(duì)哪些類中的哪些方法進(jìn)行增強(qiáng),進(jìn)行切割,指的是被增強(qiáng)的方法。既要切哪些東西。 |
連接點(diǎn)(JoinPoint) | 被攔截到的點(diǎn),如被攔截的方法、對(duì)類成員的訪問以及異常處理程序塊的執(zhí)行等等,自身還能嵌套其他的 Joint Point。 | 知道了要切哪些方法后,剩下的就是什么時(shí)候切,在原方法的哪一個(gè)執(zhí)行階段加入增加代碼,這個(gè)就是連接點(diǎn)。如方法調(diào)用前,方法調(diào)用后,發(fā)生異常時(shí)等等。 |
通知(Advice) | 攔截到連接點(diǎn)之后所要執(zhí)行的代碼,通知分為前置、后置、異常、最終、環(huán)繞通知五類。 | 通知被織入方法,該如何被增強(qiáng)。定義切面的具體實(shí)現(xiàn)。那么這里面就涉及到一個(gè)問題,空間(切哪里)和時(shí)間(什么時(shí)候切,在何時(shí)加入增加代碼),空間我們已經(jīng)知道了就是切入點(diǎn)中定義的方法,而什么時(shí)候切,則是連接點(diǎn)的概念。 |
目標(biāo)對(duì)象(Target Object) | 切入點(diǎn)選擇的對(duì)象,也就是需要被通知的對(duì)象。由于 Spring AOP 通過代理模式實(shí)現(xiàn),所以該對(duì)象永遠(yuǎn)是被代理對(duì)象。 | 被一個(gè)或多個(gè)切面所通知的對(duì)象,即為目標(biāo)對(duì)象。即業(yè)務(wù)邏輯本身。 |
AOP 代理對(duì)象(AOP Proxy Object) | Spring AOP 可以使用 JDK 動(dòng)態(tài)代理或者 CGLIB 代理,前者基于接口,后者基于類。 | AOP代理是AOP框架所生成的對(duì)象,該對(duì)象是目標(biāo)對(duì)象的代理對(duì)象。代理對(duì)象能夠在目標(biāo)對(duì)象的基礎(chǔ)上,在相應(yīng)的連接點(diǎn)上調(diào)用通知。 |
織入(Weaving) | 把切面應(yīng)用到目標(biāo)對(duì)象從而創(chuàng)建出AOP代理對(duì)象的過程??椚肟梢栽诰幾g期、類裝載期、運(yùn)行期進(jìn)行,而 Spring 采用在運(yùn)行期完成。 | 將切面切入到目標(biāo)方法之中,使目標(biāo)方法得到增強(qiáng)的過程被稱之為織入。 |
四、相關(guān)注解
注解 | 說明 |
@Aspect | 將一個(gè) java 類定義為切面類 |
@Pointcut | 定義一個(gè)切入點(diǎn),定義需要攔截的東西,即上下文中所關(guān)注的某件事情的入口,切入點(diǎn)定義了事件觸發(fā)時(shí)機(jī)??梢允且粋€(gè)規(guī)則表達(dá)式(execution() 表達(dá)式,annotation() 表達(dá)式),比如下例中某個(gè) package 下的所有函數(shù),也可以是一個(gè)注解等 |
@Before | 在切入點(diǎn)開始處切入內(nèi)容 |
@After? ? ? | 在切入點(diǎn)結(jié)尾處切入內(nèi)容 |
@AfterReturning | 在切入點(diǎn) return 內(nèi)容之后處理邏輯 |
@Around | 在切入點(diǎn)前后切入內(nèi)容,并自己控制何時(shí)執(zhí)行切入點(diǎn)自身的內(nèi)容 |
@AfterThrowing | 用來處理當(dāng)切入內(nèi)容部分拋出異常之后的處理邏輯 |
@Order(100) | AOP 切面執(zhí)行順序,@Before 數(shù)值越小越先執(zhí)行,@After 和 @AfterReturning 數(shù)值越大越先執(zhí)行 |
其中 @Before、@After、@AfterReturning、@Around、@AfterThrowing 都屬于通(Advice)。?
五、添加 AOP Maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
六、第一個(gè)實(shí)例
接下來我們先看一個(gè)例子,利用AOP+Swagger注解實(shí)現(xiàn)日志記錄功能,所有接口請(qǐng)求完成時(shí),打印日志記錄。
具體實(shí)現(xiàn)如下:
1、創(chuàng)建一個(gè) AOP 切面類,只要在類上加個(gè) @Aspect 注解即可。@Aspect 注解用來描述一個(gè)切面類,定義切面類的時(shí)候需要打上這個(gè)注解。@Component 注解將該類交給 Spring 來管理。在這個(gè)類里實(shí)現(xiàn) advice:
@Aspect
@Slf4j
@Component
@Order(1)
public class WebLogAspect {
private ThreadLocal<Long> startTime = new Thre
// 定義一個(gè)切點(diǎn):所有接口中被@ApiOperation 注解修飾的方法會(huì)織入advice
@Pointcut("@annotation(operation)")
public void logPointcut(ApiOperation operation
}
// Before 表示 before 將在目標(biāo)方法執(zhí)行前執(zhí)行
@Before(value = "logPointcut(operation)", argN
public void before(JoinPoint joinPoint, ApiOperation operation) {
startTime.set(System.currentTimeMillis());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String[] moduleName = getModuleName(joinPoint, operation);
log.info("[{}]-[{}]-start {}", moduleName[0], moduleName[1], request.getRequestURL().toString());
log.info("參數(shù) : {}", Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "logPointcut(operation) ", argNames = "joinPoint,ret,operation")
public void after(JoinPoint joinPoint, Object ret, ApiOperation operation) {
// 處理完請(qǐng)求,返回內(nèi)容
String[] moduleName = getModuleName(joinPoint, operation);
String retMesage = JSON.toJSONString(ret);
log.info("[{}]-[{}]-end [{}ms] 響應(yīng):{}", moduleName[0], moduleName[1], (System.currentTimeMillis() - startTime.get()), retMesage);
startTime.remove();
}
private String[] getModuleName(JoinPoint joinPoint, ApiOperation operation) {
Class<?> clazz = joinPoint.getTarget().getClass();
Api api = clazz.getAnnotation(Api.class);
String pInfo = Optional.ofNullable(api).map(Api::value).orElse(clazz.getName());
return new String[]{pInfo, operation.value()};
}
}
2、創(chuàng)建一個(gè)接口類,在方法上加上 swagger 的 @ApiOperation 的注解
@PostMapping("/getSupplierOrderDetailInfo")
@ApiOperation(value = "售賣渠道訂單查詢", notes = "售賣渠道訂單查詢")
public ApiResultResponse<SupplierOrderDetailResponse> getSupplierOrderDetailInfo(@RequestBody @Valid SupplierOrderDetailRequest request) {
SupplierOrderDetailResponse response = appVipOrderService.getSupplierOrderDetailInfo(request);
return ApiResponseUtils.buildSuccessMsg(response);
}
3、項(xiàng)目啟動(dòng)后,請(qǐng)求該接口時(shí),日志打印如下:
INFO n.b.z.c.l.WebLogAspect - [售賣渠道信息相關(guān)]-[售賣渠道資金池余額查詢接口]-start http://localhost:8080/openapi/supplier/getSupplier
INFO n.b.z.c.l.WebLogAspect - 參數(shù) : [SupplierAssetsPoolRequest(supplierId=8)]
INFO n.b.z.c.l.WebLogAspect - [售賣渠道信息相關(guān)]-[售賣渠道資金池余額查詢接口]-end [136ms] 響應(yīng):{"apiResult":{"balance":2910},"retCode":"000000","retMsg":"成功","retState":"SUCCESS"}
七、第二個(gè)實(shí)例
下面將問題復(fù)雜化一些,該例的場(chǎng)景是:
1、自定義一個(gè)注解?SupplierCheck?
2、創(chuàng)建一個(gè)切面類,切點(diǎn)設(shè)置為校驗(yàn)所有標(biāo)注 SupplierCheck 的方法,截取到接口的參數(shù),進(jìn)行簡(jiǎn)單的 ip 白名單校驗(yàn)
3、將 SupplierCheck 標(biāo)注在接口類上面的方法上
具體的實(shí)現(xiàn)步驟:
1、讓我們來自定義一個(gè)注解,注解名為 SupplierCheck,如下所示
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface SupplierCheck {
/**
* 校驗(yàn)白名單
* 標(biāo)識(shí)該方法是否需要校驗(yàn)白名單(默認(rèn)校驗(yàn)),白話文就是說是否需要執(zhí)行該校驗(yàn)白名單方法
*/
boolean checkIpWhite() default true;
}
1、@Target 注解
用于指明被修飾的注解最終可以作用的目標(biāo)是誰,也就是指明,你的注解到底是用來修飾方法的?修飾類的?還是用來修飾字段屬性的。可以指定多個(gè)位置(@Target({ElementType.METHOD, ElementType.FIELD})),語法如下:
作用:用于描述注解的使用范圍(即:被描述的注解可以用在什么地方)?
類型 | 描述 |
ElementType.TYPE | 可以用于類、接口和枚舉類型 |
ElementType.FIELD | 可以用于字段(包括枚舉常量) |
ElementType.METHOD | 可以用于方法(controller上面的接口里它也是方法) |
ElementType.PARAMETER | 可以用于方法的參數(shù) |
ElementType.CONSTRUCTOR | 可以用于構(gòu)造函數(shù) |
ElementType.LOCAL_VARIABLE | 可以用于局部變量 |
ElementType.ANNOTATION_TYPE | 可以用于注解類型 |
ElementType.PACKAGE | 可以用于包 |
ElementType.TYPE_PARAMETER | 可以用于類型參數(shù)聲明(Java 8新增) |
ElementType.TYPE_USE | 可以用于使用類型的任何語句中(Java 8新增) |
2、@Rerention 注解
該注解指定了被修飾的注解的生命周期,語法如下:
作用:表示需要在什么級(jí)別保存該注釋信息,用于描述注解的生命周期(即:被描述的注解在什么范圍內(nèi)有效)?
類型 | 描述 |
RetentionPolicy.SOURCE | 在源文件中有效(即源文件保留) |
RetentionPolicy.CLASS | 在class文件中有效(即class保留),不會(huì)被加載到JVM中 |
RetentionPolicy.RUNTIME | 在運(yùn)行時(shí)有效(即運(yùn)行時(shí)保留),會(huì)被加載到JVM中 |
3、@Documented
????????@Documented 注解表示被它修飾的注解將被 javadoc 工具提取成文檔。
4、Inherited
????????@Inherited 注解表示被它修飾的注解具有繼承性,即如果一個(gè)類聲明了被 @Inherited 修飾的注解,那么它的子類也將具有這個(gè)注解。
2、創(chuàng)建一個(gè) AOP 切面類
只要在類上加個(gè) @Aspect 注解即可。@Aspect 注解用來描述一個(gè)切面類,定義切面類的時(shí)候需要打上這個(gè)注解。@Component 注解將該類交給 Spring 來管理。在這個(gè)類里面實(shí)現(xiàn)第一步白名單校驗(yàn)邏輯:
@Aspect
@Order(3)
@Slf4j
@Component
public class SupplierAspect {
@Around("@annotation(supplierCheck)")
public Object around(ProceedingJoinPoint pjp, SupplierCheck supplierCheck) throws Throwable {
try {
Object obj = pjp.getArgs()[0];
// 業(yè)務(wù)邏輯
doCheck(obj, supplierCheck);
return pjp.proceed(pjp.getArgs());
} catch (BizException e) {
log.warn(e.getLogMsg());
return buildErrorMsg(e.getApiCode(), e.getApiMsg());
} catch (Exception e) {
log.error("SupplierAspect 調(diào)用失敗 ", e);
return buildErrorMsg(ResponseCode.FAILED.code, ResponseCode.FAILED.desc);
}
}
private void doCheck(Object obj, SupplierCheck,supplierCheck) throws BizException {
AppAuthRequest authRequest = JSON.parseObject(JSON.toJSONString(obj), AppAuthRequest.class);
log.info("authRequest = {}", authRequest);
//白名單校驗(yàn)
if (supplierCheck.checkIpWhite()) {
// 白名單業(yè)務(wù)邏輯
}
}
3、創(chuàng)建接口類,并在目標(biāo)方法上標(biāo)注自定義注解?SupplierCheck :
@PostMapping("/getSupplierOrderDetailInfo")
@ApiOperation(value = "售賣渠道訂單查詢", notes = "售賣渠道訂單查詢")
@SupplierCheck
public ApiResultResponse<SupplierOrderDetailResponse> getSupplierOrderDetailInfo(@RequestBody @Valid SupplierOrderDetailRequest request) {
SupplierOrderDetailResponse response = appVipOrderService.getSupplierOrderDetailInfo(request);
return ApiResponseUtils.buildSuccessMsg(response);
}
3、有人會(huì)問,上面一個(gè)接口設(shè)置了多個(gè)切面類進(jìn)行了校驗(yàn)怎么辦?這些切面的執(zhí)行順序如何管理?
很簡(jiǎn)單,一個(gè)自定義的 AOP 注解可以對(duì)應(yīng)多個(gè)切面類,這些切面類執(zhí)行順序由 @Order 注解管理,該注解后的數(shù)字越小,所在切面類越先執(zhí)行。文章來源:http://www.zghlxwxcb.cn/news/detail-745195.html
比如上面接口中?@ApiOperation 增強(qiáng)的注解中(第一個(gè)實(shí)例介紹的)的 @Order(1),那么這個(gè)注解先執(zhí)行,@SupplierCheck 白名單校驗(yàn)的注解的 @Order(3) 后面執(zhí)行。文章來源地址http://www.zghlxwxcb.cn/news/detail-745195.html
到了這里,關(guān)于【SpringBoot】AOP 自定義注解的使用詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!