本文繼續上一篇使用Metrics方法級遠端監控Java程式來講。在上文中,我們其實只是實現了功能,但是如果做成庫,給多個工程使用,那就還差一些。於是我對這個庫又做了一些優化。
1 不足點
- 寫死了切點@RestController,@Controller,和@Service,不靈活。然而我們專案中部分服務還使用了自己自定義的註解,如@XXController和@XXService等(這裡就不寫具體名字了)。在我們的業務邏輯中同屬於Controller層和Service層。如果是之前的編碼方式,這些類中的方法就監控不到了。於是我希望監控的切面是可以配置的。
- 寫死了包名,如果我換一個業務,起不同的包名,就監控不到了。所以我希望包名也是可以配置的。
2 實現可配置的切面
2.1 希望的使用姿勢
我希望使用這個庫的工程,可以通過以下方式決定自己要監控的類。
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
// 這是我們自定義的Controller註解
import com.sinafenqi.cashloan.annotations.XXXControler;
@SpringBootApplication
//通過在Application上加一個註解,配置切點
@MonitorEnable({Controller.class, Service.class, XXXControler.class})
public class MonitorApplication {
public static void main(String[] args) {
SpringApplication.run(MonitorApplication.class, args);
}
}
複製程式碼
我希望可以在Application上使用一個註解(如:MonitorEnable),然後在其中指定切點(甚至自定義的註解)。這樣我們便可以任意選擇自己想要監控的業務層程式碼。那麼接下來看看如何實現。
2.2 從自定義註解中獲取配置的切點
2.2.1 自定義配置使用的註解
首先自定義註解MonitorEnable
,並定義value
為Annotation的Class陣列。這裡我們為了簡單,限制一下切點的型別。之後如果需要擴充套件功能再放開。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MonitorEnable {
Class<? extends Annotation>[] value();
}
複製程式碼
這樣其他工程就可以在類上使用這個註解了。接下來我們獲取註解中的Value值。
2.2.2 從註解中獲取配置的切點
MonitorEnable
作用是提供配置資料的。那麼我們想要獲取它裡面資訊的話,需要為MonitorEnable
加上@Import
註解,併為其指定一個配置類,這裡我們指定MonitorConfig
為配置類。
更改後的MonitorEnable
註解檔案:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MonitorConfig.class)
public @interface MonitorEnable {
Class<? extends Annotation>[] value();
}
複製程式碼
要想在MonitorConfig
配置類中獲取MonitorEnable
中的配置資訊,需要實現ImportAware
介面,這樣Spring在載入完MetaData
的時候會回撥setImportMetadata
方法。我們可以在這裡獲取註解中的內容。
@Configuration
@Log
public class MonitorConfig implements ImportAware{
// MonitorProperty類中包裝了監控屬性。用來儲存配置的切點
public MonitorProperty monitorProperty = new MonitorProperty();
// 原來的內容不變,這裡省略,詳情請參考上一篇文章
// 這裡把MonitorProperty裝載到Spring容器。以供其他人使用
@Bean
public MonitorProperty monitorProperty() {
return monitorProperty;
}
// 這裡獲取配置的切點,並設定到monitorProperty中
@Override
public void setImportMetadata(AnnotationMetadata annotationMetadata) {
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(MonitorEnable.class.getName(), false);
AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(attributes);
Class<? extends Annotation>[] aopClasses = (Class<? extends Annotation>[]) annotationAttributes.getClassArray("value");
if (aopClasses == null || aopClasses.length == 0) {
throw new RuntimeException("monitor cannot get aop annotation classes. nothing to monitor. Please use MonitorEnable annotation on your application.");
}
monitorProperty.setAopAnnotationClasses(aopClasses);
}
}
複製程式碼
MonitorProperty類:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MonitorProperty {
private Class<? extends Annotation>[] aopAnnotationClasses;
}
複製程式碼
這樣我們在程式啟動中就可以獲取MonitorEnable
使用者配置的值,並且儲存在了MonitorProperty
中。
2.3 根據切點為監控方法準備Timer
現在切點已經是配置進來的了,那麼為監控方法準備Timer
這一步也要做相應更改。這一步比較簡單。MethodMonitorCenter
類增加程式碼如下,從MonitorProperty
中獲取切點,替換之前寫死的邏輯:
@Log
public class MethodMonitorCenter implements ApplicationContextAware {
// 將MonitorProperty注入進來
@Autowired
private MonitorProperty monitorProperty;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Object> monitorBeans = new HashMap<>();
// 這裡從monitorProperty中獲取切點
Class<? extends Annotation>[] classes = monitorProperty.getAopAnnotationClasses();
if (classes == null || classes.length == 0) {
return;
}
for (Class<? extends Annotation> aClass : classes) {
// 這裡使用獲取的切點獲取要監控的類,下面的篩選邏輯與之前相同,省略
monitorBeans.putAll(applicationContext.getBeansWithAnnotation(aClass));
}
// 之後和以前一摸一樣,這裡省略。
}
}
複製程式碼
2.4 根據切點建立切面
對Spring AOP還不熟悉的讀者可以上網上搜尋一下。有很多的文章介紹。我就不再贅述了。
我們常見的Spring AOP的使用姿勢都是硬編碼方式。所謂硬編碼的方式就是指,Java註解(我上一篇文章中所使用的方法),和XML配置的方式。現在我們的切點是配置進來的。那麼就不能通過硬編碼來實現了。然而Java動態代理和AspectJ都需要知道代理目標類。顯然也不適合我們這種場景。但是我相信硬編碼能夠做到的,軟編碼肯定可以做到,只不過可能會比較麻煩。於是翻了翻Spring原始碼。找到了方法。本篇文章不想涉及原始碼和原理,只講實現。
前提,刪除上一篇文章中的MetricsMonitorAOP
類,因為我們已經不能用硬編碼的方式了。
2.4.1 準備建立切面處理類
自定義類MonitorAdvice
實現MethodInterceptor
介面,其中的invoke
方法相當於環繞切面的方法。
@Log
public class MonitorAdvice implements MethodInterceptor {
MetricRegistry metricRegistry;
public MonitorAdvice(MetricRegistry metricRegistry) {
this.metricRegistry = metricRegistry;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String methodName = invocation.getMethod().toString();
log.info("monitor invoke. method: " + methodName);
boolean contains = metricRegistry.getNames().contains(methodName);
if (!contains) {
return invocation.proceed();
}
log.info("monitor start method = [" + methodName + "]");
Timer timer = metricRegistry.timer(methodName);
Timer.Context context = timer.time();
try {
return invocation.proceed();
} finally {
context.stop();
}
}
}
複製程式碼
2.4.2 用軟程式碼根據切點建立切面
MonitorConfig
類中增加程式碼,講解請看註釋:
@Configuration
@Log
// 讓MonitorConfig實現BeanFactoryPostProcessor介面,
// 在其postProcessBeanFactory方法中我們可以軟程式碼向Spring裝載Bean
public class MonitorConfig implements ImportAware, BeanFactoryPostProcessor {
// 該類中其他程式碼保留不變,省略
// 這裡將上面自定義的MonitorAdvice類裝載到Spring中
@Bean
public MonitorAdvice monitorAdvice(MetricRegistry metricRegistry) {
return new MonitorAdvice(metricRegistry);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory;
MonitorAdvice monitorAdvice = (MonitorAdvice) factory.getBean("monitorAdvice");
// 獲取配置的切點
Class<? extends Annotation>[] classes = monitorProperty.getAopAnnotationClasses();
if (classes == null || classes.length == 0) {
return;
}
for (Class<? extends Annotation> aClass : classes) {
// 軟程式碼根據切點建立Pointcut
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(aClass);
// 軟程式碼建立Advisor(硬編碼的方式也是轉化成這個東西)
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(DefaultPointcutAdvisor.class.getName())
.addPropertyValue("pointcut", pointcut)
.addPropertyValue("advice", monitorAdvice)
.getBeanDefinition();
// 然後將Advisor裝載到Spring
factory.registerBeanDefinition("monitorAdvisor" + aClass.getName(), beanDefinition);
}
}
}
複製程式碼
這樣,就可以通過軟程式碼的方式實現之前硬編碼實現的切面功能。
3 可配置的包名
這個相對於上一個優化簡單很多。
3.1 希望的使用姿勢
我希望使用者可以通過以下兩種方式任意一種,達到配置包名的需求:
通過application.properties
檔案配置,如,在application.properties
檔案中增加如下程式碼:
monitor.property.basePackages=com.xxx,com.yyy
複製程式碼
或者通過MonitorEnable
註解進行如下配置:
@MonitorEnable(value = {/*這裡是配置的切點們,省略*/}, basePackages = {"com.xxx","com.yyy"})
複製程式碼
來實現監控制定的包名。
3.2 實現包名可配置
這裡只講通過application.properties
檔案配置的方式實現方案。通過註解的配置的實現只是獲取方式不同,有興趣的可以直接去看原始碼。
3.2.1 MonitorProperty增加包名屬性
我們可以注意到,3.1中都是可以配置多個包名的,那麼在MonitorProperty
中增加屬性basePackages
:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MonitorProperty {
private Class<? extends Annotation>[] aopAnnotationClasses;
private String[] basePackages;
}
複製程式碼
3.2.2 為MonitorProperty賦值
這一步方法有很多種,我們使用ConfigurationProperties
註解為其賦值:
@Configuration
@Log
public class MonitorConfig implements ImportAware, BeanFactoryPostProcessor {
@Bean
// 在裝載MonitorProperty的地方加上ConfigurationProperties註解,為其賦值。
@ConfigurationProperties("monitor.property")
public MonitorProperty monitorProperty() {
return monitorProperty;
}
}
複製程式碼
3.2.3 更改篩選邏輯
已經獲取了使用者配置的包名,那麼我們用使用者配置的包名重寫原來根據包名篩選的邏輯:
@Log
public class MethodMonitorCenter implements ApplicationContextAware {
@Autowired
private MonitorProperty monitorProperty;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Object> monitorBeans = new HashMap<>();
// 獲取要監控的Bean過程省略
monitorBeans.values().stream()
.map(obj -> obj.getClass().getName())
.map(this::trimString)
.map(this::getClass)
.filter(Objects::nonNull)
.filter(this::isInPackages) // 這裡根據包名過濾
.forEach(this::getClzMethods);
}
private boolean isInPackages(Class<?> clazz) {
// 根據使用者配置的包名過濾想要監控的類
String[] basePackages = monitorProperty.getBasePackages();
if (basePackages == null || basePackages.length == 0) {
return true;
}
return Stream.of(basePackages).anyMatch(basePackage -> clazz.getName().startsWith(basePackage));
}
// 其他程式碼不變,省略
}
複製程式碼
這樣使用者就可以配置自己的包名了。
4 優化效果
使用者通過在自己的Application類上增加MonitorEnable
註解,然後可以自定義配置切點:
@MonitorEnable({RestController.class, Controller.class, Service.class, XXControler.class})
複製程式碼
然後通過在application.properties
檔案中配置monitor.property.basePackages
屬性,配置自己想監控的包名:
monitor.property.basePackages=com.xxx,com.yyy
複製程式碼
然後通過/monitor/metrics
這個Restful介面獲取監控方法的資料。
5 總結
本次優化的兩點中,使用軟程式碼方式建立切面是比較困難的,相關的文獻比較少。如果有時間,我會單獨寫一篇文章講解一下原始碼和原理。
最後歡迎關注我的個人公眾號。提問,嘮嗑,都可以。