Spring Boot AOP 掃盲
大家好,我是二哥呀。AOP 是 Spring 體系中非常重要的兩個(gè)概念之一(另外一個(gè)是 IoC),今天這篇文章就來(lái)帶大家通過(guò)實(shí)戰(zhàn)的方式,在編程貓 SpringBoot 項(xiàng)目中使用 AOP 技術(shù)為 controller 層添加一個(gè)切面來(lái)實(shí)現(xiàn)接口訪(fǎng)問(wèn)的統(tǒng)一日志記錄。
一、關(guān)于 AOP
AOP,也就是 Aspect-oriented Programming,譯為面向切面編程,是計(jì)算機(jī)科學(xué)中的一個(gè)設(shè)計(jì)思想,旨在通過(guò)切面技術(shù)為業(yè)務(wù)主體增加額外的通知(Advice),從而對(duì)聲明為“切點(diǎn)”(Pointcut)的代碼塊進(jìn)行統(tǒng)一管理和裝飾。
這種思想非常適用于,將那些與核心業(yè)務(wù)不那么密切關(guān)聯(lián)的功能添加到程序中,就好比我們今天的主題——日志功能,就是一個(gè)典型的案例。
AOP 是對(duì)面向?qū)ο缶幊?Object-oriented Programming,俗稱(chēng) OOP)的一種補(bǔ)充,OOP 的核心單元是類(lèi)(class),而 AOP 的核心單元是切面(Aspect)。利用 AOP 可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而降低耦合度,提高程序的可重用性,同時(shí)也提高了開(kāi)發(fā)效率。
我們可以簡(jiǎn)單的把 AOP 理解為貫穿于方法之中,在方法執(zhí)行前、執(zhí)行時(shí)、執(zhí)行后、返回值后、異常后要執(zhí)行的操作。
二、AOP 的相關(guān)術(shù)語(yǔ)
來(lái)看下面這幅圖,這是一個(gè) AOP 的模型圖,就是在某些方法執(zhí)行前后執(zhí)行一些通用的操作,并且這些操作不會(huì)影響程序本身的運(yùn)行。
我們了解下 AOP 涉及到的 5 個(gè)關(guān)鍵術(shù)語(yǔ):
1)橫切關(guān)注點(diǎn),從每個(gè)方法中抽取出來(lái)的同一類(lèi)非核心業(yè)務(wù)
2)切面(Aspect),對(duì)橫切關(guān)注點(diǎn)進(jìn)行封裝的類(lèi),每個(gè)關(guān)注點(diǎn)體現(xiàn)為一個(gè)通知方法;通常使用 @Aspect 注解來(lái)定義切面。
3)通知(Advice),切面必須要完成的各個(gè)具體工作,比如我們的日志切面需要記錄接口調(diào)用前后的時(shí)長(zhǎng),就需要在調(diào)用接口前后記錄時(shí)間,再取差值。通知的方式有五種:
- @Before:通知方法會(huì)在目標(biāo)方法調(diào)用之前執(zhí)行
- @After:通知方法會(huì)在目標(biāo)方法調(diào)用后執(zhí)行
- @AfterReturning:通知方法會(huì)在目標(biāo)方法返回后執(zhí)行
- @AfterThrowing:通知方法會(huì)在目標(biāo)方法拋出異常后執(zhí)行
- @Around:把整個(gè)目標(biāo)方法包裹起來(lái),在被調(diào)用前和調(diào)用之后分別執(zhí)行通知方法
4)連接點(diǎn)(JoinPoint),通知應(yīng)用的時(shí)機(jī),比如接口方法被調(diào)用時(shí)就是日志切面的連接點(diǎn)。
5)切點(diǎn)(Pointcut),通知功能被應(yīng)用的范圍,比如本篇日志切面的應(yīng)用范圍是所有 controller 的接口。通常使用 @Pointcut 注解來(lái)定義切點(diǎn)表達(dá)式。
切入點(diǎn)表達(dá)式的語(yǔ)法格式規(guī)范如下所示:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?
name-pattern(param-pattern)
throws-pattern?)
- modifiers-pattern? 為訪(fǎng)問(wèn)權(quán)限修飾符
- ret-type-pattern 為返回類(lèi)型,通常用 *來(lái)表示任意返回類(lèi)型
- declaring-type-pattern? 為包名
- name-pattern 為方法名,可以使用 * 來(lái)表示所有,或者 set* 來(lái)表示所有以 set 開(kāi)頭的類(lèi)名
- param-pattern) 為參數(shù)類(lèi)型,多個(gè)參數(shù)可以用 , 隔開(kāi),各個(gè)參與也可以使用 * 來(lái)表示所有類(lèi)型的參數(shù),還可以使用 (..) 表示零個(gè)或者任意參數(shù)
- throws-pattern? 為異常類(lèi)型
- ? 表示前面的為可選項(xiàng)
舉個(gè)例子:
@Pointcut("execution(public * com.codingmore.controller.*.*(..))")
表示 com.codingmore.controller 包下的所有 public 方法都要應(yīng)用切面的通知。
三、實(shí)操 AOP 記錄接口訪(fǎng)問(wèn)日志
第一步,在 Spring Boot 項(xiàng)目的 pom.xml 文件中添加 spring-boot-starter-aop 依賴(lài)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
第二步,添加日志信息封裝類(lèi) WebLog,用于記錄什么樣的操作、操作的人是誰(shuí)、開(kāi)始時(shí)間、花費(fèi)的時(shí)間、操作的路徑、操作的方法名、操作主機(jī)的 IP、請(qǐng)求參數(shù)、返回結(jié)果等。
/**
* Controller層的日志封裝類(lèi)
* Created by macro on 2018/4/26.
*/
public class WebLog {
private String description;
private String username;
private Long startTime;
private Integer spendTime;
private String basePath;
private String uri;
private String url;
private String method;
private String ip;
private Object parameter;
private Object result;
//省略了getter,setter方法
}
第三步,添加統(tǒng)一日志處理切面 WebLogAspect。
/**
* 統(tǒng)一日志處理切面
* Created by 石磊
*/
@Aspect
@Component
@Order(1)
public class WebLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);
@Pointcut("execution(public * com.codingmore.controller.*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
@AfterReturning(value = "webLog()", returning = "ret")
public void doAfterReturning(Object ret) throws Throwable {
}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
//獲取當(dāng)前請(qǐng)求對(duì)象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//記錄請(qǐng)求信息(通過(guò)Logstash傳入Elasticsearch)
WebLog webLog = new WebLog();
Object result = joinPoint.proceed();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(ApiOperation.class)) {
ApiOperation log = method.getAnnotation(ApiOperation.class);
webLog.setDescription(log.value());
}
long endTime = System.currentTimeMillis();
String urlStr = request.getRequestURL().toString();
webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
webLog.setIp(request.getRemoteUser());
Map<String,Object> logMap = new HashMap<>();
logMap.put("spendTime",webLog.getSpendTime());
logMap.put("description",webLog.getDescription());
LOGGER.info("{}", JSONUtil.parse(webLog));
return result;
}
}
第四步,運(yùn)行項(xiàng)目,并對(duì) controller 下的某個(gè)控制器進(jìn)行測(cè)試。
Swagger knife4j 訪(fǎng)問(wèn)地址:http://localhost:9022/doc.html
執(zhí)行登錄用戶(hù)查詢(xún)操作:
可以在控制臺(tái)可以看到以下日志信息:
源碼地址:https://github.com/itwanger/coding-more
參考鏈接:
作者 cxuan:https://www.cnblogs.com/cxuanBlog/p/13060510.html
灰小猿:https://bbs.huaweicloud.com/blogs/289045
山高我為峰:https://www.cnblogs.com/liaojie970/p/7883687.html
macrozheng:https://github.com/macrozheng/mall
本文轉(zhuǎn)載自微信公眾號(hào)「沉默王二」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系沉默王二公眾號(hào)。