本文以Spring Web的后台开发讲解。html
上一篇讲解了如何使用jvisualvm
监控Java程序。jvisualvm
虽然已经挺强大了,可是在实际的应用中依然不知足咱们的需求。如今,咱们想要监控应用程序中全部Controller
提供的接口的访问数量,频次,响应时长。Service
层方法的执行次数,执行时长,频次等等。以便以后对系统的性能优化作准备。这个时候jvisualvm
已经不能知足咱们的需求了。java
这是我对于方法级监控Java程序的方案:程序员
Controller
层和Service
层的方法。那么能够经过Spring中的切面完成咱们的需求。我调查的方案和分析基本这样,其余人若是有更好的方案能够提出一块儿探讨。web
下面是讲解+部分代码,本次讲解还有优化篇。spring
关于Metrics的使用方法,已经有不少文章介绍了,我在这里推荐我认为还不错的给你们,而后我再介绍的使用方法.性能优化
其余的文章我就很少分享了,感受大同小异。没什么太大差异。bash
要使用Metric,那么首先须要MetricRegistry
。架构
咱们须要提供Http的报表,因此咱们须要将MetricsServlet
注册到Spring中,以即可以经过Http接口获取监控结果。下面代码咱们将监控接口定义为:/monitor/metrics
。app
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");
}
}
复制代码
另外,为了方便调试,我但愿支持终端输出报表的方式。可是要能够配置打开和关闭,因而我使用另一个配置类,ConditionalOnProperty
注解,让配置根据配置属性加载:ide
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的版本库中找出来的,本身没有再去尝试,若有问题欢迎留言。请谅解。目前我已经对代码进行了多处优化,优化内容将在下一篇讲解,并会附上源码。
最后欢迎关注个人我的公众号。提问,唠嗑,均可以。