本文以Spring Web的后台开发讲解。
上一篇讲解了如何使用 jvisualvm
监控Java程序。虽然已经挺强大了,但是在实际的应用中依然不满足我们的需求。现在,我们想要监控应用程序中所有 Controller
提供的接口的访问数量,频次,响应时长。 Service
层方法的执行次数,执行时长,频次等等。以便之后对系统的性能优化做准备。这个时候 jvisualvm
已经不能满足我们的需求了。
这是我对于监控Java程序中的方案:
Controller
层和 Service
层的方法。那么可以通过Spring中的切面完成我们的需求。 我调查的方案和分析基本这样,其他人如果有更好的方案可以提出一起探讨。
下面是讲解+部分代码,本次讲解还有后篇。在第二篇文后,
关于Metrics的使用方法,已经有很多文章介绍了,我在这里推荐我认为还不错的给大家,然后我再介绍的使用方法.
其他的文章我就不多分享了,感觉大同小异。没什么太大差别。
要使用Metric,那么首先需要 MetricRegistry
。我们需要提供Http的报表,所以我们需要将 MetricsServlet
注册到Spring中,以便可以通过Http接口监控系统。下面代码我们将Http接口定义为: /monitor/metrics
。
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.servlets.MetricsServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MonitorConfig { @Bean public MetricRegistry metricRegistry() { return new MetricRegistry(); } @Bean public ServletRegistrationBean servletRegistrationBean(MetricRegistry metricRegistry) { return new ServletRegistrationBean(new MetricsServlet(metricRegistry), "/monitor/metrics"); } }
另外,为了方便调试,我希望支持终端报表的方式,并且要可以配置打开和关闭,于是我使用另外一个配置类:
import com.codahale.metrics.ConsoleReporter; import com.codahale.metrics.MetricRegistry; import lombok.extern.java.Log; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import java.util.concurrent.TimeUnit; @Configuration @Log @ConditionalOnProperty(prefix = "monitor.report", name = "console", havingValue = "true") @Import(MonitorConfig.class) public class MonitorReportConfig { @Bean public ConsoleReporter consoleReporter(MetricRegistry metrics) { ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics) .convertRatesTo(TimeUnit.SECONDS) .convertDurationsTo(TimeUnit.MILLISECONDS) .build(); reporter.start(10, TimeUnit.SECONDS); return reporter; } }
这样可以在工程中的 application.properties
文件中,通过下面配置开启终端报表,而且自动开启,10秒钟一次报表:
monitor.report.console = true
Metrics中可以统计的信息很多,其中Timer已经满足了我们需要的信息。
我为什么要先为监控的方法准备Timer,而不是在方法执行的时候再创建呢?原因有两点。
我们使用 MethodMonitorCenter
类来收集我们想要监控的方法。通过实现 ApplicationContextAware
接口,在Spring容器装载完毕之后,会回掉 setApplicationContext
方法,我们通过 getBeansWithAnnotation
方法找到包含指定注解的类。然后对其进行过滤,并获取我们想要监控的方法。在最后我们通过 metricRegistry.timer(method.toString());
方法为我们的关心的方法准备一个timer。
@Component @Getter @Log public class MethodMonitorCenter implements ApplicationContextAware { public static final String PACKAGE_NAME = "com.sinafenqi"; // 这里换成自己的包名 @Autowired private MetricRegistry metricRegistry; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Map<String, Object> monitorBeans = new HashMap<>(); monitorBeans.putAll(applicationContext.getBeansWithAnnotation(Controller.class)); monitorBeans.putAll(applicationContext.getBeansWithAnnotation(Service.class)); monitorBeans.putAll(applicationContext.getBeansWithAnnotation(RestController.class)); log.info("monitor begin scan methods"); monitorBeans.values().stream() .map(obj -> obj.getClass().getName()) .map(this::trimString) .map(clzName -> { try { return Class.forName(clzName); } catch (Exception e) { return null; } }) .filter(Objects::nonNull) .filter(aClass -> aClass.getName().startsWith(PACKAGE_NAME)) .forEach(this::getClzMethods); } private void getClzMethods(Class<?> clz) { Stream.of(clz.getDeclaredMethods()) .filter(method -> method.getName().indexOf('$') < 0) .forEach(method -> { log.info("add method timer, method name :" + method.toGenericString()); metricRegistry.timer(method.toString()); }); } private String trimString(String clzName) { if (clzName.indexOf('$') < 0) return clzName; return clzName.substring(0, clzName.indexOf('$')); } }
然后我们可以在切面中监控我们关心的方法。这里使用环绕式切面对 RestController
, Controller
,和 Service
三个注解做切面。这样就可以在方法之前和之后加一些监控代码。当进入around函数的时候,我们先去MetricRegistry中查找有没有对应的timer,如果没有说明是我们不关心的方法,那么我么就可以直接执行,如果存在,那么我就对其进行监控。详情可见代码:
@Component @Aspect @Log public class MetricsMonitorAOP { @Autowired private MetricRegistry metricRegistry; @Pointcut("@within(org.springframework.stereotype.Controller)" + "||@within(org.springframework.stereotype.Service)" + "||@within(org.springframework.web.bind.annotation.RestController)") public void monitor() { } @Around("monitor()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { String target = joinPoint.getSignature().toLongString(); Object[] args = joinPoint.getArgs(); if (!metricRegistry.getNames().contains(target)) { return joinPoint.proceed(args); } Timer timer = metricRegistry.timer(target); Timer.Context context = timer.time(); try { return joinPoint.proceed(args); } finally { context.stop(); } } }
之后访问 /monitor/metrics
接口,就可以以Json的数据格式获取监控结果。大家实验的时候记得把 MethodMonitorCenter
类中的 PACKAGE_NAME
常量换成自己的。
现在基本已经实现监控所有Controller,和Service层我们定义的方法了,但是代码依然有很大的优化空间。这些代码是我从Git的版本库中找出来的,自己没有再去尝试,如有问题欢迎留言。请谅解。目前我已经对代码进行了多处优化,优化内容将在下一篇讲解,并会附上源码。
最后欢迎关注我的个人公众号。提问,唠嗑,都可以。