开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

「MySQL系列」分析Sql执行时间及查询执行计划(附数据库

发表于 2020-12-04

在查询sql执行时间,查看sql执行计划的时候。发现自己数据量太少,时间差距不明显。来来来,给你一千万条数据。

一 准备数据

1. 创建表和导入一千万条数据

表和数据地址

2. 大批量数据导入数据

a 将数据库导入服务器中(如果是windows系统,这步省略)

b 创建一个数据库

1
复制代码创建数据库(db2),表tb_sku

c 命令行登录数据库

1
css复制代码mysql -u 用户名 -p 密码 ;

d 切换到使用的数据库

1
ini复制代码use db2;

e 使用命令

1
lua复制代码load data local infile '/tmp/tb_sku1.sql' into table `tb_sku` fields terminated by ',' lines terminated by '\n';

对命令解释: ‘/tmp/tb_sku1.sql’ 数据的目录(windows目录例如:D:\life\tb_sku1.sql),tb_sku 要导入到的表。

注意: 我们之前使用insert的sql将数据导入到数据库中,但是往库中导入上千万数据会需要很久时间。

二 慢查询分析(查找执行时间长的sql)

2.1 show profiles

show profiles是mysql提供可以用来分析当前会话中语句执行的资源消耗情 况。可以用来SQL的调优测量。

2.1.1 设置MySQL支持profile

1. 查看是否支持

1
sql复制代码select @@have_profiling

结果为YES,代表支持。

2. 查看profiling(profiling默认是关闭的)

1
sql复制代码select @@profiling

结果为0,代表没有开启
3. 开启profiling

1
ini复制代码set profiling=1;

2.1.2 show profiles的使用

1. 输入一系列查询语句

1
2
3
4
5
sql复制代码show databases;
use db01;
show tables;
select * from tb_ksu where id < 5;
select count(*) from tb_ksu;

2. 查看没一条SQL执行时间

1
sql复制代码show profiles;    //如果执行没有反应,查看profiling是否开启了,命令为select @@profiling;

查看没每一条sql执行时间。

3. 查询每一条sql每个阶段执行时间

1
csharp复制代码select profile for query 6;   //6,代表Query_ID

上图解释

1
2
3
kotlin复制代码Sending data MySQL线程开始访问数据行并把结果返回给客户端,而不仅仅是
返回给客户端。在Sending data状态下,MySQL线程往往进行大量的磁盘读取
操作,所以在查询中最耗时的状态。

4. 查看线程在什么资源上耗费过高 (类型 all、cpu、block io 、context、switch、page faults)

1
ini复制代码show profile cpu for query 7;

上图说明

2.2 慢查询日志

慢查询日志记录了所有执行时间超过参数(long_query_time)设置值并且扫描 记录数不少于min_examined_row_limit,的所有SQL日志。long_query_time默 认为10秒,最小为0,精度可以到微秒。

2.2.1 设置慢查询日志

1. 修改配置文件(慢查询日志默认关闭的)
修改配置文件命令 vi /etc/my.cnf 然后在配置文件最下方加入下面配置

1
2
3
4
5
6
ini复制代码# 该参数用来控制慢查询日志是否开启,可取值:1和0,1代表开启,0代表关闭
slow_query_log=1
#该参数用来指定慢查询日志的文件名
slow_query_log_file=slow_query.log
#该选项用来配置查询的时间限制, 超过这个时间将认为是慢查询, 将进行日志记录, 默认10s
long_query_time=10

2. 重启mysql服务

1
复制代码service mysqld restart

备注 如果执行命令报如下错误

请使用命令 systemctl restart mysqld.service

3. 查看慢查询日志目录

1
bash复制代码cd /var/lib/mysql

2.2.2 日志读取

1. 查询long_query_time的值

1
sql复制代码show variables like 'long%';

2. 执行查询操作

1
2
sql复制代码select * from tb_sku where id = '100000030074'\G;
select * from tb_sku where name like '%HuaWei手机Meta87384 Pro%'\G;

3. 查询慢查询日志

a 使用cat

b 如果慢查询日志很多,借助借助于mysql自带的mysqldumpslow工具,进行分类汇总

三 explain执行计划、索引使用和SQL优化(对某个sql进行分析)

通过以上步骤查询到效率低的SQL语句后,可以通过EXPLAIN命令获取Mysql如 何执行Select语句信息,包含select语句执行过程中表如何连接和连接的顺 序。

3.1 执行explain命令,进行分析

1
csharp复制代码explain select * from tb_sku where id = '100000030074';

1
sql复制代码explain select * from tb_sku where name like '%HuaWei 手机Meta87384 Pro%';

执行计划字段解释

3.2 对字段取值解释

1. id

1
2
3
bash复制代码A. id 相同表示加载表的顺序是从上到下。
B. id 不同id值越大,优先级越高,越先被执行。
C. id 有相同,也有不同,同时存在。id相同的可以认为是一组,从上往下顺序执行;在所有的组中,id的值越大,优先级越高,越先执行。

2. select_type

3. type

结果由好到坏

1
2
3
sql复制代码NULL > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge >unique_subquery > index_subquery > range > index > ALL

system > const > eq_ref > ref > range > index > ALL

4. key

1
2
3
less复制代码A. possible_keys : 显示可能应用在这张表的索引, 一个或多个。
B. key : 实际使用的索引, 如果为NULL, 则没有使用索引。
C. key_len : 表示索引中使用的字节数, 该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下, 长度越短越好 。

5. rows

1
复制代码扫描行的数量。

6. filtered

1
vbscript复制代码这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例。

备注

接下来文章内容涉及索引使用、常见的sql优化。

推荐文章

使用Netty开发,踩坑到解决全过程(附解决方案源码、Netty系列)| 掘金年度征文

WebSocket实战集成SSL,到阿里云生成SSL(网络编程安全二)

一文入门Netty(Netty一)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于Actuator的可修改配置的线程池监控 基于Actua

发表于 2020-12-03

基于Actuator的可修改配置的线程池监控

1.概要

之前公司因为使用线程池习惯不好,导致线程池负载负载过高。触发了拒绝策略,导致大量任务丢失。而并没有对这个情况进行监控,导致业务出现故障之后才发现抛出了拒绝异常。所以有必要对大量使用线程池的项目进行监控,并且最好能在不停机的情况下对线程池的参数进行修改,由此我们可以用线程池的hook方法去对线程池的状态进行埋点,并且通过Actuator做可视化监控,自定义Endpoint去修改线程池内部参数,实现可以动态修改线程池参数。

2.实现

1.导入Maven依赖

1
2
3
4
pom复制代码        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.编写ThreadPoolMonitor.java监控类

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
java复制代码

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
* 继承ThreadPoolExecutor类,覆盖了shutdown(), shutdownNow(), beforeExecute() 和 afterExecute()
* 方法来统计线程池的执行情况
*/
public class ThreadPoolMonitor extends ThreadPoolExecutor {

private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolMonitor.class);

/**
* 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间
*/
private final ConcurrentHashMap<String, Date> startTimes;

/**
* 线程池名称,一般以业务名称命名,方便区分
*/
private final String poolName;


private long totalDiff;

/**
* 调用父类的构造方法,并初始化HashMap和线程池名称
*
* @param corePoolSize 线程池核心线程数
* @param maximumPoolSize 线程池最大线程数
* @param keepAliveTime 线程的最大空闲时间
* @param unit 空闲时间的单位
* @param workQueue 保存被提交任务的队列
* @param poolName 线程池名称
*/
public ThreadPoolMonitor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, String poolName) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
new EventThreadFactory(poolName), poolName);
}


/**
* 调用父类的构造方法,并初始化HashMap和线程池名称
*
* @param corePoolSize 线程池核心线程数
* @param maximumPoolSize 线程池最大线程数
* @param keepAliveTime 线程的最大空闲时间
* @param unit 空闲时间的单位
* @param workQueue 保存被提交任务的队列
* @param threadFactory 线程工厂
* @param poolName 线程池名称
*/
public ThreadPoolMonitor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, String poolName) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
this.startTimes = new ConcurrentHashMap<>();
this.poolName = poolName;
}

/**
* 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计线程池情况
*/
@Override
public void shutdown() {
// 统计已执行任务、正在执行任务、未执行任务数量
LOGGER.info("{} Going to shutdown. Executed tasks: {}, Running tasks: {}, Pending tasks: {}",
this.poolName, this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size());
super.shutdown();
}

/**
* 线程池立即关闭时,统计线程池情况
*/
@Override
public List<Runnable> shutdownNow() {
// 统计已执行任务、正在执行任务、未执行任务数量
LOGGER.info("{} Going to immediately shutdown. Executed tasks: {}, Running tasks: {}, Pending tasks: {}",
this.poolName, this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size());
return super.shutdownNow();
}

/**
* 任务执行之前,记录任务开始时间
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTimes.put(String.valueOf(r.hashCode()), new Date());
}

/**
* 任务执行之后,计算任务结束时间
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
Date finishDate = new Date();
long diff = finishDate.getTime() - startDate.getTime();
totalDiff += diff;
// 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、
// 已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、
// 最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止
LOGGER.info("{}-pool-monitor: " +
"Duration: {} ms, PoolSize: {}, CorePoolSize: {}, Active: {}, " +
"Completed: {}, Task: {}, Queue: {}, LargestPoolSize: {}, " +
"MaximumPoolSize: {}, KeepAliveTime: {}, isShutdown: {}, isTerminated: {}",
this.poolName,
diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(),
this.getCompletedTaskCount(), this.getTaskCount(), this.getQueue().size(), this.getLargestPoolSize(),
this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS), this.isShutdown(), this.isTerminated());
}


/**
* 生成线程池所用的线程,只是改写了线程池默认的线程工厂,传入线程池名称,便于问题追踪
*/
static class EventThreadFactory implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;

/**
* 初始化线程工厂
*
* @param poolName 线程池名称
*/
EventThreadFactory(String poolName) {
SecurityManager s = System.getSecurityManager();
group = Objects.nonNull(s) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = poolName + "-pool-" + POOL_NUMBER.getAndIncrement() + "-thread-";
}

@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}

public long getTotalDiff() {
return totalDiff;
}


}

3.实现ResizeableBlockingQueue.java可变队列

这里我们直接修改LinkedBlockingQueue的代码,把capacity去掉final,变成一个可变参数。再新增get和set方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
java复制代码/**
* The type Resizeable blocking queue.
*
* @param <E> the type parameter
*/
public class ResizeableBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private static final long serialVersionUID = -1232131234709194L;
/*
* 基于LinkedBlockingQueue 实现的一个可变队列容量的阻塞队列
*
* */

/**
* The type Node.
*
* @param <E> the type parameter
*/
static class Node<E> {
E item;

Node<E> next;

Node(E x) { item = x; }
}

private int capacity;

private final AtomicInteger count = new AtomicInteger();

transient Node<E> head;

private transient Node<E> last;

private final ReentrantLock takeLock = new ReentrantLock();

private final Condition notEmpty = takeLock.newCondition();

private final ReentrantLock putLock = new ReentrantLock();

private final Condition notFull = putLock.newCondition();


/**
* Gets capacity.
*
* @return the capacity
*/
public int getCapacity() {
return capacity;
}

/**
* Sets capacity.
*
* @param capacity the capacity
*/
public void setCapacity(int capacity) {
this.capacity = capacity;
}

private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}

private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}

private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}

private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}

/**
* Fully lock.
*/
void fullyLock() {
putLock.lock();
takeLock.lock();
}

/**
* Fully unlock.
*/
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}



/**
* Instantiates a new Resizeable blocking queue.
*/
public ResizeableBlockingQueue() {
this(Integer.MAX_VALUE);
}

/**
* Instantiates a new Resizeable blocking queue.
*
* @param capacity the capacity
*/
public ResizeableBlockingQueue(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException();
}
this.capacity = capacity;
last = head = new Node<E>(null);
}

/**
* Instantiates a new Resizeable blocking queue.
*
* @param c the c
*/
public ResizeableBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null) {
throw new NullPointerException();
}
if (n == capacity) {
throw new IllegalStateException("Queue full");
}
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}

// this doc comment is overridden to remove the reference to collections
// greater in size than Integer.MAX_VALUE
@Override
public int size() {
return count.get();
}

// this doc comment is a modified copy of the inherited doc comment,
// without the reference to unlimited queues.
@Override
public int remainingCapacity() {
return capacity - count.get();
}

@Override
public void put(E e) throws InterruptedException {
if (e == null) {
throw new NullPointerException();
}
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity) {
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0) {
signalNotEmpty();
}
}

@Override
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {

if (e == null) {
throw new NullPointerException();
}
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0) {
return false;
}
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity) {
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0) {
signalNotEmpty();
}
return true;
}

@Override
public boolean offer(E e) {
if (e == null) {
throw new NullPointerException();
}
final AtomicInteger count = this.count;
if (count.get() == capacity) {
return false;
}
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity) {
notFull.signal();
}
}
} finally {
putLock.unlock();
}
if (c == 0) {
signalNotEmpty();
}
return c >= 0;
}

@Override
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1) {
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity) {
signalNotFull();
}
return x;
}

@Override
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0) {
return null;
}
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1) {
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity) {
signalNotFull();
}
return x;
}

@Override
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0) {
return null;
}
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1) {
notEmpty.signal();
}
}
} finally {
takeLock.unlock();
}
if (c == capacity) {
signalNotFull();
}
return x;
}

@Override
public E peek() {
if (count.get() == 0) {
return null;
}
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null) {
return null;
} else {
return first.item;
}
} finally {
takeLock.unlock();
}
}

void unlink(Node<E> p, Node<E> trail) {
// assert isFullyLocked();
// p.next is not changed, to allow iterators that are
// traversing p to maintain their weak-consistency guarantee.
p.item = null;
trail.next = p.next;
if (last == p) {
last = trail;
}
if (count.getAndDecrement() == capacity) {
notFull.signal();
}
}

@Override
public boolean remove(Object o) {
if (o == null) {
return false;
}
fullyLock();
try {
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
fullyUnlock();
}
}

@Override
public boolean contains(Object o) {
if (o == null) {
return false;
}
fullyLock();
try {
for (Node<E> p = head.next; p != null; p = p.next) {
if (o.equals(p.item)) {
return true;
}
}
return false;
} finally {
fullyUnlock();
}
}

@Override
public Object[] toArray() {
fullyLock();
try {
int size = count.get();
Object[] a = new Object[size];
int k = 0;
for (Node<E> p = head.next; p != null; p = p.next) {
a[k++] = p.item;
}
return a;
} finally {
fullyUnlock();
}
}

@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
fullyLock();
try {
int size = count.get();
if (a.length < size) {
a = (T[])java.lang.reflect.Array.newInstance
(a.getClass().getComponentType(), size);
}

int k = 0;
for (Node<E> p = head.next; p != null; p = p.next) {
a[k++] = (T)p.item;
}
if (a.length > k) {
a[k] = null;
}
return a;
} finally {
fullyUnlock();
}
}

@Override
public String toString() {
fullyLock();
try {
Node<E> p = head.next;
if (p == null) {
return "[]";
}

StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = p.item;
sb.append(e == this ? "(this Collection)" : e);
p = p.next;
if (p == null) {
return sb.append(']').toString();
}
sb.append(',').append(' ');
}
} finally {
fullyUnlock();
}
}

@Override
public void clear() {
fullyLock();
try {
for (Node<E> p, h = head; (p = h.next) != null; h = p) {
h.next = h;
p.item = null;
}
head = last;
// assert head.item == null && head.next == null;
if (count.getAndSet(0) == capacity) {
notFull.signal();
}
} finally {
fullyUnlock();
}
}

@Override
public int drainTo(Collection<? super E> c) {
return drainTo(c, Integer.MAX_VALUE);
}

@Override
public int drainTo(Collection<? super E> c, int maxElements) {
if (c == null) {
throw new NullPointerException();
}
if (c == this) {
throw new IllegalArgumentException();
}
if (maxElements <= 0) {
return 0;
}
boolean signalNotFull = false;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
int n = Math.min(maxElements, count.get());
// count.get provides visibility to first n Nodes
Node<E> h = head;
int i = 0;
try {
while (i < n) {
Node<E> p = h.next;
c.add(p.item);
p.item = null;
h.next = h;
h = p;
++i;
}
return n;
} finally {
// Restore invariants even if c.add() threw
if (i > 0) {
// assert h.item == null;
head = h;
signalNotFull = (count.getAndAdd(-i) == capacity);
}
}
} finally {
takeLock.unlock();
if (signalNotFull) {
signalNotFull();
}
}
}

@Override
public Iterator<E> iterator() {
return new Itr();
}

private class Itr implements Iterator<E> {
/*
* Basic weakly-consistent iterator. At all times hold the next
* item to hand out so that if hasNext() reports true, we will
* still have it to return even if lost race with a take etc.
*/

private Node<E> current;
private Node<E> lastRet;
private E currentElement;

Itr() {
fullyLock();
try {
current = head.next;
if (current != null) {
currentElement = current.item;
}
} finally {
fullyUnlock();
}
}

@Override
public boolean hasNext() {
return current != null;
}

private Node<E> nextNode(Node<E> p) {
for (;;) {
Node<E> s = p.next;
if (s == p) {
return head.next;
}
if (s == null || s.item != null) {
return s;
}
p = s;
}
}

@Override
public E next() {
fullyLock();
try {
if (current == null) {
throw new NoSuchElementException();
}
E x = currentElement;
lastRet = current;
current = nextNode(current);
currentElement = (current == null) ? null : current.item;
return x;
} finally {
fullyUnlock();
}
}

@Override
public void remove() {
if (lastRet == null) {
throw new IllegalStateException();
}
fullyLock();
try {
Node<E> node = lastRet;
lastRet = null;
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (p == node) {
unlink(p, trail);
break;
}
}
} finally {
fullyUnlock();
}
}
}

/**
* The type Lbq spliterator.
*
* @param <E> the type parameter
*/
static final class LBQSpliterator<E> implements Spliterator<E> {
static final int MAX_BATCH = 1 << 25; // max batch array size;
final ResizeableBlockingQueue<E> queue;
Node<E> current; // current node; null until initialized
int batch; // batch size for splits
boolean exhausted; // true when no more nodes
long est; // size estimate
LBQSpliterator(ResizeableBlockingQueue<E> queue) {
this.queue = queue;
this.est = queue.size();
}

@Override
public long estimateSize() { return est; }

@Override
public Spliterator<E> trySplit() {
Node<E> h;
final ResizeableBlockingQueue<E> q = this.queue;
int b = batch;
int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1;
if (!exhausted &&
((h = current) != null || (h = q.head.next) != null) &&
h.next != null) {
Object[] a = new Object[n];
int i = 0;
Node<E> p = current;
q.fullyLock();
try {
if (p != null || (p = q.head.next) != null) {
do {
if ((a[i] = p.item) != null) {
++i;
}
} while ((p = p.next) != null && i < n);
}
} finally {
q.fullyUnlock();
}
if ((current = p) == null) {
est = 0L;
exhausted = true;
}
else if ((est -= i) < 0L) {
est = 0L;
}
if (i > 0) {
batch = i;
return Spliterators.spliterator
(a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL |
Spliterator.CONCURRENT);
}
}
return null;
}

@Override
public void forEachRemaining(Consumer<? super E> action) {
if (action == null) {
throw new NullPointerException();
}
final ResizeableBlockingQueue<E> q = this.queue;
if (!exhausted) {
exhausted = true;
Node<E> p = current;
do {
E e = null;
q.fullyLock();
try {
if (p == null) {
p = q.head.next;
}
while (p != null) {
e = p.item;
p = p.next;
if (e != null) {
break;
}
}
} finally {
q.fullyUnlock();
}
if (e != null) {
action.accept(e);
}
} while (p != null);
}
}

@Override
public boolean tryAdvance(Consumer<? super E> action) {
if (action == null) {
throw new NullPointerException();
}
final ResizeableBlockingQueue<E> q = this.queue;
if (!exhausted) {
E e = null;
q.fullyLock();
try {
if (current == null) {
current = q.head.next;
}
while (current != null) {
e = current.item;
current = current.next;
if (e != null) {
break;
}
}
} finally {
q.fullyUnlock();
}
if (current == null) {
exhausted = true;
}
if (e != null) {
action.accept(e);
return true;
}
}
return false;
}

@Override
public int characteristics() {
return Spliterator.ORDERED | Spliterator.NONNULL |
Spliterator.CONCURRENT;
}
}

public Spliterator<E> spliterator() {
return new LBQSpliterator<E>(this);
}

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {

fullyLock();
try {
// Write out any hidden stuff, plus capacity
s.defaultWriteObject();

// Write out all elements in the proper order.
for (Node<E> p = head.next; p != null; p = p.next) {
s.writeObject(p.item);
}

// Use trailing null as sentinel
s.writeObject(null);
} finally {
fullyUnlock();
}
}

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in capacity, and any hidden stuff
s.defaultReadObject();

count.set(0);
last = head = new Node<E>(null);

// Read in all elements and place in queue
for (;;) {
@SuppressWarnings("unchecked")
E item = (E)s.readObject();
if (item == null) {
break;
}
add(item);
}
}
}

4.实现ThreadPoolUtil.java

编写线程池工具类,通过Util去创建线程池,并且用HashMap去指向创建的线程池,之后可以通过这个HashMap去获取线程池。

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
java复制代码/**
* The type Thread pool util.
* 线程池工具类
*/
@Component
public class ThreadPoolUtil {
/**
* 通过Hash去指向创建的线程池,之后可以通过这个HashMap去获取线程池
*/
private final HashMap<String, ThreadPoolMonitor> threadPoolExecutorHashMap = new HashMap<>();

/**
* Creat thread pool thread pool monitor.
*
* 可以自定义队列类型的构造器
*
* @param corePoolSize the core pool size
* @param maximumPoolSize the maximum pool size
* @param keepAliveTime the keep alive time
* @param unit the unit
* @param workQueue the work queue
* @param poolName the pool name
* @return the thread pool monitor
*/
public ThreadPoolMonitor creatThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, String poolName) {
ThreadPoolMonitor threadPoolExecutor = new ThreadPoolMonitor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, poolName);
threadPoolExecutorHashMap.put(poolName, threadPoolExecutor);
return threadPoolExecutor;
}

/**
* Creat thread pool thread pool monitor.
*
* ResizeableBlockingQueue 里面修改了capacity参数
* 可以通过set方法去修改队列的大小
* 使用默认队列的构造器
*
* @param corePoolSize the core pool size
* @param maximumPoolSize the maximum pool size
* @param keepAliveTime the keep alive time
* @param unit the unit
* @param queueSize the queue size
* @param poolName the pool name
* @return the thread pool monitor
*/
public ThreadPoolMonitor creatThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, int queueSize, String poolName) {
ThreadPoolMonitor threadPoolExecutor = new ThreadPoolMonitor(corePoolSize, maximumPoolSize, keepAliveTime, unit, new ResizeableBlockingQueue<>(queueSize), poolName);
threadPoolExecutorHashMap.put(poolName, threadPoolExecutor);
return threadPoolExecutor;
}

/**
* Gets thread pool executor hash map.
*
* @return the thread pool executor hash map
*/
public HashMap<String, ThreadPoolMonitor> getThreadPoolExecutorHashMap() {
return threadPoolExecutorHashMap;
}
}

5.实现线程池信息的实体类

实现线程池信息的实体类用来EndPoint返回数据

ThreadPoolDetailInfo.java

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
java复制代码

/**
* The type Thread pool detail info.
*/
public class ThreadPoolDetailInfo {
private String threadPoolName;
private Integer poolSize;
private Integer corePoolSize;
private Integer largestPoolSize;
private Integer maximumPoolSize;
private long completedTaskCount;
private Integer active;
private long task;
private long keepAliveTime;
private String activePercent;
private Integer queueCapacity;
private Integer queueSize;
private long avgDiff;

/**
* Instantiates a new Thread pool detail info.
*
* @param threadPoolName the thread pool name
* @param poolSize the pool size
* @param corePoolSize the core pool size
* @param largestPoolSize the largest pool size
* @param maximumPoolSize the maximum pool size
* @param completedTaskCount the completed task count
* @param active the active
* @param task the task
* @param keepAliveTime the keep alive time
* @param activePercent the active percent
* @param queueCapacity the queue capacity
* @param queueSize the queue size
* @param avgDiff the avg diff
*/
public ThreadPoolDetailInfo(String threadPoolName, Integer poolSize, Integer corePoolSize, Integer largestPoolSize, Integer maximumPoolSize, long completedTaskCount, Integer active, long task, long keepAliveTime, String activePercent, Integer queueCapacity, Integer queueSize, long avgDiff) {
this.threadPoolName = threadPoolName;
this.poolSize = poolSize;
this.corePoolSize = corePoolSize;
this.largestPoolSize = largestPoolSize;
this.maximumPoolSize = maximumPoolSize;
this.completedTaskCount = completedTaskCount;
this.active = active;
this.task = task;
this.keepAliveTime = keepAliveTime;
this.activePercent = activePercent;
this.queueCapacity = queueCapacity;
this.queueSize = queueSize;
this.avgDiff = avgDiff;
}

/**
* Gets thread pool name.
*
* @return the thread pool name
*/
public String getThreadPoolName() {
return threadPoolName;
}

/**
* Sets thread pool name.
*
* @param threadPoolName the thread pool name
*/
public void setThreadPoolName(String threadPoolName) {
this.threadPoolName = threadPoolName;
}

/**
* Gets pool size.
*
* @return the pool size
*/
public Integer getPoolSize() {
return poolSize;
}

/**
* Sets pool size.
*
* @param poolSize the pool size
*/
public void setPoolSize(Integer poolSize) {
this.poolSize = poolSize;
}

/**
* Gets core pool size.
*
* @return the core pool size
*/
public Integer getCorePoolSize() {
return corePoolSize;
}

/**
* Sets core pool size.
*
* @param corePoolSize the core pool size
*/
public void setCorePoolSize(Integer corePoolSize) {
this.corePoolSize = corePoolSize;
}

/**
* Gets largest pool size.
*
* @return the largest pool size
*/
public Integer getLargestPoolSize() {
return largestPoolSize;
}

/**
* Sets largest pool size.
*
* @param largestPoolSize the largest pool size
*/
public void setLargestPoolSize(Integer largestPoolSize) {
this.largestPoolSize = largestPoolSize;
}

/**
* Gets maximum pool size.
*
* @return the maximum pool size
*/
public Integer getMaximumPoolSize() {
return maximumPoolSize;
}

/**
* Sets maximum pool size.
*
* @param maximumPoolSize the maximum pool size
*/
public void setMaximumPoolSize(Integer maximumPoolSize) {
this.maximumPoolSize = maximumPoolSize;
}

/**
* Gets completed task count.
*
* @return the completed task count
*/
public long getCompletedTaskCount() {
return completedTaskCount;
}

/**
* Sets completed task count.
*
* @param completedTaskCount the completed task count
*/
public void setCompletedTaskCount(long completedTaskCount) {
this.completedTaskCount = completedTaskCount;
}

/**
* Gets active.
*
* @return the active
*/
public Integer getActive() {
return active;
}

/**
* Sets active.
*
* @param active the active
*/
public void setActive(Integer active) {
this.active = active;
}

/**
* Gets task.
*
* @return the task
*/
public long getTask() {
return task;
}

/**
* Sets task.
*
* @param task the task
*/
public void setTask(long task) {
this.task = task;
}

/**
* Gets keep alive time.
*
* @return the keep alive time
*/
public long getKeepAliveTime() {
return keepAliveTime;
}

/**
* Sets keep alive time.
*
* @param keepAliveTime the keep alive time
*/
public void setKeepAliveTime(long keepAliveTime) {
this.keepAliveTime = keepAliveTime;
}

/**
* Gets active percent.
*
* @return the active percent
*/
public String getActivePercent() {
return activePercent;
}

/**
* Sets active percent.
*
* @param activePercent the active percent
*/
public void setActivePercent(String activePercent) {
this.activePercent = activePercent;
}

/**
* Gets queue capacity.
*
* @return the queue capacity
*/
public Integer getQueueCapacity() {
return queueCapacity;
}

/**
* Sets queue capacity.
*
* @param queueCapacity the queue capacity
*/
public void setQueueCapacity(Integer queueCapacity) {
this.queueCapacity = queueCapacity;
}

/**
* Gets queue size.
*
* @return the queue size
*/
public Integer getQueueSize() {
return queueSize;
}

/**
* Sets queue size.
*
* @param queueSize the queue size
*/
public void setQueueSize(Integer queueSize) {
this.queueSize = queueSize;
}

/**
* Gets avg diff.
*
* @return the avg diff
*/
public long getAvgDiff() {
return avgDiff;
}

/**
* Sets avg diff.
*
* @param avgDiff the avg diff
*/
public void setAvgDiff(long avgDiff) {
this.avgDiff = avgDiff;
}
}

ThreadPoolInfo.java

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
java复制代码
/**
* The type Thread pool info.
*/
public class ThreadPoolInfo {
private String threadPoolName;
private int corePoolSize;
private int maximumPoolSize;
private String queueType;
private int queueCapacity;

/**
* Instantiates a new Thread pool info.
*
* @param threadPoolName the thread pool name
* @param corePoolSize the core pool size
* @param maximumPoolSize the maximum pool size
* @param queueType the queue type
* @param queueCapacity the queue capacity
*/
public ThreadPoolInfo(String threadPoolName, int corePoolSize, int maximumPoolSize, String queueType, int queueCapacity) {
this.threadPoolName = threadPoolName;
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.queueType = queueType;
this.queueCapacity = queueCapacity;
}

/**
* Gets thread pool name.
*
* @return the thread pool name
*/
public String getThreadPoolName() {
return threadPoolName;
}

/**
* Sets thread pool name.
*
* @param threadPoolName the thread pool name
*/
public void setThreadPoolName(String threadPoolName) {
this.threadPoolName = threadPoolName;
}

/**
* Gets core pool size.
*
* @return the core pool size
*/
public int getCorePoolSize() {
return corePoolSize;
}

/**
* Sets core pool size.
*
* @param corePoolSize the core pool size
*/
public void setCorePoolSize(int corePoolSize) {
this.corePoolSize = corePoolSize;
}

/**
* Gets maximum pool size.
*
* @return the maximum pool size
*/
public int getMaximumPoolSize() {
return maximumPoolSize;
}

/**
* Sets maximum pool size.
*
* @param maximumPoolSize the maximum pool size
*/
public void setMaximumPoolSize(int maximumPoolSize) {
this.maximumPoolSize = maximumPoolSize;
}

/**
* Gets queue type.
*
* @return the queue type
*/
public String getQueueType() {
return queueType;
}

/**
* Sets queue type.
*
* @param queueType the queue type
*/
public void setQueueType(String queueType) {
this.queueType = queueType;
}

/**
* Gets capacity.
*
* @return the capacity
*/
public int getqueueCapacity() {
return queueCapacity;
}

/**
* Sets capacity.
*
* @param queueCapacity the queue capacity
*/
public void setqueueCapacity(int queueCapacity) {
this.queueCapacity = queueCapacity;
}
}

6.编写EndPoint

通过actuator里的@RestControllerEndpoint注解可以添加Endpoints接口。本质上是和@Endpoint,@WebEndpoint作用是一样的,都是为服务增加actuator 接口,方便管理运行中的服务。但是有一个明显的不同是,@RestControllerEndpoint只支持Http方式的访问,不支持JMX的访问。而且,端点的方法上面只支持@GetMapping,@PostMapping,@DeleteMapping,@RequestMapping等,而不支持@ReadOperation,@WriteOperation,@DeleteOperation。而且它返回的格式是:application/json。

由于我司的监控系统只支持json格式,实际上使用Metrics和Grafana去监控会更好。

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
java复制代码
/**
* The type Thread pool endpoint.
*
* @author newrank
*/
@RestControllerEndpoint(id = "threadpool")
@Component
public class ThreadPoolEndpoint {
@Autowired
private ThreadPoolUtil threadPoolUtil;

private static final ReentrantLock LOCK = new ReentrantLock();

private static final String RESIZEABLE_BLOCKING_QUEUE = "ResizeableBlockingQueue";


/**
* getThreadPools
* 获取当前所有线程池的线程名称
*/
@GetMapping("getThreadPools")
private List<String> getThreadPools (){
List<String> threadPools = new ArrayList<>();
if (!threadPoolUtil.getThreadPoolExecutorHashMap().isEmpty()){
for (Map.Entry<String, ThreadPoolMonitor> entry : threadPoolUtil.getThreadPoolExecutorHashMap().entrySet()) {
threadPools.add(entry.getKey());
}
}
return threadPools;
}

/**
* 获取线程池可变参数信息
* @param threadPoolName
* @return
*/
@GetMapping("getThreadPoolFixInfo")
private ThreadPoolInfo getThreadPoolInfo(@RequestParam String threadPoolName){
if (threadPoolUtil.getThreadPoolExecutorHashMap().containsKey(threadPoolName)){
ThreadPoolMonitor threadPoolExecutor = threadPoolUtil.getThreadPoolExecutorHashMap().get(threadPoolName);
int queueCapacity = 0;
if (RESIZEABLE_BLOCKING_QUEUE.equals(threadPoolExecutor.getQueue().getClass().getSimpleName())){
ResizeableBlockingQueue queue = (ResizeableBlockingQueue) threadPoolExecutor.getQueue();
queueCapacity = queue.getCapacity();
}
return new ThreadPoolInfo(threadPoolName,threadPoolExecutor.getCorePoolSize(),threadPoolExecutor.getMaximumPoolSize(),
threadPoolExecutor.getQueue().getClass().getSimpleName(),queueCapacity);
}
return null;
}


/**
* 修改线程池配置
* @param threadPoolInfo
* @return
*/
@PostMapping("setThreadPoolFixInfo")
private Boolean setThreadPoolInfo(@RequestBody ThreadPoolInfo threadPoolInfo){
if (threadPoolUtil.getThreadPoolExecutorHashMap().containsKey(threadPoolInfo.getThreadPoolName())){
LOCK.lock();
try {
ThreadPoolMonitor threadPoolExecutor = threadPoolUtil.getThreadPoolExecutorHashMap().get(threadPoolInfo.getThreadPoolName());
threadPoolExecutor.setMaximumPoolSize(threadPoolInfo.getMaximumPoolSize());
threadPoolExecutor.setCorePoolSize(threadPoolInfo.getCorePoolSize());
if (RESIZEABLE_BLOCKING_QUEUE.equals(threadPoolExecutor.getQueue().getClass().getSimpleName())){
ResizeableBlockingQueue queue = (ResizeableBlockingQueue) threadPoolExecutor.getQueue();
queue.setCapacity(threadPoolInfo.getqueueCapacity());
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
finally {
LOCK.unlock();
}
}
return false;
}

/**
* 获取线程池监控信息
* @return
*/
@GetMapping("getThreadPoolListInfo")
private List<ThreadPoolDetailInfo> getThreadPoolListInfo(){
List<ThreadPoolDetailInfo> detailInfoList = new ArrayList<>();
if (!threadPoolUtil.getThreadPoolExecutorHashMap().isEmpty()){
for (Map.Entry<String, ThreadPoolMonitor> entry : threadPoolUtil.getThreadPoolExecutorHashMap().entrySet()) {
ThreadPoolDetailInfo threadPoolDetailInfo = threadPoolInfo(entry.getValue(),entry.getKey());
detailInfoList.add(threadPoolDetailInfo);
}
}
return detailInfoList;
}

/**
* 组装线程池详情
* @param threadPool
* @param threadPoolName
* @return
*/
private ThreadPoolDetailInfo threadPoolInfo(ThreadPoolMonitor threadPool,String threadPoolName) {
BigDecimal activeCount = new BigDecimal(threadPool.getActiveCount());
BigDecimal maximumPoolSize = new BigDecimal(threadPool.getMaximumPoolSize());
BigDecimal result =activeCount.divide(maximumPoolSize, 2, BigDecimal.ROUND_HALF_UP);
NumberFormat numberFormat = NumberFormat.getPercentInstance();
numberFormat.setMaximumFractionDigits(2);
int queueCapacity = 0;
if (RESIZEABLE_BLOCKING_QUEUE.equals(threadPool.getQueue().getClass().getSimpleName())){
ResizeableBlockingQueue queue = (ResizeableBlockingQueue) threadPool.getQueue();
queueCapacity = queue.getCapacity();
}
return new ThreadPoolDetailInfo(threadPoolName,threadPool.getPoolSize(), threadPool.getCorePoolSize(),
threadPool.getLargestPoolSize(), threadPool.getMaximumPoolSize(), threadPool.getCompletedTaskCount(),
threadPool.getActiveCount(),threadPool.getTaskCount(),threadPool.getKeepAliveTime(TimeUnit.MILLISECONDS),
numberFormat.format(result.doubleValue()),queueCapacity,threadPool.getQueue().size(),threadPool.getTotalDiff()/threadPool.getTaskCount());
}


}

7.使用线程池监控

  • 注解
1
2
3
4
java复制代码 @Async("asyncExecutor")  
public void getTrendQuery(){
//do something
}
  • 直接使用
1
2
3
4
5
java复制代码public void test() {  
asyncExecutor.execute(()->{
//do something
}
);

1. 查看线程详情

1
ruby复制代码http://localhost/actuator/threadpool/getThreadPoolListInfo //GET请求

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
json复制代码	 [
{
"active": 0, //正在进行的任务数
"activePercent": "0%",//线程池负载
"completedTaskCount": 17, //完成的任务数
"corePoolSize": 16, //核心线程数
"keepAliveTime": 60000,//线程存活时间
"largestPoolSize": 16,//到达的最大线程数
"maximumPoolSize": 32, //最大线程数
"poolSize": 16,//当前线程数
"queueCapacity": 500,//队列长度 ps:如果不是ResizeableBlockingQueue 队列则默认为0
"task": 0, //任务总数
"queueSize":0,//队列中缓存的任务数量
"threadPoolName": "asyncExecutor" //线程池名称
}
]

2. 查看线程池参数

1
ini复制代码http://localhost/actuator/threadpool/getThreadPoolFixInfo?threadPoolName=asyncExecutor //GET请求

参数:

名称 类型
threadPoolName String

返回:

1
2
3
4
5
6
7
json复制代码		{
"corePoolSize": 16, //核心线程数
"maximumPoolSize": 32, //最大线程数
"queueCapacity": 500, //队列大小
"queueType": "ResizeableBlockingQueue", //队列类型
"threadPoolName": "asyncExecutor" //线程池名称
}

3. 修改线程池参数

1
ruby复制代码https://localhost/actuator/threadpool/setThreadPoolInfo  //Post请求

参数:

名称 类型 备注
threadPoolName String
corePoolSize int 可变
maximumPoolSize int 可变
queueCapacity int 可变
queueType String 不可变

请求类型:json

返回: Boolean

以上完整代码在Github中

Github

个人博客

西西弗的石头

作者水平有限,若有错误遗漏,请指出。

参考文章

1.Java线程池实现原理及其在美团业务中的实践

2.Java并发(六)线程池监控

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Jetpack新成员,一篇文章带你玩转Hilt和依赖注入

发表于 2020-12-03

本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

各位小伙伴们大家早上好。

终于要写这样一篇我自己都比较怕的文章了。

虽然今年的 Google I/O 大会由于疫情的原因没能开成,但是 Google 每年要发布的各种新技术可一样都没少。

随着 Android 11 系统的发布,Jetpack 家族又迎来了不少新成员,包括 Hilt、App Startup、Paging3 等等。

关于 App Startup,我在之前已经写过一篇文章进行讲解了,感兴趣的朋友可以参考 Jetpack 新成员,App Startup 一篇就懂 这篇文章 。

本篇文章的主题是 Hilt。

Hilt 是一个功能强大且用法简单的依赖注入框架,同时也可以说是今年 Jetpack 家族中最重要的一名新成员。

那么为什么说这是一篇我自己都比较怕的文章呢?因为关于依赖注入的文章太难写了。我觉得如果只是向大家讲解 Hilt 的用法倒还算是简单,但是如果想要让大家弄明白为什么要使用 Hilt?或者再进一步,为什么要使用依赖注入?这就不是一个非常好写的话题了。

本篇文章我会尝试将以上几个问题全部讲清楚,希望我可以做得到。

另外请注意,依赖注入这个话题本身是不分语言的,但由于我还要在本文中讲解 Hilt 的知识,所以文中所有的代码都会使用 Kotlin 来演示。对 Kotlin 还不熟悉的朋友,可以去参考我的新书 《第一行代码 Android 第 3 版》 。

为什么要使用依赖注入?

依赖注入的英文名是 Dependency Injection,简称 DI。事实上这并不是什么新兴的名词,而是软件工程学当中比较古老的概念了。

如果要说对于依赖注入最知名的应用,大概就是 Java 中的 Spring 框架了。Spring 在刚开始其实就是一个用于处理依赖注入的框架,后来才慢慢变成了一个功能更加广泛的综合型框架。

我在学生时代学习 Spring 时产生了和绝大多数开发者一样的疑惑,就是为什么我们要使用依赖注入呢?

现在的我或许可以给出更好的答案了,一言以蔽之:解耦。

耦合度过高可能会是你的项目中一个比较严重的隐患,它会让你的项目到了后期变得越来越难以维护。

为了让大家更容易理解,这里我准备通过一个具体的例子来讲述一下。

假设我们开了一家卡车配送公司,公司里目前有一辆卡车每天用来送货,并以此赚钱维持公司运营。

今天接到了一个配送订单,有客户委托我们公司去配送两台电脑。

为了完成这个任务,我们可以编写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码class Truck {

val computer1 = Computer()
val computer2 = Computer()

fun deliver() {
loadToTruck(computer1)
loadToTruck(computer2)
beginToDeliver()
}

}

这里有一辆卡车 Truck,卡车中有一个 deliver() 函数用于执行配送任务。我们在 deliver() 函数中先将两台电脑装上卡车,然后开始进行配送。

这种写法可以完成任务吗?当然可以,我们的任务是配送两台电脑,现在将两台电脑都配送出去了,任务当然也就完成了。

但是这种写法有没有问题呢?有,而且很严重。

具体问题在哪里呢?明眼的小伙伴应该已经看出来了,我们在 Truck 类当中创建了两台电脑的实例,然后才对它们进行的配送。也就是说,现在我们的卡车不光要会送货,还要会生产电脑才行。

这就是刚才所说的耦合度过高所造成的问题,卡车和电脑这两样原本不相干的东西耦合到一起去了。

如果你觉得目前这种写法问题还不算严重,第二天公司又接到了一个新的订单,要求我们去配送手机,因此这辆卡车还要会生产手机才行。第三天又接到了一个配送蔬果的订单,那么这辆卡车还要会种地。。。

最后你会发现,这已经不是一辆卡车了,而是一个全球商品制造中心。

现在我们都意识到了问题的严重性,那么回过头来反思一下,我们的项目到底是从哪里开始跑偏的呢?

这就是一个结构设计上的问题了。仔细思考一下,卡车其实并不需要关心配送的货物具体是什么,它的任务就只是负责送货而已。因此你可以理解成,卡车是依赖于货物的,给了卡车货物,它就去送货,不给卡车货物,它就待命。

那么根据这种说法,我们就可以将刚才的代码进行如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class Truck {

lateinit var cargos: List<Cargo>

fun deliver() {
for (cargo in cargos) {
loadToTruck(cargo)
}
beginToDeliver()
}

}

现在 Truck 类当中添加了 cargos 字段,这就意味着,卡车是依赖于货物的了。经过这样的修改之后,我们的卡车不再关心任何商品制造的事情,而是依赖了什么货物,就去配送什么货物,只做本职应该做的事情。

这种写法,我们就可以称之为:依赖注入。

依赖注入框架的作用是什么?

目前 Truck 类已经设计得比较合理了,但是紧接着又会产生一个新的问题。假如我们的身份现在发生了变化,变成了一家电脑公司的老板,我该如何让一辆卡车来帮我运送电脑呢?

这还不好办?很多人自然而然就能写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class ComputerCompany {

val computer1 = Computer()
val computer2 = Computer()

fun deliverByTruck() {
val truck = Truck()
truck.cargos = listOf(computer1, computer2)
truck.deliver()
}

}

这段代码同样是可以正常工作的,但是这段代码同样也存在比较严重的问题。

问题在哪儿呢?就是在 deliverByTruck() 函数中,为了让卡车帮我们送货,这里自己制造了一辆卡车。这很明显是不合理的,电脑公司应该只负责生产电脑,它不应该去生产卡车。

因此,更加合理的做法是,我们通过拨打卡车配送公司的电话,让他们派辆空闲的卡车过来,这样就不用自己去造车了。当卡车到达之后,我们再将电脑装上卡车,然后执行配送任务即可。

这个过程可以用如下示意图来表示:

使用这种结构设计出来的项目,将会拥有非常出色的扩展性。假如现在又有一家蔬果公司需要找一辆卡车来送菜,我们完全可以使用同样的结构来完成任务:

注意,重点的地方来了。呼叫卡车公司并让他们安排空闲车辆的这个部分,我们可以通过自己手写来实现,也可以借助一些依赖注入框架来简化这个过程。

因此,如果你想问依赖注入框架的作用是什么,那么实际上它就是为了替换下图所示的部分。

看到这里,希望你已经能明白为什么我们要使用依赖注入,以及依赖注入框架的作用是什么了。

Android开发也需要依赖注入框架吗?

有不少人会存在这样的观点,他们认为依赖注入框架主要是应用在服务器这用复杂度比较高的程序上的,Android 开发通常根本就用不到依赖注入框架。

这种观点在我看来可能并没有错,不过我更希望大家把依赖注入框架当成是一个帮助我们简化代码和优化项目的工具,而不是一个额外的负担。

所以,不管程序的复杂度是高是低,既然依赖注入框架可以帮助我们简化代码和优化项目,那么就完全可以使用它。

说到优化项目,大家可能觉得我刚才举的让卡车去生产电脑的例子太搞笑了。可是你信不信,在我们实际的开发过程中,这样的例子简直每天都在上演。

思考一下,你平时在 Activity 中编写的代码,有没有创建过其实并不应该由 Activity 去创建的实例呢?

比如说我们都会使用 OkHttp 来进行网络请求,你有没有在 Activity 中创建过 OkHttpClient 的实例呢?如果有的话,那么恭喜你,你相当于就是在让卡车去生产电脑了(Activity 是卡车,OkHttpClient 是电脑)。

当然,如果只是一个比较简单的项目,我们确实可以在 Activity 中去创建 OkHttpClient 的实例。不考虑代码耦合度的话,即使真的让卡车去生产电脑,也不会出现什么太大的问题,因为它的确可以正常工作。至少暂时可以。

我第一次清晰地意识到自己迫切需要一个依赖注入框架,是我在使用 MVVM 架构来搭建项目的时候。

在 Android 开发者官网有一张关于 MVVM 架构的示意图,如下图所示。

这就是现在 Google 最推荐我们使用的 Android 应用程序架构。

为防止有些同学还没接触过 MVVM,我来对这张图做一下简单的解释。

这张架构图告诉我们,一个拥有良好架构的项目应该要分为若干层。

其中绿色部分表示的是 UI 控制层,这部分就是我们平时写的 Activity 和 Fragment。

蓝色部分表示的是 ViewModel 层,ViewModel 用于持有和 UI 元素相关的数据,以及负责和仓库之间进行通讯。

橙色部分表示的是仓库层,仓库层要做的工作是判断接口请求的数据应该是从数据库中读取还是从网络中获取,并将数据返回给调用方。简而言之,仓库的工作就是在本地和网络数据之间做一个分配和调度的工作。

另外,图中所有的箭头都是单向的,比方说 Activity 指向了 ViewModel,表示 Activity 是依赖于 ViewModel 的,但是反过来 ViewModel 不能依赖于 Activity。其他的几层也是一样的道理,一个箭头就表示一个依赖关系。

还有,依赖关系是不可以跨层的,比方说 UI 控制层不能和仓库层有依赖关系,每一层的组件都只能和它的相邻层交互。

使用这套架构设计出来的项目,结构清晰、分层明确,一定会是一个代码质量非常高的项目。

但是在按照这张架构示意图具体实现的过程中,我却发现了一个问题。

UI 控制层当中,Activity 是四大组件之一,它的实例创建是不用我们去操心的。

而 ViewModel 层当中,Google 在 Jetpack 中提供了专门的 API 来获取 ViewModel 的实例,所以它的实例创建也是不用我们去操心的。

但是到了仓库层,一个尴尬的事情出现了,谁应该去负责创建仓库的实例呢?ViewModel 吗?不对,ViewModel 只是依赖了仓库而已,它不应该负责创建仓库的实例,并且其他不同的 ViewModel 也可能会依赖同一个仓库实例。Activity 吗?这就更扯了,因为 Activity 和 ViewModel 通常都是一一对应的。

所以最后我发现,没人应该负责创建仓库的实例,最简单的方式就是将仓库设置成单例类,这样就不需要操心实例创建的问题了。

但是设置成单例类之后又会出现一个新的问题,就是依赖关系不可以跨层这个规则被打破了。因为仓库已经设置成了单例类,那么自然相当于谁都拥有它的依赖关系了,UI 控制层可以绕过 ViewModel 层,直接和仓库层进行通讯。

从代码设计的层面来讲,这是一个非常不好解决的问题。但如果我们借助依赖注入框架,就可以很灵活地解决这个问题。

从刚才的示意图中已经可以看出,依赖注入框架就是帮助我们呼叫和安排空闲卡车的,我并不关心这个卡车是怎么来的,只要你能帮我送货就行。

因此,ViewModel 层也不应该关心仓库的实例是怎么来的,我只需要声明 ViewModel 是需要依赖仓库的,剩下的让依赖注入框架帮我去解决就行了。

通过这样一个类比,你是不是对于依赖注入框架的理解又更加深刻了一点呢?

Android常用的依赖注入框架

接下来我们聊一聊 Android 有哪些常用的依赖注入框架。

在很早的时候,绝大部分的 Android 开发者都是没有使用依赖注入框架这种意识的。

大名鼎鼎的 Square 公司在 2012 年推出了至今仍然知名度极高的开源依赖注入框架:Dagger。

Square 公司有许多非常成功的开源项目,OkHttp、Retrofit、LeakCanary 等等大家都耳熟能详,而且几乎所有的 Android 项目都在使用。但是 Dagger 却空有知名度,现在应该没有任何项目还在使用它了,为什么呢?

这就是一个很有意思的故事了。

Dagger 的依赖注入理念虽然非常先进,但是却存在一个问题,它是基于 Java 反射去实现的,这就导致了两个潜在的隐患。

第一,我们都知道反射是比较耗时的,所以用这种方式会降低程序的运行效率。当然这个问题并不大,因为现在的程序中到处都在用反射。

第二,依赖注入框架的用法总体来说是非常有难度的,除非你能相当熟练地使用它,否则很难一次性编写正确。而基于反射实现的依赖注入功能,使得在编译期我们无法得知依赖注入的用法到底对不对,只能在运行时通过程序有没有崩溃来判断。这样测试的效率就很低,而且容易将一些 bug 隐藏得很深。

接下来就到了最有意思的地方,我们现在都知道 Dagger 的实现方式存在问题,那么 Dagger2 自然是要去解决这些问题的。但是 Dagger2 并不是由 Square 开发的,而是由 Google 开发的。

这就很奇怪了,正常情况下一个库的 1 版和 2 版应该都是由同一个公司或者同一批开发者维护的,怎么 Dagger1 到 Dagger2 会变化这么大呢?我也不知道为什么,但是我注意到,Google 现在维护的 Dagger 项目是从 Square 的 Dagger 项目 Fork 过来的。

所以我猜测,大概是 Google Fork 了一份 Dagger 的源码,然后在此基础上进行修改,并发布了 Dagger2 版本。Square 看到了之后,认为 Google 的这个版本做得非常好,自己没有必要再重做一遍,也没有必要继续维护 Dagger1 了,所以就发布了这样一条声明:

那么 Dagger2 和 Dagger1 不同的地方在哪里呢?最重要的不同点在于,实现方式完全发生了变化。刚才我们已经知道,Dagger1 是基于 Java 反射实现的,并且列举了它的一些弊端。而 Google 开发的 Dagger2 是基于 Java 注解实现的,这样就把反射的那些弊端全部解决了。

通过注解,Dagger2 会在编译时期自动生成用于依赖注入的代码,所以不会增加任何运行耗时。另外,Dagger2 会在编译时期检查开发者的依赖注入用法是否正确,如果不正确的话则会直接编译失败,这样就能将问题尽可能早地抛出。也就是说,只要你的项目正常编译通过,基本也就说明你的依赖注入用法没什么问题了。

那么 Google 的这个 Dagger2 有没有取得成功呢?简直可以说是大获成功。

根据 Google 官方给出的数据,在 Google Play 排名前 1000 的 App 当中,有 74% 的 App 都使用了 Dagger2。

这里我要提一句,海外和国内的 Android 开发者喜欢研究的技术栈不太一样。在海外,没有人去研究像热修复或插件化这种国内特有的 Android 技术。那么你可能想问了,海外开发者们都是学什么进阶的呢?

答案就是 Dagger2。

是的,Dagger2 在海外是非常受到欢迎和广泛认可的技术栈,如果你能用得一手好 Dagger2,基本也就说明你是水平比较高的开发者了。

不过有趣的是,在国内反倒没有多少人愿意去使用 Dagger2,我在公众号之前也推送过几篇关于 Dagger2 的文章,但是从反馈上来看感觉这项技术在国内始终比较小众。

虽然 Dagger2 在海外很受欢迎,但是其复杂程度也是众所周知的,如果你不能很好地使用它的话,反而可能会拖累你的项目。所以一直也有声音说,使用 Dagger2 会将一些简单的项目过度设计。

根据 Android 团队发布的调查,49% 的 Android 开发者希望 Jetpack 中能够提供一个更加简单的依赖注入解决方案。

于是,Google 在今年发布了 Hilt。

你是不是觉得我讲了这么多的长篇大论,现在才终于讲到主题?不要这么想,我认为了解以上这些综合的内容,比仅仅只是掌握了 Hilt 的用法要更加重要。

我们都知道,Dagger 是匕首的意思,依赖注入就好像是把匕首直接插入了需要注入的地方,直击要害。

而 Hilt 是刀把的意思,它把匕首最锋利的地方隐藏了起来,因为如果你用不好匕首的话反而可能会误伤自己。Hilt 给你提供了一个安稳的把手,确保你可以安全简单地使用。

事实上,Hilt 和 Dagger2 有着千丝万缕的关系。Hilt 就是 Android 团队联系了 Dagger2 团队,一起开发出来的一个专门面向 Android 的依赖注入框架。相比于 Dagger2,Hilt 最明显的特征就是:1. 简单。2. 提供了 Android 专属的 API。

那么接下来,就让我们开始学习一下 Hilt 的具体用法。

引入Hilt

在开始使用 Hilt 之前,我们需要先将 Hilt 引入到你当前的项目当中。这个过程稍微有点繁琐,所以请大家一步步按照文章中的步骤操作。

第一步,我们需要在项目根目录的 build.gradle 文件中配置 Hilt 的插件路径:

1
2
3
4
5
6
7
arduino复制代码buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}

可以看到,目前 Hilt 最新的插件版本还在 alpha 阶段,但是没有关系,我自己用下来感觉已经是相当稳定了,等正式版本发布之后升级一下就可以了,用法上不会有什么太大变化。

接下来,在 app/build.gradle 文件中,引入 Hilt 的插件并添加 Hilt 的依赖库:

1
2
3
4
5
6
7
8
arduino复制代码...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

这里同时还引入了 kotlin-kapt 插件,是因为 Hilt 是基于编译时注解来实现的,而启用编译时注解功能一定要先添加 kotlin-kapt 插件。如果你还在用 Java 开发项目,则可以不引入这个插件,同时将添加注解依赖库时使用的 kapt 关键字改成 annotationProcessor 即可。

最后,由于 Hilt 还会用到 Java 8 的特性,所以我们还得在当前项目中启用 Java 8 的功能,编辑 app/build.gradle 文件,并添加如下内容即可:

1
2
3
4
5
6
7
markdown复制代码android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

好了,要配置的内容总共就这么多。现在你已经成功将 Hilt 引入到了你的项目当中,下面我们就来学习一下如何使用它吧。

Hilt的简单用法

我们先从最简单的功能学起。

相信大家都知道,每个 Android 程序中都会有一个 Application,这个 Application 可以自定义,也可以不定义,如果你不定义的话,系统会使用一个默认的 Application。

而到了 Hilt 当中,你必须要自定义一个 Application 才行,否则 Hilt 将无法正常工作。

这里我们自定义一个 MyApplication 类,代码如下所示:

1
2
3
kotlin复制代码@HiltAndroidApp
class MyApplication : Application() {
}

你的自定义 Application 中可以不写任何代码,但是必须要加上一个 @HiltAndroidApp 注解,这是使用 Hilt 的一个必备前提。

接下来将 MyApplication 注册到你的 AndroidManifest.xml 文件当中:

1
2
3
4
5
xml复制代码<application
android:
...>

</application>

这样准备工作就算是完成了,接下来的工作就是根据你具体的业务逻辑使用 Hilt 去进行依赖注入。

Hilt 大幅简化了 Dagger2 的用法,使得我们不用通过 @Component 注解去编写桥接层的逻辑,但是也因此限定了注入功能只能从几个 Android 固定的入口点开始。

Hilt 一共支持 6 个入口点,分别是:

  • Application
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

其中,只有 Application 这个入口点是使用 @HiltAndroidApp 注解来声明的,这个我们刚才已经看过了。其他的所有入口点,都是用 @AndroidEntryPoint 注解来声明的。

以最常见的 Activity 来举例吧,如果我希望在 Activity 中进行依赖注入,那么只需要这样声明 Activity 即可:

1
2
3
4
5
6
7
8
9
kotlin复制代码@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

}

接下来我们尝试向 Activity 中注入点东西吧。注入什么呢?还记得刚才的那辆卡车吗,我们试着看把它注入到 Activity 当中吧。

定义一个 Truck 类,代码如下所示:

1
2
3
4
5
6
7
kotlin复制代码class Truck {

fun deliver() {
println("Truck is delivering cargo.")
}

}

可以看到,目前这辆卡车有一个 deliver() 方法,说明它具备送货功能。

然后修改 Activity 中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var truck: Truck

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
truck.deliver()
}

}

这里的代码可能乍一看上去稍微有点奇怪,我来解释一下。

首先 lateinit 是 Kotlin 中的关键字,和 Hilt 无关。这个关键字用于对变量延迟初始化,因为 Kotlin 默认在声明一个变量时就要对其进行初始化,而这里我们并不想手动初始化,所以要加上 lateinit。如果你是用 Java 开发的话,那么可以无视这个关键字。

接下来我们在 truck 字段的上方声明了一个 @Inject 注解,表示我希望通过 Hilt 来注入 truck 这个字段。如果让我类比的话,这大概就相当于电脑公司打电话让卡车配送公司安排卡车的过程。我们可以把 MainActivity 看作电脑公司,它是依赖于卡车的,但是至于这个卡车是怎么来的,电脑公司并不关心。而 Hilt 在这里承担的职责就类似于卡车配送公司,它负责想办法安排车辆,甚至有义务造一辆出来。

另外提一句,Hilt 注入的字段是不可以声明成 private 的,这里大家一定要注意。

不过代码写到这里还是不可以正常工作的,因为 Hilt 并不知道该如何提供一辆卡车。因此,我们还需要对 Truck 类进行如下修改:

1
2
3
4
5
6
7
kotlin复制代码class Truck @Inject constructor() {

fun deliver() {
println("Truck is delivering cargo.")
}

}

这里我们在 Truck 类的构造函数上声明了一个 @Inject 注解,其实就是在告诉 Hilt,你是可以通过这个构造函数来安排一辆卡车的。

好了,就是这么简单。现在可以运行一下程序了,你将会在 Logcat 中看到如下内容:

说明卡车真的已经在好好送货了。

有没有觉得很神奇?我们在 MainActivity 中并没有去创建 Truck 的实例,只是用 @Inject 声明了一下,结果真的可以调用它的 deliver() 方法。

这就是 Hilt 给我们提供的依赖注入功能。

带参数的依赖注入

必须承认,刚才我们所举的例子确实太简单了,在真实的编程场景中用处应该非常有限,因为真实场景中不可能永远是这样的理想情况。

那么下面我们就开始逐步学习如何在各种更加复杂的场景下使用 Hilt 进行依赖注入。

首先一个很容易想到的场景,如果我的构造函数中带有参数,Hilt 要如何进行依赖注入呢?

我们对 Truck 类进行如下改造:

1
2
3
4
5
6
7
kotlin复制代码class Truck @Inject constructor(val driver: Driver) {

fun deliver() {
println("Truck is delivering cargo. Driven by $driver")
}

}

可以看到,现在 Truck 类的构造函数中增加了一个 Driver 参数,说明卡车是依赖一位司机的,毕竟没有司机的话卡车自己是不会开的。

那么问题来了,既然卡车是依赖司机的,Hilt 现在要如何对卡车进行依赖注入呢?毕竟 Hilt 不知道这位司机来自何处。

这个问题其实没有想象中的困难,因为既然卡车是依赖司机的,那么如果我们想要对卡车进行依赖注入,自然首先要能对司机进行依赖注入才行。

所以可以这样去声明 Driver 类:

1
2
kotlin复制代码class Driver @Inject constructor() {
}

非常简单,我们在 Driver 类的构造函数上声明了一个 @Inject 注解,如此一来,Driver 类就变成了无参构造函数的依赖注入方式。

然后就不需要再修改任何代码了,因为 Hilt 既然知道了要如何依赖注入 Driver,也就知道要如何依赖注入 Truck 了。

总结一下,就是 Truck 的构造函数中所依赖的所有其他对象都支持依赖注入了,那么 Truck 才可以被依赖注入。

现在重新运行一下程序,打印日志如下所示:

可以看到,现在卡车正在被一位司机驾驶,这位司机的身份证号是 de5edf5。

接口的依赖注入

解决了带参构造函数的依赖注入,接下来我们继续看更加复杂的场景:如何对接口进行依赖注入。

毫无疑问,我们目前所掌握的技术是无法对接口进行依赖注入的,原因也很简单,接口没有构造函数。

不过不用担心,Hilt 对接口的依赖注入提供了相当完善的支持,所以你很快就能掌握这项技能。

我们继续通过具体的示例来学习。

任何一辆卡车都需要有引擎才可以正常行驶,那么这里我定义一个 Engine 接口,如下所示:

1
2
3
4
kotlin复制代码interface Engine {
fun start()
fun shutdown()
}

非常简单,接口中有两个待实现方法,分别用于启用引擎和关闭引擎。

既然有接口,那就还要有实现类才行。这里我再定义一个 GasEngine 类,并实现 Engine 接口,代码如下所示:

1
2
3
4
5
6
7
8
9
kotlin复制代码class GasEngine() : Engine {
override fun start() {
println("Gas engine start.")
}

override fun shutdown() {
println("Gas engine shutdown.")
}
}

可以看到,我们在 GasEngine 中实现了启动引擎和关闭引擎的功能。

另外,现在新能源汽车非常火,特斯拉已经快要遍地都是了。所以汽车引擎除了传统的燃油引擎之外,现在还有了电动引擎。于是这里我们再定义一个 ElectricEngine 类,并实现 Engine 接口,代码如下所示:

1
2
3
4
5
6
7
8
9
kotlin复制代码class ElectricEngine() : Engine {
override fun start() {
println("Electric engine start.")
}

override fun shutdown() {
println("Electric engine shutdown.")
}
}

类似地,ElectricEngine 中也实现了启动引擎和关闭引擎的功能。

刚才已经说了,任何一辆卡车都需要有引擎才可以正常行驶,也就是说,卡车是依赖于引擎的。现在我想要通过依赖注入的方式,将引擎注入到卡车当中,那么需要怎么写呢?

根据刚才已学到的知识,最直观的写法就是这样:

1
2
3
4
5
6
7
kotlin复制代码class Truck @Inject constructor(val driver: Driver) {

@Inject
lateinit var engine: Engine
...

}

我们在 Truck 中声明一个 engine 字段,这就说明 Truck 是依赖于 Engine 的了。然后在 engine 字段的上方使用 @Inject 注解对该字段进行注入。或者你也可以将 engine 字段声明到构造函数当中,这样就不需要加入 @Inject 注解了,效果是一样的。

假如 Engine 字段是一个普通的类,使用这种写法当然是没问题的。但问题是 Engine 是一个接口,Hilt 肯定是无法知道要如何创建这个接口的实例,因此这样写一定会报错。

下面我们就来看看该如何一步步解决这个问题。

首先,刚才编写的 GasEngine 和 ElectricEngine 这两个实现类,它们是可以依赖注入的,因为它们都有构造函数。

因此分别修改 GasEngine 和 ElectricEngine 中的代码,如下所示:

1
2
3
4
5
6
7
kotlin复制代码class GasEngine @Inject constructor() : Engine {
...
}

class ElectricEngine @Inject constructor() : Engine {
...
}

这又是我们刚才学过的技术了,在这两个类的构造函数上分别声明 @Inject 注解。

接下来我们需要新建一个抽象类,类名叫什么都可以,但是最好要和业务逻辑有相关性,因此我建议起名 EngineModule.kt,如下所示:

1
2
3
4
5
less复制代码@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

}

这里注意,我们需要在 EngineModule 的上方声明一个 @Module 注解,表示这一个用于提供依赖注入实例的模块。

如果你之前学习过 Dagger2,那么对于这部分理解起来一定会相当轻松,这完全就是和 Dagger2 是一模一样的嘛。

而如果你之前没有学习过 Dagger2,也没有关系,跟着接下来的步骤一步步实现,你自然就能明白它的作用了。

另外可能你会注意到,除了 @Module 注解之外,这里还声明了一个 @InstallIn 注解,这个就是 Dagger2 中没有的东西了。关于 @InstallIn 注解的作用,待会我会使用一块单独的主题进行讲解,暂时你只要知道必须这么写就可以了。

定义好了 EngineModule 之后,接下来我们需要在这个模块当中提供 Engine 接口所需要的实例。怎么提供呢?非常简单,代码如下所示:

1
2
3
4
5
6
7
8
less复制代码@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine

}

这里有几个关键的点我逐个说明一下。

首先我们要定义一个抽象函数,为什么是抽象函数呢?因为我们并不需实现具体的函数体。

其次,这个抽象函数的函数名叫什么都无所谓,你也不会调用它,不过起个好点的名字可以有助于你的阅读和理解。

第三,抽象函数的返回值必须是 Engine,表示用于给 Engine 类型的接口提供实例。那么提供什么实例给它呢?抽象函数接收了什么参数,就提供什么实例给它。由于我们的卡车还比较传统,使用的仍然是燃油引擎,所以 bindEngine() 函数接收了 GasEngine 参数,也就是说,会将 GasEngine 的实例提供给 Engine 接口。

最后,在抽象函数上方加上 @Bind 注解,这样 Hilt 才能识别它。

经过一系列的代码编写之后,我们再回到 Truck 类当中。你会发现,这个时候我们再向 engine 字段去进行依赖注入就变得有道理了,因为借助刚才定义的 EngineModule,很明显将会注入一个 GasEngine 的实例到 engine 字段当中。

实际是不是这样呢?我们来操作一下就知道了,修改 Truck 类中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class Truck @Inject constructor(val driver: Driver) {

@Inject
lateinit var engine: Engine

fun deliver() {
engine.start()
println("Truck is delivering cargo. Driven by $driver")
engine.shutdown()
}

}

我们在开始送货之前先启动车辆引擎,然后在送货完成之后完毕车辆引擎,非常合理的逻辑。

现在重新运行一下程序,控制台打印信息如图所示:

正如我们所预期的那样,在送货的前后分别打印了燃油引擎启动和燃油引擎关闭的日志,说明 Hilt 确实向 engine 字段注入了一个 GasEngine 的实例。

这样也就解决了给接口进行依赖注入的问题。

给相同类型注入不同的实例

友情提醒,别忘了刚才我们定义的 ElectricEngine 还没用上呢。

现在卡车配送公司通过送货赚到了很多钱,解决了温饱问题,就该考虑环保问题了。用燃油引擎来送货实在是不够环保,为了拯救地球,我们决定对卡车进行升级改造。

但是目前电动车还不够成熟,存在续航里程短,充电时间长等问题。怎么办呢?于是我们准备采取一个折中的方案,暂时使用混动引擎来进行过渡。

也就是说,一辆卡车中将会同时包含燃油引擎和电动引擎。

那么问题来了,我们通过 EngineModule 中的 bindEngine() 函数为 Engine 接口提供实例,这个实例要么是 GasEngine,要么是 ElectricEngine,怎么能同时为一个接口提供两种不同的实例呢?

可能你会想到,那我定义两个不同的函数,分别接收 GasEngine 和 ElectricEngine 参数不就行了,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
less复制代码@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

@Binds
abstract fun bindGasEngine(gasEngine: GasEngine): Engine

@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine

}

这种写法看上去好像挺有道理,但是如果你编译一下就会发现报错了:

注意红框中的文字即可,这个错误在提醒我们,Engine 被绑定了多次。

其实想想也有道理,我们在 EngineModule 中提供了两个不同的函数,它们的返回值都是 Engine。那么当在 Truck 中给 engine 字段进行依赖注入时,到底是使用 bindGasEngine() 函数提供的实例呢?还是使用 bindElectricEngine() 函数提供的实例呢?Hilt 也搞不清楚了。

因此这个问题需要借助额外的技术手段才能解决:Qualifier 注解。

Qualifier 注解的作用就是专门用于解决我们目前碰到的问题,给相同类型的类或接口注入不同的实例。

这里我们分别定义两个注解,如下所示:

1
2
3
4
5
6
7
less复制代码@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine

一个注解叫 BindGasEngine,一个注解叫 BindElectricEngine,这样两个注解的作用就明显区分开了。

另外,注解的上方必须使用 @Qualifier 进行声明,这个是毫无疑问的。至于另外一个 @Retention,是用于声明注解的作用范围,选择 AnnotationRetention.BINARY 表示该注解在编译之后会得到保留,但是无法通过反射去访问这个注解。这应该是最合理的一个注解作用范围。

定义好了上述两个注解之后,我们再回到 EngineModule 当中。现在就可以将刚才定义的两个注解分别添加到 bindGasEngine() 和 bindElectricEngine() 函数的上方,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

@BindGasEngine
@Binds
abstract fun bindGasEngine(gasEngine: GasEngine): Engine

@BindElectricEngine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine

}

如此一来,我们就将两个为 Engine 接口提供实例的函数进行了分类,一个分到了 @BindGasEngine 注解上,一个分到了 @BindElectricEngine 注解上。

不过现在还没结束,因为增加了 Qualifier 注解之后,所有为 Engine 类型进行依赖注入的地方也需要去声明注解,明确指定自己希望注入哪种类型的实例。

因此我们还需要修改 Truck 类中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
less复制代码class Truck @Inject constructor(val driver: Driver) {

@BindGasEngine
@Inject
lateinit var gasEngine: Engine

@BindElectricEngine
@Inject
lateinit var electricEngine: Engine

fun deliver() {
gasEngine.start()
electricEngine.start()
println("Truck is delivering cargo. Driven by $driver")
gasEngine.shutdown()
electricEngine.shutdown()
}

}

这段代码现在看起来是不是很容易理解了呢?

我们定义了 gasEngine 和 electricEngine 这两个字段,它们的类型都是 Engine。但是在 gasEngine 的上方,使用了 @BindGasEngine 注解,这样 Hilt 就会给它注入 GasEngine 的实例。在 electricEngine 的上方,使用了 @BindElectricEngine 注解,这样 Hilt 就会给它注入 ElectricEngine 的实例。

最后在 deliver() 当中,我们先启动燃油引擎,再启动电动引擎,送货结束后,先关闭燃油引擎,再关闭电动引擎。

最终的结果会是什么样呢?运行一下看看吧,如下图所示。

非常棒,一切正如我们所预期地那样运行了。

这样也就解决了给相同类型注入不同实例的问题。

第三方类的依赖注入

卡车这个例子暂时先告一段落,接下来我们看一些更加实际的例子。

刚才有说过,如果我们想要在 MainActivity 中使用 OkHttp 发起网络请求,通常会创建一个 OkHttpClient 的实例。不过原则上 OkHttpClient 的实例又不应该由 Activity 去创建,那么很明显,这个时候使用依赖注入是一个非常不错的解决方案。即,让 MainActivity 去依赖 OkHttpClient 即可。

但是这又会引出一个新的问题,OkHttpClient 这个类是由 OkHttp 库提供的啊,我们并没有这个类的编写权限,因此自然也不可能在 OkHttpClient 的构造函数中加上 @Inject 注解,那么要如何对它进行依赖注入呢?

这个时候又要借助 @Module 注解了,它的解决方案有点类似于刚才给接口类型提供依赖注入,但是并不完全一样。

首先定义一个叫 NetworkModule 的类,代码如下所示:

1
2
3
4
5
less复制代码@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {

}

它的初始声明和刚才的 EngineModule 非常相似,只不过这里没有将它声明成抽象类,因为我们不会在这里定义抽象函数。

很明显,在 NetworkModule 当中,我们希望给 OkHttpClient 类型提供实例,因此可以编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {

@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}

}

同样,provideOkHttpClient() 这个函数名是随便定义的,Hilt 不做任何要求,但是返回值必须是 OkHttpClient,因为我们就是要给 OkHttpClient 类型提供实例嘛。

注意,不同的地方在于,这次我们写的不是抽象函数了,而是一个常规的函数。在这个函数中,按正常的写法去创建 OkHttpClient 的实例,并进行返回即可。

最后,记得要在 provideOkHttpClient() 函数的上方加上 @Provides 注解,这样 Hilt 才能识别它。

好了,现在如果你想要在 MainActivity 中去依赖注入 OkHttpClient,只需要这样写即可:

1
2
3
4
5
6
7
8
kotlin复制代码@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var okHttpClient: OkHttpClient
...

}

然后你可以在 MainActivity 的任何地方去使用 okHttpClient 对象,代码一定会正常运行的。

这样我们就解决了给第三方库的类进行依赖注入的问题,不过这个问题其实还可以再进一步拓展一下。

现在直接使用 OkHttp 的人已经越来越少了,更多的开发者选择使用 Retrofit 来作为他们的网络请求解决方案,而 Retrofit 实际上也是基于 OkHttp 的。

为了方便开发者的使用,我们希望在 NetworkModule 中给 Retrofit 类型提供实例,而在创建 Retrofit 实例的时候,我们又可以选择让其依赖 OkHttpClient,具体要怎么写呢?特别简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {

...

@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com/")
.client(okHttpClient)
.build()
}

}

这里定义了一个 provideRetrofit() 函数,然后在函数中按常规的方式去创建 Retrofit 的实例,并将其返回即可。

但是我们注意到,provideRetrofit() 函数还接收了一个 OkHttpClient 参数,并且我们在创建 Retrofit 实例的时候还依赖了这个参数。那么你可能会问了,我们要如何向 provideRetrofit() 函数去传递 OkHttpClient 这个参数呢?

答案是,完全不需要传递,因为这个过程是由 Hilt 自动完成的。我们所需要做的,就是保证 Hilt 能知道如何得到一个 OkHttpClient 的实例,而这个工作我们早在前面一步就已经完成了。

所以,假如现在你在 MainActivity 中去编写这样的代码:

1
2
3
4
5
6
7
8
kotlin复制代码@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var retrofit: Retrofit
...

}

绝对是没有问题的。

Hilt内置组件和组件作用域

刚才我们在学习给接口和第三方类进行依赖注入时,跳过了 @InstallIn 这个注解,现在是时候该回头看一下了。

其实这个注解的名字起得还是相当准确的,InstallIn,就是安装到的意思。那么 @InstallIn(ActivityComponent::class),就是把这个模块安装到 Activity 组件当中。

既然是安装到了 Activity 组件当中,那么自然在 Activity 中是可以使用由这个模块提供的所有依赖注入实例。另外,Activity 中包含的 Fragment 和 View 也可以使用,但是除了 Activity、Fragment、View 之外的其他地方就无法使用了。

比如说,我们在 Service 中使用 @Inject 来对 Retrofit 类型的字段进行依赖注入,就一定会报错。

不过不用慌,这些都是有办法解决的。

Hilt 一共内置了 7 种组件类型,分别用于注入到不同的场景,如下表所示。

这张表中,每个组件的作用范围都不相同。其中,ApplicationComponent 提供的依赖注入实例可以在全项目中使用。因此,如果我们希望刚才在 NetworkModule 中提供的 Retrofit 实例也能在 Service 中进行依赖注入,只需要这样修改就可以了:

1
2
3
4
5
less复制代码@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
...
}

另外和 Hilt 内置组件相关的,还有一个叫组件作用域的概念,我们也要学习一下它的作用。

或许 Hilt 的这个行为和你预想的并不一致,但是这确实就是事实:Hilt 会为每次的依赖注入行为都创建不同的实例。

这种默认行为在很多时候确实是非常不合理的,比如我们提供的 Retrofit 和 OkHttpClient 的实例,理论上它们全局只需要一份就可以了,每次都创建不同的实例明显是一种不必要的浪费。

而更改这种默认行为其实也很简单,借助 @Singleton 注解即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kotlin复制代码@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {

@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}

@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com")
.client(okHttpClient)
.build()
}

}

这样就可以保证 OkHttpClient 和 Retrofit 在全局都只会存在一份实例了。

Hilt 一共提供了 7 种组件作用域注解,和刚才的 7 个内置组件分别是一一对应的,如下表所示。

也就是说,如果想要在全程序范围内共用某个对象的实例,那么就使用 @Singleton。如果想要在某个 Activity,以及它内部包含的 Fragment 和 View 中共用某个对象的实例,那么就使用 @ActivityScoped。以此类推。

另外,我们不必非得在某个 Module 中使用作用域注解,也可以直接将它声明到任何可注入类的上方。比如我们对 Driver 类进行如下声明:

1
2
3
less复制代码@Singleton
class Driver @Inject constructor() {
}

这就表示,Driver 在整个项目的全局范围内都会共享同一个实例,并且全局都可以对 Driver 类进行依赖注入。

而如果我们将注解改成 @ActivityScoped,那么就表示 Driver 在同一个 Activity 内部将会共享同一个实例,并且 Activity、Fragment、View 都可以对 Driver 类进行依赖注入。

你可能会好奇,这个包含关系是如何确定的,为什么声明成 @ActivityScoped 的类在 Fragment 和 View 中也可以进行依赖注入?

关于包含关系的定义,我们来看下面这张图就一目了然了:

简单来讲,就是对某个类声明了某种作用域注解之后,这个注解的箭头所能指到的地方,都可以对该类进行依赖注入,同时在该范围内共享同一个实例。

比如 @Singleton 注解的箭头可以指向所有地方。而 @ServiceScoped 注解的箭头无处可指,所以只能限定在 Service 自身当中使用。@ActivityScoped 注解的箭头可以指向 Fragment、View 当中。

这样你应该就将 Hilt 的内置组件以及组件作用域的相关知识都掌握牢了。

预置Qualifier

Android 开发相比于传统的 Java 开发有其特有的特殊性,比如说 Android 中有个 Context 的概念。

刚入门 Android 开发的新手可能总会疑惑 Context 到底是什么,而做过多年 Android 开发的人估计根本就不关心这个问题了,我天天都在用,甚至到处都在用它,对 Context 是什么已经麻木了。

确实,Android 开发中有太多的地方要依赖于 Context,动不动调用的什么接口就会要求你传入 Context 参数。

那么,如果有个我们想要依赖注入的类,它又是依赖于 Context 的,这个情况要如何解决呢?

举个例子,现在 Driver 类的构造函数接收一个 Context 参数,如下所示:

1
2
3
less复制代码@Singleton
class Driver @Inject constructor(val context: Context) {
}

现在你编译一下项目一定会报错,原因也很简单,Driver 类无法被依赖注入了,因为 Hilt 不知道要如何提供 Context 这个参数。

感觉似曾相识是不是?好像我们让 Truck 类去依赖 Driver 类的时候也遇到了这个问题,当时的解决方案是在 Driver 的构造函数上声明 @Inject 注解,让其也可以被依赖注入就可以了。

但是很明显,这里我们不能用同样的方法解决问题,因为我们根本就没有 Context 类的编写权限,所以肯定无法在其构造函数上声明 @Inject 注解。

那么你可能又会想到了,没有 Context 类的编写权限,那么我们再使用刚才学到的 @Module 的方式,以第三方类的形式给 Context 提供依赖注入不就行了?

这种方案乍看之下好像确实可以,但是当你实际去编写的时候又会发现问题了,比如说:

1
2
3
4
5
6
7
8
9
10
less复制代码@Module
@InstallIn(ApplicationComponent::class)
class ContextModule {

@Provides
fun provideContext(): Context {
???
}

}

这里我定义好了一个 ContextModule,定义好了一个 provideContext() 函数,它的返回值也确实是 Context,但是我接下来不知道该怎么写了,因为我不能 new 一个 Context 的实例去返回啊。

没错,像 Context 这样的系统组件,它的实例都是由 Android 系统去创建的,我们不可以随便去 new 它的实例,所以自然也就不能用前面所学的方案去解决。

那么要如何解决呢?非常简单,Android 提供了一些预置 Qualifier,专门就是用于给我们提供 Context 类型的依赖注入实例的。

比如刚才的 Truck 类,其实只需要在 Context 参数前加上一个 @ApplicationContext 注解,代码就能编译通过了,如下所示:

1
2
3
less复制代码@Singleton
class Driver @Inject constructor(@ApplicationContext val context: Context) {
}

这种写法 Hilt 会自动提供一个 Application 类型的 Context 给到 Truck 类当中,然后 Truck 类就可以使用这个 Context 去编写具体的业务逻辑了。

但是如果你说,我需要的并不是 Application 类型的 Context,而是 Activity 类型的 Context。也没有问题,Hilt 还预置了另外一种 Qualifier,我们使用 @ActivityContext 即可:

1
2
3
less复制代码@Singleton
class Driver @Inject constructor(@ActivityContext val context: Context) {
}

不过这个时候如果你编译一下项目,会发现报错了。原因也很好理解,现在我们的 Driver 是 Singleton 的,也就是全局都可以使用,但是却依赖了一个 Activity 类型的 Context,这很明显是不可能的。

至于解决方案嘛,相信学了上一块主题的你一定已经知道了,我们将 Driver 上方的注解改成 @ActivityScoped、@FragmentScoped、@ViewScoped,或者直接删掉都可以,这样再次编译就不会报错了。

关于预置 Qualifier 其实还有一个隐藏的小技巧,就是对于 Application 和 Activity 这两个类型,Hilt 也是给它们预置好了注入功能。也就是说,如果你的某个类依赖于 Application 或者 Activity,不需要想办法为这两个类提供依赖注入的实例,Hilt 自动就能识别它们。如下所示:

1
2
3
4
5
kotlin复制代码class Driver @Inject constructor(val application: Application) {
}

class Driver @Inject constructor(val activity: Activity) {
}

这种写法编译将可以直接通过,无需添加任何注解声明。

注意必须是 Application 和 Activity 这两个类型,即使是声明它们的子类型,编译都无法通过。

那么你可能会说,我的项目会在自定义的 MyApplication 中提供一些全局通用的函数,导致很多地方都是要依赖于我自己编写的 MyApplication 的,而 MyApplication 又不能被 Hilt 识别,这种情况要怎么办呢?

这里我教大家一个小窍门,因为 Application 全局只会存在一份实例,因此 Hilt 注入的 Application 实例其实就是你自定义的 MyApplication 实例,所以想办法做一下向下类型转换就可以了。

比如说这里我定义了一个 ApplicationModule,代码如下所示:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {

@Provides
fun provideMyApplication(application: Application): MyApplication {
return application as MyApplication
}

}

可以看到,provideMyApplication() 函数中接收一个 Application 参数,这个参数 Hilt 是自动识别的,然后我们将其向下转型成 MyApplication 即可。

接下来你在 Truck 类中就可以去这样声明依赖了:

1
2
kotlin复制代码class Driver @Inject constructor(val application: MyApplication) {
}

完美解决。

ViewModel的依赖注入

到目前为止,你已经将 Hilt 中几乎所有的重要知识点都学习完了。

做事情讲究有始有终,让我们回到开始时候的一个话题:在 MVVM 架构中,仓库层的实例到底应该由谁来创建?

这个问题现在你有更好的答案了吗?

我在学完 Hilt 之后,这个问题就已经释怀了。很明显,根据 MVVM 的架构示意图,ViewModel 层只是依赖于仓库层,它并不关心仓库的实例是从哪儿来的,因此由 Hilt 去管理仓库层的实例创建再合适不过了。

至于具体该如何实现,我总结下来大概有两种方式,这里分别跟大家演示一下。

注意,以下代码只是做了 MVVM 架构中与依赖注入相关部分的演示,如果你还没有了解过 MVVM 架构,或者没有了解过 Jetpack 组件,可能会看不懂下面的代码。这部分朋友建议先去参考 《第一行代码 Android 第 3 版》的第 13 和第 15 章。

第一种方式就是纯粹利用我们前面所学过的知识自己手写。

比如说我们有一个 Repository 类用于表示仓库层:

1
2
3
kotlin复制代码class Repository @Inject constructor() {
...
}

由于 Repository 要依赖注入到 ViewModel 当中,所以我们需要给 Repository 的构造函数加上 @Inject 注解。

然后有一个 MyViewModel 继承自 ViewModel,用于表示 ViewModel 层:

1
2
3
4
less复制代码@ActivityRetainedScoped
class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {
...
}

这里注意以下三点。

第一,MyViewModel 的头部要为其声明 @ActivityRetainedScoped 注解,参照刚才组件作用域那张表,我们知道这个注解就是专门为 ViewModel 提供的,并且它的生命周期也和 ViewModel 一致。

第二,MyViewModel 的构造函数中要声明 @Inject 注解,因为我们在 Activity 中也要使用依赖注入的方式获得 MyViewModel 的实例。

第三,MyViewModel 的构造函数中要加上 Repository 参数,表示 MyViewModel 是依赖于 Repository 的。

接下来就很简单了,我们在 MainActivity 中通过依赖注入的方式得到 MyViewModel 的实例,然后像往常一样的方式去使用它就可以了:

1
2
3
4
5
6
7
8
kotlin复制代码@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var viewModel: MyViewModel
...

}

这种方式虽然可以正常工作,但有个缺点是,我们改变了获取 ViewModel 实例的常规方式。本来我只是想对 Repository 进行依赖注入的,现在连 MyViewModel 也要跟着一起依赖注入了。

为此,对于 ViewModel 这种常用 Jetpack 组件,Hilt 专门为其提供了一种独立的依赖注入方式,也就是我们接下来要介绍的第二种方式了。

这种方式我们需要在 app/build.gradle 文件中添加两个额外的依赖:

1
2
3
4
5
arduino复制代码dependencies {
...
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
}

然后修改 MyViewModel 中的代码,如下所示:

1
2
3
kotlin复制代码class MyViewModel @ViewModelInject constructor(val repository: Repository) : ViewModel() {
...
}

注意这里的变化,首先 @ActivityRetainedScoped 这个注解不见了,因为我们不再需要它了。其次,@Inject 注解变成了 @ViewModelInject 注解,从名字上就可以看出,这个注解是专门给 ViewModel 使用的。

现在回到 MainActivity 当中,你就不再需要使用依赖注入的方式去获取 MyViewModel 的实例了,而是完全按照常规的写法去获取即可:

1
2
3
4
5
6
7
kotlin复制代码@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

val viewModel: MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
...

}

看上去和我们平时使用 ViewModel 时的写法完全无二,这都是由 Hilt 在背后帮我们施了神奇的魔法。

需要注意的是,这种写法下,虽然我们在 MainActivity 里没有使用依赖注入功能,但是 @AndroidEntryPoint 这个注解仍然是不能少的。不然的话,在编译时期 Hilt 确实检测不出来语法上的异常,一旦到了运行时期,Hilt 找不到入口点就无法执行依赖注入了。

不支持的入口点怎么办?

在最开始学习 Hilt 的时候,我就提到了,Hilt 一共支持 6 个入口点,分别是:

  • Application
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

之所以做这样的设定,是因为我们的程序基本都是由这些入口点出发的。

比如一个 Android 程序肯定不可能凭空从 Truck 类开始执行代码,而一定要从上述的某个入口点开始执行,然后才能辗转执行到 Truck 类中的代码。

但是不知道你有没有发现,Hilt 支持的入口点中少了一个关键的 Android 组件:ContentProvider。

我们都知道,ContentProvider 是四大组件之一,并且它也是可以称之为一个入口点的,因为代码可以从这里开始直接运行,而并不需要经过其他类的调用才能到达它。

那么为什么 Hilt 支持的入口点中不包括 ContentProvider 呢?这个问题我也很疑惑,所以在上次的上海 GDG 圆桌会议上,我将这个问题直接提给了 Yigit Boyar,毕竟他在 Google 是专门负责 Jetpack 项目的。

当然我也算得到了一个比较满意的回答,主要原因就是 ContentProvider 的生命周期问题。如果你比较了解 ContentProvider 的话,应该知道它的生命周期是比较特殊的,它在 Application 的 onCreate() 方法之前就能得到执行,因此很多人会利用这个特性去进行提前初始化,详见 Jetpack 新成员,App Startup 一篇就懂 这篇文章。

而 Hilt 的工作原理是从 Application 的 onCreate() 方法中开始的,也就是说在这个方法执行之前,Hilt 的所有功能都还无法正常工作。

也正是因为这个原因,Hilt 才没有将 ContentProvider 纳入到支持的入口点当中。

不过,即使 ContentProvider 并不是入口点,我们仍然还有其他办法在其内部使用依赖注入功能,只是要稍微麻烦一点。

首先可以在 ContentProvider 中自定义一个自己的入口点,并在其中定义好要依赖注入的类型,如下所示:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码class MyContentProvider : ContentProvider() {

@EntryPoint
@InstallIn(ApplicationComponent::class)
interface MyEntryPoint {
fun getRetrofit(): Retrofit
}
...

}

可以看到,这里我们定义了一个 MyEntryPoint 接口,然后在其上方使用 @EntryPoint 来声明这是一个自定义入口点,并用 @InstallIn 来声明其作用范围。

接着我们在 MyEntryPoint 中定义了一个 getRetrofit() 函数,并且函数的返回类型就是 Retrofit。

而 Retrofit 是我们已支持依赖注入的类型,这个功能早在 NetworkModule 当中就已经完成了。

现在,如果我们想要在 MyContentProvider 的某个函数中获取 Retrofit 的实例(事实上,ContentProvider 中不太可能会用到网络功能,这里只是举例),只需要这样写就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码class MyContentProvider : ContentProvider() {

...
override fun query(...): Cursor {
context?.let {
val appContext = it.applicationContext
val entryPoint = EntryPointAccessors.fromApplication(appContext, MyEntryPoint::class.java)
val retrofit = entryPoint.getRetrofit()
}
...
}

}

借助 EntryPointAccessors 类,我们调用其 fromApplication() 函数来获得自定义入口点的实例,然后再调用入口点中定义的 getRetrofit() 函数就能得到 Retrofit 的实例了。

不过我认为,自定义入口点这个功能在实际开发当中并不常用,这里只是考虑知识完整性的原因,所以将这块内容也加入了进来。

结尾

到这里,这篇文章总算是结束了。

不愧称它是一篇我自己都怕的文章,这篇文章大概花了我半个月左右的时间,可能是我写过的最长的一篇文章。

由于 Hilt 涉及的知识点繁多,即使它将 Dagger2 的用法进行了大幅的简化,但如果你之前对于依赖注入完全没有了解,直接上手 Hilt 相信还是会有不少的困难。

我在本文当中尽可能地将 “什么是依赖注入,为什么要使用依赖注入,如何使用依赖注入” 这几个问题描述清楚了,但介于依赖注入这个话题本身复杂度的客观原因,我也不知道本文的难易程度到底在什么等级。希望阅读过的读者朋友们都能达到掌握 Hilt,并用好 Hilt 的水平吧。

另外,由于 Hilt 和 Dagger2 的关系过于紧密,我们在本文中所学的知识,有些是 Hilt 提供的,有些是 Dagger2 本身就自带。但是我对此在文中并没有进行严格的区分,统一都是以 Hilt 的视角去讲的。所以,熟悉 Dagger2 的朋友请不要觉得文中的说法不够严谨,因为太过严谨的话可能会增加没有学过 Dagger2 这部分读者朋友的理解成本。

最后,我将本文中用到的一些代码示例,写成了一个 Demo 程序上传到了 GitHub 上,有需要的朋友直接去下载源码即可。

github.com/guolindev/H…

关注我的技术公众号,每天都有优质技术文章推送。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

还不知道,如何设计订单系统?来看这篇文章,很不错!

发表于 2020-12-03

本文主要讲述了在传统电商企业中,订单系统应承载的角色,就订单系统所包含的主要功能模块梳理了设计思路,并对订单系统未来的发展做了一些思考。

1. 订单系统在企业中的角色

在搭建企业订单系统之前,需要先梳理企业整体业务系统之间的关系和订单系统上下游关系,只有划分清业务系统边界,才能确定订单系统的职责与功能,进而保证各系统之间高效简洁的工作。

2. 订单系统与各业务系统的关系

(1)对外系统:

所有给企业外部用户使用的系统都在这一层,包括官网、普通用户使用的C端,还包括给商户使用的商家后台和在各个销售渠道进行分销的系统,比如与银行信用卡中心合作、微信合作在合作商的平台露出本企业的产品。这类系统站在与客户接触的最前线,是公司实现商业模式的桥头堡。

(2)管理中后台:

每个C端的业务形态都会有一个对应的系统模块,如负责管理平台交易的订单系统,管理优惠信息的促销系统,管理平台所有产品的产品系统,以及管理所有对外系统显示内容的内容系统等。

(3)公共服务系统:

随着企业的发展,信息化建设到达一定程度后,企业需要将通用功能服务化、平台化,以保证应用架构的合理性,提升服务效率。这类系统主要给其他应用系统提供基础服务能力支持。关注公众号:程序员白楠楠, 领取2020最新Java面试题手册(200多页PDF文档)。

3. 订单系统上下游关系

由此可见,订单系统对上接收用户信息,将用户信息转化为产品订单,同时管理并跟踪订单信息和数据,承载了公司整个交易线的重要对客环节。对下则衔接产品系统、促销系统、仓储系统、会员系统、支付系统等,对整个电商平台起着承上启下的作用。

4. 订单系统的业务架构

(1)订单服务

该模块的主要功能是用户日常使用的服务和页面,主要有订单列表、订单详情、在线下单等,还包括为公共业务模块提供的多维度订单数据服务。

(2)订单逻辑

订单系统的核心,起着至关重要的作用,在订单系统负责管理订单创建、订单支付、订单生产、订单确认、订单完成、取消订单等订单流程。还涉及到复杂的订单状态规则、订单金额计算规则以及增减库存规则等。在4节核心功能设计中会重点来说。

(3)底层服务

信息化建设达到一定程度的企业,一般会将公司公共服务模块化,比如:产品,会构建对应的产品系统,代码、数据库,接口等相对独立。但是,这也带来了一个问题,比如:订单创建的场景下需要获取的信息分散在各个系统。

如果需要从各个公共服务系统调用:一是会花费大量时间,二是代码的维护成本非常高。因此,订单系统接入所需的公共服务模块接口,在订单系统即可完成对接公共系统的服务。

订单系统核心功能

1. 订单中所包含的内容信息

为了使订单系统能够对订单进行高效、精准的管理和跟踪,订单会储存关于产品、优惠、用户、支付信息等一系列的订单实时数据,来和下游系统,如:促销、仓储、物流进行交互。

以一个通用B2C商城的订单为例,梳理其包含的信息如下:

这里要注意的是订单类型,随着平台业务的不断发展,品类丰富、交易方式丰富后,需要对订单进行多维度的分类管理,同时订单类型利于订单系统的扩展性。每种订单类型将会对应一套流程及一套状态,便于对订单进行分类管理和复用。

2. 流程引擎

流程是指从平台角度出发,将订单从创建到完成的整个流转过程进行抽象,从而形成了一套标准流程规则。而不同的产品类型或交易类型在系统中的流程会千差万别,因此为了方便对订单流程进行管理,会组建流程引擎模块。

每套订单流程中会包含正向流程及逆向流程,正向流程可以比作一次顺利的网购体验过程中,后台系统之间的信息流转。逆向流程则是修改订单、取消订单、退款、退货等各种动作引起的后台系统流程,同时每个流程触发的条件又可分为系统触发和人工触发两种场景。关注公众号:程序员白楠楠, 领取2020最新Java面试题手册(200多页PDF文档)。

(1)正向流程

以一个通用B2C商城的订单系统为例,根据其实际业务场景,其订单流程可抽象为5大步骤:订单创建>订单支付>订单生产>订单确认>订单完成。

而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图:

订单创建:

用户下单后,系统需要生成订单,此时需要先获取下单中涉及的商品信息,然后获取该商品所涉及到的优惠信息,如果商品不参与优惠信息,则无此环节。

接着获取该账户的会员权益,这里要注意的是:优惠信息与会员权益的区别,比如:商品满减是优惠信息,SUPER会员全场9.8折指的是会员权益,一个是针对商品,另一个是针对账户。其次就是优惠活动的叠加规则和优先级规则等。

增减库存规则是指订单中的商品,何时从仓储系统中对相应商品库存进行扣除,目前主流有两种方式:

下单减库存——即用户下单成功时减少库存数量

  • 优势:用户体验友好,系统逻辑简洁;
  • 缺点:会导致恶意下单或下单后却不买,使得真正有需求的用户无法购买,影响真实销量;

解决办法:

  1. 设置订单有效时间,若订单创建成功N分钟不付款,则订单取消,库存回滚;
  2. 限购,用各种条件来限制买家的购买件数,比如一个账号、一个ip,只能买一件;
  3. 风控,从技术角度进行判断,屏蔽恶意账号,禁止恶意账号购买。

付款减库存——即用户支付完成并反馈给平台后再减少库存数量

  • 优势:减少无效订单带来的资源损耗;
  • 缺点:因第三方支付返回结果存在时差,同一时间多个用户同时付款成功,会导致下单数目超过库存,商家库存不足容易引发断货和投诉,成本增加。

解决办法:

  1. 付款前再次校验库存,如确认订单要付款时再验证一次,并友好提示用户库存不足;
  2. 增加提示信息:在商品详情页,订单步骤页面提示不及时付款,不能保证有库存等。

综上所述,两种方式各有优缺点,因此,需结合实际场景进行考虑,如:秒杀、抢购、促销活动等,可使用下单减库存的方式。而对于产品库存量大,并发流量没有那么强的产品使用付款减库存的方式。

将两种方式带入到销售场景中,关联商品类型、促销类型、供需关系等,灵活使用,以充分发挥计算机系统的优势。

订单支付:

用户支付完订单后,需要获取订单的支付信息,包括支付流水号、支付时间等。支付完订单接着就是等商家发货,但在发货过程中,根据平台业务模式的不同,可能会涉及到订单的拆分。

订单拆分一般分两种:

  • 一种是用户挑选的商品来自于不同渠道(自营与商家,商家与商家);
  • 另一种是在SKU层面上拆分订单:不同仓库,不同运输要求的SKU,包裹重量体积限制等因素需要将订单拆分。

订单拆分也是一个相对独立的模块,这里就不详细描述了。

订单生产:订单生产,是指产品从企业到用户这一流程的概述。如电商平台中,商家发货过程已有一个标准化的流程,订单内容会发送到仓库,仓库对商品进行打单、拣货、包装、交接快递进行配送。

订单确认:收到货后,订单系统需要在快递被签收后提醒用户对商品做评价。这里要注意,确认收到货不代表交易成功,相反是售后服务的开始。

订单完成:订单完成是指在收到货X天的状态,此时订单不在售后的支持时间范围内。到此,一个订单的正向流程就算走完了。

(2)逆向流程

上面说到逆向流程是各种修改订单、取消订单、退款、退货等操作,需要梳理清楚这些流程与正向流程的关系,才能理清订单系统完整的订单流程。

订单修改:可梳理订单内信息,根据信息关联程度及业务诉求,设定订单的可修改范围是什么,比如:客户下单后,想修改收货人地址及电话。此时只需对相应数据进行更新即可。

订单取消:用户提交订单后没有进行支付操作,此时用户原则上属于取消订单,因为还未付款,则比较简单,只需要将原本提交订单时扣减的库存补回,促销优惠中使用的优惠券,权益等视平台规则,进行相应补回。

退款:用户支付成功后,客户发出退款的诉求后,需商户进行退款审核,双方达成一致后,系统应以退款单的形式完成退款,关联原订单数据。因商品无变化,所以不需考虑与库存系统的交互,仅需考虑促销系统及支付系统交互即可。

退货:用户支付成功后,客户发出退货的诉求后,需商户进行退款审核,双方达成一致后,需对库存系统进行补回,支付系统、促销系统以退款单形式完成退款。最后,在退款/退货流程中,需结合平台业务场景,考虑优惠分摊的逻辑,在发生退款/退货时,优惠该如何退回的处理规则和流程。

(3)状态机

状态机是管理订单状态逻辑的工具。状态机可归纳为3个要素,即现态、动作、次态。

  1. 现态:是指当前所处的状态。
  2. 动作:动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。
  3. 次态:动作满足后要迁往的新状态,“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

状态机的设计需要结合平台实际业务场景,将状态间的切换细化成了执行了某个动作。

以一个B2C商城的订单系统举例如下:

订单系统为了高效的对订单进行跟踪和管理,会对订单流程当中的关键节点,抽象出订单状态。而订单状态从不同用户的角度可分为,系统订单状态、商家订单状态、买家订单状态等。

对于订单系统来说,订单状态细分的颗粒度越细、越明确,订单系统管理的精度和可靠性就越高,比如:在待付款和待发货两个状态中,订单系统后台会细分为订单超时取消、订单支付失败、订单付款完成等。

因此,订单状态模块中,通常会维护状态映射表,以不同的用户角色对系统订单状态进行重新划分,以满足不同用户的需求。

除此以外,随着电商平台的不断发展,不同的业务类型,所对应的订单状态都会有所区别。所以,订单系统中一般会储存多套状态机,以满足不同的订单类型来使用。

订单系统的发展

订单系统的主体框架,和主要业务模块已基本讲完,那么随着企业的发展,业务量和业务形式不断变化,企业有可能形成多个订单系统并存以满足不同的业务需要的情况。

业务系统架构如下:

这种状况的出现,将会给平台带来非常大的发展瓶颈,如:

三个订单系统,每个订单系统处理不同类型的订单,没有统一的订单销量、订单状态信息,网站前台对订单的状态展示与控制不统一,只能是在网站前台会员中心硬代码维护一套面向会员的统一订单明细与状态数据。而无线侧上线后,由于不了解前台网站会员中心的订单状态管理逻辑,所以需要把前台网站的订单明细及状态管理再在无线应用侧再实现一遍。

三套后台订单系统与公共业务系统如会员中心、支付与财务、促销工具、客户分单等系统都需要对接一遍,公共业务处理逻辑不统一,一旦逻辑变更,多个系的同一个接口都要修改一遍,接口的重复维护开发工作量大。

订单开发目前分到事业部,各个事业部只会考虑自己的逻辑,不会考虑公共架构,只会越走越远。碰到像无线这样的项目,需要对接各个事业部,无线侧应用上线进展慢。

因此未来的订单系统可拆分为订单中心与业务订单系统两个模块,以管理公司所有订单数据,并为各个模块提供统一服务。

最后

对于企业订单系统的搭建,并不是要做的大而全、也不是要小而精。而需要结合市场、公司、业务的实际情况来最终制定系统设计方案和产品迭代计划。

最终,和公司整体发展相互协调,相辅相成。

关注公众号:程序员白楠楠, 领取2020最新Java面试题手册(200多页PDF文档)。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从零开始搭建Kafka+SpringBoot分布式消息系统

发表于 2020-12-03

前言

由于kafka强依赖于zookeeper,所以需先搭建好zookeeper集群。由于zookeeper是由java编写的,需运行在jvm上,所以首先应具备java环境。
(ps:默认您的centos系统可联网,本教程就不教配置ip什么的了)
(ps2:没有wget的先装一下:yum install wget)
(ps3:人啊,就是要条理。东边放一点,西边放一点,过段时间就不知道自己装在哪里了。本教程所有下载均放在/usr/local目录下)
(ps4:kafka可能有内置zookeeper,感觉可以越过zookeeper教程,但是这里也配置出来了。我没试过)

文章首发公众号:Java架构师联盟,每日更新技术好文

一、配置jdk

因为oracle 公司不允许直接通过wget 下载官网上的jdk包。所以你直接wget以下地址下载下来的是一个只有5k的网页文件而已,并不是需要的jdk包。(垄断地位就是任性)。
(请通过java -version判断是否自带jdk,我的没带)

1、官网下载

下面是jdk8的官方下载地址:

1
ruby复制代码https://www.oracle.com/technetwork/java/javase/downloads/java-archive-javase8u211-later-5573849.html

从零开始搭建Kafka+SpringBoot分布式消息系统

2、上传解压

这里通过xftp上传到服务器指定位置:/usr/local

从零开始搭建Kafka+SpringBoot分布式消息系统

对压缩文件进行解压:

1
复制代码tar -zxvf jdk-8u221-linux-x64.tar.gz

对解压后的文件夹进行改名:

1
bash复制代码mv jdk1.8.0_221 jdk1.8

3、配置环境变量

1
2
3
4
5
bash复制代码vim /etc/profile
#java environment
export JAVA_HOME=/usr/local/jdk1.8
export CLASSPATH=.:${JAVA_HOME}/jre/lib/rt.jar:${JAVA_HOME}/lib/dt.jar:${JAVA_HOME}/lib/tools.jar
export PATH=$PATH:${JAVA_HOME}/bin

操作之后的界面如下:

从零开始搭建Kafka+SpringBoot分布式消息系统

运行命令使环境生效

1
bash复制代码source /etc/profile

从零开始搭建Kafka+SpringBoot分布式消息系统

二、搭建zookeeper集群

1、下载zookeeper

创建zookeeper目录,在该目录下进行下载:

1
bash复制代码mkdir /usr/local/zookeeper

这一步如果出现连接被拒绝时可多试几次,我就是第二次请求才成功的。

1
bash复制代码wget http://archive.apache.org/dist/zookeeper/zookeeper-3.4.6/zookeeper-3.4.6.tar.gz

从零开始搭建Kafka+SpringBoot分布式消息系统

等待下载完成之后解压:

1
复制代码tar -zxvf zookeeper-3.4.6.tar.gz

从零开始搭建Kafka+SpringBoot分布式消息系统

重命名为zookeeper1

1
2
3
bash复制代码mv zookeeper-3.4.6 zookeeper1
cp -r zookeeper1 zookeeper2
cp -r zookeeper1 zookeeper3

2、创建data、logs文件夹

在zookeeper1目录下创建

从零开始搭建Kafka+SpringBoot分布式消息系统

在data目录下新建myid文件。内容为1

从零开始搭建Kafka+SpringBoot分布式消息系统

从零开始搭建Kafka+SpringBoot分布式消息系统

3、修改zoo.cfg文件

1
2
bash复制代码cd /usr/local/zookeeper/zookeeper1/conf/
cp zoo_sample.cfg zoo.cfg

进行过上面两步之后,有zoo.cfg文件了,现在修改内容为:

从零开始搭建Kafka+SpringBoot分布式消息系统

1
2
3
4
5
ini复制代码dataDir=/usr/local/zookeeper/zookeeper1/data
dataLogDir=/usr/local/zookeeper/zookeeper1/logs
server.1=192.168.233.11:2888:3888
server.2=192.168.233.11:2889:3889
server.3=192.168.233.11:2890:3890

4、搭建zookeeper2

首先,复制改名。

1
2
bash复制代码cd /usr/local/zookeeper/
cp -r zookeeper1 zookeeper2

然后修改具体的某些配置:

1
bash复制代码vim zookeeper2/conf/zoo.cfg

将下图三个地方1改成2

从零开始搭建Kafka+SpringBoot分布式消息系统

1
bash复制代码vim zookeeper2/data/myid

同时将myid中的值改成2

从零开始搭建Kafka+SpringBoot分布式消息系统

5、搭建zookeeper3

同上,复制改名

1
bash复制代码cp -r zookeeper1 zookeeper3

从零开始搭建Kafka+SpringBoot分布式消息系统

1
bash复制代码vim zookeeper3/conf/zoo.cfg

修改为3

从零开始搭建Kafka+SpringBoot分布式消息系统

1
bash复制代码vim zookeeper3/data/myid

修改为3

从零开始搭建Kafka+SpringBoot分布式消息系统

6、测试zookeeper集群

1
bash复制代码cd /usr/local/zookeeper/zookeeper1/bin/

由于启动所需代码比较多,这里简单写了一个启动脚本:

1
sql复制代码vim start

start的内容如下

1
2
3
4
5
6
bash复制代码cd /usr/local/zookeeper/zookeeper1/bin/
./zkServer.sh start ../conf/zoo.cfg
cd /usr/local/zookeeper/zookeeper2/bin/
./zkServer.sh start ../conf/zoo.cfg
cd /usr/local/zookeeper/zookeeper3/bin/
./zkServer.sh start ../conf/zoo.cfg

下面是连接脚本:

1
复制代码vim login

login内容如下:

1
bash复制代码./zkCli.sh -server 192.168.233.11:2181,192.168.233.11:2182,192.168.233.11:2183

脚本编写完成,接下来启动:

1
2
sql复制代码sh start
sh login

启动集群成功,如下图:

从零开始搭建Kafka+SpringBoot分布式消息系统

这里zookeeper就告一段落了,由于zookeeper占用着输入窗口,这里可以在xshell右键标签,新建ssh渠道。然后就可以在新窗口继续操作kafka了!

从零开始搭建Kafka+SpringBoot分布式消息系统

三、搭建kafka集群

1、下载kafka

首先创建kafka目录:

1
bash复制代码mkdir /usr/local/kafka

然后在该目录下载

1
2
bash复制代码cd /usr/local/kafka/
wget https://archive.apache.org/dist/kafka/1.1.0/kafka_2.11-1.1.0.tgz

下载成功之后解压:

1
复制代码tar -zxvf kafka_2.11-1.1.0.tgz

2、修改集群配置

首先进入conf目录下:

1
bash复制代码cd /usr/local/kafka/kafka_2.11-1.1.0/config

修改server.properties
修改内容:

1
2
3
ini复制代码broker.id=0
log.dirs=/tmp/kafka-logs
listeners=PLAINTEXT://192.168.233.11:9092

复制两份server.properties

1
2
matlab复制代码cp server.properties server2.properties
cp server.properties server3.properties

修改server2.properties

1
matlab复制代码vim server2.properties

修改主要内容为:

1
2
3
ini复制代码broker.id=1
log.dirs=/tmp/kafka-logs1
listeners=PLAINTEXT://192.168.233.11:9093

如上,修改server3.properties
修改内容为:

1
2
3
ini复制代码broker.id=2
log.dirs=/tmp/kafka-logs2
listeners=PLAINTEXT://192.168.233.11:9094

3、启动kafka

这里还是在bin目录编写一个脚本:

1
2
bash复制代码cd ../bin/
vim start

脚本内容为:

1
2
3
bash复制代码./kafka-server-start.sh ../config/server.properties &
./kafka-server-start.sh ../config/server2.properties &
./kafka-server-start.sh ../config/server3.properties &

通过jps命令可以查看到,共启动了3个kafka。

从零开始搭建Kafka+SpringBoot分布式消息系统

4、创建Topic

1
2
bash复制代码cd /usr/local/kafka/kafka_2.11-1.1.0
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic my-replicated-topic

从零开始搭建Kafka+SpringBoot分布式消息系统

kafka打印了几条日志

从零开始搭建Kafka+SpringBoot分布式消息系统

在启动的zookeeper中可以通过命令查询到这条topic!

1
bash复制代码ls /brokers/topics

从零开始搭建Kafka+SpringBoot分布式消息系统

查看kafka状态

1
css复制代码bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic my-replicated-topic

从零开始搭建Kafka+SpringBoot分布式消息系统

可以看到此时有三个节点 1 , 2 , 0

Leader 是1 ,
因为分区只有一个 所以在0上面,
Replicas:主从备份是 1,2,0,
ISR(in-sync):现在存活的信息也是 1,2,0

5、启动生产者

1
bash复制代码bin/kafka-console-producer.sh --broker-list localhost:9092 --topic my-replicated-topic

由于不能按删除,不能按左右键去调整,所以语句有些乱啊。em…

从零开始搭建Kafka+SpringBoot分布式消息系统

6、启动消费者

1
javascript复制代码bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic my-replicated-topic

可以看出,启动消费者之后就会自动消费。

从零开始搭建Kafka+SpringBoot分布式消息系统

在生产者又造了一条。

从零开始搭建Kafka+SpringBoot分布式消息系统

消费者自动捕获成功!

从零开始搭建Kafka+SpringBoot分布式消息系统

四、集成springboot

先贴一张kafka兼容性目录:

从零开始搭建Kafka+SpringBoot分布式消息系统

不满足的话启动springboot的时候会抛异常的!!!ps:该走的岔路我都走了o(╥﹏╥)o
(我的kafka-clients是1.1.0,spring-kafka是2.2.2,中间那列暂时不用管)

从零开始搭建Kafka+SpringBoot分布式消息系统

回归正题,搞了两个小时,终于搞好了,想哭…
遇到的问题基本就是jar版本不匹配。
上面的步骤我也都会相应的去修改,争取大家按照本教程一遍过!!!

1、pom文件

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gzky</groupId>
<artifactId>study</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>study</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.3.8.RELEASE</version>
</dependency>

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

pom文件中,重点是下面这两个版本。

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>

2、application.yml

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
yaml复制代码spring:
redis:
cluster:
#设置key的生存时间,当key过期时,它会被自动删除;
expire-seconds: 120
#设置命令的执行时间,如果超过这个时间,则报错;
command-timeout: 5000
#设置redis集群的节点信息,其中namenode为域名解析,通过解析域名来获取相应的地址;
nodes: 192.168.233.11:9001,192.168.233.11:9002,192.168.233.11:9003,192.168.233.11:9004,192.168.233.11:9005,192.168.233.11:9006
kafka:
# 指定kafka 代理地址,可以多个
bootstrap-servers: 192.168.233.11:9092,192.168.233.11:9093,192.168.233.11:9094
producer:
retries: 0
# 每次批量发送消息的数量
batch-size: 16384
buffer-memory: 33554432
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
# 指定默认消费者group id
group-id: test-group
auto-offset-reset: earliest
enable-auto-commit: true
auto-commit-interval: 100
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer

server:
port: 8085
servlet:
#context-path: /redis
context-path: /kafka

没有配置Redis的可以把Redis部分删掉,也就是下图:
想学习配置Redis集群的可以参考:《Redis集群redis-cluster的搭建及集成springboot》

从零开始搭建Kafka+SpringBoot分布式消息系统

3、生产者

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
typescript复制代码package com.gzky.study.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

/**
* kafka生产者工具类
*
* @author biws
* @date 2019/12/17
**/
@Component
public class KfkaProducer {

private static Logger logger = LoggerFactory.getLogger(KfkaProducer.class);

@Autowired
private KafkaTemplate<String, String> kafkaTemplate;

/**
* 生产数据
* @param str 具体数据
*/
public void send(String str) {
logger.info("生产数据:" + str);
kafkaTemplate.send("testTopic", str);
}
}

4、消费者

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
kotlin复制代码package com.gzky.study.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

/**
* kafka消费者监听消息
*
* @author biws
* @date 2019/12/17
**/
@Component
public class KafkaConsumerListener {

private static Logger logger = LoggerFactory.getLogger(KafkaConsumerListener.class);

@KafkaListener(topics = "testTopic")
public void onMessage(String str){
//insert(str);//这里为插入数据库代码
logger.info("监听到:" + str);
System.out.println("监听到:" + str);
}

}

5、对外接口

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
less复制代码package com.gzky.study.controller;

import com.gzky.study.utils.KfkaProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* kafka对外接口
*
* @author biws
* @date 2019/12/17
**/
@RestController
public class KafkaController {

@Autowired
KfkaProducer kfkaProducer;

/**
* 生产消息
* @param str
* @return
*/
@RequestMapping(value = "/sendKafkaWithTestTopic",method = RequestMethod.GET)
@ResponseBody
public boolean sendTopic(@RequestParam String str){
kfkaProducer.send(str);
return true;
}
}

6、postman测试

这里首先应该在服务器启动监听器(kafka根目录),下面命令必须是具体的服务器ip,不能是localhost,是我踩过的坑:

推荐此处重启一下集群
关闭kafka命令:

1
2
3
4
bash复制代码cd /usr/local/kafka/kafka_2.11-1.1.0/bin
./kafka-server-stop.sh ../config/server.properties &
./kafka-server-stop.sh ../config/server2.properties &
./kafka-server-stop.sh ../config/server3.properties &

此处应该jps看一下,等待所有的kafka都关闭(关不掉的kill掉),再重新启动kafka:

1
2
3
bash复制代码./kafka-server-start.sh ../config/server.properties &
./kafka-server-start.sh ../config/server2.properties &
./kafka-server-start.sh ../config/server3.properties &

等待kafka启动成功后,启动消费者监听端口:

1
2
bash复制代码cd /usr/local/kafka/kafka_2.11-1.1.0
bin/kafka-console-consumer.sh --bootstrap-server 192.168.233.11:9092 --from-beginning --topic testTopic

从零开始搭建Kafka+SpringBoot分布式消息系统

曾经我乱输的测试信息全部被监听过来了!

启动springboot服务

从零开始搭建Kafka+SpringBoot分布式消息系统

然后用postman生产消息:

从零开始搭建Kafka+SpringBoot分布式消息系统

然后享受成果,服务器端监听成功。

从零开始搭建Kafka+SpringBoot分布式消息系统

项目中也监听成功!

从零开始搭建Kafka+SpringBoot分布式消息系统

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot整合Mybatis-Plus 实战之动态

发表于 2020-12-03

MyBatis的动态SQL是最令人喜欢的功能

在了解 动态SQL之前,你首先得知道一个表达式 OGNL,这个是基础!

在这里插入图片描述

  • 面试常问问题 : Mybatis 中$与#的区别?
  • #是将传入的值当做字符串的形式,select id,name,age from test where id =#{id},

当把id值传入到后台的时候,就相当于 select id,name,age from test where id =‘1’.

  • “”是将传入的数据直接显示生成sql语句,selectid,name,agefromtestwhereid=”是将传入的数据直接显示生成sql语句,select id,name,age from test where id = “是将传入的数据直接显示生成sql语句,selectid,name,agefromtestwhereid={id},

当把id值1,传入到后台的时候,就相当于 select id,name,age from test where id = 1.

  • 使用#可以很大程度上防止sql注入。(语句的拼接)

if 标签

  • mapper

select
from test
where 1=1

and username like concat(‘%’, #{username}, ‘%’)

and ip=#{ip}

  • 在mapper 接口中映射这个方法

List selectByTestSelective(Test example);

下面每个标签都会有对应的方法,但下文没有一一写出,现参考如下

1
2
3
4
5
6
7
8
scss复制代码List<Test> selectByExample(TestExample example);
List<Test> selectByTestSelective(Test example);
List<Test> selectByIdOrUserName(Test example);
List<Test> selectByTestSelectiveWhereTag(Test example);
List<Test> selectByTestIdList(List<Integer> ids);
int insertList(List<Test> students);
int updateTestSetTag(Test example);
int selectSelectiveTrim(Test example);
  • 测试
1
2
3
4
5
6
7
8
9
10
less复制代码@RequestMapping(value = "/dongtaiSql")
@ResponseBody
public void dongtaiSql() {
Test example = new Test();
example.setUsername("周");
List<Test> selectByTestSelective = testMapper.selectByTestSelective(example);
for (Test test : selectByTestSelective) {
System.out.println(test.getUsername());
}
}
  • 打印结果

在这里插入图片描述

  • 也就是说,你传什么值 它会根据你传的值来拼接sql,不传值则不拼接,这种相对来说比较简单,易于理解。

【注意】 下文所有的请求都是通过postman发出的。

在这里插入图片描述

include标签

  • 一个非常好用的辅助性标签,用于放一些公共的返回结果集,方便其他的查询方法使用,比如在mapper中使用方式如下:

username, lastloginTime

select

from test
where id = #{id,jdbcType=BIGINT}

choose标签 ,配合when ,otherwise 标签使用

choose when otherwise 标签可以帮我们实现 if else 的逻辑。一个 choose 标签至少有一个 when,

最多一个otherwise。

  • mapper

select

from test
where 1=1

and id=#{id}

and username=#{username}

and 1=2

  • 打印结果

在这里插入图片描述

找不到 周 ,因为我只有周杰伦或者周杰 。 这个choose和 if 的功能有点类似,但是和if 不同的是choose 有点你有什么我就根据你给的查,而if 则是你传了所有条件,我逐个判断你的条件然后给你查。if 更多适用于表单查询的时候用。而choose 更多的时候。。。其实这两个达到的目的是一样的,我更喜欢用choose.

where 标签

  • mapper

select

from test

and username like concat(‘%’, #{username}, ‘%’)

and ip=#{ip}

  • 结果

在这里插入图片描述

  • 我什么条件也没传,他在where中找不到匹配的条件就查找了全部给了我,这种其实和上面choose 中的最后那个条件有异曲同工之处,上变改成1=1 一样的效果。

foreach 标签

  • mapper

select

from test
where id in

#{id}

  • 代码
1
2
3
4
5
6
7
8
9
10
11
less复制代码@RequestMapping(value = "/dongtaiSql3")
@ResponseBody
public void dongtaiSql3() {
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(6);
arrayList.add(5);
List<Test> selectByTestSelective = testMapper.selectByTestIdList(arrayList);
for (Test test : selectByTestSelective) {
System.out.println(test.getUsername());
}
}
  • 结果

在这里插入图片描述

  • 这个标签太好用了,foreach 也可以用来批量插入数据,比如:
  • mapper

insert into test(username, gender, ip)
values

(
#{test.username}, #{test.gender},#{test.ip}
)

  • 代码

@RequestMapping(value = “/dongtaiSql4”)
@ResponseBody
public void dongtaiSql4() {
Test example = new Test();
example.setUsername(“刘德华”);
example.setGender(1);
example.setIp(“123232113111”);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码	Test example2 = new Test();
example2.setUsername("郭富城");
example2.setGender(1);
example2.setIp("123232113122");

Test example3 = new Test();
example3.setUsername("邱淑贞");
example3.setGender(0);
example3.setIp("123232113333");
ArrayList<Test> arrayList = new ArrayList<Test>();
arrayList.add(example);
arrayList.add(example2);
arrayList.add(example3);

int selectByTestSelective = testMapper.insertList(arrayList);
if (selectByTestSelective == 1) {
System.out.println("批量插入:"+arrayList.size()+"条数据");
}
}
  • 结果

在这里插入图片描述

  • 妈的 这里的mapper 每次修改都要重新启动,很是麻烦。注意这里 #{test.username}, #{test.gender},#{test.ip} 最后不要有逗号,否则会报一个sql语法错误,原因是多了,。还有就是这里如果传的值是list等非实体类的参数的时候,是不用声明parameterType 的。
  • foreach 的变量说明

collection: 必填, 集合/数组/Map的名称.
item: 变量名。即从迭代的对象中取出的每一个值
index: 索引的属性名。当迭代的对象为 Map 时, 该值为 Map 中的 Key.
open: 循环开头的字符串
close: 循环结束的字符串
separator: 每次循环的分隔符

bind 标签

  • 使用 bind 来让该 SQL 达到支持两个数据库的作用
  • mapper

select

from test
where 1=1

and username like #{nameLike} and ip=#{ip} * 代码

@RequestMapping(value = “/dongtaiSql”)
@ResponseBody
public void dongtaiSql() {
Test example = new Test();
example.setUsername(“周”);
List selectByTestSelective = testMapper.selectByTestSelective(example);
for (Test test : selectByTestSelective) {
System.out.println(test.getUsername());
}
}

  • 结果

在这里插入图片描述

发现依然可以。 说明这个bind 就是绑定一些变量的 ,nameLike 就代表了username 的模糊搜索,就是如果别的地方用得到它的模糊搜索,拿来用即可。用法是 like 后面直接加上 #{nameLike }。

set 标签

这个标签常用于做修改语句,比如

  • mapper

UPDATE
Products

username = #{username},

ip = #{ip},

id = #{id}

  • 代码

@RequestMapping(value = “/dongtaiSql5”)
@ResponseBody
public void dongtaiSql5() {
Test example = new Test();
example.setUsername(“周”);
example.setIp(“cium”);
example.setId(27);
int selectByTestSelective = testMapper.updateTestSetTag(example);
System.out.println(selectByTestSelective);

1
复制代码}
  • 结果

在这里插入图片描述

  • 这个set 说白了就是update语句的 set 时候的一个灵活操作。

trim 标签

  • mapper

select * from test
   
     AND username=#{username}
     AND ip=#{ip}

  • 【注意】
  • 这里有很多坑,首先mybatis-plus 中不是 prefixoverride 而是prefixOverrides
  • 然后”AND |OR” 必须有空格,原因如下图
  • 如果 ip 不是字符串就不能用length() 方法

在这里插入图片描述

trim标签各参数的说明

1
2
3
4
vbnet复制代码prefix:在trim标签内sql语句加上前缀。
suffix:在trim标签内sql语句加上后缀。
prefixOverrides:指定去除多余的前缀内容
suffixOverrides:指定去除多余的后缀内容,如:suffixOverrides=",",去除trim标签内sql语句多余的后缀","。

在这里插入图片描述

然而我在配置的时候却遇到了更坑的问题,迟迟得不到解决…欢迎有兴趣的朋友一起交流下解决最后这个问题。

最后

感谢大家看到这里,文章有不足,欢迎大家指出;如果你觉得写得不错,那就给我一个赞吧。

也欢迎大家关注我的公众号:Java程序员聚集地,麦冬每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

synchronized的对象锁和类锁的区别

发表于 2020-12-03

synchronized可以锁对象,代码块,类对象 ,那么他们用起来会有区别吗,这里我们用实际代码来探究下

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
java复制代码package com.example.hxk.thread.synchroized;

public class SyncTest1 {

// synchronized修饰实例方法
public synchronized void test1() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

// synchronized代码块
public void test2() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public static void main(String[] args) {
SyncTest1 t1 = new SyncTest1();

new Thread(new Runnable() {
@Override
public void run() {
t1.test1();
}
}, "thread1").start();

new Thread(new Runnable() {
@Override
public void run() {
t1.test2();
}
}, "thread2").start();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
java复制代码thread1 : 0
thread1 : 1
thread1 : 2
thread1 : 3
thread1 : 4
thread2 : 0
thread2 : 1
thread2 : 2
thread2 : 3
thread2 : 4

这里thread2会等thread1运行完成才会开始运行,说明thread1和thread2请求的是同一把锁,也就说明了 synchronized代码块锁当前对象和锁实例方法,他们的效果是一样的, 锁的都是当前对象。

然后我们再来看看类锁,修改代码如下

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
java复制代码package com.example.hxk.thread.synchroized;

public class SyncTest1 {

// 修饰静态方法
public static synchronized void test1() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

// 修饰类对象
public void test2() {
synchronized (SyncTest1.class) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public static void main(String[] args) {
SyncTest1 t1 = new SyncTest1();

new Thread(new Runnable() {
@Override
public void run() {
SyncTest1.test1();
}
}, "thread1").start();

new Thread(new Runnable() {
@Override
public void run() {
t1.test2();
}
}, "thread2").start();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
java复制代码thread1 : 0
thread1 : 1
thread1 : 2
thread1 : 3
thread1 : 4
thread2 : 0
thread2 : 1
thread2 : 2
thread2 : 3
thread2 : 4

这里可以看到thread2也是被thread1阻塞,所以他们持有的是同一把锁,也就说明synchronized修饰静态方法和锁类对象,他们的效果是一样的。
接下来我们同时用类锁和对象锁试试,代码如下

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
java复制代码package com.example.hxk.thread.synchroized;

public class SyncTest1 {

// 修饰静态方法
public static synchronized void test1() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

// 修饰类对象
public synchronized void test2() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
SyncTest1 t1 = new SyncTest1();

new Thread(new Runnable() {
@Override
public void run() {
SyncTest1.test1();
}
}, "thread1").start();

new Thread(new Runnable() {
@Override
public void run() {
t1.test2();
}
}, "thread2").start();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
java复制代码thread1 : 0
thread2 : 0
thread1 : 1
thread2 : 1
thread2 : 2
thread1 : 2
thread2 : 3
thread1 : 3
thread2 : 4
thread1 : 4

运行结果是交替进行的,说明对象锁和类锁锁的不是同一个锁,他们是两个锁,互不影响

总结:

1,synchronized修饰在实例方法上和synchronized(this){} 同步代码块效果是一样的

2,synchronized修饰在静态方法上和 synchronized (SyncTest1.class) {} 同步代码块效果是一样的

3,synchronized修饰在实例方法表示锁的是当前对象,修饰静态方法表示锁的是类对象(一个类在jvm中只有一个class对象)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聊一聊 155K 的 FileSaver,是如何工作的?

发表于 2020-12-03

FileSaver.js 是在客户端保存文件的解决方案,非常适合在客户端上生成文件的 Web 应用程序。它简单易用且兼容大多数浏览器,被作为项目依赖应用在 6.3 万的项目中。在近期的项目中,阿宝哥再一次使用到了它,所以就想写篇文章来聊一聊这个优秀的开源项目。

一、FileSaver.js 简介

FileSaver.js 是 HTML5 的 saveAs() FileSaver 实现。它支持大多数主流的浏览器,其兼容性如下图所示:

(图片来源:github.com/eligrey/Fil…

关注「全栈修仙之路」阅读阿宝哥原创的 3 本免费电子书(累计下载2万+)及 50 几篇 “重学TS” 教程。

1.1 saveAs API

1
2
3
4
> arduino复制代码FileSaver saveAs(Blob/File/Url, optional DOMString filename, optional Object { autoBom })
>
>
>

saveAs 方法支持三个参数,第一个参数表示它支持 Blob/File/Url 三种类型,第二个参数表示文件名(可选),而第三个参数表示配置对象(可选)。如果你需要 FlieSaver.js 自动提供 Unicode 文本编码提示(参考:字节顺序标记),则需要设置 { autoBom: true}。

1.2 保存文本

1
2
javascript复制代码let blob = new Blob(["大家好,我是阿宝哥!"], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, "hello.txt");

1.3 保存线上资源

1
javascript复制代码FileSaver.saveAs("https://httpbin.org/image", "image.jpg");

如果下载的 URL 地址与当前站点是同域的,则将使用 a[download] 方式下载。否则,会先使用 同步的 HEAD 请求 来判断是否支持 CORS 机制,若支持的话,将进行数据下载并使用 Blob URL 实现文件下载。如果不支持 CORS 机制的话,将会尝试使用 a[download] 方式下载。

标准的 W3C File API Blob 接口并非在所有浏览器中都可用,对于这个问题,你可以考虑使用 Blob.js 来解决兼容性问题。

(图片来源:caniuse.com/?search=blo…

1.4 保存 Canvas 画布内容

1
2
3
4
javascript复制代码let canvas = document.getElementById("my-canvas");
canvas.toBlob(function(blob) {
saveAs(blob, "abao.png");
});

需要注意的是 canvas.toBlob() 方法并非在所有浏览器中都可用,对于这个问题,你可以考虑使用 canvas-toBlob.js 来解决兼容性问题。

(图片来源:caniuse.com/?search=toB…

在以上的示例中,我们多次见到 Blob 的身影,因此在介绍 FileSaver.js 源码时,阿宝哥先来简单介绍一下 Blob 的相关知识。

二、Blob 简介

Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据。

2.1 Blob 构造函数

Blob 由一个可选的字符串 type(通常是 MIME 类型)和 blobParts 组成:

MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型,是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。

常见的 MIME 类型有:超文本标记语言文本 .html text/html、PNG 图像 .png image/png、普通文本 .txt text/plain 等。

在 JavaScript 中我们可以通过 Blob 的构造函数来创建 Blob 对象,Blob 构造函数的语法如下:

1
javascript复制代码var aBlob = new Blob(blobParts, options);

相关的参数说明如下:

  • blobParts:它是一个由 ArrayBuffer,ArrayBufferView,Blob,DOMString 等对象构成的数组。DOMStrings 会被编码为 UTF-8。
  • options:一个可选的对象,包含以下两个属性:
    • type —— 默认值为 "",它代表了将会被放入到 blob 中的数组内容的 MIME 类型。
    • endings —— 默认值为 "transparent",用于指定包含行结束符 \n 的字符串如何被写入。 它是以下两个值中的一个: "native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持 blob 中保存的结束符不变。

介绍完 Blob 之后,我们再来介绍一下 Blob URL。

2.2 Blob URL

Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。在浏览器中,我们使用 URL.createObjectURL 方法来创建 Blob URL,该方法接收一个 Blob 对象,并为其创建一个唯一的 URL,其形式为 blob:<origin>/<uuid>,对应的示例如下:

1
javascript复制代码blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 较短,但可以访问 Blob。生成的 URL 仅在当前文档打开的状态下才有效。它允许引用 <img>、<a> 中的 Blob,但如果你访问的 Blob URL 不再存在,则会从浏览器中收到 404 错误。

上述的 Blob URL 看似很不错,但实际上它也有副作用。 虽然存储了 URL → Blob 的映射,但 Blob 本身仍驻留在内存中,浏览器无法释放它。映射在文档卸载时自动清除,因此 Blob 对象随后被释放。但是,如果应用程序寿命很长,那不会很快发生。因此,如果我们创建一个 Blob URL,即使不再需要该 Blob,它也会存在内存中。

针对这个问题,我们可以调用 URL.revokeObjectURL(url) 方法,从内部映射中删除引用,从而允许删除 Blob(如果没有其他引用),并释放内存。

好的,现在我们已经介绍了 Blob 和 Blob URL。如果你还意犹未尽,想深入理解 Blob 的话,可以阅读 你不知道的 Blob 这篇文章,接下来我们开始分析 FileSaver.js 的源码。

如果你想了解阅读源码的思路与技巧,可以阅读 使用这些思路与技巧,我读懂了多个优秀的开源项目 这篇文章。

三、FileSaver.js 源码解析

在 FileSaver.js 内部提供了三种方案来实现文件保存,因此接下来我们将分别来介绍这三种方案。

3.1 方案一

当 FileSaver.js 在保存文件时,如果当前平台中 a 标签支持 download 属性且非 MacOS WebView 环境,则会优先使用 a[download] 来实现文件保存。在具体使用过程中,我们是通过调用 saveAs 方法来保存文件,该方法的定义如下:

1
javascript复制代码FileSaver saveAs(Blob/File/Url, optional DOMString filename, optional Object { autoBom })

通过观察 saveAs 方法的签名,我们可知该方法支持字符串和 Blob 两种类型的参数,因此在 saveAs 方法内部需要分别处理这两种类型的参数,下面我们先来分析字符串参数的情形。

3.1.1 字符串类型参数

在前面的示例中,我们演示了如何利用 saveAs 方法来保存线上的图片:

1
javascript复制代码FileSaver.saveAs("https://httpbin.org/image", "image.jpg");

在方案一中,saveAs 方法的处理逻辑如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
javascript复制代码// Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
function saveAs(blob, name, opts) {
var URL = _global.URL || _global.webkitURL;
var a = document.createElement("a");
name = name || blob.name || "download";

a.download = name;
a.rel = "noopener";

if (typeof blob === "string") {
a.href = blob;
if (a.origin !== location.origin) { // (1)
corsEnabled(a.href)
? download(blob, name, opts)
: click(a, (a.target = "_blank"));
} else { // (2)
click(a);
}
} else {
// 省略处理Blob类型参数
}
}

在以上代码中,如果发现下载资源的 URL 地址与当前站点是非同域的,则会先使用 同步的 HEAD 请求 来判断是否支持 CORS 机制,若支持的话,就会调用 download 方法进行文件下载。首先我们先来分析 corsEnabled 方法:

1
2
3
4
5
6
7
8
javascript复制代码function corsEnabled(url) {
var xhr = new XMLHttpRequest();
xhr.open("HEAD", url, false);
try {
xhr.send();
} catch (e) {}
return xhr.status >= 200 && xhr.status <= 299;
}

corsEnabled 方法的实现很简单,就是通过 XMLHttpRequest API 发起一个同步的 HEAD 请求,然后判断返回的状态码是否在 [200 ~ 299] 的范围内。接着我们来看一下 download 方法的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码function download(url, name, opts) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.onload = function () {
saveAs(xhr.response, name, opts);
};
xhr.onerror = function () {
console.error("could not download file");
};
xhr.send();
}

同样 download 方法的实现也很简单,也是通过 XMLHttpRequest API 来发起 HTTP 请求,与大家熟悉的 JSON 格式不同的是,我们需要设置 responseType 的类型为 blob。此外,因为返回的结果是 blob 类型的数据,所以在成功回调函数内部会继续调用 saveAs 方法来实现文件保存。

而对于不支持 CORS 机制或同域的情形,它会调用内部的 click 方法来完成下载功能,该方法的具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码// `a.click()` doesn't work for all browsers (#465)
function click(node) {
try {
node.dispatchEvent(new MouseEvent("click"));
} catch (e) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent(
"click", true, true, window, 0, 0, 0, 80, 20,
false, false, false, false, 0, null
);
node.dispatchEvent(evt);
}
}

在 click 方法内部,会优先调用 node 对象上的 dispatchEvent 方法来派发 click 事件。当出现异常的时候,会在 catch 语句进行相应的异常处理,catch 语句中的 MouseEvent.initMouseEvent() 方法用于初始化鼠标事件的值。但需要注意的是,该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。

3.1.2 blob 类型参数

同样,在前面的示例中,我们演示了如何利用 saveAs 方法来保存 Blob 类型数据:

1
2
javascript复制代码let blob = new Blob(["大家好,我是阿宝哥!"], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, "hello.txt");

blob 类型参数的处理逻辑,被定义在 saveAs 方法体的 else 分支中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
javascript复制代码// Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
function saveAs(blob, name, opts) {
var URL = _global.URL || _global.webkitURL;
var a = document.createElement("a");
name = name || blob.name || "download";

a.download = name;
a.rel = "noopener";

if (typeof blob === "string") {
// 省略处理字符串类型参数
} else {
a.href = URL.createObjectURL(blob);
setTimeout(function () {
URL.revokeObjectURL(a.href);
}, 4e4); // 40s
setTimeout(function () {
click(a);
}, 0);
}
}

对于 blob 类型的参数,首先会通过 createObjectURL 方法来创建 Object URL,然后在通过 click 方法执行文件保存。为了能及时释放内存,在 else 处理分支中,会启动一个定时器来执行清理操作。此时,方案一我们已经介绍完了,接下去要介绍的方案二主要是为了兼容 IE 浏览器。

3.2 方案二

在 Internet Explorer 10 浏览器中,msSaveBlob 和 msSaveOrOpenBlob 方法允许用户在客户端上保存文件,其中 msSaveBlob 方法只提供一个保存按钮,而 msSaveOrOpenBlob 方法提供了保存和打开按钮,对应的使用方式如下所示:

1
2
javascript复制代码window.navigator.msSaveBlob(blobObject, 'msSaveBlob_hello.txt');
window.navigator.msSaveOrOpenBlob(blobObject, 'msSaveBlobOrOpenBlob_hello.txt');

了解完上述的知识和方案一中介绍的 corsEnabled、download 和 click 方法后,再来看方案二的代码,就很清晰明了。在满足 "msSaveOrOpenBlob" in navigator 条件时, FileSaver.js 会使用方案二来实现文件保存。跟前面一样,我们先来分析 字符串类型参数 的处理逻辑。

3.2.1 字符串类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
javascript复制代码// Use msSaveOrOpenBlob as a second approach
function saveAs(blob, name, opts) {
name = name || blob.name || "download";
if (typeof blob === "string") {
if (corsEnabled(blob)) { // 判断是否支持CORS
download(blob, name, opts);
} else {
var a = document.createElement("a");
a.href = blob;
a.target = "_blank";
setTimeout(function () {
click(a);
});
}
} else {
// 省略处理Blob类型参数
}
}
3.2.2 blob 类型参数
1
2
3
4
5
6
7
8
9
javascript复制代码// Use msSaveOrOpenBlob as a second approach
function saveAs(blob, name, opts) {
name = name || blob.name || "download";
if (typeof blob === "string") {
// 省略处理字符串类型参数
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name); // 提供了保存和打开按钮
}
}

3.3 方案三

如果方案一和方案二都不支持的话,FileSaver.js 就会降级使用 FileReader API 和 open API 新开窗口来实现文件保存。

3.3.1 字符串类型参数
1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码// Fallback to using FileReader and a popup
function saveAs(blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open("", "_blank");
if (popup) {
popup.document.title = popup.document.body.innerText = "downloading...";
}

if (typeof blob === "string") return download(blob, name, opts);
// 处理Blob类型参数
}
3.3.2 blob 类型参数

对于 blob 类型的参数来说,在 saveAs 方法内部会根据不同的环境选用不同的方案,比如在 Safari 浏览器环境中,它会利用 FileReader API 先把 Blob 对象转换为 Data URL,然后再把该 Data URL 地址赋值给新开的窗口或当前窗口的 location 对象,具体的代码如下:

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
javascript复制代码// Fallback to using FileReader and a popup
function saveAs(blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open("", "_blank");
if (popup) { // 设置新开窗口的标题
popup.document.title = popup.document.body.innerText = "downloading...";
}

if (typeof blob === "string") return download(blob, name, opts);

var force = blob.type === "application/octet-stream"; // 二进制流数据
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);

if (
(isChromeIOS || (force && isSafari) || isMacOSWebView) &&
typeof FileReader !== "undefined"
) {
// Safari doesn't allow downloading of blob URLs
var reader = new FileReader();
reader.onloadend = function () {
var url = reader.result;
url = isChromeIOS
? url
: url.replace(/^data:[^;]*;/, "data:attachment/file;"); // 处理成附件的形式
if (popup) popup.location.href = url;
else location = url;
popup = null; // reverse-tabnabbing #460
};
reader.readAsDataURL(blob);
} else {
// 省略Object URL的处理逻辑
}
}

其实对于 FileReader API 来说,除了支持把 File/Blob 对象转换为 Data URL 之外,它还提供了 readAsArrayBuffer() 和 readAsText() 方法,用于把 File/Blob 对象转换为其它的数据格式。在 玩转前端二进制 文章中,阿宝哥详细介绍了 FileReader API 在前端图片处理场景中的应用,阅读完该文章之后,你们将能轻松看懂以下转换关系图:

最后我们再来看一下 else 分支的代码:

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
javascript复制代码function saveAs(blob, name, opts, popup) {
popup = popup || open("", "_blank");
if (popup) {
popup.document.title = popup.document.body.innerText = "downloading...";
}

// 处理字符串类型参数
if (typeof blob === "string") return download(blob, name, opts);

if (
(isChromeIOS || (force && isSafari) || isMacOSWebView) &&
typeof FileReader !== "undefined"
) {
// 省略FileReader API处理逻辑
} else {
var URL = _global.URL || _global.webkitURL;
var url = URL.createObjectURL(blob);
if (popup) popup.location = url;
else location.href = url;
popup = null; // reverse-tabnabbing #460
setTimeout(function () {
URL.revokeObjectURL(url);
}, 4e4); // 40s
}
}

到这里 FileSaver.js 这个库的源码已经分析完成了,跟着阿宝哥阅读上述源码之后,是不是觉得写一个兼容性好、简单易用的第三方库是多么不容易。在实际项目中,如果你需要保存超过 blob 大小限制的超大文件,或者没有足够的内存空间,你可以考虑使用更高级的 StreamSaver.js 库来实现文件保存功能。

关注「全栈修仙之路」阅读阿宝哥原创的 3 本免费电子书(累计下载2万+)及 8 篇源码分析系列教程。

四、参考资源

  • 你不知道的 Blob
  • 玩转前端二进制
  • MDN - FileReader API
  • 百度百科 - 字节顺序标记(ByteOrderMark)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

更加优雅的Token认证方式JWT

发表于 2020-12-02

基于Token的认证

通过上一篇你大体已经了解session和cookie认证了,session认证需要服务端做大量的工作来保证session信息的一致性以及session的存储,所以现代的web应用在认证的解决方案上更倾向于客户端方向,cookie认证是基于客户端方式的,但是cookie缺点也很明显,到底有哪些缺点可以跳转上一次的文章。那有没有一种比较折中的方案呢?有的

把认证信息保存在客户端,关键点就是安全的验证,如果能解决认证信息的安全性问题,完全可以把认证信息保存在客户端,服务端完全无认证状态,这样的话服务端扩展起来要方便很多。关于信息的安全解决方案,现在普遍的做法就是签名机制,像微信公众接口的验证方式就基于签名机制。

签名,就是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。

当用户成功登陆系统并成功验证有效之后,服务器会利用某种机制产生一个token字符串,这个token中可以包含很多信息,例如来源IP,过期时间,用户信息等, 把这个字符串下发给客户端,客户端在之后的每次请求中都携带着这个token,携带方式其实很自由,无论是cookie方式还是其他方式都可以,但是必须和服务端协商一致才可以。当然这里我不推荐cookie。当服务端收到请求,取出token进行验证(可以验证来源ip,过期时间等信息),如果合法则允许进行操作。

基于token的验证方式也是现代互联网普通使用的认证方式,那它有什么优点吗?

  1. 支持跨域访问,Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
  2. 无状态:Token机制在服务端不需要存储session信息,因为Token自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
  3. 解耦 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
  4. 适用性更广:只要是支持http协议的客户端,就可以使用token认证。
  5. 服务端只需要验证token的安全,不必再去获取登录用户信息,因为用户的登录信息已经在token信息中。
  6. 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python,PHP)和多家公司的支持(如:Firebase,Google, Microsoft).

那基于token的认证方式有哪些缺点呢?

  1. 网络传输的数据量增大:由于token中存储了大量的用户和安全相关的信息,所以比单纯的cookie信息要大很多,传输过程中需要消耗更多流量,占用更多带宽,
  2. 和所有的客户端认证方式一样,如果想要在服务端控制token的注销有难度,而且也很难解决客户端的劫持问题。
  3. 由于token信息在服务端增加了一次验证数据完整性的操作,所以比session的认证方式增加了cpu的开销。

但是整体来看,基于token的认证方式还是比session和cookie方式要有很大优势。在所知的token认证中,jwt是一种优秀的解决方案

jwt

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部

header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

1
2
3
4
json复制代码{
"alg": "HS256",
"typ": "JWT"
}
Payload

Payload 部分也是一个JSON对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

1
2
3
4
5
6
7
scss复制代码iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

除了以上字段之外,你完全可以添加自己想要的任何字段,这里还是提醒一下,由于jwt的标准,信息是不加密的,所以一些敏感信息最好不要添加到json里面

1
2
3
4
json复制代码{
"Name":"菜菜",
"Age":18
}
Signature

为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥(这个秘钥只有服务端知道),签名算法是header中指定的那个,然对它们签名即可。

1
scss复制代码HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。需要提醒一下:base64是一种编码方式,并非加密方式。

写在最后

基于token的认证方式,大体流程为:

  1. 客户端携带用户的登录凭证(一般为用户名密码)提交请求
  2. 服务端收到登录请求,验证凭证正确性,如果正确则按照协议规定生成token信息,经过签名并返回给客户端
  3. 客户端收到token信息,可以保存在cookie或者其他地方,以后每次请求的时候都携带上token信息
  4. 业务服务器收到请求,验证token的正确性,如果正确则进行下一步操作

image

这里再重复一次,无论是token认证,cookie认证,还是session认证,一旦别人拿到客户端的标识,还是可以伪造操作。所以采用任何一种认证方式的时候请考虑加入来源ip或者白名单,过期时间,另外有条件的情况下一定要使用https。

更多精彩文章

  • 分布式大并发系列
  • 架构设计系列
  • 趣学算法和数据结构系列
  • 设计模式系列

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

架构设计:文件服务存储设计 架构调整 本地存储 多租户 单目

发表于 2020-12-02

在架构设计:文件服务的设计与实现一文中,通过实现一个文件服务来梳理了一个架构设计的一般流程,并得到如下静态架构图

架构设计:文件服务存储设计

本文继续聊聊文件服务中的子模块:「存储模块」的设计,包括:

  • 引入「存储模块」后的架构调整
  • 本地文件存储
  • libfuse
  • RAID
  • 分布式文件存储

架构调整

前面的架构没有对存储进行特别设计,直接使用了本地存储。考虑到后期文件数量可能会越来越多,本地存储可能无法支撑,且本地存储的安全性也没有保障。为了便于后期扩展,需要对「存储」部分进行设计。

存储的方式有很多,本地存储、NAS、分布式存储,为了能支持不同的存储方式,需要对「存储模块」进行抽象。考虑到「存储模块」涉及到IO,是一个相对底层的模块。「上传」这个核心模块不能依赖于具体的存储,所以这里也需要对其进行依赖反转。

架构设计:文件服务存储设计

见紫色部分,UploadService调用了FileInfoRepository来存储FileInfo,而FileInfoRepository是个接口,具体实现由存储模块中的实现类来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public interface FileInfoRepository {
public Path save(FileInfo fileInfo) throws IOException;
}
public class LocalFileInfoRepository implements FileInfoRepository {
public Path save(FileInfo fileInfo) throws IOException {
...
}
}
public class NASFileInfoRepository implements FileInfoRepository {
public Path save(FileInfo fileInfo) throws IOException {
...
}
}
public class DistributedFileInfoRepository implements FileInfoRepository {
public Path save(FileInfo fileInfo) throws IOException {
...
}
}

本地存储

我们先看本地存储。最简单的实现,就是直接使用IO将文件写到对应的目录下就可以了。但是,本地存储会有如下几个问题:

  • 如果文件服务支持多租户,所有租户文件都写在同一个目录下,没有做区分,可能导致文件混乱,迁移繁琐。且文件数量增加会比较快。
  • 随着文件数量的增多,某些文件系统的访问速度可能会下降
  • 文件数增加,单机磁盘可能容量不够
  • 没有容错和备份,磁盘损坏可能导致数据的永久丢失

下面我们针对上面的问题,来一个个的解决。

多租户

首先,对于多租户来说,在我们的架构中,实际对应的是Group,我们按照Group的不同,来划分目录即可。即不同的租户有不同的文件根目录,后期某个租户迁移时,直接迁移对应目录即可。这也稍微解决了单目录文件数量多的问题。

单目录文加数量过多

对于单目录下,随着文件数量的增加导致访问速度下降的问题,我们该如何解决呢?

如果你做过分布式系统,那么想一想,我们是否可以把单目录看成是一个服务器,访问目录下的文件看成是一个个的请求呢?如果可以,那解决单目录下访问速度慢的问题是不是就变成了「如何解决单服务器下,负载过高」的问题了?那解决服务端负载过高的方法是否适用于解决目录访问速度下降的问题呢?

我们从下面几个方面来分析一下:

  • 解决服务端负载过高的方法
  • 目录访问和服务器的区别
  • 解决服务端负载过高的方法对目录的适用性

首先来看「解决服务端负载过高的方法」!答案很明显:分流+负载均衡!

分布式服务的负载均衡有几种方式呢?

  • 随机
  • 轮询
  • 加权随机
  • 加权轮询
  • 源地址Hash
  • 一致性Hash

再来看「目录访问和服务器的区别」,虽然可以把目录看成服务器,但是两者还是有区别的:

  • 部署一个服务要花费较长的时间,启动服务最快也要几秒钟,且需要额外的硬件
  • 而目录是系统基本功能,创建目录非常的快速,只要磁盘够,创建目录基本没有任何限制

也就是说,对于目录来说,我们不需要考虑创建成本。

那么针对服务器负载高的解决方案是否适合目录访问呢?或者哪种方式适合目录访问呢?我们一个个来分析:

  • 随机:在创建文件时,我们随机的选择一个目录进行创建。那么问题来了,我们一开始该创建多少目录呢?新增目录后,是否需要调整程序?因为随机基数变了。对于服务来说,一开始需要做容量规划,确定有几个服务,因为创建一个服务的成本还是挺高的,比重启服务的成本要高不少。但是创建目录的成本却非常的低,初期先确定目录数量的方式并不合适。
  • 轮询:问题和随机类似。
  • 加权随机:同随机
  • 加权轮询:同轮询
  • 源地址Hash:对于服务来说是对源地址hash,对文件来说,可以对文件进行hash。hash完如何与目录进行匹配呢?
  • 一致性Hash:同上

可以看到,主要的问题就是创建目录的问题!如何保证在目录数量改变时,不需要调整程序呢?

实际上git已经给出了答案:

  • 对文件内容取sha1散列
  • 得到的散列值的前两位作为目录
  • 目录不存在就新建
  • 如果已存在就直接保存
  • 后面的散列值作为文件名

也就是说,根据sha1散列的前两位对文件进行归类。这样既解决了目录创建问题,也解决了文件分布问题。可能的问题是,「sha1散列2^80次,可能会发生一次碰撞」。这个问题对于一般文件系统来说,好像也没有担心的必要。

数据安全

解决了「单目录文件过多,导致访问速度下降」的问题,我们来看下一个问题:数据安全。

文件数据是存放在电脑磁盘上的,如果硬盘损坏,可能导致文件的丢失。这实际还是一个「单点问题」!

「单点问题」的解决方案是什么呢?冗余啊!

最简单的方案就是定时去备份数据,可以有如下几种方案:

  • 人工备份
  • 代码实现
  • libfuse
  • RAID

我们继续一个个的讨论。

人工备份

首先是人工备份,这是最low的方案,当然也是最简单的,即有人定期去备份就行了。问题是时效性不高,例如一天备份一次,如果磁盘在备份前坏了,那就会丢失一天的数据。同时恢复比较耗时,需要人工处理。

代码实现

第二个方案是代码实现,即在上传文件时,程序就自动备份。以上面的架构为例,可以添加一个BackupListener,当上传完成后,通过事件,自动备份上传的文件。同时下载时需要判定文件是否完整,如果有问题则使用备份数据。此方案时效性得到了保障,但是将数据备份和业务放到了一起,且需要编码实现,增加了业务代码量。

libfuse

第三个方案是libfuse,libfuse是用户态文件系统接口。下面是libfuse官方简介:

FUSE (Filesystem in Userspace)是一个构建用户态文件系统的接口。libfuse项目包括两个组件:一个fuse内核模块以及libufuse用户态库。libfuse用户态库提供了与FUSE内核模块的通讯实现。

通过libfuse可以实现一个用户态文件系统。libfuse提供方法,支持挂载文件系统、取消挂载文件系统、读取内核请求及作出响应。lifuse提供了两类API:高层级的同步API和低层级的异步API。不过无论哪种方式,内核都是通过回调的方式和主程序通讯。当使用高层级API的时候,回调基于文件名和路径而不是索引节点(inodes),并且回调返回后这个进程也同时结束;当使用低层级API的时候,回调基于索引节点(inodes)工作并且响应必须使用独立的API方法返回。

简单来说,就是可以用libfuse构建一个用户态文件系统。之前在老东家做了一个日志分析平台,日志的收集就使用了libfuse,大致架构如下:

架构设计:文件服务存储设计

业务系统写日志到挂载的用户态文件系统中,用户态文件系统自动转发到了后续的处理中间件:redis、消息队列、文件系统。

在这里也可以用类似的功能,即在文件上传后,用户态文件系统自动备份。此方案解耦了文件备案逻辑与业务逻辑。

RAID

最后一个方案是RAID,即廉价冗余磁盘阵列。RAID不但可备份文件,还支持并发读写,提高上传下载速率。

常用的RAID有:RAID0,RAID1,RAID01/RAID10,RAID5和RAID6等。我们来看看这几种RAID的特点,以及是否适用于我们的文件服务。你会发现从RAID0到RAID6,又是一个从单点到分布式的过程。

具体RAID相关内容可参考wiki,文末有链接!

  • RAID 0:相当于是一个支持并发的单点应用。就是将两个以上的磁盘并联起来,成为一个大容量的磁盘。同时在存放数据时,将数据分段后分散存储在这些磁盘中。这样就能并行的处理读写,提高了读写速度。但是没有数据冗余和容错能力,假如一个磁盘坏了,那所有数据都会丢失。也就是说RAID0只起到了扩容和提高读写速度的作用。就我们的文件服务来说,选RAID0肯定是不合适的,因为最重要的数据安全没有得到保障,它比单磁盘的数据安全还要差!
  • RAID 1:相当于是个单线程的主从应用。将磁盘分为两组,一组工作磁盘、一组镜像磁盘。当写入时,同时写工作磁盘和镜像磁盘,所以写入速度上会比RAID0慢一点。当一个工作磁盘坏了,可以用镜像磁盘。RAID1提供了数据安全保障,性能也足够高。缺点就是利用率低,只用了磁盘的50%。不过对于一般场景来说,RAID1是个不错的选择。我们的文件服务也可以选择RAID1
  • RAID10/RAID01是RAID0和RAID1的组合
  • RAID10:相当于是支持并发的集群应用。即先对数据备份,然后数据被分段写入
  • RAID01:相当于是支持并发的分布式系统。即先将数据分段,然后对分段数据备份写入

看下面的两张图应该能更好的理解:

架构设计:文件服务存储设计

架构设计:文件服务存储设计

无论是RAID10还是RAID01,对磁盘的使用效率都不高。那如何提高磁盘使用率呢?就有了RAID3。

  • RAID3:相当于有注册中心的分布式系统。RAID3的一个前提是,一般情况下,磁盘不会同时坏两个。所以RAID3使用一个磁盘记录校验数据,其它盘作为数据盘,写入数据时,将数据分为n-1份,写到数据盘中,将校验数据写入校验盘。当其中一个磁盘损坏了,可以通过校验盘和其它数据盘来恢复数据。RAID3读取速度很快,但是写入时因为要计算校验值,所以较慢。适合读多写少的情况。我们的文件服务刚好是这样的场景,所以也适合RAID3。但是眼尖的朋友肯定已经发现问题了,这里有个单点问题,就是校验盘。因为校验盘读写次数明显大于数据盘,损坏的几率也就更大,频繁更换校验盘,重建校验数据也是一件很麻烦的事情。那怎么解决这个问题呢?
  • RAID5:通过将校验数据也分散到各个磁盘中来解决这个问题。RAID5相当于是去中心化的分布式系统。当一个磁盘损坏时,可以通过其它磁盘的数据和校验数据来恢复该磁盘的数据和校验数据。和RAID3同样的问题,重建数据速度比较慢,同时对于多个磁盘同时损坏的情况也无能为力。RAID5也同样适用于我们的文件服务。
  • RAID 6:相当于融合了注册中心和去中心化的分布式系统,也可以说有两套注册中心的分布式系统。它在RAID5的基础上增加了两个校验盘,以另一种校验算法来进行校验。当磁盘损坏时,通过其它数据盘的数据和校验数据以及校验盘的校验数据来恢复数据。RAID6能恢复两块磁盘同时损坏情况下的数据,相应的其写入数据更慢,实现难度也更高。除非数据安全要求很高,一般不常用。

对于本地存储来说,RAID是个相对实用的解决方案,既能提高数据安全、快速扩容,也提高了读写速率。但是无论扩展多少磁盘,容量还是相对有限,吞吐也相对有限,同时由于其还是单点,如果文件服务本身挂掉,就会导致单点故障。所以就有了分布式文件系统。

分布式文件系统下次单独讨论!

参考资料

  • libfuse:github.com/libfuse/lib…
  • wiki RAID:zh.wikipedia.org/wiki/RAID
  • 《Git版本控制管理》
  • 《操作系统概念》

架构设计:文件服务存储设计

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…759760761…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%