前提

最近在做创业项目的时候因为有比较多的新需求,需要频繁基于DDL生成Mybatis适合的实体、Mapper接口和映射文件。其中,代码生成器是MyBatis Generator(MBG),用到了Mybatis-Generator-Core相关依赖,这里通过一篇文章详细地分析这个代码生成器的使用方式。本文编写的时候使用的Mybatis-Generator版本为1.4.0,其他版本没有进行过调研。

前提

Hystrix在2018年11月20日之后已经停止维护,最后一个提交记录是:Latest commit 3cb2158 on 20 Nov 2018,最后一个正式版本为1.5.18。鉴于目前所在公司的技术栈是Spring Cloud,熔断和降级组件主要用的还是Hystrix,这里就Hystrix的完整列表做一个分析记录,方便以后可以随时查询。本文主要参考:Hystrix Configuration。其中,命令配置是针对HystrixCommand,主要包括命令执行(execution)配置、命令降级(fallback)配置、熔断器(circuit breaker)配置、度量统计(metrics)配置和请求上下文配置。

通过micrometer实时监控线程池的各项指标

前提

最近的一个项目中涉及到文件上传和下载,使用到JUC的线程池ThreadPoolExecutor,在生产环境中出现了某些时刻线程池满负载运作,由于使用了CallerRunsPolicy拒绝策略,导致满负载情况下,应用接口调用无法响应,处于假死状态。考虑到之前用micrometer + prometheus + grafana搭建过监控体系,于是考虑使用micrometer做一次主动的线程池度量数据采集,最终可以相对实时地展示在grafana的面板中。

实践过程

下面通过真正的实战过程做一个仿真的例子用于复盘。

代码改造

首先我们要整理一下ThreadPoolExecutor中提供的度量数据项和micrometer对应的Tag的映射关系:

  • 线程池名称,Tag:thread.pool.name,这个很重要,用于区分各个线程池的数据,如果使用IOC容器管理,可以使用BeanName代替。
  • int getCorePoolSize():核心线程数,Tag:thread.pool.core.size
  • int getLargestPoolSize():历史峰值线程数,Tag:thread.pool.largest.size
  • int getMaximumPoolSize():最大线程数(线程池线程容量),Tag:thread.pool.max.size
  • int getActiveCount():当前活跃线程数,Tag:thread.pool.active.size
  • int getPoolSize():当前线程池中运行的线程总数(包括核心线程和非核心线程),Tag:thread.pool.thread.count
  • 当前任务队列中积压任务的总数,Tag:thread.pool.queue.size,这个需要动态计算得出。

接着编写具体的代码,实现的功能如下:

  • 1、建立一个ThreadPoolExecutor实例,核心线程和最大线程数为10,任务队列长度为10,拒绝策略为AbortPolicy
  • 2、提供两个方法,分别使用线程池实例模拟短时间耗时的任务和长时间耗时的任务。
  • 3、提供一个方法用于清空线程池实例中的任务队列。
  • 4、提供一个单线程的调度线程池用于定时收集ThreadPoolExecutor实例中上面列出的度量项,保存到micrometer内存态的收集器中。

由于这些统计的值都会跟随时间发生波动性变更,可以考虑选用Gauge类型的Meter进行记录。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// ThreadPoolMonitor
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
* @author throwable
* @version v1.0
* @description
* @since 2019/4/7 21:02
*/
@Service
public class ThreadPoolMonitor implements InitializingBean {

private static final String EXECUTOR_NAME = "ThreadPoolMonitorSample";
private static final Iterable<Tag> TAG = Collections.singletonList(Tag.of("thread.pool.name", EXECUTOR_NAME));
private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();

private final ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10), new ThreadFactory() {

private final AtomicInteger counter = new AtomicInteger();

@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("thread-pool-" + counter.getAndIncrement());
return thread;
}
}, new ThreadPoolExecutor.AbortPolicy());


private Runnable monitor = () -> {
//这里需要捕获异常,尽管实际上不会产生异常,但是必须预防异常导致调度线程池线程失效的问题
try {
Metrics.gauge("thread.pool.core.size", TAG, executor, ThreadPoolExecutor::getCorePoolSize);
Metrics.gauge("thread.pool.largest.size", TAG, executor, ThreadPoolExecutor::getLargestPoolSize);
Metrics.gauge("thread.pool.max.size", TAG, executor, ThreadPoolExecutor::getMaximumPoolSize);
Metrics.gauge("thread.pool.active.size", TAG, executor, ThreadPoolExecutor::getActiveCount);
Metrics.gauge("thread.pool.thread.count", TAG, executor, ThreadPoolExecutor::getPoolSize);
// 注意如果阻塞队列使用无界队列这里不能直接取size
Metrics.gauge("thread.pool.queue.size", TAG, executor, e -> e.getQueue().size());
} catch (Exception e) {
//ignore
}
};

@Override
public void afterPropertiesSet() throws Exception {
// 每5秒执行一次
scheduledExecutor.scheduleWithFixedDelay(monitor, 0, 5, TimeUnit.SECONDS);
}

public void shortTimeWork() {
executor.execute(() -> {
try {
// 5秒
Thread.sleep(5000);
} catch (InterruptedException e) {
//ignore
}
});
}

public void longTimeWork() {
executor.execute(() -> {
try {
// 500秒
Thread.sleep(5000 * 100);
} catch (InterruptedException e) {
//ignore
}
});
}

public void clearTaskQueue() {
executor.getQueue().clear();
}
}

//ThreadPoolMonitorController
import club.throwable.smp.service.ThreadPoolMonitor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author throwable
* @version v1.0
* @description
* @since 2019/4/7 21:20
*/
@RequiredArgsConstructor
@RestController
public class ThreadPoolMonitorController {

private final ThreadPoolMonitor threadPoolMonitor;

@GetMapping(value = "/shortTimeWork")
public ResponseEntity<String> shortTimeWork() {
threadPoolMonitor.shortTimeWork();
return ResponseEntity.ok("success");
}

@GetMapping(value = "/longTimeWork")
public ResponseEntity<String> longTimeWork() {
threadPoolMonitor.longTimeWork();
return ResponseEntity.ok("success");
}

@GetMapping(value = "/clearTaskQueue")
public ResponseEntity<String> clearTaskQueue() {
threadPoolMonitor.clearTaskQueue();
return ResponseEntity.ok("success");
}
}

配置如下:

1
2
3
4
5
6
7
8
9
10
server:
port: 9091
management:
server:
port: 9091
endpoints:
web:
exposure:
include: '*'
base-path: /management

prometheus的调度Job也可以适当调高频率,这里默认是15秒拉取一次/prometheus端点,也就是会每次提交3个收集周期的数据。项目启动之后,可以尝试调用/management/prometheus查看端点提交的数据:

j-m-t-p-1.png

因为ThreadPoolMonitorSample是我们自定义命名的Tag,看到相关字样说明数据收集是正常的。如果prometheus的Job没有配置错误,在本地的spring-boot项目起来后,可以查下prometheus的后台:

j-m-t-p-2.png

j-m-t-p-3.png

OK,完美,可以进行下一步。

grafana面板配置

确保JVM应用和prometheus的调度Job是正常的情况下,接下来重要的一步就是配置grafana面板。如果暂时不想认真学习一下prometheus的PSQL的话,可以从prometheus后台的/graph面板直接搜索对应的样本表达式拷贝进去grafana配置中就行,当然最好还是去看下prometheus的文档系统学习一下怎么编写PSQL。

  • 基本配置:

j-m-t-p-4.png

  • 可视化配置,把右边的标签勾选,宽度尽量调大点:

j-m-t-p-5.png

  • 查询配置,这个是最重要的,最终图表就是靠查询配置展示的:

j-m-t-p-6.png

查询配置具体如下:

  • A:thread_pool_active_size,Legend:-线程池活跃线程数。
  • B:thread_pool_largest_size,Legend:-线程池历史峰值线程数。
  • C:thread_pool_max_size,Legend:-线程池容量。
  • D:thread_pool_core_size,Legend:-线程池核心线程数。
  • E:thread_pool_thread_count,Legend:-线程池运行中的线程数。
  • F:thread_pool_queue_size,Legend:-线程池积压任务数。

最终效果

多调用几次例子中提供的几个接口,就能得到一个监控线程池呈现的图表:

j-m-t-p-7.png

小结

针对线程池ThreadPoolExecutor的各项数据进行监控,有利于及时发现使用线程池的接口的异常,如果想要快速恢复,最有效的途径是:清空线程池中任务队列中积压的任务。具体的做法是:可以把ThreadPoolExecutor委托到IOC容器管理,并且把ThreadPoolExecutor任务队列清空的方法暴露成一个REST端点即可。像HTTP客户端的连接池如Apache-Http-Client或者OkHttp等的监控,可以用类似的方式实现,数据收集的时候可能由于加锁等原因会有少量的性能损耗,不过这些都是可以忽略的,如果真的怕有性能影响,可以尝试用反射API直接获取ThreadPoolExecutor实例内部的属性值,这样就可以避免加锁的性能损耗

(本文完 c-2-d 20190414)

zuul源码分析-探究原生zuul的工作原理

前提

最近在项目中使用了SpringCloud,基于Zuul搭建了一个提供加解密、鉴权等功能的网关服务。鉴于之前没怎么使用过Zuul,于是顺便仔细阅读了它的源码。实际上,Zuul原来提供的功能是很单一的:通过一个统一的Servlet入口(ZuulServlet,或者Filter入口,使用ZuulServletFilter)拦截所有的请求,然后通过内建的com.netflix.zuul.IZuulFilter链对请求做拦截和过滤处理。ZuulFilterjavax.servlet.Filter的原理相似,但是它们本质并不相同。javax.servlet.Filter在Web应用中是独立的组件,ZuulFilterZuulServlet处理请求时候调用的,后面会详细分析。

源码环境准备

Zuul的项目地址是https://github.com/Netflix/zuul,它是著名的"开源框架提供商"Netflix的作品,项目的目的是:Zuul是一个网关服务,提供动态路由、监视、弹性、安全性等。在SpringCloud中引入了zuul,配合Netflix的另一个负载均衡框架Ribbon和Netflix的另一个提供服务发现与注册框架Eureka,可以实现服务的动态路由。值得注意的是,zuul在2.x甚至3.x的分支中已经引入了netty,框架的复杂性大大提高。但是当前的SpringCloud体系并没有升级zuul的版本,目前使用的是zuul1.x的最高版本1.3.1。

z-s-c-1

因此我们需要阅读它的源码的时候可以选择这个发布版本。值得注意的是,由于这些版本的发布时间已经比较久,有部分插件或者依赖包可能找不到,笔者在构建zuul1.3.1的源码的时候发现这几个问题:

  • 1、nebula.netflixoss插件的旧版本已经不再支持,所有build.gradle文件中的nebula.netflixoss插件的版本修改为5.2.0。
  • 2、2017年的时候Gradle支持的版本是2.x,笔者这里选择了gradle-2.14,选择高版本的Gradle有可能在构建项目的时候出现jetty插件不支持。
  • 3、Jdk最好使用1.8,Gradle构建文件中的sourceCompatibility、targetCompatibility、languageLevel等配置全改为1.8。

另外,如果使用IDEA进行构建,注意配置项目的Jdk和Java环境,所有配置改为Jdk1.8,Gradle构建成功后如下:

z-s-c-2

zuul-1.3.1中提供了一个Web应用的Sample项目,我们直接运行zuul-simple-webapp的Gradle配置中的Tomcat插件即可启动项目,开始Debug之旅:

z-s-c-3

源码分析

ZuulFilter的加载

从Zuul的源码来看,ZuulFilter的加载模式可能跟我们想象的大有不同,Zuul设计的初衷是ZuulFilter是存放在Groovy文件中,可以实现基于最后修改时间进行热加载。我们先看看Zuul核心类之一com.netflix.zuul.filters.FilterRegistry(Filter的注册中心,实际上是ZuulFilter的全局缓存):

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
public class FilterRegistry {

// 饿汉式单例,确保全局只有一个ZuulFilter的缓存
private static final FilterRegistry INSTANCE = new FilterRegistry();
public static final FilterRegistry instance() {
return INSTANCE;
}

//缓存字符串到ZuulFilter实例的映射关系,如果是从文件加载,字符串key的格式是:文件绝对路径 + 文件名,当然也可以自实现
private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();

private FilterRegistry() {
}

public ZuulFilter remove(String key) {
return this.filters.remove(key);
}

public ZuulFilter get(String key) {
return this.filters.get(key);
}

public void put(String key, ZuulFilter filter) {
this.filters.putIfAbsent(key, filter);
}

public int size() {
return this.filters.size();
}

public Collection<ZuulFilter> getAllFilters() {
return this.filters.values();
}

}

实际上Zuul使用了简单粗暴的方式(直接使用ConcurrentHashMap)缓存了ZuulFilter,这些缓存除非主动调用remove方法,否则不会自动清理。Zuul提供默认的动态代码编译器,接口是DynamicCodeCompiler,目的是把代码编译为Java的类,默认实现是GroovyCompiler,功能就是把Groovy代码编译为Java类。还有一个比较重要的工厂类接口是FilterFactory,它定义了ZuulFilter类生成ZuulFilter实例的逻辑,默认实现是DefaultFilterFactory,实际上就是利用Class#newInstance()反射生成ZuulFilter实例。接着,我们可以进行分析FilterLoader的源码,这个类的作用就是加载文件中的ZuulFilter实例:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public class FilterLoader {
//静态final实例,注意到访问权限是包许可,实际上就是饿汉式单例
final static FilterLoader INSTANCE = new FilterLoader();

private static final Logger LOG = LoggerFactory.getLogger(FilterLoader.class);

//缓存Filter名称(主要是从文件加载,名称为绝对路径 + 文件名的形式)->Filter最后修改时间戳的映射
private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>();
//缓存Filter名字->Filter代码的映射,实际上这个Map只使用到get方法进行存在性判断,一直是一个空的结构
private final ConcurrentHashMap<String, String> filterClassCode = new ConcurrentHashMap<String, String>();
//缓存Filter名字->Filter名字的映射,用于存在性判断
private final ConcurrentHashMap<String, String> filterCheck = new ConcurrentHashMap<String, String>();
//缓存Filter类型名称->List<ZuulFilter>的映射
private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();

//前面提到的ZuulFilter全局缓存的单例
private FilterRegistry filterRegistry = FilterRegistry.instance();
//动态代码编译器实例,Zuul提供的默认实现是GroovyCompiler
static DynamicCodeCompiler COMPILER;
//ZuulFilter的工厂类
static FilterFactory FILTER_FACTORY = new DefaultFilterFactory();
//下面三个方法说明DynamicCodeCompiler、FilterRegistry、FilterFactory可以被覆盖
public void setCompiler(DynamicCodeCompiler compiler) {
COMPILER = compiler;
}

public void setFilterRegistry(FilterRegistry r) {
this.filterRegistry = r;
}

public void setFilterFactory(FilterFactory factory) {
FILTER_FACTORY = factory;
}
//饿汉式单例获取自身实例
public static FilterLoader getInstance() {
return INSTANCE;
}
//返回所有缓存的ZuulFilter实例的总数量
public int filterInstanceMapSize() {
return filterRegistry.size();
}

//通过ZuulFilter的类代码和Filter名称获取ZuulFilter实例
public ZuulFilter getFilter(String sCode, String sName) throws Exception {
//检查filterCheck是否存在相同名字的Filter,如果存在说明已经加载过
if (filterCheck.get(sName) == null) {
//filterCheck中放入Filter名称
filterCheck.putIfAbsent(sName, sName);
//filterClassCode中不存在加载过的Filter名称对应的代码
if (!sCode.equals(filterClassCode.get(sName))) {
LOG.info("reloading code " + sName);
//从全局缓存中移除对应的Filter
filterRegistry.remove(sName);
}
}
ZuulFilter filter = filterRegistry.get(sName);
//如果全局缓存中不存在对应的Filter,就使用DynamicCodeCompiler加载代码,使用FilterFactory实例化ZuulFilter
//注意加载的ZuulFilter类不能是抽象的,必须是继承了ZuulFilter的子类
if (filter == null) {
Class clazz = COMPILER.compile(sCode, sName);
if (!Modifier.isAbstract(clazz.getModifiers())) {
filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
}
}
return filter;
}

//通过文件加加载ZuulFilter
public boolean putFilter(File file) throws Exception {
//Filter名称为文件的绝对路径+文件名(这里其实绝对路径已经包含文件名,这里再加文件名的目的不明确)
String sName = file.getAbsolutePath() + file.getName();
//如果文件被修改过则从全局缓存从移除对应的Filter以便重新加载
if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) {
LOG.debug("reloading filter " + sName);
filterRegistry.remove(sName);
}
//下面的逻辑和上一个方法类似
ZuulFilter filter = filterRegistry.get(sName);
if (filter == null) {
Class clazz = COMPILER.compile(file);
if (!Modifier.isAbstract(clazz.getModifiers())) {
filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
//这里说明了一旦文件有修改,hashFiltersByType中对应的当前文件加载出来的Filter类型的缓存要移除,原因见下一个方法
if (list != null) {
hashFiltersByType.remove(filter.filterType()); //rebuild this list
}
filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
filterClassLastModified.put(sName, file.lastModified());
return true;
}
}
return false;
}
//通过Filter类型获取同类型的所有ZuulFilter
public List<ZuulFilter> getFiltersByType(String filterType) {
List<ZuulFilter> list = hashFiltersByType.get(filterType);
if (list != null) return list;
list = new ArrayList<ZuulFilter>();
//如果hashFiltersByType缓存被移除,这里从全局缓存中加载所有的ZuulFilter,按照指定类型构建一个新的列表
Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
ZuulFilter filter = iterator.next();
if (filter.filterType().equals(filterType)) {
list.add(filter);
}
}
//注意这里会进行排序,是基于filterOrder
Collections.sort(list); // sort by priority
//这里总是putIfAbsent,这就是为什么上个方法可以放心地在修改的情况下移除指定Filter类型中的全部缓存实例的原因
hashFiltersByType.putIfAbsent(filterType, list);
return list;
}
}

上面的几个方法和缓存容器都比较简单,这里实际上有加载和存放动作的方法只有putFilter,这个方法正是Filter文件管理器FilterFileManager依赖的,接着看FilterFileManager的源码:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public class FilterFileManager {

private static final Logger LOG = LoggerFactory.getLogger(FilterFileManager.class);

String[] aDirectories;
int pollingIntervalSeconds;
Thread poller;
boolean bRunning = true;
//文件名过滤器,Zuul中的默认实现是GroovyFileFilter,只接受.groovy后缀的文件
static FilenameFilter FILENAME_FILTER;

static FilterFileManager INSTANCE;

private FilterFileManager() {
}

public static void setFilenameFilter(FilenameFilter filter) {
FILENAME_FILTER = filter;
}
//init方法是核心静态方法,它具备了配置,预处理和激活后台轮询线程的功能
public static void init(int pollingIntervalSeconds, String... directories) throws Exception, IllegalAccessException, InstantiationException{
if (INSTANCE == null) INSTANCE = new FilterFileManager();
INSTANCE.aDirectories = directories;
INSTANCE.pollingIntervalSeconds = pollingIntervalSeconds;
INSTANCE.manageFiles();
INSTANCE.startPoller();
}

public static FilterFileManager getInstance() {
return INSTANCE;
}

public static void shutdown() {
INSTANCE.stopPoller();
}

void stopPoller() {
bRunning = false;
}
//启动后台轮询守护线程,每休眠pollingIntervalSeconds秒则进行一次文件扫描尝试更新Filter
void startPoller() {
poller = new Thread("GroovyFilterFileManagerPoller") {
public void run() {
while (bRunning) {
try {
sleep(pollingIntervalSeconds * 1000);
//预处理文件,实际上是ZuulFilter的预加载
manageFiles();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
//设置为守护线程
poller.setDaemon(true);
poller.start();
}
//根据指定目录路径获取目录,主要需要转换为ClassPath
public File getDirectory(String sPath) {
File directory = new File(sPath);
if (!directory.isDirectory()) {
URL resource = FilterFileManager.class.getClassLoader().getResource(sPath);
try {
directory = new File(resource.toURI());
} catch (Exception e) {
LOG.error("Error accessing directory in classloader. path=" + sPath, e);
}
if (!directory.isDirectory()) {
throw new RuntimeException(directory.getAbsolutePath() + " is not a valid directory");
}
}
return directory;
}

//遍历配置目录,获取所有配置目录下的所有满足FilenameFilter过滤条件的文件
List<File> getFiles() {
List<File> list = new ArrayList<File>();
for (String sDirectory : aDirectories) {
if (sDirectory != null) {
File directory = getDirectory(sDirectory);
File[] aFiles = directory.listFiles(FILENAME_FILTER);
if (aFiles != null) {
list.addAll(Arrays.asList(aFiles));
}
}
}
return list;
}
//遍历指定文件列表,调用FilterLoader单例中的putFilter
void processGroovyFiles(List<File> aFiles) throws Exception, InstantiationException, IllegalAccessException {
for (File file : aFiles) {
FilterLoader.getInstance().putFilter(file);
}
}
//获取指定目录下的所有文件,调用processGroovyFiles,个人认为这两个方法没必要做单独封装
void manageFiles() throws Exception, IllegalAccessException, InstantiationException {
List<File> aFiles = getFiles();
processGroovyFiles(aFiles);
}

分析完FilterFileManager源码之后,Zuul中基于文件加载ZuulFilter的逻辑已经十分清晰:后台启动一个守护线程,定时轮询指定文件夹里面的文件,如果文件存在变更,则尝试更新指定的ZuulFilter缓存,FilterFileManager的init方法调用的时候在启动后台线程之前会进行一次预加载。

RequestContext

在分析ZuulFilter的使用之前,有必要先了解Zuul中的请求上下文对象RequestContext。首先要有一个共识:每一个新的请求都是由一个独立的线程处理(这个线程是Tomcat里面起的线程),换言之,请求的所有参数(Http报文信息解析出来的内容,如请求头、请求体等等)总是绑定在处理请求的线程中。RequestContext的设计就是简单直接有效,它继承于ConcurrentHashMap<String, Object>,所以参数可以直接设置在RequestContext中,Zuul没有设计一个类似于枚举的类控制RequestContext的可选参数,因此里面的设置值和提取值的方法都是硬编码的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public HttpServletRequest getRequest() {
return (HttpServletRequest) get("request");
}

public void setRequest(HttpServletRequest request) {
put("request", request);
}

public HttpServletResponse getResponse() {
return (HttpServletResponse) get("response");
}

public void setResponse(HttpServletResponse response) {
set("response", response);
}
...

看起来很暴力并且不怎么优雅,但是实际上是高效的。RequestContext一般使用静态方法RequestContext#getCurrentContext()进行初始化,我们分析一下它的初始化流程:

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
//保存RequestContext自身类型
protected static Class<? extends RequestContext> contextClass = RequestContext.class;
//静态对象
private static RequestContext testContext = null;
//静态final修饰的ThreadLocal实例,用于存放所有的RequestContext,每个RequestContext都会绑定在自身请求的处理线程中
//注意这里的ThreadLocal实例的initialValue()方法,当ThreadLocal的get()方法返回null的时候总是会调用initialValue()方法
protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
@Override
protected RequestContext initialValue() {
try {
return contextClass.newInstance();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
};


public RequestContext() {
super();
}

public static RequestContext getCurrentContext() {
//这里混杂了测试的代码,暂时忽略
if (testContext != null) return testContext;
//当ThreadLocal的get()方法返回null的时候总是会调用initialValue()方法,所以这里是"无则新建RequestContext"的逻辑
RequestContext context = threadLocal.get();
return context;
}

注意上面的ThreadLocal覆盖了初始化方法initialValue()ThreadLocal的初始化方法总是在ThreadLocal#get()方法返回null的时候调用,实际上静态方法RequestContext#getCurrentContext()的作用就是:如果ThreadLocal中已经绑定了RequestContext静态实例就直接获取绑定在线程中的RequestContext实例,否则新建一个RequestContext实例存放在ThreadLocal(绑定到当前的请求线程中)。了解这一点后面分析ZuulServletFilter和ZuulServlet的时候就很简单了。

ZuulFilter

抽象类com.netflix.zuul.ZuulFilter是Zuul里面的核心组件,它是用户扩展Zuul行为的组件,用户可以实现不同类型的ZuulFilter、定义它们的执行顺序、实现它们的执行方法达到定制化的目的,SpringCloud的netflix-zuul就是一个很好的实现包。ZuulFilter实现了IZuulFilter接口,我们先看这个接口的定义:

1
2
3
4
5
6
public interface IZuulFilter {

boolean shouldFilter();

Object run() throws ZuulException;
}

很简单,shouldFilter()方法决定是否需要执行(也就是执行时机由使用者扩展,甚至可以禁用),而run()方法决定执行的逻辑。接着看ZuulFilter的源码:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {
//netflix的配置组件,实际上就是基于配置文件提取的指定key的值
private final AtomicReference<DynamicBooleanProperty> filterDisabledRef = new AtomicReference<>();

//定义Filter的类型
abstract public String filterType();

//定义当前Filter实例执行的顺序
abstract public int filterOrder();

//是否静态的Filter,静态的Filter是无状态的
public boolean isStaticFilter() {
return true;
}

//禁用当前Filter的配置属性的Key名称
//Key=zuul.${全类名}.${filterType}.disable
public String disablePropertyName() {
return "zuul." + this.getClass().getSimpleName() + "." + filterType() + ".disable";
}

//判断当前的Filter是否禁用,通过disablePropertyName方法从配置中读取,默认是不禁用,也就是启用
public boolean isFilterDisabled() {
filterDisabledRef.compareAndSet(null, DynamicPropertyFactory.getInstance().getBooleanProperty(disablePropertyName(), false));
return filterDisabledRef.get().get();
}

//这个是核心方法,执行Filter,如果Filter不是禁用、并且满足执行时机则调用run方法,返回执行结果,记录执行轨迹
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
if (!isFilterDisabled()) {
if (shouldFilter()) {
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
Object res = run();
zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
} catch (Throwable e) {
t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
zr = new ZuulFilterResult(ExecutionStatus.FAILED);
//注意这里只保存异常的实例,即使执行抛出异常
zr.setException(e);
} finally {
t.stopAndLog();
}
} else {
zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
}
}
return zr;
}

//实现Comparable,基于filterOrder升序排序,也就是filterOrder越大,执行优先度越低
public int compareTo(ZuulFilter filter) {
return Integer.compare(this.filterOrder(), filter.filterOrder());
}
}

这里注意几个地方,第一个是filterOrder()方法和compareTo(ZuulFilter filter)方法,子类实现ZuulFilter时候,filterOrder()方法返回值越大,或者说Filter的顺序系数越大,ZuulFilter执行的优先度越低。第二个地方是可以通过zuul.${全类名}.${filterType}.disable=false通过类名和Filter类型禁用对应的Filter。第三个值得注意的地方是Zuul中定义了四种类型的ZuulFilter,后面分析ZuulRunner的时候再详细展开。ZuulFilter实际上就是使用者扩展的核心组件,通过实现ZuulFilter的方法可以在一个请求处理链中的特定位置执行特定的定制化逻辑。第四个值得注意的地方是runFilter()方法执行不会抛出异常,如果出现异常,Throwable实例会保存在ZuulFilterResult对象中返回到外层方法,如果正常执行,则直接返回runFilter()方法的结果。

FilterProcessor

前面花大量功夫分析完ZuulFilter基于Groovy文件的加载机制(在SpringCloud体系中并没有使用此策略,因此,我们持了解的态度即可)以及RequestContext的设计,接着我们分析FilterProcessor去了解如何使用加载好的缓存中的ZuulFilter。我们先看FilterProcessor的基本属性:

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
public class FilterProcessor {

static FilterProcessor INSTANCE = new FilterProcessor();
protected static final Logger logger = LoggerFactory.getLogger(FilterProcessor.class);

private FilterUsageNotifier usageNotifier;


public FilterProcessor() {
usageNotifier = new BasicFilterUsageNotifier();
}

public static FilterProcessor getInstance() {
return INSTANCE;
}

public static void setProcessor(FilterProcessor processor) {
INSTANCE = processor;
}

public void setFilterUsageNotifier(FilterUsageNotifier notifier) {
this.usageNotifier = notifier;
}
...
}

像之前分析的几个类一样,FilterProcessor设计为单例,提供可以覆盖单例实例的方法。需要注意的一点是属性usageNotifier是FilterUsageNotifier类型,FilterUsageNotifier接口的默认实现是BasicFilterUsageNotifier(FilterProcessor的一个静态内部类),BasicFilterUsageNotifier依赖于Netflix的一个工具包servo-core,提供基于内存态的计数器统计每种ZuulFilter的每一次调用的状态ExecutionStatus。枚举ExecutionStatus的可选值如下:

  • 1、SUCCESS,代表该Filter处理成功,值为1。
  • 2、SKIPPED,代表该Filter跳过处理,值为-1。
  • 3、DISABLED,代表该Filter禁用,值为-2。
  • 4、SUCCESS,代表该FAILED处理出现异常,值为-3。

当然,使用者也可以覆盖usageNotifier属性。接着我们看FilterProcessor中真正调用ZuulFilter实例的核心方法:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//指定Filter类型执行该类型下的所有ZuulFilter
public Object runFilters(String sType) throws Throwable {
//尝试打印Debug日志
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
//获取所有指定类型的ZuulFilter
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ZuulFilter zuulFilter = list.get(i);
Object result = processZuulFilter(zuulFilter);
//如果处理结果是Boolean类型尝试做或操作,其他类型结果忽略
if (result != null && result instanceof Boolean) {
bResult |= ((Boolean) result);
}
}
}
return bResult;
}
//执行ZuulFilter,这个就是ZuulFilter执行逻辑
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
boolean bDebug = ctx.debugRouting();
final String metricPrefix = "zuul.filter-";
long execTime = 0;
String filterName = "";
try {
long ltime = System.currentTimeMillis();
filterName = filter.getClass().getSimpleName();
RequestContext copy = null;
Object o = null;
Throwable t = null;
if (bDebug) {
Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
copy = ctx.copy();
}
//简单调用ZuulFilter的runFilter方法
ZuulFilterResult result = filter.runFilter();
ExecutionStatus s = result.getStatus();
execTime = System.currentTimeMillis() - ltime;
switch (s) {
case FAILED:
t = result.getException();
//记录调用链中当前Filter的名称,执行结果状态和执行时间
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
break;
case SUCCESS:
o = result.getResult();
//记录调用链中当前Filter的名称,执行结果状态和执行时间
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
if (bDebug) {
Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
Debug.compareContextState(filterName, copy);
}
break;
default:
break;
}

if (t != null) throw t;
//这里做计数器的统计
usageNotifier.notify(filter, s);
return o;

} catch (Throwable e) {
if (bDebug) {
Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
}
//这里做计数器的统计
usageNotifier.notify(filter, ExecutionStatus.FAILED);
if (e instanceof ZuulException) {
throw (ZuulException) e;
} else {
ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
//记录调用链中当前Filter的名称,执行结果状态和执行时间
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
throw ex;
}
}
}

上面介绍了FilterProcessor中的processZuulFilter(ZuulFilter filter)方法主要提供ZuulFilter执行的一些度量相关记录(例如Filter执行耗时摘要,会形成一个链,记录在一个字符串中)和ZuulFilter的执行方法,ZuulFilter执行结果可能是成功或者异常,前面提到过,如果抛出异常Throwable实例会保存在ZuulFilterResult中,在processZuulFilter(ZuulFilter filter)发现ZuulFilterResult中的Throwable实例不为null则直接抛出,否则返回ZuulFilter正常执行的结果。另外,FilterProcessor中通过指定Filter类型执行所有对应类型的ZuulFilterrunFilters(String sType)方法,我们知道了runFilters(String sType)方法如果处理结果是Boolean类型尝试做或操作,其他类型结果忽略,可以理解为此方法的返回值是没有很大意义的。参考SpringCloud里面对ZuulFilter的返回值处理一般是直接塞进去当前线程绑定的RequestContext中,选择特定的ZuulFilter子类对前面的ZuulFilter产生的结果进行处理。FilterProcessor基于runFilters(String sType)方法提供了其他指定filterType的方法:

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
36
37
public void postRoute() throws ZuulException {
try {
runFilters("post");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName());
}
}

public void preRoute() throws ZuulException {
try {
runFilters("pre");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
}
}

public void error() {
try {
runFilters("error");
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
}

public void route() throws ZuulException {
try {
runFilters("route");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
}
}

上面提供的方法很简单,无法是指定参数为post、pre、error、routerunFilters(String sType)方法进行调用,至于这些FilterType的执行位置见下一个小节的分析。

ZuulServletFilter和ZuulServlet

Zuul本来就是设计为Servlet规范组件的一个类库,ZuulServlet就是javax.servlet.http.HttpServlet的实现类,而ZuulServletFilterjavax.servlet.Filter的实现类。这两个类都依赖到ZuulRunner完成ZuulFilter的调用,它们的实现逻辑是完全一致的,我们只需要看其中一个类的实现,这里挑选ZuulServlet

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class ZuulServlet extends HttpServlet {

private static final long serialVersionUID = -3374242278843351500L;
private ZuulRunner zuulRunner;

@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
String bufferReqsStr = config.getInitParameter("buffer-requests");
boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;
zuulRunner = new ZuulRunner(bufferReqs);
}

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
//实际上委托到ZuulRunner的init方法
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
//初始化RequestContext实例
RequestContext context = RequestContext.getCurrentContext();
//设置RequestContext中zuulEngineRan=true
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}

} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}

void postRoute() throws ZuulException {
zuulRunner.postRoute();
}

void route() throws ZuulException {
zuulRunner.route();
}

void preRoute() throws ZuulException {
zuulRunner.preRoute();
}

void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
zuulRunner.init(servletRequest, servletResponse);
}
//这里会先设置RequestContext实例中的throwable属性为执行抛出的Throwable实例
void error(ZuulException e) {
RequestContext.getCurrentContext().setThrowable(e);
zuulRunner.error();
}
}

ZuulServletFilterZuulServlet不相同的地方仅仅是初始化和处理方法的方法签名(参数列表和方法名),其他逻辑甚至是代码是一模一样,使用过程中我们需要了解javax.servlet.http.HttpServletjavax.servlet.Filter的作用去选择到底使用ZuulServletFilter还是ZuulServlet。上面的代码可以看到,ZuulServlet初始化的时候可以配置初始化布尔值参数buffer-requests,这个参数默认为false,它是ZuulRunner实例化的必须参数。ZuulServlet中的调用ZuulFilter的方法都委托到ZuulRunner实例去完成,但是我们可以从service(servletRequest, servletResponse)方法看出四种FilterType(pre、route、post、error)的ZuulFilter的执行顺序,总结如下:

  • 1、pre、route、post都不抛出异常,顺序是:pre->route->post,error不执行。
  • 2、pre抛出异常,顺序是:pre->error->post。
  • 3、route抛出异常,顺序是:pre->route->error->post。
  • 4、post抛出异常,顺序是:pre->route->post->error。

注意,一旦出现了异常,会把抛出的Throwable实例设置到绑定到当前请求线程的RequestContext实例中的throwable属性。还需要注意在service(servletRequest, servletResponse)的finally块中调用了RequestContext.getCurrentContext().unset();,实际上是从RequestContextThreadLocal实例中移除当前的RequestContext实例,这样做可以避免ThreadLocal使用不当导致内存泄漏。

接着看ZuulRunner的源码:

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
36
37
38
public class ZuulRunner {

private boolean bufferRequests;

public ZuulRunner() {
this.bufferRequests = true;
}

public ZuulRunner(boolean bufferRequests) {
this.bufferRequests = bufferRequests;
}

public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
RequestContext ctx = RequestContext.getCurrentContext();
if (bufferRequests) {
ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
} else {
ctx.setRequest(servletRequest);
}
ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}

public void postRoute() throws ZuulException {
FilterProcessor.getInstance().postRoute();
}

public void route() throws ZuulException {
FilterProcessor.getInstance().route();
}

public void preRoute() throws ZuulException {
FilterProcessor.getInstance().preRoute();
}

public void error() {
FilterProcessor.getInstance().error();
}
}

postRoute()route()preRoute()error()都是直接委托到FilterProcessor中完成的,实际上就是执行对应类型的所有ZuulFilter实例。这里需要注意的是,初始化ZuulRunner时候,HttpServletResponse会被包装为com.netflix.zuul.http.HttpServletResponseWrapper实例,它是Zuul实现的javax.servlet.http.HttpServletResponseWrapper的子类,主要是添加了一个属性status用来记录Http状态码。如果初始化参数bufferRequests为true,HttpServletRequest会被包装为com.netflix.zuul.http.HttpServletRequestWrapper,它是Zuul实现的javax.servlet.http.HttpServletRequestWrapper的子类,这个包装类主要是把请求的表单参数和请求体都缓存在实例属性中,这样在一些特定场景中可以提高性能。如果没有特殊需要,这个参数bufferRequests一般设置为false。

Zuul简单的使用例子

我们做一个很简单的例子,场景是:对于每个POST请求,使用pre类型的ZuulFilter打印它的请求体,然后使用post类型的ZuulFilter,响应结果硬编码为字符串"Hello World!"。我们先为CounterFactory、`TracerFactory添加两个空的子类,因为Zuul处理逻辑中依赖到这两个组件实现数据度量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DefaultTracerFactory extends TracerFactory {

@Override
public Tracer startMicroTracer(String name) {
return null;
}
}

public class DefaultCounterFactory extends CounterFactory {

@Override
public void increment(String name) {

}
}

接着我们分别继承ZuulFilter,实现一个pre类型的用于打印请求参数的Filter,命名为PrintParameterZuulFilter,实现一个post类型的用于返回字符串"Hello World!"的Filter,命名为SendResponseZuulFilter

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class PrintParameterZuulFilter extends ZuulFilter {

@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
return "POST".equalsIgnoreCase(request.getMethod());
}

@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
if (null != request.getContentType()) {
if (request.getContentType().contains("application/json")) {
try {
ServletInputStream inputStream = request.getInputStream();
String result = StreamUtils.copyToString(inputStream, Charset.forName("UTF-8"));
System.out.println(String.format("请求URI为:%s,请求参数为:%s", request.getRequestURI(), result));
} catch (IOException e) {
throw new ZuulException(e, 500, "从输入流中读取请求参数异常");
}
} else if (request.getContentType().contains("application/x-www-form-urlencoded")) {
StringBuilder params = new StringBuilder();
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String name = parameterNames.nextElement();
params.append(name).append("=").append(request.getParameter(name)).append("&");
}
String result = params.toString();
System.out.println(String.format("请求URI为:%s,请求参数为:%s", request.getRequestURI(),
result.substring(0, result.lastIndexOf("&"))));
}
}
return null;
}
}

public class SendResponseZuulFilter extends ZuulFilter {

@Override
public String filterType() {
return "post";
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
return "POST".equalsIgnoreCase(request.getMethod());
}

@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
String output = "Hello World!";
try {
context.getResponse().getWriter().write(output);
} catch (IOException e) {
throw new ZuulException(e, 500, e.getMessage());
}
return true;
}
}

接着,我们引入嵌入式Tomcat,简单地创建一个Servlet容器,Maven依赖为:

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
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.34</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>8.5.34</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.34</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.34</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jsp-api</artifactId>
<version>8.5.34</version>
</dependency>

添加带main方法的类把上面的组件和Tomcat的组件组装起来:

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
36
37
38
39
40
41
42
public class ZuulMain {

private static final String WEBAPP_DIRECTORY = "src/main/webapp/";
private static final String ROOT_CONTEXT = "";

public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
File tempDir = File.createTempFile("tomcat" + ".", ".8080");
tempDir.delete();
tempDir.mkdir();
tempDir.deleteOnExit();
//创建临时目录,这一步必须先设置,如果不设置默认在当前的路径创建一个'tomcat.8080文件夹'
tomcat.setBaseDir(tempDir.getAbsolutePath());
tomcat.setPort(8080);
StandardContext ctx = (StandardContext) tomcat.addWebapp(ROOT_CONTEXT,
new File(WEBAPP_DIRECTORY).getAbsolutePath());
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes",
new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);
ctx.setDefaultWebXml(new File("src/main/webapp/WEB-INF/web.xml").getAbsolutePath());
// FixBug: no global web.xml found
for (LifecycleListener ll : ctx.findLifecycleListeners()) {
if (ll instanceof ContextConfig) {
((ContextConfig) ll).setDefaultWebXml(ctx.getDefaultWebXml());
}
}
//这里添加两个度量父类的空实现
CounterFactory.initialize(new DefaultCounterFactory());
TracerFactory.initialize(new DefaultTracerFactory());
//这里添加自实现的ZuulFilter
FilterRegistry.instance().put("printParameterZuulFilter", new PrintParameterZuulFilter());
FilterRegistry.instance().put("sendResponseZuulFilter", new SendResponseZuulFilter());
//这里添加ZuulServlet
Context context = tomcat.addContext("/zuul", null);
Tomcat.addServlet(context, "zuul", new ZuulServlet());
//设置Servlet的路径
context.addServletMappingDecoded("/*", "zuul");
tomcat.start();
tomcat.getServer().await();
}
}

执行main方法,Tomcat正常启动后打印出熟悉的日志如下:

z-s-c-4

接下来,用POSTMAN请求模拟一下请求:

z-s-c-5

小结

Zuul虽然在它的Github仓库中的简介中说它是一个提供动态路由、监视、弹性、安全性等的网关框架,但是实际上它原生并没有提供这些功能,这些功能是需要使用者扩展ZuulFilter实现的,例如基于负载均衡的动态路由需要配置Netflix自己家的Ribbon实现。Zuul在设计上的扩展性什么良好,ZuulFilter就像插件一个可以通过类型、排序系数构建一个调用链,通过Filter或者Servlet做入口,嵌入到Servlet(Web)应用中。不过,在Zuul后续的版本如2.x和3.x中,引入了Netty,基于TCP做底层的扩展,但是编码和使用的复杂度大大提高。也许这就是SpringCloud在netflix-zuul组件中选用了zuul1.x的最后一个发布版本1.3.1的原因吧。springcloud-netflix中使用到Netflix的zuul(动态路由)、robbin(负载均衡)、eureka(服务注册与发现)、hystrix(熔断)等核心组件,这里立个flag先逐个组件分析其源码,逐个击破后再对springcloud-netflix做一次完整的源码分析。

(本文完 c-5-d r-a-20190310 最近996,不能经常更新,顺便祝自己生日快乐…)

项目架构级别规约框架Archunit调研

背景

最近在做一个新项目的时候引入了一个架构方面的需求,就是需要检查项目的编码规范、模块分类规范、类依赖规范等,刚好接触到,正好做个调研。

很多时候,我们会制定项目的规范,例如:

  • 硬性规定项目包结构中service层不能引用controller层的类(这个例子有点极端)。
  • 硬性规定定义在controller包下的Controller类的类名称以"Controller"结尾,方法的入参类型命名以"Request"结尾,返回参数命名以"Response"结尾。
  • 枚举类型必须放在common.constant包下,以类名称Enum结尾。

还有很多其他可能需要定制的规范,最终可能会输出一个文档。但是,谁能保证所有参数开发的人员都会按照文档的规范进行开发?为了保证规范的实行,Archunit以单元测试的形式通过扫描类路径(甚至Jar)包下的所有类,通过单元测试的形式对各个规范进行代码编写,如果项目代码中有违背对应的单测规范,那么单元测试将会不通过,这样就可以从CI/CD层面彻底把控项项目架构和编码规范。

简介

Archunit是一个免费、简单、可扩展的类库,用于检查Java代码的体系结构。提供检查包和类的依赖关系、调用层次和切面的依赖关系、循环依赖检查等其他功能。它通过导入所有类的代码结构,基于Java字节码分析实现这一点。的主要关注点是使用任何普通的Java单元测试框架自动测试代码体系结构和编码规则

引入依赖

一般来说,目前常用的测试框架是Junit4,需要引入Junit4和archunit:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

由于junit4中依赖到slf4j,因此最好在测试依赖中引入一个slf4j的实现,例如logback:

1
2
3
4
5
6
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>

如何使用

主要从下面的两个方面介绍一下的使用:

  • 指定参数进行类扫描。
  • 内建规则定义。

指定参数进行类扫描

需要对代码或者依赖规则进行判断前提是要导入所有需要分析的类,类扫描导入依赖于ClassFileImporter,底层依赖于ASM字节码框架针对类文件的字节码进行解析,性能会比基于反射的类扫描框架高很多。ClassFileImporter的构造可选参数为ImportOption(s),扫描规则可以通过ImportOption接口实现,默认提供可选的规则有:

1
2
3
4
5
6
7
8
// 不包含测试类
ImportOption.Predefined.DONT_INCLUDE_TESTS

// 不包含Jar包里面的类
ImportOption.Predefined.DONT_INCLUDE_JARS

// 不包含Jar和Jrt包里面的类,JDK9的特性
ImportOption.Predefined.DONT_INCLUDE_ARCHIVES

举个例子,我们实现一个自定义的ImportOption实现,用于指定需要排除扫描的包路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DontIncludePackagesImportOption implements ImportOption {

private final Set<Pattern> EXCLUDED_PATTERN;

public DontIncludePackagesImportOption(String... packages) {
EXCLUDED_PATTERN = new HashSet<>(8);
for (String eachPackage : packages) {
EXCLUDED_PATTERN.add(Pattern.compile(String.format(".*/%s/.*", eachPackage.replace("/", "."))));
}
}

@Override
public boolean includes(Location location) {
for (Pattern pattern : EXCLUDED_PATTERN) {
if (location.matches(pattern)) {
return false;
}
}
return true;
}
}

ImportOption接口只有一个方法:

1
boolean includes(Location location)

其中,Location包含了路径信息、是否Jar文件等判断属性的元数据,方便使用正则表达式或者直接的逻辑判断。

接着我们可以通过上面实现的DontIncludePackagesImportOption去构造ClassFileImporter实例:

1
2
3
4
5
6
ImportOptions importOptions = new ImportOptions()
// 不扫描jar包
.with(ImportOption.Predefined.DONT_INCLUDE_JARS)
// 排除不扫描的包
.with(new DontIncludePackagesImportOption("com.sample..support"));
ClassFileImporter classFileImporter = new ClassFileImporter(importOptions);

得到ClassFileImporter实例后我们可以通过对应的方法导入项目中的类:

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
// 指定类型导入单个类
public JavaClass importClass(Class<?> clazz)

// 指定类型导入多个类
public JavaClasses importClasses(Class<?>... classes)
public JavaClasses importClasses(Collection<Class<?>> classes)

// 通过指定路径导入类
public JavaClasses importUrl(URL url)
public JavaClasses importUrls(Collection<URL> urls)
public JavaClasses importLocations(Collection<Location> locations)

// 通过类路径导入类
public JavaClasses importClasspath()
public JavaClasses importClasspath(ImportOptions options)

// 通过文件路径导入类
public JavaClasses importPath(String path)
public JavaClasses importPath(Path path)
public JavaClasses importPaths(String... paths)
public JavaClasses importPaths(Path... paths)
public JavaClasses importPaths(Collection<Path> paths)

// 通过Jar文件对象导入类
public JavaClasses importJar(JarFile jar)
public JavaClasses importJars(JarFile... jarFiles)
public JavaClasses importJars(Iterable<JarFile> jarFiles)

// 通过包路径导入类 - 这个是比较常用的方法
public JavaClasses importPackages(Collection<String> packages)
public JavaClasses importPackages(String... packages)
public JavaClasses importPackagesOf(Class<?>... classes)
public JavaClasses importPackagesOf(Collection<Class<?>> classes)

导入类的方法提供了多维度的参数,用起来会十分便捷。例如想导入com.sample包下面的所有类,只需要这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ClassFileImporterTest {

@Test
public void testImportBootstarpClass() throws Exception {
ImportOptions importOptions = new ImportOptions()
// 不扫描jar包
.with(ImportOption.Predefined.DONT_INCLUDE_JARS)
// 排除不扫描的包
.with(new DontIncludePackagesImportOption("com.sample..support"));
ClassFileImporter classFileImporter = new ClassFileImporter(importOptions);
long start = System.currentTimeMillis();
JavaClasses javaClasses = classFileImporter.importPackages("com.sample");
long end = System.currentTimeMillis();
System.out.println(String.format("Found %d classes,cost %d ms", javaClasses.size(), end - start));
}
}

得到的JavaClassesJavaClass的集合,可以简单类比为反射中Class的集合,后面使用的代码规则和依赖规则判断都是强依赖于JavaClasses或者JavaClass

内建规则定义

类扫描和类导入完成之后,我们需要定检查规则,然后应用于所有导入的类,这样子就能完成对所有的类进行规则的过滤 - 或者说把规则应用于所有类并且进行断言。

规则定义依赖于ArchRuleDefinition类,创建出来的规则是ArchRule实例,规则实例的创建过程一般使用ArchRuleDefinition类的流式方法,这些流式方法定义上符合人类思考的思维逻辑,上手比较简单,举个例子:

1
2
3
4
5
6
7
8
9
ArchRule archRule = ArchRuleDefinition.noClasses()
// 在service包下的所有类
.that().resideInAPackage("..service..")
// 不能调用controller包下的任意类
.should().accessClassesThat().resideInAPackage("..controller..")
// 断言描述 - 不满足规则的时候打印出来的原因
.because("不能在service包中调用controller中的类");
// 对所有的JavaClasses进行判断
archRule.check(classes);

上面展示了自定义新的ArchRule的例子,中已经为我们内置了一些常用的ArchRule实现,它们位于GeneralCodingRules中:

  • NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS:不能调用System.out、System.err或者(Exception.)printStackTrace。
  • NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS:类不能直接抛出通用异常Throwable、Exception或者RuntimeException。
  • NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING:不能使用java.util.logging包路径下的日志组件。

更多内建的ArchRule或者通用的内置规则使用,可以参考官方例子

基本使用例子

基本使用例子,主要从一些常见的编码规范或者项目规范编写规则对项目所有类进行检查。

包依赖关系检查

j-a-r-u-1

1
2
3
ArchRule archRule = ArchRuleDefinition.noClasses()
.that().resideInAPackage("..com.source..")
.should().dependOnClassesThat().resideInAPackage("..com.target..");

j-a-r-u-2

1
2
3
ArchRule archRule = ArchRuleDefinition.classes()
.that().resideInAPackage("..com.foo..")
.should().onlyAccessClassesThat().resideInAnyPackage("..com.source..", "..com.foo..");

类依赖关系检查

j-a-r-u-3

1
2
3
ArchRule archRule = ArchRuleDefinition.classes()
.that().haveNameMatching(".*Bar")
.should().onlyBeAccessed().byClassesThat().haveSimpleName("Bar");

类包含于包的关系检查

j-a-r-u-4

1
2
3
ArchRule archRule = ArchRuleDefinition.classes()
.that().haveSimpleNameStartingWith("Foo")
.should().resideInAPackage("com.foo");

继承关系检查

j-a-r-u-5

1
2
3
ArchRule archRule = ArchRuleDefinition.classes()
.that().implement(Collection.class)
.should().haveSimpleNameEndingWith("Connection");

j-a-r-u-6

1
2
3
ArchRule archRule = ArchRuleDefinition.classes()
.that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byAnyPackage("..persistence..");

注解检查

j-a-r-u-7

1
2
3
ArchRule archRule = ArchRuleDefinition.classes()
.that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byClassesThat().areAnnotatedWith(Transactional.class)

逻辑层调用关系检查

例如项目结构如下:

1
2
3
4
5
6
7
8
- com.myapp.controller
SomeControllerOne.class
SomeControllerTwo.class
- com.myapp.service
SomeServiceOne.class
SomeServiceTwo.class
- com.myapp.persistence
SomePersistenceManager

例如我们规定:

  • 包路径com.myapp.controller中的类不能被其他层级包引用。
  • 包路径com.myapp.service中的类只能被com.myapp.controller中的类引用。
  • 包路径com.myapp.persistence中的类只能被com.myapp.service中的类引用。

编写规则如下:

1
2
3
4
5
6
7
8
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")

.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")

循环依赖关系检查

例如项目结构如下:

1
2
3
4
5
6
7
8
9
- com.myapp.moduleone
ClassOneInModuleOne.class
ClassTwoInModuleOne.class
- com.myapp.moduletwo
ClassOneInModuleTwo.class
ClassTwoInModuleTwo.class
- com.myapp.modulethree
ClassOneInModuleThree.class
ClassTwoInModuleThree.class

例如我们规定:com.myapp.moduleonecom.myapp.moduletwocom.myapp.modulethree三个包路径中的类不能形成一个循环依赖缓,例如:

1
ClassOneInModuleOne -> ClassOneInModuleTwo -> ClassOneInModuleThree -> ClassOneInModuleOne

编写规则如下:

1
slices().matching("com.myapp.(*)..").should().beFreeOfCycles()

核心API

把API分为三层,最重要的是"Core"层、"Lang"层和"Library"层。

Core层API

ArchUnit的Core层API大部分类似于Java原生反射API,例如JavaMethodJavaField对应于原生反射中的MethodField,它们提供了诸如getName()getMethods()getType()getParameters()等方法。

此外ArchUnit扩展一些API用于描述依赖代码之间关系,例如JavaMethodCallJavaConstructorCallJavaFieldAccess。还提供了例如Java类与其他Java类之间的导入访问关系的API如JavaClass#getAccessesFromSelf()

而需要导入类路径下或者Jar包中已经编译好的Java类,ArchUnit提供了ClassFileImporter完成此功能:

1
JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");

Lang层API

Core层的API十分强大,提供了需要关于Java程序静态结构的信息,但是直接使用Core层的API对于单元测试会缺乏表现力,特别表现在架构规则方面。

出于这个原因,ArchUnit提供了Lang层的API,它提供了一种强大的语法来以抽象的方式表达规则。Lang层的API大多数是采用流式编程方式定义方法,例如指定包定义和调用关系的规则如下:

1
2
3
4
5
6
ArchRule rule =
classes()
// 定义在service包下的所欲类
.that().resideInAPackage("..service..")
// 只能被controller包或者service包中的类访问
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

编写好规则后就可以基于导入所有编译好的类进行扫描:

1
2
3
JavaClasses classes = new ClassFileImporter().importPackage("com.myapp");
ArchRule rule = // 定义的规则
rule.check(classes);

Library层API

Library层API通过静态工厂方法提供了更多复杂而强大的预定义规则,入口类是:

1
com.tngtech.archunit.library.Architectures

目前,这只能为分层架构提供方便的检查,但将来可能会扩展为六边形架构\管道和过滤器,业务逻辑和技术基础架构的分离等样式。

还有其他几个相对强大的功能:

  • 代码切片功能,入口是com.tngtech.archunit.library.dependencies.SlicesRuleDefinition
  • 一般编码规则,入口是com.tngtech.archunit.library.GeneralCodingRules
  • PlantUML组件支持,功能位于包路径com.tngtech.archunit.library.plantuml下。

编写复杂的规则

一般来说,内建的规则不一定能够满足一些复杂的规范校验规则,因此需要编写自定义的规则。这里仅仅举一个前文提到的相对复杂的规则:

  • 定义在controller包下的Controller类的类名称以"Controller"结尾,方法的入参类型命名以"Request"结尾,返回参数命名以"Response"结尾。

官方提供的自定义规则的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DescribedPredicate<JavaClass> haveAFieldAnnotatedWithPayload =
new DescribedPredicate<JavaClass>("have a field annotated with @Payload"){
@Override
public boolean apply(JavaClass input) {
boolean someFieldAnnotatedWithPayload = // iterate fields and check for @Payload
return someFieldAnnotatedWithPayload;
}
};

ArchCondition<JavaClass> onlyBeAccessedBySecuredMethods =
new ArchCondition<JavaClass>("only be accessed by @Secured methods") {
@Override
public void check(JavaClass item, ConditionEvents events) {
for (JavaMethodCall call : item.getMethodCallsToSelf()) {
if (!call.getOrigin().isAnnotatedWith(Secured.class)) {
String message = String.format(
"Method %s is not @Secured", call.getOrigin().getFullName());
events.add(SimpleConditionEvent.violated(call, message));
}
}
}
};

classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods);

我们只需要模仿它的实现即可,具体如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ArchunitTest {

@Test
public void controller_class_rule() {
JavaClasses classes = new ClassFileImporter().importPackages("club.throwable");
DescribedPredicate<JavaClass> predicate =
new DescribedPredicate<JavaClass>("定义在club.throwable.controller包下的所有类") {
@Override
public boolean apply(JavaClass input) {
return null != input.getPackageName() && input.getPackageName().contains("club.throwable.controller");
}
};
ArchCondition<JavaClass> condition1 = new ArchCondition<JavaClass>("类名称以Controller结尾") {
@Override
public void check(JavaClass javaClass, ConditionEvents conditionEvents) {
String name = javaClass.getName();
if (!name.endsWith("Controller")) {
conditionEvents.add(SimpleConditionEvent.violated(javaClass, String.format("当前控制器类[%s]命名不以\"Controller\"结尾", name)));
}
}
};
ArchCondition<JavaClass> condition2 = new ArchCondition<JavaClass>("方法的入参类型命名以\"Request\"结尾,返回参数命名以\"Response\"结尾") {
@Override
public void check(JavaClass javaClass, ConditionEvents conditionEvents) {
Set<JavaMethod> javaMethods = javaClass.getMethods();
String className = javaClass.getName();
// 其实这里要做严谨一点需要考虑是否使用了泛型参数,这里暂时简化了
for (JavaMethod javaMethod : javaMethods) {
Method method = javaMethod.reflect();
Class<?>[] parameterTypes = method.getParameterTypes();
for (Class parameterType : parameterTypes) {
if (!parameterType.getName().endsWith("Request")) {
conditionEvents.add(SimpleConditionEvent.violated(method,
String.format("当前控制器类[%s]的[%s]方法入参不以\"Request\"结尾", className, method.getName())));
}
}
Class<?> returnType = method.getReturnType();
if (!returnType.getName().endsWith("Response")) {
conditionEvents.add(SimpleConditionEvent.violated(method,
String.format("当前控制器类[%s]的[%s]方法返回参数不以\"Response\"结尾", className, method.getName())));
}
}
}
};
ArchRuleDefinition.classes()
.that(predicate)
.should(condition1)
.andShould(condition2)
.because("定义在controller包下的Controller类的类名称以\"Controller\"结尾,方法的入参类型命名以\"Request\"结尾,返回参数命名以\"Response\"结尾")
.check(classes);
}
}

因为导入了所有需要的编译好的类的静态属性,基本上是可以编写所有能够想出来的规约,更多的内容或者实现可以自行摸索。

小结

通过最近的一个项目引入了Archunit,并且进行了一些编码规范和架构规范的规约,起到了十分明显的效果。之前口头或者书面文档的规范可以通过单元测试直接控制,项目构建的时候强制必须执行单元测试,只有所有单测通过才能构建和打包(禁止使用-Dmaven.test.skip=true参数),起到了十分明显的成效。

参考资料:

(e-a-2019216 c-1-d)

CGLIB动态代理原理分析

前提

前一篇文章介绍了CGLIB中常用的API,实际上使用了EnhancerMethodInterceptor之后会生成代理子类,这篇文章就是分析一下CGLIB动态代理的原理。

CGLIB动态代理原理分析

我们经常说CGLIB的动态代理的底层通过被代理类生成代理子类实现的,那么下面我们就分析一下生成的子类到底是什么样的。开启CGLIB的debug模式,输出它生成的类到指定的目录:

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
public class DebuggingCglibDemo {

private static final String METHOD_NAME = "sayHello";

public static void main(String[] args) throws Exception {
String location = DebuggingCglibDemo.class.getResource("").getPath() + "debugging/";
System.out.println("location -> " + location);
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, location);
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object result;
if (METHOD_NAME.equals(method.getName())) {
System.out.println("Before invoking sayHello...");
result = methodProxy.invokeSuper(obj, objects);
System.out.println("After invoking sayHello...");
} else {
result = methodProxy.invokeSuper(obj, objects);
}
return result;
}
});
SampleClass sampleClass = (SampleClass) enhancer.create();
System.out.println(sampleClass.sayHello("throwable"));
}
}

输出结果:

1
2
3
4
5
location -> /D:/Projects/cglib-seed/target/classes/club/throwable/cglib/debugging/
CGLIB debugging enabled, writing to '/D:/Projects/cglib-seed/target/classes/club/throwable/cglib/debugging/'
Before invoking sayHello...
After invoking sayHello...
throwable say hello!

这个时候,看下target下面生成的类如下:

c-d-p-1

一共有五个类:

../net.sf.cglib包下:

MethodWrapper$MethodWrapperKey$$KeyFactoryByCGLIB$$d45e49f7.class
Enhancer$EnhancerKey$$KeyFactoryByCGLIB$$7fb24d72.class

这两个类主要很缓存的Key相关,这里不做详细展开。

用户自定义包../club/throwable/cglib包下:

  • SampleClass$$EnhancerByCGLIB$$53c7afed$$FastClassByCGLIB$$da5c8621.class
  • SampleClass$$EnhancerByCGLIB$$53c7afed.class
  • SampleClass$$FastClassByCGLIB$$cf1a549b.class

这三个就是实际使用到的子类,其中有一个是被代理类的直接子类SampleClass$$EnhancerByCGLIB$$53c7afed,其他的两个是FastClass

接着我们先看一下SampleClass$$EnhancerByCGLIB$$53c7afed这个类的源码:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
与Java原生代理类似,仍然以静态变量保存了指向代理方法的引用
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
//每个函数有两个反射方法,一个是通过原生反射获得的,另外一个是通过Cglib构建的。
private static final Method CGLIB$sayHello$0$Method;
private static final MethodProxy CGLIB$sayHello$0$Proxy;
private static final Object[] CGLIB$emptyArgs;
private static final Method CGLIB$equals$1$Method;
private static final MethodProxy CGLIB$equals$1$Proxy;
private static final Method CGLIB$toString$2$Method;
private static final MethodProxy CGLIB$toString$2$Proxy;
private static final Method CGLIB$hashCode$3$Method;
private static final MethodProxy CGLIB$hashCode$3$Proxy;
private static final Method CGLIB$clone$4$Method;
private static final MethodProxy CGLIB$clone$4$Proxy;

//这里通过静态代码块初始化上面用到的静态变量,主要使用到反射
static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class var0 = Class.forName("club.throwable.cglib.SampleClass$$EnhancerByCGLIB$$53c7afed");
Class var1;
Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
CGLIB$equals$1$Method = var10000[0];
CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
CGLIB$toString$2$Method = var10000[1];
CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
CGLIB$hashCode$3$Method = var10000[2];
CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
CGLIB$clone$4$Method = var10000[3];
CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
CGLIB$sayHello$0$Method = ReflectUtils.findMethods(new String[]{"sayHello", "(Ljava/lang/String;)Ljava/lang/String;"}, (var1 = Class.forName("club.throwable.cglib.SampleClass")).getDeclaredMethods())[0];
CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)Ljava/lang/String;", "sayHello", "CGLIB$sayHello$0");
}

//这个方法就是直接调用原来的被代理类(父类)的方法
final String CGLIB$sayHello$0(String var1) {
return super.sayHello(var1);
}
//这个方法就是通过方法代理进行回调,里面用到了Callback实例
public final String sayHello(String var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}

return var10000 != null ? (String)var10000.intercept(this, CGLIB$sayHello$0$Method, new Object[]{var1}, CGLIB$sayHello$0$Proxy) : super.sayHello(var1);
}

final boolean CGLIB$equals$1(Object var1) {
return super.equals(var1);
}

public final boolean equals(Object var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}

if (var10000 != null) {
Object var2 = var10000.intercept(this, CGLIB$equals$1$Method, new Object[]{var1}, CGLIB$equals$1$Proxy);
return var2 == null ? false : (Boolean)var2;
} else {
return super.equals(var1);
}
}

final String CGLIB$toString$2() {
return super.toString();
}

public final String toString() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}

return var10000 != null ? (String)var10000.intercept(this, CGLIB$toString$2$Method, CGLIB$emptyArgs, CGLIB$toString$2$Proxy) : super.toString();
}

final int CGLIB$hashCode$3() {
return super.hashCode();
}

public final int hashCode() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}

if (var10000 != null) {
Object var1 = var10000.intercept(this, CGLIB$hashCode$3$Method, CGLIB$emptyArgs, CGLIB$hashCode$3$Proxy);
return var1 == null ? 0 : ((Number)var1).intValue();
} else {
return super.hashCode();
}
}

final Object CGLIB$clone$4() throws CloneNotSupportedException {
return super.clone();
}

protected final Object clone() throws CloneNotSupportedException {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}

return var10000 != null ? var10000.intercept(this, CGLIB$clone$4$Method, CGLIB$emptyArgs, CGLIB$clone$4$Proxy) : super.clone();
}

public static MethodProxy CGLIB$findMethodProxy(Signature var0) {
String var10000 = var0.toString();
switch(var10000.hashCode()) {
case -1816210712:
if (var10000.equals("sayHello(Ljava/lang/String;)Ljava/lang/String;")) {
return CGLIB$sayHello$0$Proxy;
}
break;
case -508378822:
if (var10000.equals("clone()Ljava/lang/Object;")) {
return CGLIB$clone$4$Proxy;
}
break;
case 1826985398:
if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
return CGLIB$equals$1$Proxy;
}
break;
case 1913648695:
if (var10000.equals("toString()Ljava/lang/String;")) {
return CGLIB$toString$2$Proxy;
}
break;
case 1984935277:
if (var10000.equals("hashCode()I")) {
return CGLIB$hashCode$3$Proxy;
}
}

return null;
}

public SampleClass$$EnhancerByCGLIB$$53c7afed() {
CGLIB$BIND_CALLBACKS(this);
}

public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] var0) {
CGLIB$THREAD_CALLBACKS.set(var0);
}

public static void CGLIB$SET_STATIC_CALLBACKS(Callback[] var0) {
CGLIB$STATIC_CALLBACKS = var0;
}

private static final void CGLIB$BIND_CALLBACKS(Object var0) {
SampleClass$$EnhancerByCGLIB$$53c7afed var1 = (SampleClass$$EnhancerByCGLIB$$53c7afed)var0;
if (!var1.CGLIB$BOUND) {
var1.CGLIB$BOUND = true;
Object var10000 = CGLIB$THREAD_CALLBACKS.get();
if (var10000 == null) {
var10000 = CGLIB$STATIC_CALLBACKS;
if (CGLIB$STATIC_CALLBACKS == null) {
return;
}
}

var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
}

}

public Object newInstance(Callback[] var1) {
CGLIB$SET_THREAD_CALLBACKS(var1);
SampleClass$$EnhancerByCGLIB$$53c7afed var10000 = new SampleClass$$EnhancerByCGLIB$$53c7afed();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}

public Object newInstance(Callback var1) {
CGLIB$SET_THREAD_CALLBACKS(new Callback[]{var1});
SampleClass$$EnhancerByCGLIB$$53c7afed var10000 = new SampleClass$$EnhancerByCGLIB$$53c7afed();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}

public Object newInstance(Class[] var1, Object[] var2, Callback[] var3) {
CGLIB$SET_THREAD_CALLBACKS(var3);
SampleClass$$EnhancerByCGLIB$$53c7afed var10000 = new SampleClass$$EnhancerByCGLIB$$53c7afed;
switch(var1.length) {
case 0:
var10000.<init>();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
default:
throw new IllegalArgumentException("Constructor not found");
}
}

public Callback getCallback(int var1) {
CGLIB$BIND_CALLBACKS(this);
MethodInterceptor var10000;
switch(var1) {
case 0:
var10000 = this.CGLIB$CALLBACK_0;
break;
default:
var10000 = null;
}

return var10000;
}

public void setCallback(int var1, Callback var2) {
switch(var1) {
case 0:
this.CGLIB$CALLBACK_0 = (MethodInterceptor)var2;
default:
}
}

public Callback[] getCallbacks() {
CGLIB$BIND_CALLBACKS(this);
return new Callback[]{this.CGLIB$CALLBACK_0};
}

public void setCallbacks(Callback[] var1) {
this.CGLIB$CALLBACK_0 = (MethodInterceptor)var1[0];
}

static {
CGLIB$STATICHOOK1();
}
}

这个类十分长,因为里面有很多的变量,它们都通过了静态代码块使用反射初始化。类的代码比JDK动态代理的子类多,因此生成效率会比较低。相关比较重要的注释已经写在类中,我们最主要关注两点:

  • 第一点:
1
2
private static final Method CGLIB$sayHello$0$Method;
private static final MethodProxy CGLIB$sayHello$0$Proxy;

这两个静态变量都是指向sayHello这个方法,CGLIB$sayHello$0$Method直接指向父类方法,CGLIB$sayHello$0$Proxy是CGLIB生成的方法代理。

  • 第二点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//这个方法就是直接调用原来的被代理类(父类)的方法
final String CGLIB$sayHello$0(String var1) {
return super.sayHello(var1);
}
//这个方法就是通过方法代理进行回调,里面用到了Callback实例
public final String sayHello(String var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
//如果找不到Callback会直接调用父类的原方法
return var10000 != null ? (String)var10000.intercept(this, CGLIB$sayHello$0$Method, new Object[]{var1}, CGLIB$sayHello$0$Proxy) : super.sayHello(var1);
}

也就是如果想要启用CGLIB的回调,我们主观上应该是这样操作的:

1
2
SampleClass$$EnhancerByCGLIB$$53c7afed sample = new SampleClass$$EnhancerByCGLIB$$53c7afed();
sample.sayHello("doge");

但是由于这个代理类是动态生成的,只能通过反射调用。

那么,剩下的两个FastClass的作用是什么?我们先看一下MethodProxyinvoke()invokeSuper()方法:

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
36
37
38
39
40
41
42
43
44
  //初始化FastClass中的index索引
private void init() {
if (this.fastClassInfo == null) {
Object var1 = this.initLock;
synchronized(this.initLock) {
if (this.fastClassInfo == null) {
MethodProxy.CreateInfo ci = this.createInfo;
MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
fci.f1 = helper(ci, ci.c1);
fci.f2 = helper(ci, ci.c2);
fci.i1 = fci.f1.getIndex(this.sig1);
fci.i2 = fci.f2.getIndex(this.sig2);
this.fastClassInfo = fci;
this.createInfo = null;
}
}
}
}

public Object invoke(Object obj, Object[] args) throws Throwable {
try {
this.init();
MethodProxy.FastClassInfo fci = this.fastClassInfo;
return fci.f1.invoke(fci.i1, obj, args);
} catch (InvocationTargetException var4) {
throw var4.getTargetException();
} catch (IllegalArgumentException var5) {
if (this.fastClassInfo.i1 < 0) {
throw new IllegalArgumentException("Protected method: " + this.sig1);
} else {
throw var5;
}
}
}

public Object invokeSuper(Object obj, Object[] args) throws Throwable {
try {
this.init();
MethodProxy.FastClassInfo fci = this.fastClassInfo;
return fci.f2.invoke(fci.i2, obj, args);
} catch (InvocationTargetException var4) {
throw var4.getTargetException();
}
}

这里,两个方法各自使用了不同的FastClassInfo实例fci.f2fci.f2。其中,SampleClass$$FastClassByCGLIB$$cf1a549b.class是对应于父类,而SampleClass$$EnhancerByCGLIB$$53c7afed$$FastClassByCGLIB$$da5c8621是对应于CGLIB生成的被代理类的子类。下面展开SampleClass$$EnhancerByCGLIB$$53c7afed$$FastClassByCGLIB$$da5c8621的源码:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
public class SampleClass$$EnhancerByCGLIB$$53c7afed$$FastClassByCGLIB$$da5c8621 extends FastClass {
public SampleClass$$EnhancerByCGLIB$$53c7afed$$FastClassByCGLIB$$da5c8621(Class var1) {
super(var1);
}

public int getIndex(Signature var1) {
String var10000 = var1.toString();
switch(var10000.hashCode()) {
case -2055565910:
if (var10000.equals("CGLIB$SET_THREAD_CALLBACKS([Lnet/sf/cglib/proxy/Callback;)V")) {
return 10;
}
break;
case -1882565338:
if (var10000.equals("CGLIB$equals$1(Ljava/lang/Object;)Z")) {
return 18;
}
break;
case -1816210712:
if (var10000.equals("sayHello(Ljava/lang/String;)Ljava/lang/String;")) {
return 7;
}
break;
case -1457535688:
if (var10000.equals("CGLIB$STATICHOOK1()V")) {
return 15;
}
break;
case -1411842725:
if (var10000.equals("CGLIB$hashCode$3()I")) {
return 19;
}
break;
case -894172689:
if (var10000.equals("newInstance(Lnet/sf/cglib/proxy/Callback;)Ljava/lang/Object;")) {
return 5;
}
break;
case -623122092:
if (var10000.equals("CGLIB$findMethodProxy(Lnet/sf/cglib/core/Signature;)Lnet/sf/cglib/proxy/MethodProxy;")) {
return 14;
}
break;
case -508378822:
if (var10000.equals("clone()Ljava/lang/Object;")) {
return 3;
}
break;
case -419626537:
if (var10000.equals("setCallbacks([Lnet/sf/cglib/proxy/Callback;)V")) {
return 13;
}
break;
case 560567118:
if (var10000.equals("setCallback(ILnet/sf/cglib/proxy/Callback;)V")) {
return 8;
}
break;
case 811063227:
if (var10000.equals("newInstance([Ljava/lang/Class;[Ljava/lang/Object;[Lnet/sf/cglib/proxy/Callback;)Ljava/lang/Object;")) {
return 6;
}
break;
case 973717575:
if (var10000.equals("getCallbacks()[Lnet/sf/cglib/proxy/Callback;")) {
return 11;
}
break;
case 1221173700:
if (var10000.equals("newInstance([Lnet/sf/cglib/proxy/Callback;)Ljava/lang/Object;")) {
return 4;
}
break;
case 1230699260:
if (var10000.equals("getCallback(I)Lnet/sf/cglib/proxy/Callback;")) {
return 12;
}
break;
case 1298742135:
if (var10000.equals("CGLIB$sayHello$0(Ljava/lang/String;)Ljava/lang/String;")) {
return 16;
}
break;
case 1306468936:
if (var10000.equals("CGLIB$toString$2()Ljava/lang/String;")) {
return 20;
}
break;
case 1584330438:
if (var10000.equals("CGLIB$SET_STATIC_CALLBACKS([Lnet/sf/cglib/proxy/Callback;)V")) {
return 9;
}
break;
case 1800494055:
if (var10000.equals("CGLIB$clone$4()Ljava/lang/Object;")) {
return 17;
}
break;
case 1826985398:
if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
return 0;
}
break;
case 1913648695:
if (var10000.equals("toString()Ljava/lang/String;")) {
return 1;
}
break;
case 1984935277:
if (var10000.equals("hashCode()I")) {
return 2;
}
}

return -1;
}

public int getIndex(String var1, Class[] var2) {
switch(var1.hashCode()) {
case -2012993625:
if (var1.equals("sayHello")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("java.lang.String")) {
return 7;
}
}
}
break;
case -1983192202:
if (var1.equals("CGLIB$sayHello$0")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("java.lang.String")) {
return 16;
}
}
}
break;
case -1776922004:
if (var1.equals("toString")) {
switch(var2.length) {
case 0:
return 1;
}
}
break;
case -1295482945:
if (var1.equals("equals")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("java.lang.Object")) {
return 0;
}
}
}
break;
case -1053468136:
if (var1.equals("getCallbacks")) {
switch(var2.length) {
case 0:
return 11;
}
}
break;
case -124978609:
if (var1.equals("CGLIB$equals$1")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("java.lang.Object")) {
return 18;
}
}
}
break;
case -60403779:
if (var1.equals("CGLIB$SET_STATIC_CALLBACKS")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("[Lnet.sf.cglib.proxy.Callback;")) {
return 9;
}
}
}
break;
case -29025555:
if (var1.equals("CGLIB$hashCode$3")) {
switch(var2.length) {
case 0:
return 19;
}
}
break;
case 85179481:
if (var1.equals("CGLIB$SET_THREAD_CALLBACKS")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("[Lnet.sf.cglib.proxy.Callback;")) {
return 10;
}
}
}
break;
case 94756189:
if (var1.equals("clone")) {
switch(var2.length) {
case 0:
return 3;
}
}
break;
case 147696667:
if (var1.equals("hashCode")) {
switch(var2.length) {
case 0:
return 2;
}
}
break;
case 161998109:
if (var1.equals("CGLIB$STATICHOOK1")) {
switch(var2.length) {
case 0:
return 15;
}
}
break;
case 495524492:
if (var1.equals("setCallbacks")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("[Lnet.sf.cglib.proxy.Callback;")) {
return 13;
}
}
}
break;
case 1154623345:
if (var1.equals("CGLIB$findMethodProxy")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("net.sf.cglib.core.Signature")) {
return 14;
}
}
}
break;
case 1543336189:
if (var1.equals("CGLIB$toString$2")) {
switch(var2.length) {
case 0:
return 20;
}
}
break;
case 1811874389:
if (var1.equals("newInstance")) {
switch(var2.length) {
case 1:
String var10001 = var2[0].getName();
switch(var10001.hashCode()) {
case -845341380:
if (var10001.equals("net.sf.cglib.proxy.Callback")) {
return 5;
}
break;
case 1730110032:
if (var10001.equals("[Lnet.sf.cglib.proxy.Callback;")) {
return 4;
}
}
case 2:
default:
break;
case 3:
if (var2[0].getName().equals("[Ljava.lang.Class;") && var2[1].getName().equals("[Ljava.lang.Object;") && var2[2].getName().equals("[Lnet.sf.cglib.proxy.Callback;")) {
return 6;
}
}
}
break;
case 1817099975:
if (var1.equals("setCallback")) {
switch(var2.length) {
case 2:
if (var2[0].getName().equals("int") && var2[1].getName().equals("net.sf.cglib.proxy.Callback")) {
return 8;
}
}
}
break;
case 1905679803:
if (var1.equals("getCallback")) {
switch(var2.length) {
case 1:
if (var2[0].getName().equals("int")) {
return 12;
}
}
}
break;
case 1951977610:
if (var1.equals("CGLIB$clone$4")) {
switch(var2.length) {
case 0:
return 17;
}
}
}

return -1;
}

public int getIndex(Class[] var1) {
switch(var1.length) {
case 0:
return 0;
default:
return -1;
}
}

public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
53c7afed var10000 = (53c7afed)var2;
int var10001 = var1;

try {
switch(var10001) {
case 0:
return new Boolean(var10000.equals(var3[0]));
case 1:
return var10000.toString();
case 2:
return new Integer(var10000.hashCode());
case 3:
return var10000.clone();
case 4:
return var10000.newInstance((Callback[])var3[0]);
case 5:
return var10000.newInstance((Callback)var3[0]);
case 6:
return var10000.newInstance((Class[])var3[0], (Object[])var3[1], (Callback[])var3[2]);
case 7:
return var10000.sayHello((String)var3[0]);
case 8:
var10000.setCallback(((Number)var3[0]).intValue(), (Callback)var3[1]);
return null;
case 9:
53c7afed.CGLIB$SET_STATIC_CALLBACKS((Callback[])var3[0]);
return null;
case 10:
53c7afed.CGLIB$SET_THREAD_CALLBACKS((Callback[])var3[0]);
return null;
case 11:
return var10000.getCallbacks();
case 12:
return var10000.getCallback(((Number)var3[0]).intValue());
case 13:
var10000.setCallbacks((Callback[])var3[0]);
return null;
case 14:
return 53c7afed.CGLIB$findMethodProxy((Signature)var3[0]);
case 15:
53c7afed.CGLIB$STATICHOOK1();
return null;
case 16:
return var10000.CGLIB$sayHello$0((String)var3[0]);
case 17:
return var10000.CGLIB$clone$4();
case 18:
return new Boolean(var10000.CGLIB$equals$1(var3[0]));
case 19:
return new Integer(var10000.CGLIB$hashCode$3());
case 20:
return var10000.CGLIB$toString$2();
}
} catch (Throwable var4) {
throw new InvocationTargetException(var4);
}

throw new IllegalArgumentException("Cannot find matching method/constructor");
}

public Object newInstance(int var1, Object[] var2) throws InvocationTargetException {
53c7afed var10000 = new 53c7afed;
53c7afed var10001 = var10000;
int var10002 = var1;

try {
switch(var10002) {
case 0:
var10001.<init>();
return var10000;
}
} catch (Throwable var3) {
throw new InvocationTargetException(var3);
}

throw new IllegalArgumentException("Cannot find matching method/constructor");
}

public int getMaxIndex() {
return 20;
}
}

这个类更加长,但是其实大部分都是switch-case的代码块,它的功能就是为方法的调用添加基于整型数字的索引,主要目的是为了减少反射调用的时间,将反射调用转化为直接调用。简单来说,invokeSuper的流程就是这样的

  • 通过MethodProxy的init方法,用当前方法的Signature(签名)构建两个FastClass实例(当然,这里会做缓存)和当前方法对应的index,存放在FastClassInfo实例中。
  • 通过FastClassInfo中的FastClass实例和index,在FastClass中找到对应的方法(在switch-case块中基于整数索引index进行查找)直接调用。

直观来看,CGLIB在类生成期间的操作会相对耗时,而且生成的类数目比较多,会占据大量永久代或者元空间的内存。子类一旦生成,后面的方法调用就会变成搜索方法索引和直接调用,这样的操作在特定的条件下效率会比JDK的反射高。这里特定的场景是指CGLIB子类中的switch-case块不大并且当前调用的方法的index在switch-case块的前部而不是中后部(简单来说就是子类中的方法要尽量少从而提高switch-case中的搜寻效率)。详细可以参考这篇性能对比的文章:cglib和jdk动态代理调用性能测

小结

CGLIB提供了许多基于代码生成的高级功能的API,可以在通过上面的例子熟悉它的使用,并且在合适的场景用于实战中。可能最常用到的是基于Enhancer的动态代理,这里总结一下CGLIB和JDK动态代理的区别(老生常谈):

  • JDK动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类,但是该普通类必须能够被继承(不能用final修饰符)。
  • JDK动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行修改,使用了FastClass的特性,在某些情况下类的方法执行会比较高效。

(本文完 e-a-20181216 c-1-d)

简述CGLIB常用API

CGLIB简介

CGLIB,即Code Generation Library,是一个强大的、高性能的代码生成库。其被广泛应用于AOP框架(例如Spring)中,用以提供方法拦截操作。Hibernate作为一个比较受欢迎的ORM框架,同样使用CGLIB来代理单端(多对一和一对一)关联(延迟提取集合使用的另一种机制)。CGLIB作为一个开源项目,其代码托管在github,地址为:https://github.com/cglib/cglib

CGLIB的github简介:CGLIB - 字节码生成库,是用于生成和转换Java字节码的高级API。它被AOP、测试、数据访问框架用于生成动态代理对象和拦截字段访问。(原文:cglib - Byte Code Generation Library is high level API to generate and transform Java byte code. It is used by AOP, testing, data access frameworks to generate dynamic proxy objects and intercept field access.)

CGLIB提供两种类型的JAR包:

  • cglib-nodep-x.x.x.jar:使用nodep包不需要关联ASM的jar包,也就是jar包内部包含ASM的类库。
  • cglib-x.x.x.jar:使用此jar包需要另外提供ASM的jar包,否则运行时报错,建议选用不包含ASM类库的jar包,可以方便控制ASM的。

本文中使用的CGLIB依赖为:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.2.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>

CGLIB基本原理

  • 基本原理:动态生成一个要代理类的子类(被代理的类作为继承的父类),子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用Java反射的JDK动态代理要快,因为它采用了整形变量建立了方法索引。
  • 底层实现:使用字节码处理框架ASM,用于转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求必须对JVM内部结构包括class文件的格式和JVM指令集都很熟悉,否则一旦出现错误将会是JVM崩溃级别的异常
  • 劣势:对于final方法或者final的类,无法进行代理。

CGLIB的包结构

  • net.sf.cglib.core:底层字节码处理类,它们大部分与ASM有关系,在使用者角度来看不需要过多关注此包。
  • net.sf.cglib.transform:编译期或运行期类和类文件的转换。
  • net.sf.cglib.proxy:实现创建代理和方法拦截器的类。
  • net.sf.cglib.reflect:反射相关工具类。
  • net.sf.cglib.util:集合排序等工具类。
  • net.sf.cglib.beans:JavaBean相关的工具类。

CGLIB常用API介绍

下面介绍一下CGLIB中常用的API,先建立一个模特接口类和普通模特类:

1
2
3
4
5
6
7
8
9
10
11
public class SampleClass {

public String sayHello(String name) {
return String.format("%s say hello!", name);
}
}

public interface SampleInterface {

String sayHello(String name);
}

Enhancer

Enhancer,即(字节码)增强器。它是CGLIB库中最常用的一个类,功能JDK动态代理中引入的Proxy类差不多,但是Enhancer既能够代理普通的Java类,也能够代理接口。Enhancer创建一个被代理对象的子类并且拦截所有的方法调用(包括从Object中继承的toString和hashCode方法)。Enhancer不能够拦截final方法,例如Object.getClass()方法,这是由于final关键字的语义决定的。基于同样的道理,Enhancer也不能对fianl类进行代理操作。这也是Hibernate为什么不能持久化final关键字修饰的类的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EnhancerClassDemo {

public static void main(String[] args) throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
//使用FixedValue,拦截返回值,每次返回固定值"Doge say hello!"
enhancer.setCallback((FixedValue) () -> "Doge say hello!");
SampleClass sampleClass = (SampleClass) enhancer.create();
System.out.println(sampleClass.sayHello("throwable-10086"));
System.out.println(sampleClass.sayHello("throwable-doge"));
System.out.println(sampleClass.toString());
System.out.println(sampleClass.getClass());
System.out.println(sampleClass.hashCode());
}
}

输出结果:

1
2
3
4
5
6
Doge say hello!
Doge say hello!
Doge say hello!
class club.throwable.cglib.SampleClass$$EnhancerByCGLIB$$6f6e7a68
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
......

上述代码中,FixedValue用来对所有拦截的方法返回相同的值,从输出我们可以看出来,Enhancer对非final方法test()、toString()、hashCode()进行了拦截,没有对getClass进行拦截。由于hashCode()方法需要返回一个Number,但是我们返回的是一个String,这解释了上面的程序中为什么会抛出异常。

Enhancer3setSuperclass()用来设置父类型,从toString()方法可以看出,使用CGLIB生成的类为被代理类的一个子类,类简写名称为SampleClass$$EnhancerByCGLIB$$e3ea9b7

Enhancer#create(Class[] argumentTypes, Object[] arguments)方法是用来创建增强对象的,其提供了很多不同参数的方法用来匹配被增强类的不同构造方法。我们也可以先使用Enhancer#createClass()来创建字节码(.class),然后用字节码加载完成后的类动态生成增强后的对象。Enhancer中还有其他几个方法名为create的方法,提供不同的参数选择,具体可以自行查阅。

下面再举个例子说明一下使用Enhancer代理接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EnhancerInterfaceDemo {

public static void main(String[] args) throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setInterfaces(new Class[]{SampleInterface.class});
enhancer.setCallback((FixedValue) () -> "Doge say hello!");
SampleInterface sampleInterface = (SampleInterface) enhancer.create();
System.out.println(sampleInterface.sayHello("throwable-10086"));
System.out.println(sampleInterface.sayHello("throwable-doge"));
System.out.println(sampleInterface.toString());
System.out.println(sampleInterface.getClass());
System.out.println(sampleInterface.hashCode());
}
}

输出结果和上一个例子一致。

Callback

Callback,即回调。值得注意的是,它是一个标识接口(空接口,没有任何方法),它的回调时机是生成的代理类的方法被调用的时候。也就是说,生成的代理类的方法被调用的时候,Callback的实现逻辑就会被调用。Enhancer通过setCallback()setCallbacks()设置Callback设置了多个Callback实例将会按照设置的顺序进行回调。CGLIB中提供的Callback的子类有以下几种:

  • NoOp
  • FixedValue
  • InvocationHandler
  • MethodInterceptor
  • Dispatcher
  • LazyLoader

NoOp

NoOp,No Operation,也就是不做任何操作。这个回调实现只是简单地把方法调用委托给了被代理类的原方法(也就是调用原始类的原始方法),不做任何其它的操作,所以不能使用在接口代理。

1
2
3
4
5
6
7
8
9
10
public class NoOpDemo {

public static void main(String[] args) throws Exception{
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(NoOp.INSTANCE);
SampleClass sampleClass = (SampleClass) enhancer.create();
System.out.println(sampleClass.sayHello("throwable"));
}
}

输出结果:

1
throwable say hello!

FixedValue

FixedValue,Fixed Value,即固定值。它提供了一个loadObject()方法,不过这个方法返回的不是代理对象,而是原方法调用想要的结果。也就是说,在这个Callback里面,看不到任何原方法的信息,也就没有调用原方法的逻辑,不管原方法是什么都只会调用loadObject()并返回一个固定结果。需要注意的是,如果loadObject()方法的返回值并不能转换成原方法的返回值类型,那么会抛出类型转换异常(ClassCastException)。

最前面的Enhancer两个例子就是用FixedValue做分析的,这里不再举例。

InvocationHandler

InvocationHandler全类名为net.sf.cglib.proxy.InvocationHandler,它的功能和JDK动态代理中的java.lang.reflect.InvocationHandler类似,提供了一个Object invoke(Object proxy, Method method, Object[] objects)方法。需要注意的是:所有对invoke方法的参数proxy对象的方法调用都会被委托给同一个InvocationHandler,所以可能会导致无限循环(因为invoke中调用的任何原代理类方法,均会重新代理到invoke方法中)。举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InvocationHandlerDeadLoopDemo {

public static void main(String[] args) throws Exception{
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
return method.invoke(o, objects);
}
});
SampleClass sampleClass = (SampleClass) enhancer.create();
System.out.println(sampleClass.sayHello("throwable"));
}
}

上面的main方法执行后会直接爆栈,因为method#invoke()方法会重新调用InvocationHandler的invoke方法,形成死循环。正确的使用例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class InvocationHandlerDemo {

public static void main(String[] args) throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
if (!Objects.equals(method.getDeclaringClass(), Object.class) && Objects.equals(String.class, method.getReturnType())) {
return String.format("%s say hello!", objects);
}
return "No one say hello!";
}
});
SampleClass sampleClass = (SampleClass) enhancer.create();
System.out.println(sampleClass.sayHello("throwable"));
}
}

输出结果:

1
throwable say hello!

MethodInterceptor

MethodInterceptor,即方法拦截器,这是一个功能很强大的接口,它可以实现类似于AOP编程中的环绕增强(Around Advice)。它只有一个方法public Object intercept(Object obj,java.lang.reflect.Method method,Object[] args,MethodProxy methodProxy) throws Throwable。设置了MethodInterceptor后,代理类的所有方法调用都会转而执行这个接口中的intercept方法而不是原方法。如果需要在intercept方法中执行原方法可以使用参数method基于代理实例obj进行反射调用,但是使用方法代理methodProxy效率会更高(反射调用比正常的方法调用的速度慢很多)。MethodInterceptor的生成效率不高,它的优势在于调用效率,它需要产生不同类型的字节码,并且需要生成一些运行时对象(InvocationHandler就不需要)。注意,在使用MethodProxy调用invokeSuper方法相当于通过方法代理直接调用原类的对应方法,如果调用MethodProxy的invoke会进入死循环导致爆栈,原因跟InvocationHandler差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MethodInterceptorDemo {

public static void main(String[] args) throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("Before invoking sayHello...");
Object result = methodProxy.invokeSuper(obj, objects);
System.out.println("After invoking sayHello...");
return result;
}
});
SampleClass sampleClass = (SampleClass) enhancer.create();
System.out.println(sampleClass.sayHello("throwable"));
}
}

输出结果:

1
2
3
Before invoking sayHello...
After invoking sayHello...
throwable say hello!

这个例子就是Spring的AOP中的环绕增强(Around Advice)的简化版,这里没有改变原来的方法的行为,只是在方法调用前和调用后织入额外的逻辑。

Dispatcher

Dispatcher,即分发器,提供一个方法Object loadObject() throws Exception;,同样地返回一个代理对象,这个对象同样可以代理原方法的调用。Dispatcher的loadObject()方法在每次发生对原方法的调用时都会被调用并返回一个代理对象来调用原方法。Dispatcher可以类比为Spring中的Prototype类型。

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
public class DispatcherDemo {

private static final AtomicInteger COUNTER = new AtomicInteger(0);

public static void main(String[] args) throws Exception {
Enhancer enhancer = new Enhancer();
SampleInterfaceImpl impl = new SampleInterfaceImpl();
enhancer.setInterfaces(new Class[]{SampleInterface.class});
enhancer.setCallback(new Dispatcher() {
@Override
public Object loadObject() throws Exception {
COUNTER.incrementAndGet();
return impl;
}
});
SampleInterface sampleInterface = (SampleInterface) enhancer.create();
System.out.println(sampleInterface.sayHello("throwable-1"));
System.out.println(sampleInterface.sayHello("throwable-2"));
System.out.println(COUNTER.get());
}

private static class SampleInterfaceImpl implements SampleInterface{

public SampleInterfaceImpl(){
System.out.println("SampleInterfaceImpl init...");
}

@Override
public String sayHello(String name) {
return "Hello i am SampleInterfaceImpl!";
}
}
}

输出结果:

1
2
3
4
SampleInterfaceImpl init...
Hello i am SampleInterfaceImpl!
Hello i am SampleInterfaceImpl!
2

计数器输出为2,印证了每次调用方法都会回调Dispatcher中的实例进行调用。

LazyLoader

LazyLoader,即懒加载器,它只提供了一个方法Object loadObject() throws Exception;,loadObject()方法会在第一次被代理类的方法调用时触发,它返回一个代理类的对象,这个对象会被存储起来然后负责所有被代理类方法的调用,就像它的名字说的那样,一种lazy加载模式。如果被代理类或者代理类的对象的创建比较麻烦,而且不确定它是否会被使用,那么可以选择使用这种lazy模式来延迟生成代理。LazyLoader可以类比为Spring中的Lazy模式的Singleton。

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
public class LazyLoaderDemo {

private static final AtomicInteger COUNTER = new AtomicInteger(0);

public static void main(String[] args) throws Exception {
Enhancer enhancer = new Enhancer();
SampleInterfaceImpl impl = new SampleInterfaceImpl();
enhancer.setInterfaces(new Class[]{SampleInterface.class});
enhancer.setCallback(new LazyLoader() {
@Override
public Object loadObject() throws Exception {
COUNTER.incrementAndGet();
return impl;
}
});
SampleInterface sampleInterface = (SampleInterface) enhancer.create();
System.out.println(sampleInterface.sayHello("throwable-1"));
System.out.println(sampleInterface.sayHello("throwable-2"));
System.out.println(COUNTER.get());
}

private static class SampleInterfaceImpl implements SampleInterface{

public SampleInterfaceImpl(){
System.out.println("SampleInterfaceImpl init...");
}

@Override
public String sayHello(String name) {
return "Hello i am SampleInterfaceImpl!";
}
}
}

输出结果:

1
2
3
4
SampleInterfaceImpl init...
Hello i am SampleInterfaceImpl!
Hello i am SampleInterfaceImpl!
1

计数器输出为1,印证了LazyLoader中的实例只回调了1次,这就是懒加载。

BeanCopier

JavaBean属性拷贝器,提供从一个JavaBean实例中拷贝属性到另一个JavaBean实例中,注意类型必须完全匹配属性才能拷贝成功(原始类型和其包装类不属于相同类型)。它还提供了一个net.sf.cglib.core.Converter转换器回调接口让使用者控制拷贝的过程。注意,BeanCopier内部使用了缓存和基于ASM动态生成BeanCopier的子类实现的转换方法中直接使用实例的Getter和Setter方法,拷贝速度极快(BeanCopier属性拷贝比直接的Setter、Getter稍慢,稍慢的原因在于首次需要动态生成BeanCopier的子类,一旦子类生成完成之后就和直接的Setter、Getter效率一致,但是效率远远高于其他使用反射的工具类库)。

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
36
public class BeanCopierDemo {

private static final Map<String, BeanCopier> CACHE = new ConcurrentHashMap<>();

public static void main(String[] args) throws Exception {
//这里useConverter设置为false,调用copy方法的时候不能传入转换器实例
BeanCopier beanCopier;
String key = generateCacheKey(Person.class, Person.class);
if (CACHE.containsKey(key)) {
beanCopier = CACHE.get(key);
} else {
beanCopier = BeanCopier.create(Person.class, Person.class, false);
CACHE.put(key, beanCopier);
}
Person person = new Person();
person.setId(10086L);
person.setName("throwable");
person.setAge(25);
Person newPerson = new Person();
beanCopier.copy(person, newPerson, null); //这里转换器实例要传null
System.out.println(newPerson);
}

private static String generateCacheKey(Class<?> source, Class<?> target) {
return String.format("%s-%s", source.getName(), target.getName());
}

@ToString
@Data
private static class Person {

private Long id;
private String name;
private Integer age;
}
}

输出结果:

1
BeanCopierDemo.Person(id=10086, name=throwable, age=25)

在使用BeanCopier时候最好缓存BeanCopier实例,因为构造BeanCopier实例是一个耗时的操作。

ImmutableBean

ImmutableBean,即不可变的Bean。ImmutableBean允许创建一个原来对象的包装类,这个包装类是不可变的,任何改变底层对象的包装类操作都会抛出IllegalStateException。但是我们可以通过直接操作底层对象来改变包装类对象。这有点类似于Guava中的不可变视图或者JDK中的不可变集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ImmutableBeanDemo {

public static void main(String[] args) throws Exception {
Person person = new Person();
person.setName("throwable");
Person immutablePerson = (Person) ImmutableBean.create(person);
System.out.println(immutablePerson.getName());
person.setName("doge");
System.out.println(immutablePerson.getName());
immutablePerson.setName("throwable-doge");
System.out.println(immutablePerson.getName());
}

@Data
private static class Person {

private String name;
}
}

输出结果:

1
2
3
4
throwable
doge
Exception in thread "main" java.lang.IllegalStateException: Bean is immutable
...

BeanGenerator

BeanGenerator,即Bean生成器,使用它能够在运行时动态的创建一个JavaBean。可以直接设置父类,生成的JavaBean就是父类类型的实例。

1
2
3
4
5
6
7
8
9
10
11
12
public class BeanGeneratorDemo {

public static void main(String[] args) throws Exception {
BeanGenerator beanGenerator = new BeanGenerator();
beanGenerator.addProperty("name", String.class);
Object target = beanGenerator.create();
Method setter = target.getClass().getDeclaredMethod("setName", String.class);
Method getter = target.getClass().getDeclaredMethod("getName");
setter.invoke(target, "throwable");
System.out.println(getter.invoke(target));
}
}

输出结果:

1
throwable

BulkBean

相比于BeanCopier,BulkBean创建时候依赖于确定的目标类型,Setter和Getter方法名称列表以及参数类型,它将copy的动作拆分为getPropertyValues()setPropertyValues()两个方法,允许自定义处理属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BulkBeanDemo {

public static void main(String[] args) throws Exception {
BulkBean bulkBean = BulkBean.create(
Person.class,
new String[]{"getName"},
new String[]{"setName"},
new Class[]{String.class});
Person person = new Person();
person.setName("throwable");
Object[] propertyValues = bulkBean.getPropertyValues(person);
System.out.println(Arrays.toString(propertyValues));
bulkBean.setPropertyValues(person, new Object[]{"doge"});
System.out.println(person.getName());
}

@Data
private static class Person {

private String name;
}
}

输出结果:

1
2
[throwable]
doge

BeanMap

BeanMap类实现了JDK的java.util.Map接口,将一个JavaBean对象中的所有属性转换为一个String-To-Obejct的Map实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BeanMapDemo {

public static void main(String[] args) throws Exception{
Person person = new Person();
person.setName("throwable");
BeanMap beanMap = BeanMap.create(person);
System.out.println(beanMap);
System.out.println(beanMap.get("name"));
}

@Data
private static class Person {

private String name;
}
}

输出结果:

1
2
{name=throwable}
throwable

KeyFactory

KeyFactory源码中的注释是:Generates classes to handle multi-valued keys, for use in things such as Maps and Sets. Code for equals and hashCode methods follow the the rules laid out in Effective Java by Joshua Bloch.(翻译一下:通过生成类来处理多值键,以便在诸如Map和集合之类的东西中使用。equals和hashCode方法的代码遵循Joshua Bloch在《Effective Java》中列出的规则)。

什么叫multi-valued keys?

就是有多个键的组合,一起作为一个Key。

比如[a b c]是一个组合,一起作为一个key,[2 3]也可以是作为一个key。

KeyFactory就是用来生成这样一组Key的,通过两组的equals,hashCode等方法判断是否为同一组key的场景。为了描述Key的组合,需要定义一个接口,仅提供一个方法,叫做newInstance(),且返回值为Object,这个是使用KeyFactory的要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class KeyFactoryDemo {

public static void main(String[] args) throws Exception {
KeyFactoryInterface keyFactoryInterface1 = (KeyFactoryInterface) KeyFactory.create(KeyFactoryInterface.class);
KeyFactoryInterface keyFactoryInterface2 = (KeyFactoryInterface) KeyFactory.create(KeyFactoryInterface.class);
System.out.println(keyFactoryInterface1 == keyFactoryInterface2);
System.out.println(keyFactoryInterface1.equals(keyFactoryInterface2));
Object key1 = keyFactoryInterface1.newInstance(1, "doge");
Object key2 = keyFactoryInterface1.newInstance(1, "doge");
System.out.println(key1.equals(key2));
key2 = keyFactoryInterface1.newInstance(1, "doge10086");
System.out.println(key1.equals(key2));
}

interface KeyFactoryInterface {

Object newInstance(Integer a, String b);
}
}

输出结果:

1
2
3
4
false
true
true
false

Mixin

Mixin能够让我们将多个接口的多个实现合并到同一个接口的单个实现。

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
36
37
38
39
40
public class MixinDemo {

interface InterfaceFirst {

String first();
}

interface InterfaceSecond {

String second();
}

static class ImplFirst implements InterfaceFirst {

@Override
public String first() {
return "I am first";
}
}

static class ImplSecond implements InterfaceSecond {

@Override
public String second() {
return "I am second";
}
}

interface MixinImpl extends InterfaceFirst, InterfaceSecond {

}

public static void main(String[] args) throws Exception {
Mixin mixin = Mixin.create(new Class[]{InterfaceFirst.class, InterfaceSecond.class, MixinImpl.class},
new Object[]{new ImplFirst(), new ImplSecond()});
MixinImpl mixinImpl = (MixinImpl) mixin;
System.out.println(mixinImpl.first());
System.out.println(mixinImpl.second());
}
}

输出结果:

1
2
I am first
I am second

StringSwitcher

用来模拟一个String到int类型的Map类型。如果在Java7以后的版本中,类似一个switch块的逻辑。

1
2
3
4
5
6
7
8
public class StringSwitcherDemo {

public static void main(String[] args) throws Exception {
StringSwitcher stringSwitcher = StringSwitcher.create(new String[]{"one", "two"}, new int[]{1, 2}, true);
System.out.println(stringSwitcher.intValue("one"));
System.out.println(stringSwitcher.intValue("two"));
}
}

输出结果:

1
2
1
2

InterfaceMaker

接口生成器,底层依赖ASM的相关API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InterfaceMakerDemo {

public static void main(String[] args) throws Exception {
Signature signature = new Signature("foo", Type.DOUBLE_TYPE, new Type[]{Type.INT_TYPE});
InterfaceMaker interfaceMaker = new InterfaceMaker();
interfaceMaker.add(signature, new Type[0]);
Class<?> clazz = interfaceMaker.create();
Method[] methods = clazz.getMethods();
System.out.println(methods.length);
Method foo = methods[0];
System.out.println(foo.getReturnType());
System.out.println(Arrays.toString(foo.getParameterTypes()));
}
}

输出结果:

1
2
3
1
double
[int]

上述的InterfaceMaker创建的接口中只含有一个方法,签名为double foo(int)。InterfaceMaker与上面介绍的其他类不同,它依赖ASM中的Type类型。由于接口仅仅只用做在编译时期进行类型检查,因此在一个运行的应用中动态的创建接口没有什么作用。但是InterfaceMaker可以用来自动生成接口代码,为以后的开发做准备。

MethodDelegate

方法代理,个人认为作用不太大,这里仅举例。

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
public class MethodDelegateDemo {

interface MethodDelegateInterface {

String getValueFromDelegate();
}

static class Delegate {

private String value;

public String getValue() {
return value;
}

public Delegate setValue(String value) {
this.value = value;
return this;
}
}

public static void main(String[] args) throws Exception {
Delegate delegate = new Delegate();
delegate.setValue("throwable");
MethodDelegate methodDelegate = MethodDelegate.create(delegate, "getValue", MethodDelegateInterface.class);
MethodDelegateInterface delegateInterface = (MethodDelegateInterface) methodDelegate;
System.out.println(delegateInterface.getValueFromDelegate());
}
}

输出结果:

1
throwable

MulticastDelegate

多重代理,个人认为作用不太大,这里仅举例。

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
public class MulticastDelegateDemo {

public interface DelegateProvider {

void setValue(String value);
}

static class MulticastBean implements DelegateProvider {

private String value;

@Override
public void setValue(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}

public static void main(String[] args) throws Exception {
MulticastDelegate multicastDelegate = MulticastDelegate.create(DelegateProvider.class);
MulticastBean first = new MulticastBean();
MulticastBean second = new MulticastBean();
multicastDelegate = multicastDelegate.add(first);
multicastDelegate = multicastDelegate.add(second);
DelegateProvider provider = (DelegateProvider) multicastDelegate;
provider.setValue("throwable");
System.out.println(first.getValue());
System.out.println(second.getValue());
}
}

输出结果:

1
2
throwable
throwable

ConstructorDelegate

构造器代理,个人认为作用不太大,这里仅举例。

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
public class ConstructorDelegateDemo {

public interface ConstructorInterface {

Object newInstance(String value);
}

static class ConstructorImpl {

private String value;

public ConstructorImpl(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public ConstructorImpl setValue(String value) {
this.value = value;
return this;
}
}

public static void main(String[] args) throws Exception {
ConstructorInterface constructorInterface =
(ConstructorInterface) ConstructorDelegate.create(ConstructorImpl.class, ConstructorInterface.class);
ConstructorImpl constructorImpl = (ConstructorImpl) constructorInterface.newInstance("throwable");
System.out.println(ConstructorImpl.class.isAssignableFrom(constructorImpl.getClass()));
System.out.println(constructorImpl.getValue());
}
}

输出结果:

1
2
true
throwable

ParallelSorter

并行排序器,能够对多个数组同时进行排序,目前实现的算法有归并排序(mergeSort)和快速排序(quickSort),查看源码的时候发现Float和Double类的比较直接用大于或者小于,有可能造成这两个类型的数据排序不准确(应该使用Float或Double的compare方法进行比较)。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ParallelSorterDemo {

public static void main(String[] args) throws Exception {
Integer[][] array = new Integer[][]{
{4, 3, 9, 0},
{2, 1, 6, 0}
};
ParallelSorter.create(array).quickSort(0);
for (Integer[] row : array) {
System.out.println(Arrays.toString(row));
}
}
}

输出结果:

1
2
[0, 3, 4, 9]
[0, 1, 2, 6]

FastClass

FastClass就是对Class对象进行特定的处理,认知上可以理解为索引类,比如通过数组保存method引用,因此FastClass引出了一个index下标的新概念,比如getIndex(String name, Class[] parameterTypes)就是以前的获取method的方法。通过数组存储method,constructor等class信息,从而将原先的反射调用,转化为class.index的直接调用以提高效率,从而体现所谓的FastClass。

1
2
3
4
5
6
7
8
9
10
public class FastClassDemo {

public static void main(String[] args) throws Exception {
FastClass fastClass = FastClass.create(SampleClass.class);
FastMethod fastMethod = fastClass.getMethod("sayHello", new Class[]{String.class});
SampleClass sampleClass = new SampleClass();
System.out.println(fastMethod.invoke(sampleClass, new Object[]{"throwable"}));
System.out.println(fastMethod.getIndex());
}
}

输出结果:

1
2
throwable say hello!
0

实际上,在接口或者代理类的方法比较少的时候,使用FastClass进行方法调用有可能比原生反射方法调用Method#invoke()高,但是实际还是需要进行测试和分析,不能盲目一概而论。

小结

本文简单分析了一下CGLIB中的常用API,其实在实现AOP、动态代理和反射调用的时候,最常用的是字节码增强器Enhancer、回调(Callback)以及快类(FastClass),掌握它们的使用方式有利于进行AOP编程以及反射性能创新性提升。

(本文完 r-a-20181216 c-1-d)

JVM应用度量框架Micrometer实战

前提

最近线上的项目使用了spring-actuator做度量统计收集,使用Prometheus进行数据收集,Grafana进行数据展示,用于监控生成环境机器的性能指标和业务数据指标。一般,我们叫这样的操作为"埋点"。SpringBoot中的依赖spring-actuator中集成的度量统计API使用的框架是Micrometer,官网是Micrometer.io。在实践中发现了业务开发者滥用了Micrometer的度量类型Counter,导致无论什么情况下都只使用计数统计的功能。这篇文章就是基于Micrometer分析其他的度量类型API的作用和适用场景。

Micrometer提供的度量类库

Meter是指一组用于收集应用中的度量数据的接口,Meter单词可以翻译为"米"或者"千分尺",但是显然听起来都不是很合理,因此下文直接叫Meter,理解它为度量接口即可。Meter是由MeterRegistry创建和保存的,可以理解MeterRegistryMeter的工厂和缓存中心,一般而言每个JVM应用在使用Micrometer的时候必须创建一个MeterRegistry的具体实现。Micrometer中,Meter的具体类型包括:TimerCounterGaugeDistributionSummaryLongTaskTimerFunctionCounterFunctionTimerTimeGauge。下面分节详细介绍这些类型的使用方法和实战使用场景。而一个Meter具体类型需要通过名字和Tag(这里指的是Micrometer提供的Tag接口)作为它的唯一标识,这样做的好处是可以使用名字进行标记,通过不同的Tag去区分多种维度进行数据统计。

MeterRegistry

MeterRegistry在Micrometer是一个抽象类,主要实现包括:

  • 1、SimpleMeterRegistry:每个Meter的最新数据可以收集到SimpleMeterRegistry实例中,但是这些数据不会发布到其他系统,也就是数据是位于应用的内存中的。
  • 2、CompositeMeterRegistry:多个MeterRegistry聚合,内部维护了一个MeterRegistry的列表。
  • 3、全局的MeterRegistry:工厂类io.micrometer.core.instrument.Metrics中持有一个静态final的CompositeMeterRegistry实例globalRegistry。

当然,使用者也可以自行继承MeterRegistry去实现自定义的MeterRegistry。SimpleMeterRegistry适合做调试的时候使用,它的简单使用方式如下:

1
2
3
MeterRegistry registry = new SimpleMeterRegistry();
Counter counter = registry.counter("counter");
counter.increment();

CompositeMeterRegistry实例初始化的时候,内部持有的MeterRegistry列表是空的,如果此时用它新增一个Meter实例,Meter实例的操作是无效的:

1
2
3
4
5
6
7
8
9
CompositeMeterRegistry composite = new CompositeMeterRegistry();

Counter compositeCounter = composite.counter("counter");
compositeCounter.increment(); // <- 实际上这一步操作是无效的,但是不会报错

SimpleMeterRegistry simple = new SimpleMeterRegistry();
composite.add(simple); // <- 向CompositeMeterRegistry实例中添加SimpleMeterRegistry实例

compositeCounter.increment(); // <-计数成功

全局的MeterRegistry的使用方式更加简单便捷,因为一切只需要操作工厂类Metrics的静态方法:

1
2
3
Metrics.addRegistry(new SimpleMeterRegistry());
Counter counter = Metrics.counter("counter", "tag-1", "tag-2");
counter.increment();

Tag与Meter的命名

Micrometer中,Meter的命名约定使用英文逗号(dot,也就是".")分隔单词。但是对于不同的监控系统,对命名的规约可能并不相同,如果命名规约不一致,在做监控系统迁移或者切换的时候,可能会对新的系统造成破坏。Micrometer中使用英文逗号分隔单词的命名规则,再通过底层的命名转换接口NamingConvention进行转换,最终可以适配不同的监控系统,同时可以消除监控系统不允许的特殊字符的名称和标记等。开发者也可以覆盖NamingConvention实现自定义的命名转换规则:registry.config().namingConvention(myCustomNamingConvention);。在Micrometer中,对一些主流的监控系统或者存储系统的命名规则提供了默认的转换方式,例如当我们使用下面的命名时候:

1
2
MeterRegistry registry = ...
registry.timer("http.server.requests");

对于不同的监控系统或者存储系统,命名会自动转换如下:

  • 1、Prometheus - http_server_requests_duration_seconds。
  • 2、Atlas - httpServerRequests。
  • 3、Graphite - http.server.requests。
  • 4、InfluxDB - http_server_requests。

其实NamingConvention已经提供了5种默认的转换规则:dot、snakeCase、camelCase、upperCamelCase和slashes。

另外,Tag(标签)是Micrometer的一个重要的功能,严格来说,一个度量框架只有实现了标签的功能,才能真正地多维度进行度量数据收集。Tag的命名一般需要是有意义的,所谓有意义就是可以根据Tag的命名可以推断出它指向的数据到底代表什么维度或者什么类型的度量指标。假设我们需要监控数据库的调用和Http请求调用统计,一般推荐的做法是:

1
2
3
MeterRegistry registry = ...
registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")

这样,当我们选择命名为"database.calls"的计数器,我们可以进一步选择分组"db"或者"users"分别统计不同分组对总调用数的贡献或者组成。一个反例如下:

1
2
3
4
5
6
7
8
MeterRegistry registry = ...
registry.counter("calls",
"class", "database",
"db", "users");

registry.counter("calls",
"class", "http",
"uri", "/api/users");

通过命名"calls"得到的计数器,由于标签混乱,数据是基本无法分组统计分析,这个时候可以认为得到的时间序列的统计数据是没有意义的。可以定义全局的Tag,也就是全局的Tag定义之后,会附加到所有的使用到的Meter上(只要是使用同一个MeterRegistry),全局的Tag可以这样定义:

1
2
3
4
MeterRegistry registry = ...
registry.config().commonTags("stack", "prod", "region", "us-east-1");
// 和上面的意义是一样的
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));

像上面这样子使用,就能通过主机,实例,区域,堆栈等操作环境进行多维度深入分析。

还有两点点需要注意:

  • 1、Tag的值必须不为null。
  • 2、Micrometer中,Tag必须成对出现,也就是Tag必须设置为偶数个,实际上它们以Key=Value的形式存在,具体可以看io.micrometer.core.instrument.Tag接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Tag extends Comparable<Tag> {
String getKey();

String getValue();

static Tag of(String key, String value) {
return new ImmutableTag(key, value);
}

default int compareTo(Tag o) {
return this.getKey().compareTo(o.getKey());
}
}

当然,有些时候,我们需要过滤一些必要的标签或者名称进行统计,或者为Meter的名称添加白名单,这个时候可以使用MeterFilterMeterFilter本身提供一些列的静态方法,多个MeterFilter可以叠加或者组成链实现用户最终的过滤策略。例如:

1
2
3
4
MeterRegistry registry = ...
registry.config()
.meterFilter(MeterFilter.ignoreTags("http"))
.meterFilter(MeterFilter.denyNameStartsWith("jvm"));

表示忽略"http"标签,拒绝名称以"jvm"字符串开头的Meter。更多用法可以参详一下MeterFilter这个类。

Meter的命名和Meter的Tag相互结合,以命名为轴心,以Tag为多维度要素,可以使度量数据的维度更加丰富,便于统计和分析。

Meters

前面提到Meter主要包括:TimerCounterGaugeDistributionSummaryLongTaskTimerFunctionCounterFunctionTimerTimeGauge。下面逐一分析它们的作用和个人理解的实际使用场景(应该说是生产环境)。

Counter

Counter是一种比较简单的Meter,它是一种单值的度量类型,或者说是一个单值计数器。Counter接口允许使用者使用一个固定值(必须为正数)进行计数。准确来说:Counter就是一个增量为正数的单值计数器。这个举个很简单的使用例子:

1
2
3
4
MeterRegistry meterRegistry = new SimpleMeterRegistry();
Counter counter = meterRegistry.counter("http.request", "createOrder", "/order/create");
counter.increment();
System.out.println(counter.measure()); // [Measurement{statistic='COUNT', value=1.0}]

使用场景:

Counter的作用是记录XXX的总量或者计数值,适用于一些增长类型的统计,例如下单、支付次数、Http请求总量记录等等,通过Tag可以区分不同的场景,对于下单,可以使用不同的Tag标记不同的业务来源或者是按日期划分,对于Http请求总量记录,可以使用Tag区分不同的URL。用下单业务举个例子:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//实体
@Data
public class Order {

private String orderId;
private Integer amount;
private String channel;
private LocalDateTime createTime;
}


public class CounterMain {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

static {
Metrics.addRegistry(new SimpleMeterRegistry());
}

public static void main(String[] args) throws Exception {
Order order1 = new Order();
order1.setOrderId("ORDER_ID_1");
order1.setAmount(100);
order1.setChannel("CHANNEL_A");
order1.setCreateTime(LocalDateTime.now());
createOrder(order1);
Order order2 = new Order();
order2.setOrderId("ORDER_ID_2");
order2.setAmount(200);
order2.setChannel("CHANNEL_B");
order2.setCreateTime(LocalDateTime.now());
createOrder(order2);
Search.in(Metrics.globalRegistry).meters().forEach(each -> {
StringBuilder builder = new StringBuilder();
builder.append("name:")
.append(each.getId().getName())
.append(",tags:")
.append(each.getId().getTags())
.append(",type:").append(each.getId().getType())
.append(",value:").append(each.measure());
System.out.println(builder.toString());
});
}

private static void createOrder(Order order) {
//忽略订单入库等操作
Metrics.counter("order.create",
"channel", order.getChannel(),
"createTime", FORMATTER.format(order.getCreateTime())).increment();
}
}

控制台输出:

1
2
name:order.create,tags:[tag(channel=CHANNEL_A), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]
name:order.create,tags:[tag(channel=CHANNEL_B), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]

上面的例子是使用全局静态方法工厂类Metrics去构造Counter实例,实际上,io.micrometer.core.instrument.Counter接口提供了一个内部建造器类Counter.Builder去实例化Counter,Counter.Builder的使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
public class CounterBuilderMain {

public static void main(String[] args) throws Exception{
Counter counter = Counter.builder("name") //名称
.baseUnit("unit") //基础单位
.description("desc") //描述
.tag("tagKey", "tagValue") //标签
.register(new SimpleMeterRegistry());//绑定的MeterRegistry
counter.increment();
}
}

FunctionCounter

FunctionCounterCounter的特化类型,它把计数器数值增加的动作抽象成接口类型ToDoubleFunction,这个接口JDK1.8中对于Function的特化类型接口。FunctionCounter的使用场景和Counter是一致的,这里介绍一下它的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FunctionCounterMain {

public static void main(String[] args) throws Exception {
MeterRegistry registry = new SimpleMeterRegistry();
AtomicInteger n = new AtomicInteger(0);
//这里ToDoubleFunction匿名实现其实可以使用Lambda表达式简化为AtomicInteger::get
FunctionCounter.builder("functionCounter", n, new ToDoubleFunction<AtomicInteger>() {
@Override
public double applyAsDouble(AtomicInteger value) {
return value.get();
}
}).baseUnit("function")
.description("functionCounter")
.tag("createOrder", "CHANNEL-A")
.register(registry);
//下面模拟三次计数
n.incrementAndGet();
n.incrementAndGet();
n.incrementAndGet();
}
}

FunctionCounter使用的一个明显的好处是,我们不需要感知FunctionCounter实例的存在,实际上我们只需要操作作为FunctionCounter实例构建元素之一的AtomicInteger实例即可,这种接口的设计方式在很多框架里面都可以看到。

Timer

Timer(计时器)适用于记录耗时比较短的事件的执行时间,通过时间分布展示事件的序列和发生频率。所有的Timer的实现至少记录了发生的事件的数量和这些事件的总耗时,从而生成一个时间序列。Timer的基本单位基于服务端的指标而定,但是实际上我们不需要过于关注Timer的基本单位,因为Micrometer在存储生成的时间序列的时候会自动选择适当的基本单位。Timer接口提供的常用方法如下:

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
36
37
public interface Timer extends Meter {
...
void record(long var1, TimeUnit var3);

default void record(Duration duration) {
this.record(duration.toNanos(), TimeUnit.NANOSECONDS);
}

<T> T record(Supplier<T> var1);

<T> T recordCallable(Callable<T> var1) throws Exception;

void record(Runnable var1);

default Runnable wrap(Runnable f) {
return () -> {
this.record(f);
};
}

default <T> Callable<T> wrap(Callable<T> f) {
return () -> {
return this.recordCallable(f);
};
}

long count();

double totalTime(TimeUnit var1);

default double mean(TimeUnit unit) {
return this.count() == 0L ? 0.0D : this.totalTime(unit) / (double)this.count();
}

double max(TimeUnit var1);
...
}

实际上,比较常用和方便的方法是几个函数式接口入参的方法:

1
2
3
4
5
6
Timer timer = ...
timer.record(() -> dontCareAboutReturnValue());
timer.recordCallable(() -> returnValue());

Runnable r = timer.wrap(() -> dontCareAboutReturnValue());
Callable c = timer.wrap(() -> returnValue());

使用场景:

根据个人经验和实践,总结如下:

  • 1、记录指定方法的执行时间用于展示。
  • 2、记录一些任务的执行时间,从而确定某些数据来源的速率,例如消息队列消息的消费速率等。

这里举个实际的例子,要对系统做一个功能,记录指定方法的执行时间,还是用下单方法做例子:

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
public class TimerMain {

private static final Random R = new Random();

static {
Metrics.addRegistry(new SimpleMeterRegistry());
}

public static void main(String[] args) throws Exception {
Order order1 = new Order();
order1.setOrderId("ORDER_ID_1");
order1.setAmount(100);
order1.setChannel("CHANNEL_A");
order1.setCreateTime(LocalDateTime.now());
Timer timer = Metrics.timer("timer", "createOrder", "cost");
timer.record(() -> createOrder(order1));
}

private static void createOrder(Order order) {
try {
TimeUnit.SECONDS.sleep(R.nextInt(5)); //模拟方法耗时
} catch (InterruptedException e) {
//no-op
}
}
}

在实际生产环境中,可以通过spring-aop把记录方法耗时的逻辑抽象到一个切面中,这样就能减少不必要的冗余的模板代码。上面的例子是通过Mertics构造Timer实例,实际上也可以使用Builder构造:

1
2
3
4
5
6
MeterRegistry registry = ...
Timer timer = Timer
.builder("my.timer")
.description("a description of what this timer does") // 可选
.tags("region", "test") // 可选
.register(registry);

另外,Timer的使用还可以基于它的内部类Timer.Sample,通过start和stop两个方法记录两者之间的逻辑的执行耗时。例如:

1
2
3
4
5
6
Timer.Sample sample = Timer.start(registry);

// 这里做业务逻辑
Response response = ...

sample.stop(registry.timer("my.timer", "response", response.status()));

FunctionTimer

FunctionTimer是Timer的特化类型,它主要提供两个单调递增的函数(其实并不是单调递增,只是在使用中一般需要随着时间最少保持不变或者说不减少):一个用于计数的函数和一个用于记录总调用耗时的函数,它的建造器的入参如下:

1
2
3
4
5
6
7
8
public interface FunctionTimer extends Meter {
static <T> Builder<T> builder(String name, T obj, ToLongFunction<T> countFunction,
ToDoubleFunction<T> totalTimeFunction,
TimeUnit totalTimeFunctionUnit) {
return new Builder<>(name, obj, countFunction, totalTimeFunction, totalTimeFunctionUnit);
}
...
}

官方文档中的例子如下:

1
2
3
4
5
6
IMap<?, ?> cache = ...; // 假设使用了Hazelcast缓存
registry.more().timer("cache.gets.latency", Tags.of("name", cache.getName()), cache,
c -> c.getLocalMapStats().getGetOperationCount(), //实际上就是cache的一个方法,记录缓存生命周期初始化的增量(个数)
c -> c.getLocalMapStats().getTotalGetLatency(), // Get操作的延迟时间总量,可以理解为耗时
TimeUnit.NANOSECONDS
);

按照个人理解,ToDoubleFunction用于统计事件个数,ToDoubleFunction用于记录执行总时间,实际上两个函数都只是Function函数的变体,还有一个比较重要的是总时间的单位totalTimeFunctionUnit。简单的使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FunctionTimerMain {

public static void main(String[] args) throws Exception {
//这个是为了满足参数,暂时不需要理会
Object holder = new Object();
AtomicLong totalTimeNanos = new AtomicLong(0);
AtomicLong totalCount = new AtomicLong(0);
FunctionTimer.builder("functionTimer", holder, p -> totalCount.get(),
p -> totalTimeNanos.get(), TimeUnit.NANOSECONDS)
.register(new SimpleMeterRegistry());
totalTimeNanos.addAndGet(10000000);
totalCount.incrementAndGet();
}
}

LongTaskTimer

LongTaskTimer也是一种Timer的特化类型,主要用于记录长时间执行的任务的持续时间,在任务完成之前,被监测的事件或者任务仍然处于运行状态,任务完成的时候,任务执行的总耗时才会被记录下来。LongTaskTimer适合用于长时间持续运行的事件耗时的记录,例如相对耗时的定时任务。在Spring应用中,可以简单地使用@Scheduled和@Timed注解,基于spring-aop完成定时调度任务的总耗时记录:

1
2
3
4
5
@Timed(value = "aws.scrape", longTask = true)
@Scheduled(fixedDelay = 360000)
void scrapeResources() {
//这里做相对耗时的业务逻辑
}

当然,在非spring体系中也能方便地使用LongTaskTimer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LongTaskTimerMain {

public static void main(String[] args) throws Exception{
MeterRegistry meterRegistry = new SimpleMeterRegistry();
LongTaskTimer longTaskTimer = meterRegistry.more().longTaskTimer("longTaskTimer");
longTaskTimer.record(() -> {

//这里编写Task的逻辑
});
//或者这样
Metrics.more().longTaskTimer("longTaskTimer").record(()-> {
//这里编写Task的逻辑
});
}
}

Gauge

Gauge(仪表)是获取当前度量记录值的句柄,也就是它表示一个可以任意上下浮动的单数值度量Meter。Gauge通常用于变动的测量值,测量值用ToDoubleFunction参数的返回值设置,如当前的内存使用情况,同时也可以测量上下移动的"计数",比如队列中的消息数量。官网文档中提到Gauge的典型使用场景是用于测量集合或映射的大小或运行状态中的线程数。Gauge一般用于监测有自然上界的事件或者任务,而Counter一般使用于无自然上界的事件或者任务的监测,所以像Http请求总量计数应该使用Counter而非Gauge。MeterRegistry中提供了一些便于构建用于观察数值、函数、集合和映射的Gauge相关的方法:

1
2
3
List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size); 
List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>());
Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>());

上面的三个方法通过MeterRegistry构建Gauge并且返回了集合或者映射实例,使用这些集合或者映射实例就能在其size变化过程中记录这个变更值。更重要的优点是,我们不需要感知Gauge接口的存在,只需要像平时一样使用集合或者映射实例就可以了。此外,Gauge还支持java.lang.Number的子类,java.util.concurrent.atomic包中的AtomicIntegerAtomicLong,还有Guava提供的AtomicDouble

1
2
3
AtomicInteger n = registry.gauge("numberGauge", new AtomicInteger(0));
n.set(1);
n.set(2);

除了使用MeterRegistry创建Gauge之外,还可以使用建造器流式创建:

1
2
3
4
5
6
//一般我们不需要操作Gauge实例
Gauge gauge = Gauge
.builder("gauge", myObj, myObj::gaugeValue)
.description("a description of what this gauge does") // 可选
.tags("region", "test") // 可选
.register(registry);

使用场景:

根据个人经验和实践,总结如下:

  • 1、有自然(物理)上界的浮动值的监测,例如物理内存、集合、映射、数值等。
  • 2、有逻辑上界的浮动值的监测,例如积压的消息、(线程池中)积压的任务等,其实本质也是集合或者映射的监测。

举个相对实际的例子,假设我们需要对登录后的用户发送一条短信或者推送,做法是消息先投放到一个阻塞队列,再由一个线程消费消息进行其他操作:

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
public class GaugeMain {

private static final MeterRegistry MR = new SimpleMeterRegistry();
private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);
private static BlockingQueue<Message> REAL_QUEUE;

static {
REAL_QUEUE = MR.gauge("messageGauge", QUEUE, Collection::size);
}

public static void main(String[] args) throws Exception {
consume();
Message message = new Message();
message.setUserId(1L);
message.setContent("content");
REAL_QUEUE.put(message);
}

private static void consume() throws Exception {
new Thread(() -> {
while (true) {
try {
Message message = REAL_QUEUE.take();
//handle message
System.out.println(message);
} catch (InterruptedException e) {
//no-op
}
}
}).start();
}
}

上面的例子代码写得比较糟糕,只为了演示相关使用方式,切勿用于生产环境。

TimeGauge

TimeGauge是Gauge的特化类型,相比Gauge,它的构建器中多了一个TimeUnit类型的参数,用于指定ToDoubleFunction入参的基础时间单位。这里简单举个使用例子:

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
public class TimeGaugeMain {

private static final SimpleMeterRegistry R = new SimpleMeterRegistry();

public static void main(String[] args) throws Exception{
AtomicInteger count = new AtomicInteger();
TimeGauge.Builder<AtomicInteger> timeGauge = TimeGauge.builder("timeGauge", count,
TimeUnit.SECONDS, AtomicInteger::get);
timeGauge.register(R);
count.addAndGet(10086);
print();
count.set(1);
print();
}

private static void print()throws Exception{
Search.in(R).meters().forEach(each -> {
StringBuilder builder = new StringBuilder();
builder.append("name:")
.append(each.getId().getName())
.append(",tags:")
.append(each.getId().getTags())
.append(",type:").append(each.getId().getType())
.append(",value:").append(each.measure());
System.out.println(builder.toString());
});
}
}

//输出
name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=10086.0}]
name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=1.0}]

DistributionSummary

Summary(摘要)主要用于跟踪事件的分布,在Micrometer中,对应的类是DistributionSummary(分发摘要)。它的使用方式和Timer十分相似,但是它的记录值并不依赖于时间单位。常见的使用场景:使用DistributionSummary测量命中服务器的请求的有效负载大小。使用MeterRegistry创建DistributionSummary实例如下:

1
DistributionSummary summary = registry.summary("response.size");

通过建造器流式创建如下:

1
2
3
4
5
6
7
DistributionSummary summary = DistributionSummary
.builder("response.size")
.description("a description of what this summary does") // 可选
.baseUnit("bytes") // 可选
.tags("region", "test") // 可选
.scale(100) // 可选
.register(registry);

DistributionSummary中有很多构建参数跟缩放和直方图的表示相关,见下一节。

使用场景:

根据个人经验和实践,总结如下:

  • 1、不依赖于时间单位的记录值的测量,例如服务器有效负载值,缓存的命中率等。

举个相对具体的例子:

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
public class DistributionSummaryMain {

private static final DistributionSummary DS = DistributionSummary.builder("cacheHitPercent")
.register(new SimpleMeterRegistry());

private static final LoadingCache<String, String> CACHE = CacheBuilder.newBuilder()
.maximumSize(1000)
.recordStats()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
return selectFromDatabase();
}
});

public static void main(String[] args) throws Exception{
String key = "doge";
String value = CACHE.get(key);
record();
}

private static void record()throws Exception{
CacheStats stats = CACHE.stats();
BigDecimal hitCount = new BigDecimal(stats.hitCount());
BigDecimal requestCount = new BigDecimal(stats.requestCount());
DS.record(hitCount.divide(requestCount,2,BigDecimal.ROUND_HALF_DOWN).doubleValue());
}
}

直方图和百分数配置

直方图和百分数配置适用于Summary和Timer,这部分相对复杂,等研究透了再补充。

基于SpirngBoot、Prometheus、Grafana集成

集成了Micrometer框架的JVM应用使用到Micrometer的API收集的度量数据位于内存之中,因此,需要额外的存储系统去存储这些度量数据,需要有监控系统负责统一收集和处理这些数据,还需要有一些UI工具去展示数据,一般大佬只喜欢看炫酷的图表或者动画。常见的存储系统就是时序数据库,主流的有Influx、Datadog等。比较主流的监控系统(主要是用于数据收集和处理)就是Prometheus(一般叫普罗米修斯,下面就这样叫吧)。而展示的UI目前相对用得比较多的就是Grafana。另外,Prometheus已经内置了一个时序数据库的实现,因此,在做一套相对完善的度量数据监控的系统只需要依赖目标JVM应用,Prometheus组件和Grafana组件即可。下面花一点时间从零开始搭建一个这样的系统,之前写的一篇文章基于Windows系统,操作可能跟生产环境不够接近,这次使用CentOS7。

SpirngBoot中使用Micrometer

SpringBoot中的spring-boot-starter-actuator依赖已经集成了对Micrometer的支持,其中的metrics端点的很多功能就是通过Micrometer实现的,prometheus端点默认也是开启支持的,实际上actuator依赖的spring-boot-actuator-autoconfigure中集成了对很多框架的开箱即用的API,其中prometheus包中集成了对Prometheus的支持,使得使用了actuator可以轻易地让项目暴露出prometheus端点,作为Prometheus收集数据的客户端,Prometheus(服务端软件)可以通过此端点收集应用中Micrometer的度量数据。

jvm-m-1.png

我们先引入spring-boot-starter-actuatorspring-boot-starter-web,实现一个CounterTimer作为示例。依赖:

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
 <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>

接着编写一个下单接口和一个消息发送模块,模拟用户下单之后向用户发送消息:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//实体
@Data
public class Message {

private String orderId;
private Long userId;
private String content;
}

@Data
public class Order {

private String orderId;
private Long userId;
private Integer amount;
private LocalDateTime createTime;
}

//控制器和服务类
@RestController
public class OrderController {

@Autowired
private OrderService orderService;

@PostMapping(value = "/order")
public ResponseEntity<Boolean> createOrder(@RequestBody Order order){
return ResponseEntity.ok(orderService.createOrder(order));
}
}

@Slf4j
@Service
public class OrderService {

private static final Random R = new Random();

@Autowired
private MessageService messageService;

public Boolean createOrder(Order order) {
//模拟下单
try {
int ms = R.nextInt(50) + 50;
TimeUnit.MILLISECONDS.sleep(ms);
log.info("保存订单模拟耗时{}毫秒...", ms);
} catch (Exception e) {
//no-op
}
//记录下单总数
Metrics.counter("order.count", "order.channel", order.getChannel()).increment();
//发送消息
Message message = new Message();
message.setContent("模拟短信...");
message.setOrderId(order.getOrderId());
message.setUserId(order.getUserId());
messageService.sendMessage(message);
return true;
}
}

@Slf4j
@Service
public class MessageService implements InitializingBean {

private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);
private static BlockingQueue<Message> REAL_QUEUE;
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
private static final Random R = new Random();

static {
REAL_QUEUE = Metrics.gauge("message.gauge", Tags.of("message.gauge", "message.queue.size"), QUEUE, Collection::size);
}

public void sendMessage(Message message) {
try {
REAL_QUEUE.put(message);
} catch (InterruptedException e) {
//no-op
}
}

@Override
public void afterPropertiesSet() throws Exception {
EXECUTOR.execute(() -> {
while (true) {
try {
Message message = REAL_QUEUE.take();
log.info("模拟发送短信,orderId:{},userId:{},内容:{},耗时:{}毫秒", message.getOrderId(), message.getUserId(),
message.getContent(), R.nextInt(50));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
});
}
}

//切面类
@Component
@Aspect
public class TimerAspect {

@Around(value = "execution(* club.throwable.smp.service.*Service.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
Timer timer = Metrics.timer("method.cost.time", "method.name", method.getName());
ThrowableHolder holder = new ThrowableHolder();
Object result = timer.recordCallable(() -> {
try {
return joinPoint.proceed();
} catch (Throwable e) {
holder.throwable = e;
}
return null;
});
if (null != holder.throwable) {
throw holder.throwable;
}
return result;
}

private class ThrowableHolder {

Throwable throwable;
}
}

yaml的配置如下:

1
2
3
4
5
6
7
8
9
10
server:
port: 9091
management:
server:
port: 10091
endpoints:
web:
exposure:
include: '*'
base-path: /management

注意多看spring官方文档关于Actuator的详细描述,在SpringBoot-2.x之后,配置Web端点暴露的权限控制和1.x有很大的不同。总结一下就是:除了shutdown端点之外,其他端点默认都是开启支持的(这里仅仅是开启支持,并不是暴露为Web端点,端点必须暴露为Web端点才能被访问),禁用或者开启端点支持的配置方式如下:

1
management.endpoint.${端点ID}.enabled=true/false

可以查看actuator-api文档查看所有支持的端点的特性,这个是2.1.0.RELEASE版本的官方文档,不知道日后链接会不会挂掉。端点只开启支持,但是不暴露为Web端点,是无法通过http://{host}:{management.port}/{management.endpoints.web.base-path}/{endpointId}访问的。暴露监控端点为Web端点的配置是:

1
2
management.endpoints.web.exposure.include=info,health
management.endpoints.web.exposure.exclude=prometheus

management.endpoints.web.exposure.include用于指定暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔。management.endpoints.web.exposure.exclude用于指定不暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔。
management.endpoints.web.exposure.include默认指定的只有info和health两个端点,我们可以直接指定暴露所有的端点:management.endpoints.web.exposure.include=*,如果采用YAML配置,记得*要加单引号’*’。暴露所有Web监控端点是一件比较危险的事情,如果需要在生产环境这样做,请务必先确认http://{host}:{management.port}不能通过公网访问(也就是监控端点访问的端口只能通过内网访问,这样可以方便后面说到的Prometheus服务端通过此端口收集数据)。

Prometheus的安装和配置

Prometheus目前的最新版本是2.5,鉴于笔者没深入玩过Docker,这里还是直接下载它的压缩包解压安装。

1
2
3
wget https://github.com/prometheus/prometheus/releases/download/v2.5.0/prometheus-2.5.0.linux-amd64.tar.gz
tar xvfz prometheus-*.tar.gz
cd prometheus-*

先编辑解压出来的目录下的prometheus配置文件prometheus.yml,主要修改scrape_configs节点的属性:

1
2
3
4
5
6
7
8
9
10
11
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'

# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
# 这里配置需要拉取度量信息的URL路径,这里选择应用程序的prometheus端点
metrics_path: /management/prometheus
static_configs:
# 这里配置host和port
- targets: ['localhost:10091']

配置拉取度量数据的路径为localhost:10091/management/metrics,此前记得把前一节提到的应用在虚拟机中启动。接着启动Prometheus应用:

1
2
# 可选参数 --storage.tsdb.path=存储数据的路径,默认路径为./data
./prometheus --config.file=prometheus.yml

Prometheus引用的默认启动端口是9090,启动成功后,日志如下:

jvm-m-2.png

此时,访问http://${虚拟机host}:9090/targets就能看到当前Prometheus中执行的Job:

jvm-m-3.png

访问http://${虚拟机host}:9090/graph可以查找到我们定义的度量Meter和spring-boot-starter-actuator中已经定义好的一些关于JVM或者Tomcat的度量Meter。我们先对应用的/order接口进行调用,然后查看一下监控前面在应用中定义的order_count_totalmethod_cost_time_seconds_sum

jvm-m-4.png

jvm-m-5.png

可以看到,Meter的信息已经被收集和展示,但是显然不够详细和炫酷,这个时候就需要使用Grafana的UI做一下点缀。

Grafana的安装和使用

Grafana的安装过程如下:

1
2
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.3.4-1.x86_64.rpm 
sudo yum localinstall grafana-5.3.4-1.x86_64.rpm

安装完成后,通过命令service grafana-server start启动即可,默认的启动端口为3000,通过http://${host}:3000访问即可。初始的账号密码都为admin,权限是管理员权限。接着需要在Home面板添加一个数据源,目的是对接Prometheus服务端从而可以拉取它里面的度量数据。数据源添加面板如下:

jvm-m-6.png

其实就是指向Prometheus服务端的端口就可以了。接下来可以天马行空地添加需要的面板,就下单数量统计的指标,可以添加一个Graph的面板:

jvm-m-7.png

配置面板的时候,需要在基础(General)中指定Title:

jvm-m-9.png

接着比较重要的是Metrics的配置,需要指定数据源和Prometheus的查询语句:

jvm-m-8.png

最好参考一下Prometheus的官方文档,稍微学习一下它的查询语言PromQL的使用方式,一个面板可以支持多个PromQL查询。前面提到的两项是基本配置,其他配置项一般是图表展示的辅助或者预警等辅助功能,这里先不展开,可以取Grafana的官网挖掘一下使用方式。然后我们再调用一下下单接口,过一段时间,图表的数据就会自动更新和展示:

jvm-m-10.png

接着添加一下项目中使用的Timer的Meter,便于监控方法的执行时间,完成之后大致如下:

jvm-m-11.png

上面的面板虽然设计相当粗糙,但是基本功能已经实现。设计面板并不是一件容易的事,如果有需要可以从Github中搜索一下grafana dashboard关键字找现成的开源配置使用或者二次加工后使用。

小结

常言道:工欲善其事,必先利其器。Micrometer是JVM应用的一款相当优异的度量框架,它提供基于Tag和丰富的度量类型和API便于多维度地进行不同角度度量数据的统计,可以方便地接入Prometheus进行数据收集,使用Grafana的面板进行炫酷的展示,提供了天然的spring-boot体系支持。但是,在实际的业务代码中,度量类型Counter经常被滥用,一旦工具被不加思考地滥用,就反而会成为混乱或者毒瘤。因此,这篇文章就是对Micrometer中的各种Meter的使用场景基于个人的理解做了调研和分析,分享一下实战中的经验和踩坑经历。

以前写过的一篇文章:

参考资料:

(To be continue c-10-d n-e-20181102 最近有点忙,没办法经常更新 r-a-201918)