使用Metrics方法级远程监控Java程序(优化)

本文继续上一篇使用Metrics方法级远程监控Java程序来讲。在上文中,我们其实只是实现了功能,但是如果做成库,给多个工程使用,那就还差一些。于是我对这个库又做了一些优化。

1 不足点

  1. 写死了切点@RestController,@Controller,和@Service,不灵活。然而我们项目中部分服务还使用了自己自定义的注解,如@XXController和@XXService等(这里就不写具体名字了)。在我们的业务逻辑中同属于Controller层和Service层。如果是之前的编码方式,这些类中的方法就监控不到了。于是我希望监控的切面是可以配置的。
  2. 写死了包名,如果我换一个业务,起不同的包名,就监控不到了。所以我希望包名也是可以配置的。

2 实现可配置的切面

2.1 希望的使用姿势

我希望使用这个库的工程,可以通过以下方式决定自己要监控的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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数组。这里我们为了简单,限制一下切点的类型。之后如果需要扩展功能再放开。

1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MonitorEnable {
Class<? extends Annotation>[] value();
}

这样其他工程就可以在类上使用这个注解了。接下来我们获取注解中的Value值。

2.2.2 从注解中获取配置的切点

MonitorEnable作用是提供配置数据的。那么我们想要获取它里面信息的话,需要为MonitorEnable加上@Import注解,并为其指定一个配置类,这里我们指定MonitorConfig为配置类。

更改后的MonitorEnable注解文件:

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MonitorConfig.class)
public @interface MonitorEnable {
Class<? extends Annotation>[] value();
}

要想在MonitorConfig配置类中获取MonitorEnable中的配置信息,需要实现ImportAware接口,这样Spring在加载完MetaData的时候会回调setImportMetadata方法。我们可以在这里获取注解中的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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类:

1
2
3
4
5
6
7
8
9
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MonitorProperty {
private Class<? extends Annotation>[] aopAnnotationClasses;
}

这样我们在程序启动中就可以获取MonitorEnable使用者配置的值,并且存储在了MonitorProperty中。

2.3 根据切点为监控方法准备Timer

现在切点已经是配置进来的了,那么为监控方法准备Timer这一步也要做相应更改。这一步比较简单。MethodMonitorCenter类增加代码如下,从MonitorProperty中获取切点,替换之前写死的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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方法相当于环绕切面的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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类中增加代码,讲解请看注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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文件中增加如下代码:

1
monitor.property.basePackages=com.xxx,com.yyy

或者通过MonitorEnable注解进行如下配置:

1
@MonitorEnable(value = {/*这里是配置的切点们,省略*/}, basePackages = {"com.xxx","com.yyy"})

来实现监控制定的包名。

3.2 实现包名可配置

这里只讲通过application.properties文件配置的方式实现方案。通过注解的配置的实现只是获取方式不同,有兴趣的可以直接去看源码。

3.2.1 MonitorProperty增加包名属性

我们可以注意到,3.1中都是可以配置多个包名的,那么在MonitorProperty中增加属性basePackages

1
2
3
4
5
6
7
8
9
10
11
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MonitorProperty {
private Class<? extends Annotation>[] aopAnnotationClasses;
private String[] basePackages;
}

3.2.2 为MonitorProperty赋值

这一步方法有很多种,我们使用ConfigurationProperties注解为其赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@Log
public class MonitorConfig implements ImportAware, BeanFactoryPostProcessor {
@Bean
// 在装载MonitorProperty的地方加上ConfigurationProperties注解,为其赋值。
@ConfigurationProperties("monitor.property")
public MonitorProperty monitorProperty() {
return monitorProperty;
}
}

3.2.3 更改筛选逻辑

已经获取了用户配置的包名,那么我们用用户配置的包名重写原来根据包名筛选的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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注解,然后可以自定义配置切点:

1
@MonitorEnable({RestController.class, Controller.class, Service.class, XXControler.class})

然后通过在application.properties文件中配置monitor.property.basePackages属性,配置自己想监控的包名:

1
monitor.property.basePackages=com.xxx,com.yyy

然后通过/monitor/metrics这个Restful接口获取监控方法的数据。

5 总结

本次优化的两点中,使用软代码方式创建切面是比较困难的,相关的文献比较少。如果有时间,我会单独写一篇文章讲解一下源码和原理。

最后欢迎关注我的个人公众号。提问,唠嗑,都可以。

本文源码