Fork me on GitHub

慕课网 ThreadLocal 教学视频学习笔记

课程地址:https://www.imooc.com/learn/1217

作者:求老仙奶我不到 P10(这昵称 🤔,我奶一口,你到不了 P10 🐶)

作者简介:我是一名有 10 年经验的互联网老兵,创过业、也曾任数家大型互联网公司架构师、团队 Leader,30 岁(2018)任职阿里巴巴高级技术专家(P8)。曾负责架构 PHP 高负载、前端(React/RN)方向、Java 领域化中间件方向、大数据(BI 和数据可视化)等多方向业界知名项目。大学刷完算法导论,参加过 ACM 和国际机器人竞赛,多年在项目中实践技术驱动、前端后端领域化、数据可视化,多个领域实战经验丰富。

代码地址(跟着课程手敲的):https://github.com/gaohanghang/spring-threadlocal-demo

简介:多线程增加了我们的不确定性,破坏了可预测性——当然,这对于【艺高人胆大】的未来的你,都是小事,因为你会不断进步成长,只要你把握好现在的光阴。科学的美,在于它的模型可以不断的迭代和进步,Java 是一种简化和进步,ThreadLocal 也一种简化和进步,如同 Java 给编程带来了很多安全感,而 ThreadLocal 给多线程时代带了更多的安全感(可预测性、确定性,一致性……)。课程是一种爬坡训练,难度会一直上去直到你完全理解,可以自己动手实现。

第 1 章 纵观课程纲要

了解一致性等基础概念,解决一致性的基本方法,把 ThreadLocal 放到一个宏观背景去思考。

1-1 论程序的安全感 (09:09)

改变思维方式

Single Source Of Truth: 单一数据源

不断的解决数据不一致的问题

1-2 课程介绍 (07:12)


第 2 章 是什么?怎么用?何时用?如何不出问题?

手把手带着 Coding,解决基本概念、API 以及讲 4 个关键应用场景。以及工作中并发场景,如何不出问题。

2-1 ThreadLocal 是什么 (07:41)


一个进程有多个线程

定义:提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值(副本)

特点:简单(开箱即用)、快速(无额外开销)、安全(线程安全)

场景:多线程场景(资源持有、线程一致性、并发计算、线程安全等场景)

进程 -> 线程表 -> 线程局部变量

2-2 ThreadLocal 基本 API (07:52)

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

// ThreadLocal<T>
public static ThreadLocal<Long> x = new ThreadLocal<Long>(){
@Override
protected Long initialValue() {
System.out.println("Initial Value run..");
return Thread.currentThread().getId();
}
};

public static void main(String[] args) {

new Thread() {
@Override
public void run() {
System.out.println(x.get());
}
}.start();
x.set(107L);
// 移除当前线程上的ThreadLocal的值
x.remove();
/*
get操作会延迟加载,如果不get,不会触发initialValue
*/
System.out.println(x.get());

}

}

2-3 ThreadLocal 的 4 种核心场景 (07:51)


总结:

  • 持有资源——持有线程资源供线程的各个部分使用,全局获取,减少编程难度
  • 线程一致——帮助需要保持线程一致的资源(如数据库事务)维护一致性,降低编程难度
  • 线程安全——帮助只考虑了单线程的程序库,无缝向多线程场景迁移
  • 分布式计算——帮助分布式计算场景的各个线程累计局部计算结果。

2-4 Thread Local 并发场景分析 01 (16:50)

代码地址:https://github.com/gaohanghang/spring-threadlocal-demo

MAC 系统上安装 Apache ab 测试工具教程地址: https://www.cnblogs.com/cjsblog/p/10506647.html

1
2
brew install apr
brew install pcre
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class StartController {

static Integer c = 0;

@RequestMapping("/stat")
public Integer stat() {
return c;
}
@RequestMapping("/add")
public Integer add() throws InterruptedException {
Thread.sleep(100);
c++;
return 1;
}

}

测试:

1
2
3
ab -n 10000 -c 1 localhost:8080/add

curl localhost:8080/stat

使用 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
@RestController
public class StartController {

static Integer c = 0;

synchronized void __add() throws InterruptedException {
Thread.sleep(100);
c++;
}

@RequestMapping("/stat")
public Integer stat() {
return c;
}

@RequestMapping("/add")
public Integer add() throws InterruptedException {
//Thread.sleep(100);
//c++;
__add();
return 1;
}

}

测试:

1
2
3
4
5
使用synchronize后测试

ab -n 100 -c 100 localhost:8080/add

curl localhost:8080/stat

使用 ThreadLocal

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
@RestController
public class StartController {

static ThreadLocal<Integer> c = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};

void __add() throws InterruptedException {
Thread.sleep(100);
c.set(c.get() + 1);
}

@RequestMapping("/stat")
public Integer stat() {
return c.get();
}

@RequestMapping("/add")
public Integer add() throws InterruptedException {
__add();
return 1;
}

}

测试:

1
2
3
4
5
使用ThreadLocal

ab -n 10000 -c 100 localhost:8080/add

curl localhost:8080/stat

总结

  • 基于线程池模型 synchronize(排队操作很危险
  • 用 ThreadLocal 收集数据很快且安全
  • 思考:如何在多个 ThreadLocal 中收集数据?

2-5 ThreadLocal 场景分析——减少同步 (10:10)

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
@RestController
public class StartController {

static HashSet<Val<Integer>> set = new HashSet<>();

synchronized static void addSet(Val<Integer> v) {
set.add(v);
}

static ThreadLocal<Val<Integer>> c = new ThreadLocal<Val<Integer>>() {
@Override
protected Val<Integer> initialValue() {
Val<Integer> v = new Val<>();
v.set(0);
addSet(v);
return v;
}
};

void __add() throws InterruptedException {
Thread.sleep(100);
Val<Integer> v = c.get();
v.set(v.get() + 1);
}

@RequestMapping("/stat")
public Integer stat() {
return set.stream().map(x -> x.get()).reduce((a,x) -> a+x).get();
}

@RequestMapping("/add")
public Integer add() throws InterruptedException {
__add();
return 1;
}

}

测试:

1
2
3
4
5
6
7
使用ThreadLocal

ab -n 10000 -c 100 localhost:8080/add

ab -n 10000 -c 200 localhost:8080/add

curl localhost:8080/stat

问题:set.add(v); 为什么会有线程安全问题

set 是所有线程共享的,是个临界区

第 3 章 【极客视角】大神们怎么用 ThreadLocal 的

挑选了 3 个 Java 领域影响深远的应用,Spring/Mybatis/Quartz 中使用到 ThreadLocal 的源码,理解大神们在思考什么,为什么会用到 ThreadLocal。

3-1 源码分析 1-Quartz SimpleSemaphore (06:42)

3-2 源码分析 2 Mybatis 框架保持连接池线程一致 (04:46)

A(Atomic))原子性,操作不可分割。
C(Consistency)一致性,任何时刻数据都能保持一致。
I(Isolation)隔离性,多事务并发执行的时序不影响结果。
D(Durability)持久性,对数据结构的存储是永久的。

3-3 源码分析 03 Spring 框架对分布式事务的支持 (04:03)

A(Atomic))原子性,操作不可分割。
C(Consistency)一致性,任何时刻数据都能保持一致。
I(Isolation)隔离性,多事务并发执行的时序不影响结果。
D(Durability)持久性,对数据结构的存储是永久的。

A:要么发生,要么不发生
C:要做对,做错了不算
I:同时发生两个事务,两个事务的结果能够叠加的一致,同时扣款
D:数据要被持久化下来

context 环境

第 4 章 【设计者视角】源码级实现&源码分析

4-1 实现自己的 ThreadLocal (12:16)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MyThreadLocal<T> {

// 共享空间
static HashMap<Thread, HashMap<MyThreadLocal<?>, Object>> threadLocalMap = new HashMap<>();

// 临界区

/**
* 获取当前线程的数据
* @return
*/
synchronized static HashMap<MyThreadLocal<?>, Object> getMap() {
// 获取当前线程
Thread thread = Thread.currentThread();
// 判断threadLocalMap是否包含当前线程,不包含就put进去
if (!threadLocalMap.containsKey(thread)) {
threadLocalMap.put(thread, new HashMap<MyThreadLocal<?>,Object>());
}
// 获取当前thread的map
return threadLocalMap.get(thread);
}

protected T initialValue() {
return null;
}

public T get() {
HashMap<MyThreadLocal<?>, Object> map = getMap();
if (!map.containsKey(this)) {
map.put(this, initialValue());
}
return (T) map.get(this);
}

public void set(T v) {
HashMap<MyThreadLocal<?>, Object> map = getMap();
map.put(this, v);
}

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

static MyThreadLocal<Long> v = new MyThreadLocal<Long>() {
@Override
protected Long initialValue() {
return Thread.currentThread().getId();
}
};

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(v.get());
}).start();
}
}

}


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
public class MyThreadLocal<T> {

static AtomicInteger atomic = new AtomicInteger();

// 自增
Integer threadLocalHash = atomic.addAndGet(0x61c88647);

// 共享空间
static HashMap<Thread, HashMap<Integer, Object>> threadLocalMap = new HashMap<>();

// 临界区

/**
* 获取当前线程的数据
* @return
*/
synchronized static HashMap<Integer, Object> getMap() {
// 获取当前线程
Thread thread = Thread.currentThread();
// 判断threadLocalMap是否包含当前线程,不包含就put进去
if (!threadLocalMap.containsKey(thread)) {
threadLocalMap.put(thread, new HashMap<Integer,Object>());
}
// 获取当前thread的map
return threadLocalMap.get(thread);
}

protected T initialValue() {
return null;
}

public T get() {
HashMap<Integer, Object> map = getMap();
if (!map.containsKey(this.threadLocalHash)) {
map.put(this.threadLocalHash, initialValue());
}
return (T) map.get(this.threadLocalHash);
}

public void set(T v) {
HashMap<Integer, Object> map = getMap();
map.put(this.threadLocalHash, v);
}

}

4-2 选学 HashTable (03:56)


4-3 ThreadLocal 源码分析 (13:40)


第 5 章 全课总结

ThreadLocal 只是一个简单的数据结构,却引出了这么多问题,可见真理常常隐藏在容易忽视微小的地方,优秀的程序员不仅仅要大局观强,还更加需要磨砺细节——和老师一起思考未来应该怎样学习?

5-1 总结 (04:19)

一些建议

  • 无论将来到什么样的高度,永远认为自己是个菜鸡:总有比我们厉害的大牛,永远都去学习
  • 保持兴趣,体会乐趣:投入在工作上的时间是很多的,在工作上一定要保持兴趣
  • 技术创造是有价值的(切记):自己尝试去创造,尝试学新东西去用

最后

首先感谢大佬的课程分享,通过课程学到了很多东西

另外是我有个公众号,叫《骇客与画家》,欢迎大家来关注 😆

骇客与画家,名称来自书籍《黑客与画家》,画家学习绘画的方法是动手去画,骇客学习编程的方法也是动手去实践

如何使用两个线程交替打印1--100?

原文地址:https://blog.csdn.net/Java_stud/article/details/82347135

作者:四两数字先生

wait():令当前线程放弃 CPU 的资源,使别的线程可以访问共享的资源,而当前的线程排队等待再次对资源的访问

notify():唤醒正在排队的等待的同步资源的线程,

notifyAll():唤醒正在排队等待的所有的线程

在 java.lang.Object:

用这三个方法的注意点: 同步方法或者同步代码块里

使用两个线程打印1—-100.线程1和线程2交替打印

分析:

  1. 我先使用两个线程打印1—100,(先不用交替打印)
  2. 然后在使用上面的三个方法,在代码里添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class PrintNum implements Runnable {

int num = 1;

@Override
public void run() {

synchronized (this) {
while (true) {
//唤醒wait()的一个或者所有线程
notify();
if (num <= 100) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
} else {
break;
}

try {
wait();
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}

}

public class Test {

public static void main(String[] args) {
PrintNum printNum = new PrintNum();

Thread t1 = new Thread(printNum);
Thread t2 = new Thread(printNum);

t1.setName("甲");
t2.setName("乙");

t1.start();
t2.start();
}

}

运行效果:

用好Java中的枚举,真的没有那么简单!

最近重看 Java 枚举,看到这篇觉得还不错的文章,于是简单翻译和完善了一些内容,分享给大家,希望你们也能有所收获。另外,不要忘了文末还有补充哦!

ps: 这里发一篇枚举的文章,也是因为后面要发一篇非常实用的关于 SpringBoot 全局异常处理的比较好的实践里面就用到了枚举。

这篇文章由 JavaGuide 翻译,

公众号: JavaGuide,原文地址:https://www.baeldung.com/a-guide-to-java-enums

转载请注明上面这段文字。

1.概览

在本文中,我们将看到什么是 Java 枚举,它们解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式。

enum关键字在 java5 中引入,表示一种特殊类型的类,其总是继承java.lang.Enum类,更多内容可以自行查看其官方文档。

枚举在很多时候会和常量拿来对比,可能因为本身我们大量实际使用枚举的地方就是为了替代常量。那么这种方式由什么优势呢?

以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录可接受值的列表,并避免由于传入无效值而引起的意外行为。

下面示例定义一个简单的枚举类型 pizza 订单的状态,共有三种 ORDERED, READY, DELIVERED状态:

1
2
3
4
5
6
7
package shuang.kou.enumdemo.enumtest;

publicenum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}

简单来说,我们通过上面的代码避免了定义常量,我们将所有和 pizza 订单的状态的常量都统一放到了一个枚举类型里面。

1
2
3
4
System.out.println(PizzaStatus.ORDERED.name());//ORDERED
System.out.println(PizzaStatus.ORDERED);//ORDERED
System.out.println(PizzaStatus.ORDERED.name().getClass());//class java.lang.String
System.out.println(PizzaStatus.ORDERED.getClass());//class shuang.kou.enumdemo.enumtest.PizzaStatus

2.自定义枚举方法

现在我们对枚举是什么以及如何使用它们有了基本的了解,让我们通过在枚举上定义一些额外的API方法,将上一个示例提升到一个新的水平:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
publicclass Pizza {
private PizzaStatus status;
publicenum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}

public boolean isDeliverable() {
if (getStatus() == PizzaStatus.READY) {
returntrue;
}
returnfalse;
}

// Methods that set and get the status variable.
}

3.使用 == 比较枚举类型

由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用“ ==”运算符比较两个变量,如上例所示;此外,“ ==”运算符可提供编译时和运行时的安全性。

首先,让我们看一下以下代码段中的运行时安全性,其中“ ==”运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException:

1
2
if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED));
if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);

对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为getStatus方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误:

1
2
if(testPz.getStatus().equals(TestColor.GREEN));
if(testPz.getStatus() == TestColor.GREEN);

4.在 switch 语句中使用枚举类型

1
2
3
4
5
6
7
8
public int getDeliveryTimeInDays() {
switch (status) {
case ORDERED: return5;
case READY: return2;
case DELIVERED: return0;
}
return0;
}

5.枚举类型的属性,方法和构造函数

文末有我(JavaGuide)的补充。

你可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。

下面,让我们扩展上面的示例,实现从比萨的一个阶段到另一个阶段的过渡,并了解如何摆脱之前使用的if语句和switch语句:

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
publicclass Pizza {

private PizzaStatus status;
publicenum PizzaStatus {
ORDERED (5){
@Override
public boolean isOrdered() {
returntrue;
}
},
READY (2){
@Override
public boolean isReady() {
returntrue;
}
},
DELIVERED (0){
@Override
public boolean isDelivered() {
returntrue;
}
};

privateint timeToDelivery;

public boolean isOrdered() {returnfalse;}

public boolean isReady() {returnfalse;}

public boolean isDelivered(){returnfalse;}

public int getTimeToDelivery() {
return timeToDelivery;
}

PizzaStatus (int timeToDelivery) {
this.timeToDelivery = timeToDelivery;
}
}

public boolean isDeliverable() {
returnthis.status.isReady();
}

public void printTimeToDeliver() {
System.out.println("Time to delivery is " +
this.getStatus().getTimeToDelivery());
}

// Methods that set and get the status variable.
}

下面这段代码展示它是如何 work 的:

1
2
3
4
5
6
@Test
public void givenPizaOrder_whenReady_thenDeliverable() {
Pizza testPz = new Pizza();
testPz.setStatus(Pizza.PizzaStatus.READY);
assertTrue(testPz.isDeliverable());
}

6.EnumSet and EnumMap

6.1. EnumSet

EnumSet 是一种专门为枚举类型所设计的 Set 类型。

HashSet相比,由于使用了内部位向量表示,因此它是特定 Enum 常量集的非常有效且紧凑的表示形式。

它提供了类型安全的替代方法,以替代传统的基于int的“位标志”,使我们能够编写更易读和易于维护的简洁代码。

EnumSet 是抽象类,其有两个实现:RegularEnumSetJumboEnumSet,选择哪一个取决于实例化时枚举中常量的数量。

在很多场景中的枚举常量集合操作(如:取子集、增加、删除、containsAllremoveAll批操作)使用EnumSet非常合适;如果需要迭代所有可能的常量则使用Enum.values()

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
publicclass Pizza {

privatestatic EnumSet<PizzaStatus> undeliveredPizzaStatuses =
EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);

private PizzaStatus status;

publicenum PizzaStatus {
...
}

public boolean isDeliverable() {
returnthis.status.isReady();
}

public void printTimeToDeliver() {
System.out.println("Time to delivery is " +
this.getStatus().getTimeToDelivery() + " days");
}

public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
return input.stream().filter(
(s) -> undeliveredPizzaStatuses.contains(s.getStatus()))
.collect(Collectors.toList());
}

public void deliver() {
if (isDeliverable()) {
PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
.deliver(this);
this.setStatus(PizzaStatus.DELIVERED);
}
}

// Methods that set and get the status variable.
}

下面的测试演示了展示了 EnumSet 在某些场景下的强大功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {
List<Pizza> pzList = new ArrayList<>();
Pizza pz1 = new Pizza();
pz1.setStatus(Pizza.PizzaStatus.DELIVERED);

Pizza pz2 = new Pizza();
pz2.setStatus(Pizza.PizzaStatus.ORDERED);

Pizza pz3 = new Pizza();
pz3.setStatus(Pizza.PizzaStatus.ORDERED);

Pizza pz4 = new Pizza();
pz4.setStatus(Pizza.PizzaStatus.READY);

pzList.add(pz1);
pzList.add(pz2);
pzList.add(pz3);
pzList.add(pz4);

List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);
assertTrue(undeliveredPzs.size() == 3);
}

6.2. EnumMap

EnumMap是一个专门化的映射实现,用于将枚举常量用作键。与对应的 HashMap 相比,它是一个高效紧凑的实现,并且在内部表示为一个数组:

1
EnumMap<Pizza.PizzaStatus, Pizza> map;

让我们快速看一个真实的示例,该示例演示如何在实践中使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
publicstatic EnumMap<PizzaStatus, List<Pizza>>
groupPizzaByStatus(List<Pizza> pizzaList) {
EnumMap<PizzaStatus, List<Pizza>> pzByStatus =
new EnumMap<PizzaStatus, List<Pizza>>(PizzaStatus.class);

for (Pizza pz : pizzaList) {
PizzaStatus status = pz.getStatus();
if (pzByStatus.containsKey(status)) {
pzByStatus.get(status).add(pz);
} else {
List<Pizza> newPzList = new ArrayList<Pizza>();
newPzList.add(pz);
pzByStatus.put(status, newPzList);
}
}
return pzByStatus;
}

下面的测试演示了展示了 EnumMap 在某些场景下的强大功能:

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
@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
List<Pizza> pzList = new ArrayList<>();
Pizza pz1 = new Pizza();
pz1.setStatus(Pizza.PizzaStatus.DELIVERED);

Pizza pz2 = new Pizza();
pz2.setStatus(Pizza.PizzaStatus.ORDERED);

Pizza pz3 = new Pizza();
pz3.setStatus(Pizza.PizzaStatus.ORDERED);

Pizza pz4 = new Pizza();
pz4.setStatus(Pizza.PizzaStatus.READY);

pzList.add(pz1);
pzList.add(pz2);
pzList.add(pz3);
pzList.add(pz4);

EnumMap<Pizza.PizzaStatus,List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);
assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);
assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);
assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);
}

7. 通过枚举实现一些设计模式

7.1 单例模式

通常,使用类实现 Singleton 模式并非易事,枚举提供了一种实现单例的简便方法。

《Effective Java 》和《Java与模式》都非常推荐这种方式,使用这种方式方式实现枚举可以有什么好处呢?

《Effective Java》

这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。—-《Effective Java 中文版 第二版》

《Java与模式》

《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

下面的代码段显示了如何使用枚举实现单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
publicenum PizzaDeliverySystemConfiguration {
INSTANCE;
PizzaDeliverySystemConfiguration() {
// Initialization configuration which involves
// overriding defaults like delivery strategy
}

private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;

public static PizzaDeliverySystemConfiguration getInstance() {
return INSTANCE;
}

public PizzaDeliveryStrategy getDeliveryStrategy() {
return deliveryStrategy;
}
}

如何使用呢?请看下面的代码:

1
PizzaDeliveryStrategy deliveryStrategy = PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy();

通过 PizzaDeliverySystemConfiguration.getInstance() 获取的就是单例的 PizzaDeliverySystemConfiguration

7.2 策略模式

通常,策略模式由不同类实现同一个接口来实现的。

这也就意味着添加新策略意味着添加新的实现类。使用枚举,可以轻松完成此任务,添加新的实现意味着只定义具有某个实现的另一个实例。

下面的代码段显示了如何使用枚举实现策略模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
publicenum PizzaDeliveryStrategy {
EXPRESS {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in express mode");
}
},
NORMAL {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in normal mode");
}
};

public abstract void deliver(Pizza pz);
}

Pizza增加下面的方法:

1
2
3
4
5
6
7
public void deliver() {
if (isDeliverable()) {
PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
.deliver(this);
this.setStatus(PizzaStatus.DELIVERED);
}
}

如何使用呢?请看下面的代码:

1
2
3
4
5
6
7
@Test
public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
pz.deliver();
assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
}

8. Java 8 与枚举

Pizza 类可以用Java 8重写,您可以看到方法 lambda 和Stream API如何使 getAllUndeliveredPizzas()groupPizzaByStatus()方法变得如此简洁:

getAllUndeliveredPizzas():

1
2
3
4
5
public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
return input.stream().filter(
(s) -> !deliveredPizzaStatuses.contains(s.getStatus()))
.collect(Collectors.toList());
}

groupPizzaByStatus() :

1
2
3
4
5
6
7
publicstatic EnumMap<PizzaStatus, List<Pizza>>
groupPizzaByStatus(List<Pizza> pzList) {
EnumMap<PizzaStatus, List<Pizza>> map = pzList.stream().collect(
Collectors.groupingBy(Pizza::getStatus,
() -> new EnumMap<>(PizzaStatus.class), Collectors.toList()));
return map;
}

9. Enum 类型的 JSON 表现形式

使用Jackson库,可以将枚举类型的JSON表示为POJO。下面的代码段显示了可以用于同一目的的Jackson批注:

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
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
publicenum PizzaStatus {
ORDERED (5){
@Override
public boolean isOrdered() {
returntrue;
}
},
READY (2){
@Override
public boolean isReady() {
returntrue;
}
},
DELIVERED (0){
@Override
public boolean isDelivered() {
returntrue;
}
};

privateint timeToDelivery;

public boolean isOrdered() {return false;}

public boolean isReady() {return false;}

public boolean isDelivered(){return false;}

@JsonProperty("timeToDelivery")
public int getTimeToDelivery() {
return timeToDelivery;
}

private PizzaStatus (int timeToDelivery) {
this.timeToDelivery = timeToDelivery;
}
}

我们可以按如下方式使用 PizzaPizzaStatus

1
2
3
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
System.out.println(Pizza.getJsonString(pz));

生成 Pizza 状态以以下JSON展示:

1
2
3
4
5
6
7
8
9
{
"status" : {
"timeToDelivery" : 2,
"ready" : true,
"ordered" : false,
"delivered" : false
},
"deliverable" : true
}

有关枚举类型的JSON序列化/反序列化(包括自定义)的更多信息,请参阅Jackson-将枚举序列化为JSON对象。

10.总结

本文我们讨论了Java枚举类型,从基础知识到高级应用以及实际应用场景,让我们感受到枚举的强大功能。

11. 补充

我们在上面讲到了,我们可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。

下面我通过一个实际的例子展示一下,当我们调用短信验证码的时候可能有几种不同的用途,我们在下面这样定义:

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
publicenum PinType {

REGISTER(100000, "注册使用"),
FORGET_PASSWORD(100001, "忘记密码使用"),
UPDATE_PHONE_NUMBER(100002, "更新手机号码使用");

privatefinalint code;
privatefinal String message;

PinType(int code, String message) {
this.code = code;
this.message = message;
}

public int getCode() {
return code;
}

public String getMessage() {
return message;
}

@Override
public String toString() {
return"PinType{" +
"code=" + code +
", message='" + message + '\'' +
'}';
}
}

实际使用:

1
2
3
System.out.println(PinType.FORGET_PASSWORD.getCode());
System.out.println(PinType.FORGET_PASSWORD.getMessage());
System.out.println(PinType.FORGET_PASSWORD.toString());

Output:

1
2
3
100001
忘记密码使用
PinType{code=100001, message='忘记密码使用'}

这样的话,在实际使用起来就会非常灵活方便!

Redis和mysql数据怎么保持数据一致的?

需求起因

在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。

这个业务场景,主要是解决读数据从Redis缓存,一般都是按照下图的流程来进行业务操作。

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:

1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。

2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

如来解决?这里给出两个解决方案,先易后难,结合业务和技术代价选择使用。

缓存和数据库一致性解决方案

1.第一种方案:采用延时双删策略

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。

伪代码如下

1
2
3
4
5
6
7
public void write( String key, Object data )
{
redis.delKey( key );
db.updateData( data );
Thread.sleep( 500 );
redis.delKey( key );
}

2.具体的步骤就是:

  1. 先删除缓存

  2. 再写数据库

  3. 休眠500毫秒

  4. 再次删除缓存

那么,这个500毫秒怎么确定的,具体该休眠多久呢?

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

3.设置缓存过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

4.该方案的弊端

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

2、第二种方案:异步更新缓存(基于订阅binlog的同步机制)

1.技术整体思路:

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

  • 读Redis:热数据基本都在Redis
  • 写MySQL:增删改都是操作MySQL
  • 更新Redis数据:MySQ的数据操作binlog,来更新到Redis

2.Redis更新

(1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)
  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

(2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis!

最后

后续会持续更新Redis专题知识,写的不好的地方也希望大牛能指点一下,大家觉得不错可以点个赞在关注下,以后还会分享更多文章!

作者:若小寒链接:https://juejin.im/post/5c96fb795188252d5f0fdff2来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

译|Arrays.sort VS Arrays.parallelSort

英文原文地址:Arrays.sort vs Arrays.parallelSort

作者:baeldung

翻译:高行行

小结:

当我们要排序的数据集很大时,parallelSort() 可能是更好的选择。但是,在数组较小的情况下,最好使用 sort(),因为它可以提供更好的性能

1. 概述

我们都使用过 Arrays.sort() 对对象或原始数据类型数组(byteshortintlongcharfloatdoubleboolean)进行排序。在 JDK 8 中,创造者增强了 API 以提供一种新方法:_Arrays.parallelSort()_。

在本教程中,我们将对 sort() 和 parallelSort() 方法进行比较。

2. Arrays.sort()

Arrays.sort() 方法对对象或原始数据类型的数组进行排序。此方法中使用的排序算法是 Dual-Pivot Quicksort 换句话说,它是快速排序算法的自定义实现,以实现更好的性能。

此方法是单线程的 ,有两种变体:

  • _sort(array)_–将整个数组按升序排序
  • _sort(array, fromIndex, toIndex)_–仅将从 fromIndex 到 toIndex 的元素排序

让我们看一下两种变体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void givenArrayOfIntegers_whenUsingArraysSortMethod_thenSortFullArrayInAscendingOrder() {
int[] array = { 10, 4, 6, 2, 1, 9, 7, 8, 3, 5 };
int[] expected = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

Arrays.sort(array);

assertArrayEquals(expected, array);

}

@Test
public void givenArrayOfIntegers_whenUsingArraysSortMethodWithRange_thenSortRangeOfArrayInAscendingOrder() {
int[] array = { 10, 4, 6, 2, 1, 9, 7, 8, 3, 5 };
int[] expected = { 10, 4, 1, 2, 6, 7, 8, 9, 3, 5 };

Arrays.sort(array, 2, 8);

assertArrayEquals(expected, array);
}

让我们总结一下这种方法的优缺点:

优点 缺点
快速处理较小的数据集 大型数据集的性能下降
没有利用系统的多个核心

3. Arrays.parallelSort()

此方法对对象或原始数据类型的数组进行排序。与 sort() 类似,它也有两个变体来对完整数组和部分数组进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void givenArrayOfIntegers_whenUsingArraysParallelSortMethod_thenSortFullArrayInAscendingOrder() {
int[] array = { 10, 4, 6, 2, 1, 9, 7, 8, 3, 5 };
int[] expected = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

Arrays.parallelSort(array);

assertArrayEquals(expected, array);
}

@Test
public void givenArrayOfIntegers_whenUsingArraysParallelSortMethodWithRange_thenSortRangeOfArrayInAscendingOrder() {
int[] array = { 10, 4, 6, 2, 1, 9, 7, 8, 3, 5 };
int[] expected = { 10, 4, 1, 2, 6, 7, 8, 9, 3, 5 };

Arrays.parallelSort(array, 2, 8);

assertArrayEquals(expected, array);
}

parallelSort() 在功能上有所不同。与 sort() 使用单个线程对数据进行顺序排序不同,它使用并行排序-合并排序算法。它将数组分成子数组,这些子数组本身先进行排序然后合并。

为了执行并行任务,它使用 ForkJoin 池。

但是我们需要知道,只有在满足某些条件时,它才会使用并行性。如果数组大小小于或等于 8192,或者处理器只有一个核心,则它将使用顺序的 Dual-Pivot Quicksort 算法。否则,它使用并行排序。

让我们总结一下使用它的优缺点:

优点 缺点
为大型数据集提供更好的性能 对于大小较小的数组,处理速度较慢
利用系统的多个核心

4.比较

现在,让我们看看在不同大小的数据集上两种方法怎样执行。以下数字是使用JMH 基准测试得出的。测试环境使用 AMD A10 PRO 2.1Ghz 四核处理器和 JDK 1.8.0_221:

数组大小 Arrays.sort() Arrays.parallelSort()
1000 o.048 0.054
10000 0.847 0.425
100000 7.570 4.395
1000000 65.301 37.998

5.结论

在这篇短篇文章中,我们看到了 sort() 和 parallelSort() 的不同之处。

根据性能结果,我们可以得出结论,当我们要排序的数据集很大时,parallelSort() 可能是更好的选择。但是,在数组较小的情况下,最好使用 sort(),因为它可以提供更好的性能。

与往常一样,完整的源代码可以在 GitHub 找到。

转|Synchronized同步静态方法和非静态方法总结

Photo by SnapwireSnaps

原文地址:https://blog.csdn.net/u010842515/article/details/65443084

作者:编程初丁

1. Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。

Java中每个对象都有一个锁,并且是唯一的。假设分配的一个对象空间,里面有多个方法,相当于空间里面有多个小房间,如果我们把所有的小房间都加锁,因为这个对象只有一把钥匙,因此同一时间只能有一个人打开一个小房间,然后用完了还回去,再由JVM 去分配下一个获得钥匙的人。

情况1:同一个对象在两个线程中分别访问该对象的两个同步方法

结果:会产生互斥。

解释:因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。

情况2:不同对象在两个线程中调用同一个同步方法

结果:不会产生互斥。

解释:因为是两个对象,锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new 一个对象,那么就会出现两个空间,两把钥匙,

2. Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。

情况1:用类直接在两个线程中调用两个不同的同步方法

结果:会产生互斥。

解释:因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。

注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。

情况2:用一个类的静态对象在两个线程中调用静态方法或非静态方法

结果:会产生互斥。

解释:因为是一个对象调用,同上。

情况3:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法

结果:不会产生互斥。

解释:因为虽然是一个对象调用,但是两个方法的锁类型不同,调用的静态方法实际上是类对象在调用,即这两个方法产生的并不是同一个对象锁,因此不会互斥,会并发执行。

测试代码:

同步方法类:SynchronizedTest.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
public class SynchronizedTest {
/*private SynchronizedTest(){}
private static SynchronizedTest st; //懒汉式单例模式,线程不安全,需要加synchronized同步
public static SynchronizedTest getInstance(){
if(st == null){
st = new SynchronizedTest();
}
return st;
}*/
/*private SynchronizedTest(){}
private static final SynchronizedTest st = new SynchronizedTest(); //饿汉式单利模式,天生线程安全
public static SynchronizedTest getInstance(){
return st;
}*/

public static SynchronizedTest staticIn = new SynchronizedTest(); //静态对象

public synchronized void method1(){ //非静态方法1
for(int i = 0;i < 10;i++){
System.out.println("method1 is running!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public synchronized void method2(){ //非静态方法2
for( int i = 0; i < 10 ; i++){
System.out.println("method2 is running!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public synchronized static void staticMethod1(){ //静态方法1
for( int i = 0; i < 10 ; i++){
System.out.println("static method1 is running!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public synchronized static void staticMethod2(){ //静态方法2
for( int i = 0; i < 10 ; i++){
System.out.println("static method2 is running!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

线程类1:Thread1.java(释放不同的注释可以测试不同的情况)

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

@Override
public void run() {
// SynchronizedTest s = SynchronizedTest.getInstance();
// s.method1();
// SynchronizedTest s1 = new SynchronizedTest();
// s1.method1();
SynchronizedTest.staticIn.method1();

// SynchronizedTest.staticMethod1();
// SynchronizedTest.staticMethod2();
}
}

线程类2:Thread2.Java

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

@Override
public void run() {
// TODO Auto-generated method stub
// SynchronizedTest s = SynchronizedTest.getInstance();
// SynchronizedTest s2 = new SynchronizedTest();
// s2.method1();
// s.method2();
// SynchronizedTest.staticMethod1();
// SynchronizedTest.staticMethod2();
// SynchronizedTest.staticIn.method2();
SynchronizedTest.staticIn.staticMethod1();
}
}

主类:ThreadMain.java

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadMain {
public static void main(String[] args) {
Thread t1 = new Thread(new Thread1());
Thread t2 = new Thread(new Thread2());
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(t1);
exec.execute(t2);
exec.shutdown();
}
}

3. 总结:

1.对象锁钥匙只能有一把才能互斥,才能保证共享变量的唯一性

2.在静态方法上的锁,和 实例方法上的锁,默认不是同样的,如果同步需要制定两把锁一样。

3.关于同一个类的方法上的锁,来自于调用该方法的对象,如果调用该方法的对象是相同的,那么锁必然相同,否则就不相同。比如 new A().x() 和 new A().x(),对象不同,锁不同,如果A的单利的,就能互斥。

4.静态方法加锁,能和所有其他静态方法加锁的 进行互斥

5.静态方法加锁,和xx.class 锁效果一样,直接属于类的

mysql优化:覆盖索引、延迟关联

作者:一枝花算不算浪漫

原文地址:https://www.cnblogs.com/wang-meng/p/ae6d1c4a7b553e9a5c8f46b67fb3e3aa.html

小结:

回表:回到主键索引树搜索的过程,我们称为回表。

覆盖索引:就是 select 的数据列只用从索引中就能够取得,不必从数据表中读取。简单点说就是你要查的数据索引里都有,一次搞定,美滋滋 😎。

延迟关联:通过使用覆盖索引查询返回需要的主键,再根据主键关联原表获得需要的数据。

1. 前言

上周新系统改版上线,上线第二天就出现了较多的线上慢 sql 查询,紧接着 dba 给出了定位及解决方案,这里较多的是使用延迟关联去优化。

而我对于这个延迟关联也是第一次听说(o(╥﹏╥)o),所以今天一定要学习并产出一篇学习笔记。(^▽^)

2. 回表

我们都知道 InnoDB 采用的 B+ tree 来实现索引的,索引又分为主键索引(聚簇索引)和普通索引(二级索引)。

那么我们就来看下基于主键索引和普通索引的查询有什么区别?

  • 如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+树;
  • 如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。

举个栗子:

可以看出我们有一个普通索引 k,那么两颗 B+树的示意图如下:

(注:图来自极客时间专栏)

当我们查询 select \* from T where k=5 其实会先到 k 那个索引树上查询 k = 5,然后找到对应的 id 为 500,最后回表到主键索引的索引树找返回所需数据。

如果我们查询**select id from T where k=5** 则不需要回表就直接返回。

也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。

3. 覆盖索引

  • 解释一: 就是 select 的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。
  • 解释二: 索引是高效找到行的一个方法,当能通过检索索引就可以读取想要的数据,那就不需要再到数据表中读取行了。如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫做覆盖索引。
  • 解释三:是非聚集组合索引的一种形式,它包括在查询里的 Select、Join 和 Where 子句用到的所有列(即建立索引的字段正好是覆盖查询语句[select 子句]与查询条件[Where 子句]中所涉及的字段,也即,索引包含了查询正在查找的所有数据)。
  • 不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引的列,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以 MySQL 只能使用 B-Tree 索引做覆盖索引
  • 当发起一个被索引覆盖的查询(也叫作索引覆盖查询)时,在 EXPLAIN 的 Extra 列可以看到“Using index”的信息

概念如上,这里我们还是用例子来说明:

(注:图来自极客时间专栏)

现在,我们一起来看看这条 SQL 查询语句的执行流程: select * from T where k between 3 and 5

  1. 在 k 索引树上找到 k=3 的记录,取得 ID = 300;
  2. 再到 ID 索引树查到 ID=300 对应的 R3;
  3. 在 k 索引树取下一个值 k=5,取得 ID=500;
  4. 再回到 ID 索引树查到 ID=500 对应的 R4;
  5. 在 k 索引树取下一个值 k=6,不满足条件,循环结束。

在这个过程中,回到主键索引树搜索的过程,我们称为回表。可以看到,这个查询过程读了 k 索引树的 3 条记录(步骤 1、3 和 5),回表了两次(步骤 2 和 4)。

在这个例子中,由于查询结果所需要的数据只在主键索引上有,所以不得不回表。那么,有没有可能经过索引优化,避免回表过程呢?

如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

需要注意的是,在引擎内部使用覆盖索引在索引 k 上其实读了三个记录,R3~R5(对应的索引 k 上的记录项),但是对于 MySQL 的 Server 层来说,它就是找引擎拿到了两条记录,因此 MySQL 认为扫描行数是 2。

4. 延迟关联

上面介绍了那么多 其实是在为延迟关联做铺垫,这里直接续上我们本次慢查询的 sql:

我们都知道在做分页时会用到 Limit 关键字去筛选所需数据,limit 接受 1 个或者 2 个参数,接受两个参数时第一个参数表示偏移量,即从哪一行开始取数据,第二个参数表示要取的行数。 如果只有一个参数,相当于偏移量为 0。

当偏移量很大时,如 limit 100000,10 取第 100001-100010 条记录,mysql 会取出 100010 条记录然后将前 100000 条记录丢弃,这无疑是一种巨大的性能浪费。

这个 sql 并没有利用索引覆盖,因为所要 select 的字段不全都在索引上,每次根据二级索引(expert_id) 查询到一条记录,都要再走一遍主键索引去表里找出所需要的其他列,速度自然慢。

当有这种写法时,我们可以采用延迟关联来进行优化,重点关注:SELECT id FROM qa_question WHERE expert_id = 69 AND STATUS = 30 ORDER BY over_time DESC LIMIT 0, 10, 这里其实利用了索引覆盖,where 条件后的 expert_id 是有添加索引的,这里查询 id 可以避免回表,大大提升效率。

5. 结语

工作中会遇到各种各样的问题,对于一个研发来说最重要的是能够从这些问题中学到什么。好久没有写博客了,究其原因还是自己变得懒惰了。 ( ̄ェ ̄)

最后以《高性能 Mysql》中的一段话结束:

傻傻分不清之 Cookie、Session、Token、JWT

原文地址:https://juejin.im/post/5e055d9ef265da33997a42cc

作者:秋天不落叶

1. 什么是认证(Authentication)

  • 通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)
  • 互联网中的认证:
    • 用户名密码登录
    • 邮箱发送登录链接
    • 手机号接收验证码
    • 只要你能收到邮箱/验证码,就默认你是账号的主人

2. 什么是授权(Authorization)

  • 用户授予第三方应用访问该用户某些资源的权限
    • 你在安装手机应用的时候,APP 会询问是否允许授予权限(访问相册、地理位置等权限)
    • 你在访问微信小程序时,当登录时,小程序会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息)
  • 实现授权的方式有:cookie、session、token、OAuth

3. 什么是凭证(Credentials)

  • 实现认证和授权的前提

    是需要一种

    媒介(证书)

    来标记访问者的身份

    • 在战国时期,商鞅变法,发明了照身帖。照身帖由官府发放,是一块打磨光滑细密的竹板,上面刻有持有人的头像和籍贯信息。国人必须持有,如若没有就被认为是黑户,或者间谍之类的。
    • 在现实生活中,每个人都会有一张专属的居民身份证,是用于证明持有人身份的一种法定证件。通过身份证,我们可以办理手机卡/银行卡/个人贷款/交通出行等等,这就是认证的凭证。
    • 在互联网应用中,一般网站(如掘金)会有两种模式,游客模式和登录模式。游客模式下,可以正常浏览网站上面的文章,一旦想要点赞/收藏/分享文章,就需要登录或者注册账号。当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌,就可以使用游客模式下无法使用的功能。
  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。
  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的靠的是 domain)

cookie 重要的属性

属性 说明
name=value 键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型 - 如果值为 Unicode 字符,需要为字符编码。 - 如果值为二进制数据,则需要使用 BASE64 编码。
domain 指定 cookie 所属域名,默认是当前域名
path **指定 cookie 在哪个路径(路由)下生效,默认是 ‘/‘**。 如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read
maxAge cookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。 - 比 expires 好用
expires 过期时间,在设置的某个时间点后该 cookie 就会失效。 一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除
secure 该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL 等,在网络上传输数据之前先将数据加密。默认为 false。 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
httpOnly 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全

5. 什么是 Session

  • session 是另一种记录服务器和客户端会话状态的机制
  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的 cookie 中

session.png

  • session 认证流程:
    • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
    • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
    • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
    • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
  • 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
  • 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

7. 什么是 Token(令牌)

7.1 Acesss Token

  • 访问资源接口(API)时所需要的资源凭证
  • 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
  • 特点:
    • 服务端无状态化、可扩展性好
    • 支持移动端设备
    • 安全
    • 支持跨程序调用
  • token 的身份验证流程:

img

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
  4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
  • 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
  • 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
  • token 完全由应用管理,所以它可以避开同源策略

7.2 Refresh Token

  • 另外一种 token——refresh token
  • refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。

img

  • Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。
  • Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。

8. Token 和 Session 的区别

  • Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
  • Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
  • 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App 上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。

9. 什么是 JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
  • 是一种认证授权机制
  • JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
  • 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。
  • 阮一峰老师的 JSON Web Token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了

9.1 生成 JWT

jwt.io/
www.jsonwebtoken.io/

9.2 JWT 的原理

img

  • JWT 认证流程:
    • 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
    • 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
    • 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用 Bearer 模式添加 JWT,其内容看起来是下面这样
1
Authorization: Bearer <token>
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
  • 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
  • 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

9.3 JWT 的使用方式

  • 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

方式一

  • 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。

    1
    2
    3
    GET /calendar/v1/events
    Host: api.example.com
    Authorization: Bearer <token>
  • 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制

    • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。
    • 由于 JWT 是自包含的,因此减少了需要查询数据库的需要
    • JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。
    • 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

方式二

  • 跨域的时候,可以把 JWT 放在 POST 请求的数据体里。

方式三

  • 通过 URL 传输
1
http://www.example.com/user?token=xxx

9.4 项目中使用 JWT

项目地址

10. Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌
  • 都可以记录用户的信息
  • 都是使服务端无状态化
  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源

区别:

  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
  • JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

11. 常见的前后端鉴权方式

  1. Session-Cookie
  2. Token 验证(包括 JWT,SSO)
  3. OAuth2.0(开放授权)

12. 常见的加密算法

image.png

  • 哈希算法(Hash Algorithm)又称散列算法、散列函数、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。哈希算法将数据重新打乱混合,重新创建一个哈希值。
  • 哈希算法主要用来保障数据真实性(即完整性),即发信人将原始消息和哈希值一起发送,收信人通过相同的哈希函数来校验原始数据是否真实。
  • 哈希算法通常有以下几个特点:
    • 正像快速:原始数据可以快速计算出哈希值
    • 逆向困难:通过哈希值基本不可能推导出原始数据
    • 输入敏感:原始数据只要有一点变动,得到的哈希值差别很大
    • 冲突避免:很难找到不同的原始数据得到相同的哈希值,宇宙中原子数大约在 10 的 60 次方到 80 次方之间,所以 2 的 256 次方有足够的空间容纳所有的可能,算法好的情况下冲突碰撞的概率很低:
      • 2 的 128 次方为 340282366920938463463374607431768211456,也就是 10 的 39 次方级别
      • 2 的 160 次方为 1.4615016373309029182036848327163e+48,也就是 10 的 48 次方级别
      • 2 的 256 次方为 1.1579208923731619542357098500869 × 10 的 77 次方,也就是 10 的 77 次方

注意:

  1. 以上不能保证数据被恶意篡改,原始数据和哈希值都可能被恶意篡改,要保证不被篡改,可以使用 RSA 公钥私钥方案,再配合哈希值。
  2. 哈希算法主要用来防止计算机传输过程中的错误,早期计算机通过前 7 位数据第 8 位奇偶校验码来保障(12.5% 的浪费效率低),对于一段数据或文件,通过哈希算法生成 128bit 或者 256bit 的哈希值,如果校验有问题就要求重传。

13. 常见问题

  • 因为存储在客户端,容易被客户端篡改,使用前需要验证合法性
  • 不要存储敏感数据,比如用户密码,账户余额
  • 使用 httpOnly 在一定程度上提高安全性
  • 尽量减少 cookie 的体积,能存储的数据量不能超过 4kb
  • 设置正确的 domain 和 path,减少数据传输
  • cookie 无法跨域
  • 一个浏览器针对一个网站最多存 20 个 Cookie,浏览器一般只允许存放 300 个 Cookie
  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

13.2 使用 session 时需要考虑的问题

  • 将 session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session
  • 当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。因为 session 是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建 session 的服务器,那么该服务器就无法拿到之前已经放入到 session 中的登录凭证之类的信息了。
  • 当多个应用要共享 session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 cookie 跨域的处理。
  • sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 怎么办? 一般会把 sessionId 跟在 url 参数后面即重写 url,所以 session 不一定非得需要靠 cookie 实现
  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

13.3 使用 token 时需要考虑的问题

  • 如果你认为用数据库来存储 token 会导致查询时间太长,可以选择放在内存当中。比如 redis 很适合你对 token 查询的需求。
  • token 完全由应用管理,所以它可以避开同源策略
  • token 可以避免 CSRF 攻击(因为不需要 cookie 了)
  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

13.4 使用 JWT 时需要考虑的问题

  • 因为 JWT 并不依赖 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  • JWT 不加密的情况下,不能将秘密数据写入 JWT。
  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  • JWT 最大的优势是服务器不再需要存储 Session,使得服务器认证鉴权业务可以方便扩展。但这也是 JWT 最大的缺点:由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。
  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  • JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT,真正实现无状态。
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

13.5 使用加密算法时需要考虑的问题

  • 绝不要以明文存储密码
  • 永远使用 哈希算法 来处理密码,绝不要使用 Base64 或其他编码方式来存储密码,这和以明文存储密码是一样的,使用哈希,而不要使用编码。编码以及加密,都是双向的过程,而密码是保密的,应该只被它的所有者知道, 这个过程必须是单向的。哈希正是用于做这个的,从来没有解哈希这种说法, 但是编码就存在解码,加密就存在解密。
  • 绝不要使用弱哈希或已被破解的哈希算法,像 MD5 或 SHA1 ,只使用强密码哈希算法。
  • 绝不要以明文形式显示或发送密码,即使是对密码的所有者也应该这样。如果你需要 “忘记密码” 的功能,可以随机生成一个新的 一次性的(这点很重要)密码,然后把这个密码发送给用户。

13.6 分布式架构下 session 共享方案

1. session 复制

  • 任何一个服务器上的 session 发生改变(增删改),该节点会把这个 session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 session ,以此来保证 session 同步

优点: 可容错,各个服务器间 session 能够实时响应。
缺点: 会对网络负荷造成一定压力,如果 session 量大的话可能会造成网络堵塞,拖慢服务器性能。

2. 粘性 session /IP 绑定策略

  • 采用 Ngnix 中的 ip_hash 机制,将某个 ip 的所有请求都定向到同一台服务器上,即将用户与服务器绑定。 用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 session 机制。

优点: 简单,不需要对 session 做任何处理。
缺点: 缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 session 信息都将失效。
适用场景: 发生故障对客户产生的影响较小;服务器发生故障是低概率事件 。
实现方式: 以 Nginx 为例,在 upstream 模块配置 ip_hash 属性即可实现粘性 session。

3. session 共享(常用)

  • 使用分布式缓存方案比如 Memcached 、Redis 来缓存 session,但是要求 Memcached 或 Redis 必须是集群
  • 把 session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis ,但是这种方案带来的好处也是很大的:
    • 实现了 session 共享;
    • 可以水平扩展(增加 Redis 服务器);
    • 服务器重启 session 不丢失(不过也要注意 session 在 Redis 中的刷新/失效机制);
    • 不仅可以跨服务器 session 共享,甚至可以跨平台(例如网页端和 APP 端)

img

4. session 持久化

  • 将 session 存储到数据库中,保证 session 的持久化

优点: 服务器出现问题,session 不会丢失
缺点: 如果网站的访问量很大,把 session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。

13.7 只要关闭浏览器 ,session 真的就消失了?

不对。对 session 来说,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 log off 的时候发个指令去删除 session。
然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间。

14. 项目地址

在项目中使用 JWT

15. 后语

  • 本文只是基于自己的理解讲了理论知识,因为对后端/算法知识不是很熟,如有谬误,还请告知,万分感谢
  • 如果本文对你有所帮助,还请点个赞~~

16. 参考

百度百科-cookie

百度百科-session

详解 Cookie,Session,Token

一文彻底搞懂 Cookie、Session、Token 到底是什么

3 种 web 会话管理的方式!!!

Token ,Cookie 和 Session 的区别!!!

彻底理解 cookie、session、token!!!

前端鉴权

SHA-1

SHA-2

SHA-3

不要再使用 MD5 和 SHA1 加密密码了!

廖雪峰 Node 教程之 crypto

17. 推荐阅读

你真的了解 React 生命周期吗

React Hooks 详解 【近 1W 字】+ 项目实战

React SSR 详解【近 1W 字】+ 2 个项目实战

从 0 到 1 实现一款简易版 Webpack

反射的基本原理

原文地址:https://juejin.im/post/5b2f8bd2f265da59b457cf47

作者:YangAM

小结:

反射就是指程序在运行时能够动态的获取到一个类的类型信息的一种操作。

获取一个 Class 对象的方法主要有以下三种。

类名.class

getClass 方法

forName 方法

img

图片来自 https://www.cnblogs.com/fengmingyue/p/5973260.html

『反射』就是指程序在运行时能够动态的获取到一个类的类型信息的一种操作。它是现代框架的灵魂,几尽所有的框架能够提供的一些自动化机制都是靠反射实现的,这也是为什么各类框架都不允许你覆盖掉默认的无参构造器的原因,因为框架需要以反射机制利用无参构造器创建实例。

总的来说,『反射』是很值得大家花时间学习的,尽管大部分人都很少有机会去手写框架,但是这将有助于你对于各类框架的理解。不奢求你通过本篇文章的学习对于『反射』能够有多么深层次的理解,但至少保证你了解『反射』的基本原理及使用。

1. Class 类型信息

之间介绍过虚拟机的类加载机制,其中我们提到过,每一种类型都会在初次使用时被加载进虚拟机内存的『方法区』中,包含类中定义的属性字段,方法字节码等信息。

Java 中使用类 java.lang.Class 来指向一个类型信息,通过这个 Class 对象,我们就可以得到该类的所有内部信息。而获取一个 Class 对象的方法主要有以下三种。

类名.class

这种方式就比较简单,只要使用类名点 class 即可得到方法区该类型的类型信息。例如:

1
2
3
4
5
Object.class;
Integer.class;
int.class;
String.class;
//等等

getClass 方法

Object 类有这么一个方法:

1
public final native Class<?> getClass();

这是一个本地方法,并且不允许子类重写,所以理论上所有类型的实例都具有同一个 getClass 方法。具体使用上也很简单:

1
2
3
Integer integer = new Integer(12);
integer.getClass();
复制代码

forName 方法

forName 算是获取 Class 类型的一个最常用的方法,它允许你传入一个全类名,该方法会返回方法区代表这个类型的 Class 对象,如果这个类还没有被加载进方法区,forName 会先进行类加载。

1
2
3
4
public static Class<?> forName(String className) {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

由于方法区 Class 类型信息由类加载器和类全限定名唯一确定,所以想要去找这么一个 Class 就必须提供类加载器和类全限定名,这个 forName 方法默认使用调用者的类加载器。

当然,Class 类中也有一个 forName 重载,允许你传入类加载器和类全限定名来匹配方法区类型信息。

1
2
3
4
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader){
//.....
}

至此,通过这些方法你可以得到任意类的类型信息,该类的所有字段属性,方法表等信息都可以通过这个 Class 对象进行获取。

2. 反射字段属性

Class 中有关获取字段属性的方法主要以下几个:

  • public Field[] getFields():返回该类型的所有 public 修饰的属性,包括父类的
  • public Field getField(String name):根据字段名称返回相应的字段
  • public Field[] getDeclaredFields():返回本类型中申明的所有字段,包含非 public 修饰的但不包含父类中的
  • public Field getDeclaredField(String name):同理

当然,一个 Field 实例包含某个类的一个属性的所有信息,包括字段名称,访问修饰符,字段类型。除此之外,Field 还提供了大量的操作该属性值的方法,通过传入一个类实例,就可以直接使用 Field 实例操作该实例的当前字段属性的值。

例如:

1
2
3
4
//定义一个待反射类
public class People {
public String name;
}
1
2
3
4
5
Class<People> cls = People.class;
Field name = cls.getField("name");
People people = new People();
name.set(people,"hello");
System.out.println(people.name);

程序会输出:

1
hello

其实也很简单,set 方法会检索 People 对象是否具有一个 name 代表的字段,如果有将字符串 hello 赋值给该字段即可。

整个 Field 类主要由两大部分组成,第一部分就是有关该字段属性的描述信息,例如名称,类型,外围类 Class 对象等,第二部分就是大量的 get 和 set 方法用于间接操作任意的外围类实例的当前属性值。

3. 反射方法

同样的,Class 类也提供了四种方法来获取其中的方法属性:

  • public Method[] getMethods():返回所有的 public 方法,包括父类中的
  • public Method getMethod(String name, Class<?>… parameterTypes):返回指定的方法
  • public Method[] getDeclaredMethods():返回本类申明的所有方法,包括非 public 修饰的,但不包括父类中的
  • public Method getDeclaredMethod(String name, Class<?>… parameterTypes):同理

Method 抽象地代表了一个方法,同样有描述这个方法基本信息的字段和方法,例如方法名,方法的参数集合,方法的返回值类型,异常类型集合,方法的注解等。

除此之外的还有一个 invoke 方法用于间接调用其他实例的该方法,例如:

1
2
3
4
5
6
public class People {

public void sayHello(){
System.out.println("hello wrold ");
}
}
1
2
3
4
Class<People> cls = People.class;
Method sayHello = cls.getMethod("sayHello");
People people = new People();
sayHello.invoke(people);

程序输出:

1
hello wrold

4. 反射构造器

对于 Constructor 来说,Class 类依然为它提供了四种获取实例的方法:

  • public Constructor<?>[] getConstructors():返回所有 public 修饰的构造器
  • public Constructor<?>[] getDeclaredConstructors():返回所有的构造器,无视访问修饰符
  • public Constructor getConstructor(Class<?>… parameterTypes):带指定参数的
  • public Constructor getDeclaredConstructor(Class<?>… parameterTypes) :同理

Constructor 本质上也是一个方法,只是没有返回值而已,所以内部的基本内容和 Method 是类似的,只不过 Constructor 类中有一个 newInstance 方法用于创建一个该 Class 类型的实例对象出来。

1
2
3
4
//最简单的一个反射创建实例的过程
Class<People> cls = People.class;
Constructor c = cls.getConstructor();
People p = (People) c.newInstance();

以上,我们简单的介绍了反射的基本使用情况,但都很基础,下面我们看看反射和一些稍微复杂的类型结合使用的情况,例如:数组,泛型,注解等。

5. 反射的其他细节

5.1 反射与数组

我们都知道,数组是一种特殊的类型,它本质上由虚拟机在运行时动态生成,所以在反射这种类型的时候会稍有不同。

1
public native Class<?> getComponentType();

Class 中有这么一个方法,该方法将返回数组 Class 实例元素的基本类型。只有当前的 Class 对象代表的是一个数组类型的时候,该方法才会返回数组的元素实际类型,其他的任何时候都会返回 null。

当然,有一点需要注意下,代表数组的这个由虚拟机动态创建的类型,它直接继承的 Object 类,并且所有有关数组类的操作,比如为某个元素赋值或是获取数组长度的操作都直接对应一个单独的虚拟机数组操作指令。

同样也因为数组类直接由虚拟机运行时动态创建,所以你不可能从一个数组类型的 Class 实例中得到构造方法,编译器根本没机会为类生成默认的构造器。于是你也不能以常规的方法通过 Constructor 来创建一个该类的实例对象。

如果你非要尝试使用 Constructor 来创建一个新的实例的话,那么运行时程序将告诉你无法匹配一个构造器。像这样:

1
2
3
Class<String[]> cls = String[].class;
Constructor constructor = cls.getConstructor();
String[] strs = (String[]) constructor.newInstance();

控制台输出:

告诉你,Class 实例中根本找不到一个无参的构造器。那么难道我们就没有办法来动态创建一个数组了吗?

当然不是,Java 中有一个类 java.lang.reflect.Array 提供了一些静态的方法用于动态的创建和获取一个数组类型。

1
2
3
4
5
//创建一个一维数组,componentType 为数组元素类型,length 数组长度
public static Object newInstance(Class<?> componentType, int length)

//可变参数 dimensions,指定多个维度的单维度长度
public static Object newInstance(Class<?> componentType, int... dimensions)

这是我认为 Array 类中最重要的两个方法,当然了 Array 类中还有一些其它方法用于获取指定数组的指定位置元素,这里不再赘述了。

完全是因为数组这种类型并不是由常规的编译器编译生成,而是由虚拟机动态创建的,所以想要通过反射的方式实例化一个数组类型是得依赖 Array 这个类型的相关 newInstance 方法的。

5.2 反射与泛型

泛型是 Java 编译器范围内的概念,它能够在程序运行之前提供一定的安全检查,而反射是运行时发生的,也就是说如果你反射调用一个泛型方法,实际上就绕过了编译器的泛型检查了。我们看一段代码:

1
2
3
4
5
6
7
8
ArrayList<Integer> list = new ArrayList<>();
list.add(23);
//list.add("fads");编译不通过

Class<?> cls = list.getClass();
Method add = cls.getMethod("add",Object.class);
add.invoke(list,"hello");
System.out.println(list.get(1));

最终你会发现我们从整型容器中取出一个字符串,因为虚拟机只管在运行时从方法区找到 ArrayList 这个类的类型信息并解析出它的 add 方法,接着执行这个方法。

它不像一般的方法调用,调用之前编译器会检测这个方法存在不存在,参数类型是否匹配等,所以没了编译器的这层安全检查,反射地调用方法更容易遇到问题。

除此之外,之前我们说过的泛型在经过编译期之后会被类型擦除,但实际上代表该类型的 Class 类型信息中是保存有一些基本的泛型信息的,这一点我们可以通过反射得到。

这里不再带大家一起去看了,Class ,Field 和 Method 中都是有相关方法可以获取类或者方法在定义的时候所使用到的泛型类名名称。注意这里说的,只是名称,类似 E、V 这样的东西。

Spring IOC过程源码分析

作者:valarchie
原文地址:http://vc2x.com/articles/2019/09/10/1568056055049.html

小结:

  • 第一步:读取 xml 文件形成 DOM 对象
  • 第二步:读取 DOM 文档对象里的 Bean 定义并装载进 BeanFactory 中
  • 第三步:使用创建好的 Bean 定义,开始实例化 Bean。

废话不多说,我们先做一个傻瓜版的 IOC demo 作为例子

自定义的 Bean 定义

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

public String id;
public String className;
public String value;

public MyBeanDefinition(String id, String className, String value) {
this.id = id;
this.className = className;
this.value = value;
}

}

自定义的 Bean 工厂

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

Map<String, Object> beanMap = new HashMap<>();

public MyBeanFactory(MyBeanDefinition beanDefinition) throws ClassNotFoundException,
IllegalAccessException, InstantiationException {

Class<?> beanClass = Class.forName(beanDefinition.className);
Object bean = beanClass.newInstance();
((UserService) bean).setName(beanDefinition.value);
beanMap.put(beanDefinition.id, bean);

}

public Object getBean(String id) {
return beanMap.get(id);
}


}

测试傻瓜版 IOC 容器

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

public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {

MyBeanDefinition beanDefinition = new MyBeanDefinition("userService",
"com.valarchie.UserService", "archie");

MyBeanFactory beanFactory = new MyBeanFactory(beanDefinition);
UserService userService = (UserService) beanFactory.getBean("userService");

System.out.println(userService.getName());

}


}

看完以上这个傻瓜版的例子我们可以思考一下?让我们自己实现 IOC 的容器的关键是什么呢?

按照我的理解,我总结为以下三步

  • 读取 xml 文件形成 DOM 对象
  • 读取 DOM 文档对象里的 Bean 定义并装载进 BeanFactory 中
  • 根据 bean 定义生成实例放进容器,以供使用

所以,接下来我们不会通盘分析整个 IOC 的流程,因为旁枝细节太多读者看完也云里雾里抓不到重点。
我们通过分析最重要的这条代码主干线来理解 IOC 的过程。

开始分析:

首先我们从 xml 的配置方式开始分析,因为 Spring 最初的配置方式就是利用 xml 来进行配置,所以大部分人对 xml 的配置形式较为熟悉,也比较方便理解。

从 ClassPathXmlApplicationContext 的构造器开始讲起。

1
2
3
4
5
6
7
8
9
public class TestSpring {
public static void main(String[] args) {
// IOC容器的启动就从ClassPathXmlApplicationContext的构造方法开始
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml");
UserService userService = (UserService) context.getBean("userService");
System.out.println(userService.getName());

}
}

进入到构造方法中,调用重载的另一个构造方法。

1
2
3
4
// 创建ClassPathXmlApplicationContext,加载给定的位置的xml文件,并自动刷新context
public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
this(new String[] {configLocation}, true, null);
}

重载的构造方法中,由于刚才 parrent 参数传为 null,所以不设置父容器。refresh 刚才设置为 true,流程就会进入 refresh()方法中

1
2
3
4
5
6
7
8
9
10
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {
// 由于之前的方法调用将parent设置为null,所以我们就不分析了
super(parent);
// 设置路径数组,并依次对配置路径进行简单占位符替换处理,比较简单,我们也不进入分析了
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}

整个 refresh()方法中就是 IOC 容器启动的主干脉络了,Spring 采用了模板方法设计模式进行 refresh()方法的设计,先规定好整个 IOC 容器的具体步骤,然后将每一个小步骤由各种不同的子类自己实现。

所有重要的操作都是围绕着 BeanFactory 在进行。
在注释当中,我们详细的列出了每一步方法所完成的事情。ApplicationContext 内部持有了 FactoryBean 的实例。其实 ApplicationContext 本身最上层的父接口也是 BeanFactory,他拓展了 BeanFactory 之外的功能(提供国际化的消息访问、资源访问,如 URL 和文件、事件传播、载入多个(有继承关系)上下文)

我们先通过阅读代码中的注释来了解大概的脉络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public void refresh() throws BeansException, IllegalStateException {
// 先加锁防止启动、结束冲突
synchronized (this.startupShutdownMonitor) {
// 在刷新之前做一些准备工作
// 设置启动的时间、相关状态的标志位(活动、关闭)、初始化占位符属性源,并确认
// 每个标记为必须的属性都是可解析的。
prepareRefresh();

// 获取一个已刷新的BeanFactory实例。
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 定义好Bean工厂的环境特性,例如类加载器,或者后置处理器
prepareBeanFactory(beanFactory);

try {
// 设置在BeanFactory完成初始化之后做一些后置操作,spring留给子类的扩展。
postProcessBeanFactory(beanFactory);

// 启动之前已设置的BeanFactory后置处理器
invokeBeanFactoryPostProcessors(beanFactory);

// 注册Bean处理器
registerBeanPostProcessors(beanFactory);

// 为我们的应用上下文设置消息源(i18n)
initMessageSource();

// 初始化事件广播器
initApplicationEventMulticaster();

// 初始化特殊的Bean在特殊的Context中,默认实现为空,交给各个具体子类实现
onRefresh();

// 检查监听器并注册
registerListeners();

// 实例化所有非懒加载的Bean
finishBeanFactoryInitialization(beanFactory);

// 最后一步发布相应的事件
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// 如果启动失败的话,要销毁之前创建的Beans。
destroyBeans();

// 重置ApplicationContext内部active的标志位
cancelRefresh(ex);

// 向调用者抛出异常
throw ex;
}

finally {
// 重置Spring核心内的缓存,因为我们可能不再需要单例bean相关的元数据
resetCommonCaches();
}
}
}

阅读完之后我们重点关注 obtainFreshBeanFactory()、finishBeanFactoryInitialization(beanFactory)这两个方法,因为实质上整个 IOC 的流程都在这两个方法当中,其他的方法一部分是 Spring 预留给用户的自定义操作如 BeanFactory 的后置处理器和 Bean 后置处理器,一部分是关键启动事件的发布和监听操作,一部分是关于 AOP 的操作。

首先,先从 obtainFreshBeanFactory()开始说起。

第一步:读取 xml 文件形成 DOM 对象

在 getBeanFactory()方法之前,先调用 refreshBeanFactory()方法进行刷新。我们先说明一下,getBeanFactory()非常简单,默认实现只是将上一步刷新成功好构建好的 Bean 工厂进行返回。返回出去的 Bean 工厂已经加载好 Bean 定义了。所以在 refreshBeanFactory()这个方法中已经包含了第一步读取 xml 文件构建 DOM 对象和第二步解析 DOM 中的元素生成 Bean 定义进行保存。记住,这里仅仅是保存好 Bean 定义,此时并未涉及 Bean 的实例化。

1
2
3
4
5
6
7
8
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}

进入 refreshBeanFactory()方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected final void refreshBeanFactory() throws BeansException {
// 如果当前ApplicationContext中已存在FactoryBean的话进行销毁
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
// 先生成一个BeanFactory
DefaultListableBeanFactory beanFactory = createBeanFactory();
// 设置序列化
beanFactory.setSerializationId(getId());
// 设置是否可以覆盖Bean定义和是否可以循环依赖,具体我就不解释了
customizeBeanFactory(beanFactory);
// 加载Bean定义到Factory当中去
// 重点!
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

接下来,进入核心方法 loadBeanDefinitions(beanFactory)中,参数是刚创建的 beanFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 根据传入的beanfactory创建一个xml读取器
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);


// 设置bean定义读取器的相关资源加载环境
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// 这个方法让子类自定义读取器Reader的初始化
initBeanDefinitionReader(beanDefinitionReader);
// 接着开始实际加载Bean定义
loadBeanDefinitions(beanDefinitionReader);
}

进入 loadBeanDefinitions(beanDefinitionReader)方法中,参数是刚刚创建好的 Reader 读取器。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
// 如果有已经生成好的Resouce实例的话就直接进行解析。
// 默认的实现是返回null,由子类自行实现。
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
// 没有Resouces的话就进行路径解析。
String[] configLocations = getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}

我们进入 reader.loadBeanDefinitions(configLocations)方法中,这里面方法调用有点绕,我这边只简单地描述一下

该方法会根据多个不同位置的 xml 文件依次进行处理。
接着会对路径的不同写法进行不同处理,例如 classpath 或者 WEB-INF 的前缀路径。
根据传入的 locations 变量生成对应的 Resouces。
紧接着进入 reader.loadBeanDefinitions(resource)此时参数是 Resource。
在经过一层进入 loadBeanDefinitions(new EncodedResource(resource))的方法调用中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
// 通过ThreadLocal实现的当前currentResource
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<EncodedResource>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {

// 最主要的方法在这段
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
// 传入流对象,并设置好编码
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}

该方法最主要是创建了对应的输入流,并设置好编码。

然后开始调用 doLoadBeanDefinitions()方法。

1
2
3
4
// 内部核心代码就这两句
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);

在 loadDocument()方法中会生成一个 DocumentBuilderImpl 对象,这个对象会调用 parse 方法,在 parse 方法中使用 SAX 进行解析刚才的输入流包装的 InputSource,生成 DOM 对象返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Document parse(InputSource is) throws SAXException, IOException {
if (is == null) {
throw new IllegalArgumentException(
DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN,
"jaxp-null-input-source", null));
}
if (fSchemaValidator != null) {
if (fSchemaValidationManager != null) {
fSchemaValidationManager.reset();
fUnparsedEntityHandler.reset();
}
resetSchemaValidator();
}
// 解析xml
domParser.parse(is);
// 获取刚才解析好的dom
Document doc = domParser.getDocument();
domParser.dropDocumentReferences();
return doc;
}

此时我们的 xml 文件已经加载并解析成 DOM 结构对象了,第一步已经完成了。

第二步:读取 DOM 文档对象里的 Bean 定义并装载进 BeanFactory 中

1
2
3
4
// 内部核心代码就这两句
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);

我们再回到刚刚讲到的这两句核心代码,第一句获取 DOM 对象后,紧接着第二句 registerBeanDefinitions(doc, resource)开始了 bean 定义的注册工作。

进入 registerBeanDefinitions(doc, resource)方法中

1
2
3
4
5
6
7
8
9
10
11
12
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 生成DOM读取器,这个和刚才的读取器不一样,之前的读取器是xml读取器。
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 获取之前的bean定义数量
int countBefore = getRegistry().getBeanDefinitionCount();

// 进入重点
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));

// 用刚刚又创建的bean定义数量 - 之前的bean定义数量 = 刚刚一共创建的bean定义
return getRegistry().getBeanDefinitionCount() - countBefore;
}

进入 documentReader.registerBeanDefinitions(doc, createReaderContext(resource))方法。
方法内读取文档的 root 元素。

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
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
BeanDefinitionParserDelegate parent = this.delegate;

// 生成Bean定义解析类
this.delegate = createDelegate(getReaderContext(), root, parent);

// 如果是xml文档中的namespace,进行相应处理
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

// spring预留给子类的拓展性方法
preProcessXml(root);

// 重点
// 开始解析Bean定义
parseBeanDefinitions(root, this.delegate);

// spring预留给子类的拓展性方法
postProcessXml(root);

this.delegate = parent;

}

进入 parseBeanDefinitions(root, this.delegate)。将之前的文档对象和 bean 定义解析类作为参数传入。

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
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
// 遍历去解析根节点的每个子节点元素
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
// 如果是标签元素的话
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {

// 解析默认的元素
// 重点
parseDefaultElement(ele, delegate);
}
else {
// 解析指定自定义元素
delegate.parseCustomElement(ele);
}
}
}
}
else {
// 非默认命名空间的,进行自定义解析,命名空间就是xml文档头内的xmlns,用来定义标签。
delegate.parseCustomElement(root);
}
}

进入到 parseDefaultElement(ele, delegate)当中,会发现其实对四种标签进行分别的解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
// 分析Bean标签
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}

我们主要分析 Bean 元素标签的解析,进入 processBeanDefinition(ele, delegate)方法中最内层。

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
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
// 获取bean标签内的id
String id = ele.getAttribute(ID_ATTRIBUTE);
// 获取bean标签内的name
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);

// 设置多别名
List<String> aliases = new ArrayList<String>();
if (StringUtils.hasLength(nameAttr)) {
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
aliases.addAll(Arrays.asList(nameArr));
}

// 当没有设置id的时候
String beanName = id;
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
beanName = aliases.remove(0);
if (logger.isDebugEnabled()) {
logger.debug("No XML 'id' specified - using '" + beanName +
"' as bean name and " + aliases + " as aliases");
}
}

// 检查beanName是否唯一
if (containingBean == null) {
checkNameUniqueness(beanName, aliases, ele);
}
// 内部做了Bean标签的解析工作
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
if (!StringUtils.hasText(beanName)) {
try {
if (containingBean != null) {
beanName = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, this.readerContext.getRegistry(), true);
}
else {
beanName = this.readerContext.generateBeanName(beanDefinition);
// Register an alias for the plain bean class name, if still possible,
// if the generator returned the class name plus a suffix.
// This is expected for Spring 1.2/2.0 backwards compatibility.
String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName != null &&
beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
aliases.add(beanClassName);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Neither XML 'id' nor 'name' specified - " +
"using generated bean name [" + beanName + "]");
}
}
catch (Exception ex) {
error(ex.getMessage(), ele);
return null;
}
}
String[] aliasesArray = StringUtils.toStringArray(aliases);
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}

return null;
}

将解析好的 Bean 定义并附加别名数组填入 new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray)中进行返回。然后调用以下这个方法。

1
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())

最主要的操作就是将刚才解析好的 Bean 定义放入 beanDefinitionMap 中去。

解析成功后将 Bean 定义进行保存。第二步也已经完成。

第三步:使用创建好的 Bean 定义,开始实例化 Bean。

我们回到最开始的 refresh 方法中,在 finishBeanFactoryInitialization(beanFactory)方法中,开始实例化非懒加载的 Bean 对象。我们跟着调用链进入到 preInstantiateSingletons()方法中

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
@Override
public void preInstantiateSingletons() throws BeansException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Pre-instantiating singletons in " + this);
}

// 将之前做好的bean定义名列表拷贝放进beanNames中,然后开始遍历
List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);

// 触发所有非懒加载的单例Bean实例化
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);

// 如果非抽象并且是单例和非懒加载的话
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 检测是否是工厂方法Bean。 创建Bean的不同方式,读者可自行百度。
if (isFactoryBean(beanName)) {
final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
return ((SmartFactoryBean<?>) factory).isEagerInit();
}
}, getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
getBean(beanName);
}
}
else {
getBean(beanName);
}
}
}

// 关于实例化之后做的自定义操作代码省略....
}

在该方法中根据 Bean 实例是通过工厂方法实例还是普通实例化,最主要的方法还是 getBean(beanName)方法。我们继续分析普通实例化的过程。进入 getBean()方法当中 doGetBean()方法,发现方法参数 doGetBean(name, null, null, false)后三个参数全部为 null,它就是整个 IOC 中的核心代码。

代码中先通过实例化 Bean,实例化好之后再判断该 Bean 所需的依赖,并递归调用进行实例化 bean,成功后整个 IOC 的核心流程也就完成了。

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
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {

final String beanName = transformedBeanName(name);
Object bean;

// 从缓存中获取beanName对应的单例
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
if (logger.isDebugEnabled()) {
if (isSingletonCurrentlyInCreation(beanName)) {
logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
"' that is not fully initialized yet - a consequence of a circular reference");
}
else {
logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
}
}
// 返回beanName对应的实例对象(主要用于FactoryBean的特殊处理,
// 普通Bean会直接返回sharedInstance本身)
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}

else {
// scope为prototype的循环依赖校验:如果beanName已经正在创建Bean实
// 例中,而此时我们又要再一次创建beanName的实例,则代表出现了循环
// 依赖,需要抛出异常。
// 例子:如果存在A中有B的属性,B中有A的属性,那么当依赖注入的时候
//,就会产生当A还未创建完的时候因为对于B的创建再次返回创建A,造成
//循环依赖
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}

//如果parentBeanFactory存在,并且beanName在当前BeanFactory不存在
//Bean定义,则尝试从parentBeanFactory中获取bean实例
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// 将别名解析成真正的beanName
String nameToLookup = originalBeanName(name);
if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}

// 如果不需要类型检查的话 标记为已创建
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}

try {
// 将子Bean定义与父Bean定义进行整合
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// 整合后如果发现是抽象类不能实例 抛出异常
checkMergedBeanDefinition(mbd, beanName, args);

// 获取Bean定义所需的依赖并逐一初始化填充
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
// 判断是否循环依赖
if (isDependent(beanName, dep)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
}
// 注册依赖的Bean
registerDependentBean(dep, beanName);
try {
// 递归调用生成所需依赖的Bean
getBean(dep);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
}
}
}

// 如果是单例的话
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

// 如果是原型的话
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
// 非单例和原型 范围的情况 例如session,request等情况
else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}

// 检测实例Bean的类型和所需类型是否一致
if (requiredType != null && bean != null && !requiredType.isInstance(bean)) {
try {
return getTypeConverter().convertIfNecessary(bean, requiredType);
}
catch (TypeMismatchException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to convert bean '" + name + "' to required type '" +
ClassUtils.getQualifiedName(requiredType) + "'", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
return (T) bean;
}

根据 Bean 定义去实例化 Bean。第三步也已经完成。

文章篇幅有限,IOC 整个的创建过程还是比较冗长的,希望读者看完文章对 IOC 的创建过程有一个主干脉络的思路之后还是需要翻开源码进行解读,其实阅读源码并不难,因为 Spring 的代码注释都挺健全,如果遇到不清楚的稍微 google 一下就知道了。建议读者自己试着一步一步的分析 IOC 过程的源码。

  • Copyrights © 2015-2023 高行行
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信