ReentrantLock重入锁

​ 使用ReentrantLock可以完成和synchronized同样的功能,但是使用synchronized可以自动释放锁,使用重入锁必须要手动释放锁,所以一般会放在finally块中释放锁。

尝试锁定

​ 使用ReentrantLock可以进行尝试锁定,这样在无法锁定或者在指定时间内无法锁定,线程可以决定是否继续等待。在尝试锁定之后,不管有没有锁定,方法都将继续执行。所以,可以在方法中根据是否锁定来进行相应的逻辑处理。

获取不到锁可被interrupt

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

Lock lock = new ReentrantLock();

void m1 () {
try {
lock.lock();
System.out.println("t1 start");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
System.out.println("t1 end");
} catch (InterruptedException e) {
System.out.println("t1 interrupted");
} finally {
lock.unlock();
}
}

void m2() {
boolean isLock = false;
try {
// lock.lock();
lock.lockInterruptibly();
isLock = lock.tryLock();
System.out.println("t2 start");
TimeUnit.SECONDS.sleep(5);
System.out.println("t2 end");
} catch (Exception e) {
System.out.println("t2 interrupted");
} finally {
if (isLock) {
lock.unlock();
}

}
}

public static void main(String[] args) {
Lock02 lock01 = new Lock02();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lock01.m1();
}
});
t1.start();

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lock01.m2();
}
});

t2.start();

try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();
}
/**
控制台输出:
t1 start
t2 interrupted
*/

}

​ 线程t1在Integer.MAX_VALUE的时间内会一直执行,正常情况下t2是获取不到锁的,这个时候如果不做处理,那么t2将会一直等待下去,不会被其他线程打断。但是如果在t2中调用了lock.lockInterruptibly();方法之后,则表明t2是可以被打断的。也就是说,t2在等待t1释放锁的过程中,如果不想继续等了,主线程调用t2.interrupt()方法即可将t2线程打断。

指定为公平锁

​ 假如现在有6个线程,其中一个线程拿到了锁顺利执行完逻辑后释放了锁,这时候剩下的5个线程都会去争用这把锁,谁先拿到就是谁的,谁就可以执行,并不会关心其他的线程等了多长的时间,也就是说不会根据谁等的时间长,谁就优先获得锁。这种称为竞争锁,不是公平锁。这种的效率会高些,因为线程调度器不用计算哪个线程等待的时间更长。synchronized默认是非公平锁。

​ 公平锁自然就是根据等待时间来判断谁可以先获取到锁的。new ReentrantLock(true)即可声明为公平锁。

​ 项目崩溃,Web页面进不去,用top命令查看发现java和mysql占用的CPU异常高,服务器配置的16G内存,也只剩几百M了。

​ 排查过程:

  1. 查看日志,各应用日志没有发现明显的异常,但是在Tomcat的日志中却疯狂出现OutOfMemoryError异常。
  2. 抓取dump文件进行分析,发现一个接收mq的处理类里面用了ArrayBlockingQueue,每一次mq消息过来之后,并不是马上就处理,而是先添加到BlockingQueue里面,之后再从BlockingQueue中取出消息进行处理。问题也就出现在这个Queue里面,当时设的这个queue的最大值为100000。出问题时,队列里还有十多万的数据堆积,也就是说queue缓存队列已经塞满了,查看dump文件,也确实这个BlockingQueue就占用了600多m的内存,而且不仅这一个队列,还有另外一个缓存队列也占用了近300M的内存大小。
  3. 发现这个问题之后,为了让现场可以继续使用,马上将BlockingQueue的大小改小了,重新打包扔到线上。
  4. 出现这个问题的原因也有因为消费者是单线程消费而且每次只从BlockingQueue中poll出一条数据进行处理的原因,后续改成了多线程消费,且每次从队列中取出20条数据进行处理。这样Java的内存消耗和CPU占用也就降下来了。
  5. 但是MySQL的占用依然很高,使用mysql -h 127.0.0.1 -u mysql -p 登录到数据库之后,使用show full processlist;发现有一条SQL的执行效率比较低,需要2S多的时间,而且这条SQL还在大量的使用。
  6. 查看对应的表发现这个表没有建索引,根据条件字段建立索引之后,MySQL的CPU占用率立马降下来了。这样,线上的应用也就恢复了正常
  7. 这样跑了两天之后,突然又崩溃了。但是这次Java和MySQL的内存和CPU占用都不高,这就很奇怪了。
  8. 刚开始查看了日志,发现RabbitMq连接不上,日志中一直在报RabbitMq Connect time out的错误。但是大佬说这应该不会影响整个应用,导致web页面进不去(页面报read time out异常),于是就就继续找原因。开始抓取线上的堆栈信息进行分析。
  9. 之后,还发现一个现象,所有通过http方式访问Tomcat中的应用的地方都不行了。
  10. 最后发现,应用对外提供了一个http接口,在这个接口中会发送RabbitMq消息给其他系统,而这时的RabbitMq是连不上的,但是,每一次请求过来都会去新建一个Cononection和Channel,这样就会导致在调用这个接口的时候需要等到RabbitMq连接超时了之后,才会断开。而RabbitMq的超时时间默认是1分钟,而在这一分钟内,这个接口被大量的调用,导致大量的http连接资源被占用得不到及时的释放。
  11. 查看Tomcat Server.xml中的配置,了解到配置的MaxThreads为150,MaxAccept为300。也就是说最多支持的http并发连接为450
  12. 在项目中,由于设备采集的人脸、车辆抓拍的信息相当多,再加上RabbitMq连接不上,也就出现了这个问题。
  13. 最后,当然是配置了正确的RabbitMq地址之后,解决了问题。

synchronized的作用

互斥锁,对某个对象加锁,这里所说的并不是锁住栈中的一个引用,而是锁定的是堆中的对象,这一点很重要,如果是锁定的是对象的引用,那么引用可能在某个时刻之后指向的是其他的对象,这就有问题了。

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

private int count = 10;

private static int staticCount = 10;

public void printCount() {
synchronized (this) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}

public synchronized static void printCount2() {
staticCount--;
}

}

​ printCount( )也可以直接在方法上加synchronized,因为该方法中的所有操作步骤都是在synchronized中的。printCount锁定的是当前对象,而printCount2( ) 锁定的是Synchronized01的Class对象,这两者是不一样的,当调用静态方法时是不需要创建对象的,那么自然也就没有对当前对象 this 上锁这么一说了。

同步方法和非同步方法是否可以同时调用?

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 T{

public synchronized void m1() {
System.out.println("method m1() start");
try {
Thread.sleep(10000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method m1() end");
}

public void m2() {
System.out.println("method m2() start");
}

public static void main(String[] args) {
T t = new T();
new Thread(() -> t.m1()).start();
new Thread(t::m2).start();
}

}

​ 如上的代码中,在m1( )方法被调用的时候,m2( ) 是否会执行?答案是肯定的,因为m2( )没有加锁,自然也就不需要等待m1( )执行完释放锁之后再执行了。这也说明了,在对业务写方法加锁,而对业务读方法没有加锁时,容易产生脏读的问题,因为在写过程中,状态的值还没有修改完,就已经被另外的线程读取了,而另外的线程读取到的并不是完全修改之后的值。

一个同步方法可以调用另外一个同步方法

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

synchronized void print1() {
System.out.println("method print1 ");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
print2();

}

synchronized void print2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method print2");
}

public static void main(String[] args) {
Synchronized02 synchronized02 = new Synchronized02();
synchronized02.print1();
}
/** console output:
method print1
method print2
*/

}

​ 一个线程已经拥有了某个对象的锁,那么再次申请 的时候仍然可以获得该对象的锁。也就是说synchronized的锁是可重入的。

​ 对象在内存中的布局可以分为三部分:对象头信息、实例数据、对齐填充。

​ 而在对象头信息中又包含了两部分的信息,其中一部分就存储了GC分代年龄、hash码以及锁状态标志等的信息。

​ 在子类的同步中也同样是可以调用父类的同步方法的。

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

public synchronized void m1() {
System.out.println("parent method start");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("parent method end");
}

public static void main(String[] args) {
TT tt = new TT();
tt.m1();
}

}

class TT extends T{
@Override
public synchronized void m1() {
System.out.println("child method start");
super.m1();
System.out.println("child method end");
}
}
/**
console output:
child method start
parent method start
parent method end
child method end
*/

发生异常时,锁会被释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Synchronized03 {

private int count = 0;

synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start");
while (true) {
count++;
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count = " + count);

// 抛出异常,锁释放,其他线程可以争用
if (count == 3) {
int i = 1 / 0;// 抛出异常,锁被释放,如果不想释放,可以在这里catch,然后让循环继续。
}

}
}

public static void main(String[] args) {
Synchronized03 synchronized03 = new Synchronized03();
new Thread(() -> synchronized03.m(), "t1").start();

try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> synchronized03.m(), "t2").start();
}

/**
console output:
t1 start
count = 1
count = 2
count = 3
Exception in thread "t1" java.lang.ArithmeticException: / by zero
t2 start
at com.ethereal.thread.syncdemo.Synchronized03.m(Synchronized03.java:29)
at com.ethereal.thread.syncdemo.Synchronized03.lambda$main$0(Synchronized03.java:37)
at java.lang.Thread.run(Thread.java:748)
count = 4
count = 5
count = 6
*/

}

​ 如果线程t1在执行过程中抛出异常之后,锁没有释放,那么线程t2是永远不会执行的,显然与结果不符。如果想要锁不被释放,那么可以将异常捕获再进行相应的处理。

Volatile关键字

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

private volatile boolean running = true;

void m() {
System.out.println("method m start");
while (running) {
/*try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("adfsadf");*/
}
System.out.println("method m end");
}

public static void main(String[] args) {
Volatile01 t = new Volatile01();
new Thread(t::m, "t1").start();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}

}

​ 如上的代码中,如果running属性没有使用volatile修饰的话,那么整个程序将陷入无限循环中,不会结束,而加了之后,则可以正常结束。这是因为,线程在执行的过程中,会将主内存(包括堆、栈内存等)中的数据读取到自己的缓冲区中,之后再使用的时候是从自己的缓冲区中读取该变量的值,而不是每次都从主内存中重新刷取一遍。所以,在main线程修改了running的值之后,会将修改之后的值刷同步到主内存中。但是其他线程如 t1 由于每次只从自己的缓冲区中读取数据,所以并不知道running的值已经被修改了,程序也就不会停止了。

​ 在使用了volatile修饰running后,当一个线程修改了对象的共享状态的值的时候,会通知其他线程再从主内存中将该变量的值写回到自己的缓冲区中,这样就解决了内存的可见性问题。注意,这里说的是通知其他线程重新获取,而不是每次都从主内存中获取变量的值。

​ 但是,如果在 while(running) 循环中,加入打印语句或者休眠一会,程序是有可能停止的。这是因为,当在执行这些操作的时候,CPU有可能有空闲的时间从主内存中,将自己缓冲区的数据刷新一遍。但这并不一定会发生,所以,该用volatile的时候就要用。它相对于synchronized轻的多的多。

​ 使用了volatile之后,性能会稍微下降一些,因为使用了volatile之后,jvm将不会进行重排序。另外,volatile并不能保证原子性也不具备“互斥性”。

AtomicXXX原子类

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

private AtomicInteger count = new AtomicInteger(0);

void m() {
for (int i = 0; i < 10000; i++) {
if (count.get() < 10) {
System.out.println(Thread.currentThread().getName() + ", count=" + count.get());
count.incrementAndGet();
}

}
}

public static void main(String[] args) {
Atomic01 atomic01 = new Atomic01();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(()-> atomic01.m(), "t" + i));
}
threads.forEach(o -> o.start());
threads.forEach(o -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(atomic01.count.get());
}

/**控制台输出:
t3, count=0
t3, count=1
t3, count=2
t3, count=3
t3, count=4
t0, count=0
t0, count=6
t0, count=7
t0, count=8
t0, count=9
t5, count=0
t2, count=0
t1, count=0
t4, count=0
t3, count=5
15
*/

}

​ 当去除上述代码中方法 m() 中for循环中的条件判断时,最后的打印结果永远是10,因为AtomicInteger的incrementAndGet() 操作是原子性的。但是如果加上条件判断之后,可以看到控制台最后的结果是15。这是因为,当t3线程进行条件判断完之后,执行累加操作之前,此时可能其他线程抢到了CPU的资源,导致 t3 线程进入等待状态,t3的程序计数器记录下该线程执行到哪一个位置,等到其他线程执行完之后,t3重新获得CPU资源,进入运行状态,开始从程序计数器记录的位置继续向下执行(这个时候 if 条件是在之前就已经完成的了,不会在进行一次判断),但是 t3 进行累加操作的时候,此时count的值已经是其他线程修改累加过的值了。所以出现了最终的结果比10大。

synchronized优化

缩小锁定的粒度

​ 1. 尽可能的减少需要加锁的代码块。

​ 2. 锁定一个对象obj,如果obj的属性发生改变,不会影响锁的使用,但是如果obj变成另外一个对象,那么锁定的对象也会发生改变。所以应该避免将锁定的应用变换成其他对象。这也说明了,是对堆中对象的实例加锁,而非对栈中对象的引用加锁

​ 3.不要锁定字符串常量

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

String s1 = "hello";
String s2 = "hello";

void m1() {
synchronized(s1) {

}
}

void m2() {
synchronized (s2) {

}
}

}

​ 如上的代码中,m1和m2分别锁定了s1和s2,这个时候会以为锁定的是两个对象,但其实是同一个对象,s1和s2都指向了常量池中的”hello”。要尽量的避免使用字符串常量的方式上锁,否则可能会造成难以排查的死锁阻塞的现象。

线程安全性

​ 要编写线程安全的代码,核心是对状态访问操作进行管理,尤其是那些可变的、共享的状态的访问,而对象的状态可以理解为存储在状态变量(实例或静态域)中的数据,当然对象的状态还可能会包含其他依赖对象的域。

什么是线程安全性

​ 当多个线程访问一个类时,不管运行时采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要额外的同步和协同,这个类始终都能表现出正确的行为,那么这个类就称作是线程安全的。

​ 无状态对象一定是线程安全的(不包含任何域也不包含任何引用对象的域),比如大多数的Servlet都是无状态的,只有当Servlet在处理一些请求需要保存数据时,才不是线程安全的。

​ 从虚拟机的角度来看,无状态的对象的临时状态都是存储在线程栈中的局部变量中的,只允许当前线程访问。而不会受其他线程的影响。另外,类信息是存储在方法区中的,是共享的。类信息中包含了类的版本、字段、类名、接口、方法等信息。类的字段信息是可变的,线程的不安全性也正是因为对共享的、可变的状态的访问无法保证其正确性而引起的。所以,无状态的对象一定是线程安全的。

原子性

​ 原子性的操作是不可被中断的,即使是多个线程同时执行,也互不干扰。

竞态条件

​ 当某个结果的正确性取决于多个线程的交替时序的时候,那么就会发生竞态条件。最常见的竞态类型就是“先检查再运行”,即通过一个可能失效的结果来决定下一步的动作。这个失效的结果可能给你带来未预期的异常,数据被覆盖的。

​ “先检查后执行”的最常见的一种情况就是延迟初始化,所谓延迟初始化,就是将对象的初始化延迟到使用该对象时才进行,比如单例模式,如下代码:

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

private Singleton singleton = null;

private Singleton() {
}

public Singleton getInstance() {
if (null == singleton) {
singleton = new Singleton();
}
return singleton;
}
}

​ 如上的代码可能会出现多个Singleton的实例。比如线程A在进行条件判断,看到singleton为空,就会去创建实例,而此时线程B进来,也同样需要判断条件,而此时的singleton是否为null,则取决于不可预计的时序,以及线程的调度方式,所以,这两次调用getInstance( ),可能得到的实例并不是同一个实例。这就违法了单例模式了。

​ 另外一种常见的竞态条件,发生在“读取-修改-写入”的操作中,例如递增计数器( count++ )。在使用递增计数器的时候,你必须要先获取先前的值,然后确保在执行更新过程中不会被其他线程修改和读取这个值。

复合操作

​ 这是另外一个常见的竟态条件,包含了一组需要以原子方式执行的操作,也就是当前线程在修改某个变量时,必须通过某种方式来阻止其他线程来使用这个变量,从而确保其他线程只能在修改操作完成之前或之后来读取或修改这个变量。

基本构成

SqlSessionFactoryBuilder(构造器)

​ 根据配置信息来生成SqlSessionFacory(工厂接口)

SqlSessionFactory

​ 是一个接口工厂接口而不是实现类,可以通过SqlSessonFactoryBuilder获得。它的任务是创建SqlSession(会话),可以说是每个Mybatis的应用的核心。

​ 可以通过XML配置方式或者编码方式来创建SqlSessionFactory,强烈建议使用XML配置文件的方式。

​ 通过解析XML配置信息到org.apache.ibatis.session.Confuguration中,这个Configuration类对象将会存在整个Mybatis的生命周期中,另外,Mybatis提供了两个SqlSessionFactory的实现类,一个是DefaultSqlSessionFactory和SqlSessionManger,不过,SqlSessionManger目前还没有使用,Mybatis目前使用的是DefaultSqlSessionFactory

SqlSession

​ 一个既可以发送Sql去执行并返回结果,也可以获取Mapper的接口

​ 是一个接口类,扮演着门面的作用,也就是说,我们只要告诉它我们要什么信息(参数),要做什么(SQL),那么它在一段时间之后就会将结果返回给我们,在这个过程中,我们也只需要关心我们需要什么样的参数和功能,之后会返回什么样的结果给我们就可以了。而中间的处理过程对我们来说是透明的,中间的黑盒操作主要是依靠Executor来实现的。

​ SQLSession有两个实现类:DefaultSqlSession、SQLSessionManger,每次在使用完SQLSession之后需要正确的进行关闭动作,将连接资源交还给数据库,以免导致资源的匮乏而导致其他问题。

​ 用途:

1. 获取映射器,让映射器通过命名空间和方法名找到对应的SQL,发送给数据库执行后返回结果
 2. 在SqlSession层,可以通过update、insert、delete、select等方法,带上SQL的ID来操作在XML中配置好的SQL,从而完成我们的工作

SQL Mapper

​ 由一个接口和XML文件(或注解)组成,需要给出SQL和对应的映射规则,负责发送SQL去执行,并返回结果

​ 作用:

1. 定义参数类型
 2. 描述缓存
 3. 描述SQL语句
 4. 定义SQL返回结果和POJO的映射关系

生命周期

SqlSessionFactoryBuilder

​ 它可以通过XML文件或者java编码来构造SQLSessionFactory,可以构造多个SessionFactory,我们在构造完SqlSessionFactory之后,Builder也就完成了它的使命,我们就可以对其进行回收了,所以它的生命周期只需要在方法局部内就可以了。

SQLSessionFactory

​ 每次程序需要访问数据库的时候,SqlSessionFactory就会创建出SqlSession会话,所以,SqlSessionFactory应该要贯穿于Mybatis的整个生命周期,但是,如果程序中多次创建同一个数据库的SqlSessionFactory,那么每次创建SqlSessionFactory就会打开更多的数据库连接资源,那么数据库连接资源就会很快的被耗尽。所以SqlSessionFactory果断需要配置成单例的,在整个Mybatis的生命周期中,只需要存在同一个数据库的一个SqlSessionFactory实例来管理好数据库的连接就可以了。

SqlSession

​ 是一个会话,相当于JDBC的一个Connection对象,它的生命周期是在请求数据库处理事务的过程中,是一个线程不安全的对象。

Mapper

​ 应该在一个SqlSession事务方法之内,是一个方法级别的东西。最大的范围是和SqlSession范围相同的。尽量在一个SqlSession中使用它

1. XML方式与注解方式对比

  1. 使用Mapper.xml的方式

    优势:和接口分离,便于统一管理; 复杂的语句不会影响接口的可读性,可以带来更灵活的空间

    劣势:存在较多的XML文件

  2. 使用注解的方式

    优势:在接口层就能看到对应的SQL,不需要再去查找xml文件,可读性高,较为方便

    劣势:如果存在较为复杂的联合查询,那么会因为SQL语句过长而影响阅读,导致可读性降低;另外,这种方式的SQL无法进行复用,也就是无法使用xml文件中的include方式。

2. 配置文件

​ mybatis-config.xml中配置的每一个信息都可以在org.apache.ibatis.session.Configuration类中找到对应的属性,其实说白了,在xml中配置这么多的属性,就是为了组装这个Configuration的

属性配置

​ mybatis的配置文件中的值可以在外部配置,可以通过配置properties文件的方式。在mybatis-config.xml中以如${ driver.class }的方式引入。

​ 如果在项目中多处配置了属性的值,优先级如下:

​ 方法参数传递的属性 –> properties resource中指定的URL文件 –> 最后是properties体中指定的属性

​ 切记不要使用混合的方式,否则会使得代码看上去很混乱,首选的方式应该是properties文件的方式

​ 如果系统有对密码、用户名等进行加解密的需求,建议使用配置properties文件,然后通过代码方式来创建SqlSessionFactory的方式。

​ 从mybatis3.4.2开始,可以为占位符设定一个默认值,如:${username: mysql},这里如果username没有配置的话,那么将取mysql作为参数值。如果需要使用这一特性,必须要启用默认值特性:

1
2
3
4
<properties resource="org/mybatis/example/config.properties">
<!-- ... -->
<property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> <!-- 启用默认值特性 -->
</properties>
设置(settings)

​ 是Mybatis最复杂的配置,也是Mybatis最重要的配置之一,它可以改变Mybatis运行时的行为。比如配置proxyFactory指定Mybatis创建具有延时加载能力的对象所使用到的代理工具等。

1
2
3
<settings>
<setting name="cacheEnabled" value="true"></setting>
</settings>
别名(typealias)

​ 可以使用一个简短的名称来代替类的全限定名,这个名称可以在Mybatis的上下文中使用。别名是不区分大小写的,别名是在解析配置文件中生成,并长期保存在Configuration对象中的,在需要的时候取出来即可,这样就可以不用在运行时再创建它的实例了。

​ Mybatis为我们定义好了常用的类型的别名,比如:数值、字符串、日期和集合等,这里展示部分TypeAliasRegistry中的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class);

registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class);

registerAlias("ResultSet", ResultSet.class);

​ 当然也可以自定别名,通过标签:

1
2
3
4
5
6
7
<typeAliases>
<!-- 配置别名 -->
<typeAlias alias="testAlias" type="com.summer.test.TestAlias"></typeAlias>

<!-- 通过包扫描的方式定义别名 -->
<package name="com.summer.domain"/>
</typeAliases>

​ 包扫描的方式可以配合注解的方式使用,如果没有使用Alias注解的话,也是会被扫描的,只不过是将类的首字母小写作为别名

1
2
3
4
@Alias("role")
public class Role {
...
}
typeHandler类型处理器

​ Mybatis在预处理语句中( PreparedStatement ),在设置每一个参数或者从结果中取出一个值时,都会用注册了的typeHandler进行处理。

​ 也分为系统定义和自定义两种,但是一般来说,使用系统已经设置好的就已经可以完成大部分功能

​ 系统定义部分展示:

1
2
3
4
5
6
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
register(JdbcType.CHAR, new StringTypeHandler());
register(JdbcType.VARCHAR, new StringTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
自定义typeHandler
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
package com.epoch.mybatis.study.typehandler;

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
* @ClassName MyStringTypeHandler
* @Description TODO
* @Author cjh
* @Date 2019/5/15 22:55
* @Version 1.0
*/
@MappedTypes({String.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public class MyStringTypeHandler implements TypeHandler<String> {

Logger logger = LoggerFactory.getLogger(MyStringTypeHandler.class);


@Override
public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
logger.info("设置MyStringTypeHandler类型数值...");
ps.setString(i, parameter);
}

@Override
public String getResult(ResultSet rs, String columnName) throws SQLException {
logger.info("获取MyStringTypeHandler类型数值...");
return rs.getString(columnName);
}

@Override
public String getResult(ResultSet rs, int columnIndex) throws SQLException {
logger.info("使用MyStringTypeHandler获取下标字符串");
return rs.getString(columnIndex);
}

@Override
public String getResult(CallableStatement cs, int columnIndex) throws SQLException {
logger.info("使用MyStringTypeHandler CallableStatement获取下标字符串");
return cs.getString(columnIndex);
}
}
枚举类型typeHandler

​ Mybatis为我们提供了两种类型的typeHandler:EnumTypeHandler、EnumOrdinalTypeHandler

​ EnumTypeHandler:使用枚举字符串的名字作为参数传递

​ EnumOrdinalTypeHandler:使用整数下标作为参数传递,是Mybatis默认的枚举类型处理器

​ 在更多的应用场景中,我们希望使用呢我们自己的typeHandler来处理枚举类型。比如我们在定义了Sex枚举类型

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
package com.epoch.mybatis.study.enums;

/**
* @ClassName Sex
* @Description TODO
* @Author cjh
* @Date 2019/5/15 23:47
* @Version 1.0
*/
public enum Sex {

MALE(1, "男"),
FMALE(2, "女");

private Sex(Integer id, String name) {
this.id = id;
this.name = name;
}

private Integer id;

private String name;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public static Sex getSex(int id) {
if (id == 1) {
return MALE;
} else if (id == 2) {
return FMALE;
}
return null;
}

}

之后,可以在Mybatis中添加如下配置:

1
2
3
<typeHandlers>
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.xxx.xxx.Sex" />
</typeHandlers>

然后在resultMap中:

1
<result column="sex" property="sex" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler" />

如果系统提供的两个枚举类型处理器都不能满足我们的要求,我们可以通过实现TypeHandler接口来自定义自己的枚举类型转换器

ObjectFactory

​ 当Mybatis在构建一个结果返回的时候,都会使用到ObjectFactory(对象工厂)来创建一个pojo。Mybatis有自己的默认的对象工厂的实现类DefaultObjectFactory,如果我们想要定义我们自己的对象工厂,那么可以通过继承DefaultObjectFactory或者实现ObjectFactory接口来实现。然后进行如下配置:

1
2
3
<objectFactory type="com.summer.mybatis.MyObjectFactory">
<property name="name" value="MyObjectFactory" />
</objectFactory>

RTTI(Run-Time Type Identification运行时类型识别)

​ 运行时类型信息让我们可以在程序运行的过程中发现和使用类型信息

​ 含义:在运行时,识别一个对象的类型(所有的类型转换都是在运行时进行正确性检查的)

两种方式识别对象和类的信息
  1. 传统的RTTI
  2. 反射机制
为什么需要RTTI

​ 先来了解一下多态的概念

多态

​ 面向对象编程基本的目的是:让代码只操作对基类的引用,这样如果要添加一个新类来扩展程序就不会影响到原来的代码。所以即使是通过泛化的对象引用来调用,也能产生正确的行为。这就是多态。典型的例子:Shape基类有多个子类Circle、Square等

1
2
3
4
List<Shape> shapeList = Arrays.asList(new Circle(), new Square());
for(Shape shape: shapeList) {
shape.draw();
}

​ 在上面的代码中,Circle、Square会发生向上转型,在转型的过程中也失去了具体类型信息。对于shapeList来说,他们只是Shape对象。

​ 而当从容器中取出对象的时候,实际上他们都会被当做Object持有,只不过会自动的将结果转型Shape,这里的RTTI类型转换并不彻底,Object只转型成了Shape但是没有转型为其子类,因为在List中,我们只知道保存的都是Shape,在编译时通过java的容器和泛型来强制确保这一点,而在运行时由类型转换来确保这一点。

​ 而接下去的事情就要交给多态了

class对象

​ Class对象给出了类型信息在运行时是如何表示的,它包含了与类的有关信息,用来创建类的所有“常规”对象,被保存在与类名同名的一个.class文件中。Java使用Class对象来执行其RTTI

​ 所有的类都是在对其第一次引用时,动态的加载到JVM中的

​ 所以,Java程序在它开始运行之前并非完全被加载,而是在需要时动态的加载到JVM中的,而一旦一个类的Class对象被加载到内存中之后,它就会被用来创建这个类的所有对象

获取Class对象

获取Class对象的几种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. Class.forName("XXX");
/*
a. 使用这种方式获取Class对象会导致该类被加载,前提是该类还没有被加载的情况下,使用这种方式的好处是可以不用通过对象实例来获取Class对象;
b. 但是使用这种方式必须要捕获ClassNotFoundException异常,因为在编译期forName()无法对传入的字符串进行检查对应的类是否真实存在的,只能在运行时进行检查,如果不存在,则抛出异常
*/

2. Student.class;
/*
a. 更简便,且更安全,因为它在编译时就会受到检查(所以也不需要置于try语句块中),根除了forName()方法的调用,也更高效了,所以建议使用这种方式来获取;
b. 这种方式不仅可以应用于类,也可以应用于接口、数组以及基本数据类型;
c. 这种方式触发的是类的加载阶段,在这个阶段类的创建已经完成,获取其引用并不困难,但是不会触发类的初始化过程

*/
3. obj.getClass();
/*
也会触发类的初始化过程
*/

来看个例子:

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
package com.cjh.study.classinfo;

import java.util.Random;

/**
* @ClassName InitTest
* @Description TODO
* @Author cjh
* @Date 2019/5/4 16:18
* @Version 1.0
*/
public class ClassInitialization {

public static int random = new Random().nextInt(100);

public static void main(String[] args) {
// 使用字面量方式获取Class对象
Class<InitTest> initTestClass = InitTest.class;

System.out.println("创建InitTest引用...");

// 不会触发类的初始化
System.out.println("InitTest compileStaticFinal = " +
InitTest.compileStaticFinal);

// 会触发类的初始化
System.out.println("InitTest nonCompileStaticFinal= " +
InitTest.nonCompileStaticFinal);

// 会触发类的初始化
System.out.println("InitTest2.staticNonFinal = " +
InitTest2.staticNonFinal);

try {
Class<?> initTest3 =
Class.forName("com.cjh.study.classinfo.InitTest3");

System.out.println("创建InitTest3引用...");

// 会触发类的初始化
System.out.println("InitTest3.staticNonFinal = " +
InitTest3.staticNonFinal);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

}
}

class InitTest {

// 编译期静态常量
static final int compileStaticFinal = 12;

// 非编译期静态常量
static final int nonCompileStaticFinal = ClassInitialization.random ;

static {
System.out.println("初始化 InitTest...");
}

}

class InitTest2 {
// 静态成员变量
static int staticNonFinal = 23;

static {
System.out.println("初始化 InitTest2...");
}
}

class InitTest3 {
// 静态成员变量
static int staticNonFinal = 55;

static {
System.out.println("初始化 InitTest3...");
}
}

输出结果:
创建InitTest引用...
InitTest compileStaticFinal = 12
初始化 InitTest...
InitTest nonCompileStaticFinal = 32
初始化 InitTest2...
InitTest2.staticNonFinal = 23
初始化 InitTest3...
创建InitTest3引用...
InitTest3.staticNonFinal = 55

​ 从如上的代码结果可以得出结论:

1. 使用字面量的方式获取Class对象不会触发类的初始化
 2. 访问编译期静态常量(如:InitTest.compileStaticFinal)不会触发类的初始化。这是因为<font color=red>编译期静态常量会通过传播优化的方式被存储到一个NotInitialization常量池中</font>,如InitTest中的静态常量会被存储到NotInitialization常量池中,而之后对InitTest的常量的引用就会被转化为对NotInitialization类对自身常量池的引用,所以在编译期后,对编译期静态常量的获取都是从NotInitialization池中获取的,所以这是编译期静态常量不触发类的初始化的重要原因。
泛化的Class引用

类型转换

​ 许多需要类型转换的场景中,我们更多的是进行的强制类型转换,例如:

1
2
Animal ani = new Dog();
Dog dog = (Dog) ani;

​ 之所以可以进行强制类型转换,这完全要归功于RTTI,所有的类型转换都是在运行时进行正确性检查的,利用RTTI验证类型是否正确从而确保强制类型转换的完成,如果类型转换失败,那么将会抛出类型转换异常。

instanceof(RTTI的第三种形式)

​ 在进行向下的类型转换时,可以通过判断 x instanceof Animal的返回值来判定是否可以进行向下的类型转换操作。

​ 它的返回结果和isInstanceOf( )方法一致,如:class.isInstanceOf( obj ); isInstanceOf()是Class类Native方法,其中obj是被测试的对象或者变量,如果obj是调用这个方法的class或者接口的实例,则返回true

1. generator的配置

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

<context id="mysqlTables" targetRuntime="MyBatis3">

<!--是否生成注释-->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
<property name="suppressDate" value="true" />
</commentGenerator>


<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis_test"
userId="root"
password="root">
</jdbcConnection>

<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>

<!--生成model类存放位置-->
<javaModelGenerator targetPackage="com.epoch.mybatis.study.domain" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>

<!--生成映射文件存放位置-->
<sqlMapGenerator targetPackage="com.epoch.mybatis.study.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>

<!--生成dao存放路径-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.epoch.mybatis.study.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>

<!--需要生成的实体类对应的表-->
<table tableName="table_02"></table>
</context>
</generatorConfiguration>

2. maven引入插件

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
<!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>

<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<configuration>
<verbose>true</verbose>
<overwrite>true</overwrite>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
</configuration>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<!--这里在插件中手动加入mysql的驱动依赖,否则可能会出错,报找不到驱动包的异常-->
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.30</version>
</dependency>
</dependencies>
</plugin>

3. idea启动项配置

点击导航栏中的Run -> edit Configurations... -> 添加maven配置,Command line输入:
    mybatis-generator:generate -edit

4. 运行效果

4. 优劣势

优势

可以自动的生成代码,使用XXXExample的方式,可以不用自己写SQL

劣势

这会导致如果滥用的话,导致索引设计不好设计,建立好的索引会因为使用不当而导致索引失效,出现问题之后无法快速定位到问题

所以是否使用Example的方式需要慎重