本文以 Spring web 的后台开发讲解。

上一篇讲解了如何使用 jvisualvm 监控 Java 程序。jvisualvm 虽然已经挺强大了,但是在实际的应用中依然不满足我们的需求。现在,我们想要监控应用程序中所有 Controller 提供的接口的访问数量,频次,响应时长。Service 层方法的执行次数,执行时长,频次等等。以便之后对系统的性能优化做准备。这个时候 jvisualvm 已经不能满足我们的需求了。

1 方法级监控 Java 程序的方案

这是我对于方法级监控 Java 程序的方案:

  1. 付费的,比如 YourKit,JProfile 等。我尝试了 YourKit,功能确实强大,但是现在性能并不是我们现在的瓶颈,我们尽量使用不付费的。
  2. Metrics-Spring。Metrics-Spring 需要在每个方法上使用注解。我们采用微服务架构,20 多个服务,每个工程预计平均有 100 左右个方法要监控。如果是一开始就用这个我觉得还可以。
  3. Metrics+Spring AOP。从 Metrics-Spring 中可以看到,Metrics 统计的信息基本满足我们的需求。我们的项目需求是统计 Controller 层和 Service 层的方法。那么可以通过 Spring 中的切面完成我们的需求。

我调查的方案和分析基本这样,其他人如果有更好的方案可以提出一起探讨。

下面是讲解 + 部分代码,本次讲解还有优化篇。

2 Metrics 的功能

关于 Metrics 的使用方法,已经有很多文章介绍了,我在这里推荐我认为还不错的给大家,然后我再介绍的使用方法.

  1. Metrics 介绍 。这篇文章对 Metrics 的基本功能介绍的已经很全面了。
  2. Metrics-Spring 官方文档 。这篇文章介绍了 Metrics 与 Spring 的集成,但是文档感觉不全呀。

其他的文章我就不多分享了,感觉大同小异。没什么太大差别。

3 将 Metrics 相关类装载到 Spring 容器

要使用 Metric,那么首先需要 MetricRegistry。

我们需要提供 Http 的报表,所以我们需要将 MetriCSServlet 注册到 Spring 中,以便可以通过 Http 接口获取监控结果。下面代码我们将监控接口定义为:/monitor/metrics。

  1. import com.codahale.metrics.MetricRegistry;
  2. import com.codahale.metrics.servlets.MetricsServlet;
  3. import org.springframework.boot.web.servlet.ServletRegistrationBean;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6.  
  7. @Configuration
  8. public class MonitorConfig {
  9.  
  10. @Bean
  11. public MetricRegistry metricRegistry() {
  12. return new MetricRegistry();
  13. }
  14.  
  15. @Bean
  16. public ServletRegistrationBean servletRegistrationBean(MetricRegistry metricRegistry) {
  17. return new ServletRegistrationBean(new MetricsServlet(metricRegistry), "/monitor/metrics");
  18. }
  19.  
  20. }

4 提供可控的终端报表

另外,为了方便调试,我希望支持终端输出报表的方式。但是要可以配置打开和关闭,于是我使用另外一个配置类,

  1. ConditionalOnProperty
注解,让配置根据配置属性加载:

  1. import com.codahale.metrics.ConsoleReporter;
  2. import com.codahale.metrics.MetricRegistry;
  3. import lombok.extern.java.Log;
  4. import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.context.annotation.Import;
  8.  
  9. import java.util.concurrent.TimeUnit;
  10.  
  11. @Configuration
  12. @Log
  13. @ConditionalOnProperty(prefix = "monitor.report", name = "console", havingValue = "true")
  14. @Import(MonitorConfig.class)
  15. public class MonitorReportConfig {
  16.  
  17.  
  18. @Bean
  19. public ConsoleReporter consoleReporter(MetricRegistry metrics) {
  20. ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics)
  21. .convertRatesTo(TimeUnit.SECONDS)
  22. .convertDurationsTo(TimeUnit.MILLISECONDS)
  23. .build();
  24. reporter.start(10, TimeUnit.SECONDS);
  25. return reporter;
  26. }
  27.  
  28. }

这样可以在工程中的

  1. application.properties
文件中,通过下面配置开启终端报表,10 秒钟输出一次:

  1. monitor.report.console = true

5 为要监控的方法准备 Timer

Metrics 中可以统计的信息很多,其中 Timer 已经满足了我们需要的信息。

我为什么要先为监控的方法准备 Timer,而不是在方法执行的时候再创建呢?原因有两点。

  1. 我们既关心方法被调,也关心它从来没有被调用,如果是在方法执行的时候再创建,那么我们就不知道是方法没有被监控还是方法没有被调用了。
  2. 我们之后打算直接对 @RestController,@Controller 和 @Service 注解进行切面。这种类级别的切面力度会包含我们不关心的方法,例如 toString 等方法,所以准备好关心的方法,调用的时候发现不是我们关心的方法直接放过。

我们使用 MethodMonitorCenter 类来收集我们想要监控的方法。通过实现

  1. ApplicationContextAware
接口,在 Spring 容器装载完毕之后,会回掉
  1. setApplicationContext
方法,我们通过
  1. getBeansWithAnnotation
方法找到包含指定注解的类。然后对其进行过滤,并获取我们想要监控的方法。在最后我们通过
  1. metricRegistry.timer(method.toString());
方法为我们的关心的方法准备一个 timer。

  1. @Component
  2. @Getter
  3. @Log
  4. public class MethodMonitorCenter implements ApplicationContextAware {
  5.  
  6. public static final String PACKAGE_NAME = "com.sinafenqi"; // 这里换成自己的包名
  7.  
  8. @Autowired
  9. private MetricRegistry metricRegistry;
  10.  
  11. @Override
  12. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  13. Map<String, Object> monitorBeans = new HashMap<>();
  14. monitorBeans.putAll(applicationContext.getBeansWithAnnotation(Controller.class));
  15. monitorBeans.putAll(applicationContext.getBeansWithAnnotation(Service.class));
  16. monitorBeans.putAll(applicationContext.getBeansWithAnnotation(RestController.class));
  17.  
  18. log.info("monitor begin scan methods");
  19. monitorBeans.values().stream()
  20. .map(obj -> obj.getClass().getName())
  21. .map(this::trimString)
  22. .map(clzName -> {
  23. try {
  24. return Class.forName(clzName);
  25. } catch (Exception e) {
  26. return null;
  27. }
  28. })
  29. .filter(Objects::nonNull)
  30. .filter(aClass -> aClass.getName().startsWith(PACKAGE_NAME))
  31. .forEach(this::getClzMethods);
  32. }
  33.  
  34. private void getClzMethods(Class<?> clz) {
  35. Stream.of(clz.getDeclaredMethods())
  36. .filter(method -> method.getName().indexOf('$') < 0)
  37. .forEach(method -> {
  38. log.info("add method timer, method name :" + method.toGenericString());
  39. metricRegistry.timer(method.toString());
  40. });
  41. }
  42.  
  43. private String trimString(String clzName) {
  44. if (clzName.indexOf('$') < 0) return clzName;
  45. return clzName.substring(0, clzName.indexOf('$'));
  46. }
  47.  
  48. }

6 在切面中对方法进行监控

然后我们可以在切面中监控我们关心的方法。这里使用环绕式切面对 RestController,Controller,和 Service 三个注解做切面。这样就可以在方法之前和之后加一些监控代码。当进入 around 函数的时候,我们先去 MetricRegistry 中查找有没有对应的 timer,如果没有说明不是我们关心的方法,那么我们就可以直接执行,如果存在,那么我就对其进行监控。详情可见代码:

  1. @Component
  2. @Aspect
  3. @Log
  4. public class MetricsMonitorAOP {
  5.  
  6. @Autowired
  7. private MetricRegistry metricRegistry;
  8.  
  9. @Pointcut("@within(org.springframework.stereotype.Controller)" +
  10. "||@within(org.springframework.stereotype.Service)" +
  11. "||@within(org.springframework.web.bind.annotation.RestController)")
  12. public void monitor() {
  13.  
  14. }
  15.  
  16. @Around("monitor()")
  17. public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
  18. String target = joinPoint.getSignature().toLongString();
  19. Object[] args = joinPoint.getArgs();
  20. if (!metricRegistry.getNames().contains(target)) {
  21. return joinPoint.proceed(args);
  22. }
  23. Timer timer = metricRegistry.timer(target);
  24. Timer.Context context = timer.time();
  25. try {
  26. return joinPoint.proceed(args);
  27. } finally {
  28. context.stop();
  29. }
  30. }
  31. }

7 效果

之后访问 / monitor/metrics 接口,就可以以 Json 的数据格式获取监控结果。大家实验的时候记得把 MethodMonitorCenter 类中的 PACKAGE_NAME 常量换成自己的。

现在基本已经实现监控所有 Controller,和 Service 层我们定义的方法了,但是代码依然有很大的优化空间。这些代码是我从 Git 的版本库中找出来的,自己没有再去尝试,如有问题欢迎留言。请谅解。目前我已经对代码进行了多处优化,优化内容将在下一篇讲解,并会附上源码。

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