本文继续上一篇 使用 Metrics 方法级远程监控 Java 程序 来讲。在上文中,我们其实只是实现了功能,但是如果做成库,给多个工程使用,那就还差一些。于是我对这个库又做了一些优化。
我希望使用这个库的工程,可以通过以下方式决定自己要监控的类。
- 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),然后在其中指定切点(甚至自定义的注解)。这样我们便可以任意选择自己想要监控的业务层代码。那么接下来看看如何实现。
首先自定义注解 MonitorEnable,并定义 value 为 Annotation 的 Class 数组。这里我们为了简单,限制一下切点的类型。之后如果需要扩展功能再放开。
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface MonitorEnable {
- Class<? extends Annotation>[] value();
- }
这样其他工程就可以在类上使用这个注解了。接下来我们获取注解中的 Value 值。
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 中。
现在切点已经是配置进来的了,那么
这一步也要做相应更改。这一步比较简单。MethodMonitorCenter 类增加代码如下,从 MonitorProperty 中获取切点,替换之前写死的逻辑:
- 为监控方法准备Timer
- @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));
- }
- // 之后和以前一摸一样,这里省略。
- }
- }
对 Spring AOP 还不熟悉的读者可以上网上搜索一下。有很多的文章介绍。我就不再赘述了。
我们常见的 Spring AOP 的使用姿势都是硬编码方式。所谓硬编码的方式就是指,Java 注解(我上一篇文章中所使用的方法),和 XML 配置的方式。现在我们的切点是配置进来的。那么就不能通过硬编码来实现了。然而 Java 动态代理和 AspectJ 都需要知道代理目标类。显然也不适合我们这种场景。但是我相信硬编码能够做到的,软编码肯定可以做到,只不过可能会比较麻烦。于是翻了翻 Spring 源码。找到了方法。本篇文章不想涉及源码和原理,只讲实现。
前提,删除上一篇文章中的 MetricsMonitorAOP 类,因为我们已经不能用硬编码的方式了。
自定义类 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();
- }
- }
- }
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);
- }
- }
- }
这样,就可以通过软代码的方式实现之前硬编码实现的切面功能。
这个相对于上一个优化简单很多。
我希望用户可以通过以下两种方式任意一种,达到配置包名的需求:
通过
文件配置,如,在
- application.properties
文件中增加如下代码:
- application.properties
- monitor.property.basePackages=com.xxx,com.yyy
或者通过 MonitorEnable 注解进行如下配置:
- @MonitorEnable(value = {/*这里是配置的切点们,省略*/}, basePackages = {"com.xxx","com.yyy"})
来实现监控制定的包名。
这里只讲通过
文件配置的方式实现方案。通过注解的配置的实现只是获取方式不同,有兴趣的可以直接去看源码。
- application.properties
我们可以注意到,3.1 中都是可以配置多个包名的,那么在 MonitorProperty 中增加属性 basePackages:
- @Data
- @Builder
- @AllArgsConstructor
- @NoArgsConstructor
- public class MonitorProperty {
- private Class<? extends Annotation>[] aopAnnotationClasses;
- private String[] basePackages;
- }
这一步方法有很多种,我们使用
注解为其赋值:
- ConfigurationProperties
- @Configuration
- @Log
- public class MonitorConfig implements ImportAware, BeanFactoryPostProcessor {
- @Bean
- // 在装载MonitorProperty的地方加上ConfigurationProperties注解,为其赋值。
- @ConfigurationProperties("monitor.property")
- public MonitorProperty monitorProperty() {
- return monitorProperty;
- }
- }
已经获取了用户配置的包名,那么我们用用户配置的包名重写原来根据包名筛选的逻辑:
- @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));
- }
- // 其他代码不变,省略
- }
这样使用者就可以配置自己的包名了。
使用者通过在自己的 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 接口获取监控方法的数据。
本次优化的两点中,使用软代码方式创建切面是比较困难的,相关的文献比较少。如果有时间,我会单独写一篇文章讲解一下源码和原理。
最后欢迎关注我的个人公众号。提问,唠嗑,都可以。
来源: https://juejin.im/post/5a4382c46fb9a0450c49b18a