-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
384 lines (241 loc) · 252 KB
/
atom.xml
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>laywin</title>
<link href="/atom.xml" rel="self"/>
<link href="http://laywin.github.io/"/>
<updated>2024-01-02T11:56:37.942Z</updated>
<id>http://laywin.github.io/</id>
<author>
<name>laywin</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>spring 如何解决循环依赖的问题</title>
<link href="http://laywin.github.io/2024/01/02/spring-%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96%E7%9A%84%E9%97%AE%E9%A2%98/"/>
<id>http://laywin.github.io/2024/01/02/spring-如何解决循环依赖的问题/</id>
<published>2024-01-02T11:52:09.000Z</published>
<updated>2024-01-02T11:56:37.942Z</updated>
<content type="html"><![CDATA[<p>很古老的一个问题,面试可能问的比较多,最近在排查问题的时间专门去跟踪了一下.</p><h3 id="要解答这个问题,需要首先知道spring-bean的创建流程:"><a href="#要解答这个问题,需要首先知道spring-bean的创建流程:" class="headerlink" title="要解答这个问题,需要首先知道spring bean的创建流程:"></a>要解答这个问题,需要首先知道spring bean的创建流程:</h3><ol><li>实例化bean<br>bean new 出来之后会放入singletonFactories中,singletonFactories是遇到循环依赖后,用来创建Bean的</li><li>设置bean的property</li><li>调用bean生命周期hook</li></ol><p>完了之后bean会放入singletonObjects中.</p><h3 id="举一个循环依赖的情况:"><a href="#举一个循环依赖的情况:" class="headerlink" title="举一个循环依赖的情况:"></a>举一个循环依赖的情况:</h3><p>A依赖B, B依赖A</p><p>首先创建Bean A.</p><ol><li>实例化 Bean A</li><li>将Bean A 放入 singletonFactories</li><li>设置Bean A的property, 这时候会去找Bean B, 发现Bean B并没有创建,同样走Bean A实例化的流程</li><li>实例化 Bean B</li><li>将Bean B 放入 singletonFactories</li><li>设置Bean B的property, 这时候会去找Bean A, 下面我们看一下查找过程:</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">getSingleton</span><span class="params">(String beanName, <span class="keyword">boolean</span> allowEarlyReference)</span> </span>{</span><br><span class="line"><span class="comment">// 看 singletonObjects 中有没,singletonObjects是完全初始化好的</span></span><br><span class="line">Object singletonObject = <span class="keyword">this</span>.singletonObjects.get(beanName);</span><br><span class="line"> <span class="comment">// 如果查找的bean正在创建过程中,继续往下走</span></span><br><span class="line"><span class="keyword">if</span> (singletonObject == <span class="keyword">null</span> && isSingletonCurrentlyInCreation(beanName)) {</span><br><span class="line"> <span class="comment">// 看 earlySingletonObjects 中有没,earlySingletonObjects中的数据就是在这个方法后面放的</span></span><br><span class="line">singletonObject = <span class="keyword">this</span>.earlySingletonObjects.get(beanName);</span><br><span class="line"><span class="keyword">if</span> (singletonObject == <span class="keyword">null</span> && allowEarlyReference) {</span><br><span class="line"> <span class="comment">// 同步互斥</span></span><br><span class="line"><span class="keyword">synchronized</span> (<span class="keyword">this</span>.singletonObjects) {</span><br><span class="line"><span class="comment">// Consistent creation of early reference within full singleton lock</span></span><br><span class="line">singletonObject = <span class="keyword">this</span>.singletonObjects.get(beanName);</span><br><span class="line"><span class="keyword">if</span> (singletonObject == <span class="keyword">null</span>) {</span><br><span class="line">singletonObject = <span class="keyword">this</span>.earlySingletonObjects.get(beanName);</span><br><span class="line"><span class="keyword">if</span> (singletonObject == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 注意 singletonFactories 中的数据在 bean new出来之后就会放</span></span><br><span class="line">ObjectFactory<?> singletonFactory = <span class="keyword">this</span>.singletonFactories.get(beanName);</span><br><span class="line"><span class="keyword">if</span> (singletonFactory != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 将bean拿出来</span></span><br><span class="line">singletonObject = singletonFactory.getObject();</span><br><span class="line"> <span class="comment">// 放入 earlySingletonObjects 中</span></span><br><span class="line"><span class="keyword">this</span>.earlySingletonObjects.put(beanName, singletonObject);</span><br><span class="line"> <span class="comment">// 从单例工厂中移除</span></span><br><span class="line"><span class="keyword">this</span>.singletonFactories.remove(beanName);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> singletonObject;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>getSingleton 方法就是解决循环依赖的答案,在我们的例子中就是Bean B 在找Bean A的时候,其实Bean A并不是完全构造的,就是 Bean A的property还是设置完,但并不影响解决循环依赖.</p>]]></content>
<summary type="html">
<p>很古老的一个问题,面试可能问的比较多,最近在排查问题的时间专门去跟踪了一下.</p>
<h3 id="要解答这个问题,需要首先知道spring-bean的创建流程:"><a href="#要解答这个问题,需要首先知道spring-bean的创建流程:" class="hea
</summary>
</entry>
<entry>
<title>spring hook</title>
<link href="http://laywin.github.io/2023/12/24/spring-hook/"/>
<id>http://laywin.github.io/2023/12/24/spring-hook/</id>
<published>2023-12-24T14:53:55.000Z</published>
<updated>2023-12-24T15:06:53.001Z</updated>
<content type="html"><![CDATA[<p>最近在排查一个拦截bean创建问题的时候,重新梳理了一下spring的相关hook. 其作用是当我们需要做一些框架层面的拦截的时候,需要确定将这个拦截加在什么位置。</p><h3 id="SpringBoot-相关事件"><a href="#SpringBoot-相关事件" class="headerlink" title="SpringBoot 相关事件"></a>SpringBoot 相关事件</h3><ul><li><p>ApplicationStartingEvent<br>spring boot 开始启动事件</p></li><li><p>ApplicationEnvironmentPreparedEvent<br>environment 准备事件</p></li><li><p>ApplicationContextInitializedEvent<br>applicationContext 初始化事件</p></li><li><p>ApplicationPreparedEvent<br>applicationContext 准备完毕事件</p></li><li><p>ApplicationStartedEvent<br>spring boot启动完成事件</p></li><li><p>ApplicationReadyEvent<br>spring boot 就绪事件</p><p>在ApplicationPreparedEvent 和 ApplicationStartedEvent之间一个重要的操作就是applicationContext refresh即Bean的加载.</p></li><li><p>ApplicationFailedEvent</p><p>spring boot启动失败事件</p></li><li><p>ContextClosedEvent</p><p>applicationContext 关闭事件</p></li></ul><h3 id="ApplicationContextInitializer"><a href="#ApplicationContextInitializer" class="headerlink" title="ApplicationContextInitializer"></a>ApplicationContextInitializer</h3><ul><li>initialize<br>applicationContext准备完成之前的回调</li></ul><h3 id="ApplicationRunner"><a href="#ApplicationRunner" class="headerlink" title="ApplicationRunner"></a>ApplicationRunner</h3><ul><li>run<br>spring boot 启动完成之后</li></ul><h3 id="CommandLineRunner"><a href="#CommandLineRunner" class="headerlink" title="CommandLineRunner"></a>CommandLineRunner</h3><ul><li>run<br>spring boot 启动完成之后</li></ul><h3 id="EnvironmentPostProcessor"><a href="#EnvironmentPostProcessor" class="headerlink" title="EnvironmentPostProcessor"></a>EnvironmentPostProcessor</h3><p>加载配置文件,例如 spring 的 application.properties 是通过 ConfigFileApplicationListener 来加载的,apollo的配置文件是通过 ApolloApplicationContextInitializer 来加载的</p><h3 id="BeanFactoryPostProcessor"><a href="#BeanFactoryPostProcessor" class="headerlink" title="BeanFactoryPostProcessor"></a>BeanFactoryPostProcessor</h3><ul><li>postProcessBeanFactory</li></ul><p>在ApplicationContext的refresh中调用. 例如:PropertySourcesPlaceholderConfigurer会在这个hook里面解析注入的变量值</p><h3 id="BeanDefinitionRegistryPostProcessor"><a href="#BeanDefinitionRegistryPostProcessor" class="headerlink" title="BeanDefinitionRegistryPostProcessor"></a>BeanDefinitionRegistryPostProcessor</h3><ul><li>postProcessBeanDefinitionRegistry</li></ul><p>在ApplicationContext的refresh中调用. BeanDefinitionRegistryPostProcessor 继承了 BeanFactoryPostprocessor,比如 mybatis 的 ClassPathMapperScanner 就是一个 beanFactoryPostprocessor,用来扫描mapper生成BeanDefinition,放到容器中.</p><h3 id="BeanPostProcessor"><a href="#BeanPostProcessor" class="headerlink" title="BeanPostProcessor"></a>BeanPostProcessor</h3><ul><li>postProcessBeforeInitialization</li><li>postProcessAfterInitialization</li></ul><p>拦截bean的初始化过程</p><h3 id="InitializingBean"><a href="#InitializingBean" class="headerlink" title="InitializingBean"></a>InitializingBean</h3><ul><li>afterPropertiesSet</li></ul><p>bean 初始化回调</p><h3 id="InitMethod"><a href="#InitMethod" class="headerlink" title="InitMethod"></a>InitMethod</h3><ul><li>@PostConstruct</li></ul><p>执行顺序在 afterPropertiesSet后面</p><h3 id="InstantiationAwareBeanPostProcessor"><a href="#InstantiationAwareBeanPostProcessor" class="headerlink" title="InstantiationAwareBeanPostProcessor"></a>InstantiationAwareBeanPostProcessor</h3><ul><li>postProcessBeforeInstantiation</li><li>postProcessAfterInstantiation</li><li>postProcessProperties</li></ul><p>InstantiationAwareBeanPostProcessor 继承了 BeanPostProcessor, 其回调返回的Object如果不是null,则后续通过BeanDefinition创建bean的过程不会执行。在PopulateBean的过程中, 优先级比BeanPostProcessor高(例如: 通过在p roperties设置之前,修改bean的状态来支持field注入), AutowiredAnnotationBeanPostProcessor 就是一个 InstantiationAwareBeanPostProcessor.</p><h3 id="DestructionAwareBeanPostProcessor"><a href="#DestructionAwareBeanPostProcessor" class="headerlink" title="DestructionAwareBeanPostProcessor"></a>DestructionAwareBeanPostProcessor</h3><ul><li>postProcessBeforeDestruction</li></ul><p>在bean销毁之前进行拦截</p><h3 id="PreDestroy"><a href="#PreDestroy" class="headerlink" title="PreDestroy"></a>PreDestroy</h3><ul><li>@PreDestroy</li></ul><p>拦截bean的销毁</p><h3 id="MergedBeanDefinitionPostProcessor"><a href="#MergedBeanDefinitionPostProcessor" class="headerlink" title="MergedBeanDefinitionPostProcessor"></a>MergedBeanDefinitionPostProcessor</h3><ul><li>postProcessMergedBeanDefinition</li></ul><p>InstantiationAwareBeanPostProcessor 继承了 BeanPostProcessor, 能够处理到BeanDefinition</p><h3 id="BeanNameAware"><a href="#BeanNameAware" class="headerlink" title="BeanNameAware"></a>BeanNameAware</h3><ul><li>setBeanName</li></ul><p>初始化bean的阶段(已设置properties)调用</p><h3 id="BeanClassLoaderAware"><a href="#BeanClassLoaderAware" class="headerlink" title="BeanClassLoaderAware"></a>BeanClassLoaderAware</h3><ul><li>setBeanClassLoader</li></ul><p>初始化bean的阶段(已设置properties)调用</p><h3 id="BeanFactoryAware"><a href="#BeanFactoryAware" class="headerlink" title="BeanFactoryAware"></a>BeanFactoryAware</h3><ul><li>setBeanFactory</li></ul><p>初始化bean的阶段(已设置properties)调用</p><h3 id="常用拦截"><a href="#常用拦截" class="headerlink" title="常用拦截"></a>常用拦截</h3><ul><li><p>需要注入propertySource</p><p>实现EnvironmentPostProcessor,这个拦截会在收到 ApplicationEnvironmentPreparedEvent事件后执行</p></li><li><p>需要注册BeanDefinition</p><p>实现BeanDefinitionRegistryPostProcessor</p></li><li><p>拦截BeanDefinition</p><p>实现BeanFactoryPostProcessor或者MergedBeanDefinitionPostProcessor</p></li><li><p>拦截Bean的创建</p><p>实现InstantiationAwareBeanPostProcessor</p></li><li><p>拦截Bean的初始化</p><p>实现BeanPostProcessor</p><p>注解@PostConstruct</p></li><li><p>拦截bean的销毁</p><p>实现DestructionAwareBeanPostProcessor</p><p>注解PreDestroy</p></li></ul><h3 id="SpringBoot初始化顺序"><a href="#SpringBoot初始化顺序" class="headerlink" title="SpringBoot初始化顺序"></a>SpringBoot初始化顺序</h3><ul><li>初始化environment</li><li>初始化日志系统</li><li>初始化applicationContext</li><li>Refresh applicationContext</li></ul>]]></content>
<summary type="html">
<p>最近在排查一个拦截bean创建问题的时候,重新梳理了一下spring的相关hook. 其作用是当我们需要做一些框架层面的拦截的时候,需要确定将这个拦截加在什么位置。</p>
<h3 id="SpringBoot-相关事件"><a href="#SpringBoot-相关事件
</summary>
</entry>
<entry>
<title>seata分布式事务</title>
<link href="http://laywin.github.io/2023/11/05/seata%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"/>
<id>http://laywin.github.io/2023/11/05/seata分布式事务/</id>
<published>2023-11-05T14:31:42.000Z</published>
<updated>2023-11-05T14:58:53.295Z</updated>
<content type="html"><![CDATA[<p>seata是一个两阶段的分布式事务,由TM, TC, RM 构成,全局事务由TM发起,一阶段RM注册分支事务到TC, 二阶段由TM决定根据事务执行情况进行提交或者回滚(TC 后台通过全局事务是否超时,也能决定是否进行回滚)</p><h3 id="AT模式的优缺点"><a href="#AT模式的优缺点" class="headerlink" title="AT模式的优缺点"></a>AT模式的优缺点</h3><ul><li><p>优点</p><p>接入成本低,代码侵入性弱,只需要加个注解搞定,写隔离,强一致性。</p></li><li><p>缺点</p><p>写隔离,强一致性会要求在全局事务执行期间锁定对应的事务资源, 对热点数据的锁定会导致系统并发性能衰退,所以AT模式不适合长事务。</p></li></ul><h3 id="TM相关事务请求"><a href="#TM相关事务请求" class="headerlink" title="TM相关事务请求"></a>TM相关事务请求</h3><ul><li>GlobalBeginRequest</li></ul><p>TM:发起请求,生成XID</p><p>TC: 创建globalSession</p><ul><li>GlobalCommitRequest</li></ul><p>TM:发起请求,提交全局事务</p><p>TC: 查找全局事务,判断是否超时,设置支持异步提交,设置异步提交的标识,返回提交状态</p><ul><li>GlobalRollbackRequest</li></ul><p>TM: 当在全局事务执行中,出现异常时,发起全局事务回滚</p><p>TC: 根据全局事务查找对应的分支事务,执行branchRollbackRequest.</p><h3 id="RM-相关事务请求"><a href="#RM-相关事务请求" class="headerlink" title="RM 相关事务请求"></a>RM 相关事务请求</h3><ul><li>BranchRegisterRequest</li></ul><p>RM: 在RM本地事务中,当connection提交的时候会去注册分支事务</p><p>TC: 根据rowkey生成对应的锁记录,实现写隔离</p><ul><li>BranchRollbackRequest</li></ul><p>RM: RM处理TC下发的 BranchRollbackRequest请求,删除本地的undolog日志</p><p>TC: TC接受TM的rollback的决议或者判断全局事务已超时后,发起分支事务回滚</p><ul><li>BranchCommitRequest</li></ul><p>RM: 由TC发起,异步删除undolog日志,实际的做法是将二阶段提交的上下文放到队列中,定时任务会定时获取队列中的上下文来删除undolog日志</p><p>TC: 当收到TM的全局事务提交请求后,发起分支事务提交</p><h3 id="锁"><a href="#锁" class="headerlink" title="锁"></a>锁</h3><h4 id="RowKey-组成"><a href="#RowKey-组成" class="headerlink" title="RowKey 组成"></a>RowKey 组成</h4><p>xid:transactionId:branchID:tableName:pk:resourceId</p><h3 id="判断锁是否获取成功"><a href="#判断锁是否获取成功" class="headerlink" title="判断锁是否获取成功"></a>判断锁是否获取成功</h3><ul><li>判断rowkey是否能查询到锁的数据,如果查询到数据, 判断XID是否一致,如果一致,说明重入锁</li><li>如果没有查询到数据,说明没有加锁,插入数据,表占用锁</li></ul><h3 id="生成undolog日志"><a href="#生成undolog日志" class="headerlink" title="生成undolog日志"></a>生成undolog日志</h3><h4 id="生成时机"><a href="#生成时机" class="headerlink" title="生成时机"></a>生成时机</h4><p>preparedstatement代理执行的时候,会生成beforeImage&afterImage.</p><ul><li><p>beforeImage</p><p>回滚语句</p></li><li><p>afterImage</p><p>回滚的时候用来对比事务相关的数据有没发生变化,如果发生改变,不能直接回滚</p></li></ul><h3 id="beforeImage-amp-AfterImage生成逻辑"><a href="#beforeImage-amp-AfterImage生成逻辑" class="headerlink" title="beforeImage&AfterImage生成逻辑"></a>beforeImage&AfterImage生成逻辑</h3><ul><li><p>delete</p><ul><li><p>before</p><p>反向生成用于回滚的insert语句</p></li><li><p>after</p><p>Empty</p></li></ul></li><li><p>Update</p><ul><li><p>before</p><p>将更新前的数据select出来用来回滚</p></li><li><p>after</p><p>更新后的数据select出来用来在回滚的时候看数据是否发生了变化</p></li></ul></li><li><p>Insert</p><ul><li><p>before</p><p>empty</p></li><li><p>after</p><p>生成一条delete语句</p></li></ul></li><li><p>select</p><p>无镜像语句</p></li></ul><h4 id="seata-持久化undolog"><a href="#seata-持久化undolog" class="headerlink" title="seata 持久化undolog"></a>seata 持久化undolog</h4><ol><li>对undolog进行序列化</li><li>对序列化的结果进行压缩</li><li>持久化到数据库</li></ol><h3 id="三层代理"><a href="#三层代理" class="headerlink" title="三层代理"></a>三层代理</h3><ul><li><p>DataSourceProxy</p></li><li><p>ConnectionProxy</p></li><li><p>StatementProxy</p></li></ul><h3 id="正常流程"><a href="#正常流程" class="headerlink" title="正常流程"></a>正常流程</h3><ul><li>提交</li></ul><p>当请求链路调用完成后,发起方通知TC提交或回滚分布式事务,进入二阶段调用流程,此时,TC会根据之前注册的分布式事务回调到对应的参与者去执行对应资源的第二阶段. (事务提交的时候会通过XID查询到全局事务,并加锁,关闭,目的是防止该事务后续还有分支继续注册上来)</p><ul><li>提交过程</li></ul><p>AT 模式下直接将事务状态由begining 变为 asyncComming,然后查询出待提交的全局事务日志进行提交,如果全局事务提交成功,则会释放全局锁,并删除事务日志(AT模式是异步的事物提交,因为一阶段相关的数据已经完成落库)。</p><ul><li>回滚</li></ul><p>回滚失败,不会释放锁</p><p><img src="/2023/11/05/seata分布式事务/seata001.png" alt></p><h3 id="隔离"><a href="#隔离" class="headerlink" title="隔离"></a>隔离</h3><p><img src="/2023/11/05/seata分布式事务/seata002.png" alt></p><p>写隔离主要是通过全局锁进行隔离,注册分支事务的时候会尝试获取全局锁,全局锁获取成功,提交本地事务</p>]]></content>
<summary type="html">
<p>seata是一个两阶段的分布式事务,由TM, TC, RM 构成,全局事务由TM发起,一阶段RM注册分支事务到TC, 二阶段由TM决定根据事务执行情况进行提交或者回滚(TC 后台通过全局事务是否超时,也能决定是否进行回滚)</p>
<h3 id="AT模式的优缺点"><a
</summary>
</entry>
<entry>
<title>redis延迟队列</title>
<link href="http://laywin.github.io/2023/10/08/redis%E5%BB%B6%E8%BF%9F%E9%98%9F%E5%88%97/"/>
<id>http://laywin.github.io/2023/10/08/redis延迟队列/</id>
<published>2023-10-08T14:42:46.000Z</published>
<updated>2023-10-08T14:43:15.357Z</updated>
<content type="html"><![CDATA[<h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><p>电商领域有很多延迟队列的应用场景,例如:商品的上下架,一般会指定一个特定的上下架时间,时间到了,就会触发对应的操作. 在比如 订单的关单,超过特定的时间,如果订单没有支付就需要关闭订单,释放库存.</p><h3 id="实现方式"><a href="#实现方式" class="headerlink" title="实现方式"></a>实现方式</h3><p>实现方式可分为2种,一种是进程内的实现方式,如 DelayQueue。另外一种是中间件的实现方式:redis, rocketMq,rabbitMq,这里主要会介绍一下Redis的实现方式,原因在于 DelayQueue只是一种 jvm 的数据结构,不太适合实际的应用,至于两种MQ,如果Redis能够很从容的实现延迟队列,那大可不必引用MQ带来额外的运维成本.</p><h4 id="DelayQueue"><a href="#DelayQueue" class="headerlink" title="DelayQueue"></a>DelayQueue</h4><p>delayQueue逻辑的是 内部会引入一个优先级队列进行堆排序,堆顶的数据即是最先到期的,获取的时候会判断堆顶数据是否到期,如果是,则从优先级队列中弹出.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"> <span class="function"><span class="keyword">public</span> E <span class="title">poll</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> ReentrantLock lock = <span class="keyword">this</span>.lock;</span><br><span class="line"> lock.lock();</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 获取优先级队列中的第一个元素</span></span><br><span class="line"> E first = q.peek();</span><br><span class="line"> <span class="comment">// 如果到期了从 PriorityQueue 中移除,并返回</span></span><br><span class="line"> <span class="keyword">return</span> (first == <span class="keyword">null</span> || first.getDelay(NANOSECONDS) > <span class="number">0</span>)</span><br><span class="line"> ? <span class="keyword">null</span></span><br><span class="line"> : q.poll();</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> lock.unlock();</span><br><span class="line"> }</span><br><span class="line">} </span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">offer</span><span class="params">(E e)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> ReentrantLock lock = <span class="keyword">this</span>.lock;</span><br><span class="line"> lock.lock();</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// q 是 PriorityQueue</span></span><br><span class="line"> q.offer(e);</span><br><span class="line"> <span class="keyword">if</span> (q.peek() == e) {</span><br><span class="line"> leader = <span class="keyword">null</span>;</span><br><span class="line"> available.signal();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> lock.unlock();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="RocketMq"><a href="#RocketMq" class="headerlink" title="RocketMq"></a>RocketMq</h3><p>rocketMq原生支持延迟队列,后面会做介绍.</p><h3 id="RabbitMq"><a href="#RabbitMq" class="headerlink" title="RabbitMq"></a>RabbitMq</h3><p>rabbitMq 的延迟队列是通过生产者设置消息的TTL(time to live),当TTL到期之后还没有被消费的时候,会将消息放入死信队列,消费者订阅死信队列就好.</p><h3 id="Redis"><a href="#Redis" class="headerlink" title="Redis"></a>Redis</h3><p>redis 主要通过zset这个数据结构来实现,zset可以通过关联一个分数来进行排序和delayQueue里面的PriorityQueue一样都提供了排序的功能,zset的分数对应就是延迟时间. 这里主要介绍一下redission的实现方式,也即zset的封装版本,它主要在提供了一套调度方案.</p><p>首先看一下实现的example:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DelayMsgServiceImpl</span> <span class="keyword">implements</span> <span class="title">IDelayMsgService</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissonClient redissonClient;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> RDelayedQueue<DelayData> delayQueue;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> RBlockingQueue<DelayData> blockingQueue;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@PostConstruct</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">()</span> </span>{</span><br><span class="line">blockingQueue = redissonClient.getBlockingQueue(<span class="string">"test"</span>);</span><br><span class="line">delayQueue = redissonClient.getDelayedQueue(blockingQueue);</span><br><span class="line"></span><br><span class="line">delayQueueConsume();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">delayQueueConsume</span><span class="params">()</span> </span>{</span><br><span class="line">Thread thread = <span class="keyword">new</span> Thread(() -> {</span><br><span class="line"><span class="keyword">while</span> (<span class="keyword">true</span>) {</span><br><span class="line"><span class="keyword">if</span> (redissonClient.isShutdown()) {</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line">DelayData delayData = blockingQueue.take();</span><br><span class="line">log.info(<span class="string">"处理延迟任务:{}"</span>, JSONUtil.toJsonStr(delayData));</span><br><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">log.error(<span class="string">"延迟任务消费失败"</span>, e);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line">thread.start();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">pushDelayData</span><span class="params">(DelayData delayData)</span> </span>{</span><br><span class="line">log.info(<span class="string">"添加延迟任务数据:{}"</span>, JSONUtil.toJsonStr(delayData));</span><br><span class="line">delayQueue.offer(delayData, delayData.getDelayTime(), TimeUnit.MILLISECONDS);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>redission 提供了RDelayedQueue的数据结构,这里主要看消费线程,在while里面通过阻塞队列去获取延迟任务, 简单明了.内部的实现逻辑我们主要从消息的生产和消费来梳理.</p><h4 id="消息的生产"><a href="#消息的生产" class="headerlink" title="消息的生产"></a>消息的生产</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> RFuture<Void> <span class="title">offerAsync</span><span class="params">(V e, <span class="keyword">long</span> delay, TimeUnit timeUnit)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (delay < <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"Delay can't be negative"</span>);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">long</span> delayInMs = timeUnit.toMillis(delay);</span><br><span class="line"> <span class="keyword">long</span> timeout = System.currentTimeMillis() + delayInMs;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">byte</span>[] random = getServiceManager().generateIdArray(<span class="number">8</span>);</span><br><span class="line"> <span class="keyword">return</span> commandExecutor.evalWriteNoRetryAsync(getRawName(), codec, RedisCommands.EVAL_VOID,</span><br><span class="line"> <span class="comment">// 参数打包</span></span><br><span class="line"> <span class="string">"local value = struct.pack('Bc0Lc0', string.len(ARGV[2]), ARGV[2], string.len(ARGV[3]), ARGV[3]);"</span></span><br><span class="line"> <span class="comment">// 延迟消息放到zset中</span></span><br><span class="line"> + <span class="string">"redis.call('zadd', KEYS[2], ARGV[1], value);"</span></span><br><span class="line"> <span class="comment">// 延迟消息放一份到延迟队列中</span></span><br><span class="line"> + <span class="string">"redis.call('rpush', KEYS[3], value);"</span></span><br><span class="line"> <span class="comment">// if new object added to queue head when publish its startTime </span></span><br><span class="line"> <span class="comment">// to all scheduler workers </span></span><br><span class="line"> <span class="comment">// 如果zset的第一个延迟数据是我们刚放入的延迟数据,就将这个消息发布出去</span></span><br><span class="line"> + <span class="string">"local v = redis.call('zrange', KEYS[2], 0, 0); "</span></span><br><span class="line"> + <span class="string">"if v[1] == value then "</span></span><br><span class="line"> + <span class="string">"redis.call('publish', KEYS[4], ARGV[1]); "</span></span><br><span class="line"> + <span class="string">"end;"</span>,</span><br><span class="line"> Arrays.asList(getRawName(), timeoutSetName, queueName, channelName),</span><br><span class="line"> timeout, random, encode(e));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p> 生产线程中主要有3个操作:</p><ol><li>将延迟消息放入zset</li><li>将延迟消息放入一个队列中</li><li>取出zset中第一个消息,如果就是刚刚我们方式的消息,就将这个消息发布出去.</li></ol><h4 id="消息消费"><a href="#消息消费" class="headerlink" title="消息消费"></a>消息消费</h4><p>这里我们从延迟队列的初始化看起,延迟队列的构造函数如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="title">RedissonDelayedQueue</span><span class="params">(QueueTransferService queueTransferService, Codec codec, <span class="keyword">final</span> CommandAsyncExecutor commandExecutor, String name)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(codec, commandExecutor, name);</span><br><span class="line"> channelName = prefixName(<span class="string">"redisson_delay_queue_channel"</span>, getRawName());</span><br><span class="line"> queueName = prefixName(<span class="string">"redisson_delay_queue"</span>, getRawName());</span><br><span class="line"> timeoutSetName = prefixName(<span class="string">"redisson_delay_queue_timeout"</span>, getRawName());</span><br><span class="line"> </span><br><span class="line"> QueueTransferTask task = <span class="keyword">new</span> QueueTransferTask(commandExecutor.getServiceManager()) {</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> RFuture<Long> <span class="title">pushTaskAsync</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG,</span><br><span class="line"> <span class="string">"local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); "</span></span><br><span class="line"> + <span class="string">"if #expiredValues > 0 then "</span></span><br><span class="line"> + <span class="string">"for i, v in ipairs(expiredValues) do "</span></span><br><span class="line"> + <span class="string">"local randomId, value = struct.unpack('Bc0Lc0', v);"</span></span><br><span class="line"> + <span class="string">"redis.call('rpush', KEYS[1], value);"</span></span><br><span class="line"> + <span class="string">"redis.call('lrem', KEYS[3], 1, v);"</span></span><br><span class="line"> + <span class="string">"end; "</span></span><br><span class="line"> + <span class="string">"redis.call('zrem', KEYS[2], unpack(expiredValues));"</span></span><br><span class="line"> + <span class="string">"end; "</span></span><br><span class="line"> <span class="comment">// get startTime from scheduler queue head task</span></span><br><span class="line"> + <span class="string">"local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); "</span></span><br><span class="line"> + <span class="string">"if v[1] ~= nil then "</span></span><br><span class="line"> + <span class="string">"return v[2]; "</span></span><br><span class="line"> + <span class="string">"end "</span></span><br><span class="line"> + <span class="string">"return nil;"</span>,</span><br><span class="line"> Arrays.asList(getRawName(), timeoutSetName, queueName),</span><br><span class="line"> System.currentTimeMillis(), <span class="number">100</span>);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> RTopic <span class="title">getTopic</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> RedissonTopic.createRaw(LongCodec.INSTANCE, commandExecutor, channelName);</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line"> </span><br><span class="line"> queueTransferService.schedule(queueName, task);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">this</span>.queueTransferService = queueTransferService;</span><br></pre></td></tr></table></figure><p>可以看到 声明了3个数据结构:</p><ol><li><p>redisson_delay_queue_timeout 开头的就是zset,用来做延迟数据排序。</p></li><li><p>redisson_delay_queue 开头的是一个队列,会同步放一份延迟数据到队列中。</p></li><li>redisson_delay_queue_channel 开头的是一个消息队列,作用后面会做介绍。</li></ol><p>创建了一个任务,这个任务主要干这个事:</p><ol><li>通过zrangebyscore取出到期的延迟数据,分页100条</li><li>如果有延迟数据将参数解包, 放到最终的一个队列中去(注意:区别于前面的 redisson_delay_queue_timeout, redisson_delay_queue ),然后从zset中删除</li><li>从zset获取第一个延迟数据,如果有就将延迟时间返回出去</li></ol><p>最后在构造函数倒数第二行,schedule了前面创建的task</p><p>接下来看一下调度任务:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">//获取前面的 redisson_delay_queue_channel 消息队列</span></span><br><span class="line"> RTopic schedulerTopic = getTopic();</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 注册当订阅成功时调用 pushTask</span></span><br><span class="line"> statusListenerId = schedulerTopic.addListener(<span class="keyword">new</span> BaseStatusListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onSubscribe</span><span class="params">(String channel)</span> </span>{</span><br><span class="line"> pushTask();</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 注册当在redisson_delay_queue_channel 消息队列 收到消息时 scheduleTask </span></span><br><span class="line"> messageListenerId = schedulerTopic.addListener(Long<span class="class">.<span class="keyword">class</span>, <span class="title">new</span> <span class="title">MessageListener</span><<span class="title">Long</span>>() </span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onMessage</span><span class="params">(CharSequence channel, Long startTime)</span> </span>{</span><br><span class="line"> scheduleTask(startTime);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>调度任务主要有2个操作</p><ol><li>订阅成功的时候 pushTask()</li><li>收到 push 消息的时候 scheduleTask(startTime)</li></ol><p>看一下pushTask:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">pushTask</span><span class="params">()</span> </span>{</span><br><span class="line"> RFuture<Long> startTimeFuture = pushTaskAsync();</span><br><span class="line"> startTimeFuture.whenComplete((res, e) -> {</span><br><span class="line"> <span class="keyword">if</span> (e != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (e <span class="keyword">instanceof</span> RedissonShutdownException) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> log.error(e.getMessage(), e);</span><br><span class="line"> scheduleTask(System.currentTimeMillis() + <span class="number">5</span> * <span class="number">1000L</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (res != <span class="keyword">null</span>) {</span><br><span class="line"> scheduleTask(res);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当收到zset中第一个没有到期的延迟消息时,调用scheduleTask(res).</p><p>看一下scheduleTask:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">scheduleTask</span><span class="params">(<span class="keyword">final</span> Long startTime)</span> </span>{</span><br><span class="line"> TimeoutTask oldTimeout = lastTimeout.get();</span><br><span class="line"> <span class="keyword">if</span> (startTime == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (oldTimeout != <span class="keyword">null</span>) {</span><br><span class="line"> oldTimeout.getTask().cancel();</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">long</span> delay = startTime - System.currentTimeMillis();</span><br><span class="line"> <span class="keyword">if</span> (delay > <span class="number">10</span>) {</span><br><span class="line"> Timeout timeout = serviceManager.newTimeout(<span class="keyword">new</span> TimerTask() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">(Timeout timeout)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> pushTask();</span><br><span class="line"> </span><br><span class="line"> TimeoutTask currentTimeout = lastTimeout.get();</span><br><span class="line"> <span class="keyword">if</span> (currentTimeout.getTask() == timeout) {</span><br><span class="line"> lastTimeout.compareAndSet(currentTimeout, <span class="keyword">null</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }, delay, TimeUnit.MILLISECONDS);</span><br><span class="line"> <span class="keyword">if</span> (!lastTimeout.compareAndSet(oldTimeout, <span class="keyword">new</span> TimeoutTask(startTime, timeout))) {</span><br><span class="line"> timeout.cancel();</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> pushTask();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>scheduleTask的主要逻辑是收到zset中第一个没有到期的延迟消息时,如果延迟时间大于10ms, 放入时间轮(见:<a href="https://laywin.github.io/2023/09/20/HashedWheelTimer-%E6%97%B6%E9%97%B4%E8%BD%AE/),在时间轮中超时后,执行task任务,继续获取下一个没有到期的延迟消息,如果小于10ms则直接执行任务">https://laywin.github.io/2023/09/20/HashedWheelTimer-%E6%97%B6%E9%97%B4%E8%BD%AE/),在时间轮中超时后,执行task任务,继续获取下一个没有到期的延迟消息,如果小于10ms则直接执行任务</a>. 继续获取下一个没有到期的延迟消息,如果zset中没有消息了,停止调度. 停止调度后怎么来触发后续调度了?前面在提交延迟数据的时候如果zset的第一个延迟数据是我们刚放入的延迟数据,就将这个消息发布出去,此时会触发到上面消息队列获取消息的监听器,从而重新触发调度.</p><p>调用 blockingQueue.take() 获取消息的时候:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> RFuture<V> <span class="title">takeAsync</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> commandExecutor.writeAsync(getRawName(), codec, RedisCommands.BLPOP_VALUE, getRawName(), <span class="number">0</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>实际执行了 BLPOP 指令</p><p>最后还有一个问题?为什么多一个 redisson_delay_queue 队列?</p><p>我们看到当产生延迟数据时,不仅会加入到zset中,还会放到 redisson_delay_queue 一份,主要原因是为了方便后续对延迟数据的操作,例如通过索引获取数据</p><h3 id="总结一下:"><a href="#总结一下:" class="headerlink" title="总结一下:"></a>总结一下:</h3><p>redission 的延迟队列总共涉及到4个数据结构:</p><ol><li>zset(redisson_delay_queue_timeout:{xx}): 用来通过分数排序</li><li>消息队列(redisson_delay_queue_channel:{xx}):用来实现发布订阅,从而实现调度功能,订阅成功时,执行 任务,zset添加的延迟消息是第一个消息时,重新拉起调度任务(订阅消息)</li><li>队列1(xx):用来实现最终的消费</li><li>队列2(redisson_delay_queue:{xx}): 用来支持通过索引操作延迟数据</li></ol><p>如何调度:</p><ol><li>服务启动时,订阅成功,会调度一次,如果有未到达延迟时间的数据,会进行两个操作,如果延迟时间大于10ms, 进入时间轮,否则直接调度,如果zset中已经没有延迟数据时,停止调度。</li><li>当添加消息时,如果 zset中第一个延迟消息就是添加的消息时,向消息队列中发布消息,重新拉起调度逻辑。</li></ol><p>调度的任务:</p><ol><li>通过zrangebyscore取出到期的延迟数据,分页100条</li><li>如果有延迟数据将参数解包, 放到最终的一个队列中去(注意:区别于前面的 redisson_delay_queue_timeout, redisson_delay_queue ),然后从zset中删除</li><li>从zset获取第一个延迟数据,如果有就将延迟时间返回出去。</li></ol><p>生产者执行操作:</p><ol><li>将延迟消息放入zset</li><li>将延迟消息放入一个队列中</li><li>取出zset中第一个消息,如果就是刚刚我们方式的消息,就将这个消息发布出去.</li></ol><p> </p>]]></content>
<summary type="html">
<h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><p>电商领域有很多延迟队列的应用场景,例如:商品的上下架,一般会指定一个特定的上下架时间,时间到了,就会触发对应的操作. 在比如
</summary>
</entry>
<entry>
<title>HashedWheelTimer(时间轮)</title>
<link href="http://laywin.github.io/2023/09/20/HashedWheelTimer-%E6%97%B6%E9%97%B4%E8%BD%AE/"/>
<id>http://laywin.github.io/2023/09/20/HashedWheelTimer-时间轮/</id>
<published>2023-09-20T08:17:30.000Z</published>
<updated>2023-10-08T14:12:40.444Z</updated>
<content type="html"><![CDATA[<p>最近在用Redssion做延迟队列,Redssion内部使用了HashedWheelTimer作为延迟队列, 做一下总结。</p><p><strong><em>HashedWheelTimer主要用来做近似的io调度</em></strong></p><h3 id="Tick-持续时间"><a href="#Tick-持续时间" class="headerlink" title="Tick 持续时间"></a>Tick 持续时间</h3><p>之所以是近似,是因为HashedWheelTimer并不能准时的调度TimerTask。</p><p>通过在构造函数增大或减小tick的持续时间来提高或者减少执行的精度,在大多数网络应用程序中,IO超时不需要太精确,因此,默认的tick持续时间为 100ms, 在大多数情况下,并不需要去尝试不同的配置,用默认的就好。</p><h3 id="wheel-的大小"><a href="#wheel-的大小" class="headerlink" title="wheel 的大小"></a>wheel 的大小</h3><p>HashedWheelTimer 维护了一个叫 “wheel” 的数据结构,简单来说 wheel 是一个hash table, 它的hash函数是 task的dead line, 默认情况下,一个 wheel 的 tick总数是 512,如果需要调度更多的超时任务,可以配置这个值配置的更大一点</p><h3 id="不要创建过多的实例"><a href="#不要创建过多的实例" class="headerlink" title="不要创建过多的实例"></a>不要创建过多的实例</h3><p>HashedWheelTimer 实例化并且启动的时候会创建一个新的线程,因此,需要确保只创建一个实例并且在应用程序中共享,一个导致应用无响应的普遍错误是为每一个连接创建一个实例,</p><h3 id="实现详情"><a href="#实现详情" class="headerlink" title="实现详情"></a>实现详情</h3><p>HashedWheelTimer是基于 George Varghese and Tony Lauck’s 论文,论文地址:<a href="http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z,更全面的内容:http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt" target="_blank" rel="noopener">http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z,更全面的内容:http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt</a></p><p>我们主要从数据结构和任务调度来分析一下 HashedWheelTimer</p><h3 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h3><p>HashedWheelTimer 主要是由数组和链表组成,数组部分是对应的桶,链表部分是任务, 和hashmap类似.</p><h4 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">HashedWheelTimer</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> ThreadFactory threadFactory,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">long</span> tickDuration, TimeUnit unit, <span class="keyword">int</span> ticksPerWheel, <span class="keyword">boolean</span> leakDetection,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">long</span> maxPendingTimeouts)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 创建wheel</span></span><br><span class="line"> wheel = createWheel(ticksPerWheel);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 取 mask</span></span><br><span class="line"> mask = wheel.length - <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// tick 持续时间转换</span></span><br><span class="line"> <span class="keyword">long</span> duration = unit.toNanos(tickDuration);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// tick 持续时间校验</span></span><br><span class="line"> <span class="keyword">if</span> (duration >= Long.MAX_VALUE / wheel.length) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(String.format(</span><br><span class="line"> <span class="string">"tickDuration: %d (expected: 0 < tickDuration in nanos < %d"</span>,</span><br><span class="line"> tickDuration, Long.MAX_VALUE / wheel.length));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// tick 持续时间最小不能小于1ms</span></span><br><span class="line"> <span class="keyword">if</span> (duration < MILLISECOND_NANOS) {</span><br><span class="line"> logger.warn(<span class="string">"Configured tickDuration {} smaller then {}, using 1ms."</span>,</span><br><span class="line"> tickDuration, MILLISECOND_NANOS);</span><br><span class="line"> <span class="keyword">this</span>.tickDuration = MILLISECOND_NANOS;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">this</span>.tickDuration = duration;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 创建调度小城</span></span><br><span class="line"> workerThread = threadFactory.newThread(worker);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 最大任务数</span></span><br><span class="line"> <span class="keyword">this</span>.maxPendingTimeouts = maxPendingTimeouts;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//..</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> HashedWheelBucket[] createWheel(<span class="keyword">int</span> ticksPerWheel) {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 创建对应的桶</span></span><br><span class="line"> HashedWheelBucket[] wheel = <span class="keyword">new</span> HashedWheelBucket[ticksPerWheel];</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < wheel.length; i ++) {</span><br><span class="line"> wheel[i] = <span class="keyword">new</span> HashedWheelBucket();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> wheel;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h4 id="任务添加"><a href="#任务添加" class="headerlink" title="任务添加"></a>任务添加</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Timeout <span class="title">newTimeout</span><span class="params">(TimerTask task, <span class="keyword">long</span> delay, TimeUnit unit)</span> </span>{</span><br><span class="line"> ObjectUtil.checkNotNull(task, <span class="string">"task"</span>);</span><br><span class="line"> ObjectUtil.checkNotNull(unit, <span class="string">"unit"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">long</span> pendingTimeoutsCount = pendingTimeouts.incrementAndGet();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// pendingTimeoutsCount 已经超过的上线</span></span><br><span class="line"> <span class="keyword">if</span> (maxPendingTimeouts > <span class="number">0</span> && pendingTimeoutsCount > maxPendingTimeouts) {</span><br><span class="line"> pendingTimeouts.decrementAndGet();</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> RejectedExecutionException(<span class="string">"Number of pending timeouts ("</span></span><br><span class="line"> + pendingTimeoutsCount + <span class="string">") is greater than or equal to maximum allowed pending "</span></span><br><span class="line"> + <span class="string">"timeouts ("</span> + maxPendingTimeouts + <span class="string">")"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 启动任务调度</span></span><br><span class="line"> start();</span><br><span class="line"></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">long</span> deadline = System.nanoTime() + unit.toNanos(delay) - startTime;</span><br><span class="line"> </span><br><span class="line"> HashedWheelTimeout timeout = <span class="keyword">new</span> HashedWheelTimeout(<span class="keyword">this</span>, task, deadline);</span><br><span class="line"> <span class="comment">// 将任务加入到队列中</span></span><br><span class="line"> timeouts.add(timeout);</span><br><span class="line"> <span class="keyword">return</span> timeout;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">switch</span> (WORKER_STATE_UPDATER.get(<span class="keyword">this</span>)) {</span><br><span class="line"> <span class="keyword">case</span> WORKER_STATE_INIT:</span><br><span class="line"> <span class="keyword">if</span> (WORKER_STATE_UPDATER.compareAndSet(<span class="keyword">this</span>, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {</span><br><span class="line"> <span class="comment">//启动调度任务</span></span><br><span class="line"> workerThread.start();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> WORKER_STATE_STARTED:</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> WORKER_STATE_SHUTDOWN:</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"cannot be started once stopped"</span>);</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> Error(<span class="string">"Invalid WorkerState"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="任务调度"><a href="#任务调度" class="headerlink" title="任务调度"></a>任务调度</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">Worker</span> <span class="keyword">implements</span> <span class="title">Runnable</span></span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> <span class="comment">// 初始化startTime</span></span><br><span class="line"> startTime = System.nanoTime();</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> <span class="comment">// 等待下一个tick,deadline为过程消耗的时间</span></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">long</span> deadline = waitForNextTick();</span><br><span class="line"> <span class="keyword">if</span> (deadline > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// tick取余来找对应桶</span></span><br><span class="line"> <span class="keyword">int</span> idx = (<span class="keyword">int</span>) (tick & mask);</span><br><span class="line"> <span class="comment">// 处理已经取消的任务</span></span><br><span class="line"> processCancelledTasks();</span><br><span class="line"> HashedWheelBucket bucket =</span><br><span class="line"> wheel[idx];</span><br><span class="line"> <span class="comment">// 将任务从队列加入到桶里面去</span></span><br><span class="line"> transferTimeoutsToBuckets();</span><br><span class="line"> <span class="comment">//任务处理</span></span><br><span class="line"> bucket.expireTimeouts(deadline);</span><br><span class="line"> <span class="comment">// tick 自增</span></span><br><span class="line"> tick++;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">while</span> (WORKER_STATE_UPDATER.get(HashedWheelTimer.<span class="keyword">this</span>) == WORKER_STATE_STARTED);</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">long</span> <span class="title">waitForNextTick</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// 总的tick持续时间</span></span><br><span class="line"> <span class="keyword">long</span> deadline = tickDuration * (tick + <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (;;) {</span><br><span class="line"> <span class="comment">// 当前时间相对于开始时间的差值</span></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">long</span> currentTime = System.nanoTime() - startTime;</span><br><span class="line"> <span class="comment">// 见备注1</span></span><br><span class="line"> <span class="keyword">long</span> sleepTimeMs = (deadline - currentTime + <span class="number">999999</span>) / <span class="number">1000000</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (sleepTimeMs <= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (currentTime == Long.MIN_VALUE) {</span><br><span class="line"> <span class="keyword">return</span> -Long.MAX_VALUE;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> currentTime;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> Thread.sleep(sleepTimeMs);</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException ignored) {</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">/***</span></span><br><span class="line"><span class="comment"> * 将队列中的任务添加到桶里面去</span></span><br><span class="line"><span class="comment"> **/</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">transferTimeoutsToBuckets</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < <span class="number">100000</span>; i++) {</span><br><span class="line"> HashedWheelTimeout timeout = timeouts.poll();</span><br><span class="line"> <span class="keyword">if</span> (timeout == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 计算总的tick数,注意 deadline = 任务添加时时间 + delay - startTime</span></span><br><span class="line"> <span class="keyword">long</span> calculated = timeout.deadline / tickDuration;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 计算任务应该有多少个wheel</span></span><br><span class="line"> timeout.remainingRounds = (calculated - tick) / wheel.length;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">long</span> ticks = Math.max(calculated, tick);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 计算桶的索引</span></span><br><span class="line"> <span class="keyword">int</span> stopIndex = (<span class="keyword">int</span>) (ticks & mask);</span><br><span class="line"></span><br><span class="line"> HashedWheelBucket bucket = wheel[stopIndex];</span><br><span class="line"> bucket.addTimeout(timeout);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> <span class="comment">/***</span></span><br><span class="line"><span class="comment"> * 任务到期处理</span></span><br><span class="line"><span class="comment"> **/</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">expireTimeouts</span><span class="params">(<span class="keyword">long</span> deadline)</span> </span>{</span><br><span class="line"> HashedWheelTimeout timeout = head;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">while</span> (timeout != <span class="keyword">null</span>) {</span><br><span class="line"> HashedWheelTimeout next = timeout.next;</span><br><span class="line"> <span class="keyword">if</span> (timeout.remainingRounds <= <span class="number">0</span>) {</span><br><span class="line"> next = remove(timeout);</span><br><span class="line"> <span class="keyword">if</span> (timeout.deadline <= deadline) {</span><br><span class="line"> timeout.expire();</span><br><span class="line"> } <span class="keyword">else</span> { </span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(String.format(</span><br><span class="line"> <span class="string">"timeout.deadline (%d) > deadline (%d)"</span>, timeout.deadline, deadline));</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (timeout.isCancelled()) {</span><br><span class="line"> next = remove(timeout);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> timeout.remainingRounds --;</span><br><span class="line"> }</span><br><span class="line"> timeout = next;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> <span class="comment">/***</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * 任务到期后调用task run执行,注意task的run方法尽量不要执行耗时的任务或者在run里面异步执行,</span></span><br><span class="line"><span class="comment"> * 否则延迟任务执行误差会很大</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> **/</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">expire</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (!compareAndSetState(ST_INIT, ST_EXPIRED)) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> task.run(<span class="keyword">this</span>);</span><br><span class="line"> } <span class="keyword">catch</span> (Throwable t) {</span><br><span class="line"> <span class="keyword">if</span> (logger.isWarnEnabled()) {</span><br><span class="line"> logger.warn(<span class="string">"An exception was thrown by "</span> + TimerTask<span class="class">.<span class="keyword">class</span>.<span class="title">getSimpleName</span>() + '.', <span class="title">t</span>)</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><ul><li>备注1 </li></ul><p>加999999 除以 1000000 的原因是向上取整和四舍五入一个意思,正常情况是睡deadline - currentTime的时间,其中 deadline 计算的是tick + 1的持续时间,而 currentTime = System.nanoTime() - startTime 是实际运行时间,理论情况下当:deadline - currentTime = 0 时 return, 而 计算的 tick + 1的持续时间 和 实际运行时间总会存在差别,计算时sleepTimeMs 就向上取整1ms.</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>HashedWheelTimer 是一个类似于HashMap的数据结构,在应用程序中共用一个HashedWheelTimer实例, 任务调度时在单线程中执行的,调度过程会先暂停一个tick的时间以到达下一个tick,然后将任务队列中的任务数据放到对应的桶里面,任务数据维护了链表的结构,并且包含deadline和remainingRounds,当 remainingRounds <= 0并且任务deadline < 时机deadline的时候,任务得到调度,值得注意的是,尽量不要执行耗时的任务或者异步执行执行,否则延迟任务执行误差会比较大。</p>]]></content>
<summary type="html">
<p>最近在用Redssion做延迟队列,Redssion内部使用了HashedWheelTimer作为延迟队列, 做一下总结。</p>
<p><strong><em>HashedWheelTimer主要用来做近似的io调度</em></strong></p>
<h3 id="T
</summary>
</entry>
<entry>
<title>xxljob</title>
<link href="http://laywin.github.io/2021/07/23/xxljob/"/>
<id>http://laywin.github.io/2021/07/23/xxljob/</id>
<published>2021-07-23T06:03:49.000Z</published>
<updated>2023-10-08T14:12:40.437Z</updated>
<content type="html"><![CDATA[<h3 id="定时任务的两个要素"><a href="#定时任务的两个要素" class="headerlink" title="定时任务的两个要素"></a>定时任务的两个要素</h3><ul><li><p>trigger</p><p>定义定时任务调度的频率</p></li><li><p>job</p><p>定义任务本身</p></li></ul><p> 一个job可以关联多个trigger…</p><h3 id="快速开始"><a href="#快速开始" class="headerlink" title="快速开始"></a>快速开始</h3><ul><li>在xxljob 管理端配置job </li></ul><p> <img src="/2021/07/23/xxljob/4.png" alt></p><p> 1. 执行器</p><p> 相当于这个任务在哪被执行</p><p> 2. 路由策略</p><p> 根据路由策略选择在执行器中选择对应的服务器来执行定时任务</p><p> 3. Cron表达式</p><p> 定时任务执行的频率</p><p> 4. 运行模式</p><p> Job是怎样被定义的</p><p> 5. jobHandler</p><p> 基于前面Bean的运行模式,通过jobHandler的名字去定时任务执行的服务器上找到对应的job, 从而来执行</p><p> 6. 阻塞处理策略</p><p> 定任务运行的时间超过了两次定时任务调度的间隔后,处理的方式</p><p> 7. 失败重试次数</p><p> job运行失败后,重试的次数</p><ul><li>服务本地配置</li></ul><p> 1. maven依赖配置</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><dependency></span><br><span class="line"> <groupId>com.xuxueli</groupId></span><br><span class="line"> <artifactId>xxl-job-core</artifactId></span><br><span class="line"> <version>2.1.0</version></span><br><span class="line"></dependency></span><br></pre></td></tr></table></figure><p> 2. job 定义</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"> <span class="meta">@Component</span></span><br><span class="line"> <span class="meta">@JobHandler</span>(value=<span class="string">"demoJobHandler"</span>)</span><br><span class="line"> <span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SampleXxlJob</span> <span class="keyword">extends</span> <span class="title">IJobHandler</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> Logger logger = LoggerFactory.getLogger(SampleXxlJob<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> ReturnT<String> <span class="title">execute</span><span class="params">(String s)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> logger.info(<span class="string">"hello world"</span>);</span><br><span class="line"> <span class="keyword">return</span> ReturnT.SUCCESS;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p> 3. 配置</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">xxl.job.admin.addresses=http://127.0.0.1:8081/xxl-job-admin</span><br><span class="line"><span class="comment">### 执行器通讯TOKEN [选填]:非空时启用;</span></span><br><span class="line">xxl.job.accessToken=</span><br><span class="line"><span class="comment">### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册</span></span><br><span class="line">xxl.job.executor.appname=optional</span><br><span class="line"><span class="comment">### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。</span></span><br><span class="line">xxl.job.executor.address=</span><br><span class="line"><span class="comment">### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";</span></span><br><span class="line">xxl.job.executor.ip=</span><br><span class="line"><span class="comment">### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;</span></span><br><span class="line">xxl.job.executor.port=9999</span><br><span class="line"><span class="comment">### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;</span></span><br><span class="line">xxl.job.executor.logpath=xxl-job/jobhandler</span><br><span class="line"><span class="comment">### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;</span></span><br><span class="line">xxl.job.executor.logretentiondays=30</span><br></pre></td></tr></table></figure><h3 id="xxlJob"><a href="#xxlJob" class="headerlink" title="xxlJob"></a>xxlJob</h3><ul><li><p>架构</p><p><img src="/2021/07/23/xxljob/1.png" alt></p></li></ul><ul><li>执行模式 server-client</li></ul><p><img src="/2021/07/23/xxljob/2.png" alt></p><p>在job到达调度的时间点时,admin会将相关的job参数通过http的方式推送给执行器端,执行器端通过job参数在本地找到对应的job,然后执行.</p><ul><li><p>路由调度策略</p><ul><li><p>轮询</p></li><li><p>故障转移</p></li></ul></li></ul><p> 只针对调度结算选择执行器的服务器时,会首选发送一个探活请求,如果成功则选择对应的执行器.</p><ul><li><p>失败重试</p><p>针对job执行的失败重试,失败后,如果retry count > 0, admin会重新触发调度过程,进行执行.</p></li></ul><p> job失败的方式 (在job中通过返回ReturnT.FAIL并不能代表失败)</p><ul><li><p>在job中抛出异常</p></li><li><p>通过在job上下文中设置失败状态</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext();</span><br><span class="line">xxlJobContext.setHandleCode(XxlJobContext.HANDLE_COCE_FAIL);</span><br><span class="line">xxlJobContext.setHandleMsg(<span class="string">"exception occur"</span>);</span><br></pre></td></tr></table></figure></li></ul><ul><li><p>阻塞处理策略</p><ul><li><p>串行(默认)</p></li><li><p>丢弃后续</p><p>如果当前存在正在执行的任务,将要被执行的任务丢弃</p></li><li><p>覆盖之前</p><p>中断当前任务的线程,重新创新线程执行任务</p></li></ul></li><li><p>执行器服务器注册方式</p><ul><li><p>自动注册</p><p><img src="/2021/07/23/xxljob/3.png" alt></p></li></ul></li></ul><p> 执行器端会定时注册执行器服务器到admin, admin会通过超时时间来判断是否需要踢出对应的执行服务器,从而在定时任务调度的时候来让对应的服务器不被调度到.</p><ul><li>手动注册</li></ul><ul><li><p>推荐配置</p><p>自动注册 + 轮询的路由策略 + 串行的阻塞方式,失败重试看业务需要.</p></li><li><p>xxljob优点</p><ol><li>中心化配置管理</li><li>调度的执行和job的执行解耦</li><li>跨语言</li></ol></li><li><p>为什么基于http的调度不安全</p><p>80端口可以穿越防火墙,其它端口不对外暴露.</p></li></ul>]]></content>
<summary type="html">
<h3 id="定时任务的两个要素"><a href="#定时任务的两个要素" class="headerlink" title="定时任务的两个要素"></a>定时任务的两个要素</h3><ul>
<li><p>trigger</p>
<p>定义定时任务调度的频率</p>
</
</summary>
<category term="xxljob" scheme="http://laywin.github.io/tags/xxljob/"/>
</entry>
<entry>
<title>redis事务&cas</title>
<link href="http://laywin.github.io/2021/06/17/redis%E4%BA%8B%E5%8A%A1-cas/"/>
<id>http://laywin.github.io/2021/06/17/redis事务-cas/</id>
<published>2021-06-17T12:32:58.000Z</published>
<updated>2023-10-08T14:12:40.437Z</updated>
<content type="html"><![CDATA[<p>以下内容来自于对 <a href="https://redis.io/topics/transactions" target="_blank" rel="noopener">https://redis.io/topics/transactions</a> 的总结.</p><h3 id="redis-的事务"><a href="#redis-的事务" class="headerlink" title="redis 的事务"></a>redis 的事务</h3><p>redis的事务相当于将多个命令放到一个队列中,然后在执行,执行的过程中出现错误不进行回滚. 其中MULTI指令表示事务的开始,在MULTI到EXEC之间的指令都会入队,EXEC的执行会将入队的指令拿出来顺序执行。</p><ul><li><p>隔离性</p><blockquote><p>事务中的所有命令都被序列化并且被顺序执行,如果一个redis事务的正在执行过程当中,那么它不会同时执行其他客户端的请求。</p></blockquote></li><li><p>原子性</p><blockquote><p>要么所有的都执行,要么一个指令都不执行,如果一个客户端在调用EXEC指令之前丢失连接,那么什么都不会执行,如果EXEC指令被调用,那么所有指令都会被执行。</p></blockquote></li></ul><h4 id="事务执行过程中的错误"><a href="#事务执行过程中的错误" class="headerlink" title="事务执行过程中的错误"></a>事务执行过程中的错误</h4><p>在一个事务期间,可能遇到两种类型的错误</p><a id="more"></a><ul><li><p>失败入队的指令</p><blockquote><p>某些指令在执行EXEC之前入队出错,比如错误的参数个数,指令名称等</p></blockquote></li><li><p>执行EXEC指令之后失败</p><blockquote><p>比如对一个string的value执行list的操作(值类型问题)</p></blockquote></li></ul><p>第一种错误大多数客户端会丢弃入队的指令并且终止事务,如果客户端不管指令入队时发生的错误,还是将队列中的指令发送到了服务器,那么redis针对不同的版本有不同的处理方式,在redis 2.6.5之前会执行成功入队的指令,在2.6.5之后,服务器端会记住那儿有一个错,并会拒绝执行对应的事务。</p><p>第二种错误redis会执行所有的指令,即使执行的过程当中一些指令实行失败了。</p><h4 id="为什么redis不支持回滚"><a href="#为什么redis不支持回滚" class="headerlink" title="为什么redis不支持回滚"></a>为什么redis不支持回滚</h4><p>有两个原因redis团队觉得不应该支持回滚</p><ul><li>redis 指令在执行EXEC之后报错主要有两个原因,一个是语法错误,一个是类型错误,这两种错误应该被认为是程序的bug,应该在测试阶段解决,而不是在生产中出现,这种错误和关系型数据库的事务失败还有些区别,后者大多数情况是业务上的条件不满足。最后,即便redis支持回滚也解决不了问题。</li><li>redis不支持回滚而更加简单和快速</li></ul><h4 id="CAS-语意"><a href="#CAS-语意" class="headerlink" title="CAS 语意"></a>CAS 语意</h4><p>Redis 事务相当于给指令的执行加了一把独占锁,如果事务中执行的指令较多,必然会使得并发性能减弱,cas(check-and-set)则是一种更好的执行方式。redis中cas的执行过程是:WATCH…..MULTI….EXEC, 先watch某个key,如果这个key的值在EXEC指令执行之前被修改了,那么这个事务会失败,redis会返回一个空的值。</p>]]></content>
<summary type="html">
<p>以下内容来自于对 <a href="https://redis.io/topics/transactions" target="_blank" rel="noopener">https://redis.io/topics/transactions</a> 的总结.</p>
<h3 id="redis-的事务"><a href="#redis-的事务" class="headerlink" title="redis 的事务"></a>redis 的事务</h3><p>redis的事务相当于将多个命令放到一个队列中,然后在执行,执行的过程中出现错误不进行回滚. 其中MULTI指令表示事务的开始,在MULTI到EXEC之间的指令都会入队,EXEC的执行会将入队的指令拿出来顺序执行。</p>
<ul>
<li><p>隔离性</p>
<blockquote>
<p>事务中的所有命令都被序列化并且被顺序执行,如果一个redis事务的正在执行过程当中,那么它不会同时执行其他客户端的请求。</p>
</blockquote>
</li>
<li><p>原子性</p>
<blockquote>
<p>要么所有的都执行,要么一个指令都不执行,如果一个客户端在调用EXEC指令之前丢失连接,那么什么都不会执行,如果EXEC指令被调用,那么所有指令都会被执行。</p>
</blockquote>
</li>
</ul>
<h4 id="事务执行过程中的错误"><a href="#事务执行过程中的错误" class="headerlink" title="事务执行过程中的错误"></a>事务执行过程中的错误</h4><p>在一个事务期间,可能遇到两种类型的错误</p>
</summary>
<category term="redis事务" scheme="http://laywin.github.io/tags/redis%E4%BA%8B%E5%8A%A1/"/>
<category term="cas" scheme="http://laywin.github.io/tags/cas/"/>
</entry>
<entry>
<title>sleuth集成zipkin</title>
<link href="http://laywin.github.io/2021/06/11/sleuth%E9%9B%86%E6%88%90zipkin/"/>
<id>http://laywin.github.io/2021/06/11/sleuth集成zipkin/</id>
<published>2021-06-11T06:53:37.000Z</published>
<updated>2023-10-08T14:12:40.434Z</updated>
<content type="html"><![CDATA[<p>在[<a href="https://laywin.github.io/2021/06/10/基于sleuth的trace数据生成/">基于sleuth的trace数据生成</a>]中,我们生成了trace数据,接下来为了直观的展示链路追踪,我们需要将trace数据推送到zipkin中,过程很简单,第一步将zipkin搭建起来,第二步推送数据到zipkin.</p><h3 id="zipkin搭建"><a href="#zipkin搭建" class="headerlink" title="zipkin搭建"></a>zipkin搭建</h3><blockquote><p>docker run -d -p 9411:9411 openzipkin/zipkin</p></blockquote><p>启动之后,访问 <a href="http://localhost:9411/" target="_blank" rel="noopener">http://localhost:9411/</a> , 如下:</p><p><img src="/2021/06/11/sleuth集成zipkin/1.png" alt></p><a id="more"></a><h3 id="修改trace数据推送配置"><a href="#修改trace数据推送配置" class="headerlink" title="修改trace数据推送配置"></a>修改trace数据推送配置</h3><ul><li><p>添加sleuth集成zipkin的依赖</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><dependency></span><br><span class="line"> <groupId>org.springframework.cloud</groupId></span><br><span class="line"> <artifactId>spring-cloud-sleuth-zipkin</artifactId></span><br><span class="line"></dependency></span><br></pre></td></tr></table></figure></li><li><p>添加推送配置</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">spring.zipkin.baseUrl: http://localhost:9411/</span><br></pre></td></tr></table></figure></li></ul><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><p>重新请求endingpoint, 然后查询zipkin</p><p><img src="/2021/06/11/sleuth集成zipkin/2.png" alt></p><p><img src="/2021/06/11/sleuth集成zipkin/3.png" alt></p>]]></content>
<summary type="html">
<p>在[<a href="https://laywin.github.io/2021/06/10/基于sleuth的trace数据生成/">基于sleuth的trace数据生成</a>]中,我们生成了trace数据,接下来为了直观的展示链路追踪,我们需要将trace数据推送到zipkin中,过程很简单,第一步将zipkin搭建起来,第二步推送数据到zipkin.</p>
<h3 id="zipkin搭建"><a href="#zipkin搭建" class="headerlink" title="zipkin搭建"></a>zipkin搭建</h3><blockquote>
<p>docker run -d -p 9411:9411 openzipkin/zipkin</p>
</blockquote>
<p>启动之后,访问 <a href="http://localhost:9411/" target="_blank" rel="noopener">http://localhost:9411/</a> , 如下:</p>
<p><img src="/2021/06/11/sleuth集成zipkin/1.png" alt></p>
</summary>
<category term="zipkin" scheme="http://laywin.github.io/tags/zipkin/"/>
</entry>
<entry>
<title>基于sleuth的trace数据生成</title>
<link href="http://laywin.github.io/2021/06/10/%E5%9F%BA%E4%BA%8Esleuth%E7%9A%84trace%E6%95%B0%E6%8D%AE%E7%94%9F%E6%88%90/"/>
<id>http://laywin.github.io/2021/06/10/基于sleuth的trace数据生成/</id>
<published>2021-06-10T10:21:02.000Z</published>
<updated>2023-10-08T14:12:40.430Z</updated>
<content type="html"><![CDATA[<h3 id="快速开始"><a href="#快速开始" class="headerlink" title="快速开始"></a>快速开始</h3><p>只需要简单的几个步骤就能产生相应的trace数据</p><ul><li><p>添加maven依赖</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><dependencies></span><br><span class="line"> <dependency></span><br><span class="line"> <groupId>org.springframework.cloud</groupId></span><br><span class="line"> <artifactId>spring-cloud-starter-sleuth</artifactId></span><br><span class="line"> </dependency></span><br><span class="line"></dependencies></span><br><span class="line"><dependencyManagement></span><br><span class="line"> <dependencies></span><br><span class="line"> <dependency></span><br><span class="line"> <groupId>org.springframework.cloud</groupId></span><br><span class="line"> <artifactId>spring-cloud-dependencies</artifactId></span><br><span class="line"> <version>Hoxton.SR11</version></span><br><span class="line"> <<span class="built_in">type</span>>pom</<span class="built_in">type</span>></span><br><span class="line"> <scope>import</scope></span><br><span class="line"> </dependency></span><br><span class="line"> <dependency></span><br><span class="line"> <groupId>io.zipkin.brave</groupId></span><br><span class="line"> <artifactId>brave-bom</artifactId></span><br><span class="line"> <version><span class="variable">${brave.version}</span></version></span><br><span class="line"> <<span class="built_in">type</span>>pom</<span class="built_in">type</span>></span><br><span class="line"> <scope>import</scope></span><br><span class="line"> </dependency></span><br><span class="line"> </dependencies></span><br><span class="line"></dependencyManagement></span><br></pre></td></tr></table></figure></li><li><p>修改日志pattern</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">%d{ABSOLUTE} [%X{traceId}/%X{spanId}] %-5p [%t] %C{2} - %m%n</span><br></pre></td></tr></table></figure></li></ul><ul><li>访问我们的endingpoint,查看日志<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">2021-06-10 18:28:03,126 [/] INFO [main] s.d.s.w.p.DocumentationPluginsBootstrapper - Context refreshed</span><br><span class="line">2021-06-10 18:28:03,133 [/] INFO [main] s.d.s.w.p.DocumentationPluginsBootstrapper - Found 1 custom documentation plugin(s)</span><br><span class="line">2021-06-10 18:28:03,156 [/] INFO [main] s.d.s.w.s.ApiListingReferenceScanner - Scanning <span class="keyword">for</span> api listing references</span><br><span class="line">2021-06-10 18:28:03,258 [/] INFO [main] o.a.j.l.DirectJDKLog - Starting ProtocolHandler [<span class="string">"http-nio-8080"</span>]</span><br><span class="line">2021-06-10 18:28:03,285 [/] INFO [main] o.s.b.w.e.t.TomcatWebServer - Tomcat started on port(s): 8080 (http) with context path <span class="string">''</span></span><br><span class="line">2021-06-10 18:28:03,289 [/] INFO [main] o.s.b.StartupInfoLogger - Started Application <span class="keyword">in</span> 3.163 seconds (JVM running <span class="keyword">for</span> 3.809)</span><br><span class="line">2021-06-10 18:28:06,845 [/] INFO [http-nio-8080-exec-1] o.a.j.l.DirectJDKLog - Initializing Spring DispatcherServlet <span class="string">'dispatcherServlet'</span></span><br><span class="line">2021-06-10 18:28:06,845 [/] INFO [http-nio-8080-exec-1] o.s.w.s.FrameworkServlet - Initializing Servlet <span class="string">'dispatcherServlet'</span></span><br><span class="line">2021-06-10 18:28:06,855 [/] INFO [http-nio-8080-exec-1] o.s.w.s.FrameworkServlet - Completed initialization <span class="keyword">in</span> 10 ms</span><br><span class="line">2021-06-10 18:28:06,880 [04cb10dbe7166b21/04cb10dbe7166b21] INFO [http-nio-8080-exec-1] o.e.t.Application - tracing</span><br></pre></td></tr></table></figure></li></ul><p> 可以看到日志最后一行,traceId已经生成了,但是目前并没有生成trace数据</p> <a id="more"></a><ul><li><p>添加reporter配置</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Reporter<Span> <span class="title">reporter</span><span class="params">()</span></span>{</span><br><span class="line"><span class="keyword">return</span> span -> logger.info(span.toString());</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><p> 相当于将trace数据打印在本地</p><p> 重新访问一下endingpoint,生成了一条日志如下:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">2021-06-10 18:34:25,696 [842dc5405ac67d2a/842dc5405ac67d2a] INFO [http-nio-8080-exec-1] o.e.t.c.CommonConfig - {<span class="string">"traceId"</span>:<span class="string">"842dc5405ac67d2a"</span>,<span class="string">"id"</span>:<span class="string">"842dc5405ac67d2a"</span>,<span class="string">"kind"</span>:<span class="string">"SERVER"</span>,<span class="string">"name"</span>:<span class="string">"get /"</span>,<span class="string">"timestamp"</span>:1623321265653239,<span class="string">"duration"</span>:34273,<span class="string">"localEndpoint"</span>:{<span class="string">"serviceName"</span>:<span class="string">"default"</span>,<span class="string">"ipv4"</span>:<span class="string">"10.13.2.101"</span>},<span class="string">"remoteEndpoint"</span>:{<span class="string">"ipv6"</span>:<span class="string">"::1"</span>,<span class="string">"port"</span>:50245},<span class="string">"tags"</span>:{<span class="string">"http.method"</span>:<span class="string">"GET"</span>,<span class="string">"http.path"</span>:<span class="string">"/"</span>,<span class="string">"mvc.controller.class"</span>:<span class="string">"Application"</span>,<span class="string">"mvc.controller.method"</span>:<span class="string">"index"</span>}}</span><br></pre></td></tr></table></figure><p> 将trace数据拿出来format一下</p> <figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"> {</span><br><span class="line"> <span class="attr">"traceId"</span>: <span class="string">"842dc5405ac67d2a"</span>,</span><br><span class="line"> <span class="attr">"id"</span>: <span class="string">"842dc5405ac67d2a"</span>,</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"SERVER"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"get /"</span>,</span><br><span class="line"> <span class="attr">"timestamp"</span>: <span class="number">1623321265653239</span>,</span><br><span class="line"> <span class="attr">"duration"</span>: <span class="number">34273</span>,</span><br><span class="line"> <span class="attr">"localEndpoint"</span>: {</span><br><span class="line"> <span class="attr">"serviceName"</span>: <span class="string">"default"</span>,</span><br><span class="line"> <span class="attr">"ipv4"</span>: <span class="string">"10.13.2.101"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"remoteEndpoint"</span>: {</span><br><span class="line"> <span class="attr">"ipv6"</span>: <span class="string">"::1"</span>,</span><br><span class="line"> <span class="attr">"port"</span>: <span class="number">50245</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"tags"</span>: {</span><br><span class="line"> <span class="attr">"http.method"</span>: <span class="string">"GET"</span>,</span><br><span class="line"> <span class="attr">"http.path"</span>: <span class="string">"/"</span>,</span><br><span class="line"> <span class="attr">"mvc.controller.class"</span>: <span class="string">"Application"</span>,</span><br><span class="line"> <span class="attr">"mvc.controller.method"</span>: <span class="string">"index"</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p> tags代表相关的endingpoint信息,duration代表这次请求消耗的时间为34273微秒</p><h3 id="复杂一点"><a href="#复杂一点" class="headerlink" title="复杂一点"></a>复杂一点</h3><p>我们让一个trace里面有两个span,在endingpoint中我们去请求百度的页面,如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">index</span><span class="params">()</span> </span>{</span><br><span class="line">logger.info(<span class="string">"tracing"</span>);</span><br><span class="line"><span class="keyword">return</span> restTemplate.getForObject(<span class="string">"https://www.baidu.com"</span>, String<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>访问endingpoint, 产生了两条trace数据</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">2021-06-10 20:25:41,368 [d98dfd05c5ed8282/d98dfd05c5ed8282] INFO [http-nio-8080-exec-1] o.e.t.c.CommonConfig - {<span class="string">"traceId"</span>:<span class="string">"d98dfd05c5ed8282"</span>,<span class="string">"parentId"</span>:<span class="string">"d98dfd05c5ed8282"</span>,<span class="string">"id"</span>:<span class="string">"13e802e1f9dd19bc"</span>,<span class="string">"kind"</span>:<span class="string">"CLIENT"</span>,<span class="string">"name"</span>:<span class="string">"get"</span>,<span class="string">"timestamp"</span>:1623327940939897,<span class="string">"duration"</span>:419782,<span class="string">"localEndpoint"</span>:{<span class="string">"serviceName"</span>:<span class="string">"default"</span>,<span class="string">"ipv4"</span>:<span class="string">"10.13.2.101"</span>},<span class="string">"tags"</span>:{<span class="string">"http.method"</span>:<span class="string">"GET"</span>,<span class="string">"http.path"</span>:<span class="string">"/"</span>}}</span><br><span class="line">2021-06-10 20:25:41,390 [d98dfd05c5ed8282/d98dfd05c5ed8282] INFO [http-nio-8080-exec-1] o.e.t.c.CommonConfig - {<span class="string">"traceId"</span>:<span class="string">"d98dfd05c5ed8282"</span>,<span class="string">"id"</span>:<span class="string">"d98dfd05c5ed8282"</span>,<span class="string">"kind"</span>:<span class="string">"SERVER"</span>,<span class="string">"name"</span>:<span class="string">"get /"</span>,<span class="string">"timestamp"</span>:1623327940910964,<span class="string">"duration"</span>:477309,<span class="string">"localEndpoint"</span>:{<span class="string">"serviceName"</span>:<span class="string">"default"</span>,<span class="string">"ipv4"</span>:<span class="string">"10.13.2.101"</span>},<span class="string">"remoteEndpoint"</span>:{<span class="string">"ipv6"</span>:<span class="string">"::1"</span>,<span class="string">"port"</span>:50764},<span class="string">"tags"</span>:{<span class="string">"http.method"</span>:<span class="string">"GET"</span>,<span class="string">"http.path"</span>:<span class="string">"/"</span>,<span class="string">"mvc.controller.class"</span>:<span class="string">"Application"</span>,<span class="string">"mvc.controller.method"</span>:<span class="string">"index"</span>}}</span><br></pre></td></tr></table></figure><p>前一条数据是restTemplate生成的,后一条是servlet生成的,他们的traceId相同(代表同一个请求下),spanId不同,第一个span的id和traceId一样。</p><h3 id="接下来"><a href="#接下来" class="headerlink" title="接下来"></a>接下来</h3><blockquote><p>前面我们已经生成 traceId/spanId, 并且也在本地打印了trace数据,接下来我们要看下sleuth是如何做到的.</p></blockquote><ul><li>寻找自动配置</li></ul><p> 往往我们能够通过自动配置找到一些关键信息,我们在maven配置中依赖了spring-cloud-starter-sleuth来实现trace数据的生成,可以发现spring-cloud-starter-sleuth主要依赖了spring-cloud-sleuth-core,通过spring.factories来找到相关的自动配置项,如下:</p><p> <img src="/2021/06/10/基于sleuth的trace数据生成/1.png" alt></p><p> 大概率猜到TraceWebServletAutoConfiguration是servlet开启新trace的入口,TraceWebServletAutoConfiguration会自动配置 TracingFilter,TracingFilter如下:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">TracingFilter</span> <span class="keyword">implements</span> <span class="title">Filter</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">//...省略非关键部分</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doFilter</span><span class="params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> IOException, ServletException </span>{</span><br><span class="line"> HttpServletRequest req = (HttpServletRequest) request;</span><br><span class="line"> HttpServletResponse res = servlet.httpServletResponse(response);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Prevent duplicate spans for the same request</span></span><br><span class="line"> TraceContext context = (TraceContext) request.getAttribute(TraceContext<span class="class">.<span class="keyword">class</span>.<span class="title">getName</span>())</span>;</span><br><span class="line"> <span class="keyword">if</span> (context != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// A forwarded request might end up on another thread, so make sure it is scoped</span></span><br><span class="line"> Scope scope = currentTraceContext.maybeScope(context);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> chain.doFilter(request, response);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> scope.close();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> Span span = handler.handleReceive(<span class="keyword">new</span> HttpServletRequestWrapper(req));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Add attributes for explicit access to customization or span context</span></span><br><span class="line"> request.setAttribute(SpanCustomizer<span class="class">.<span class="keyword">class</span>.<span class="title">getName</span>(), <span class="title">span</span>.<span class="title">customizer</span>())</span>;</span><br><span class="line"> request.setAttribute(TraceContext<span class="class">.<span class="keyword">class</span>.<span class="title">getName</span>(), <span class="title">span</span>.<span class="title">context</span>())</span>;</span><br><span class="line"> SendHandled sendHandled = <span class="keyword">new</span> SendHandled();</span><br><span class="line"> request.setAttribute(SendHandled<span class="class">.<span class="keyword">class</span>.<span class="title">getName</span>(), <span class="title">sendHandled</span>)</span>;</span><br><span class="line"></span><br><span class="line"> Throwable error = <span class="keyword">null</span>;</span><br><span class="line"> Scope scope = currentTraceContext.newScope(span.context());</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// any downstream code can see Tracer.currentSpan() or use Tracer.currentSpanCustomizer()</span></span><br><span class="line"> chain.doFilter(req, res);</span><br><span class="line"> } <span class="keyword">catch</span> (Throwable e) {</span><br><span class="line"> error = e;</span><br><span class="line"> <span class="keyword">throw</span> e;</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> <span class="comment">// When async, even if we caught an exception, we don't have the final response: defer</span></span><br><span class="line"> <span class="keyword">if</span> (servlet.isAsync(req)) {</span><br><span class="line"> servlet.handleAsync(handler, req, res, span);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (sendHandled.compareAndSet(<span class="keyword">false</span>, <span class="keyword">true</span>)){</span><br><span class="line"> <span class="comment">// we have a synchronous response or error: finish the span</span></span><br><span class="line"> HttpServerResponse responseWrapper = HttpServletResponseWrapper.create(req, res, error);</span><br><span class="line"> handler.handleSend(responseWrapper, span);</span><br><span class="line"> }</span><br><span class="line"> scope.close();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p> 该filter大概处理逻辑是通过TraceContext看当前是否处于一个scope,如果是继续之前的scope,创建新的span,开启一个新的scope(将span相关的数据放到threadlocal中)</p><p> 在restTemplate的执行中,会执行一个TracingClientHttpRequestInterceptor的拦截器如下:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">TracingClientHttpRequestInterceptor</span> <span class="keyword">implements</span> <span class="title">ClientHttpRequestInterceptor</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> </span><br><span class="line"> <span class="function"><span class="keyword">public</span> ClientHttpResponse <span class="title">intercept</span><span class="params">(HttpRequest req, <span class="keyword">byte</span>[] body,</span></span></span><br><span class="line"><span class="function"><span class="params"> ClientHttpRequestExecution execution)</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> HttpRequestWrapper request = <span class="keyword">new</span> HttpRequestWrapper(req);</span><br><span class="line"> Span span = handler.handleSend(request);</span><br><span class="line"> ClientHttpResponse response = <span class="keyword">null</span>;</span><br><span class="line"> Throwable error = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">try</span> (Scope ws = currentTraceContext.newScope(span.context())) {</span><br><span class="line"> <span class="keyword">return</span> response = execution.execute(req, body);</span><br><span class="line"> } <span class="keyword">catch</span> (Throwable e) {</span><br><span class="line"> error = e;</span><br><span class="line"> <span class="keyword">throw</span> e;</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> handler.handleReceive(<span class="keyword">new</span> ClientHttpResponseWrapper(request, response, error), span);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p> 该拦截器会开启一个新的scope并在这个scope中执行http请求,而这个拦截器也可以发现在上面的TraceWebClientAutoConfiguration配置类中配置的</p><h3 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h3><p>sleuth是基于拦截器的方式来生成trace数据的</p>]]></content>
<summary type="html">
<h3 id="快速开始"><a href="#快速开始" class="headerlink" title="快速开始"></a>快速开始</h3><p>只需要简单的几个步骤就能产生相应的trace数据</p>
<ul>
<li><p>添加maven依赖</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependencies&gt;</span><br><span class="line"> &lt;dependency&gt;</span><br><span class="line"> &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line"> &lt;artifactId&gt;spring-cloud-starter-sleuth&lt;/artifactId&gt;</span><br><span class="line"> &lt;/dependency&gt;</span><br><span class="line">&lt;/dependencies&gt;</span><br><span class="line">&lt;dependencyManagement&gt;</span><br><span class="line"> &lt;dependencies&gt;</span><br><span class="line"> &lt;dependency&gt;</span><br><span class="line"> &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line"> &lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;</span><br><span class="line"> &lt;version&gt;Hoxton.SR11&lt;/version&gt;</span><br><span class="line"> &lt;<span class="built_in">type</span>&gt;pom&lt;/<span class="built_in">type</span>&gt;</span><br><span class="line"> &lt;scope&gt;import&lt;/scope&gt;</span><br><span class="line"> &lt;/dependency&gt;</span><br><span class="line"> &lt;dependency&gt;</span><br><span class="line"> &lt;groupId&gt;io.zipkin.brave&lt;/groupId&gt;</span><br><span class="line"> &lt;artifactId&gt;brave-bom&lt;/artifactId&gt;</span><br><span class="line"> &lt;version&gt;<span class="variable">$&#123;brave.version&#125;</span>&lt;/version&gt;</span><br><span class="line"> &lt;<span class="built_in">type</span>&gt;pom&lt;/<span class="built_in">type</span>&gt;</span><br><span class="line"> &lt;scope&gt;import&lt;/scope&gt;</span><br><span class="line"> &lt;/dependency&gt;</span><br><span class="line"> &lt;/dependencies&gt;</span><br><span class="line">&lt;/dependencyManagement&gt;</span><br></pre></td></tr></table></figure>
</li>
<li><p>修改日志pattern</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">%d&#123;ABSOLUTE&#125; [%X&#123;traceId&#125;/%X&#123;spanId&#125;] %-5p [%t] %C&#123;2&#125; - %m%n</span><br></pre></td></tr></table></figure>
</li>
</ul>
<ul>
<li>访问我们的endingpoint,查看日志<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">2021-06-10 18:28:03,126 [/] INFO [main] s.d.s.w.p.DocumentationPluginsBootstrapper - Context refreshed</span><br><span class="line">2021-06-10 18:28:03,133 [/] INFO [main] s.d.s.w.p.DocumentationPluginsBootstrapper - Found 1 custom documentation plugin(s)</span><br><span class="line">2021-06-10 18:28:03,156 [/] INFO [main] s.d.s.w.s.ApiListingReferenceScanner - Scanning <span class="keyword">for</span> api listing references</span><br><span class="line">2021-06-10 18:28:03,258 [/] INFO [main] o.a.j.l.DirectJDKLog - Starting ProtocolHandler [<span class="string">"http-nio-8080"</span>]</span><br><span class="line">2021-06-10 18:28:03,285 [/] INFO [main] o.s.b.w.e.t.TomcatWebServer - Tomcat started on port(s): 8080 (http) with context path <span class="string">''</span></span><br><span class="line">2021-06-10 18:28:03,289 [/] INFO [main] o.s.b.StartupInfoLogger - Started Application <span class="keyword">in</span> 3.163 seconds (JVM running <span class="keyword">for</span> 3.809)</span><br><span class="line">2021-06-10 18:28:06,845 [/] INFO [http-nio-8080-exec-1] o.a.j.l.DirectJDKLog - Initializing Spring DispatcherServlet <span class="string">'dispatcherServlet'</span></span><br><span class="line">2021-06-10 18:28:06,845 [/] INFO [http-nio-8080-exec-1] o.s.w.s.FrameworkServlet - Initializing Servlet <span class="string">'dispatcherServlet'</span></span><br><span class="line">2021-06-10 18:28:06,855 [/] INFO [http-nio-8080-exec-1] o.s.w.s.FrameworkServlet - Completed initialization <span class="keyword">in</span> 10 ms</span><br><span class="line">2021-06-10 18:28:06,880 [04cb10dbe7166b21/04cb10dbe7166b21] INFO [http-nio-8080-exec-1] o.e.t.Application - tracing</span><br></pre></td></tr></table></figure>
</li>
</ul>
<p> 可以看到日志最后一行,traceId已经生成了,但是目前并没有生成trace数据</p>
</summary>
<category term="分布书追踪" scheme="http://laywin.github.io/tags/%E5%88%86%E5%B8%83%E4%B9%A6%E8%BF%BD%E8%B8%AA/"/>
<category term="apm" scheme="http://laywin.github.io/tags/apm/"/>
</entry>
<entry>
<title>mybatis-sql输出小插曲</title>
<link href="http://laywin.github.io/2021/05/26/mybatis-sql%E8%BE%93%E5%87%BA%E5%B0%8F%E6%8F%92%E6%9B%B2/"/>
<id>http://laywin.github.io/2021/05/26/mybatis-sql输出小插曲/</id>
<published>2021-05-26T12:44:49.000Z</published>
<updated>2023-10-08T14:12:40.429Z</updated>
<content type="html"><![CDATA[<p> 背景是这样的,笔者到了一家新公司,拿到了一个老项目,通过交接和粗略的view发现项目有点乱,二话不说,先把工程跑起来……看到了一块涉及到库操作的业务逻辑,发现控制台没打印sql,还是二话不说,加上了如下配置:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl</span><br></pre></td></tr></table></figure><p>很显然sql日志会输出到控制台.后来同事反馈给我说sql文件没sql输出了,问是不是这个添加导致的,然后笔者就开启了本文的debug.</p><p>首先笔者找到了日志配置文件,发现本身有mapper的日志配置,如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><logger name="com.*.*.mapper" level="DEBUG" additivity="false"></span><br><span class="line"> <appender-ref ref="SQL"/></span><br><span class="line"></logger></span><br></pre></td></tr></table></figure><p>这个配置表示”com.*.*.mapper” logger 的日志级别为DEBUG并输出到对应的appender里面,这个配置是如何使得执行的sql得以输出了?翻了一下mybatis的代码,咋一看并没有发现哪个地方有SQL打印, 通过反向定位日志关键字Preparing,发现在ConnectionLogger中有相关输出:</p><a id="more"></a><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">ConnectionLogger</span> <span class="keyword">extends</span> <span class="title">BaseJdbcLogger</span> <span class="keyword">implements</span> <span class="title">InvocationHandler</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Connection connection;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="title">ConnectionLogger</span><span class="params">(Connection conn, Log statementLog, <span class="keyword">int</span> queryStack)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(statementLog, queryStack);</span><br><span class="line"> <span class="keyword">this</span>.connection = conn;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Object <span class="title">invoke</span><span class="params">(Object proxy, Method method, Object[] params)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> Throwable </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (Object<span class="class">.<span class="keyword">class</span>.<span class="title">equals</span>(<span class="title">method</span>.<span class="title">getDeclaringClass</span>())) </span>{</span><br><span class="line"> <span class="keyword">return</span> method.invoke(<span class="keyword">this</span>, params);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"prepareStatement"</span>.equals(method.getName())) {</span><br><span class="line"> <span class="keyword">if</span> (isDebugEnabled()) {</span><br><span class="line"> debug(<span class="string">" Preparing: "</span> + removeBreakingWhitespace((String) params[<span class="number">0</span>]), <span class="keyword">true</span>);</span><br><span class="line"> }</span><br><span class="line"> PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);</span><br><span class="line"> stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);</span><br><span class="line"> <span class="keyword">return</span> stmt;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">"prepareCall"</span>.equals(method.getName())) {</span><br><span class="line"> <span class="keyword">if</span> (isDebugEnabled()) {</span><br><span class="line"> debug(<span class="string">" Preparing: "</span> + removeBreakingWhitespace((String) params[<span class="number">0</span>]), <span class="keyword">true</span>);</span><br><span class="line"> }</span><br><span class="line"> ... <span class="comment">//省略非关键部分</span></span><br><span class="line"> } <span class="keyword">catch</span> (Throwable t) {</span><br><span class="line"> <span class="keyword">throw</span> ExceptionUtil.unwrapThrowable(t);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Creates a logging version of a connection.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> conn - the original connection</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> - the connection with logging</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Connection <span class="title">newInstance</span><span class="params">(Connection conn, Log statementLog, <span class="keyword">int</span> queryStack)</span> </span>{</span><br><span class="line"> InvocationHandler handler = <span class="keyword">new</span> ConnectionLogger(conn, statementLog, queryStack);</span><br><span class="line"> ClassLoader cl = Connection<span class="class">.<span class="keyword">class</span>.<span class="title">getClassLoader</span>()</span>;</span><br><span class="line"> <span class="keyword">return</span> (Connection) Proxy.newProxyInstance(cl, <span class="keyword">new</span> Class[]{Connection<span class="class">.<span class="keyword">class</span>}, <span class="title">handler</span>)</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * return the wrapped connection.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> the connection</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Connection <span class="title">getConnection</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> connection;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">BaseJdbcLogger</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">//...省略非关键部分</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">debug</span><span class="params">(String text, <span class="keyword">boolean</span> input)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (statementLog.isDebugEnabled()) {</span><br><span class="line"> statementLog.debug(prefix(input) + text);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">trace</span><span class="params">(String text, <span class="keyword">boolean</span> input)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (statementLog.isTraceEnabled()) {</span><br><span class="line"> statementLog.trace(prefix(input) + text);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>ConnectionLogger是一个jdk 动态代理 InvocationHandler的一个实现类,mybatis创建了connection的一个动态代理,当调用connection.prepareStatement等方法时,如果日志级别是DEBUG就会输出对应的日志,不得不说mybatis对动态代理的应用很巧妙(还有mapper的动态代理,在MybatisMapperProxyFactory中)。</p><p>到目前为止,我们找到了mybatis sql打印的位置,实际上是通过BaseJdbcLogger中的statementLog来进行打印的,这个statementLog和我们前面”com.*.*.mapper” logger配置有什么关系了?继续向上debug:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <E> <span class="function">List<E> <span class="title">doQuery</span><span class="params">(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)</span> <span class="keyword">throws</span> SQLException </span>{</span><br><span class="line"> Statement stmt = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> Configuration configuration = ms.getConfiguration();</span><br><span class="line"> StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);</span><br><span class="line"> stmt = prepareStatement(handler, ms.getStatementLog(), <span class="keyword">false</span>);</span><br><span class="line"> <span class="keyword">return</span> stmt == <span class="keyword">null</span> ? Collections.emptyList() : handler.query(stmt, resultHandler);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> closeStatement(stmt);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>发现这个statementLog是从MappedStatement(映射的SQL语句相关配置)中来的,这时我们需要回到MappedStatement创建的地方:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">MappedStatement</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">//... 省略非关键部分</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">Builder</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> MappedStatement mappedStatement = <span class="keyword">new</span> MappedStatement();</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">Builder</span><span class="params">(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType)</span> </span>{</span><br><span class="line"> <span class="comment">//... 省略非关键部分</span></span><br><span class="line"> String logId = id;</span><br><span class="line"> <span class="keyword">if</span> (configuration.getLogPrefix() != <span class="keyword">null</span>) {</span><br><span class="line"> logId = configuration.getLogPrefix() + id;</span><br><span class="line"> }</span><br><span class="line"> mappedStatement.statementLog = LogFactory.getLog(logId);</span><br><span class="line"> mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MapperBuilderAssistant</span> <span class="keyword">extends</span> <span class="title">BaseBuilder</span></span>{</span><br><span class="line"><span class="function"><span class="keyword">public</span> MappedStatement <span class="title">addMappedStatement</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> ...)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">//...省略非关键部分</span></span><br><span class="line"> id = applyCurrentNamespace(id, <span class="keyword">false</span>);</span><br><span class="line"> <span class="keyword">boolean</span> isSelect = sqlCommandType == SqlCommandType.SELECT;</span><br><span class="line"></span><br><span class="line"> MappedStatement.Builder statementBuilder = <span class="keyword">new</span> MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)</span><br><span class="line"> .resource(resource)</span><br><span class="line"> .fetchSize(fetchSize)</span><br><span class="line"> .timeout(timeout)</span><br><span class="line"> .statementType(statementType)</span><br><span class="line"> .keyGenerator(keyGenerator)</span><br><span class="line"> .keyProperty(keyProperty)</span><br><span class="line"> .keyColumn(keyColumn)</span><br><span class="line"> .databaseId(databaseId)</span><br><span class="line"> .lang(lang)</span><br><span class="line"> .resultOrdered(resultOrdered)</span><br><span class="line"> .resultSets(resultSets)</span><br><span class="line"> .resultMaps(getStatementResultMaps(resultMap, resultType, id))</span><br><span class="line"> .resultSetType(resultSetType)</span><br><span class="line"> .flushCacheRequired(valueOrDefault(flushCache, !isSelect))</span><br><span class="line"> .useCache(valueOrDefault(useCache, isSelect))</span><br><span class="line"> .cache(currentCache);</span><br><span class="line"></span><br><span class="line"> ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);</span><br><span class="line"> <span class="keyword">if</span> (statementParameterMap != <span class="keyword">null</span>) {</span><br><span class="line"> statementBuilder.parameterMap(statementParameterMap);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> MappedStatement statement = statementBuilder.build();</span><br><span class="line"> configuration.addMappedStatement(statement);</span><br><span class="line"> <span class="keyword">return</span> statement;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p> 通过上面的代码发现MappedStatement中的id就是namespace + method拼接起来的,所以对mapper logger的配置能最终影响到SQL的输出.</p><p>最后我们来看一下</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl</span><br></pre></td></tr></table></figure><p>这个配置对SQL输出的影响.</p><p>首先StdOutImpl是一个标准输出,然后对mybatis-plus.configuration.log-impl的配置,实际上会应用到Configuration中如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Configuration</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setLogImpl</span><span class="params">(Class<? extends Log> logImpl)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (logImpl != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">this</span>.logImpl = logImpl;</span><br><span class="line"> LogFactory.useCustomLogging(<span class="keyword">this</span>.logImpl);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>LogFactory.useCustomLogging会设置日志实现的方式,默认情况下mybatis会尝试各种标准实现如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">LogFactory</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Marker to be used by logging implementations that support markers.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String MARKER = <span class="string">"MYBATIS"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> Constructor<? extends Log> logConstructor;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> {</span><br><span class="line"> tryImplementation(LogFactory::useSlf4jLogging);</span><br><span class="line"> tryImplementation(LogFactory::useCommonsLogging);</span><br><span class="line"> tryImplementation(LogFactory::useLog4J2Logging);</span><br><span class="line"> tryImplementation(LogFactory::useLog4JLogging);</span><br><span class="line"> tryImplementation(LogFactory::useJdkLogging);</span><br><span class="line"> tryImplementation(LogFactory::useNoLogging);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//...省略非关键部分</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>而StdOutImpl的实现会将代码中的标准实现替换为标准输出,尽管我们有mapper的logger配置,但并不会用到.</p>]]></content>
<summary type="html">
<p> 背景是这样的,笔者到了一家新公司,拿到了一个老项目,通过交接和粗略的view发现项目有点乱,二话不说,先把工程跑起来……看到了一块涉及到库操作的业务逻辑,发现控制台没打印sql,还是二话不说,加上了如下配置:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl</span><br></pre></td></tr></table></figure>
<p>很显然sql日志会输出到控制台.后来同事反馈给我说sql文件没sql输出了,问是不是这个添加导致的,然后笔者就开启了本文的debug.</p>
<p>首先笔者找到了日志配置文件,发现本身有mapper的日志配置,如下:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;logger name=&quot;com.*.*.mapper&quot; level=&quot;DEBUG&quot; additivity=&quot;false&quot;&gt;</span><br><span class="line"> &lt;appender-ref ref=&quot;SQL&quot;/&gt;</span><br><span class="line">&lt;/logger&gt;</span><br></pre></td></tr></table></figure>
<p>这个配置表示”com.*.*.mapper” logger 的日志级别为DEBUG并输出到对应的appender里面,这个配置是如何使得执行的sql得以输出了?翻了一下mybatis的代码,咋一看并没有发现哪个地方有SQL打印, 通过反向定位日志关键字Preparing,发现在ConnectionLogger中有相关输出:</p>
</summary>
</entry>
<entry>
<title>为什么我的日志输入无效</title>
<link href="http://laywin.github.io/2021/05/26/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84%E6%97%A5%E5%BF%97%E8%BE%93%E5%85%A5%E6%97%A0%E6%95%88/"/>
<id>http://laywin.github.io/2021/05/26/为什么我的日志输入无效/</id>
<published>2021-05-26T12:00:51.000Z</published>
<updated>2023-10-08T14:12:40.429Z</updated>
<content type="html"><![CDATA[<p>背景是这样的,我写了一个spring boot的应用监听器,但是部分日志无法输出,直接上代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyApplicationListener</span> <span class="keyword">implements</span> <span class="title">SmartApplicationListener</span> </span>{</span><br><span class="line"><span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">supportsEventType</span><span class="params">(Class<? extends ApplicationEvent> eventType)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> ApplicationStartingEvent<span class="class">.<span class="keyword">class</span>.<span class="title">isAssignableFrom</span>(<span class="title">eventType</span>)</span></span><br><span class="line"><span class="class"> || <span class="title">ApplicationStartedEvent</span>.<span class="title">class</span>.<span class="title">isAssignableFrom</span>(<span class="title">eventType</span>)</span>;</span><br><span class="line">}</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onApplicationEvent</span><span class="params">(ApplicationEvent event)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (event <span class="keyword">instanceof</span> ApplicationStartingEvent) {</span><br><span class="line">logger.info(<span class="string">"application starting"</span>);</span><br><span class="line">} <span class="keyword">else</span> <span class="keyword">if</span> (event <span class="keyword">instanceof</span> ApplicationStartedEvent) {</span><br><span class="line">logger.info(<span class="string">"application started"</span>);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>日志输出如下:</p><a id="more"></a><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">2021-04-19 21:46:49.637 INFO 3231 --- [ main] com.myspring.sprintboottest.Application : No active profile <span class="built_in">set</span>, falling back to default profiles: default</span><br><span class="line">2021-04-19 21:46:50.176 INFO 3231 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)</span><br><span class="line">2021-04-19 21:46:50.182 INFO 3231 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]</span><br><span class="line">2021-04-19 21:46:50.182 INFO 3231 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.39]</span><br><span class="line">2021-04-19 21:46:50.227 INFO 3231 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext</span><br><span class="line">2021-04-19 21:46:50.227 INFO 3231 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed <span class="keyword">in</span> 561 ms</span><br><span class="line">2021-04-19 21:46:50.346 INFO 3231 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService <span class="string">'applicationTaskExecutor'</span></span><br><span class="line">2021-04-19 21:46:50.458 INFO 3231 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path <span class="string">''</span></span><br><span class="line">2021-04-19 21:46:50.466 INFO 3231 --- [ main] com.myspring.sprintboottest.Application : Started Application <span class="keyword">in</span> 6.435 seconds (JVM running <span class="keyword">for</span> 6.922)</span><br><span class="line">2021-04-19 21:46:53.525 INFO 3231 --- [ main] c.m.s.MyApplicationListener : application started</span><br></pre></td></tr></table></figure><p>可以看到<strong>application started</strong>输出了,<strong>application starting</strong>并没有输出,为什么了……</p><p>原因在于spring boot是在EnvironmentPrepared阶段进行日志初始化的,在ApplicationStarting和EnvironmentPrepared阶段之间是无法进行日志输出的。具体日志的初始化入口是在LoggingApplicationListener中完成的,代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">LoggingApplicationListener</span> </span>{</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + <span class="number">20</span>;</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onApplicationEvent</span><span class="params">(ApplicationEvent event)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (event <span class="keyword">instanceof</span> ApplicationStartingEvent) {</span><br><span class="line">onApplicationStartingEvent((ApplicationStartingEvent) event);</span><br><span class="line">} <span class="keyword">else</span> <span class="keyword">if</span> (event <span class="keyword">instanceof</span> ApplicationEnvironmentPreparedEvent) {</span><br><span class="line">onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);</span><br><span class="line">}</span><br><span class="line"><span class="comment">//省略非关键部分...</span></span><br><span class="line">}</span><br><span class="line"><span class="comment">//省略非关键部分...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在ApplicationStarting阶段,spring boot会禁止日志输出, EnvironmentPrepared阶段会放行日志输出,代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">LogbackLoggingSystem</span> <span class="keyword">extends</span> <span class="title">Slf4JLoggingSystem</span></span>{</span><br><span class="line"></span><br><span class="line"><span class="comment">// 禁止日志输出过滤器</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> TurboFilter FILTER = <span class="keyword">new</span> TurboFilter() {</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> FilterReply <span class="title">decide</span><span class="params">(Marker marker, ch.qos.logback.classic.Logger logger, Level level, String format,</span></span></span><br><span class="line"><span class="function"><span class="params">Object[] params, Throwable t)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> FilterReply.DENY;</span><br><span class="line">}</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">beforeInitialize</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="comment">//省略非关键部分...</span></span><br><span class="line">loggerContext.getTurboFilterList().add(FILTER);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">initialize</span><span class="params">(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile)</span> </span>{</span><br><span class="line"><span class="comment">//省略非关键部分...</span></span><br><span class="line">loggerContext.getTurboFilterList().remove(FILTER);</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当然,这个过程还要结合LoggingApplicationListener的order, 默认情况下是Ordered.HIGHEST_PRECEDENCE + 20,如果咱的listener比LoggingApplicationListener的order优先级高,那么<strong><em>application starting</em></strong>可以打印,但打印<strong><em>application starting</em></strong>并不是用的spring boot初始化的logger. 日志打印效果如下:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">21:53:19.728 [main] INFO com.myspring.sprintboottest.MyApplicationListener - application starting</span><br><span class="line"> . ____ _ __ _ _</span><br><span class="line"> /\\ / ___<span class="string">'_ __ _ _(_)_ __ __ _ \ \ \ \</span></span><br><span class="line"><span class="string">( ( )\___ | '</span>_ | <span class="string">'_| | '</span>_ \/ _` | \ \ \ \</span><br><span class="line"> \\/ ___)| |_)| | | | | || (_| | ) ) ) )</span><br><span class="line"> <span class="string">' |____| .__|_| |_|_| |_\__, | / / / /</span></span><br><span class="line"><span class="string"> =========|_|==============|___/=/_/_/_/</span></span><br><span class="line"><span class="string"> :: Spring Boot :: (v2.3.6.RELEASE)</span></span><br><span class="line"><span class="string">2021-04-19 21:53:19.956 INFO 3596 --- [ main] com.myspring.sprintboottest.Application : Starting Application on **** with PID 3596</span></span><br><span class="line"><span class="string">2021-04-19 21:53:19.957 INFO 3596 --- [ main] com.myspring.sprintboottest.Application : No active profile set, falling back to default profiles: default</span></span><br><span class="line"><span class="string">2021-04-19 21:53:20.611 INFO 3596 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)</span></span><br><span class="line"><span class="string">2021</span></span><br></pre></td></tr></table></figure><p>见第一条,可以看到和spring boot初始化的log pattern都不一样,问题来了,如果我想在spring boot日志系统初始化之前进行日志打印,该怎么办了?答案是延迟日志输出,直到日志系统初始化完成, 代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyApplicationListener</span> <span class="keyword">implements</span> <span class="title">SmartApplicationListener</span> </span>{</span><br><span class="line"><span class="keyword">private</span> DeferredLog logger = <span class="keyword">new</span> DeferredLog();</span><br><span class="line"> <span class="comment">//省略非关键部分...</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onApplicationEvent</span><span class="params">(ApplicationEvent event)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (event <span class="keyword">instanceof</span> ApplicationStartingEvent) {</span><br><span class="line">logger.info(<span class="string">"application starting"</span>);</span><br><span class="line">} <span class="keyword">else</span> <span class="keyword">if</span> (event <span class="keyword">instanceof</span> ApplicationStartedEvent) {</span><br><span class="line">logger.info(<span class="string">"application started"</span>);</span><br><span class="line">logger.switchTo(MyApplicationListener<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用到了spring boot的DeferredLog,它的做法是进行日志打印的时候将日志暂存到一个集合中,当调用<strong><em>switchTo</em></strong>的时候将日志进行实际的输出,并清空之前的集合。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>SpringBoot的自动装配会通过@SpringBootApplicaiton中的@EnableAutoConfiguration注解来启动,其实质是通过import AutoConfigurationImportSelector来完成加载,AutoConfigurationImportSelector最终会加载META-INF/spring.factories中相应AutoConfiguration来完成自动装配</p>]]></content>
<summary type="html">
<p>背景是这样的,我写了一个spring boot的应用监听器,但是部分日志无法输出,直接上代码:</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyApplicationListener</span> <span class="keyword">implements</span> <span class="title">SmartApplicationListener</span> </span>&#123;</span><br><span class="line"> <span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">supportsEventType</span><span class="params">(Class&lt;? extends ApplicationEvent&gt; eventType)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">return</span> ApplicationStartingEvent<span class="class">.<span class="keyword">class</span>.<span class="title">isAssignableFrom</span>(<span class="title">eventType</span>)</span></span><br><span class="line"><span class="class"> || <span class="title">ApplicationStartedEvent</span>.<span class="title">class</span>.<span class="title">isAssignableFrom</span>(<span class="title">eventType</span>)</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onApplicationEvent</span><span class="params">(ApplicationEvent event)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">if</span> (event <span class="keyword">instanceof</span> ApplicationStartingEvent) &#123;</span><br><span class="line"> logger.info(<span class="string">"application starting"</span>);</span><br><span class="line"> &#125; <span class="keyword">else</span> <span class="keyword">if</span> (event <span class="keyword">instanceof</span> ApplicationStartedEvent) &#123;</span><br><span class="line"> logger.info(<span class="string">"application started"</span>);</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>日志输出如下:</p>
</summary>
</entry>
<entry>
<title>springboot LoggingSystem</title>
<link href="http://laywin.github.io/2020/06/29/springboot-%E6%97%A5%E5%BF%97/"/>
<id>http://laywin.github.io/2020/06/29/springboot-日志/</id>
<published>2020-06-29T07:29:50.000Z</published>
<updated>2023-10-08T14:12:40.429Z</updated>
<content type="html"><![CDATA[<p>springboot 默认使用logback作为日志实现,可以看到在spring-boot-starter-logging中依赖了logback,springboot实际是通过判断classpath下是否存在对应的class来决定使用具体的日志实现。代码在LoggingApplicationListener中。</p><p>LoggingApplicationListener捕获了5种类型的事件,分别是ApplicationStartingEvent,ApplicationEnvironmentPreparedEvent,ApplicationPreparedEvent,ContextClosedEvent,ApplicationFailedEvent。而日志系统的初始化则在相应的事件处理方法中完成的。下面我们看一下具体的事件处理方法。</p><a id="more"></a><ul><li>ApplicationStartingEvent(应用程序启动事件)</li></ul><p>当springboot启动时,框架会默认选择对应的日志实现,前面也说到了,是通过判断classpath下是否存在相关的class来进行判断,如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// LoggingApplicationListener.class</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">onApplicationStartingEvent</span><span class="params">(ApplicationStartingEvent event)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());</span><br><span class="line"><span class="keyword">this</span>.loggingSystem.beforeInitialize();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// LoggingSystem.class</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String SYSTEM_PROPERTY = LoggingSystem<span class="class">.<span class="keyword">class</span>.<span class="title">getName</span>()</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Map<String, String> SYSTEMS;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> {</span><br><span class="line">Map<String, String> systems = <span class="keyword">new</span> LinkedHashMap<>();</span><br><span class="line">systems.put(<span class="string">"ch.qos.logback.core.Appender"</span>, <span class="string">"org.springframework.boot.logging.logback.LogbackLoggingSystem"</span>);</span><br><span class="line">systems.put(<span class="string">"org.apache.logging.log4j.core.impl.Log4jContextFactory"</span>,</span><br><span class="line"><span class="string">"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem"</span>);</span><br><span class="line">systems.put(<span class="string">"java.util.logging.LogManager"</span>, <span class="string">"org.springframework.boot.logging.java.JavaLoggingSystem"</span>);</span><br><span class="line">SYSTEMS = Collections.unmodifiableMap(systems);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> LoggingSystem <span class="title">get</span><span class="params">(ClassLoader classLoader)</span> </span>{</span><br><span class="line">String loggingSystem = System.getProperty(SYSTEM_PROPERTY);</span><br><span class="line"><span class="keyword">if</span> (StringUtils.hasLength(loggingSystem)) {</span><br><span class="line"><span class="keyword">if</span> (NONE.equals(loggingSystem)) {</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> NoOpLoggingSystem();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> get(classLoader, loggingSystem);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))</span><br><span class="line">.map((entry) -> get(classLoader, entry.getValue())).findFirst()</span><br><span class="line">.orElseThrow(() -> <span class="keyword">new</span> IllegalStateException(<span class="string">"No suitable logging system located"</span>));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上所示,收到应用程序启动的事件后,会创建对应的LoggingSystem, LoggingSystem首先会通过system property来进行查找是否有相关配置,否则根据默认的方式来进行实例化。</p><p>loggingSystem 的beforeInitialize则主要创建了对应的loggerContext</p><ul><li>ApplicationEnvironmentPreparedEvent (应用程序环境准备事件)</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">onApplicationEnvironmentPreparedEvent</span><span class="params">(ApplicationEnvironmentPreparedEvent event)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">this</span>.loggingSystem == <span class="keyword">null</span>) {</span><br><span class="line"><span class="keyword">this</span>.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());</span><br><span class="line">}</span><br><span class="line">initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Initialize the logging system according to preferences expressed through the</span></span><br><span class="line"><span class="comment"> * {<span class="doctag">@link</span> Environment} and the classpath.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> environment the environment</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> classLoader the classloader</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">initialize</span><span class="params">(ConfigurableEnvironment environment, ClassLoader classLoader)</span> </span>{</span><br><span class="line"><span class="keyword">new</span> LoggingSystemProperties(environment).apply();</span><br><span class="line"><span class="keyword">this</span>.logFile = LogFile.get(environment);</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">this</span>.logFile != <span class="keyword">null</span>) {</span><br><span class="line"><span class="keyword">this</span>.logFile.applyToSystemProperties();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">this</span>.loggerGroups = <span class="keyword">new</span> LoggerGroups(DEFAULT_GROUP_LOGGERS);</span><br><span class="line">initializeEarlyLoggingLevel(environment);</span><br><span class="line">initializeSystem(environment, <span class="keyword">this</span>.loggingSystem, <span class="keyword">this</span>.logFile);</span><br><span class="line">initializeFinalLoggingLevels(environment, <span class="keyword">this</span>.loggingSystem);</span><br><span class="line">registerShutdownHookIfNecessary(environment, <span class="keyword">this</span>.loggingSystem);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个阶段主要是通过相关的配置来初始化LoggingSystem,配置主要来自于Environment和classpath的日志配置文件</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">initialize</span><span class="params">(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (StringUtils.hasLength(configLocation)) {</span><br><span class="line">initializeWithSpecificConfig(initializationContext, configLocation, logFile);</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line">initializeWithConventions(initializationContext, logFile);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上所示。如果配置的有configLocation,则通过该地址来进行日志配置,否则根据默认的规则进行配置,默认的规则如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">initializeWithConventions</span><span class="params">(LoggingInitializationContext initializationContext, LogFile logFile)</span> </span>{</span><br><span class="line">String config = getSelfInitializationConfig();</span><br><span class="line"><span class="keyword">if</span> (config != <span class="keyword">null</span> && logFile == <span class="keyword">null</span>) {</span><br><span class="line"><span class="comment">// self initialization has occurred, reinitialize in case of property changes</span></span><br><span class="line">reinitialize(initializationContext);</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (config == <span class="keyword">null</span>) {</span><br><span class="line">config = getSpringInitializationConfig();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (config != <span class="keyword">null</span>) {</span><br><span class="line">loadConfiguration(initializationContext, config, logFile);</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line">loadDefaults(initializationContext, logFile);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以logback为例,如果根据标准的日志配置文件名(”logback-test.groovy”, “logback-test.xml”, “logback.groovy”, “logback.xml”)没有找到,则根据spring的日志配置文件名来找(”logback-test-spring.groovy”, “logback-test-spring.xml”, “logback-spring.groovy”, “logback-spring.xml”),如果都没找到,就通过默认的方式来进行日志配置。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">apply</span><span class="params">(LogbackConfigurator config)</span> </span>{</span><br><span class="line"><span class="keyword">synchronized</span> (config.getConfigurationLock()) {</span><br><span class="line">base(config);</span><br><span class="line">Appender<ILoggingEvent> consoleAppender = consoleAppender(config);</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">this</span>.logFile != <span class="keyword">null</span>) {</span><br><span class="line">Appender<ILoggingEvent> fileAppender = fileAppender(config, <span class="keyword">this</span>.logFile.toString());</span><br><span class="line">config.root(Level.INFO, consoleAppender, fileAppender);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> {</span><br><span class="line">config.root(Level.INFO, consoleAppender);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>默认的方式会创建一个控制台的Appender</p><ul><li>ApplicationPreparedEvent(应用程序准备事件)</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">onApplicationPreparedEvent</span><span class="params">(ApplicationPreparedEvent event)</span> </span>{</span><br><span class="line">ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();</span><br><span class="line"><span class="keyword">if</span> (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {</span><br><span class="line">beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, <span class="keyword">this</span>.loggingSystem);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">this</span>.logFile != <span class="keyword">null</span> && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {</span><br><span class="line">beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, <span class="keyword">this</span>.logFile);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">this</span>.loggerGroups != <span class="keyword">null</span> && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {</span><br><span class="line">beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, <span class="keyword">this</span>.loggerGroups);</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上所示,应用程序准备事件会注册相应的Bean</p><ul><li><p>ContextClosedEvent (上下文关闭事件)</p></li><li><p>ApplicationFailedEvent(应用程序启动失败事件)</p></li></ul><p>上下文关闭事件和应用程序启动失败事件都会对loggingSystem进行清理</p>]]></content>
<summary type="html">
<p>springboot 默认使用logback作为日志实现,可以看到在spring-boot-starter-logging中依赖了logback,springboot实际是通过判断classpath下是否存在对应的class来决定使用具体的日志实现。代码在LoggingApplicationListener中。</p>
<p>LoggingApplicationListener捕获了5种类型的事件,分别是ApplicationStartingEvent,ApplicationEnvironmentPreparedEvent,ApplicationPreparedEvent,ContextClosedEvent,ApplicationFailedEvent。而日志系统的初始化则在相应的事件处理方法中完成的。下面我们看一下具体的事件处理方法。</p>
</summary>
<category term="springboot" scheme="http://laywin.github.io/tags/springboot/"/>
<category term="日志" scheme="http://laywin.github.io/tags/%E6%97%A5%E5%BF%97/"/>
</entry>
<entry>
<title>自定义 springboot starter</title>
<link href="http://laywin.github.io/2020/06/19/%E8%87%AA%E5%AE%9A%E4%B9%89starter/"/>
<id>http://laywin.github.io/2020/06/19/自定义starter/</id>
<published>2020-06-19T03:37:06.000Z</published>
<updated>2023-10-08T14:12:40.427Z</updated>
<content type="html"><![CDATA[<p>springboot 最大的特点就是自动装配,而自动装配的对象便是starter,starter使得springboot更加组件化,接下来介绍一下如何自定义spring boot的starter</p><p>以创建spring-boot-starter-AA为例,工程结构如下:<br><a id="more"></a><br><img src="/2020/06/19/自定义starter/starter001.png" alt></p><p>AAService 是我们需要自动装配的服务,通俗一点来讲即我们需要让AAService成为spring的一个Bean,实际的应用中一个starter通常会装配很多的Bean,如下所示:<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AAService</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> String aa;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> String bb;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> String cc;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">AAService</span><span class="params">(String aa, String bb, String cc)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.aa = aa;</span><br><span class="line"><span class="keyword">this</span>.bb = bb;</span><br><span class="line"><span class="keyword">this</span>.cc = cc;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span></span>{</span><br><span class="line"><span class="comment">//ignore...</span></span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>AAProperties 则是自动装配的属性,如下所示:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ConfigurationProperties</span>(prefix = <span class="string">"aa"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AAProperties</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> String magicA;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> String magicB;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> String magicC;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getMagicA</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> magicA;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setMagicA</span><span class="params">(String magicA)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.magicA = magicA;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getMagicB</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> magicB;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setMagicB</span><span class="params">(String magicB)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.magicB = magicB;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getMagicC</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> magicC;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setMagicC</span><span class="params">(String magicC)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.magicC = magicC;</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>AAProperties 定义了三个属性,magicA,magicB,magicC,这三个属性的作用是用来装配上面的AAService,实际可能会有更多的属性用来装配其它的Bean,ConfigurationProperties是一个标记注解</p><p>AAAutoConfiguration 是自动配置类,如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableConfigurationProperties</span>(AAProperties<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">ConditionalOnProperty</span>(<span class="title">prefix</span> </span>= <span class="string">"aa"</span>, value = <span class="string">"enabled"</span>, matchIfMissing = <span class="keyword">true</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AAAutoConfiguration</span> </span>{</span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> AAService <span class="title">AAService</span><span class="params">(AAProperties aaProperties)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> AAService(aaProperties.getMagicA(), aaProperties.getMagicB(), aaProperties.getMagicC());</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上所示,该配置类会启动 AAProperties 配置,相当于会注入一个AAProperties的Bean,@Conditional开头的注解则是用来做一些条件过滤,AAAutoConfiguration内部则是我们熟悉的通过配置类配置AAService,这样就完成了自动装配</p><p>另外一个关键点在于需要在classpath的META-INF目录下添加spring.factories配置文件,内容如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.test.aa.AAAutoConfiguration</span><br></pre></td></tr></table></figure><p>springboot会通过spring.factories文件来加载AAAutoConfiguration配置类,从而最终完成自动装配,过程见<a href="https://laywin.github.io/2020/06/15/spring-boot-autoconfiguration/">springboot autoconfiguration</a></p>]]></content>
<summary type="html">
<p>springboot 最大的特点就是自动装配,而自动装配的对象便是starter,starter使得springboot更加组件化,接下来介绍一下如何自定义spring boot的starter</p>
<p>以创建spring-boot-starter-AA为例,工程结构如下:<br></p>
</summary>
<category term="springboot" scheme="http://laywin.github.io/tags/springboot/"/>
</entry>
<entry>
<title>springboot 对Quartz Job的依赖注入</title>
<link href="http://laywin.github.io/2020/06/16/springboot-%E5%AF%B9Quartz-job%E7%9A%84%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5/"/>
<id>http://laywin.github.io/2020/06/16/springboot-对Quartz-job的依赖注入/</id>
<published>2020-06-16T07:14:13.000Z</published>
<updated>2023-10-08T14:12:40.427Z</updated>
<content type="html"><![CDATA[<p>前两天在项目里面看见了类似如下的代码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.quartz.Job;</span><br><span class="line"><span class="keyword">import</span> org.quartz.JobExecutionContext;</span><br><span class="line"><span class="keyword">import</span> org.quartz.JobExecutionException;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyJob</span> <span class="keyword">implements</span> <span class="title">Job</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> MyService myService;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">execute</span><span class="params">(JobExecutionContext context)</span> <span class="keyword">throws</span> JobExecutionException </span>{</span><br><span class="line"><span class="comment">//...</span></span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><a id="more"></a><p>表达的就是Quartz的Job中有对spring bean的依赖,第一感觉是能依赖注入到吗?如果能,spring肯定会对MyJob进行管理,而对Job的引用类似如下的方式:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">JobDetail job = JobBuilder.newJob(MyJob.class).withIdentity("myJob").setJobData(new JobDataMap()).build();</span><br><span class="line"></span><br><span class="line">TriggerKey triggerKey = TriggerKey.triggerKey(<span class="string">"trigger"</span>, <span class="string">"group"</span>);</span><br><span class="line"></span><br><span class="line">Trigger alertTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).startNow().forJob(job)</span><br><span class="line">.withSchedule(CronScheduleBuilder.cronSchedule(<span class="string">"0 0/5 * * * ?"</span>)).build();</span><br><span class="line"></span><br><span class="line">scheduler.scheduleJob(job, alertTrigger);</span><br></pre></td></tr></table></figure><p>很显然这里和spring无关,于是只能追踪到Quartz Job创建的时机, 在JobRunShell中,如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">initialize</span><span class="params">(QuartzScheduler sched)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> SchedulerException </span>{</span><br><span class="line"> <span class="keyword">this</span>.qs = sched;</span><br><span class="line"></span><br><span class="line"> Job job = <span class="keyword">null</span>;</span><br><span class="line"> JobDetail jobDetail = firedTriggerBundle.getJobDetail();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> job = sched.getJobFactory().newJob(firedTriggerBundle, scheduler);</span><br><span class="line"> } <span class="keyword">catch</span> (SchedulerException se) {</span><br><span class="line"> sched.notifySchedulerListenersError(</span><br><span class="line"> <span class="string">"An error occured instantiating job to be executed. job= '"</span></span><br><span class="line"> + jobDetail.getKey() + <span class="string">"'"</span>, se);</span><br><span class="line"> <span class="keyword">throw</span> se;</span><br><span class="line"> } <span class="keyword">catch</span> (Throwable ncdfe) { <span class="comment">// such as NoClassDefFoundError</span></span><br><span class="line"> SchedulerException se = <span class="keyword">new</span> SchedulerException(</span><br><span class="line"> <span class="string">"Problem instantiating class '"</span></span><br><span class="line"> + jobDetail.getJobClass().getName() + <span class="string">"' - "</span>, ncdfe);</span><br><span class="line"> sched.notifySchedulerListenersError(</span><br><span class="line"> <span class="string">"An error occured instantiating job to be executed. job= '"</span></span><br><span class="line"> + jobDetail.getKey() + <span class="string">"'"</span>, se);</span><br><span class="line"> <span class="keyword">throw</span> se;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>.jec = <span class="keyword">new</span> JobExecutionContextImpl(scheduler, firedTriggerBundle, job);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>关键点在于 job = sched.getJobFactory().newJob(firedTriggerBundle, scheduler);这行,会通过jobfactory来创建Job, 这个方法实际上会调用到spring的一个桥接的Jobfactory,即:AdaptableJobFactory,如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AdaptableJobFactory</span> <span class="keyword">implements</span> <span class="title">JobFactory</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Job <span class="title">newJob</span><span class="params">(TriggerFiredBundle bundle, Scheduler scheduler)</span> <span class="keyword">throws</span> SchedulerException </span>{</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line">Object jobObject = createJobInstance(bundle);</span><br><span class="line"><span class="keyword">return</span> adaptJob(jobObject);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">catch</span> (Throwable ex) {</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> SchedulerException(<span class="string">"Job instantiation failed"</span>, ex);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>createJobInstance(bundle) 方法实际上会调用到子类SpringBeanJobFactory的createJobInstance方法如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">createJobInstance</span><span class="params">(TriggerFiredBundle bundle)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line">Object job = (<span class="keyword">this</span>.applicationContext != <span class="keyword">null</span> ?</span><br><span class="line"><span class="keyword">this</span>.applicationContext.getAutowireCapableBeanFactory().createBean(</span><br><span class="line">bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, <span class="keyword">false</span>) :</span><br><span class="line"><span class="keyword">super</span>.createJobInstance(bundle));</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (isEligibleForPropertyPopulation(job)) {</span><br><span class="line">BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job);</span><br><span class="line">MutablePropertyValues pvs = <span class="keyword">new</span> MutablePropertyValues();</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">this</span>.schedulerContext != <span class="keyword">null</span>) {</span><br><span class="line">pvs.addPropertyValues(<span class="keyword">this</span>.schedulerContext);</span><br><span class="line">}</span><br><span class="line">pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap());</span><br><span class="line">pvs.addPropertyValues(bundle.getTrigger().getJobDataMap());</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">this</span>.ignoredUnknownProperties != <span class="keyword">null</span>) {</span><br><span class="line"><span class="keyword">for</span> (String propName : <span class="keyword">this</span>.ignoredUnknownProperties) {</span><br><span class="line"><span class="keyword">if</span> (pvs.contains(propName) && !bw.isWritableProperty(propName)) {</span><br><span class="line">pvs.removePropertyValue(propName);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">bw.setPropertyValues(pvs);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> {</span><br><span class="line">bw.setPropertyValues(pvs, <span class="keyword">true</span>);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> job;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>重点在第一行,spring对Job会进行依赖注入,重要的是我们项目用的springboot, Quartz是自动装配的,可以看到在QuartzAutoConfiguration中,创建了如下Bean</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> SchedulerFactoryBean <span class="title">quartzScheduler</span><span class="params">(QuartzProperties properties,</span></span></span><br><span class="line"><span class="function"><span class="params">ObjectProvider<SchedulerFactoryBeanCustomizer> customizers, ObjectProvider<JobDetail> jobDetails,</span></span></span><br><span class="line"><span class="function"><span class="params">Map<String, Calendar> calendars, ObjectProvider<Trigger> triggers, ApplicationContext applicationContext)</span> </span>{</span><br><span class="line">SchedulerFactoryBean schedulerFactoryBean = <span class="keyword">new</span> SchedulerFactoryBean();</span><br><span class="line">SpringBeanJobFactory jobFactory = <span class="keyword">new</span> SpringBeanJobFactory();</span><br><span class="line">jobFactory.setApplicationContext(applicationContext);</span><br><span class="line">schedulerFactoryBean.setJobFactory(jobFactory);</span><br><span class="line">...</span><br><span class="line"><span class="keyword">return</span> schedulerFactoryBean;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上所以,创建了SpringBeanJobFactory,正好和我们跟踪的代码一致</p>]]></content>
<summary type="html">
<p>前两天在项目里面看见了类似如下的代码</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.quartz.Job;</span><br><span class="line"><span class="keyword">import</span> org.quartz.JobExecutionContext;</span><br><span class="line"><span class="keyword">import</span> org.quartz.JobExecutionException;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyJob</span> <span class="keyword">implements</span> <span class="title">Job</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Resource</span></span><br><span class="line"> <span class="keyword">private</span> MyService myService;</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">execute</span><span class="params">(JobExecutionContext context)</span> <span class="keyword">throws</span> JobExecutionException </span>&#123;</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</summary>
<category term="springboot" scheme="http://laywin.github.io/tags/springboot/"/>
<category term="日常挖码" scheme="http://laywin.github.io/tags/%E6%97%A5%E5%B8%B8%E6%8C%96%E7%A0%81/"/>
</entry>
<entry>
<title>springboot autoconfiguration</title>
<link href="http://laywin.github.io/2020/06/15/spring-boot-autoconfiguration/"/>
<id>http://laywin.github.io/2020/06/15/spring-boot-autoconfiguration/</id>
<published>2020-06-15T09:43:33.000Z</published>
<updated>2023-10-08T14:12:40.426Z</updated>
<content type="html"><![CDATA[<h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>自动装配可以说是springboot最大的创新,其使得程序更加组件化,同时对组件的引用和配置也更加简单</p><h3 id="自动装配过程分析"><a href="#自动装配过程分析" class="headerlink" title="自动装配过程分析"></a>自动装配过程分析</h3><p>通过springboot启动类上面的@SpringBootApplication注解,我们能发现自动装配的细节,其包含了@EnableAutoConfiguration注解<br><a id="more"></a><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Target</span>(ElementType.TYPE)</span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.RUNTIME)</span><br><span class="line"><span class="meta">@Documented</span></span><br><span class="line"><span class="meta">@Inherited</span></span><br><span class="line"><span class="meta">@SpringBootConfiguration</span></span><br><span class="line"><span class="meta">@EnableAutoConfiguration</span></span><br><span class="line"><span class="meta">@ComponentScan</span>(excludeFilters = { <span class="meta">@Filter</span>(type = FilterType.CUSTOM, classes = TypeExcludeFilter<span class="class">.<span class="keyword">class</span>),</span></span><br><span class="line"><span class="class">@<span class="title">Filter</span>(<span class="title">type</span> </span>= FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter<span class="class">.<span class="keyword">class</span>) })</span></span><br><span class="line"><span class="class"><span class="title">public</span> @<span class="title">interface</span> <span class="title">SpringBootApplication</span></span></span><br></pre></td></tr></table></figure></p><p>进入注解@EnableAutoConfiguration</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Target</span>(ElementType.TYPE)</span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.RUNTIME)</span><br><span class="line"><span class="meta">@Documented</span></span><br><span class="line"><span class="meta">@Inherited</span></span><br><span class="line"><span class="meta">@AutoConfigurationPackage</span></span><br><span class="line"><span class="meta">@Import</span>(AutoConfigurationImportSelector<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class"><span class="title">public</span> @<span class="title">interface</span> <span class="title">EnableAutoConfiguration</span></span></span><br></pre></td></tr></table></figure><p>实际上会调用到 AutoConfigurationImportSelector selectImports,如下(省略非关键部分)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AutoConfigurationImportSelector</span> <span class="keyword">implements</span> <span class="title">DeferredImportSelector</span>, <span class="title">BeanClassLoaderAware</span>,</span></span><br><span class="line"><span class="class"><span class="title">ResourceLoaderAware</span>, <span class="title">BeanFactoryAware</span>, <span class="title">EnvironmentAware</span>, <span class="title">Ordered</span> </span>{</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> String[] selectImports(AnnotationMetadata annotationMetadata) {</span><br><span class="line"><span class="keyword">if</span> (!isEnabled(annotationMetadata)) {</span><br><span class="line"><span class="keyword">return</span> NO_IMPORTS;</span><br><span class="line">}</span><br><span class="line">AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader</span><br><span class="line">.loadMetadata(<span class="keyword">this</span>.beanClassLoader);</span><br><span class="line">AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,</span><br><span class="line">annotationMetadata);</span><br><span class="line"><span class="keyword">return</span> StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Return the {<span class="doctag">@link</span> AutoConfigurationEntry} based on the {<span class="doctag">@link</span> AnnotationMetadata}</span></span><br><span class="line"><span class="comment"> * of the importing {<span class="doctag">@link</span> Configuration <span class="doctag">@Configuration</span>} class.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> autoConfigurationMetadata the auto-configuration metadata</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> annotationMetadata the annotation metadata of the configuration class</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> the auto-configurations that should be imported</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> AutoConfigurationEntry <span class="title">getAutoConfigurationEntry</span><span class="params">(AutoConfigurationMetadata autoConfigurationMetadata,</span></span></span><br><span class="line"><span class="function"><span class="params">AnnotationMetadata annotationMetadata)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (!isEnabled(annotationMetadata)) {</span><br><span class="line"><span class="keyword">return</span> EMPTY_ENTRY;</span><br><span class="line">}</span><br><span class="line">AnnotationAttributes attributes = getAttributes(annotationMetadata);</span><br><span class="line">List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);</span><br><span class="line">configurations = removeDuplicates(configurations);</span><br><span class="line">Set<String> exclusions = getExclusions(annotationMetadata, attributes);</span><br><span class="line">checkExcludedClasses(configurations, exclusions);</span><br><span class="line">configurations.removeAll(exclusions);</span><br><span class="line">configurations = filter(configurations, autoConfigurationMetadata);</span><br><span class="line">fireAutoConfigurationImportEvents(configurations, exclusions);</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> AutoConfigurationEntry(configurations, exclusions);</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>关键看getAutoConfigurationEntry中getCandidateConfigurations方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> List<String> <span class="title">getCandidateConfigurations</span><span class="params">(AnnotationMetadata metadata, AnnotationAttributes attributes)</span> </span>{</span><br><span class="line">List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),</span><br><span class="line">getBeanClassLoader());</span><br><span class="line">Assert.notEmpty(configurations, <span class="string">"No auto configuration classes found in META-INF/spring.factories. If you "</span></span><br><span class="line">+ <span class="string">"are using a custom packaging, make sure that file is correct."</span>);</span><br><span class="line"><span class="keyword">return</span> configurations;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> Class<?> getSpringFactoriesLoaderFactoryClass() {</span><br><span class="line"><span class="keyword">return</span> EnableAutoConfiguration<span class="class">.<span class="keyword">class</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>SpringFactoriesLoader会加载META-INF/spring.factories配置文件中的内容</p><p>我们看一下spring-boot-autoconfigure的spring.factories的配置(省略部分内容)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//...</span></span><br><span class="line"></span><br><span class="line"># Initializers</span><br><span class="line">org.springframework.context.ApplicationContextInitializer=\</span><br><span class="line">org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\</span><br><span class="line">org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener</span><br><span class="line"></span><br><span class="line"># Application Listeners</span><br><span class="line">org.springframework.context.ApplicationListener=\</span><br><span class="line">org.springframework.boot.autoconfigure.BackgroundPreinitializer</span><br><span class="line"></span><br><span class="line"># Auto Configuration Import Listeners</span><br><span class="line">org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\</span><br><span class="line">org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener</span><br><span class="line"></span><br><span class="line"># Auto Configuration Import Filters</span><br><span class="line">org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\</span><br><span class="line">org.springframework.boot.autoconfigure.condition.OnBeanCondition,\</span><br><span class="line">org.springframework.boot.autoconfigure.condition.OnClassCondition,\</span><br><span class="line">org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition</span><br><span class="line"></span><br><span class="line"># Auto Configure</span><br><span class="line">org.springframework.boot.autoconfigure.EnableAutoConfiguration=\</span><br><span class="line">org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\</span><br><span class="line">org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\</span><br><span class="line">org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\</span><br><span class="line">org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration</span><br><span class="line"><span class="comment">//...</span></span><br></pre></td></tr></table></figure><p>重点关注 org.springframework.boot.autoconfigure.EnableAutoConfiguration 开头的这行,会加载等号后面的AutoConfiguration</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>SpringBoot的自动装配会通过@SpringBootApplicaiton中的@EnableAutoConfiguration注解来启动,其实质是通过import AutoConfigurationImportSelector来完成加载,AutoConfigurationImportSelector最终会加载META-INF/spring.factories中相应AutoConfiguration来完成自动装配</p>]]></content>
<summary type="html">
<h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>自动装配可以说是springboot最大的创新,其使得程序更加组件化,同时对组件的引用和配置也更加简单</p>
<h3 id="自动装配过程分析"><a href="#自动装配过程分析" class="headerlink" title="自动装配过程分析"></a>自动装配过程分析</h3><p>通过springboot启动类上面的@SpringBootApplication注解,我们能发现自动装配的细节,其包含了@EnableAutoConfiguration注解<br></p>
</summary>
<category term="springboot" scheme="http://laywin.github.io/tags/springboot/"/>
<category term="源码分析" scheme="http://laywin.github.io/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
</entry>
</feed>