本文继续上一篇 使用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
中。
现在切点已经是配置进来的了,那么 为监控方法准备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)); } // 之后和以前一摸一样,这里省略。 } }
对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接口获取监控方法的数据。
本次优化的两点中,使用软代码方式创建切面是比较困难的,相关的文献比较少。如果有时间,我会单独写一篇文章讲解一下源码和原理。
最后欢迎关注我的个人公众号。提问,唠嗑,都可以。
本文源码