使用Metrics方法級遠端監控Java程式(優化)

柴澤建_Jack發表於2017-12-27

本文繼續上一篇使用Metrics方法級遠端監控Java程式來講。在上文中,我們其實只是實現了功能,但是如果做成庫,給多個工程使用,那就還差一些。於是我對這個庫又做了一些優化。

1 不足點

  1. 寫死了切點@RestController,@Controller,和@Service,不靈活。然而我們專案中部分服務還使用了自己自定義的註解,如@XXController和@XXService等(這裡就不寫具體名字了)。在我們的業務邏輯中同屬於Controller層和Service層。如果是之前的編碼方式,這些類中的方法就監控不到了。於是我希望監控的切面是可以配置的。
  2. 寫死了包名,如果我換一個業務,起不同的包名,就監控不到了。所以我希望包名也是可以配置的。

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 總結

本次優化的兩點中,使用軟程式碼方式建立切面是比較困難的,相關的文獻比較少。如果有時間,我會單獨寫一篇文章講解一下原始碼和原理。

最後歡迎關注我的個人公眾號。提問,嘮嗑,都可以。

使用Metrics方法級遠端監控Java程式(優化)

本文原始碼

相關文章