spring-boot-starter-aop及其使用場景說明

如今,AOP(Aspect Oriented Programming)已經不是什麼嶄新的概念了,在經歷了代碼生成、動態代理、字節碼增強甚至靜態編譯等不同時代的洗禮之後,Java 平臺上的 AOP 方案基本上已經以 SpringAOP 結合 AspectJ 的方式穩固下來(雖然大家依然可以自己通過各種字節碼工具偶爾“打造一些輪子”)。

現在 Spring 框架提供的 AOP 方案倡導了一種各取所長的方案,即使用 SpringAOP 的面向對象的方式來編寫和組織織入邏輯,並使用 AspectJ 的 Pointcut 描述語言配合 Annotation 來標註和指明織入點(Jointpoint)。

原則上來說,我們只要引入 Spring 框架中 AOP 的相應依賴就可以直接使用 Spring 的 AOP 支持了,不過,為了進一步為大家使用 SpringAOP 提供便利,SpringBoot 還是“不厭其煩”地為我們提供了一個 spring-boot-starter-aop 自動配置模塊。

spring-boot-starter-aop 自動配置行為由兩部分內容組成:

  1. 位於 spring-boot-autoconfigure的org.springframework.boot.autoconfigure.aop.AopAutoConfiguration 提供 @Configuration 配置類和相應的配置項。
  2. spring-boot-starter-aop 模塊自身提供了針對 spring-aop、aspectjrt 和 aspectjweaver 的依賴。

一般情況下,只要項目依賴中加入了 spring-boot-starter-aop,其實就會自動觸發 AOP 的關聯行為,包括構建相應的 AutoProxyCreator,將橫切關注點織入(Weave)相應的目標對象等,不過 AopAutoConfiguration 依然為我們提供了可憐的兩個配置項,用來有限地干預 AOP 相關配置:

  • spring.aop.auto=true
  • spring.aop.proxy-target-class=false

對我們來說,這兩個配置項的最大意義在於:允許我們投反對票,比如可以選擇關閉自動的 aop 配置(spring.aop.auto=false),或者啟用針對 class 而不是 interface 級別的 aop 代理(aop proxy)。

AOP 的應用場景很多,我們不妨以當下最熱門的 APM(Application Performance Monitoring)為實例場景,嘗試使用 spring-boot-starter-aop 的支持打造一個應用性能監控的工具原型。

spring-boot-starter-aop 在構建 spring-boot-starter-metrics 自定義模塊中的應用

對於應用性能監控來說,架構邏輯上其實很簡單,基本上就是三步走(如圖 1 所示)。

本節暫時只構建一個 spring-boot-starter-metrics 自定義的自動配置模塊用來解決“應用性能數據採集”的問題。

spring-boot-starter-aop及其使用場景說明

圖 1 應用性能監控關鍵環節示意圖

在此之前,有幾個原則我們需要先說明一下:

雖然說採集應用性能數據可以幫助我們更好地分析和改進應用的性能指標,但這不意味著可以藉著 APM 的名義對應用的核心職能形成侵害,加上應用性能數據採集功能一定會對應用的性能本身帶來拖累,你拿到的所謂性能數據是分攤了你的數據採集方案帶來的負擔,所以,一般情況下,最好把應用性能數據採集模塊的性能損耗控制在 10% 以內甚至更小。

SpringAOP 其實提供了多種橫切邏輯織入機制(Weaving),性能損耗上也是各有差別,從運行期間的動態代理和字節碼增強 Weavng,到類加載期間的 Weaving,甚至高冷的 AspectJ 二次靜態編譯 Weaving,大家可以根據情況靈活把握。

針對應用性能數據的採集,最好對應用開發者是透明的,通過配置外部化的形式,可以最大限度地剝離這部分對應用開發者來說非核心的關注點,只在部署和運行之前確定採集點並配置上線即可。

雖然本節實例採用基於 @Annotation 的方式來標註性能採集點,但不意味著這是最優的方式,更多是基於技術方案(SpringAOP)的現狀給出的一種實踐方式。

下面我們正式著手構建 spring-boot-starter-metrics 自定義的自動配置模塊的設計和實現方案。

筆者一向是隻在有必要的時候才重新“造輪子”,絕不會為了炫技而去“造輪子”,所以,本次的主角我們選擇 Java 中的 Dropwizard Metrics 這個類庫作為打造我們 APM 原型的起點。

Dropwizard Metrics 為我們提供了多種不同類型的應用數據度量方案,且通過相應的數據處理算法在性能和批量狀態的管理上做了很優秀的工作,只不過,如果我們直接用它的 API 來對自己的應用代碼進行度量的話,那寫起來代碼太多,而且這些性能代碼混雜在應用的核心邏輯執行路徑上,一個是界面不友好,另外一個就是不容易維護:

<code>public class MockService implements InitializingBean {
@Autowired
MetricRegistry metricRegistry;
private Timer timer;
private Counter counter;
// define more other metrics...
public void doSth() {
counter.inc();
Timer.Context context = timer.time();
try {
System.out.println("just do something.");
} finally {
context.stop();
}
}
@Override
public void afterPropertiesSet() throws Exception {
timer = metricRegistry.timer("timerToProfilingDoSthMethod");
counter = metricRegistry.counter("counterForDoSthMethod");
}

}/<code>

所以,對於這些非功能性的性能度量代碼,我們可以使用 AOP 的方式剝離到相應的 Aspect 中單獨維護,而為了能夠將這些性能度量的 Aspect 掛接到指定的待度量代碼上,基於現有的方案選型。

可以使用 metrics-annotation 提供的一系列 Annotation 來標註織入位置,這樣,開發者只要在需要度量的代碼位置上標註相應的 Annotation,我們提供的 spring-boot-starter-metrics 自定義的自動配置模塊就會自動地收集這些位置上指定的性能度量數據。

首先,我們通過 http://start.spring.io/ 構建一個 SpringBoot 的腳手架項目,選擇以 Maven 編譯(選擇用 Gradle 的同學自行甄別後面的配置如何具體進行),然後在創建好的 SpringBoot 腳手架項目的 pom.xml 中添加如下必要配置:

<code>
<project> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelversion>4.0.0/<modelversion>
<groupid>com.keevol/<groupid>
<artifactid>spring-boot-starter-metrics/<artifactid>
<version>0.0.1-SNAPSHOT/<version>
<packaging>jar/<packaging>
<name>spring-boot-starter-metrics/<name>
<description>auto configuration module for dropwizard metrics/<description>
<parent>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-parent/<artifactid>
<version>1.3.0.RELEASE/<version>
<relativepath>
/<parent>
<properties>

<project.build.sourceencoding>UTF-8/<project.build.sourceencoding>
<java.version>1.8/<java.version>
<metrics.version>3.1.2/<metrics.version>
/<properties>
<dependencies>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter/<artifactid>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-aop/<artifactid>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-actuator/<artifactid>
/<dependency>
<dependency>
<groupid>io.dropwizard.metrics/<groupid>
<artifactid>metrics-core/<artifactid>
<version>
${metrics.version}
/<version>
/<dependency>
<dependency>
<groupid>io.dropwizard.metrics/<groupid>
<artifactid>metrics-annotation/<artifactid>
<version>${metrics.version}/<version>
/<dependency>
<dependency>
<groupid>org.aspectj/<groupid>
<artifactid>aspectjrt/<artifactid>
<version>1.8.7/<version>
/<dependency>
/<dependencies>
/<project>/<code>

pom.xml 中有幾個關鍵配置需要關注:

  • 繼承了 spring-boot-starter-parent,用於加入 springboot 的相關依賴。
  • 添加了 spring-boot-starter-aop 依賴。
  • 添加了 io.dropwizard.metrics 下相應的依賴,用來引入 dropwizard metrics 類庫和必要的 Annotations。
  • 添加了 spring-boot-starter-actuator,這個自動配置模塊教程後面會跟大家進一步介紹,在這裡我們主要是引入它對 dropwizard metrics 和 JMX 的一部分自動配置邏輯,比如針對 MetricRegistry 和 MBeanServer 的自動配置,這樣我們就可以直接 @Autowired 來注入使用 MetricRegistry 和 MBeanServer。

至於 aspectjrt,是使用了最新的版本,原則上spring-boot-starter-aop已經有依賴,這裡可以不用明確添加配置。

如果單單是一個提供必要依賴的自動配置模塊,那麼到這裡其實就可以結束了,但我們的 spring-boot-starter-metrics 需要使用 AOP 提供相應的橫切關注點邏輯。

所以,還需要編寫並提供一些必要的代碼組件,因此,最少我們先要提供一個 @Configuration 配置類,用於將我們即將提供的這些 AOP 邏輯暴露給使用者:

<code>@Configuration
@ComponentScan({ "com.keevol.springboot.metrics.lifecycle",
"com.keevol.springboot.metrics.aop" })
@AutoConfigureAfter(AopAutoConfiguration.class)
public class DropwizardMetricsMBeansAutoConfiguration {
@Value("${metrics.mbeans.domain.name:com.keevol.metrics}")
String metricsMBeansDomainName;
@Autowired
MBeanServer mbeanServer;
@Autowired
MetricRegistry metricRegistry;
@Bean
public JmxReporter jmxReporter() {
JmxReporter reporter = JmxReporte.forRegistry(metricRegistry)
.inDomain(metricsMBeansDomainName).registerWith(mbeanServer)
.build();
return reporter;
}

}/<code>

然後就是將這個配置類添加到 META-INF/spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\com.keevol.springboot.metrics.autocfg.DropwizardMetricsMBeansAuto-ConfigurationOK,

不要認為將 spring-boot-starter-metrics 打包作為類庫發佈出去就可以了,AOP 相關的代碼還沒寫。

我們回頭來看 DropwizardMetricsMBeansAutoConfiguration 配置類,這個配置類的實現很簡單,注入了 MBeanServer 和 MetricRegistry 的實例,並開放了一個 metrics.mbeans.domain.name 配置屬性(默認值 com.keevol.metrics)便於使用者指定自定義的 MBean 暴露和訪問的命名空間。

當然,以上給這些其實都不是重點,因為它們都只是為了將我們要採集的性能數據指標以 JMX 的形式暴露出去而服務的,重點在於 DropwizardMetricsMBeansAutoConfiguration 頭頂上的那幾頂“帽子”:

  • @Configuration 自然不必說了,這是一個 JavaConfig 配置類。
  • @ComponentScan({"com.keevol.springboot.metrics.lifecycle","com.keevol.springboot.metrics.aop"}),為了簡便,讓 @ComponentScan 把這兩個 java package 下的所有組件都加載到 IoC 容器中,這些組件就包括我們要提供的一系列與 AOP 和 Dropwizard Metrics 相關的實現邏輯。
  • @AutoConfigureAfter(AopAutoConfiguration.class)告訴 SpringBoot:“我希望 DropwizardMetricsMBeansAutoConfiguration 在 AopAutoConfiguration 完成之後進行配置”。

現在,最後的秘密就隱藏在 @ComponentScan 背後的兩個 java package 之下了。

首先是 com.keevol.springboot.metrics.aop,在這個 java package 下面,我們只提供了一個 AutoMetricsAspect,其定義如下:

<code>@Component
@Aspectpublic
class AutoMetricsAspect {
protected ConcurrentMap<string> meters = new ConcurrentHashMap<>();
protected ConcurrentMap<string> exceptionMeters = new ConcurrentHashMap<>();
protected ConcurrentMap<string> timers = new ConcurrentHashMap<>();
protected ConcurrentMap<string> counters = new ConcurrentHashMap<>();
@Autowired
MetricRegistry metricRegistry;
@Pointcut(value = "execution(public * *(..))")
public void publicMethods() {
}
@Before("publicMethods() && @annotation(countedAnnotation)")
public void instrumentCounted(JoinPoint jp, Counted countedAnnotation) {
String name = name(jp.getTarget().getClass(), StringUtils.hasLength(countedAnnotation.name()) ? countedAnnotation.name() : jp.getSignature().getName(), "counter");
Counter counter = counters.computeIfAbsent(name, key -> metricRegistry.counter(key));
counter.inc();
}

@Before("publicMethods() && @annotation(meteredAnnotation)")
public void instrumentMetered(JoinPoint jp, Metered meteredAnnotation) {
String name = name(jp.getTarget().getClass(), StringUtils.hasLength(meteredAnnotation.name()) ? meteredAnnotation.name() : jp.getSignature().getName(), "meter");
Meter meter = meters.computeIfAbsent(name, key -> metricRegistry.meter(key));
meter.mark();
}
@AfterThrowing(pointcut = "publicMethods() && @annotation(exMe-teredAnnotation)", throwing = "ex")
public void instrumentExceptionMetered(JoinPoint jp, Throwable ex, ExceptionMetered exMeteredAnnotation) {
String name = name(jp.getTarget().getClass(), StringUtils.hasLength(exMeteredAnnotation.name()) ? exMeteredAnnotation.name() : jp.getSignature().getName(), "meter", "exception");
Meter meter = exceptionMeters.computeIfAbsent(name, meterName -> metricRegistry.meter(meterName));
meter.mark();
}
@Around("publicMethods() && @annotation(timedAnnotation)")
public Object instrumentTimed(ProceedingJoinPoint pjp, Timed timedAnnotation) throws Throwable {
String name = name(pjp.getTarget().getClass(), StringUtils.hasLength(timedAnnotation.name()) ? timedAnnotation.name() : pjp.getSignature().getName(), "timer");
Timer timer = timers.computeIfAbsent(name, inputName -> metricRegistry.timer(inputName));
Timer.Context tc = timer.time();
try {
return pjp.proceed();
} finally {
tc.stop();
}
}
}/<string>/<string>/<string>/<string>/<code>

@Aspect+@Component 的目的在於告訴 Spring 框架:“我是一個 AOP 的 Aspect 實現類並且你可以通過 @ComponentScan 把我加入 IoC 容器之中。”當然,這不是重點。

io.dropwizard.metrics:metrics-annotation 這個依賴包為我們提供了幾個有趣的 Annotation:

  • Timed
  • Gauge
  • Counted
  • Metered
  • ExceptionMetered

這些語義良好的 Annotation 定義可以用來標註相應的 AOP 邏輯擴展點,比如,針對同一個 MockService,我們可以將性能數據的度量和採集簡化為只標註一兩個 Annotation 就可以了:

<code>@Component
public class MockService {
@Timed
@Counted
public void doSth() {
System.out.println("just do something.");
}
} /<code>

但是,Annotation 註定只是 Annotation,它們只是一些標記信息,要讓它們發揮作用,需要有“伯樂”的眷顧,所以,AutoMetricsAspect 在這裡就是這些 Dropwizard Metrics Annotation 的“伯樂”。

通過攔截每一個 public 方法並檢查方法上是否存在某個 metrics annotation,我們就可以根據具體的 metrics annotation 的類型,為匹配的方法注入相應性能數據採集代碼邏輯,從而完成整個基於 AOP 和 dropwizard metrics 的應用性能數據採集方案的實現。

受限於 SpringAOP 自身的一些限制,並不是所有 AOP 的 Joinpoint 類型都支持,而且,以上原型代碼方向也不見得是性能最優的方案,大家需要結合自己的目標和手上可用的技術手段,根據自己的具體應用場景具體分析和權衡。

最後

有需要Spring Boot視頻教程的小夥伴們注意啦:

點贊+關注+轉發+私信關鍵詞【boot】即可免費領取!!


分享到:


相關文章: