0%

前言

我相信大家都知道ArrayList使用ForEach遍历时,不能修改结构。
但是我一直知其然不知其所以然,有一次面试官问我这个问题,我发现从来都没考虑过原理。所以有了这篇博客。

一、环境(Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ArrayListForEachRemoveTest {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();

for (int i = 0; i < 10; i++) {
list.add(i);
}

// 第40行
for (Integer integer : list) {
list.remove(1);
}
}
}

当我准备编译的时候idea已经建议 不要在遍历中使用【list.remove】

二、执行代码&处理结果

不出意料的抛了一个异常。

1
2
3
4
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.github.test.ArrayListForEachRemoveTest.main(ArrayListForEachRemoveTest.java:40)

明明只是遍历了list,为什么报错在第40行循环上呢?
下面让我们看看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
package com.github.test;

import java.util.ArrayList;
import java.util.Iterator;

public class ArrayListForEachRemoveTest {
public ArrayListForEachRemoveTest() {
}

public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList();

for(int i = 0; i < 10; ++i) {
list.add(i);
}

Iterator var4 = list.iterator();

while(var4.hasNext()) {
Integer integer = (Integer)var4.next();
list.remove(1);
}

}
}

原来ForEach语句只是一个语法糖,前端编译器只是将这个语法生成一个迭代器进行处理。
异常结合class文件可知迭代器执行next()方法时抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
// ...
public E next() {
checkForComodification();
// ...
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

}

list有一个成员变量modCount,当创建迭代器时,直接通过外部类赋值expectedModCount变量。
每次执行next()方法比较外部类和迭代器中的Count
list调用add()remove()等方法时,会修改modCount的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

总结

以前写代码从来没有注意到的细节,还是缺少了一些源码阅读的意识。

一、预备知识

树叶: 没有儿子节点的叫做树叶(leaf)。
深度: 对任意节点n,n的深度(depth)为从根到n的唯一路径长。因此根的深度为0。
高度: 对任意节点n,n的高度是从n到一片树叶的最长路径的长。

二、树的实现

相比顺序结构如:链表、数组。树的结构使其每一个节点除了数据以外还需要有一些指针,使得该节点的每一个儿子都有一个指针指向它。
以二叉树为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TreeNode {
public int value;
public TreeNode left;
public TreeNode right;
public int height;

public TreeNode(int value) {
this.value = value;
}

@Override
public String toString() {
return "TreeNode{" +
"value=" + value +
'}';
}
}

三、树的遍历及应用

树有很多应用。比如文件系统、省市区搜索框等。

3.1 先序遍历

对节点的处理在它的儿子之前叫做先序遍历。

1
2
3
4
5
6
7
8
9
10
/**
* 先序打印
* parent -> left -> right
*/
public static void printPreorder(TreeNode head) {
if (head == null) return;
System.out.print(head.value + ",");
printPreorder(head.left);
printPreorder(head.right);
}

3.2 中序遍历

1
2
3
4
5
6
7
8
9
10
/**
* 中序打印
* left -> parent -> right
*/
public static void printInorder(TreeNode head) {
if (head == null) return;
printInorder(head.left);
System.out.print(head.value + ",");
printInorder(head.right);
}

3.3 后序遍历

对节点的处理在它的儿子之后叫做后序遍历。

1
2
3
4
5
6
7
8
9
10
/**
* 后序打印
* left -> right -> parent
*/
public static void printPostorder(TreeNode head) {
if (head == null) return;
printPostorder(head.left);
printPostorder(head.right);
System.out.print(head.value + ",");
}

四、二叉树

定义:每一个节点最多拥有两个儿子。

4.1 二叉查找树

定义:对于任意一个节点X,它的左子树所有关键字都小于X的关键字,而它的右子树中所有的关键字都大于X的关键字

4.1.1 Find

查找非常的简单,相当于折半查找,只是用了递归。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static TreeNode find(TreeNode node, int val) {
if (node == null) {
return null;
}

if (val < node.value) {
return find(node.left, val);
} else if (val > node.value) {
return find(node.right, val);
} else {
return node;
}
}

4.1.2 Insert

Find差不多,只是Find是返回,Insert是创建。

1
2
3
4
5
6
7
8
9
10
11
12
public static TreeNode insert(TreeNode head, int k) {
if (head == null) {
head = new TreeNode(k);
}
if (k < head.value) {
head.left = insert(head.left, k);
} else if (k > head.value) {
head.right = insert(head.right, k);
}

return head;
}

4.1.3 FindMin

以二叉查找树的定义,根节点最左边的儿子就是最小值。

1
2
3
4
public static TreeNode findMin(TreeNode node) {
if (node.left == null) return node;
return findMin(node.left);
}

4.1.4 Delete

一般删除策略只是替换删除的节点。

  • 1.该节点为树叶节点
    树叶节点可以直接删除。
  • 2.该节点只有左/右儿子
    使用左/右儿子进行替换。
  • 3.该节点都存在左右儿子
    删除策略是用其右字树的最左的节点的数据代替删除的节点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static TreeNode delete(TreeNode node, int k) {
    if (k < node.value) {
    node.left = delete(node.left, k);
    } else if (k > node.value) {
    node.right = delete(node.right, k);
    } else if (node.left != null && node.right != null) {
    // 一般删除策略是用其右字树的最小的数据代替删除的那个节点
    TreeNode minNode = findMin(node.right);
    node.value = minNode.value;
    node.right = delete(node.right, node.value);
    } else if (node.left == null) {
    node = node.right;
    } else {
    node = node.left;
    }
    return node;
    }

    4.1.5 二叉树应用

    如:4.99 * 1.06 + 5.99 + 6.99 * 1.06
    现实中常用的表达式,称为中缀表达式。一般计算表达式使用中缀转后缀,有两种常用方法:
  1. 二叉树

以二叉树为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
/**
* @author JiHongYuan
* @date 2021/6/18 14:34
*/
public class ExpressTree {
public static void main(String[] args) {
String expression = "4.99 * 1.06 + 5.99 + 6.99 * 1.06";
List<String> stringList = SuffixCalc.infixTransformSuffix(expression);


Stack<Node<String>> stack = new Stack<>();
for (String str : stringList) {
if (SuffixCalc.SymbolEnum.getSymbolEnumByString(str) != SuffixCalc.SymbolEnum.NONE) {
Node<String> parent = new Node<>(str, stack.pop(), stack.pop());
stack.push(parent);
} else {
stack.push(new Node<>(str, null, null));
}
}

Node<String> root = stack.pop();
printInorder(root);
System.out.println();
System.out.println(calc(root).toString());
}

public static <E> void printInorder(Node<E> head) {
if (head == null) return;
System.out.print("( ");
printInorder(head.left);
System.out.print(head.item + " ");
printInorder(head.right);
System.out.print(" ) ");
}

public static <E> E calc(Node<E> node) {
if (node == null) return null;

SuffixCalc.SymbolEnum symbol = SuffixCalc.SymbolEnum.getSymbolEnumByString(node.item.toString());

if (symbol != SuffixCalc.SymbolEnum.NONE) {
BigDecimal n1 = new BigDecimal(calc(node.left).toString());
BigDecimal n2 = new BigDecimal(calc(node.right).toString());

BigDecimal result;
switch (symbol) {
case ADD:
result = n1.add(n2);
break;
case SUB:
result = n1.subtract(n2);
break;
case MUL:
result = n1.multiply(n2);
break;
case DIV:
result = n1.divide(n2);
break;
default:
result = BigDecimal.ZERO;
}
node.item = (E) result.toString();
}

return node.item;
}

private static class Node<E> {
E item;
Node<E> left;
Node<E> right;


public Node(E item, Node<E> left, Node<E> right) {
this.item = item;
this.left = left;
this.right = right;
}


@Override
public String toString() {
return "Node{" +
"item=" + item +
", left=" + left +
", right=" + right +
'}';
}

}

}

4.2 AVL树

定义:AVL树是带有平衡条件的二叉查找树,每个节点的左子树和右子树的高度最多差1。
我们把必须重新平衡的节点叫做a。由于任意节点最多有两个儿子,英雌高度不平衡是,a点的两个字数的高度差2。这种不平衡的可能出现下面四种:

  1. 对a的左儿子的左树进行一次插入。
  2. 对a的左儿子的右树进行一次插入。
  3. 对a的右儿子的左树进行一次插入。
  4. 对a的右儿子的右树进行一次插入。

情形1和4是关于a点的镜像对称,而2和3是关于a点的镜像对象。因此理论上只有两个情况。

第一种情况:插入在外侧(左左、右右)使用单旋转(single rotation)。
第二种情况:插入在内侧(左右、右左)使用双旋转(double rotation)。

4.2.1 单旋转

单旋转由于查找树的特性,非常易懂。

4.2.1.1 左旋转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
            5            
/ \
4 8
/
3
/
2

|
v

5
/ \
3 8
/ \
2 4
1
2
3
4
5
6
7
8
9
10
public static TreeNode singleRotateWithLeft(TreeNode k2) {
TreeNode k1;
k1 = k2.left;
k2.left = k1.right;
k1.right = k2;

k1.height = Math.max(TreeUtil.height(k1.left), TreeUtil.height(k1.right)) + 1;
k2.height = Math.max(TreeUtil.height(k2.left), TreeUtil.height(k2.right)) + 1;
return k1;
}

4.2.1.2 右旋转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
          5            
/ \
4 8
\
9
\
10

|
v

5
/ \
4 9
/ \
8 10
1
2
3
4
5
6
7
8
9
10
11
public static TreeNode singleRotateWithRight(TreeNode k2) {
TreeNode k1;

k1 = k2.right;
k2.right = k1.left;
k1.left = k2;

k1.height = Math.max(TreeUtil.height(k1.left), TreeUtil.height(k1.right)) + 1;
k2.height = Math.max(TreeUtil.height(k2.left), TreeUtil.height(k2.right)) + 1;
return k1;
}

4.2.2 双旋转

双旋转略微复杂一点,只需要将左右、右左类型转化为情况1(即左左、右右)。

4.2.2.1 左右

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
            8            
/ \
4 9
/ \
3 5
\
6

|
v

8
/ \
5 9
/ \
4 6
/
3
|
v

5
/ \
4 8
/ / \
3 6 9
1
2
3
4
public static TreeNode doubleRotateWithLeft(TreeNode k3) {
k3.left = singleRotateWithRight(k3.left);
return singleRotateWithLeft(k3);
}

4.2.2.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
            5            
/ \
4 8
/ \
6 9
\
7

|
v

5
/ \
4 6
\
8
/ \
7 9

|
v
6
/ \
5 8
/ / \
4 7 9
1
2
3
4
public static TreeNode doubleRotateWithRight(TreeNode k3) {
k3.right = singleRotateWithLeft(k3.right);
return singleRotateWithRight(k3);
}

4.2.3 Insert

递归到插入的节点时,从下往下重新计算高度。
主要难点在这里,有点难理解。

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
public static TreeNode insert(TreeNode T, int val) {
if (T == null) {
T = new TreeNode(val);
} else if (val < T.value) {
// 插入在左边
T.left = insert(T.left, val);

// 找到破坏平衡的节点
if (height(T.left) - height(T.right) == 2) {
if (val < T.left.value) {
// 左左条件 单旋转
T = singleRotateWithLeft(T);
} else {
// 左右条件 双旋转
T = doubleRotateWithLeft(T);
}
}
} else if (val > T.value) {
T.right = insert(T.right, val);
if (height(T.right) - height(T.left) == 2) {
if (val > T.right.value) {
T = singleRotateWithRight(T);
} else {
T = doubleRotateWithRight(T);
}
}
}
// 重新计算节点高度
T.height = Math.max(height(T.left), height(T.right)) + 1;
return T;
}

4.2.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
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
/**
* @author JiHongYuan
* @date 2021/6/17 17:51
*/
public class AvlTree<E extends Comparable<E>> {
Node<E> root;

private static class Node<E> {
E item;
int height;
Node<E> left;
Node<E> right;


public Node(E item, Node<E> left, Node<E> right) {
this.item = item;
this.left = left;
this.right = right;
}


@Override
public String toString() {
return "Node{" +
"item=" + item +
", height=" + height +
", left=" + left +
", right=" + right +
'}';
}
}

public int height(Node<E> node) {
if (node == null) {
return -1;
}
return node.height;
}

public boolean add(E e) {
root = insert(root, e);
return true;
}

public Node<E> get(E item) {
return node(root, item);
}

/**
* return specified element item.
*/
Node<E> node(Node<E> node, E item) {
if (item.compareTo(node.item) < 0) {
return node(node.left, item);
} else if (item.compareTo(node.item) > 0) {
return node(node.right, item);
} else {
return node;
}
}

private Node<E> singleRotateWithLeft(Node<E> k2) {
Node<E> k1;
k1 = k2.left;
k2.left = k1.right;
k1.right = k2;

k1.height = Math.max(height(k1.left), height(k1.right)) + 1;
k2.height = Math.max(height(k2.left), height(k2.right)) + 1;
return k1;
}

private Node<E> singleRotateWithRight(Node<E> k2) {
Node<E> k1;

k1 = k2.right;
k2.right = k1.left;
k1.left = k2;

k1.height = Math.max(height(k1.left), height(k1.right)) + 1;
k2.height = Math.max(height(k2.left), height(k2.right)) + 1;
return k1;
}

private Node<E> doubleRotateWithLeft(Node<E> k3) {
k3.left = singleRotateWithRight(k3.left);
return singleRotateWithLeft(k3);
}

private Node<E> doubleRotateWithRight(Node<E> k3) {
k3.right = singleRotateWithLeft(k3.right);
return singleRotateWithRight(k3);
}

private Node<E> insert(Node<E> node, E item) {
if (node == null) {
node = new Node<>(item, null, null);
} else if (item.compareTo(node.item) < 0) {
// 插入在左边
node.left = insert(node.left, item);

// 找到破坏平衡的节点
if (height(node.left) - height(node.right) == 2) {
if (item.compareTo(node.left.item) < 0) {
// 左左条件 单旋转
node = singleRotateWithLeft(node);
} else {
// 左右条件 双旋转
node = doubleRotateWithLeft(node);
}
}
} else if (item.compareTo(node.item) > 0) {
node.right = insert(node.right, item);
if (height(node.right) - height(node.left) == 2) {
if (item.compareTo(node.right.item) > 0) {
node = singleRotateWithRight(node);
} else {
node = doubleRotateWithRight(node);
}
}
}
// 重新计算节点高度
node.height = Math.max(height(node.left), height(node.right)) + 1;
return node;
}

private static void writeArray(Node currNode, int rowIndex, int columnIndex, String[][] res, int treeDepth) {
// 保证输入的树不为空
if (currNode == null) return;
// 先将当前节点保存到二维数组中
res[rowIndex][columnIndex] = String.valueOf(currNode.item);

// 计算当前位于树的第几层
int currLevel = ((rowIndex + 1) / 2);
// 若到了最后一层,则返回
if (currLevel == treeDepth) return;
// 计算当前行到下一行,每个元素之间的间隔(下一行的列索引与当前元素的列索引之间的间隔)
int gap = treeDepth - currLevel - 1;

// 对左儿子进行判断,若有左儿子,则记录相应的"/"与左儿子的值
if (currNode.left != null) {
res[rowIndex + 1][columnIndex - gap] = "/";
writeArray(currNode.left, rowIndex + 2, columnIndex - gap * 2, res, treeDepth);
}

// 对右儿子进行判断,若有右儿子,则记录相应的"\"与右儿子的值
if (currNode.right != null) {
res[rowIndex + 1][columnIndex + gap] = "\\";
writeArray(currNode.right, rowIndex + 2, columnIndex + gap * 2, res, treeDepth);
}
}

// 用于获得树的层数
private int getTreeDepth(Node<E> root) {
return root == null ? 0 : (1 + Math.max(getTreeDepth(root.left), getTreeDepth(root.right)));
}

public void show() {
// 得到树的深度
int treeDepth = getTreeDepth(root);

// 最后一行的宽度为2的(n - 1)次方乘3,再加1
// 作为整个二维数组的宽度
int arrayHeight = treeDepth * 2 - 1;
int arrayWidth = (2 << (treeDepth - 2)) * 3 + 1;
// 用一个字符串数组来存储每个位置应显示的元素
String[][] res = new String[arrayHeight][arrayWidth];
// 对数组进行初始化,默认为一个空格
for (int i = 0; i < arrayHeight; i++) {
for (int j = 0; j < arrayWidth; j++) {
res[i][j] = " ";
}
}

// 从根节点开始,递归处理整个树
// res[0][(arrayWidth + 1)/ 2] = (char)(root.val + '0');
writeArray(root, 0, arrayWidth / 2, res, treeDepth);

// 此时,已经将所有需要显示的元素储存到了二维数组中,将其拼接并打印即可
for (String[] line : res) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < line.length; i++) {
sb.append(line[i]);
if (line[i].length() > 1 && i <= line.length - 1) {
i += line[i].length() > 4 ? 2 : line[i].length() - 1;
}
}
System.out.println(sb.toString());

}
}
}


/**
* @author JiHongYuan
* @date 2021/6/17 18:24
*/
public class AvlTreeTest {
public static void main(String[] args) {
AvlTree<Integer> avlTree = new AvlTree<>();

Integer[] integers = {5, 4, 6, 7, 8, 11};
for (Integer integer : integers) {
avlTree.add(integer);
}
avlTree.show();

System.out.println(avlTree.get(5));
avlTree.show();

}
}

前言

数据结构与算法 -C语言实现
习题 3.19 3.20

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
package com.github.数据结构与算法分析.stack;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
* @author JiHongYuan
* @date 2021/6/14 13:45
*/
public class SuffixCalc {

public static void main(String[] args) {
String expression = "4.99 * 1.06 + 5.99 + 6.99 * 1.06";
List<String> suffixList = infixTransformSuffix(expression);

Stack<BigDecimal> stack = new Stack<>();
for (String s : suffixList) {
SymbolEnum symbol = SymbolEnum.getSymbolEnumByString(s);
if (symbol == SymbolEnum.NONE) {
stack.push(new BigDecimal(s));
} else {
BigDecimal i1 = stack.pop();
BigDecimal i2 = stack.pop();
BigDecimal iNew;
switch (symbol) {
case ADD:
iNew = i1.add(i2);
break;
case SUB:
iNew = i1.subtract(i2);
break;
case MUL:
iNew = i1.multiply(i2);
break;
case DIV:
iNew = i1.divide(i2);
break;
default:
iNew = new BigDecimal("0.00");
}
stack.push(iNew);
}
}

System.out.println(stack.pop());
}

/**
* 中缀表达式转换成后缀表达式
*
* @param expression 中缀表达式
* @return 后缀表达式
*/
public static List<String> infixTransformSuffix(String expression) {
List<String> suffixList = new ArrayList<>();

Stack<SymbolEnum> stack = new Stack<>();
for (String s : expression.split(SymbolEnum.NONE.symbol)) {
SymbolEnum symbol = SymbolEnum.getSymbolEnumByString(s);
if (symbol == SymbolEnum.NONE) {
suffixList.add(s);
} else {
if (symbol == SymbolEnum.RIGHT_PARENTHESIS) {
suffixFiller(stack, suffixList, SymbolEnum.RIGHT_PARENTHESIS);
} else if (stack.isEmpty() || stack.peek().priority > symbol.priority) {
stack.push(symbol);
} else {
suffixFiller(stack, suffixList, SymbolEnum.NONE);
stack.push(symbol);
}
}
}
suffixFiller(stack, suffixList, SymbolEnum.NONE);
return suffixList;
}

/**
* 后缀表达式填充
*
* @param stack 数据源栈
* @param suffixList 填充目标列表
* @param symbol 当前表达式
*/
public static void suffixFiller(Stack<SymbolEnum> stack, List<String> suffixList, SymbolEnum symbol) {
while (!stack.isEmpty()) {
SymbolEnum top = stack.peek();
if (SymbolEnum.LEFT_PARENTHESIS == top) {
if (symbol == SymbolEnum.RIGHT_PARENTHESIS) stack.pop();

return;
}
suffixList.add(stack.pop().symbol);
}
}

enum SymbolEnum {
/**
*
*/
ADD("+", 4),
SUB("-", 4),
MUL("*", 3),
DIV("/", 3),
LEFT_PARENTHESIS("(", 1),
RIGHT_PARENTHESIS(")", 1),
NONE(" ", -1);
String symbol;
int priority;

SymbolEnum(String symbol, int priority) {
this.symbol = symbol;
this.priority = priority;
}

public static SymbolEnum getSymbolEnumByString(String s) {
for (SymbolEnum value : SymbolEnum.values()) {
if (s.equals(value.symbol)) {
return value;
}
}
return NONE;
}
}

}

前言

本文分享Mybatis的Configuration初始化流程,配置参考
重点分析XML资源文件解析和功能实现。

一、加载配置

Mybatis加载配置文件分为两个步骤:

  1. 将文件转换为资源

    1
    2
    3
    String resource = "mybatis-config.xml";
    // 使用类加载器加载配置文件获取InputStream
    InputStream inputStream = inputStream = Resources.getResourceAsStream(resource);
  2. 将资源解析为Configuration对象

    1
    2
    3
    // 正式开始解析
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null, null);
    return build(parser.parse());

二、解析Config

解析Configuration的由XMLConfigBuilder实现。

1. XMLConfigBuilder

TypeAliasRegistry:在Configuration中默认创建,配置参考
TypeHandlerRegistry:在Configuration中默认创建,配置参考

实际处理XML中Configuration节点由**parseConfiguration(…)**方法,我们从这个方法开始。

2. 属性(properties)

配置参考,方便编写配置,进行动态替换,配置如下:

1
2
3
4
<properties resource="config.properties">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis-test?characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;useJDBCCompliantTimezoneShift=true&amp;useLegacyDatetimeCode=false&amp;serverTimezone=GMT%2B8&amp;allowMultiQueries=true&amp;allowPublicKeyRetrieval=true"/>
</properties>
1
2
3
# config.properties
username = root
password = 123456

配置属性有三种方法:

  1. 直接在properties节点中定义property。

  2. properties标签resource属性。

  3. properties标签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
    // root.evalNode会将占位符解析成属性
    propertiesElement(root.evalNode("properties"));

    private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
    // 在properties节点中定义的属性
    Properties defaults = context.getChildrenAsProperties();
    // 配置在资源文件中。
    String resource = context.getStringAttribute("resource");
    // 从网络中获取资源文件
    String url = context.getStringAttribute("url");
    if (resource != null && url != null) {
    throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
    }
    if (resource != null) {
    defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
    defaults.putAll(Resources.getUrlAsProperties(url));
    }
    Properties vars = configuration.getVariables();
    if (vars != null) {
    defaults.putAll(vars);
    }
    // 注意,evalNode调用时会尝试从variables中读取变量
    parser.setVariables(defaults);
    // 设置变量
    configuration.setVariables(defaults);
    }
    }

    实际上在调用evalNode()方法时已经用到了properties

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 比如getStringAttribute, attributes = parseAttributes(node);
    public String getStringAttribute(String name, Supplier<String> defSupplier) {
    String value = attributes.getProperty(name);
    return value == null ? defSupplier.get() : value;
    }

    // 解析属性
    private Properties parseAttributes(Node n) {
    Properties attributes = new Properties();
    NamedNodeMap attributeNodes = n.getAttributes();
    if (attributeNodes != null) {
    for (int i = 0; i < attributeNodes.getLength(); i++) {
    Node attribute = attributeNodes.item(i);
    // variables
    String value = PropertyParser.parse(attribute.getNodeValue(), variables);
    attributes.put(attribute.getNodeName(), value);
    }
    }
    return attributes;
    }

    3. 设置(settings)

    配置参考,设置启动 / 关闭功能,配置如下:

    1
    2
    3
    4
    <settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="defaultExecutorType" value="SIMPLE"/>
    </settings>

    设置属性太多,以这两个举例:

    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
    // 解析XML配置
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    // 设置VFS的实现
    loadCustomVfs(settings);
    // 设置自定义日志
    loadCustomLogImpl(settings);
    // ...
    // 配置设置默认属性
    settingsElement(settings);

    private Properties settingsAsProperties(XNode context) {
    if (context == null) {
    return new Properties();
    }
    // 读取配置信息
    Properties props = context.getChildrenAsProperties();
    // Check that all settings are known to the configuration class

    // 验证 Configuration中是否存在setting标签中set name属性方法。
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
    if (!metaConfig.hasSetter(String.valueOf(key))) {
    throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
    }
    }
    return props;
    }

    4. 类型别名(typeAliases)

    配置参考,设置缩写的别名,配置如下:

    1
    2
    3
    4
    <typeAliases>
    <typeAlias alias="student" type="com.example.entity.StudentEntity"/>
    <package name="com.example.entity"/>
    </typeAliases>

    XMLConfigBuilder UML图中展示了Configuration类中有一个成员变量:TypeAliasRegistry,我相信大家已经想到了Mybatis是如何做的?
    但需要一点注意,别名注册有两种形式:

  4. package:该包名下Java Bean都会注册别名(具体请看配置参考)。

  5. typeAlias:单个类注册别民。

Mybatis已经帮我注册了一些常用的别名,如:int、long…。

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
typeAliasesElement(root.evalNode("typeAliases"));

private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 把整个包名下所有对象注册别名
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// 单个对象注册
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}

5. 类型处理器(typeHandlers)

配置参考,TypeHandler相对复杂,用于Statement参数映射、ResultSet返回处理。将Java Type 与 JDBC Type相互转换,配置如下:

1
2
3
<typeHandlers>
<typeHandler handler="com.example.handler.ExampleTypeHandler"/>
</typeHandlers>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}

@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}

@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}

@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}

类型别名大体相同,同样支持两种方式,注册与类型别名相差无几,所以在此省略。

6. 对象工厂(objectFactory)

配置参考DefaultResultSetHandler会在处理结果集时使用对象工厂创建返回对象,配置如下:

1
2
3
<objectFactory type="com.example.handler.ExampleObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ExampleObjectFactory extends DefaultObjectFactory {
@Override
public Object create(Class type) {
return super.create(type);
}
@Override
public void setProperties(Properties properties) {
System.out.println(properties);
super.setProperties(properties);
}
@Override
public <T> boolean isCollection(Class<T> type) {
return Collection.class.isAssignableFrom(type);
}
}
// print
// -------------------------------
{someProperty=100}
// -------------------------------

ObjectFactory比较简单,默认实现为DefaultObjectFactory

1
2
3
4
5
6
7
8
9
10
11
12
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties properties = context.getChildrenAsProperties();
// 根据类型,创建ExampleObjectFactory
ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 调用set方法
factory.setProperties(properties);
// 设置到configuration中
configuration.setObjectFactory(factory);
}
}

7. 插件(plugins)

配置参考,Mybatis plugin提供的功能非常强大,可以实现相当丰富的功能。如:PageHelper(分页工具),强烈建议参考文档学习,对于一些特定的场景有奇效。配置如下:

1
2
3
4
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
</plugin>
</plugins>

以PageHelper为例,注意plugin节点中可以和objectFactory一样配置property标签,我在这里并没有体现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
// plugin -> property
Properties properties = child.getChildrenAsProperties();
// 创建plugin 拦截器
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
// 添加到configuration拦截器链中
configuration.addInterceptor(interceptorInstance);
}
}
}

8. environments(环境配置)

配置参考,提供三个功能:

  1. 配置事务管理器

提供事务管理,比如:Executor提供commitrollback等事务方法,实际上由Transaction执行。

  1. 配置数据源

配置数据源,可选择具体数据库连接池(这里默认Mybatis自带)。

  1. 支持切换环境

发布不同版本,切换不同的环境。
配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
<environments default="development">
<environment id="development">
<!-- 这里选择JBDC事务管理器 -->
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

根据设置默认环境,选择读取哪一个环境。
创建事务管理工厂、数据源工厂,设值进configuration成员中。

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
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 获取当前环境名称
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
// 判断当前默认环境 和 environmentId是否相同
if (isSpecifiedEnvironment(id)) {
// 解析事务管理,创建TransactionFactory实例
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析数据源
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// 构建环境
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 赋值
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}

9. databaseIdProvider(数据库厂商标识)

配置参考,根据不同数据库,可以选择不同的MappedStatement,不是很好用。配置如下:

1
2
3
4
5
6
7
8
9
10
11
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>

<select id="selectOne" resultType="com.example.entity.StudentEntity" databaseId="mysql">
select id, name from student
</select>
<select id="selectOne" resultType="com.example.entity.StudentEntity" databaseId="oracle">
select id, name from student
</select>

只能设置单个数据源databaseId,如果想用多数据源,得创建多个SqlSessionFactory做切换。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// awful patch to keep backward compatibility
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
Properties properties = context.getChildrenAsProperties();
// 创建VendorDatabaseIdProvider对象
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
// 设置当前数据源 对于databaseId
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}

10. mappers(映射器)

配置参考,告诉Mybatis去哪里寻找Mapper文件进行注册。配置如下:

1
2
3
4
5
6
7
<mappers>
<mapper resource="StudentMapper.xml"/>
<!-- 官网用例 -->
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<package name="org.mybatis.builder"/>
</mappers>

11. autoMapping(自动映射)

可以区分为两大类:

  1. XML:resource(资源路径)、url(URL)。
  2. Annotation:class(类路径)、package(包名下)。
    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
    mapperElement(root.evalNode("mappers"));
    private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
    for (XNode child : parent.getChildren()) {
    // 1. 注解模式:把整个包名下注册Mapper
    if ("package".equals(child.getName())) {
    String mapperPackage = child.getStringAttribute("name");
    configuration.addMappers(mapperPackage);
    } else {
    String resource = child.getStringAttribute("resource");
    String url = child.getStringAttribute("url");
    String mapperClass = child.getStringAttribute("class");
    // 2. XML 资源目录
    if (resource != null && url == null && mapperClass == null) {
    ErrorContext.instance().resource(resource);
    InputStream inputStream = Resources.getResourceAsStream(resource);
    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
    mapperParser.parse();
    }
    // 3. XML URL
    else if (resource == null && url != null && mapperClass == null) {
    ErrorContext.instance().resource(url);
    InputStream inputStream = Resources.getUrlAsStream(url);
    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
    mapperParser.parse();
    }
    // 4. 注解 单个
    else if (resource == null && url == null && mapperClass != null) {
    Class<?> mapperInterface = Resources.classForName(mapperClass);
    configuration.addMapper(mapperInterface);
    } else {
    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
    }
    }
    }
    }
    }

到此处,整个MybatisConfig全部完成工作,下面开始解析Mapper。

三、解析Mapper

1. XMLMapperBuilder

parse方法与XMLConfigBuilder有所不同。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void parse() {
// resource 是否加载过
if (!configuration.isResourceLoaded(resource)) {
// 解析Mapper标签
configurationElement(parser.evalNode("/mapper"));
// 添加到加载后集合
configuration.addLoadedResource(resource);
// 主要责任将Mapper加载到configuration,Mapper注册器中。
bindMapperForNamespace();
}

// 错误重新解析,有些先后初始化顺序会导致解析失败,此时在重新解析一遍。
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

2. cache(缓存)

配置参考,Mybatis一级缓存默认开启,二级缓存需要手动配置开启。
区别:
一级缓存:作用域Statement,多个Statement不共享。
二级缓存:作用域namespace,同一个namespace中Statement共享同一个缓存。有缓存淘汰机制,支持定制化操作。
配置如下:

1
<cache/>

二级缓存使用条件较为苛刻,一般不启动,以最简单的默认配置进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cacheElement(context.evalNode("cache"));
private void cacheElement(XNode context) {
if (context != null) {
// 获取缓存类型,这里以默认为例:PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
// 从别名注册器中找到该名称对应的类型
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 获取缓存获取策略, 默认LRU
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 刷新间隔
Long flushInterval = context.getLongAttribute("flushInterval");
// 引用数目
Integer size = context.getIntAttribute("size");
// 只读
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 使用助手创建缓存对象,并放到Configuration中,同时放到currentCache成员变量中,方便解析Statement使用。
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}

3. cache-ref(缓存引用)

配置参考,使多个namespace共用一个cache。有几个要注意的地方:

  1. cache配置会覆盖cache-ref(由于执行顺序cache-ref总是被先执行)
  2. 如果使用cache-ref,如果引用Mapper在此时还没有初始化,解析cache-ref、解析Statement将在后续的错误尝试中执行。

配置如下:

1
<cache-ref namespace="com.example.mapper.TestMapper"/>

涉及到缓存依赖问题,如果依赖的缓存还没有创建怎么办?
Mybatis选择在后续处理,而不是JVM / SpringBean 加载类时进行交叉加载。

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
cacheRefElement(context.evalNode("cache-ref"));
private void cacheRefElement(XNode context) {
if (context != null) {
// 添加configuration
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 解析cache-ref
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}

// 跳过了一些方法
public Cache resolveCacheRef() {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
// 未解决缓存引用 true,需要后续处理
unresolvedCacheRef = true;
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
// 这个字段用于后续statement处理缓存
currentCache = cache;
// 缓存引用已存在,必须要错误处理
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}

}

4. resultMap(结果映射)

配置参考,查询返回结果集进行映射。支持复杂结果处理,有四种方式:

  1. constructor: 调用构造方法
  2. association: 一对一
  3. collection: 一对多
  4. discriminator: 根据匹配字段的value,使用结果值来决定使用哪个 resultMapresult

    1. 支持功能

    默认的子节点:
    1
    2
    <id     property="id"   column="id"/>
    <result property="name" column="name"/>

id:一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
result:注入到字段或 JavaBean 属性的普通结果

1. constructor

1
2
3
4
5
6
7
<resultMap id="map" type="com.example.entity.StudentEntity">
<constructor>
<idArg column="id" name="id" javaType="long" />
<arg column="name" name="name" javaType="string" />
</constructor>
</resultMap>

constructor:拥有idArg、arg两个子节点,
在对结果集进行映射后,会调用构造方法进行创建对象。
需要注意一点:如果使用了该功能,java一般情况下反射是无法获取到形参名称,有两种解决方案:

  1. 添加 @Param 注解

    1
    2
    3
    4
    5
    public StudentEntity(@Param("id") Long id, @Param("name") String name) {
    this.id = id;
    this.name = name;
    }

  2. 开启编译选项**-parameter**

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
    <source>1.8</source>
    <target>1.8/</target>
    <compilerArgs>
    <arg>-parameters</arg>
    </compilerArgs>
    </configuration>
    </plugin>
    </plugins>

    2. association

    江湖人称:一对一关联关系,可嵌套。

    1
    2
    3
    4
    5
    <association property="child" javaType="com.example.entity.T4Entity">
    <id property="id" column="id5"/>
    <result property="username" column="name5"/>
    </association>
    <!ELEMENT association (constructor?,id*,result*,association*,collection*, discriminator?)>

    3. collection

    江湖人称:一对多关联关系,可嵌套。

    1
    2
    3
    4
    <collection property="childList" ofType="com.example.entity.T5Entity">
    <result property="id" column="id5"/>
    <result property="username" column="name5"/>
    </collection>

    4.discriminator

    监听器:根据字段的值case匹配的value返回不同的对象。

    1
    2
    3
    4
    5
    <discriminator javaType="long" column="id">
    <case value="3" resultType="com.example.entity.T1Entity"/>
    <case value="2" resultType="com.example.entity.T2Entity"/>
    <case value="1" resultMap ="map" />
    </discriminator>

    2.功能解析

    1. 解析resultMap

    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
    private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 获取标注的type名称
    String type = resultMapNode.getStringAttribute("type",
    resultMapNode.getStringAttribute("ofType",
    resultMapNode.getStringAttribute("resultType",
    resultMapNode.getStringAttribute("javaType"))));
    // 根据type名称获取class对象,因为可以缩写,需要别名注册器查询。
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
    // 处理没填resultMap
    typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    // 鉴定器,用来选择返回值的类型
    Discriminator discriminator = null;
    // 核心类,返回映射。resultSetHandler就是用这个来把结果集转换成指定的类型
    List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
    // 开始解析子节点
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
    // 解析构造方法
    if ("constructor".equals(resultChild.getName())) {
    processConstructorElement(resultChild, typeClass, resultMappings);
    }
    // 解析鉴定器
    else if ("discriminator".equals(resultChild.getName())) {
    discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    }
    // id、result、collection
    else {
    List<ResultFlag> flags = new ArrayList<>();
    if ("id".equals(resultChild.getName())) {
    flags.add(ResultFlag.ID);
    }
    //构建resultMapping,添加到集合中
    resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
    }
    String id = resultMapNode.getStringAttribute("id",
    resultMapNode.getValueBasedIdentifier());
    // 有没有继承
    String extend = resultMapNode.getStringAttribute("extends");
    // 有没有自动映射
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
    return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
    }
    }

    2. 处理constructor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
    List<XNode> argChildren = resultChild.getChildren();
    // 遍历constructor下元素
    for (XNode argChild : argChildren) {
    List<ResultFlag> flags = new ArrayList<>();
    flags.add(ResultFlag.CONSTRUCTOR);
    if ("idArg".equals(argChild.getName())) {
    flags.add(ResultFlag.ID);
    }
    // 添加到集合中
    resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
    }
    }

    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
    private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String typeHandler = context.getStringAttribute("typeHandler");

    // resolveClass 基本都是使用别名注册器获取
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);

    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    Map<String, String> discriminatorMap = new HashMap<>();

    // 解析case元素
    // case元素同样可以有id 、result
    for (XNode caseChild : context.getChildren()) {
    String value = caseChild.getStringAttribute("value");
    // 如果case元素的属性是resultMap, 会再次调用resultMapElement方法(注意这里会递归调用,这里很重要!!!)
    // 这个方法请看4
    String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType));
    discriminatorMap.put(value, resultMap);
    }
    // 构建一个鉴别器
    return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
    }

    4. 处理 id / result / collection

    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
    private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
    property = context.getStringAttribute("name");
    } else {
    property = context.getStringAttribute("property");
    }
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    // 如果元素属性同样使用了resultMap, 也会再次调用resultMapElement方法(注意这里会递归调用,这里很重要!!!)
    String nestedResultMap = context.getStringAttribute("resultMap", () -> processNestedResultMappings(context, Collections.emptyList(), resultType));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));

    // 别名注册器获取
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);

    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
    }

    // 处理嵌套返回映射
    private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) {
    if (Arrays.asList("association", "collection", "case").contains(context.getName())
    && context.getStringAttribute("select") == null) {
    validateCollection(context, enclosingType);
    // 这里会递归调用
    ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
    return resultMap.getId();
    }
    return null;
    }

    5. 构建ResultMapping

    不必太过研究,只需要有哪些成员有个印象。

    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
    public ResultMapping buildResultMapping(
    Class<?> resultType,
    String property,
    String column,
    Class<?> javaType,
    JdbcType jdbcType,
    // 需要select执行 MappedStatement
    String nestedSelect,
    // 需要注意一下,resultSetHandler解析需要嵌套执行
    String nestedResultMap,
    String notNullColumn,
    String columnPrefix,
    Class<? extends TypeHandler<?>> typeHandler,
    List<ResultFlag> flags,
    String resultSet,
    String foreignColumn,
    boolean lazy) {
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    List<ResultMapping> composites;
    if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
    composites = Collections.emptyList();
    } else {
    composites = parseCompositeColumnName(column);
    }
    return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
    .jdbcType(jdbcType)
    .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
    .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
    .resultSet(resultSet)
    .typeHandler(typeHandlerInstance)
    .flags(flags == null ? new ArrayList<>() : flags)
    .composites(composites)
    .notNullColumns(parseMultipleColumnNames(notNullColumn))
    .columnPrefix(columnPrefix)
    .foreignColumn(foreignColumn)
    .lazy(lazy)
    .build();
    }

    3. 发现问题

    constructor元素和discriminator case元素中resultMap属性同时使用,会导致创建resultMap, 获取构造方法名称没有类型导致NPE。
    已经提交PR到mybatis github,目前还没有同意。
    https://github.com/mybatis/mybatis-3/pull/2353

    5. sql

    配置参考,可被其它语句引用的可重用语句块。

    1
    <sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

    一般使用都是存放数据库的所有字段:

    1
    2
    3
    <sql id="baseColumn">
    t1.id, t1.account, t1.nick_name, t1.phone, t1.email, t1.pwd_reset_time, t1.avatar_name, t1.avatar_path, t1.status
    </sql>

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 存放当前数据库厂商sql
    private final Map<String, XNode> sqlFragments;

    private void sqlElement(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
    sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
    }

    private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
    // 隔离数据库厂商
    String databaseId = context.getStringAttribute("databaseId");
    String id = context.getStringAttribute("id");
    // id = namespace + "." + sql.id
    id = builderAssistant.applyCurrentNamespace(id, false);
    // 判断当前数据库厂商和 sql属性databaseId 是否匹配
    if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
    // 存放到sqlFragments
    sqlFragments.put(id, context);
    }
    }
    }

四、解析Statement

配置参考,解析Statement是在Mapper中解析的,由于篇幅过长,并且Statement很重要所有单独开一个小结。
Statement:select、insert、update、delete四大标签,对于执行器来说只有QueryUpdate方法。具体的配置就不在阐述了。

1. 解析CRUD元素

1
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

2. 解析Statement节点

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
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");

if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}

String nodeName = context.getNode().getNodeName();
// 区分 C、R、U、D 类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 是否刷新缓存,默认select不需要刷新,其他都需要
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 是否使用cache
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
// 一对多,collection 结果的笛卡尔积是否有序,可以减少缓存的对象。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());


// 参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);

String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);

// selectKey支持,可以查询某些数据,再set到insert / update语句中。
// 解析完成后需要删除 <selectKey> 元素,不参与sqlSource解析
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);

// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

// 重要!, 用于解析预处理sql, 参数。这里不会验证sql是否写对了。
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 类型,直接sql、预处理、存储过程
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
// 马上弃用了
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}

String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");

// 使用助手,构建MappedStatement,添加到configuration中。
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

// 只需要注意一下cache
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {

if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}

id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
// 这个currentCache就是当前namespace的缓存,上文提到过
// currentCache是整个namespace里的所有statement共享
.cache(currentCache);

// 弃用了,会在后续的版本中删除
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}

MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}

五、总结

照着官网走了一遍解析配置的流程,总体下来写的不是很满意,太粗糙了。
但是大部分常用的功能还是研究了,resultMap交叉解析这一块花了大功夫研究,遇到了很多问题,还碰见了Mybatis的BUG。
语言表达能力、写作能力有待训练,下面着重研究缓存和结果集解析功能。
后续可能会修改

前言

为了项目发布,老项目原本War直接放到服务器上Tomcat发布。由于Docker只有Java镜像改为Jar,使用了Jenkins+SonarQube+Docker。这里描述一下流程,不会具体的描述Jenkins一些配置。

一、pom文件改造

1
<packaging>jar</packaging>

打包方式选择Jar

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
<build>
<finalName>xxx</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.xxx.main.MainApplication</mainClass>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/**</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/webapp</directory>
<targetPath>META-INF/resources</targetPath>
<includes>
<include>**/**</include>
</includes>
</resource>
</resources>
</build>

编译配置,由于老项目是JSP,所以需要打包webaapp下资源文件。

1
<includeSystemScope>true</includeSystemScope>

如果项目采用了本地Jar导入,一定要加上这句。

二、Jenkins配置

1. 新建Maven项目

image.png

2.配置Git

image.png

3. 配置SonarQube

image.png
image.png

1
2
3
4
5
6
7
sonar.projectKey=xxx
sonar.sources=.
sonar.projectVersion=1.0
sonar.projectName=xxx
sonar.language=java
sonar.sourceEncoding=UTF-8
sonar.java.binaries=target/

sonar.java.binaries配置很重要。

4. 配置上传文件及执行Shell脚本

image.png
image.png

1
2
3
4
Source files: 需要上传的文件 如: target/projectName.version.jar
Remove prefix: 移除的前缀 如: target
Remote directory: 上传文件目标的服务器目录 如:usr/local/xxx
Exec command: 执行shell脚本的目标 如:/usr/local/xxx/server.sh
1
2
3
docker stop docker容器名称
docker rm docker容器名称
docker run --privileged=true --name docker容器名称 -d -p 8013:8080 -v /usr/local/xxx/xxx.jar:/mnt/app.jar java java -jar /mnt/app.jar

三、效果

image.png
image.png
image.png

四、总结

懂的都懂,网上都有。不是啥专业运维,只是记录一下踩坑日记。