JAVA并发编程揭开篇章,并发编程基本认识,了解多线程意义和使用

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。

一、什么是线程

线程(thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 10的线程,进行混合调度。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

一个进程可以有很多线程,每条线程并行执行不同的任务。


二、什么是并发

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

举个例子,简单来说并发是指单位时间内能够同时处理的请求数。默认情况下Tomcat可以支持的最大请求数是150,也就是同时支持150个并发。当超过这个并发数的时候,就会开始导致响应延迟,连接丢失等问题。

并发与并行

并行是指两个或者多个事件在同一时刻发生;

并发是指两个或多个事件在同一时间间隔内发生,这个词可以冲宏观和微观两个层面来讲,如果从微观角度来看。以线程为例,假设当前电脑的cpu是单核,但是能不能支持多线程呢?当然也是能的,此时如果是多线程运行的话,那么CPU是通过不断分配时间片的方式来实现线程切换,由于切换的速度足够快,我们很难感知到卡顿的过程。

三、Java中的线程

3.1 Runnable 接口

1
2
3
4
5
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}

3.2 Thread 类

1
2
3
4
5
6
7
8
9
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();

3.3 Callable/Future 带返回值的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CallableDemo implements Callable<String> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool (1);
CallableDemo callableDemo = new CallableDemo();
Future<String> future = executorService.submit(callableDemo);
System.out.println(future.get());
executorService.shutdown();
}
@Override
public String call() throws Exception {
int a = 1;
int b = 2;
System.out.println(a + b);
return "执行结果:" + (a + b);
}
}

四、 多线程的应用场景

  • 网络请求分发的场景
  • 文件导入
  • 短信发送场景

五、 Java并发编程基础

5.1 线程的生命周期

Java线程一共有 6 种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED

  • NEW:初始状态,线程被构建,但是还没有调用 start()方法;
  • RUNNABLE:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
  • BLOCK:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况:

➢ 等待阻塞:运行的线程执行 wait 方法,jvm 会把当前线程放入到等待队列

➢ 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么 jvm 会把当前的线程放入到锁池中

➢ 其他阻塞:运行的线程执行 Thread.sleep 或者 t.join 方法,或者发出了 I/O 请求时,JVM 会把当前线程设置为阻塞状态,当 sleep 结束、join 线程终止、io 处理完毕则线程恢复

  • WAITING:正在无限期等待另一个线程执行状态,需要唤醒
  • TIME_WAITING:超时等待状态,超时以后自动返回
  • TERMINATED:终止状态,表示当前线程执行完毕

5.2 线程的启动

启动线程的两种方式:

  • new Thread().start();//启动一个线程
  • Thread thread = new Thread(); thread.run();//调用实例中的方法

启动线程是调用start()方法,而不是run()方法,我们来看下Thread类中的start()方法源码:

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

public class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();//start0()方法是在此方法中注册的
}


public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();

/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);

boolean started = false;
try {
start0();//实体调用的是这个方法,它是native的
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}

private native void start0();//调用C++中的start0()


}

start0()方法注册在registerNatives()中,registerNatives的本地方法定义在Thread.c中,Thread.c定义了各个操作系统平台要用的关于线程的公共数据和操作

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
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive", "()Z", (void *)&JVM_IsThreadAlive},
{"suspend0", "()V", (void *)&JVM_SuspendThread},
{"resume0", "()V", (void *)&JVM_ResumeThread},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
{"yield", "()V", (void *)&JVM_Yield},
{"sleep", "(J)V", (void *)&JVM_Sleep},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
{"countStackFrames", "()I", (void *)&JVM_CountStackFrames},
{"interrupt0", "()V", (void *)&JVM_Interrupt},
{"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted},
{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},
{"getThreads", "()[" THD, (void *)&JVM_GetAllThreads},
{"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};

#undef THD
#undef OBJ
#undef STE

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

start0()实际上在C++中真正的执行的是JVM_StartThread方法,这个方法是在JVM层面执行的方法,这样需要下载hotspot的源码才能找到答案,我们接着找。在jvm.cpp文件中找到如下代码:

1
2
3
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;

再在thread.cppJavaThread相关的代码:

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
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
Thread()
#if INCLUDE_ALL_GCS
, _satb_mark_queue(&_satb_mark_queue_set),
_dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
if (TraceThreadEvents) {
tty->print_cr("creating thread %p", this);
}
initialize();
_jni_attach_state = _not_attaching_via_jni;
set_entry_point(entry_point);
// Create the native thread itself.
// %note runtime_23
os::ThreadType thr_type = os::java_thread;
thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
os::java_thread;
os::create_thread(this, thr_type, stack_sz);
_safepoint_visible = false;
// The _osthread may be NULL here because we ran out of memory (too many threads active).
// We need to throw and OutOfMemoryError - however we cannot do this here because the caller
// may hold a lock and all locks must be unlocked before throwing the exception (throwing
// the exception consists of creating the exception object & initializing it, initialization
// will leave the VM via a JavaCall and then all locks must be unlocked).
//
// The thread is still suspended when we reach here. Thread must be explicit started
// by creator! Furthermore, the thread must also explicitly be added to the Threads list
// by calling Threads:add. The reason why this is not done here, is because the thread
// object must be fully initialized (take a look at JVM_Start)
}

os::create_thread 就是调用系统创建线程的方法来创建java线程。创建完线程之后就来启动线程。启动线程调用Thread.cppThread::start(Thread* thread) 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Thread::start(Thread* thread) {
trace("start", thread);
// Start is different from resume in that its safety is guaranteed by context or
// being called from a Java method synchronized on the Thread object.
if (!DisableStartThread) {
if (thread->is_Java_thread()) {
// Initialize the thread state to RUNNABLE before starting this thread.
// Can not set it after the thread started because we do not know the
// exact thread state at that time. It could be in MONITOR_WAIT or
// in SLEEPING or some other state.
java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
java_lang_Thread::RUNNABLE);
}
os::start_thread(thread);
}
}

调用平台启动线程的方法,最终会调用Thread.cppJavaThread::run() 方法。

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
// The first routine called by a new Java thread
void JavaThread::run() {
// initialize thread-local alloc buffer related fields
this->initialize_tlab();

// used to test validitity of stack trace backs
this->record_base_of_stack_pointer();

// Record real stack base and size.
this->record_stack_base_and_size();

// Initialize thread local storage; set before calling MutexLocker
this->initialize_thread_local_storage();

this->create_stack_guard_pages();

this->cache_global_variables();

// Thread is now sufficient initialized to be handled by the safepoint code as being
// in the VM. Change thread state from _thread_new to _thread_in_vm
ThreadStateTransition::transition_and_fence(this, _thread_new, _thread_in_vm);

assert(JavaThread::current() == this, "sanity check");
assert(!Thread::current()->owns_locks(), "sanity check");

DTRACE_THREAD_PROBE(start, this);

// This operation might block. We call that after all safepoint checks for a new thread has
// been completed.
this->set_active_handles(JNIHandleBlock::allocate_block());

if (JvmtiExport::should_post_thread_life()) {
JvmtiExport::post_thread_start(this);
}

EventThreadStart event;
if (event.should_commit()) {
event.set_javalangthread(java_lang_Thread::thread_id(this->threadObj()));
event.commit();
}

// We call another function to do the rest so we are sure that the stack addresses used
// from there will be lower than the stack base just computed
thread_main_inner();

// Note, thread is no longer valid at this point!
}

最后来一张图总结一下Java线程的启动:

5.3 线程的终止

对于线程的终止并不是调用stop()方法的,在线程中提供了interrput()方法去优雅的中断一个线程。

下面通过一个例子来说明线程终止,调用interrupted()方法:

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

private static int i;

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
//默认情况下isInterrupted 返回 false、通过 thread.interrupt 变成了 true
while(!Thread.currentThread().isInterrupted()){
i ++;
}
System.out.println("Num:"+ i);
},"interruptDemo");

thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
}
}

通过 interrupt()方法,设置了一个标识告诉线程可以终止了 ,线程中还提供了静态方法Thread.interrupted()对设置中断标识的线程复位

5.3.1 线程复位

我们来改造上面示例中的代码:

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

private static int i;

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println("before" + Thread.currentThread().isInterrupted());
}
Thread.interrupted();//对线程进行复位,由true变为false
System.out.println("after" + Thread.currentThread().isInterrupted());
}
},"interruptDemo");

thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
}

}

除了使用Thread.interrupted()的方法对线程中断标识进行复位之外,还有一种被动的复位场景,就是抛出InterruptedException异常的方法,在InterruptedException 抛出之前,JVM 会先把线程的中断标识位清除,然后才会抛出 InterruptedException,这个时候如果调用 isInterrupted 方法,将会返回 false

5.3.2 为什么要进行复位

Thread.interrupted()是属于当前线程的,是当前线程对外界中断信号的一个响应,表示自己已经得到了中断信号,但不会立刻中断自己,具体什么时候中断由自己决定,让外界知道在自身中断前,他的中断状态仍然是 false,这就是复位的原因

5.3.3 线程的终止原理

终止线程是调用interrupt()方法,我们来看下Thread类中的interrupt()方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();

synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}

最终调用一个nativeinterrupt0()方法,和start0()方法一样,找到jvm.cpp中的JVM_Interrupt的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_Interrupt");

// Ensure that the C++ Thread and OSThread structures aren't freed before we operate
oop java_thread = JNIHandles::resolve_non_null(jthread);
MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
// We need to re-resolve the java_thread, since a GC might have happened during the
// acquire of the lock
JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
if (thr != NULL) {
Thread::interrupt(thr);
}
JVM_END

thread.cpp中的Thread::interrupt()方法源码:

1
2
3
4
5
void Thread::interrupt(Thread* thread) {
trace("interrupt", thread);
debug_only(check_for_dangling_thread_pointer(thread);)
os::interrupt(thread);
}

Thread::interrupt() 方法调用了 os::interrupt() 方法,这个是调用平台的 interrupt 方法,这个方法的实现是在 os_*.cpp
文件中,我们以 os_linux.cpp 文件为例:

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
void os::interrupt(Thread* thread) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");
//获取本地线程对象
OSThread* osthread = thread->osthread();
//判断本地线程是否为中断
if (!osthread->interrupted()) {
//设置中断状态为true
osthread->set_interrupted(true);
// More than one thread can get here with the same value of osthread,
// resulting in multiple notifications. We do, however, want the store
// to interrupted() to be visible to other threads before we execute unpark().
//内存屏障的目的是使得interrupted状态对其他线程立即可见
OrderAccess::fence();
//_SleepEvent相当于Thread.sleep,表示如果线程调用了sleep方法,则通过unpark唤醒
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}

// For JSR166. Unpark even if interrupt status already was set
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
//_ParkEvent用于synchronized同步块和Object.wait(),这里相当于也是通过unpark进行唤醒
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;

}

set_interrupted(true) 实际上就是调用osThread.hpp中的set_interrupted()方法,在 osThread 中定义了一个成员属性 volatile jint _interrupted

1
2
3
volatile jint _interrupted;     // Thread.isInterrupted state

void set_interrupted(bool z) { _interrupted = z ? 1 : 0; }

thread.interrupt()方法实际就是设置一个 interrupted 状态标识为 true、并且通过ParkEventunpark 方法来唤醒线程。

  • 对于 synchronized阻塞的线程,被唤醒以后会继续尝试获取锁,如果失败仍然可能被 park
  • 在调用 ParkEventpark方法之前,会先判断线程的中断状态,如果为 true,会清除当前线程的中断标识
  • Object.wait 、 Thread.sleep、Thread.join会抛出InterruptedException,不难发现这些方法都是阻塞的。阻塞方法的释放会取决于一些外部的事件,所以
    它允许一个线程请求自己来停止它正在做的事情。当一个方法抛出InterruptedException 时,它是在告诉调用者如果执行该方法的线程被中断,它会尝试停止正在做的事情并且通过抛出 InterruptedException 表示提前返回。

InterruptedException这个异常的意思是表示一个阻塞被其他线程中断了。然后,由于线程调用了 interrupt()中断方法,那么Object.wait**、Thread.sleep** 等被阻塞的线程被唤醒以后会通过is_interrupted方法判断中断标识的状态变化,如果发现中断标识为true,则先清除中断标识,然后抛出InterruptedException需要注意的是,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如:

  • 直接捕获异常不做任何处理
  • 将异常往外抛出
  • 停止当前线程,并打印异常信息

为了让大家能够更好的理解上面这段话,我们以Thread.sleep为例直接从jdk的源码中找到中断标识的清除以及异常抛出的方法代码找到is_interrupted() 方法:

1
public static native void sleep(long millis) throws InterruptedException;

jvm.cpp中的JVM_Sleep的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
JVMWrapper("JVM_Sleep");

if (millis < 0) {
THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
}
//判断并清除线程中断状态,如果中断状态为true,则抛出中断异常
if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
}

// Save current thread state and restore it at the end of this block.
// And set new thread state to SLEEPING.
JavaThreadSleepState jtss(thread);

os_linux.cpp中的is_interrupted()方法源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");

OSThread* osthread = thread->osthread();
//获取线程中断标识
bool interrupted = osthread->interrupted();
//如果中断标识为true
if (interrupted && clear_interrupted) {
//设置中断标识为false
osthread->set_interrupted(false);
// consider thread->_SleepEvent->reset() ... optional optimization
}

return interrupted;
}

至此,我们就已经分析清楚了中断的整个流程。最后还是来画图总结一下吧。

5.3.4 interrupt()的作用

  • 设置一个共享变量的值 true
  • 唤醒处于阻塞状态下的线程

手把手教你阅读mybatis核心源码,掌握底层工作原理与设计思想


title: 手把手教你阅读mybatis核心源码,掌握底层工作原理与设计思想
date: 2020-05-15 11:27:43
tags:

Mybatis目前作为互联网公司Java体系开源ORM框架的首选,它有着天然的优势,很多同学只关注其公司业务CRUD程序的编写,忽略了其源码阅读的重要性。下面来看一段使用Mybatis API写的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
BusinessMapper mapper = session.getMapper(BusinessMapper.class);
Business business = mapper.selectBusinessById(1);
System.out.println(business);

}finally {
session.close();
}

接下来按照示例代码的步骤一步一步地来分析代码的运行背后的秘密,揭开mybatis源码的真实面目。给出的源码片段均有中文注释,方便同学们加深理解。

一、全局配置解析过程

1.1 SqlSessionFactoryBuilder(构建工厂类)

1
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

创建一个新的SqlSessionFactoryBuilder对象,这里使用了建造者模式。调用了build()方法创建了SqlSessionFactory对象,在SqlSessionFactoryBuilder中有9个重载的build()方法,可以使用不同的方式来创建SqlSessionFactory对象,其默认是单例模式的。

1.2 XmlConfigBuilder(解析全局配置文件)

创建XmlConfigBuilder对象用来解析全局配置文件,解析完成之后会返回一个Configuration对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//1、创建XMLConfigBuilder对象
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//2、调用解析方法返回Configuration对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

XMLConfigBuilder继承自抽象类BaseBuilder,解析全局的配置文件,BaseBuilder还有一些子类,用来创建不同的目标的。例如:

  • XMLMapperBuilder:解析Mapper映射器
  • XMLStatementBuilder:解析增删改查标签
  • XMLScriptBuilder:解析动态SQL

接着来看下XMLConfigBuilder对象调用的parse()方法的源码:

1
2
3
4
5
6
7
8
9
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// XPathParser,dom 和 SAX 都有用到 >>
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

java中解析xml配置文件的方式有很多种,mybatis对DOM和SAX两种方式做了不同的封装。接着看parseConfiguration()方法。

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
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 对于全局配置文件各种标签的解析
1、解析<properties>标签,可以读取外部引入的属性文件,比如database.properties
propertiesElement(root.evalNode("properties"));
// 2、解析 settings 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
//3、获取Virtual File System自定义实现类,比如读取本地文件
loadCustomVfs(settings);
//4、根据<longImpl>标签获取日志实现类
loadCustomLogImpl(settings);
//5、解析类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//6、解析plugins标签,比如翻页插件PageHelper
pluginElement(root.evalNode("plugins"));
// 用于创建对象
objectFactoryElement(root.evalNode("objectFactory"));
// 用于对对象进行加工
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 反射工具箱
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// settings 子标签赋值,默认值就是在这里提供的 >>
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 创建了数据源 >>
environmentsElement(root.evalNode("environments"));
// 解析databaseIdProvider标签,生成DatabaseIdProvider对象
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 用来做映射的,得到JavaType和JdbcType,存放在TypeHandlerRegistry对象中
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析引用的Mapper映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

上面的方法中所有的值都会封装到Configuration对象中。下面是创建过程的时序图。

二、会话创建过程

1
SqlSession session = sqlSessionFactory.openSession();

这里实际上调用了DefaultSqlSessionFactory类的openSessionFromDataSource()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
// 获取事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 创建事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根据事务工厂和默认的执行器类型,创建执行器 >>
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

2.1 获取Environment对象

Configuration对象中获取Environment对象,环境对象中有事务工厂类;

1
2
3
4
5
public final class Environment {
private final String id;
private final TransactionFactory transactionFactory;
private final DataSource dataSource;
}

2.2 创建事务

Environment对象中获取一个TranscationFactory对象,事务工厂类型可以配置成JDBC或者MANAGED

  • JDBC;使用jdbc的Connection对象来管理事务;
  • MANAGED:事务将有容器进行管理。
1
2
3
4
5
6
private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
if (environment == null || environment.getTransactionFactory() == null) {
return new ManagedTransactionFactory();
}
return environment.getTransactionFactory();
}

2.3 创建执行器

1
final Executor executor = configuration.newExecutor(tx, execType);

执行器Executor的基本类型有三种:

  • SIMPLE(默认)
  • BATCH
  • REUSE
1
2
3
4
5
6
7
8
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 默认 SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}

抽象类BaseExecutor实现Executor接口,这是模板设计模式的体现。

缓存装饰

在newExecutor()方法中:

1
2
3
4
// 二级缓存开关,settings 中的 cacheEnabled 默认是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}

代理插件

1
2
// 植入插件的逻辑,至此,四大对象已经全部拦截完毕
executor = (Executor) interceptorChain.pluginAll(executor);

2.4 返回SqlSession

SqlSession类中包括ConfigurationExecutor两大对象。下面是创建过程的时序图。

三、获取代理对象

接口中的名称和Mapper.xml文件中的namespace是一一对应的,方法名称也是StatementId也是对应的。

1
2
BusinessMapper mapper = session.getMapper(BusinessMapper.class);
Business business = mapper.selectBusinessById(1);
1
2
3
4
5
<mapper namespace="com.sy.mapper.BusinessMapper">

<select id="selectBusinessById" resultMap="BaseResultMap" statementType="PREPARED" >
select * from bsuiness where bid = #{bid}
</select>

3.1 getMapper()方法

1、DefaultSqlSession中的getMapper()方法:

1
2
3
4
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}

2、Configuration类中的getMapper()方法:

1
2
3
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}

3、MapperRegistry中的getMapper()方法:

1
2
3
4
5
6
7
8
9
10
11
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

在解析mapper标签和Mapper.xml的时候已经把接口类型和类型对应的MapperProxyFactory放到一个map中,获取Mapper的代理对象,实际上是从map中获取对应的工厂类后,最终通过JDK动态代理创建的。

1
2
3
4
5
6
7
8
9
10
11
protected T newInstance(MapperProxy<T> mapperProxy) {
// 1:类加载器;
// 2:被代理类实现的接口;
// 3:实现了 InvocationHandler 的触发管理类
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}

MapperProxy实现了InvocationHandler接口,参数有sqlSession, mapperInterface, methodCache,最终是通过JDK的动态代理创建返回代理对象(类型是$Proxy数字)。这个对象继承Proxy类,实例被代理的接口,里面持有了一个MapperProxy类型的触发管理类。

3.2 MapperProxy 实现对接口的代理

JDK的动态代理有三个核心角色:

  • 被代理类(实现类)
  • 接口
  • 实现了InvocationHandler的触发管理类

用来生成代理对象。被代理的类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。

而MyBatis里面的Mapper没有实现类,它直接忽略了实现类,直接对接口进行代理。


获取Mapper对象的过程,实际上是获取了一个JDK动态代理对象。这个代理类继承Proxy类,实现被代理的接口,里面持有一个MapperProxy类型的触发管理类。来看下代理类过程的时序图吧。

四、执行SQL

1
Business business = mapper.selectBusinessById(1);

4.1 MapperProxy.invoke()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// toString hashCode equals getClass等方法,无需走到执行SQL的流程
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 提升获取 mapperMethod 的效率,到 MapperMethodInvoker(内部接口) 的 invoke
// 普通方法会走到 PlainMethodInvoker(内部类) 的 invoke
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}

4.1.1、首先判断是否要执行SQL还是直接执行方法

Object本身的方法和Java 8中的默认方法不需要取执行SQL

4.1.2、获取缓存

这里加入缓存时为了提升MapperMethod的获取速度

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
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// Java8 中 Map 的方法,根据 key 获取值,如果值是 null,则把后面Object 的值赋给 key
// 如果获取不到,就创建
// 获取的是 MapperMethodInvoker(接口) 对象,只有一个invoke方法
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
// 接口的默认方法(Java8),只要实现接口都会继承接口的默认方法,例如 List.sort()
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 创建了一个 MapperMethod
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}

Map 的 computeIfAbsent()方法:只有key不存在或者value为null,则把后面的Object的值赋给key。Java8Java9中的接口默认方法由特殊处理,返回DefaultMethodInvoker对象。普通的方法返回的是PlainMethodInvokerMapperMethod

在MapperMethod对象中有两个比较重要的属性:

1
2
3
4
// statement id (例如:com.sy.mapper.BusinessMapper.selectBusinessById) 和 SQL 类型
private final SqlCommand command;
// 方法签名,主要是返回值的类型
private final MethodSignature method;

4.2 MappperMethod.execute()方法

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
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
// 普通 select 语句的执行入口 >>
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}

根据不同的类型(INSERT、UPDATE、DELETE、SELECT)和返回类型:

  • 调用convertArgsToSqlCommandParam()将方法的参数转换为SQL的参数。
  • 调用sqlSession的insert()、update()、delete()、selectOne()方法。

下面重点来讲下查询的selectOne()方法。调用了DefaultSqlSession的selectOne()方法。

4.3 DefaultSqlSession.selectOne()方法

1
2
3
4
5
6
7
8
9
10
11
12
public <T> T selectOne(String statement, Object parameter) {
// 来到了 DefaultSqlSession
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}

在SelectList()中,我们先根据commandname(StatementID)从Configuration中拿到MappedStatement,这个ms上面有我们在xml中配置的所有属性,包括id、statementType、sqlSource、useCache、入参、出参等等。

1
2
3
4
5
6
7
8
9
10
11
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
// 如果 cacheEnabled = true(默认),Executor会被 CachingExecutor装饰
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

然后执行executor.query(),前面我们说到了Executor有三种基本类型,SIMPLE/REUSE/BATCH,还有一种包装类型,CachingExecutor。那么在这里到底会选择哪一种执行器呢?我们要回过头去看看DefaultSqlSession在初始化的时候是怎么赋值的,这个就是我们的会话创建过程。如果启用了二级缓存,就会先调用CachingExecutor 的query()方法,里面有缓存相关的操作,然后才是再调用基本类型的执行器,比如默认的SimpleExecutor。在没有开启二级缓存的情况下,先会走到BaseExecutor的query()方法(否则会先走到CachingExecutor)。

4.4 CachingExector.query()方法

1
2
3
4
5
6
7
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取SQL
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建CacheKey:什么样的SQL是同一条SQL? >>
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

4.4.1、创建CacheKey

二级缓存的CacheKey是如何构成的呢?换句话说,什么样的查询才能确定是同一个查询呢?在BaseExector中createCacheKey()方法,用到了六要素:

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
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset()); // 0
cacheKey.update(rowBounds.getLimit()); // 2147483647 = 2^31-1
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value); // development
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}

即为方法相同、翻页偏移相同、SQL相同、参数值相同、数据源环境相同才会被认定为同一个查询。

注意看下CacheKey类的属性,里面有一个List按照顺序存放了上面的六要素。

1
2
3
4
5
6
7
8
9
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;

private final int multiplier;
private int hashcode;
private long checksum;
private int count;
// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this is not always true and thus should not be marked transient.
private List<Object> updateList;

怎么比较两个CacheKey是否相等呢?如果一上来就依次比较六个要素是否相等,这样要比6次,这样效率不高。每一个类都继承自Object,都有一个hashCode()方法,用来生成哈希码。它是用来在集合中快速判重的。

在生成cacheKey的时候也就是调用update()方法,也更新了cacheKey的hashCode,它是用乘法哈希生成的。

1
2
3
4
5
6
7
8
9
10
11
12
public void update(Object object) {
// 加法哈希
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

count++;
checksum += baseHashCode;
baseHashCode *= count;
// 37 * 17 +
hashcode = multiplier * hashcode + baseHashCode;

updateList.add(object);
}

Object中的hashCode()方法是一个本地方法,通过随机数算法生成(OpenJDK8 默认,可以通过-XX:hashCode修改)。CacheKey中的hashCode()方法进行了重写,返回生成新的hashCode。

为什么需要用37作为乘法因子呢?这是一个经验值,跟String类中的31类似。看下String类中的hashCode()方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
//31作为乘法因子
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

CacheKey中的equals()方法也进行了重写,比较cacheKey是否相等。

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
@Override
public boolean equals(Object object) {
// 同一个对象
if (this == object) {
return true;
}
// 被比较的对象不是 CacheKey
if (!(object instanceof CacheKey)) {
return false;
}

final CacheKey cacheKey = (CacheKey) object;

// hashcode 不相等
if (hashcode != cacheKey.hashcode) {
return false;
}
// checksum 不相等
if (checksum != cacheKey.checksum) {
return false;
}
// count 不相等
if (count != cacheKey.count) {
return false;
}

for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}

如果哈希值(乘法哈希),校验值(加法哈希),要素个数任何一个不相等,都不是同一个查询,最后再循环比较要素,防止hash碰撞。

CacheKey生成之后,调用CachingExecutor类的query()方法。

1
2
3
4
5
6
7
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取SQL
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建CacheKey:什么样的SQL是同一条SQL? >>
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

4.4.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
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
// cache 对象是在哪里创建的? XMLMapperBuilder类 xmlconfigurationElement()
// 由 <cache> 标签决定
if (cache != null) {
// flushCache="true" 清空一级二级缓存 >>
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 获取二级缓存
// 缓存通过 TransactionalCacheManager、TransactionalCache 管理
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 写入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

首先从MappedStatement对象中调用getCache()方法,判断对象是否为空,如果为空,则没有查询二级缓存、写入二级缓存的流程。

那么Cache对象是什么时候被创建出来的呢?用来解析Mapper.xml的XMLMapperBuilder类,configurationElement()方法中调用cacheElement()方法:

1
cacheElement(context.evalNode("cache"));

只有Mapper.xml中的cache标签不为空才会被解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void cacheElement(XNode context) {
// 只有 cache 标签不为空才解析
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
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();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}

builderAssistant.useNewCache()方法创建了一个Cache对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}

二级缓存为什么要用TCM来进行管理呢?

我们来思考一个问题,在一个事务中:

  • 1、首先插入一条数据(没有提交),此时二级缓存会被清空。
  • 2、在这个事务中查询数据,写入二级缓存。
  • 3、提交事务,出现异常,数据回滚。

此时出现了数据库没有这条数据,但是二级缓存有这条数据的情况。所以MyBatis
的二级缓存需要跟事务关联起来。

那么为什么一级缓存不这么做?

因为一个session就是一个事务,事务回滚,会话就结束了,缓存也清空了,不存
在读到一级缓存中脏数据的情况。二级缓存是跨session的,也就是跨事务的,才有可能出现对同一个方法的不同事务访问。

4.4.2.1 写入二级缓存

1
tcm.putObject(cache, key, list); // issue #578 and #116

调用TranscationalCacheManagerputObject()方法,从map中拿出TransactionalCache对象,把value添加到待提交的map中。此时缓存还没有真正的写入。

1
2
3
4
5
6
7
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}

private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}

调用TranscationalCacheputObject()方法

1
2
3
4
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}

只有真正的提交事务的时候才真正的写入缓存。

4.4.2.2 获取二级缓存

1
List<E> list = (List<E>) tcm.getObject(cache, key);

从map中拿出Transcational对象,这个对象也是对PerpetualCache经过层层装饰的缓存对象。getObject()方法层层递归,直到到达PerpetualCache,拿到value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}

PerpetualCache中的getObject()方法:

1
2
3
4
@Override
public Object getObject(Object key) {
return cache.get(key);
}

4.5 BaseExecutor.query()方法

4.5.1 清空本地缓存

queryStack用于记录查询栈,防止递归时候查询重复处理缓存。flushCache=true的时候,会先清除本地缓存LocalCache(一级缓存)。

1
2
3
4
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// flushCache="true"时,即使是查询,也清空一级缓存
clearLocalCache();
}

如果没有缓存,会从数据库查询。调用queryFromDatabase()方法。

1
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

LocalCacheScope == STATEMENT,就会清空本地缓存。

1
2
3
4
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}

4.5.2 数据库查询

1、先在缓存用占位符进行占位。执行查询后,移除占位符,放入数据。

2、执行Exector的doQuery()方法,默认是SimpleExector。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 先占位
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 三种 Executor 的区别,看doUpdate
// 默认Simple
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 写入一级缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

4.6 SimpleExecutor.query()方法

4.6.1 创建StatementHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 注意,已经来到SQL处理的关键对象 StatementHandler >>
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 获取一个 Statement对象
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询
return handler.query(stmt, resultHandler);
} finally {
// 用完就关闭
closeStatement(stmt);
}
}

configuration.newStatementHandler()先得到RoutingStatementHandler。RoutingStatementHandler没有任何实现,用来创建基本的StatementHandler,这里会根据MappedStatement里面的statementType决定StatementHandler的类型。默认是PREPARED(STATEMENT、PREPARED、CALLABLE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// StatementType 是怎么来的? 增删改查标签中的 statementType="PREPARED",默认值 PREPARED
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
// 创建 StatementHandler 的时候做了什么? >>
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}

}

StatementHandler里面包含了处理参数的ParamterHandler和处理结果集的ResultHandler。这两个对象都是在上面new的时候创建的。

StatementHandler父类BaseStatementHandler类中的构造函数中创建以上两个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;

this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.objectFactory = configuration.getObjectFactory();

if (boundSql == null) { // issue #435, get the key before calculating the statement
generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}

this.boundSql = boundSql;

// 创建了四大对象的其它两大对象 >>
// 创建这两大对象的时候分别做了什么?
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

这些对象都是可以被插件拦截的四大对象,所以在创建之后都要用拦截器进行包装的方法。在Configuration中进行拦截调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 植入插件逻辑(返回代理对象)
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
// 植入插件逻辑(返回代理对象)
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 植入插件逻辑(返回代理对象)
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

这里只有对其中的三个对象,还有一个对象呢?它什么时候创建呢?

4.6.2 创建Statement

用new出来的StatementHandler创建Statement对象。

1
2
3
4
5
6
7
8
9
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 获取 Statement 对象,如果有插件包装,会先走到被拦截的业务逻辑
stmt = handler.prepare(connection, transaction.getTimeout());
// 为 Statement 设置参数,对sql语句进行预编译,处理参数
handler.parameterize(stmt);
return stmt;
}
1
2
3
4
@Override
public void parameterize(Statement statement) throws SQLException {
delegate.parameterize(statement);
}

4.6.3 执行StatementHandler的query()方法

RoutingStatementHandler的query()方法,delegate委派,最终执行PreparedStatementHandler的query()方法。

1
2
3
4
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
return delegate.query(statement, resultHandler);
}

4.6.4 执行PreparedStatementHandler的query()方法

1
2
3
4
5
6
7
8
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 到了JDBC的流程
ps.execute();
// 处理结果集
return resultSetHandler.handleResultSets(ps);
}

4.6.5 ResultHandler处理结果集

1
resultSetHandler.handleResultSets(ps);

怎么把ResultSet转换成List?

ResultSetHandIer只有一个实现类:DefaultResultSetHandler也就是执行
DefaultResultSetHandler的handleResultSets()方法。首先我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况,一般只有一个结果集。如果下面的这个while循环我们也不用,就执行一次。然后会调用handleResuItSet()方法。

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
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

final List<Object> multipleResults = new ArrayList<>();

int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);

List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}

String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}

return collapseSingleResultList(multipleResults);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}

面试官口中的Mybatis,工作流程、架构分层与模块划分以及缓存机制

在我们平时的业务开发中,经常会使用“半自动化”的ORM框架Mybatis解决程序对数据库操作问题。MyBatis是一个Java持久化框架,它通过XML描述符或注解把对象与存储过程或SQL语句关联起来。MyBatis是在Apache许可证2.0下分发的自由软件,是iBATIS 3.0的分支版本。2001年开始开发的,是“internet”和“abtis(障碍物)”两个单词的组合。2004年捐赠给Apache,2010年更名为MyBatis。

对于MyBatis在java程序中的使用想必大家一定都比较清楚了,这里主要说说它的工作流程、架构分层与模块划分以及缓存机制。

一、MyBatis的工作流程

1.1 解析配置文件(Configuration)

mybatis启动的时候需要解析配置文件,包括全局配置文件和映射器配置文件,我们会把它们解析成一个Configuration对象。它包含了控制mybatis的行为以及对数据库下达的指令(SQL操作)。

1.2 提供操作接口(SqlSession)

应用程序与数据库进行连接是通过SqlSession对象完成的,如果需要获取一个会话,则需要通过会话工厂SqlSessionFactory接口来获取。

通过建造者模式SqlSessionFactoryBuilder来创建一个工厂类,它包含所有配置文件的配置信息。

SqlSession只是提供了一个接口,它还不是真正的操作数据库的SQL执行对象。

1.3 执行SQL操作

Executor接口用来封装对数据库的操作。调用其中query和update接口会创建一系列的对象,来处理参数、执行SQL、处理结果集,把它简化成一个对象接口就是StatementHandler

简要的画一下MyBatis的工作流程图:

二、MyBatis的架构分层与模块划分

我们打开Mybatis的package,发现类似下面的结构:


按照不同的功能职责,也可以分成不同的工作层次。

三、MyBatis的缓存

3.1 缓存体系结构

Mybatis缓存的默认实现是PerpetualCache类,它是基于HashMap实现的。

PerpetualCache在Mybatis是基础缓存,但是缓存有额外的功能,比如策略回收、日志记录、定时刷新等等,如果需要使用这些功能,那么需要在基础缓存的基础上进行添加,需要的时候添加,不需要即可不用添加。在缓存cache包下,有很多装饰器模式的类实现了Cache接口,通过这些实现类可以实现很多缓存额外的功能。

所有的缓存实现总体上可以分为三大类:基本缓存、淘汰算法缓存、装饰器缓存。

3.2 一级缓存(Local Cache)

Mybatis的一级缓存是存放在会话(SqlSession)层面的,一级缓存是默认开启的,不需要额外的配置,关闭的话设置localCacheScope的值为STATEMENT。源码的位置在BaseExecutor中,如下图:


如果需要在同一个会话共享一级缓存的话,那么最好的办法是在SqlSession内创建会话对象,让其成为SqlSession的一个属性,这样的话就很方便的操作一级缓存了。在同一个会话里多次执行相同的SQL语句,会直接从内存拿到缓存的结果集,不会再去数据库进行操作。如果在不同的会话中,即使SQL语句一模一样,也不会使用一级缓存的。

一级缓存的验证方式

判断是否命中缓存?如果第二次发送SQL并且到数据库中执行,则说明没有命中缓存;如果直接打印对象,则说明是从内存中获取到的结果。

测试一级缓存需要先关闭二级缓存,将LocalCacheScope设置为SESSION

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
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
//在同一个session中共享
BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
Blog blog = mapper0.selectBlogById(1);
System.out.println(blog);

System.out.println("第二次查询,相同会话,获取到缓存了吗?");
System.out.println(mapper1.selectBlogById(1));
//不同的session不能共享
System.out.println("第三次查询,不同会话,获取到缓存了吗?");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));

} finally {
session1.close();
}
}

一级缓存在什么时候被清空失效的呢?在同一个session中update(包括delete)会导致一级缓存被清空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void testCacheInvalid() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.selectBlogById(1));

Blog blog = new Blog();
blog.setBid(1);
blog.setName("after modified 666");
mapper.updateByPrimaryKey(blog);
session.commit();

// 相同会话执行了更新操作,缓存是否被清空?
System.out.println("在[同一个会话]执行更新操作之后,是否命中缓存?");
System.out.println(mapper.selectBlogById(1));

} finally {
session.close();
}
}

一级缓存的工作范围是一个session中,如果跨session会出现什么问题呢?如果其它的session更新了数据,会导致读取到过时的数据(一级缓存不能跨session共享)

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
public void testDirtyRead() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));

// 会话2更新了数据,会话2的一级缓存更新
Blog blog = new Blog();
blog.setBid(1);
blog.setName("after modified 333333333333333333");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
mapper2.updateByPrimaryKey(blog);
session2.commit();

// 其他会话更新了数据,本会话的一级缓存还在么?
System.out.println("会话1查到最新的数据了吗?");
System.out.println(mapper1.selectBlogById(1));
} finally {
session1.close();
session2.close();
}
}

一级缓存的不足之处

一级缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不同的缓存。在分布式环境(多会话)下,会存在查询到过时的数据的情况。如果有解决这个问题,那么需要引进工作范围更为广发的二级缓存。

3.3 二级缓存

二级缓存的生命周期和应用同步,它是用来解决一级缓存不能跨会话共享数据的问题,范围是namespace级别的,可以被多个会话共享(只要是同一个接口的相同方法,都可以进行共享)。

二级缓存的流程图:

一级缓存是默认开始的,二级缓存如何开启呢?
1、在mybatis-config.xml中配置(默认是true)

1
2
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="true"/>

只要没有显式地设置cacheEnabled为false,都会使用CachingExector装饰基本的执行器(SIMPLE、REUSE、BATCH)。
二级缓存总是默认开启的,但是每个Mapper的二级开关是默认关闭的。

2、在Mapper中配置cache标签

1
2
3
4
5
6
<!-- 声明这个namespace使用二级缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"<!-- 最多缓存对象个数,默认是1024 -->
eviction="LRU"<!-- 缓存策略 -->
flushInterval="120000"<!-- 自动刷新时间ms,未配置是只有调用时刷新 -->
readOnly="false"/><!-- 默认是false(安全),改为true可读写时,对象必须支持序列化 -->

Cache属性详解:

默认的回收内存策略是 LRU。可用的内存回收策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

Mapper.xml 配置了cache之后,select()会被缓存。update()、delete()、insert()会刷新缓存。:如果cacheEnabled=true,Mapper.xml 没有配置标签,还有二级缓存吗?(没有)还会出现CachingExecutor 包装对象吗?(会)

只要cacheEnabled=true基本执行器就会被装饰。有没有配置cache,决定了在启动的时候会不会创建这个mapper的Cache对象,只是最终会影响到CachingExecutorquery 方法里面的判断。如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?我们可以在单个Statement ID 上显式关闭二级缓存(默认是true):

1
<select id="selectBlog" resultMap="BaseResultMap" useCache="false">

二级缓存的验证方式

1、事务不提交,二级缓存会写入吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
// 事务不提交的情况下,二级缓存会写入吗?显然不会,为什么呢?
session1.commit();

System.out.println("第二次查询");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
}
}

为什么事务不提交,二级缓存不生效呢?
因为二级缓存使用TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache 的getObject()、putObject和commit()方法,TransactionalCache里面又持有了真正的Cache对象,比如是经过层层装饰的PerpetualCache。在putObject 的时候,只是添加到了entriesToAddOnCommit里面,只有它的commit()方法被调用的时候才会调用flushPendingEntries()真正写入缓存。它就是在DefaultSqlSession 调用commit()的时候被调用的。

1
2
3
4
5
6
7
8
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 真正写入二级缓存
flushPendingEntries();
reset();
}
1
2
3
4
5
6
7
8
9
10
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}

在其它的会话中执行增删改操作,验证缓存被刷新

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
public void testCacheInvalid() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
SqlSession session3 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
BlogMapper mapper3 = session3.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
session1.commit();

// 是否命中二级缓存
System.out.println("是否命中二级缓存?");
System.out.println(mapper2.selectBlogById(1));

Blog blog = new Blog();
blog.setBid(1);
blog.setName("2020年5月13日15:03:38");
mapper3.updateByPrimaryKey(blog);
session3.commit();

System.out.println("更新后再次查询,是否命中二级缓存?");
// 在其他会话中执行了更新操作,二级缓存是否被清空?
System.out.println(mapper2.selectBlogById(1));

} finally {
session1.close();
session2.close();
session3.close();
}
}

为什么增删改操作会清空缓存?
CachingExecutor的update()方法里面会调用flushCacheIfRequired(ms),isFlushCacheRequired 就是从标签里面渠道的flushCache 的值。而增删改操作的flushCache 属性默认为true。

1
2
3
4
5
6
7
8
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 增删改查的标签上有属性:flushCache="true" (select语句默认是false)
// 一级二级缓存都会被清理
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}

什么时候开启二级缓存呢?

一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?

因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。如果多个namespace 中有针对于同一个表的操作,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper 里面只操作单表的情况使用。如果要让多个namespace共享一个二级缓存,应该怎么做?跨namespace的缓存共享的问题,可以使用cache-ref配置来解决:

1
<cache-ref namespace="com.sy.crud.dao.DepartmentMapper" />

  cache-ref 代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。

注意:在这种情况下,多个Mapper的操作都会引起缓存刷新,缓存的意义已经不大了

第三方缓存做二级缓存

除了MyBatis 自带的二级缓存之外,我们也可以通过实现Cache 接口来自定义二级缓存。MyBatis官方提供了一些第三方缓存集成方式,比如ehcache 和redis:

https://github.com/mybatis/redis-cache

当然,我们也可以使用独立的缓存服务,不使用MyBatis 自带的二级缓存。

pom文件引入的依赖:

1
2
3
4
5
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>

mapper.xml配置文件的内容:

1
2
3
<!-- 使用Redis作为二级缓存 -->
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

redis.properties配置文件内容:

1
2
3
4
5
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

当然,我们在分布式的环境中,也可以使用独立的缓存服务,不使用MyBatis自带的二级缓存。

23种设计模式的学习,我们需要掌握的是“道”,而不只是“术”

设计模式是一门艺术,因为它们来源于生活,不要为了套用设计模式而去使用设计模式。我们在编写程序的时候用好设计模式可以防范于未然,它们可以很好地提供一种解决问题的方案。从古至今,我们在遇到问题的时候,都会去寻求帮助,咨询懂的人,或者去图书馆查阅资料,或者在电脑前面搜索,等等方法。在学习23种设计模式之前我们必然需要学习软件架构的设计原则,下面用一句话来概括这七大设计原则。

一、一句话总结设计原则

二、设计模式简介

建筑师克里斯托佛·亚历山大在1977/79年编制了一本汇集设计模式的书,但是这种设计模式的思想在建筑设计领域里的影响远没有后来在软件开发领域里传播的广泛。

肯特·贝克和沃德·坎宁安在1987年,利用克里斯托佛·亚历山大在建筑设计领域里的思想开发了设计模式并把此思想应用在Smalltalk中的图形用户接口(GUI)的生成中。一年后埃里希·伽玛在他的苏黎世大学博士毕业论文中开始尝试把这种思想改写为适用于软件开发。与此同时James Coplien 在1989年至1991年也在利用相同的思想致力于C++的开发,而后于1991年发表了他的著作Advanced C++ Programming Styles and Idioms。同年Erich Gamma 得到了博士学位,然后去了美国,在那与Richard Helm, Ralph Johnson ,John Vlissides 合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns - Elements of Reusable Object-Oriented Software) 一书,在此书中共收录了 23 种设计模式。

这四位作者在软件开发领域里以“四人帮”(英语,Gang of Four,简称GoF)而闻名,并且他们在此书中的协作导致了软件设计模式的突破。有时,GoF也会用于代指《设计模式》这本书。

设计模式是前人经验的总结,提供给后人去借鉴使用的,前人种树后人乘凉。它不但可以解决复杂的业务问题,也可以帮助我们提高代码的可读性、扩展性,降低维护成本。下面对GoF23种设计模式进行归纳和总结。

2.1 创建型

创建型全部是关于如何创建实例的。这组范例可以被划分为两组:类创建范例及对象创建范例。类创建实例在实例化过程中有效的使用类之间的继承关系,对象创建范例则使用代理来完成其任务。

  • 抽象工厂 (Abstract Factory)
  • 建造者 (Builder Pattern)
  • 工厂方法 (Factory Method pattern)
  • 原型 (Prototype pattern)
  • 单例模式 (Singleton pattern)

2.2 结构型

这组都是关于类及对象复合关系的。

  • 适配器(Adapter pattern)
  • 桥接(Bridge pattern)
  • 组合(Composite pattern)
  • 装饰器(Decorator pattern)
  • 门面(Facade pattern)
  • 享元(Flyweight pattern)
  • 代理(Proxy pattern)

2.3 行为型

这组都是关于对象之间如何通讯的。

  • 职责链(Chain-of-responsibility pattern)
  • 命令(Command pattern)
  • 翻译器(Interpreter pattern)
  • 迭代器(Iterator pattern)
  • 中介者(Mediator pattern)
  • 备忘录(Memento pattern)
  • 观察者(Observer pattern)
  • 状态(State pattern)
  • 策略(Strategy pattern)
  • 模板方法(Template method pattern)
  • 访问者(Visitor)

2.4 创建型使用频率

2.5 结构型使用频率

2.6 行为型使用频率

三、一句话总结设计模式



四、设计模式之间的关系和对比

4.1 单例模式和工厂模式

在实际业务中,通常把工厂类设计为单例模式。

4.2 策略模式和工厂模式

1、工厂模式包含工厂方法模式和抽象工厂模式,它是创建型模式,而策略模式是行为型模式;

2、工厂模式的目的是封装好创建逻辑,策略模式接收工厂创建好的对象,从而实现不同的行为。

4.3 策略模式和委派模式

1、策略模式是委派模式内部的一种实现形式,策略模式关注的结构是否能够相互替代。

2、委派模式更关注分发和调度的过程。

4.4 模板方法模式和工厂方法模式

工厂方法是模板方法的一种特殊实现。

4.5 模板方法模式和策略模式

1、模板方法模式和策略模式都有封装的算法;

2、策略模式是使不同的算法可以相互替换,且不影响客户端应用层的使用;

3、模板方法是针对定义一个算法的流程,将一些有细微差异的部分交给子类实现;

4、模板方法模式不能改变算法流程,策略模式可以改变算法流程且可以替换。策略模式通常用来代理if…else等分支条件语句。

4.6 装饰者模式和静态代理模式

1、装饰者模式在于给对象动态添加方法,而代理更加关注控制对象的访问;

2、代理模式通常会在代理类中创建被代理对象的实例,而装饰者模式通常把装饰者作为构造行数。

4.7 装饰者模式和适配器模式

1、装饰者模式和适配器模式都属于包装器模式;

2、装饰者模式可以实现被装饰者与相同的接口或者继承被装饰者作为它的子类,而适配器和被适配着可以实现不同的接口。

4.8 适配器模式和静态代理模式

适配器模式可以结合静态代理来实现,保存被适配对象的引用,但不是唯一的实现方式;

4.9 适配器模式和策略模式

在适配业务复杂的情况下,利用策略模式优化动态适配逻辑。

深度阅读Spring5.x源码后,使用Java实现迷你版Spring的基本思路实践

看过Spring5源码的同学们,一开始肯定会边看边去用断点进行源码调试,调试来调试去时间长了肯定会晕车的,最有效的方式是先猜测后进行验证,当然猜测是建立在对阅读源码相当有经验的基础上的,否则也是一头雾水。对于使用很长时间Spring框架的开发者来说,应该对其架构和结构不会太陌生,可以大胆地进行猜测。

根据源码猜测,整理了迷你版Spring的基本实现思路,如下图:

一、web.xml配置文件

所有依赖于Web容器的项目,基本都是从web.xml文件开始的,首先我们先配置好web.xml的文件内容,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<display-name>mini-spring</display-name>
<servlet>
<servlet-name>symvc</servlet-name>
<servlet-class>com.sy.sa.framework.servlet.SyDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>

<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>symvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>

SyDispatcherServlet是模拟的Spring实现的核心类,后面会讲解具体的实现源码的。

二、配置application.properties

无论是xml、properties、yml都是配置文件的表现形式,无论格式怎样变化,其表现的内容大致上是没有什么变化的。具体的内容如下:

1
scanPackage=com.sy.sa

三、自定义注解Annotation

3.1 @SyAutoWired

1
2
3
4
5
6
7
8
9
10
package com.sy.sa.framework.annotation;

import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SyAutoWired {
String value() default "";
}

3.2 @SyController

1
2
3
4
5
6
7
8
9
10
package com.sy.sa.framework.annotation;

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SyController {
String value() default "";
}

3.3 @SyRequestMapping

1
2
3
4
5
6
7
8
9
10
package com.sy.sa.framework.annotation;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SyRequestMapping {
String value() default "";
}

3.4 @SyRequestParam

1
2
3
4
5
6
7
8
9
10
package com.sy.sa.framework.annotation;

import java.lang.annotation.*;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SyRequestParam {
String value() default "";
}

3.5 @SyService

1
2
3
4
5
6
7
8
9
10
package com.sy.sa.framework.annotation;

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SyService {
String value() default "";
}

四、配置注解Annotation

4.1 IDemoService接口

1
2
3
4
5
public interface IDemoService {

String get(String name);

}

4.2 DemoServiceImpl实现类

1
2
3
4
5
6
public class DemoService implements IDemoService {
@Override
public String get(String name) {
return "Hello World, [" + name + "]";
}
}

4.3 DemoController类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SyController
@SyRequestMapping("/sy")
public class DemoController {

@SyAutoWired
private IDemoService demoService;

@SyRequestMapping("/query")
public void query(HttpServletRequest request, HttpServletResponse response,
@SyRequestParam("name") String name) {
String result = demoService.get(name);
try{
response.getWriter().write(result);
} catch (Exception e) {
e.printStackTrace();
}
}

}

五、自定义SyDispatcherServlet的实现

5.1 覆盖实现HttpServlet中的init()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/**
* mini-spring的第一步初始化阶段
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {

//1.加载application.properties配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));

//2.扫描对应包下的所有类
doScanner(contextConfig.getProperty("scanPackage"));

//3.初始化扫描到的类,并将它们放入到IoC容器中
doInstance();

//4.完成DI依赖注入
doAutowried();

//5.初始化HandlerMapping
initHandlerMapping();

System.out.println("Sy Spring framework is init.");
}

/**
* 初始化url和Method的一一对应的关系
*/
private void initHandlerMapping() {
if(ioc.isEmpty()) {
return;
}

for (Map.Entry<String, Object> entry : ioc.entrySet()
) {
Class<?> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(SyController.class)) {
continue;
}
String baseUrl = "";

if(clazz.isAnnotationPresent(SyRequestMapping.class)) {
SyRequestMapping requestMapping = clazz.getAnnotation(SyRequestMapping.class);
baseUrl = requestMapping.value();
}

//获取所有的public方法
for (Method method :
clazz.getMethods()) {
if(method.isAnnotationPresent(SyRequestMapping.class)) {
SyRequestMapping requestMapping = method.getAnnotation(SyRequestMapping.class);
// //sy///query
String regex = ("/" + baseUrl + "/" + requestMapping.value())
.replaceAll("/+","/");
Pattern pattern = Pattern.compile(regex);

handlerMapping.add(new Handler(pattern, entry.getValue(), method));

System.out.println("Mapped :" + pattern + "," + method);
}
}
}
}

/**
* 将容器中的类进行依赖注入
*/
private void doAutowried() {

if(ioc.isEmpty()) {
return;
}

for (Map.Entry<String, Object> entry:
ioc.entrySet()) {
/**
* Declared 所有的,特定的 字段,包括private/protected/default
* 正常来说,普通的OOP编程只能拿到public的属性
*/
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for(Field field: fields) {
if(!field.isAnnotationPresent(SyAutoWired.class)) {
continue;
}
SyAutoWired autoWired = field.getAnnotation(SyAutoWired.class);
String beanName = autoWired.value().trim();
if("".equals(beanName)) {
//获得接口的类型,作为key待会拿这个key到ioc容器中去取值
beanName = field.getType().getName();
}

/**
* 如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
* 反射中叫做暴力访问, 强吻
*/
field.setAccessible(true);

try {
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

}

/**
* 初始化类放入IoC容器中,为依赖注入做准备
*/
private void doInstance() {
if (classNames.isEmpty()) {
return;
}
try {
for (String className :
classNames) {
Class<?> clazz = Class.forName(className);
//根据注解实例化对应的类
if(clazz.isAnnotationPresent(SyController.class)) {
Object instance = clazz.newInstance();
//Spring默认类名首字母小写
String beanName = this.toLowerFirstCase(clazz.getSimpleName());
//获取到的类的实例放入到IoC容器中
ioc.put(beanName, instance);
}else if(clazz.isAnnotationPresent(SyService.class)) {
//1、自定义的beanName
SyService service = clazz.getAnnotation(SyService.class);
String beanName = service.value();
//2、默认类名首字母小写
if("".equals(beanName.trim())) {
beanName = toLowerFirstCase(clazz.getSimpleName());
}
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
//3、根据类型自动赋值,投机取巧的方式
for (Class<?> cls:
clazz.getInterfaces()) {
if(ioc.containsKey(cls.getName())) {
throw new Exception("The “" + cls.getName() + "” is exists!!");
}
//把接口的类型直接当成key了
ioc.put(cls.getName(), instance);
}
}else {
continue;
}
}
}catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 根据包路径扫描相关的类
* @param scanPackage
*/
private void doScanner(String scanPackage) {
//将包路径中的“.”替换为“/”
URL url = this.getClass().getClassLoader().getResource("/" +
scanPackage.replaceAll("\\.", "/"));
File classPath = new File(url.getFile());
for (File file : classPath.listFiles()
) {
if(file.isDirectory()) {
doScanner(scanPackage + "." + file.getName());
}else {
if(!file.getName().endsWith(".class")) {
continue;
}
String className = (scanPackage + "."
+ file.getName().replace(".class", ""));
//将完整类名保存到List中
classNames.add(className);
}
}
}

/**
* 加载配置文件application.properties,放到properties中
* @param contextConfigLocation
*/
private void doLoadConfig(String contextConfigLocation) {
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

/**
* 将类名首字母变成小写字母
* @param simpleName
* @return
*/
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
/**
* 之所以加,是因为大小写字母的ASCII码相差32,
* 而且大写字母的ASCII码要小于小写字母的ASCII码
* 在Java中对char做算学运算,实际上就是对ASCII码做算学运算
*/
chars[0] += 32;
return String.valueOf(chars);
}

5.2 Handler类

保存url和method的对应关系,代码如下:

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
package com.sy.sa.framework.handler;

import com.sy.sa.framework.annotation.SyRequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
* 保存一个url和一个method的关系
*/
public class Handler {

private Pattern pattern;
private Method method;
private Object controller;
private Class<?>[] paramTypes;

/**
* 形参列表
* 参数的名字作为key,参数的顺序,位置作为值
*/
private Map<String, Integer> paramIndexMapping;

public Pattern getPattern() {
return pattern;
}

public Method getMethod() {
return method;
}

public Object getController() {
return controller;
}

public Class<?>[] getParamTypes() {
return paramTypes;
}

public Handler(Pattern pattern, Object controller, Method method) {
this.pattern = pattern;
this.method = method;
this.controller = controller;

paramTypes = method.getParameterTypes();

paramIndexMapping = new HashMap<String, Integer>();

this.putParamIndexMapping(method);
}

private void putParamIndexMapping(Method method) {
/**
* 根据方法得到注解是一个二维数组
* 一个参数可以有多个注解,而一个方法又有多个参数
*/
Annotation[][] result = method.getParameterAnnotations();
for(int i = 0; i < result.length; i++) {
for (Annotation annotation : result[i]) {
if(annotation instanceof SyRequestParam) {
String paramName = ((SyRequestParam) annotation).value();
if(!"".equals(paramName.trim())) {
paramIndexMapping.put(paramName, i);
}
}
}
}
/**
* 提取方法中的request、response参数
*/
Class<?>[] paramTypes = method.getParameterTypes();
for(int i = 0 ; i < paramTypes.length; i++) {
Class<?> type = paramTypes[i];
if(type == HttpServletRequest.class ||
type == HttpServletResponse.class) {
paramIndexMapping.put(type.getName(), i);
}
}
}


}

5.3 doPost()/doGet()方法

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
 @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//6、调用,运行阶段
try {
doDispatch(req,resp);
} catch (Exception e) {
e.printStackTrace();
resp.getWriter().write("500 Exection,Detail : " + Arrays.toString(e.getStackTrace()));
}
}

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
Handler handler = this.getHandler(req);
if(handler == null) {
resp.getWriter().write("404 Not Found!!!");
return;
}

Map<String, Integer> paramIndexMapping = handler.getParamIndexMapping();

//获得方法的形参列表
Class<?> [] paramTypes = handler.getParamTypes();

Object [] paramValues = new Object[paramTypes.length];

Map<String,String[]> params = req.getParameterMap();
for (Map.Entry<String, String[]> parm : params.entrySet()) {
String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]","")
.replaceAll("\\s",",");
if(!paramIndexMapping.containsKey(parm.getKey())){continue;}

int index = paramIndexMapping.get(parm.getKey());
paramValues[index] = convert(paramTypes[index],value);
}

if(paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
int reqIndex = paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[reqIndex] = req;
}

if(paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
int respIndex = paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[respIndex] = resp;
}

Object returnValue = handler.getMethod().invoke(handler.getController(),paramValues);
if(returnValue == null || returnValue instanceof Void){ return; }
resp.getWriter().write(returnValue.toString());
}

doPost()方法中使用了委派模式,委派模式的代码逻辑在doDispath()方法中。

至此,迷你版Spring的基本思路代码实践就完成了。

架构师内功心法,23种设计模式中最为复杂的访问者模式详解

访问者模式(Visitor Pattern)是一种将数据结构和数据操作分离的设计模式。是指封装一些作用于某种数据结构中的各种元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

访问者模式被称为最复杂的设计模式,并且使用频率不高,设计模式的作者也评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。访问者模式的基本思想是,针对系统中拥有固定类型数的对象结构(元素),在其内提供一个accept()方法用来接受访问者对象的访问。不同的访问者对同一元素的访问内容不同,使得相同的元素集合可以产生不同的数据结果。accept()方法可以接收不同的访问者对象,然后在内部将自己(元素)转发到接收到的访问者对象的visit()方法内。访问者内部对应类型的visit()方法就会得到回调执行,对元素进行操作。也就是通过两次动态分发(第一次是对访问者的分发accept()方法,第二次是对元素的分发visit()方法),才最终将一个具体的元素传递到一个具体的访问者。如此一来,就解耦了数据结构与操作,且数据操作不会改变元素状态。

一、访问者模式的应用场景

访问者模式在生活场景中也是非常当多的,例如每年年底的KPI考核,KPI考核标准是相对稳定的,但是参与KPI考核的员工可能每年都会发生变化,那么员工就是访问者。我们平时去食堂或者餐厅吃饭,餐厅的菜单和就餐方式是相对稳定的,但是去餐厅就餐的人员是每天都在发生变化的,因此就餐人员就是访问者。


访问者模式的核心是,解耦数据结构与数据操作,使得对元素的操作具备优秀的扩展性。可以通过扩展不同的数据操作类型(访问者)实现对相同元素的不同的操作。简而言之就是对集合中的不同类型数据(类型数量稳定)进行多种操作,则使用访问者模式。

访问者模式的应用场景适用于以下几个场景:

  • 数据结构稳定,作用于数据结构的操作经常变化的场景;
  • 需要数据结构与数据操作呢分离的场景;
  • 需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。

访问者模式主要包含五种角色:

  • 抽象访问者(Visitor):接口或抽象类,该类地冠以了对每一个具体元素(Element)的访问行为visit()方法,其参数就是具体的元素(Element)对象。理论上来说,Visitor的方法个数与元素(Element)个数是相等的。如果元素(Element)个数经常变动,会导致Visitor的方法也要进行变动,此时,该情形并不适用访问者模式;
  • 具体访问者(ConcreteVisitor):实现对具体元素的操作;
  • 抽象元素(Element):接囗或抽象类,定义了一个接受访问者访问的方法accept()表示所有元素类型都支持被访问者访问;
  • 具体元素(ConcreteElement):具体元素类型,提供接受访问者的具体实现。通常的实现都为:visitor.visit(this);
  • 结构对象(ObjectStructure):内部维护了元素集合,并提供方法接受访问者对该集合所有元素进行操作。

1.1 利用访问者模式实现公司KPI考核

每到年底,公司的管理层就要开始评定员工一年的工作绩效了,管理层有CEO和CTO,那么CEO关注的是工程师的KPI和经理的KPI以及新产品的数量,而CTO关心的是工程师的代码量、经理的新产品数量。

由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同的员工类型进行不同的处理。此时访问者模式就派上用场了。下面来看下具体的代码实现,首先创建员工Employee类:

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

private String name;

private int kpi;

public Employee(String name) {
this.name = name;
this.kpi = new Random().nextInt(10);
}

/**
* 接收访问者的访问
* @param visitor
*/
public abstract void accept(IVisitor visitor);
}

Employee类的accept()方法表示接受访问者的访问,由具体的子类实现。访问者是一个接口,传入不同的实现类,可以访问不同的数据。 分别创建工程师Engineer类和经理Manager类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Engineer extends Employee {
public Engineer(String name) {
super(name);
}

@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}

public int getCodeLines() {
return new Random().nextInt(10 * 10000);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Manager extends Employee {
public Manager(String name) {
super(name);
}

@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}

public int getPrducts() {
return new Random().nextInt(10);
}
}

工程师考核的是代码数量,经理考核的是产品数量,二者的职责不一样。也正是因为有这样的差异性,才使得访问模式能够在这个场景下发挥作用。将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的showReport()方法查看所有员工的绩效,创建BusinessReport类:

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

private List<Employee> employeeList = new LinkedList<>();

public BusinessReport() {
employeeList.add(new Engineer("工程师1"));
employeeList.add(new Engineer("工程师2"));
employeeList.add(new Engineer("工程师3"));
employeeList.add(new Engineer("工程师4"));

employeeList.add(new Manager("产品经理1"));
employeeList.add(new Manager("产品经理2"));
}

/**
*
* @param visitor 公司高层,如CEO,CTO
*/
public void showReport(IVisitor visitor) {
for(Employee employee : employeeList) {
employee.accept(visitor);
}
}
}

定义访问者类型,创建接口IVisitor,访问者声明了两个visit()方法,分别针对工程师和经理,代码如下:

1
2
3
4
5
6
7
public interface IVisitor {

void visit(Engineer engineer);

void visit(Manager manager);

}

具体访问者CEOVisitor和CTOVisitor类:

1
2
3
4
5
6
7
8
9
10
11
12
public class CEOVisitor implements IVisitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师:" + engineer.name + ", KPI:" + engineer.kpi);
}

@Override
public void visit(Manager manager) {
System.out.println("经理:" + manager.name + ", KPI:" + manager.kpi +
", 新产品数量" + manager.getPrducts() );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class CTOVisitor implements IVisitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师:" + engineer.name + ", 代码数量:" + engineer.getCodeLines());
}

@Override
public void visit(Manager manager) {
System.out.println("经理:" + manager.name +
", 新产品数量" + manager.getPrducts() );
}
}

测试main方法:

1
2
3
4
5
public static void main(String[] args) {
BusinessReport businessReport = new BusinessReport();
businessReport.showReport(new CEOVisitor());
businessReport.showReport(new CTOVisitor());
}

运行结果如下:


在上述的案例中,Employee扮演了Element角色,而Engineer和Manager都是ConcreteElement;CEOVisitor和CTOVisitor都是具体的Visitor对象;而BusinessReport就是ObjectStructure。

访问者模式最大的优点就是增加访问者非常容,我们从代码中可以看到,如果要增加一访问者,只要新实现一个访问者接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式而又不想对不同的元素进行不同的操作,那么必定需要使用if-else和类型转换,这使得代码唯以升级维护。

我们要根据具体情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定是否需要经常定义新的操作,使用访问者模式是否能优化我们的代码而不是使我们的代码变得更复杂。

1.2 从静态分派到动态分派

变量被声明时的类型叫做变量的静态类型(Static Type),有些人把静态类型叫做明显类型(Apparent Type);而变量所引用的对象的真是类型又叫做变量的实际类型(Actual Type)。 比如:

1
2
List list = null;
list = new ArrayList();

上面的代码声明了一个list,它的静态类型(也叫明显类型)是List,而它的实际类型是ArrayList。根据对象的类型而对方法进行的选择,就是分派(Dispatch)。分派又分为两种,即动态分派和静态分派。

1.2.1 静态分派

静态分派(Static Dispatch)就是按照变量的静态类型进行分派,从而确定方法的执行版本,静态分派在编译时期就可以确定方法的版本。而静态分配最经典的就是方法重载,请看下面的这段代码:

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

public void test(String string) {
System.out.println("string");
}

public void test(Integer integer) {
System.out.println("integer");
}

public static void main(String[] args) {
String string = "1";
Integer integer = 1;
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.test(string);
staticDispatch.test(integer);
}

}

在静态分派判断的时候,我们根据多个判断依据(即参数类型和个数)判断出了方法的版本,那么这个就是多分派的概念,因为有一个以上的考量标准。所以Java语言是静态多分派语言。

1.2.2 动态分派

动态分派,与静态相反,它不是在编译期间确定方法的版本,而是在运行时确定的。Java是动态单分派语言。

1.2.3 访问者模式中的伪动态双分派

通过前面分析,我们知道Java是静态多分派、动态单分派的语言。Java底层不支持动态的双分派。但是通过使用设计模式,也可以在Java语言里实现伪动态双分派。在访问者模式中使用的就是伪动态双分派。所谓动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。
还是回到前面的公司KPI考核业务场景当中,BusinessReport类中的showReport()方法:

1
2
3
4
5
6
7
8
9
 /**
*
* @param visitor 公司高层,如CEO,CTO
*/
public void showReport(IVisitor visitor) {
for(Employee employee : employeeList) {
employee.accept(visitor);
}
}

这里就是依据Employee和IVisitor两个实际类型决定了showReport()方法的执行结果从而决定了accept()方法的动作。

分析accept()方法的调用过程
1、当调用accept()方法时,根据Employee的实际类型决定是调用Engineer还是Manager的accept()方法。

2、这时accept()方法的版本已经确定,假如是Engineer,它的accept()方去是调用下面这行代码。

1
2
3
public void accept(IVisitor visitor) {
visitor.visit(this);
}

此时的this是Engineer类型,所以对应的IVisitor接口的visit(Engineer enginner)方法,此时需要再根据访问者的实际类型确定visit()方法的版本,这样一来,就完成了动态分派的过程。

以上的过程就是通过两次动态双分派,第一次对accept()方法进行动态分派,第二次访问者的visit()方法进行动态分派,从而到达了根据两个实际类型确定一个方法的行为结果。

二、访问者模式在源码中的体现

2.1 NIO中的FileVisitor接口

JDK中的NIO模块下的FileVisitor接口,它提供递归遍历文件树的支持。来看下源码:

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
public interface FileVisitor<T> {

/**
* Invoked for a directory before entries in the directory are visited.
*
* <p> If this method returns {@link FileVisitResult#CONTINUE CONTINUE},
* then entries in the directory are visited. If this method returns {@link
* FileVisitResult#SKIP_SUBTREE SKIP_SUBTREE} or {@link
* FileVisitResult#SKIP_SIBLINGS SKIP_SIBLINGS} then entries in the
* directory (and any descendants) will not be visited.
*
* @param dir
* a reference to the directory
* @param attrs
* the directory's basic attributes
*
* @return the visit result
*
* @throws IOException
* if an I/O error occurs
*/
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException;

/**
* Invoked for a file in a directory.
*
* @param file
* a reference to the file
* @param attrs
* the file's basic attributes
*
* @return the visit result
*
* @throws IOException
* if an I/O error occurs
*/
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException;

/**
* Invoked for a file that could not be visited. This method is invoked
* if the file's attributes could not be read, the file is a directory
* that could not be opened, and other reasons.
*
* @param file
* a reference to the file
* @param exc
* the I/O exception that prevented the file from being visited
*
* @return the visit result
*
* @throws IOException
* if an I/O error occurs
*/
FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException;

/**
* Invoked for a directory after entries in the directory, and all of their
* descendants, have been visited. This method is also invoked when iteration
* of the directory completes prematurely (by a {@link #visitFile visitFile}
* method returning {@link FileVisitResult#SKIP_SIBLINGS SKIP_SIBLINGS},
* or an I/O error when iterating over the directory).
*
* @param dir
* a reference to the directory
* @param exc
* {@code null} if the iteration of the directory completes without
* an error; otherwise the I/O exception that caused the iteration
* of the directory to complete prematurely
*
* @return the visit result
*
* @throws IOException
* if an I/O error occurs
*/
FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException;
}

这个接口上面定义的方法表示了遍历文件的关键过程,允许在文件被访问、目录被访问、目录已被访问、放生错误过程中进行控制整个流程。调用接口中的方法,会返回访问结果FileVisitResult对象值,用于决定当前操作完成后接下来该如何处理。FileVisitResult的标准返回值存放到枚举类型中:

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 enum FileVisitResult {
/**
* Continue. When returned from a {@link FileVisitor#preVisitDirectory
* preVisitDirectory} method then the entries in the directory should also
* be visited.
*/
//当前的遍历过程将会继续
CONTINUE,
/**
* Terminate.
*/
//表示当前的遍历过程将会停止
TERMINATE,
/**
* Continue without visiting the entries in this directory. This result
* is only meaningful when returned from the {@link
* FileVisitor#preVisitDirectory preVisitDirectory} method; otherwise
* this result type is the same as returning {@link #CONTINUE}.
*/
//当前的遍历过程将会继续,但是要忽略当前目录下的所有节点
SKIP_SUBTREE,
/**
* Continue without visiting the <em>siblings</em> of this file or directory.
* If returned from the {@link FileVisitor#preVisitDirectory
* preVisitDirectory} method then the entries in the directory are also
* skipped and the {@link FileVisitor#postVisitDirectory postVisitDirectory}
* method is not invoked.
*/
//当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点
SKIP_SIBLINGS;
}

2.2 Spring中的BeanDefinitionVisitor类

在Spring的Ioc中有个BeanDefinitionVisitor类,它有一个visitBeanDefinition()方法,看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void visitBeanDefinition(BeanDefinition beanDefinition) {
visitParentName(beanDefinition);
visitBeanClassName(beanDefinition);
visitFactoryBeanName(beanDefinition);
visitFactoryMethodName(beanDefinition);
visitScope(beanDefinition);
if (beanDefinition.hasPropertyValues()) {
visitPropertyValues(beanDefinition.getPropertyValues());
}
if (beanDefinition.hasConstructorArgumentValues()) {
ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
visitIndexedArgumentValues(cas.getIndexedArgumentValues());
visitGenericArgumentValues(cas.getGenericArgumentValues());
}
}

在其方法中分别访问了其它的数据,比如父类的名字、自己的类名、在Ioc容器中的名称等各种信息。

三、访问者模式的优缺点

优点

  • 解耦了数据结构与数据操作,使得操作集合可以独立变化;
  • 扩展性好:可以通过扩展访问者角色,实现对数据集的不同操作;
  • 元素具体类型并非单一,访问者均可操作;
  • 各角色职责分离,符合单一职责原则。

缺点

  • 无法增加元素类型:若系统数据结构对象另于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,违背了开闭原则。

架构师内功心法,被誉为摩斯密码的解释器模式详解

解释器模式(Interpreter Pattern)是指给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。是一种按照规定语法进行解析的模式。

就比如编译器可以将源码编译解释为机器码,让CPU能进行识别并运行。解释器模式的作用其实与编译器一样,都是将一些固定的文法(即语法)进行解释,构建出一个解释句子的解释器。简单理解,解释器是一个简单语法分析工具,它可以识别句子语义,分离终结符号和非终结符号,提取出需要的信息,让我们能针对不同的信息做出相应的处理。其核心思想是识别文法,构建解释。

一、解释器模式的应用场景

其中我们每天都生活在解释器模式中,平时所听到的音乐都可以通过简谱记录下来;还有战争年代发明的摩尔斯密码(又称为摩斯密码,Morse code),其实也是一种解释器。


我们在程序中,如果存在一种特定类型的问题,该类型问题涉及多个不同实例,但是具备固定文法描述,那么可以使用解释器模式对该类型问题进行解释,分离出需要的信息,根据获取的信息做出相应的处理。简而言之,对于一些固定文法构建一个解释句子的解释器。解释器模式适用于以下应用场景:

  • 一些重复出现的问题可以用一种简单的语言来进行表达;
  • 一个简单语法需要解释的场景。

解释器模式主要包含4种角色:

  • 抽象表达式(Expression):负责定义一个解释方法interpret,交由具体子类进行具体解释;

  • 终结符表达式(TerminalExpression):实现文法中与终结符有关的解释操作。文法中的每一个终结符都有一个具体终结表达式与之相对应,比如公式R=R1+R2,R1和R2就是终结符对应的解析R1和R2的解释器就是终结符表达式。通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符(R1,R2);

  • 非终结符表达式(NonterminalExpression):实现文法中与非终结符有关的解释操作。文法中的每条规则都对应于一个非终结符表达式。非终结符表达式一般是文法中的运算符或者其他关键字,比如公式R=R1+R2中,”+”就是非终结符,解析“+“的解释器就是一个非终结符表达式。非终结符表达式根据逻辑的复杂程度而增加原则上每个文法规则都对应一个非终结符表达式;

  • 上下文环境类(Context):包含解释器之外的全局信息。它的任务一般是用来存放文法中各个终结符所对应的具体值,比如R=R1+R2,给R1赋值100,给R2赋值200,这些信息需要存放到环境中。

使用解释器模式解析数据表达式

用解释器模式来实现一个数学表达式计算器,包含加减乘除四种运算能力。

首先定义抽象表达式角色接口IArithmeticInterpreter:

1
2
3
4
5
public interface IArithmeticInterpreter {

int interpret();

}

创建终结者表达式角色Interpreter抽象类:

1
2
3
4
5
6
7
8
9
10
public abstract class Interpreter implements IArithmeticInterpreter {

protected IArithmeticInterpreter left;
protected IArithmeticInterpreter right;

public Interpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
this.left = left;
this.right = right;
}
}

分别创建非终结表达式角色加、减、乘、除4个解释器,加法运算表达式AddInterpreter类:

1
2
3
4
5
6
7
8
9
10
public class AddInterpreter extends Interpreter {
public AddInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
super(left, right);
}

@Override
public int interpret() {
return this.left.interpret() + this.right.interpret();
}
}

创建减法运算表达式SubInterpreter类:

1
2
3
4
5
6
7
8
9
10
public class SubInterpreter extends Interpreter {
public SubInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
super(left, right);
}

@Override
public int interpret() {
return this.left.interpret() - this.right.interpret();
}
}

创建乘法运算表达式MultiInterpreter类:

1
2
3
4
5
6
7
8
9
10
public class MultiInterpreter extends Interpreter {
public MultiInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
super(left, right);
}

@Override
public int interpret() {
return this.left.interpret() * this.right.interpret();
}
}

创建除法运算表达式DivInterpreter类:

1
2
3
4
5
6
7
8
9
10
public class DivInterpreter extends Interpreter {
public DivInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
super(left, right);
}

@Override
public int interpret() {
return this.left.interpret() / this.right.interpret();
}
}

创建数学表达式NumInterpreter类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class NumInterpreter implements IArithmeticInterpreter {

private int value;

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

@Override
public int interpret() {
return this.value;
}
}

创建工具OperatorUtil类:

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

public static boolean isOperator(String symbol) {
return (symbol.equals("+") || symbol.equals("-") || symbol.equals("*"));
}

public static Interpreter getInterpreter(IArithmeticInterpreter left,
IArithmeticInterpreter right, String symbol) {
if(symbol.equals("+")) {
return new AddInterpreter(left, right);
}else if(symbol.equals("-")) {
return new SubInterpreter(left, right);
}else if(symbol.equals("*")) {
return new MultiInterpreter(left, right);
}else if(symbol.equals("/")) {
return new DivInterpreter(left, right);
}
return null;
}
}

创建计算器Calculator类:

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

private Stack<IArithmeticInterpreter> stack = new Stack<>();

public Calculator(String experssion) {
parse(experssion);
}

private void parse(String expression) {
String[] elements = expression.split(" ");
IArithmeticInterpreter left, right;

for(int i = 0; i < elements.length; i++) {
String operator = elements[i];
if(OperatorUtil.isOperator(operator)) {
left = this.stack.pop();
right = new NumInterpreter(Integer.valueOf(elements[++i]));
System.out.println("出栈:" + left.interpret() + "和" + right.interpret());
this.stack.push(OperatorUtil.getInterpreter(left, right, operator));
System.out.println("应用运算符:" + operator);
}else {
NumInterpreter numInterpreter = new NumInterpreter(Integer.valueOf(elements[i]));
this.stack.push(numInterpreter);
System.out.println("入栈:" + numInterpreter.interpret());
}
}
}

public int calculate() {
return this.stack.pop().interpret();
}
}

测试main方法:

1
2
3
4
5
public static void main(String[] args) {
System.out.println("测试结果是:" + new Calculator("18 - 12").calculate());
System.out.println("测试结果是:" + new Calculator("18 + 12").calculate());
System.out.println("测试结果是:" + new Calculator("18 * 2 + 12 - 6").calculate());
}

二、解释器模式在源码中的体现

2.1 正则表达式编译和继续Pattern类

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
private Pattern(String p, int f) {
pattern = p;
flags = f;

// to use UNICODE_CASE if UNICODE_CHARACTER_CLASS present
if ((flags & UNICODE_CHARACTER_CLASS) != 0)
flags |= UNICODE_CASE;

// Reset group index count
capturingGroupCount = 1;
localCount = 0;

if (pattern.length() > 0) {
compile();
} else {
root = new Start(lastAccept);
matchRoot = lastAccept;
}
}

public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}

public static Pattern compile(String regex, int flags) {
return new Pattern(regex, flags);
}

2.2 Spring中的ExpressionParser接口

1
2
3
4
5
public interface ExpressionParser {
Expression parseExpression(String expressionString) throws ParseException;

Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
}

三、解释器模式的优缺点

优点:

  • 扩展性强在解释器模式中由于语法是由很多类表示的,当语法规则更改时,只需修改相应的非终结符表达式即可;若扩展语法时,只需添加相应非终结符类即可;
  • 增加了新的解释表达式的方式;
  • 于实现文法解释器模式对应的文法应当是比较简单目易于实现的,过于复杂的语法并不适合使用解释器模式。

缺点:

  • 语法规则较复杂时,会引起类膨胀:解释器模式每个语法都要产生一个非终结符表达式,当浯法规则比较复杂时,就会产生大量的解释类,增加系统维护困难;
  • 执行效率比较低解释器模式采用递归调用方法,每个菲终结符表达式只关心与自己有关的表达式,每个表达式需要知道最终的结果,因此完整表达式的最终结果是通过从后往前递归调用的方式获取得到。当完整表达式层级较深时,解释效率下降,且出错时调试困难,因为递归迭代层级太深。

架构师内功心法,不需要中间商赚差价的中介者模式详解

中介者模式(Mediator Pattern)又称为调解者模式或者调停者模式。用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地互相作用,从而使其松散耦合,而且可以独立的改变他们之间的交互。

中介者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。当某些对象之间的作用发生改变时,不会立即影响其它的一些对象之间的作用。保证这些作用可以彼此独立的变化。其核心思想是,通过中介者解耦系统各层次对象的直接耦合,层次对象的对外依赖通信统统交由中介者转发。

一、中介者模式的应用场景

在现实生活中,中介者的存在是不可缺少的,如果没有了中介者,我们就不能与远方的朋友进行交流了。各个对象将会互相进行引用,如果每个对象都与多个对象进行交互时,将会形成如下图所示的网状结构。

从上面的图可以发现,每个对象之间过度耦合,这样既不利于信息的复用也不利于扩展。如果引入了中介者模式,那么对象之间的关系将变为星状结构,采用中介者模式后如下图所示:

引入中介者模式后,任何一个类的变化,只会影响中介者合类本身,之前的设计是任何一个类的变化都会引起其关联所有类的变化。这样的设计减少了系统的耦合度。

其实在我们日常生活中每天在刷的微信朋友圈,就是一个中介者。还有信息交易平台,也是中介者模式的体现。

中介者模式是用来降低多个对象和类之间的通信复杂性。这种模式通过提供一个中介类,将系统各层次对象间的多对多关系变成一对多关系,中介者对象可以将复杂的网状结构变成以调停者为中心的星形结构,达到降低系统的复杂性,提高可扩展性的作用。

若系统各层次对象之间存在大量的关联关系,即层次对象呈复杂的网状结构,如果直接让它们紧耦合通信,会造成系统结构变得异常复杂,且其中某个层次对象发生改变,则与其紧耦合的相应层次对象也需进行修改,系统很难进行维护。而通过为该系统增加一个中介者层次对象让其他各层次需对外通信的行为统统交由中介者进行转发,系统呈现以中介者为中心进行通讯的星形结构,系统的复杂性大大降低。

简单的说如果多个类相互耦合,形成了网状结构,则需要考虑使用中介者模式进行优化处理。中介者模式适用以下几个场景:

  • 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解;
  • 交互的公共行为,如果需要改变行为则可以新增新的中介者类。

中介者模式主要包含4个角色:

  • 抽象中介者(Mediator):定义统一的接口,用户各个同事角色间的通讯;
  • 具体中介者(ConcreateMediator):从具体的同事对象接收消息,协调各同事对象间的协作;
  • 抽象同事类(Colleague):每一个同事对象均需要依赖中介者角色,与其他同事通信时,交由中介者进行转发;
  • 具体同事类(ConcreteColleague):负责实现自发行为(Self-Method),转发依赖方法(Dep-Method)交由中介者进行协调。

1.1 简易聊天室系统使用中介者模式

假设我们要构建一个聊天室系统,用户可以向聊天室发送消息,聊天室会向所有的用户显示消息。实际上就是用户发信息与聊天室显示的通信过程,不过用户无法直接将信息发给聊天室,而是需要将信息先发到服务器上,然后服务器再将该消息发给聊天室进行显示。具体代码如下。

创建User类:

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

private String name;

private ChatRoom chatRoom;

public User(String name, ChatRoom chatRoom) {
this.name = name;
this.chatRoom = chatRoom;
}

public void sendMsg(String msg) {
chatRoom.showMsg(this, msg);
}

public String getName() {
return name;
}

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

创建聊天室ChatRoom类:

1
2
3
4
5
6
7
public class ChatRoom {

public void showMsg(User user, String msg) {
System.out.println("[" + user.getName() + "] :" + msg);
}

}

测试main方法:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {

ChatRoom chatRoom = new ChatRoom();

User kevin = new User("Kevin", chatRoom);
User jhon = new User("Jhon", chatRoom);
kevin.sendMsg("Hello, Jhon!");
jhon.sendMsg("Hi, Kevin!");
}

运行结果:

二、中介者模式在源码中的体现

JDK中的Timer类
打开Timer类的结构图我们发现Timer类中有很多schedule()方法重载,如下图:

任意点开其中的一个方法,发现所有的方法最终都是调用了私有的sched()方法,看下它们的源码:

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
 private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}

不管什么样的任务都加入到一个队列中顺序执行。把这个队列中所有的对象称之为“同事”。同事之间通过Timer来协调完成,Timer承担了中介者的角色。

三、中介者模式的优缺点

优点

  • 减少类间依赖,将多对多依赖转化成了一对多,降低了类间耦合;
  • 类间各司其职,符合迪米特法则。

缺点

  • 中介者模式中将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。

架构师内功心法,参与富文本编辑器开发的备忘录模式详解

备忘录模式(Memento Pattern)又称为快照模式(Snapshot Pattern)或者令牌模式(Token Pattern),是指在不破坏封装的前提下,捕获一个内部状态,并在对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

在软件系统中,备忘录模式为我们提供了一种“后悔药”的机制,它通过存储系统各个历史状态的快照,使得我们可以在任意时刻将系统回滚到某一个历史状态。

一、备忘录模式的应用场景

我们机会天天都在使用备忘录模式,比如使用Git、SVN提供一种代码版本撤回的功能。还有游戏的存档功能,通过将游戏当前进度存储到本地文件系统或数据库中,使得下次继续游戏时,玩家可以从之前的位置继续进行。


备忘录模式适用于以下两个场景:

  • 需要保存历史快照的场景;
  • 希望在对象之外保存状态,且除了自己其它类对象无法访问状态保存具体内容。

备忘录模式主要包含三种角色:

  • 发起人角色(Orgainator):负责创建一个备忘录,记录自身需要保存的状态,具备状态回滚功能;
  • 备忘录角色(Memento):用于存储发起人的内部状态,且可以防止发起人以外的对象进行访问;
  • 备忘录管理员(Caretaker):负责存储,提供管理备忘录,无法对备忘录内容进行操作和访问。

1.1 利用压栈管理落地备忘录模式

我们在网页上写文章或者博客都使用过富文本编辑器,它会附带草稿箱、撤销等这样的功能。

下面使用代码来实现这样的功能。假设我们需要发布一篇文章,这篇文章的编辑过程需要花很长的时间,编辑的过程中会不停的撤销,保存草稿、修改。首先创建发起人角色编辑器 Editor 类:

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

private String title;

private String content;

private String imgs;

public Editor(String title, String content, String imgs) {
this.title = title;
this.content = content;
this.imgs = imgs;
}

public ArticleMemento save2Memento() {
ArticleMemento articleMemento =
new ArticleMemento(this.title, this.content, this.imgs);
return articleMemento;
}

public void undoFromMemento(ArticleMemento articleMemento) {
this.title = articleMemento.getTitle();
this.content = articleMemento.getContent();
this.imgs = articleMemento.getImgs();
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getImgs() {
return imgs;
}

public void setImgs(String imgs) {
this.imgs = imgs;
}

@Override
public String toString() {
return "Editor{" +
"title='" + title + '\'' +
", content='" + content + '\'' +
", imgs='" + imgs + '\'' +
'}';
}
}

然后创建备忘录角色 ArticleMemento 类:

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

private String title;

private String content;

private String imgs;

public ArticleMemento(String title, String content, String imgs) {
this.title = title;
this.content = content;
this.imgs = imgs;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getImgs() {
return imgs;
}

public void setImgs(String imgs) {
this.imgs = imgs;
}

@Override
public String toString() {
return "ArticleMemento{" +
"title='" + title + '\'' +
", content='" + content + '\'' +
", imgs='" + imgs + '\'' +
'}';
}
}

创建备忘录管理角色草稿箱 DraftBox 类:

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

private final Stack<ArticleMemento> STACK = new Stack<>();

public ArticleMemento getMemento() {
ArticleMemento articleMemento = STACK.pop();
return articleMemento;
}

public void addMemento(ArticleMemento articleMemento) {
STACK.push(articleMemento);
}

}

草稿箱的Stack类是Vector的一个子类,它实现了一个标准的后进先出的栈。

二、备忘录模式在源码中的体现

备忘录模式在框架源码中的应用还是比较少见的,主要还是结合具体的应用场景来使用。spring中的webfolw源码StateManageableMessageContext接口,我们来看它的源码:

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

/**
* Create a serializable memento, or token representing a snapshot of the internal state of this message context.
* @return the messages memento
*/
public Serializable createMessagesMemento();

/**
* Set the state of this context from the memento provided. After this call, the messages in this context will match
* what is encapsulated inside the memento. Any previous state will be overridden.
* @param messagesMemento the messages memento
*/
public void restoreMessages(Serializable messagesMemento);

/**
* Configure the message source used to resolve messages added to this context. May be set at any time to change how
* coded messages are resolved.
* @param messageSource the message source
* @see MessageContext#addMessage(MessageResolver)
*/
public void setMessageSource(MessageSource messageSource);
}

createMessagesMemento()创建一个消息备忘录。可以看一下实现类:

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
public class DefaultMessageContext implements StateManageableMessageContext {

private static final Log logger = LogFactory.getLog(DefaultMessageContext.class);

private MessageSource messageSource;

@SuppressWarnings("serial")
private Map<Object, List<Message>> sourceMessages = new AbstractCachingMapDecorator<Object, List<Message>>(
new LinkedHashMap<Object, List<Message>>()) {

protected List<Message> create(Object source) {
return new ArrayList<Message>();
}
};

/**
* Creates a new default message context. Defaults to a message source that simply resolves default text and cannot
* resolve localized message codes.
*/
public DefaultMessageContext() {
init(null);
}

/**
* Creates a new default message context.
* @param messageSource the message source to resolve messages added to this context
*/
public DefaultMessageContext(MessageSource messageSource) {
init(messageSource);
}

public MessageSource getMessageSource() {
return messageSource;
}

// implementing message context

public Message[] getAllMessages() {
List<Message> messages = new ArrayList<Message>();
for (List<Message> list : sourceMessages.values()) {
messages.addAll(list);
}
return messages.toArray(new Message[messages.size()]);
}

public Message[] getMessagesBySource(Object source) {
List<Message> messages = sourceMessages.get(source);
return messages.toArray(new Message[messages.size()]);
}

public Message[] getMessagesByCriteria(MessageCriteria criteria) {
List<Message> messages = new ArrayList<Message>();
for (List<Message> sourceMessages : this.sourceMessages.values()) {
for (Message message : sourceMessages) {
if (criteria.test(message)) {
messages.add(message);
}
}
}
return messages.toArray(new Message[messages.size()]);
}

public boolean hasErrorMessages() {
for (List<Message> sourceMessages : this.sourceMessages.values()) {
for (Message message : sourceMessages) {
if (message.getSeverity() == Severity.ERROR) {
return true;
}
}
}
return false;
}

public void addMessage(MessageResolver messageResolver) {
Locale currentLocale = LocaleContextHolder.getLocale();
if (logger.isDebugEnabled()) {
logger.debug("Resolving message using " + messageResolver);
}
Message message = messageResolver.resolveMessage(messageSource, currentLocale);
List<Message> messages = sourceMessages.get(message.getSource());
if (logger.isDebugEnabled()) {
logger.debug("Adding resolved message " + message);
}
messages.add(message);
}

public void clearMessages() {
sourceMessages.clear();
}

// implementing state manageable message context

public Serializable createMessagesMemento() {
return new LinkedHashMap<Object, List<Message>>(sourceMessages);
}

@SuppressWarnings("unchecked")
public void restoreMessages(Serializable messagesMemento) {
sourceMessages.putAll((Map<Object, List<Message>>) messagesMemento);
}

public void setMessageSource(MessageSource messageSource) {
if (messageSource == null) {
messageSource = new DefaultTextFallbackMessageSource();
}
this.messageSource = messageSource;
}

// internal helpers

private void init(MessageSource messageSource) {
setMessageSource(messageSource);
// create the 'null' source message list eagerly to ensure global messages are indexed first
this.sourceMessages.get(null);
}

public String toString() {
return new ToStringCreator(this).append("sourceMessages", sourceMessages).toString();
}

private static class DefaultTextFallbackMessageSource extends AbstractMessageSource {
protected MessageFormat resolveCode(String code, Locale locale) {
return null;
}
}
}

主要逻辑就相当于是给Message留一个备份,以备恢复之用。

三、备忘录模式的优缺点

优点:

  • 简化发起人职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节;
  • 提供状态回滚功能。

缺点:
消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存。

架构师内功心法,参与电商订单业务开发的状态模式详解

状态模式在生活场景中也是比较常见的。比如我们平时网购的订单状态变化,还有平时坐电梯,电梯状态的变化。

在软件开发过程中,对于某一项的操作,可能存在不同的情况。通常处理多情况问题最直接的办法就是使用if…else或者switch…case条件语句进行判断。这种做法对于复杂状态的判断天然存在弊端:判断条件语句过于臃肿,可读性较差,不具备扩展性,维度难度也很大。如果转换一下思维,将这些不同状态独立起来用各种不同的类进行表示,系统处理哪种情况,直接使用相应的状态类进行处理,消除条件判断语句,代码也更加具有层次感,且具备良好的扩展能力。

状态模式(State Pattern)也成为状态机模式(State Machine Pattern),是允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。状态模式中类的行为是由状态决定的,不同的状态下有不同的行为。其意图是让一个对象在其内部改变的时候,其行为也随之改变。状态模式的核心就是状态与行为绑定,不同的状态对应不同的行为。

一、状态模式的应用场景

状态模式适用于以下几种场景:

  • 行为随状态改变而改变场景;
  • 一个操作中含有庞大的多分支机构,并且这些分支取决于对象的状态。

状态模式主要包含三种角色:

  • 环境类角色(Context):定义客户端需要的接囗,内部维护一个当前状态实例,并负责具体状态的切换;
  • 抽象状态角色(State):定义该状态下的行为,可以有一个或多个行为;
  • 具体状态角色(ConcreteState):具体实现该状态对应的行为并且在需要的肩况下进行状态切换。

1.1 状态模式在业务场景中的应用

我们在某社区阅读文章的时候,如果觉得某篇文章写得好,就会转发、收藏并且评论。如果用户处于登录情况下,我们就可以做评论、转发、收藏这些行为。否则需要跳转到登录页面,登录之后才能执行先前的动作。那么这里涉及到的状态有两种:已登录和未登录,行为有三种:评论、转发、收藏。下面使用代码来实现这些逻辑,首先创建抽象状态角色类UserState:

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

private AppContext appContext;

public void setAppContext(AppContext appContext) {
this.appContext = appContext;
}

public abstract void forward();

public abstract void collect();

public abstract void comment(String comment);

}

创建登录状态LoginState类:

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

@Override
public void forward() {
System.out.println("转发成功!");
}

@Override
public void collect() {
System.out.println("收藏成功!");
}

@Override
public void comment(String comment) {
System.out.println("评论成功,内容是:" + comment);
}
}

接着创建未登录状态UnLoginState类:

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
public class UnLoginState extends UserState {

@Override
public void forward() {
forward2Login();
this.appContext.forward();
}

@Override
public void collect() {
forward2Login();
this.appContext.collect();
}

@Override
public void comment(String comment) {
forward2Login();
this.appContext.comment(comment);
}

private void forward2Login() {
System.out.println("跳转到登录页面!");
this.appContext.setState(this.appContext.LOGIN_STATE);
}
}

创建上下文角色AppContext类:

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

public static final UserState LOGIN_STATE = new LoginState();

public static final UserState UNLOGIN_STATE = new UnLoginState();

private UserState currentState = UNLOGIN_STATE;

{
UNLOGIN_STATE.setAppContext(this);
LOGIN_STATE.setAppContext(this);
}

public void setState(UserState state) {
this.currentState = state;
this.currentState.setAppContext(this);
}

public UserState getState() {
return this.currentState;
}

public void forward() {
this.currentState.forward();
}

public void collect() {
this.currentState.collect();
}

public void comment(String comment) {
this.currentState.comment(comment);
}

}

测试main方法:

1
2
3
4
5
6
 public static void main(String[] args) {
AppContext context = new AppContext();
context.forward();
context.collect();
context.comment("说的太好了,双手双脚给个赞������");
}

运行结果:

1.2 利用状态机实现订单状态流转控制

状态机是状态模式的一种应用,相当于上下文角色的一个升级版本。在工作流或游戏等各种系统中有大量使用,比如各种工作流引擎,它几乎是状态机的子集和实现,封装状态的变化规则。Spring提供了一个很好的解决方案。Spring的组件名称就叫StateMachine(状态机)。状态机帮助开发者简化状态控制的开发过程,让状态机结构更加层次化。下面来用Spring状态机模拟一个订单状态流转的过程。

1、pom依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>

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
public class Order {
private int id;
private OrderStatus status;
public void setStatus(OrderStatus status) {
this.status = status;
}

public OrderStatus getStatus() {
return status;
}

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

public int getId() {
return id;
}

@Override
public String toString() {
return "订单号:" + id + ", 订单状态:" + status;
}
}

3、创建订单状态枚举类和状态转换枚举类

1
2
3
4
5
6
7
/**
* 订单状态
*/
public enum OrderStatus {
// 待支付,待发货,待收货,订单结束
WAIT_PAYMENT, WAIT_DELIVER, WAIT_RECEIVE, FINISH;
}
1
2
3
4
5
6
7
/**
* 订单状态改变事件
*/
public enum OrderStatusChangeEvent {
// 支付,发货,确认收货
PAYED, DELIVERY, RECEIVED;
}

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
/**
* 订单状态机配置
*/
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatus, OrderStatusChangeEvent> {

/**
* 配置状态
* @param states
* @throws Exception
*/
public void configure(StateMachineStateConfigurer<OrderStatus, OrderStatusChangeEvent> states) throws Exception {
states
.withStates()
.initial(OrderStatus.WAIT_PAYMENT)
.states(EnumSet.allOf(OrderStatus.class));
}

/**
* 配置状态转换事件关系
* @param transitions
* @throws Exception
*/
public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderStatusChangeEvent> transitions) throws Exception {
transitions
.withExternal().source(OrderStatus.WAIT_PAYMENT).target(OrderStatus.WAIT_DELIVER).event(OrderStatusChangeEvent.PAYED)
.and()
.withExternal().source(OrderStatus.WAIT_DELIVER).target(OrderStatus.WAIT_RECEIVE).event(OrderStatusChangeEvent.DELIVERY)
.and()
.withExternal().source(OrderStatus.WAIT_RECEIVE).target(OrderStatus.FINISH).event(OrderStatusChangeEvent.RECEIVED);
}

/**
* 持久化配置
* 实际使用中,可以配合redis等,进行持久化操作
* @return
*/
@Bean
public DefaultStateMachinePersister persister(){
return new DefaultStateMachinePersister<>(new StateMachinePersist<Object, Object, Order>() {
@Override
public void write(StateMachineContext<Object, Object> context, Order order) throws Exception {
//此处并没有进行持久化操作
}

@Override
public StateMachineContext<Object, Object> read(Order order) throws Exception {
//此处直接获取order中的状态,其实并没有进行持久化读取操作
return new DefaultStateMachineContext(order.getStatus(), null, null, null);
}
});
}
}

5、添加订单状态监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component("orderStateListener")
@WithStateMachine(name = "orderStateMachine")
public class OrderStateListenerImpl{

@OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
public boolean payTransition(Message<OrderStatusChangeEvent> message) {
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderStatus.WAIT_DELIVER);
System.out.println("支付,状态机反馈信息:" + message.getHeaders().toString());
return true;
}

@OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
public boolean deliverTransition(Message<OrderStatusChangeEvent> message) {
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderStatus.WAIT_RECEIVE);
System.out.println("发货,状态机反馈信息:" + message.getHeaders().toString());
return true;
}

@OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
public boolean receiveTransition(Message<OrderStatusChangeEvent> message){
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderStatus.FINISH);
System.out.println("收货,状态机反馈信息:" + message.getHeaders().toString());
return true;
}
}

6、创建IOrderService接口

1
2
3
4
5
6
7
8
9
10
11
12
public interface IOrderService {
//创建新订单
Order create();
//发起支付
Order pay(int id);
//订单发货
Order deliver(int id);
//订单收货
Order receive(int id);
//获取所有订单信息
Map<Integer, Order> getOrders();
}

7、在Service中添加业务逻辑

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
@Service("orderService")
public class OrderServiceImpl implements IOrderService {

@Autowired
private StateMachine<OrderStatus, OrderStatusChangeEvent> orderStateMachine;

@Autowired
private StateMachinePersister<OrderStatus, OrderStatusChangeEvent, Order> persister;

private int id = 1;
private Map<Integer, Order> orders = new HashMap<>();

public Order create() {
Order order = new Order();
order.setStatus(OrderStatus.WAIT_PAYMENT);
order.setId(id++);
orders.put(order.getId(), order);
return order;
}

public Order pay(int id) {
Order order = orders.get(id);
System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试支付,订单号:" + id);
Message message = MessageBuilder.withPayload(OrderStatusChangeEvent.PAYED).setHeader("order", order).build();
if (!sendEvent(message, order)) {
System.out.println("线程名称:" + Thread.currentThread().getName() + " 支付失败, 状态异常,订单号:" + id);
}
return orders.get(id);
}

public Order deliver(int id) {
Order order = orders.get(id);
System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试发货,订单号:" + id);
if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEvent.DELIVERY).setHeader("order", order).build(), orders.get(id))) {
System.out.println("线程名称:" + Thread.currentThread().getName() + " 发货失败,状态异常,订单号:" + id);
}
return orders.get(id);
}

public Order receive(int id) {
Order order = orders.get(id);
System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试收货,订单号:" + id);
if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEvent.RECEIVED).setHeader("order", order).build(), orders.get(id))) {
System.out.println("线程名称:" + Thread.currentThread().getName() + " 收货失败,状态异常,订单号:" + id);
}
return orders.get(id);
}


public Map<Integer, Order> getOrders() {
return orders;
}


/**
* 发送订单状态转换事件
*
* @param message
* @param order
* @return
*/
private synchronized boolean sendEvent(Message<OrderStatusChangeEvent> message, Order order) {
boolean result = false;
try {
orderStateMachine.start();
//尝试恢复状态机状态
persister.restore(orderStateMachine, order);
//添加延迟用于线程安全测试
Thread.sleep(1000);
result = orderStateMachine.sendEvent(message);
//持久化状态机状态
persister.persist(orderStateMachine, order);
} catch (Exception e) {
e.printStackTrace();
} finally {
orderStateMachine.stop();
}
return result;
}
}

测试main方法:

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
public static void main(String[] args) {

Thread.currentThread().setName("主线程");

ConfigurableApplicationContext context = SpringApplication.run(Test.class,args);

IOrderService orderService = (IOrderService)context.getBean("orderService");

orderService.create();
orderService.create();

orderService.pay(1);

new Thread("客户线程"){
@Override
public void run() {
orderService.deliver(1);
orderService.receive(1);
}
}.start();

orderService.pay(2);
orderService.deliver(2);
orderService.receive(2);

System.out.println("全部订单状态:" + orderService.getOrders());

}

二、状态模式中的源码体现

状态模式的具体应用在源码中非常少见,在源码中一般只是提供一种通用的解决方案。如果一定要找,当然也是能找到的。经历千辛万苦,持续烧脑,下面我们来看一个在JSF源码中的Lifecycle类。JSF也算是一个比较经典的前端框架,那么没用过的小伙伴也没关系,我们这是只是分析一下其设计思想。在JSF中它所有页面的处理分为6个阶段,被定义在了Phaseld类中用不同的常量来表示生命周期阶段,源码如下:

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
public class PhaseId implements Comparable {
private final int ordinal;
private String phaseName;
private static int nextOrdinal = 0;
private static final String ANY_PHASE_NAME = "ANY";
public static final PhaseId ANY_PHASE = new PhaseId("ANY");
private static final String RESTORE_VIEW_NAME = "RESTORE_VIEW";
public static final PhaseId RESTORE_VIEW = new PhaseId("RESTORE_VIEW");
private static final String APPLY_REQUEST_VALUES_NAME = "APPLY_REQUEST_VALUES";
public static final PhaseId APPLY_REQUEST_VALUES = new PhaseId("APPLY_REQUEST_VALUES");
private static final String PROCESS_VALIDATIONS_NAME = "PROCESS_VALIDATIONS";
public static final PhaseId PROCESS_VALIDATIONS = new PhaseId("PROCESS_VALIDATIONS");
private static final String UPDATE_MODEL_VALUES_NAME = "UPDATE_MODEL_VALUES";
public static final PhaseId UPDATE_MODEL_VALUES = new PhaseId("UPDATE_MODEL_VALUES");
private static final String INVOKE_APPLICATION_NAME = "INVOKE_APPLICATION";
public static final PhaseId INVOKE_APPLICATION = new PhaseId("INVOKE_APPLICATION");
private static final String RENDER_RESPONSE_NAME = "RENDER_RESPONSE";
public static final PhaseId RENDER_RESPONSE = new PhaseId("RENDER_RESPONSE");
private static final PhaseId[] values;
public static final List VALUES;

private PhaseId(String newPhaseName) {
this.ordinal = nextOrdinal++;
this.phaseName = null;
this.phaseName = newPhaseName;
}

public int compareTo(Object other) {
return this.ordinal - ((PhaseId)other).ordinal;
}

public int getOrdinal() {
return this.ordinal;
}

public String toString() {
return null == this.phaseName ? String.valueOf(this.ordinal) : this.phaseName + ' ' + this.ordinal;
}

static {
values = new PhaseId[]{ANY_PHASE, RESTORE_VIEW, APPLY_REQUEST_VALUES, PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES, INVOKE_APPLICATION, RENDER_RESPONSE};
VALUES = Collections.unmodifiableList(Arrays.asList(values));
}
}

那么这些状态的切换都在Lifecycle的execute()方、去中进行。其中会传一个参数FacesContext对象,最终所有的状态都被FacesContext保存。在此呢,我们就不做继续深入的分析。

三、状态模式的相关模式

3.1 状态模式与责任链模式

状态模式和责任链模式都能消除if分支过多的问题。但某些情况下,状态模式中的状态可以理解为责任,那么这种情况下,两种模式都可以使用。

从定义来看,状态模式强调的是一个对象内在状态的改变,而责任链模式强调的是外部节点对象间的改变。

从其代码实现上来看,他们间最大的区别就是状态模式各个状态对象知道自己下一个要进入的状态对象而责任链模式并不清楚其下一个节点处理对象,因为链式组装由客户端负责。

3.2 状态模式与策略模式

状态模式和策略模式的UML类图架构几乎完全一样,但他们的应用场景是不一样的。策略模式多种算法行为择其一都能满足,彼此之间是独立的用户可自行更换策略算法,而状态模式各个状态间是存在相互关系的,彼此之间在一定条件下存在自动切换状态效果,且用户无法指定状态,只能设置初始状态。

四、状态模式的优缺点

优点:

  • 结构清晰:将状态独立为类,消除了冗余的if…else或switch…case语句,使代码更加简洁,提高系统可维护性;
  • 将状态转换显示化通常的对象内部都是使用数值类型来定义状态,状态的切换是通过賦值进行表现,不够直观,而使用状态类,在切换状态时,是以不同的类进行表示,转换目的更加明确;
  • 状态类职责明确且具备扩展性。

缺点:

  • 类膨胀:如果一个事物具备很多状态,则会造成状态类太多;
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱;
  • 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。