0%

tmux 是 Linux 中窗口管理程序,适用于终端复用,尤其适合远程连接。最近,我正苦闷与 ssh 自动超时退出和 broken pipe,决定投入 tmux 怀抱。
使用 tmux 最直接的好处,便是可以在一个远程连接中开启多个控制台,而不用浪费额外的终端来连接远程主机。更不用说 tmux 方便的 attach/detach,随时保存工作状态,还有方便的复制粘贴功能。

基本使用
tmux 的主要元素分为三层:

Session: 一组窗口的集合,通常用来概括同一个任务。session 可以有自己的名字便于任务之间的切换。
Window: 单个可见窗口。Windows 有自己的编号,也可以认为和 ITerm2 中的 Tab 类似。
Pane: 窗格,被划分成小块的窗口,类似于 Vim 中 C-w +v 后的效果。

在安装好 tmux 后,直接在终端中输入 tmux 并回车,就进入了一个全新的 tmux 会话,输入 exit 即可退出。
可以在一台计算机上创建多个会话,并且通过为每个会话指定一个唯一的名称来管理它们:
tmux new-session -s basic
此命令可简化为:tmux new -s basic。
由于我们的程序是在 tmux 环境里运行的,因此需要一种方式来告诉 tmux 当前所输入的命令是为了让 tmux 去执行而不是 tmux 里的应用程序去执行,这就是命令前缀的作用。tmux 中默认的命令前缀是 CTRL-b 组合键,但 CTRL-b 组合键使用不是很方便,可将其修改为 CTRL-a 组合键。按下命令前缀组合键,松开,再按相应键,即发送 tmux 命令。

tmux 创建会话

  • tmux new-session 创建一个未命名的会话。可以简写为 tmux new 或者就一个简单的 tmux
  • tmux new -s development 创建一个名为 development 的会话
  • tmux new -s development -n editor 创建一个名为 development 的会话并把该会话的第一个窗口命名为 editor
  • tmux attach -t development 连接到一个名为 development 的会话

tmux 会话、窗口和面板的默认快捷键

  • PREFIX d 从一个会话中分离,让该会话在后台运行。
  • PREFIX : 进入命令模式
  • PREFIX c 在当前 tmux 会话创建一个新的窗口,是 new-window 命令的简写
  • PREFIX 0…9 根据窗口的编号选择窗口
  • PREFIX w 显示当前会话中所有窗口的可选择列表
  • PREFIX , 显示一个提示符来重命名一个窗口
  • PREFIX & 杀死当前窗口,带有确认提示
  • PREFIX % 把当前窗口垂直地一分为二,分割后的两个面板各占 50% 大小
  • PREFIX “ 把当前窗口水平地一分为二,分割后的两个面板各占 50% 大小
  • PREFIX o 在已打开的面板之间循环移动当前焦点
  • PREFIX q 短暂地显示每个面板的编号
  • PREFIX x 关闭当前面板,带有确认提示
  • PREFIX SPACE 循环地使用 tmux 的几个默认面板布局

tmux 复制粘贴

  • PREFIX [ 进入复制模式
  • PREFIX ] 粘贴
    进入复制模式后,可以用 vi 风格的快捷键进行移动(按上文的设置)。按下 sapce 就可以选择文本。回车键进行复制。然后再通过]进行粘贴。
    也可以将复制粘贴设置为类似 vi 的模式,使用 esc 进入复制模式,v 进入粘贴模式,选择后 y 进行复制。Prefix-p 进行粘贴。

Copy and paste like in vim

unbind [
bind Escape copy-mode
unbind p
bind p paste-buffer
bind -t vi-copy ‘v’ begin-selection
bind -t vi-copy ‘y’ copy-selection

所有的复制都会被记录到缓冲区,输入#或者 tmux list-buffers 查看缓冲区,同时也进入了复制模式。也可以使用”=”来选择并粘贴缓冲区内容。tmux 的缓冲区和系统剪贴板是完全独立的。

自主配置

把前缀键从 C-b 更改为 C-a

set -g prefix C-a

释放之前的 Ctrl-b 前缀快捷键

unbind C-b

设定前缀键和命令键之间的延时

set -sg escape-time 1

确保可以向其它程序发送 Ctrl-A

bind C-a send-prefix

把窗口的初始索引值从 0 改为 1

set -g base-index 1

把面板的初始索引值从 0 改为 1

setw -g pane-base-index 1

使用 Prefix r 重新加载配置文件

bind r source-file ~/.tmux.conf ; display “Reloaded!”

分割面板

bind | split-window -h
bind - split-window -v

在面板之间移动

bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R

快速选择面板

bind -r C-h select-window -t :-
bind -r C-l select-window -t :+

调整面板大小

bind -r H resize-pane -L 5
bind -r J resize-pane -D 5
bind -r K resize-pane -U 5
bind -r L resize-pane -R 5

鼠标支持 - 如果你想使用的话把 off 改为 on

setw -g mode-mouse off
set -g mouse-select-pane off
set -g mouse-resize-pane off
set -g mouse-select-window off

设置默认的终端模式为 256 色模式

set -g default-terminal “screen-256color”

开启活动通知

setw -g monitor-activity on
set -g visual-activity on

设置状态栏的颜色

set -g status-fg white
set -g status-bg black

设置窗口列表的颜色

setw -g window-status-fg cyan
setw -g window-status-bg default
setw -g window-status-attr dim

设置活动窗口的颜色

setw -g window-status-current-fg white
setw -g window-status-current-bg red
setw -g window-status-current-attr bright

设置面板和活动面板的颜色

set -g pane-border-fg green
set -g pane-border-bg black
set -g pane-active-border-fg white
set -g pane-active-border-bg yellow

设置命令行或消息的颜色

set -g message-fg white
set -g message-bg black
set -g message-attr bright

设置状态栏左侧的内容和颜色

set -g status-left-length 40
set -g status-left “#[fg=green]Session: #S #[fg=yellow]#I #[fg=cyan]#P”
set -g status-utf8 on

设置状态栏右侧的内容和颜色

15% | 28 Nov 18:15

set -g status-right “#(~/battery Discharging) | #[fg=cyan]%d %b %R”

每 60 秒更新一次状态栏

set -g status-interval 60

设置窗口列表居中显示

set -g status-justify centre

开启 vi 按键

setw -g mode-keys vi

在相同目录下使用 tmux-panes 脚本开启面板

unbind v
unbind n
bind v send-keys “ ~/tmux-panes -h” C-m
bind n send-keys “ ~/tmux-panes -v” C-m

临时最大化面板或恢复面板大小

unbind Up
bind Up new-window -d -n tmp ; swap-pane -s tmp.1 ; select-window -t tmp
unbind Down
bind Down last-window ; swap-pane -s tmp.1 ; kill-window -t tmp

把日志输出到指定文件

bind P pipe-pane -o “cat >>~/#W.log” ; display “Toggled logging to ~/#W.log”

1 Mockito 介绍

1.1 Mockito 是什么?

Mockito 是 mocking 框架,它让你用简洁的 API 做测试。而且 Mockito 简单易学,它可读性强和验证语法简洁。

1.2 为什么需要 Mock

测试驱动的开发( TDD)要求我们先写单元测试,再写实现代码。在写单元测试的过程中,我们往往会遇到要测试的类有很多依赖,这些依赖的类/对象/资源又有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。如下图所示:

为了测试类 A,我们需要 Mock B 类和 C 类(用虚拟对象来代替)如下图所示:

1.3 Stub 和 Mock 异同[1]

相同:Stub 和 Mock 都是模拟外部依赖
不同:Stub 是完全模拟一个外部依赖, 而 Mock 还可以用来判断测试通过还是失败

1.4 Mockito 资源

官网: http://mockito.org

API 文档:http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html

项目源码:https://github.com/mockito/mockito

1.5 使用场景

提前创建测试; TDD(测试驱动开发)
团队可以并行工作
你可以创建一个验证或者演示程序
为无法访问的资源编写测试
Mock 可以交给用户
隔离系统

2 使用 Mockito

添加 maven 依赖

1
2
3
4
5
6
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>

添加 junit 依赖

1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

添加引用

1
2
import static org.mockito.Mockito._;
import static org.junit.Assert._;

2.1 验证行为

1
2
3
4
5
6
7
8
9
10
11
@Test
public void verify_behaviour(){
//模拟创建一个 List 对象
List mock = mock(List.class);
//使用 mock 的对象
mock.add(1);
mock.clear();
//验证 add(1)和 clear()行为是否发生
verify(mock).add(1);
verify(mock).clear();
}

2.2 模拟我们所期望的结果

1
2
3
4
5
6
7
8
9
10
11
@Test
public void when_thenReturn(){
//mock 一个 Iterator 类
Iterator iterator = mock(Iterator.class);
//预设当 iterator 调用 next()时第一次返回 hello,第 n 次都返回 world
when(iterator.next()).thenReturn("hello").thenReturn("world");
//使用 mock 的对象
String result = iterator.next() + " " + iterator.next() + " " + iterator.next();
//验证结果
assertEquals("hello world world",result);
}
1
2
3
4
5
6
7
8
@Test(expected = IOException.class)
public void when_thenThrow() throws IOException {
OutputStream outputStream = mock(OutputStream.class);
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
//预设当流关闭时抛出异常
doThrow(new IOException()).when(outputStream).close();
outputStream.close();
}

2.3 RETURNS_SMART_NULLS 和 RETURNS_DEEP_STUBS

RETURNS_SMART_NULLS 实现了 Answer 接口的对象,它是创建 mock 对象时的一个可选参数,mock(Class,Answer)。

在创建 mock 对象时,有的方法我们没有进行 stubbing,所以调用时会放回 Null 这样在进行操作是很可能抛出 NullPointerException。如果通过 RETURNS_SMART_NULLS 参数创建的 mock 对象在没有调用 stubbed 方法时会返回 SmartNull。例如:返回类型是 String,会返回””;是 int,会返回 0;是 List,会返回空的 List。另外,在控制台窗口中可以看到 SmartNull 的友好提示。

1
2
3
4
5
6
7
8
@Test
public void returnsSmartNullsTest() {
List mock = mock(List.class, RETURNS_SMART_NULLS);
System.out.println(mock.get(0));

//使用 RETURNS_SMART_NULLS 参数创建的 mock 对象,不会抛出 NullPointerException 异常。另外控制台窗口会提示信息“SmartNull returned by unstubbed get() method on mock”
System.out.println(mock.toArray().length);
}

RETURNS_DEEP_STUBS 也是创建 mock 对象时的备选参数

RETURNS_DEEP_STUBS 参数程序会自动进行 mock 所需的对象,方法 deepstubsTest 和 deepstubsTest2 是等价的

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
@Test
public void deepstubsTest(){
Account account=mock(Account.class,RETURNS_DEEP_STUBS);
when(account.getRailwayTicket().getDestination()).thenReturn("Beijing");
account.getRailwayTicket().getDestination();
verify(account.getRailwayTicket()).getDestination();
assertEquals("Beijing",account.getRailwayTicket().getDestination());
}
@Test
public void deepstubsTest2(){
Account account=mock(Account.class);
RailwayTicket railwayTicket=mock(RailwayTicket.class);
when(account.getRailwayTicket()).thenReturn(railwayTicket);
when(railwayTicket.getDestination()).thenReturn("Beijing");

account.getRailwayTicket().getDestination();
verify(account.getRailwayTicket()).getDestination();
assertEquals("Beijing",account.getRailwayTicket().getDestination());
}

public class RailwayTicket{
private String destination;

public String getDestination() {
return destination;
}

public void setDestination(String destination) {
this.destination = destination;
}
}

public class Account{
private RailwayTicket railwayTicket;

public RailwayTicket getRailwayTicket() {
return railwayTicket;
}

public void setRailwayTicket(RailwayTicket railwayTicket) {
this.railwayTicket = railwayTicket;
}
}

2.4 模拟方法体抛出异常

@Test(expected = RuntimeException.class)
public void doThrow_when(){
List list = mock(List.class);
doThrow(new RuntimeException()).when(list).add(1);
list.add(1);
}

2.5 使用注解来快速模拟

在上面的测试中我们在每个测试方法里都 mock 了一个 List 对象,为了避免重复的 mock,是测试类更具有可读性,我们可以使用下面的注解方式来快速模拟对象:

@Mock
private List mockList;

OK,我们再用注解的 mock 对象试试

@Test
public void shorthand(){
    mockList.add(1);
    verify(mockList).add(1);
}

运行这个测试类你会发现报错了,mock 的对象为 NULL,为此我们必须在基类中添加初始化 mock 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MockitoExample2 {
@Mock
private List mockList;

public MockitoExample2(){
MockitoAnnotations.initMocks(this);
}

@Test
public void shorthand(){
mockList.add(1);
verify(mockList).add(1);
}

}

或者使用 built-in runner:MockitoJUnitRunner

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(MockitoJUnitRunner.class)
public class MockitoExample2 {
@Mock
private List mockList;

@Test
public void shorthand(){
mockList.add(1);
verify(mockList).add(1);
}

}

2.6 参数匹配

1
2
3
4
5
6
7
8
9
10
11
@Test
public void with_arguments(){
Comparable comparable = mock(Comparable.class);
//预设根据不同的参数返回不同的结果
when(comparable.compareTo("Test")).thenReturn(1);
when(comparable.compareTo("Omg")).thenReturn(2);
assertEquals(1, comparable.compareTo("Test"));
assertEquals(2, comparable.compareTo("Omg"));
//对于没有预设的情况会返回默认值
assertEquals(0, comparable.compareTo("Not stub"));
}

除了匹配制定参数外,还可以匹配自己想要的任意参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void with_unspecified_arguments(){
List list = mock(List.class);
//匹配任意参数
when(list.get(anyInt())).thenReturn(1);
when(list.contains(argThat(new IsValid()))).thenReturn(true);
assertEquals(1, list.get(1));
assertEquals(1, list.get(999));
assertTrue(list.contains(1));
assertTrue(!list.contains(3));
}

private class IsValid extends ArgumentMatcher<List>{
@Override
public boolean matches(Object o) {
return o == 1 || o == 2;
}
}

注意:如果你使用了参数匹配,那么所有的参数都必须通过 matchers 来匹配,如下代码:

1
2
3
4
5
6
7
8
9
@Test
public void all_arguments_provided_by_matchers(){
Comparator comparator = mock(Comparator.class);
comparator.compare("nihao","hello");
//如果你使用了参数匹配,那么所有的参数都必须通过 matchers 来匹配
verify(comparator).compare(anyString(),eq("hello"));
//下面的为无效的参数匹配使用
//verify(comparator).compare(anyString(),"hello");
}

2.7 自定义参数匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void argumentMatchersTest(){
//创建 mock 对象
List<String> mock = mock(List.class);

//argThat(Matches<T> matcher)方法用来应用自定义的规则,可以传入任何实现Matcher接口的实现类。
when(mock.addAll(argThat(new IsListofTwoElements()))).thenReturn(true);

mock.addAll(Arrays.asList("one","two","three"));
//IsListofTwoElements用来匹配size为2的List,因为例子传入List为三个元素,所以此时将失败。
verify(mock).addAll(argThat(new IsListofTwoElements()));
}

class IsListofTwoElements extends ArgumentMatcher<List>
{
public boolean matches(Object list)
{
return((List)list).size()==2;
}
}

2.8 捕获参数来进一步断言

较复杂的参数匹配器会降低代码的可读性,有些地方使用参数捕获器更加合适。

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
@Test
public void capturing_args(){
PersonDao personDao = mock(PersonDao.class);
PersonService personService = new PersonService(personDao);

ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
personService.update(1,"jack");
verify(personDao).update(argument.capture());
assertEquals(1,argument.getValue().getId());
assertEquals("jack",argument.getValue().getName());
}

class Person{
private int id;
private String name;

Person(int id, String name) {
this.id = id;
this.name = name;
}

public int getId() {
return id;
}

public String getName() {
return name;
}
}

interface PersonDao{
public void update(Person person);
}

class PersonService{
private PersonDao personDao;

PersonService(PersonDao personDao) {
this.personDao = personDao;
}

public void update(int id,String name){
personDao.update(new Person(id,name));
}
}

2.9 使用方法预期回调接口生成期望值(Answer 结构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void answerTest(){
when(mockList.get(anyInt())).thenAnswer(new CustomAnswer());
assertEquals("hello world:0",mockList.get(0));
assertEquals("hello world:999",mockList.get(999));
}

private class CustomAnswer implements Answer<String>{
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return "hello world:"+args[0];
}
}

也可使用匿名内部类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void answer_with_callback(){
//使用 Answer 来生成我们我们期望的返回
when(mockList.get(anyInt())).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return "hello world:"+args[0];
}
});
assertEquals("hello world:0",mockList.get(0));
assertEquals("hello world:999",mockList.get(999));
}

2.10 修改对未预设的调用返回默认期望

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void unstubbed_invocations(){
//mock 对象使用 Answer 来对未预设的调用返回默认期望值
List mock = mock(List.class,new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return 999;
}
});
//下面的 get(1)没有预设,通常情况下会返回 NULL,但是使用了 Answer 改变了默认期望值
assertEquals(999, mock.get(1));
//下面的 size()没有预设,通常情况下会返回 0,但是使用了 Answer 改变了默认期望值
assertEquals(999,mock.size());
}

2.11 用 spy 监控真实对象

Mock 不是真实的对象,它只是用类型的 class 创建了一个虚拟对象,并可以设置对象行为
Spy 是一个真实的对象,但它可以设置对象行为
InjectMocks 创建这个类的对象并自动将标记@Mock、@Spy 等注解的属性值注入到这个中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test(expected = IndexOutOfBoundsException.class)
public void spy_on_real_objects(){
List list = new LinkedList();
List spy = spy(list);
//下面预设的 spy.get(0)会报错,因为会调用真实对象的 get(0),所以会抛出越界异常
//when(spy.get(0)).thenReturn(3);

//使用doReturn-when可以避免when-thenReturn调用真实对象api
doReturn(999).when(spy).get(999);
//预设size()期望值
when(spy.size()).thenReturn(100);
//调用真实对象的api
spy.add(1);
spy.add(2);
assertEquals(100,spy.size());
assertEquals(1,spy.get(0));
assertEquals(2,spy.get(1));
verify(spy).add(1);
verify(spy).add(2);
assertEquals(999,spy.get(999));
spy.get(2);
}

2.12 真实的部分 mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void real_partial_mock(){
//通过 spy 来调用真实的 api
List list = spy(new ArrayList());
assertEquals(0,list.size());
A a = mock(A.class);
//通过 thenCallRealMethod 来调用真实的 api
when(a.doSomething(anyInt())).thenCallRealMethod();
assertEquals(999,a.doSomething(999));
}

class A{
public int doSomething(int i){
return i;
}
}

2.13 重置 mock

1
2
3
4
5
6
7
8
9
10
@Test
public void reset_mock(){
List list = mock(List.class);
when(list.size()).thenReturn(10);
list.add(1);
assertEquals(10,list.size());
//重置 mock,清除所有的互动和预设
reset(list);
assertEquals(0,list.size());
}

2.14 验证确切的调用次数

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 verifying_number_of_invocations(){
List list = mock(List.class);
list.add(1);
list.add(2);
list.add(2);
list.add(3);
list.add(3);
list.add(3);
//验证是否被调用一次,等效于下面的 times(1)
verify(list).add(1);
verify(list,times(1)).add(1);
//验证是否被调用 2 次
verify(list,times(2)).add(2);
//验证是否被调用 3 次
verify(list,times(3)).add(3);
//验证是否从未被调用过
verify(list,never()).add(4);
//验证至少调用一次
verify(list,atLeastOnce()).add(1);
//验证至少调用 2 次
verify(list,atLeast(2)).add(2);
//验证至多调用 3 次
verify(list,atMost(3)).add(3);
}

2.15 连续调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test(expected = RuntimeException.class)
public void consecutive_calls(){
//模拟连续调用返回期望值,如果分开,则只有最后一个有效
when(mockList.get(0)).thenReturn(0);
when(mockList.get(0)).thenReturn(1);
when(mockList.get(0)).thenReturn(2);
when(mockList.get(1)).thenReturn(0).thenReturn(1).thenThrow(new RuntimeException());
assertEquals(2,mockList.get(0));
assertEquals(2,mockList.get(0));
assertEquals(0,mockList.get(1));
assertEquals(1,mockList.get(1));
//第三次或更多调用都会抛出异常
mockList.get(1);
}

2.16 验证执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void verification_in_order(){
List list = mock(List.class);
List list2 = mock(List.class);
list.add(1);
list2.add("hello");
list.add(2);
list2.add("world");
//将需要排序的 mock 对象放入 InOrder
InOrder inOrder = inOrder(list,list2);
//下面的代码不能颠倒顺序,验证执行顺序
inOrder.verify(list).add(1);
inOrder.verify(list2).add("hello");
inOrder.verify(list).add(2);
inOrder.verify(list2).add("world");
}

2.17 确保模拟对象上无互动发生

1
2
3
4
5
6
7
8
9
10
11
@Test
public void verify_interaction(){
List list = mock(List.class);
List list2 = mock(List.class);
List list3 = mock(List.class);
list.add(1);
verify(list).add(1);
verify(list,never()).add(2);
//验证零互动行为
verifyZeroInteractions(list2,list3);
}

2.18 找出冗余的互动(即未被验证到的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test(expected = NoInteractionsWanted.class)
public void find_redundant_interaction(){
List list = mock(List.class);
list.add(1);
list.add(2);
verify(list,times(2)).add(anyInt());
//检查是否有未被验证的互动行为,因为 add(1)和 add(2)都会被上面的 anyInt()验证到,所以下面的代码会通过
verifyNoMoreInteractions(list);

List list2 = mock(List.class);
list2.add(1);
list2.add(2);
verify(list2).add(1);
//检查是否有未被验证的互动行为,因为add(2)没有被验证,所以下面的代码会失败抛出异常
verifyNoMoreInteractions(list2);
}

3 Mockito 如何实现 Mock

Mockito 并不是创建一个真实的对象,而是模拟这个对象,他用简单的 when(mock.method(params)).thenRetrun(result)语句设置 mock 对象的行为,如下语句:

// 设置 mock 对象的行为 - 当调用其 get 方法获取第 0 个元素时,返回”first”
Mockito.when(mockedList.get(0)).thenReturn(“first”);
在 Mock 对象的时候,创建一个 proxy 对象,保存被调用的方法名(get),以及调用时候传递的参数(0),然后在调用 thenReturn 方法时再把“first”保存起来,这样,就有了构建一个 stub 方法所需的所有信息,构建一个 stub。当 get 方法被调用的时候,实际上调用的是之前保存的 proxy 对象的 get 方法,返回之前保存的数据。

在 Go1.5 之前用 GOPATH 以及 GOROOT 这两个环境变量来管理包的位置,GOROOT 为 Go 的安装目录,以及编译过程中使用到的系统库存放位置,如 fmt。Go1.5 到 Go1.7 开始稳定到 Vendor 方式,即依赖包需要放到 $GOPATH/src/vendor 目录下,这样每个项目都有自己的 vendor 目录,但是如果依赖同样的三方包,很容易造成资源重复,Go vendor 出现了几种主流的管理工具,包括 godep、govendor、golide 等。

在 Go1.11 之前,GOPATH 是开发时的工作目录,其中包含三个子目录:

src 目录:存放 go 项目源码和依赖源码,包括使用 go get 下载的包
bin 目录:通过使用 go install 命令将 go build 编译出的二进制可执行文件存放于此
pkg 目录:go 源码包编译生成的 lib 文件存储的地方
在 Go1.11 之前,import 包时的搜索路径

GOROOT/src: 该目录保存了 Go 标准库代码(首先搜寻导入包的地方)
GOPATH/src: 该目录保存了应用自身的各个包代码和第三方依赖的代码
./vendor :vendor 方式第三方依赖包(如果支持 Vendor)
在 Unix 和类 Unix 系统上,GOPATH 默认值是 $HOME/go,Go1.11 版本后,开启 GO Modules 后,GOPATH 的作用仅仅为存放依赖的目录了。

在 Go 的 1.11 版本之前,GOPATH 是必需的,且所有的 Go 项目代码都要保存在 GOPATH/src 目录下,也就是如果想引用本地的包,你需要将包放在 $GOPATH/src 目录下才能找得到。Go 的 1.11 版本之后,GO 官方引入了 Go Modules,不仅仅方便的使用我们的依赖,而且还对依赖的版本进行了管理。

在 Go1.11 后通过 go mod vendor 和 -mod=vendor 来实现 Vendor 管理依赖方式。本来在 vgo 项目(Go Modules 前身)是要完全放弃 vendor,但是在社区反馈下还是保留了。总之就是在 Go.1.11 之后需要开启 Go Modules 条件下才能使用 Vendor,具体地感兴趣或还沿用了 Vendor 的朋友可以去了解下,不过建议以后仅使用 Go Modules 包管理方式了。

dep/govendor 机制

vendor 使用限制

使用 vendor 来管理包的项目,必须位于$GOPATH/src 下面。

vendor 目录和 json 文件

该工具将项目依赖的外部包拷贝到项目下的 vendor 目录下,并通过 vendor.json 文件来记录依赖包的版本,方便用户使用相对稳定的依赖。

vendor 机制下,如何搜索包依赖

那么查找依赖包路径的解决方案如下:

  • 当前包下的 vendor 目录。
  • 向上级目录查找,直到找到 src 下的 vendor 目录。
  • 在 GOPATH 下面查找依赖包。
  • 在 GOROOT 目录下查找

如果我们已经使用 GOPATH 去存储 packages 了,问什么还需要使用 vendor 目录呢?

这是一个很实战的问题。假如多个应用使用一个依赖包的不同版本?这个问题不只是 Go 应用,其他语言也会有这个问题。
vendor 目录允许不同的代码库拥有它自己的依赖包,并且不同于其他代码库的版本,这就很好的做到了工程的隔离。
每个项目都有各自的 vendor,每个 vendor 可以存放不同版本的依赖包。

module 机制

在 go1.11 版本中,新增了 module 管理模块功能,用来管理依赖包。要知道,在这个之前,想要对 go 语言包进行管理,只能依赖第三方库实现,比如 Vendor,GoVendor,GoDep,Dep,Glide 等等,对于初学者来说,真的是选择困难症。

开启 module 特性

要开始使用 go module 的特性, 需要先设置 GO111MODULE 环境变量。
开启 GO111MODULE。
要使用 go module,首先要设置 GO111MODULE=on,这没什么可说的,如果没设置,执行命令的时候会有提示,这个大家应该都了解了

在$GOAPTH/src 中创建项目

在$GOPATH/src 目录下创建 github.com/cnwyt/mytest 目录,mytest 为项目目录

1
mkdir -p $GOPATH/src/github.com/cnwyt/mytest
1
2
3
$ export GO111MODULE=on
$ go mod init github.com/cnwyt/mytest
go: creating new go.mod: module github.com/cnwyt/mytest

当然这个 go 模块可以创建在任意位置,不强制邀请放在 GOPATH 路径下。

在$GOPATH/src 外也可以创建项目

在 GOPATH 以外的模块,创建一个 helloworld 目录,用来调用刚刚创建的 mytest 模块。

1
2
$ mkdir helloworld && cd helloworld
$ vi main.go

创建一个 main.go 文件:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"
//import "github.com/cnwyt/mytest"

func main() {
fmt.Println("Hello, World!");

//mytest.ShowTest1()
}

初始化该模块,引入 github.com/cnwyt/mytest 模块,指定版本为 latest:

1
2
3
$ go mod init helloworld
$ go mod edit -require github.com/cnwyt/mytest@latest

初始化后,会生成一个 go.mod 文件,类似 npm 里的 package.json 或者 composer 的 composer.json 的一个文件。

1
2
3
4
module helloworld

require github.com/cnwyt/mytest v0.0.0

这样直接执行 go test 或者 go run main.go 会报错:

1
2
3
$ go test
build helloworld: cannot find module for path github.com/cnwyt/mytest

这是为啥呢? 这是因为我们虽然创建了一个名为 github.com/cnwyt/mytest 模块,在 GOPATH 路径里也有这个模块。但是,GO 模块去 Github 去找这个模块,而不是在 GOPATH 路径里去找,所以找不到。

那该怎么办呢?
有两个解决办法:
第一个办法,很简单,就是直接将 cnwyt/mytest 模块推送的 GitHub 上。
但是,如果我要修改 cnwyt/mytest 里的代码,都得先推送到 GitHub 上,才能生效,实在太麻烦了。
那就直接使用第二个办法, 使用 go replace:

直接修改 go.mod,新增一行 replace:

1
2
3
4
5
6
module helloworld

require github.com/cnwyt/mytest v0.0.0

replace github.com/cnwyt/mytest => /Users/wangtom/goworkspace/mytest

注意版本号必须填写,可以填 v0.0.0 或者 latest.

调用第三方模块

比如项目中会用到比较流行的路由模块 gorilla/mux:

直接修改 go.mod,新增一行 require,不指定版本可以直接写 latest 获取最新版本:

1
require github.com/gorilla/mux latest

运行 go build 或 go test 会自动从 GitHub 下载模块,并会修改 go.mod 文件。

比如运行后会把 latest 直接修改成目前最新的版本 v1.6.2 :

1
2
3
4
5
6
7
module helloworld

require github.com/cnwyt/mytest v0.0.0
require github.com/gorilla/mux v1.6.2

replace github.com/cnwyt/mytest => /Users/wangtom/goworkspace/godict

可以看到模块 gorilla/mux 代码会下载到 $GOPATH/pkg/mod/ 模块下:

1
2
3
4
$ ll /Users/wangtom/goworkspace/pkg/mod/github.com/gorilla
total 0
dr-xr-xr-x 22 wangtom staff 704B 12 24 22:14 mux@v1.6.2

不使用 vendor 和 module 机制时,可以手动下载所有依赖

在不使用 vendor 和 module 的情况下,可以使用较原始的方式,将代码中依赖的内容全部下载并编译在$GOPATH 路径下。
在项目目录下,执行如下命令:
go get -d -v ./…

Git 快速入门

参考此链接https://www.liaoxuefeng.com/wiki/896043488029600 或者 https://www.yiibai.com/git/git_rebase.html

Git 区域的划分与提交回滚

区域划分

分为工作区,本地库(暂存区和分支),远程库。
工作区:分为原文件和已经修改但是未暂存的文件。
暂存区:存放已经 git add 添加后的文件,此区位于本地库。
分支:存放已经 git commit 提交后的文件,此区位于本地库
远程库:存放已经 git push 后的文件。

  1. 修改本地已被跟踪文件,文件进入未暂存区域。

  2. 未暂存区域转到暂存区域 git add files

  3. 暂存区提交到本地仓库 git commit -m

  4. 直接从未暂存区提交到本地仓库
    git commit -am
    经测试,对已跟踪的文件可以正确执行,而对于未跟踪文件(即新增文件)则会出错

  5. 本地库回退到暂存区
    git reset –soft hash 值
    git reset –soft origin/master
    一般回退到暂存区的文件作排查用,不要直接修改,不然会同时出现在暂存区和未暂存区(其实即使修改了也木有太大关系)

  6. 本地库回退到未暂存区
    git reset –mixed hash 值
    git reset –mixed origin/master
    一般回退到未暂存状态就是为了进一步的修改

  7. 本地库回退到文件初始状态(即此版本的)
    git reset –hard hash 值
    注意这里,通常先执行一次 fetch,保证本地版本是 origin 的最新版本,然后再回退。(最厉害的是,这么操作不会有冲突,直接让文件变成和 origin 保持一致)
    git fetch origin
    git reset –hard origin/master
    特别注意:这么操作会使你对文件的修改全部消失,还原成最初状态。
    (针对上一条情况衍生讲解)通常在推送到 origin 时,先要 pull,然后再推送,一般是修改提交了的文件和 pull 下来的同一个文件产生冲突(所以建议修改代码前,一定先要 pull)
    git pull
    git push origin master

  8. 暂存区回退到未暂存区
    git reset – files
    git rest
    撤销所有暂存区的文件

  9. 未暂存区回退到文件初始状态
    git checkout – files

  10. 暂存区回退到文件初始状态
    git checkout head – files

换一个角度看

  • 从工作区一直到远程仓库的过程是:git add files,git commit, git push.
  • 从远程仓库到工作区的过程是:git fetch/git clone/git pull,git reset – files, git checkout – files。
    files 是指文件名。

git reset

  • git reset –soft,重置位置的同时,保留 working Tree 工作目录和 index 暂存区的内容,只让 repository 中的内容和 reset 目标节点(reset 目标节点是指 reset 命令执行后,head 所指向的节点,或者说 commit 记录)保持一致,因此原节点和 reset 节点之间的【差异变更集】会放入 index 暂存区中(Staged files)。所以效果看起来就是工作目录的内容不变,暂存区原有的内容也不变,只是原节点和 Reset 节点之间的所有差异都会放到暂存区中。
  • git reset –mixed,重置位置的同时,只保留 Working Tree 工作目录的內容,但会将 Index 暂存区 和 Repository 中的內容更改和 reset 目标节点一致,因此原节点和 Reset 节点之间的【差异变更集】会放入 Working Tree 工作目录中。所以效果看起来就是原节点和 Reset 节点之间的所有差异都会放到工作目录中。
  • git reset –hard,重置位置的同时,直接将 working Tree 工作目录、 index 暂存区及 repository 都重置成目标 Reset 节点的內容,所以效果看起来等同于清空暂存区和工作区。可以使用 **git reset –hard HEAD^ 来回退到上一次 commit 的状态。
    此命令可以用来回退到任意版本:git reset –hard commitid **
  • git reset,用来从暂存区撤销上一次 git add 添加的修改内容。可以使用 git reset HEAD filepathname (比如: git reset HEAD readme.md)来放弃指定文件的缓存,放弃所有的缓存可以使用 git reset HEAD . 命令。不影响工作区中的内容。
    git reset 详细解释可以参看,https://www.jianshu.com/p/c2ec5f06cf1a

git checkout

这要从 git 的分支说起,git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。git 是如何知道你当前在哪个分支上工作的呢?
其实答案也很简单,它保存着一个名为 HEAD 的特别指针。在 git 中,它是一个指向你正在工作中的本地分支的指针,可以将 HEAD 想象为当前分支的别名。

git diff

git 的四个工作区

git diff:是查看 workspace(工作区) 与 index(暂存区) 的差别的。
git diff –cached:是查看 index(暂存区) 与 local repositorty(本地仓库) 的差别的。
git diff HEAD:是查看 workspace 和 local repository 的差别的。(HEAD 指向的是 local repository 中最新提交的版本)

注:git diff 后跟两个参数,如果只写一个参数,表示默认跟 workspace 中的代码作比较。git diff 显示的结果为 第二个参数所指的代码在第一个参数所指代码基础上的修改。如,git diff HEAD 表示 workspace 在 最新 commit 的基础上所做的修改

比较工作区与暂存区

比较本地库的当前版本和上一次版本

git pull 时冲突了怎么办

  1. 忽略本地修改,强制拉取远程到本地
1
2
3
4
5
git fetch --all

git reset --hard origin/dev

git pull
  1. 未 commit 先 pull,视本地修改量选择 revert 或 stash

    • 本地修改量小。-> revert(把自己的代码取消) -> 重新pull -> 在最新代码上修改 -> [pull确认最新] -> commit&push
    • 本地修改量大。-> stash save(把自己的代码隐藏存起来) -> 重新pull -> stash pop(把存起来的隐藏的代码取回来 ) -> 代码文件会显示冲突 -> 右键选择resolve conflict -> 打开文件解决冲突 ->commit&push
      git stash 能够将所有未提交的修改(工作区和暂存区)保存至堆栈中,用于后续恢复当前工作目录。
  2. 已 commit 未 push,视本地修改量选择 reset 或直接 merge

    • 修改量小。-> reset(回退到未修改之前,选hard模式,把自己的更改取消) -> 重新pull -> 在最新代码上修改 -> [pull确认最新] -> commit&push
    • 修改量大。-> commit后pull显示冲突 -> 手动merge解决冲突 -> 重新commit -> push

git revert 和 reset 的区别

git revert 和 git reset 的区别

  • git revert 是用一次新的 commit 来回滚之前的 commit,此次提交之前的 commit 都会被保留;
  • git reset 是回到某次提交,提交及之前的 commit 都会被保留,但是此 commit id 之后的修改都会被删除

spi 使用

首先,通过一张图来看,完成 spi 的实现,需要哪些操作,需要遵循哪些规范?

1.代码编写

既然是 spi,那么就必须先定义好接口。其次,就是定义好接口的实现类。

2.创建一个文件夹

在项目的\src\main\resources\下创建\META-INF
\services 目录(笔者在网上找了很多文章,很多都没有告知具体这个文件夹放在哪,放在其他位置下无法加载得到)

3.文件夹下增加配置文件

在上面 META-INF
\services 的目录下再增加一个配置文件,这个文件必须以接口的全限定类名保持一致,例如:com.jiaboyan.test.HelloService

4.配置文件增加描述

上面介绍 spi 时说道,除了代码上的接口实现之外,你还需要把该实现的描述提供给 JDK。那么,此步骤就是在配置文件中撰写接口实现描述。很简单,就是在配置文件中写入具体实现类的全限定类名,如有多个便换行写入。

5.使用 JDK 来载入

编写 main()方法,输出测试接口。使用 JDK 提供的 ServiceLoader.load()来加载配置文件中的描述信息,完成类加载操作。

接口定义:

1
2
3
4
public interface HelloService {

void hello();
}

接口实现:

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

@Override
public void hello() {
System.out.println("hello jiaboyan");
}
}

public class HelloService2Impl implements HelloService {
@Override
public void hello() {
System.out.println("hello world");
}
}

添加 JDK 描述,在 META-INF\services 目录下:

1
2
com.jiaboyan.test.impl.HelloService1Impl
com.jiaboyan.test.impl.HelloService2Impl

编写 main()方法:

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

public static void main(String[] agrs) {
ServiceLoader<HelloService> loaders = ServiceLoader.load(HelloService.class);
for (HelloService helloService : loaders) {
helloService.hello();
}
}
}

SPI-机制

SPI 全称为 Service Provider Interface,是 JDK 内置的一种服务提供发现机制。简单来说,它就是一种动态替换发现机制。例如:有个接口想在运行时才发现具体的实现类,那么你只需要在程序运行前添加一个实现即可,并把新加的实现描述给 JDK 即可。此外,在程序的运行过程中,也可以随时对该描述进行修改,完成具体实现的替换。

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口是由 Java 核心库来提供,而 SPI 的实现则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)中。例如:JDBC 的实现 mysql 就是通过 maven 被依赖进来。

那么问题来了,SPI 的接口是 Java 核心库的一部分,是由引导类加载器(Bootstrap Classloader)来加载的。SPI 的实现类是由系统类加载器(System ClassLoader)来加载的。

引导类加载器在加载时是无法找到 SPI 的实现类的,因为双亲委派模型中规定,引导类加载器 BootstrapClassloader 无法委派系统类加载器 AppClassLoader 来加载。这时候,该如何解决此问题?

线程上下文类加载由此诞生,它的出现也破坏了类加载器的双亲委派模型,使得程序可以进行逆向类加载

线程上下文类加载器

通过名字可知,线程上下文类加载,就是当前线程所拥有的类加载器,可通过 Thread.currentThread()获取当前线程。

线程上下文类加载器(Thread Context ClassLoader)可以通过 java.lang.Thread 类的 setContextClassLoader()方法设置,创建线程时候未指定的话,则默认从父线程中继承。

那父线程中也没指定呢?那么会默认为应用程序的类加载器。例如:main 方法的线程上下文类加载器就是 sun.misc.Launcher$AppClassLoader。

前两篇文章中,我们讲解了类加载器的双亲委派模型,该模型的实现是通过类加载器中的 parent 属性(父加载器)来完成的,默认统一交给最上层类加载器去尝试加载。

那,这个线程上下文类加载器又是干啥的?

在介绍线程上下文类加载前,我们先了解下 Java 的 SPI 机制。

线程上下文类加载实现

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

public static void main(String[] agrs) throws ClassNotFoundException {
ClassLoader loader = JVMTest6.class.getClassLoader();
System.out.println(loader); //默认是应用类加载器

//此时获得上下文类加载器:
ClassLoader loader2 = Thread.currentThread().getContextClassLoader();
System.out.println(loader2);//默认也是应用类加载器

//设置为自定义类加载器:
Thread.currentThread().setContextClassLoader(
new ClassLoaderTest("d:/"));
System.out.println(Thread.currentThread().getContextClassLoader());

//使用自定义类加载器加载:
Class c = Thread.currentThread().getContextClassLoader().loadClass("HelloWorld");
System.out.println(c.getClassLoader());//线程上下文类加载器

ClassLoader loader3 = String.class.getClassLoader();
System.out.println(loader3);//启动类加载器 = null
}
}

测试结果

1
2
3
4
5
sun.misc.Launcher$AppClassLoader@41dee0d7
sun.misc.Launcher$AppClassLoader@41dee0d7
ClassLoaderTest@516a4aef
ClassLoaderTest@516a4aef
null

spi 具体实现:

在下面代码中,通过 SPI 方式来完成 java.sql.Driver 接口实现类的类加载操作。

java.sql.DriverManager 包中:

1
2
3
4
5
6
7
8
9
10
11
12
13
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//通过SPI方式,读取META-INF/services下文件中的类名:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {}
return null;
}
});

获取到 ServiceLoader 对象后,进行遍历操作,遍历出所有 META-INF/services 文件夹下的实现类名称,之后再进行 Class.forName(“”)类加载操作。类加载操作在 driversIterator.next()中完成。

java.util.ServiceLoader 包中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取线程上下文类加载器:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//生成ServiceLoader对象:
return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader){
return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}

在获取 ServiceLoader 对象时,获取了此时线程上下文中的类加载器,将此类加载赋值给 ServiceLoader 类中的 loader 成员变量。在后续类加载过程中,都是使用的此类加载来完成。这一步的操作,直接打破了双亲委派模型,实现了逆向类加载。

1
2
3
4
5
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {}

通过 debug 发现,driversIterator.next()方法内部会调用 Class c = Class.forName(cn, false, loader)方法进行类加载操作。而此时传递的 loader 就是之前获取的线程上下文类加载器,传递的 cn 就是 META-INF/services 文件中的具体实现类。

由于笔者是通过本地的 test 进行测试,所以上文中涉及到的类加载器都是 AppClassLoader 系统类加载器。

作者:贾博岩
链接:https://www.jianshu.com/p/e4262536000d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 模块结构
    sharding-core-merge的主要功能是利用属性DatabaseType databaseType;SQLRouteResult routeResult;(route产生),SelectSQLStatementContext selectSQLStatementContext;(preprocess产生),List<QueryResult> queryResults;(execute模块产生),产生MergedResult。按功能分类可以分成三个部分:
  1. org.apache.shardingsphere.core.merge.dal。包含若干DAL操作相关的MergedResult实现类和DALMergeEngine是一个合并引擎。此引擎对外提供merge()功能,负责将DAL操作的相关结果合并,返回包内定义的MergedResult实体类。
  2. org.apache.shardingsphere.core.merge.dql。包含若干DQL操作相关的MergedResult实现类和DQLMergeEngine是一个合并引擎。此引擎对外提供merge()功能,负责将DQL操作的相关结果合并,返回包内定义的MergedResult实体类。
  3. org.apache.shardingsphere.core.merge。包含MergedResult和MergeEngine接口的定义。MergeEngineFactory,负责创建MergeEngine实例。TransparentMergeEngine是一个合并引擎,实现了MergeEngine接口,此方法不对queryResults进行处理,直接返回迭代流式MergeResult。
  • 模块结构图
  • 主要逻辑流程和关键类分析
    分片执行的主要逻辑是从***MergeEngine实现类开始的。这里以DQLMergeEngine为例子,进行分析。首先判断List<QueryResult> queryResults;是否为1,如果为1则不需要处理,直接创建迭代流式归并MergedResult。否则,判断是否为orderBy或groupby,并创建相关MergedResult。最后,对分页进行处理,也就是在decorate()方法中,根据数据库类型,创建不同MergeResult。
  • 设计模式和设计原则
  1. 简单工厂方法
    MergeEngineFactory.newInstance()方法,根据SQLStatement类型来创建不同的MergeEngine。具体的MergeEngine类型为:DQLMergeEngine,DALMergeEngine,TransparentMergeEngine。MergeEngineFactory的存在方便了不同类型的MergeEngine创建,调用者不再需要关心如何创建MergeEngine,而只需要把这件事交给MergeEngineFactory就行。
  2. 适配器模式
    LocalMergedResultAdapter类是一个抽象类,它实现了MergedResult接口的部分方法,使得它的子类ShowDatabasesMergedResultShowShardingCTLMergedResult无需MergedResult接口的所有方法。当实现类和接口的方法无法完全匹配时,可以考虑使用适配器模式。

  • 模块结构
    sharding-sql-test的主要功能是对sql测试用例xml文件进行加载,生成测试用例封装在SQLCases对象中,JAXB 实现java对象与xml之间互相转换
    。包含三个部分:
  1. org.apache.shardingsphere.test.sql.loader。对外提供了SQL Cases的三种注册中心,里面含有SQLCasesLoader属性来读取配置文件。
  2. org.apache.shardingsphere.test.sql.SQLCase。对应与xml里部分的实体类,包含属性:id,value,databaseTypes和sqlType。
  3. org.apache.shardingsphere.test.sql.SQLCases。对应与xml里部分的实体类,包含属性:databaseTypes和sqlCases。
  4. org.apache.shardingsphere.test.sql.SQLCaseType。定义两种SQL类型,Literal是指值已经写在sql语句中,Placeholder是指sql语句使用?来做parameter的占位符。
  • 模块结构图

  • 设计模式和设计原则

  1. 单例模式
    EncryptSQLCasesRegistry,ShardingSQLCasesRegistry,ShardingUnsupportedSQLCasesRegistry这三个都采用了单例模式。内部只有一个属性private SQLCasesLoader sqlCasesLoader;用于加载测试用例文件。
    保证一个类仅有一个实例,并提供一个访问它的全局访问点。避免一个全局使用的类频繁地创建与销毁,此外单个实例还节省了内存资源。

  • 模块结构
    sharding-core-entry的主要功能是对sql进行分片得到SQLRouteResult,包含三个类:
  1. BaseShardingEngine。是抽象类,对外提供SQLRouteResult shard(final String sql, final List parameters)方法。此方法是模板方法,可以将一些子算法的实现,延迟到实现类中。cloneParameters()和route()的实现就是在子类完成的。
  2. PreparedQueryShardingEngine。它是BaseShardingEngine的实现类,实现了cloneParameters()和route()的实现就是在子类完成的。含有属性PreparedStatementRoutingEngine routingEngine;,它在route()中发挥了作用。
  3. SimpleQueryShardingEngine。它是BaseShardingEngine的实现类,实现了cloneParameters()和route()的实现就是在子类完成的。含有属性StatementRoutingEngine routingEngine;,它在route()中发挥了作用。
  • 模块结构图

  • 设计模式和设计原则

  1. 模板方法
    BaseShardingEngine类的SQLRouteResult shard(final String sql, final List parameters)方法使用了模板方法模式,父类BaseShardingEngine定义了整个算法的骨架,让子类去实现具体的细节。route()和cloneParameters()就是留给子类自定义的细节部分,使得子类在不变更整体算法的情况下,就可以重新定义该算法的特定步骤。

  • 模块结构
    sharding-core-execute的主要功能是将路由和改写生成的RoutingUnit对象(RoutingUnit内部含有SQLUnit),产生执行QueryResult。按功能分类可以分成四个部分:
  1. org.apache.shardingsphere.core.execute.hook。在执行的关键操作过程中,发送事件,用于跟踪和事务。
  2. org.apache.shardingsphere.core.execute.metadata。主要提供了加载获取TableMetaData的功能,主要存储表的元数据信息,如表中的列名,数据类,列是否为主键。
  3. org.apache.shardingsphere.core.execute.sql。主要有两个子包:prepare和execute。prepare的作用是在执行前搜集信息,产生ShardingExecuteGroup的集合。
    execute的作用是将ShardingExecuteGroup中StatementExecuteUnit发送到数据库进行执行。
  4. org.apache.shardingsphere.core.execute。 包含ShardingExecuteEngine、SharidngExecuteCallback等重要类,是整个执行模块的核心部分。
  • 模块结构图
  • 主要逻辑流程和关键类分析
    分片执行的主要逻辑是从sharding-jdbc-core的StatementExecutor开始的,真正执行的部分位于sharding-core-execute中,以查询为例,主要逻辑流程图如下。其中,虚线箭头表示从属关系,实线表示执行顺序。
  • 设计模式和设计原则
  1. 模板方法
    SQLExecuteCallback类的execute(final Collection statementExecuteUnits, final boolean isTrunkThread, final Map shardingExecuteDataMap)方法使用了模板方法模式,父类SQLExecuteCallback定义了整个算法的骨架,让子类去实现具体的细节。executeSQL(RouteUnit routeUnit, Statement statement, ConnectionMode connectionMode)就是留给子类自定义的细节部分,使得子类在不变更整体算法的情况下,就可以重新定义该算法的特定步骤。
  2. 依赖倒转原则
    SQLExecutePrepareCallback是一个接口类。BatchPreparedStatementExecutor和PreparedStatementExecutor对与这个类有依赖,由于这个类是个接口,所以调用者可以,在使用前根据实际情况,再定义接口中的抽象方法。这种使用方式对于调用者更加灵活,可以依据场景和需求,自定义相关抽象方法。
  • Java编程技巧
  1. com.google.common.collect.Lists
    SQLExecutePrepareTemplate.getSQLExecuteGroups()方法中使用了Lists,Lists.partition(sqlUnits, desiredPartitionSize);,可以将原本的List切分成多个List。
  2. com.google.common.util.concurrent
    ShardingExecutorService.ShardingExecutorService(final int executorSize, final String nameFormat)方法中,使用了异步回调线程池。同时,设定了JVM关闭时线程池采用的动作。
    在多线程编程中异步回调使得原本需要阻塞等待的异步结果,因为回调的存在而不用等待。
    1
    2
    3
    executorService = MoreExecutors.listeningDecorator(getExecutorService(executorSize, nameFormat));
    MoreExecutors.addDelayedShutdownHook(executorService, 60, TimeUnit.SECONDS);

  • 模块结构
    sharding-core-rewrite的主要功能是将路由生成的RoutingUnit对象,进行改写生成SQLUnit。按功能分类可以分成六个部分:
  1. org.apache.shardingsphere.core.rewrite.SQLRewriteEngine。SQLRewriteEngine类是整个改写功能的入口,通过构造方法和generateSQL()方法,对外提供改写功能,生成SQLUnit。SQLUnit包含两个属性:sql和parameters,作为改写后的结果返回。这两个属性分别依赖org.apache.shardingsphere.core.rewrite.sql和org.apache.shardingsphere.core.rewrite.parameter包。
  2. org.apache.shardingsphere.core.rewrite.sql。主要类是SQLBuilder,它对外提供构建改写SQL的功能。这个模块内部的SQLToken是改写时重要的依赖。
  3. org.apache.shardingsphere.core.rewrite.parameter。包含ParameterBuilder和ParameterRewriter,分别抽象了参数列表构建和参数改写。其中org.apache.shardingsphere.core.rewrite.parameter.rewriter子包的功能较重要的类是:EncryptParameterBuilderFactory和ShardingParameterBuilderFactory。它俩通过build方法来完成对parameterBuilder的相关参数的设置,例如:在StandardParameterBuilder中设置addedIndexAndParameters和replacedIndexAndParameters。
  4. org.apache.shardingsphere.core.rewrite.SQLRewriteDecorator。接口类。定义了decorate()方法,为sqlRewriteEngine的parameterBuilder设置相关的parameter参数。它的实现类为ShardingRewriterDecorator和EncryptRewriterDecorator。
  5. org.apache.shardingsphere.core.rewrite.sharding。ShardingRewriterDecorator为主要功能类。ShardingParameterBuilderFactory.build()方法,为sqlRewriteEngine的parameterBuilder设置相关的parameter参数
  6. org.apache.shardingsphere.core.rewrite.encrypt。EncryptRewriterDecorator为主要功能类。EncryptParameterBuilderFactory.build()方法,为sqlRewriteEngine的parameterBuilder设置相关的parameter参数。
  • 模块结构图
  • 主要逻辑流程和关键类分析
    sharding-core-rewrite的主要逻辑是从SQLRewriteEngine.generateSQLgenerateSQL(final RoutingUnit routingUnit, final Map<String, String> logicAndActualTables)开始的,主逻辑流程图如下。其中,虚线箭头表示从属关系,实线表示执行顺序。
  • 设计模式和设计原则
  1. 接口分离原则和里式替换原则
    org.apache.shardingsphere.core.rewrite.sql.token.pojo。这个包对面向对象编程有比较好的实践。首先,定义了SQLToken抽象类,所有实现类都继承它,它将实体类的公共的部分抽象封装了起来。其次,定义了三个接口
    Alterable、Attachable、Substitutable三个接口,这三个接口从使用角度按照改写、附加、直接替换三个方面进行了划分,实现类根据自身特点来实现其中的接口。最后,调用者在调用时使用抽象类的引用,达到了统一处理的目的,同时可以根据实现类所属的接口进行差异化操作,这点在如下代码中有体现:
    SQLBuilder的代码如下:

    1
    2
    3
    private String getSQLTokenLiterals(final SQLToken sqlToken, final RoutingUnit routingUnit, final Map<String, String> logicAndActualTables) {
    return sqlToken instanceof Alterable ? ((Alterable) sqlToken).toString(routingUnit, logicAndActualTables) : sqlToken.toString();
    }

    对父类的继承,使得调用者代码可以使用统一的类来引用不同的子类对象。
    接口的实现,使得调用者代码可以对不同接口的实现类对象,进行差异化处理。
    org.apache.shardingsphere.core.rewrite.sql.token.generator包对接口合理的使用,也达到类似的效果。同样也是优秀的面向对象编程实践。

  2. 依赖倒转原则
    org.apache.shardingsphere.core.rewrite.parameter.builder和org.apache.shardingsphere.core.rewrite.parameter.rewriter体现了依赖倒转原则,ParameterBuilder和ParameterRewriter都是接口类,是对内部多个实现类的良好抽象,调用者只需要声明接口引用,就可以使用多种实现类。