(原创)Android Binder设计与实现 - 实现篇(1)
本文属于原创作品,转载请注明出处并放于明显位置,原文地址:http://www.cnblogs.com/albert1017/p/3849585.html
前言
在学习Android的Binder机制时,看了http://blog.csdn.net/universus/article/details/6211589这篇文章(读本文前最好读一下),觉得写得非常棒,可惜只有设计篇,都几年了还没有实现篇,就想尝试完成这个工作,虽然可能没有universus写得那么好,但也希望能对同在学习Android的Binder机制的人有所帮助。同时如果文中如果有什么理解错误请及时指出,欢迎大家交流。
可以参考的资料:杨丰盛老师的《Android技术内幕》,邓平凡老师的《深入理解Android》以及罗升阳老师的相关博客或书籍(http://blog.csdn.net/luoshengyang/article)中关于Binder的部分。
1、概述
Android中基于binder的IPC,其本质都是通过文件操作与binder驱动交互实现的。在通信过程中,每个参与通信的进程都会在内核存在对应的数据,这些数据是进程在和驱动打交道,如执行fopen或者ioctl时,由binder驱动创建与维护。当然,与binder驱动直接交互实现IPC比较麻烦,故Android帮我们封装成了Binder Adapter,主要包括IPCThreadState和ProcessState相关的部分。我们平时的Service都是通过Binder Adapter层间接操作驱动的。我们首先抛开Binder Adapter层,看直接与binder驱动交互需要怎么做,然后在这个基础上分析Android是怎么通过Binder Adapter层帮我们封装的。
同时,我们说binder通信是一种支持CS架构的IPC,是因为binder从驱动级别就保存有一个进程是否处于循环监听状态,因为CS架构的基本逻辑就是Server处于循环监听状态,等待Client的请求并响应其请求,另外在Binder中还有类似于“会话”的概念,实现同步通信,在驱动层对一次会话进行了支持,这也是一种对CS架构的支持。
当然,我们还得意识到,Android的binder机制在应用层看到到的是RPC机制,即应用层的业务逻辑已经与IPC绑定了,要在IPC的基础上实现RPC,那么在客户端调用远程方法后,Binder机制会将该方法转换为对应的约定编号,再加上参数,作为IPC传递的内容,服务器端收到IPC消息后,将获取函数参数,同时按约定将编号转化为对应的服务端方法的调用,然后再将调用方法的结果转化为IPC的通信内容,然后传递给客户端,从而实现RPC。
到了RPC层其实就和每个Service的业务逻辑挂钩了,我们先抛开业务逻辑,只关注IPC通信的过程,看看底层数据是怎么通过binder驱动传递的,然后在我们搞清楚这写机制以后,再看Android是如何在IPC通信的基础上实现RPC的。
我们可以将网络七层协议模型或者TCP/IP参考模型部分与这里的Binder机制进行对比理解,我们可以认为共享内存实现了物理层,从物理上解决传输问题;而通过binder_node实现了网络层,handle就好比IP地址,通过handle来从客户端进程找到对应的服务端进程,就好比从一个IP发数据包然后找到对应的目的IP,当然,由于客户端直接记住IP地址有困难,就发明了DNS,查询URL转换为IP地址来访问,而binder中ServiceManager就是DNS服务器,实现服务名称到对应handle的转换,再往上就是RPC调用逻辑了,这就与网络模型中的应用层对应了,实现了一个具体服务的远程调用。
2、binder相关的内存模型
图1
如图1,每个使用Binder的进程,在内核空间中都存在binder_proc、binder_buffer资源,每个线程还存在个binder_thread与之对应,进程中会存在binder通信使用的用户内存空间块,它将与该进程对应的binder_buffer映射同一块内存(对binder设备的fd执行mmap函数就能完成这个共享内存的映射),这就是Binder通信只拷贝一次的根本原因。binder_proc和binder_thread分别保存对应的进程、线程相关的信息。
另外解释一下几个概念:
用户空间的binder本地对象:就是继承了IBinder类的那些子类的对象,如AudioFlinger类型的对象、MediaPlayerService类型的对象;
我们可以认为上图的内核空间中,驱动代码属于代码段,其余部分属于数据段,而用户空间进程通过系统调用调用驱动代码来操作数据段中的内容。
3、直接与Binder驱动交互实现IPC
3.1、ServiceManager需要进行的操作
//打开binder设备
、 bs->fd = open("/dev/binder", O_RDWR);//bs定义struct binder_state *bs;
//进行内存映射
、 bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, );
//设置本进程为ServiceManager进程
、 ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, );
unsigned readbuf[N];//在本用户进程空间开辟一段内存
//将BC_ENTER_LOOPER填充至bwr
readbuf[] = BC_ENTER_LOOPER;
bwr.write_buffer = readbuf;
//通知binder驱动本线程进入循环等待的loop状态,线程对应的binder_thread中的looper值也对应改变
、 ioctl(bs->fd, BINDER_WRITE_READ, &bwr);//bwr为binder_write_read类型,装载的命令为BC_ENTER_LOOPER
//进入循环,等待读binder消息,当有消息读到时,解析readbuf中返回的结果
for (;;) {
bwr.read_size = sizeof(readbuf); //需要读的大小
bwr.read_consumed = ;
bwr.read_buffer = (unsigned) readbuf; //将进程内存空间传过去
、 ioctl(bs->fd, BINDER_WRITE_READ, &bwr); //操作驱动读取消息
//解析readbuf中返回的结果
uint32_t cmd = *readbuf++;
switch(cmd) {//判断结果中的命令类型
...
case BR_TRANSACTION: {//处理其中的结果
//关键的关键:readbuf后面的数据中包含一个进程内存空间的指针,
//它是与本进程通信的那个远端进程传过来的数据在本进程对应的内核内存空间的地址,进行转换得到,
//转换是在kernel的drivers/staging/android/binder.c中:
//tr.data.ptr.buffer = (void *)t->buffer->data + proc->user_buffer_offset;
//其中user_buffer_offset是在建立进程的binder_proc时计算出的内核空间地址与本用户进程空间地址的偏移量
...
}
...
}
}
没错,本质上我们就只需要执行上面的5个步骤就可以使ServiceManager跑起来,下面说一下每个步骤执行后的结果:
1语句执行后,内核中将创建本进程对应的创建对应binder_proc;
2语句执行后,将创建本进程在内核中使用的buffer,即上面内核模型中的内核空间块,同时将其映射到用户内存空间块;
3语句执行后,由于是第一次调用ioctl,在执行binder_get_thread(proc)语句时,将为本线程创建对应的binder_thread结构体保存在内核,同时binder驱动将为ServiceManager建立 binder_node对象;并将binder_node赋值给binder_context_mgr_node,同时记住进程的PID,从而设置其为binder的ServiceManager进程;
4语句执行后,binder驱动将获知本进程已经进入循环监听等待状态,binder驱动中对应的线程信息结构体的表线程状态的looper变量也对应修改;
5语句就是ServiceManager进入循环,不断监听客户端的请求,没有请求时ioctl将阻塞,有请求到达是就分析请求类型,做出相应的处理。
3.2、普通Service需要进行的操作
、 fd = open("/dev/binder", O_RDWR);//打开Binder驱动
//binder驱动将返回本驱动的版本号,由于是第一次调用ioctl,在执行binder_get_thread(proc)语句时,将为本线程创建对应的binder_thread结构体保存在内核
、 ioctl(fd, BINDER_VERSION, &vers);
//通知binder驱动本服务最多可同时启动接收binder消息的线程数
、 ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
//进行内存映射
、 bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, );
//接下来就可以通过往bwr中填入一定的内容实现与其他进程的通信了
//这个地方是作为客户端向ServiceManager注册服务,下面将详细分析这里传输的bwr
构造注册到ServiceManager的binder_write_read的数据结构变量bwr
...
、 ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);
//读取结果
while(true){//刚才的一次写操作会出发多次读操作
、 ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);
}
bwr就是用户进程向binder驱动传递的数据,此处的bwr内容如下图2所示:(bwr的内容是通过读取源码时总结出来的,就没贴出分析源码的过程了,有兴趣的可以结合图2分析bwr的构建过程,这样会更容易阅读)
图2
bwr中的write_buffer指向mOut包装的结构,而mOut的buffer将指向真正传递的数据,而本次ioctl的目的是注册服务,所以本次传递的有效数据中包含了一个flat_binder_object类型对象(图中画了两个flat_binder_object是为了便于理解offsets的概念),它是用户进程中的待注册的Service的类型(它是继承自BBinder),即BBinder类型的对象,而如果IPC传递的实际数据内容中包含binder对象时,binder驱动需要知道数据中包含binder对象,并在驱动中将这个binder对象做一些处理,而接收进程接收到的binder对象就是被驱动修改后,在接收进程中也可以使用的binder。
5语句执行后:
(1)首先,由于传入内核的bwr的有效负载数据中,存在bind本地对象的封装flat_binder_object对象,内核驱动会拿出这个flat_binder_object对象,然后以flat_binder_object对象的binder成员值(为binder本地对象类型的指针类型,如AudioFlinger对象指针)查找本进程对应的内核数据结构binder_proc中的nodes红黑树,查找不到就为binder本地对象创建对应的内核数据结构binder_node(见:/drivers/staging/android/binder.c的1623行及其后面的代码),我们这里记它为snode,并找到ServiceManager的binder_node节点binder_context_mgr_node,在其中建立snode的引用。然后驱动会在内核中创建一个binder_transaction类型的事务,填充数据目标进程相关信息等,将binder_transaction的binder_work类型成员变量work(work.type=BINDER_WORK_TRANSACTION)加入到ServiceManager的todo队列,让ServiceManager去处理,这样会唤醒正在读等待的ServiceManager,而本进程会执行6语句,进入休眠状态等待ServiceManager的回复。
(2)ServiceManager就会从其binder_proc结构的todo队列中读取刚才放进去的binder_work,并由这个成员变量找到binder_transaction事务类型变量(记为t),然后创建binder_transaction_data类型变量(记为tr),填充t的内容到tr中,这个过程中也会将t中的指向内核数据的指针转换为tr中指向ServiceManager进程的用户空间的指针(tr.data.ptr.buffer = (void *)t->buffer->data + proc->user_buffer_offset),然后在内核创建的bwr的read_buffer中先填入BR_TRANSACTION这个cmd,再填入tr部分,另外这个地方还会把t压入ServiceManager本线程binder_thread的transaction_stack的栈顶,以等待用户空间的代码处理完毕后向返回驱动结果。然后返回ioctl处,将内核的bwr拷贝回用户空间,将ServiceManager的线程的looper的BINDER_LOOPER_STATE_NEED_RETURN标准去掉,然后就返回到用户空间的binder_loop的调用ioctrl处。后面就是在ServiceManager的用户空间中处理业务逻辑相关的问题了。ServiceManager会将服务端传过来的Binder本地对象的封装flat_binder_object的相关信息写到一个struct svcinfo结构体中,包括它的名称和句柄值等,然后插入到ServiceManager维护的svclist链表中去。
第5句调用ioctl后内核发生的代码调用有些繁琐,有兴趣的可以看一下,另外也可以看http://blog.csdn.net/luoshengyang/article/details/6629298的分析,分析代码如下:
//这部分代码是对驱动代码的一个提炼,与内核代码稍有差异,函数的参数可能被我替换成参数真正所指的对象了
//binder_thread_write函数中
//用户空间的bwr变成了函数参数arg,首先将其转换为void*类型
void __user *ubuf = (void __user *)arg;
//内核首先拷贝用户进程传递过来的bwr至内核空间
copy_from_user(&bwr, ubuf, sizeof(bwr));
//获取bwr中的cmd,此处为BC_TRANSACTION
get_user(cmd, (uint32_t __user *)bwr.write_buffer);
//用户空间的binder_transaction_data数据拷贝至内核空间的tr中
struct binder_transaction_data tr;
copy_from_user(&tr, bwr.write_buffer+sizeof(uint32_t), sizeof(tr));
//binder_transaction函数中
驱动代码会根据前面的bwr内容执行下面代码:
//下面的binder_context_mgr_node就是前面ServiceManager在内核空间中对应的binder_node
target_node = binder_context_mgr_node;
//获取ServiceManager的进程信息
target_proc = target_node->proc;
//获取目标队列
target_list = &target_proc->todo;
target_wait = &target_proc->wait; struct binder_transaction *t;
t = kzalloc(sizeof(*t), GFP_KERNEL);
//同步通信时将from成员设置为当前进程,即当前进行注册的Service进程执行的线程
//如果是在异步通信,即不需要接收进程返回结果,则from置为null
t->from = thread;
//记录发送者的euid(有效用户id)
//proc为发送者进程信息
t->sender_euid = proc->tsk->cred->euid;
//填入接收端的进程信息
t->to_proc = target_proc;
//填入接收端的线程信息
t->to_thread = target_thread;
t->code = tr->code;
t->flags = tr->flags;
t->priority = task_nice(current);
//这个函数将遍历ServiceManager进程在内核空间的free_buffers这个红黑树,
//找到可用控件大于传递数据size的最小的那个buffer作为最合适的buffer地址返回
t->buffer = binder_alloc_buf(target_proc, tr->data_size, tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
//填充buffer中的内容
t->buffer->allow_user_free = ;
t->buffer->debug_id = t->debug_id;
t->buffer->transaction = t;
t->buffer->target_node = target_node;
//增加ServiceManager的引用计数
binder_inc_node(target_node, , , NULL);
//拷贝图2中的有效负载数据到刚才找到的内核与ServiceManager共享的那部分内核空间的buffer中
copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size);
//拷贝图2中的binder_transaction_data中的offsets数组对象到buffer中的有效负载数据的后面
offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
copy_from_user(offp, tr->data.ptr.offsets, tr->offsets_size);
//offsets数组中实际上保存了本次IPC传输的数据中的binder对象(即图2中的flat_binder_object)的偏移地址
//由偏移地址可以计算每个flat_binder_object的地址,然后对它进行处理
//在服务端向ManagerService注册服务时,flat_binder_object对象只有一个,
//而该flat_binder_object中binder成员保存的就是服务端binder实体对象(可以看前面的分析),
//如mediaserver服务中的binder实体就是MediaPlayerService类型对象
off_end = (void *)offp + tr->offsets_size;
for (; offp < off_end; offp++) {
struct flat_binder_object *fp;
...
fp = (struct flat_binder_object *)(t->buffer->data + *offp);
//我们可以从图2看到这次传递的type为 BINDER_TYPE_BINDER
switch (fp->type) {
case BINDER_TYPE_BINDER:
case BINDER_TYPE_WEAK_BINDER: {
struct binder_ref *ref;
//参考图1,将在proc的nodes红黑树中查找用户空间的binder实体对象对应的内核的binder_node对象
//查找中会以binder实体对象指针fp->binder为索引值来查找
//由于前面没有创建,故返回NULL,调用binder_new_node函数创建binder_node节点
struct binder_node *node = binder_get_node(proc, fp->binder);
if (node == NULL) {
//binder_new_node函数会创建binder_node节点,并拷贝proc的信息到binder_node节点,同时将节点加入proc的nodes红黑树中
node = binder_new_node(proc, fp->binder, fp->cookie);
...//容错处理
node->min_priority = fp->flags & FLAT_BINDER_FLAG_PRIORITY_MASK;
node->accept_fds = !!(fp->flags & FLAT_BINDER_FLAG_ACCEPTS_FDS);
}
...
//一个进程使用其他进程的所有binder_node对象的引用都会存在该进程的binder_proc的refs_by_node红黑树中
//binder_node对象的所有引用在创建时都会加入binder_node成员refs这个链表中
//使用binder_get_ref_for_node来获得node的引用,首先会在目标进程的refs_by_node红黑树中查找
//由于是第一次向目标进程发送消息,故在目标进程中查找不到,然后会新建引用并加入目标进程的refs_by_node红黑树中,也加入node对象的refs中
ref = binder_get_ref_for_node(target_proc, node);
...
//关键点:将flat_binder_object这个binder实体的type进行了修改,由BINDER改为了HANDLE
if (fp->type == BINDER_TYPE_BINDER)
fp->type = BINDER_TYPE_HANDLE;
else
fp->type = BINDER_TYPE_WEAK_HANDLE;
//在创建node的引用ref的binder_get_ref_for_node函数中有如下代码:
//new_ref->desc=(node == binder_context_mgr_node) ? 0 : 1;//为1
//for (n = rb_first(&proc->refs_by_desc); n != NULL; n = rb_next(n)) {//以升序遍历refs_by_desc这个红黑树,查找最小的一个空闲的desc值
// ref = rb_entry(n, struct binder_ref, rb_node_desc);
// if (ref->desc > new_ref->desc)
// break;
// new_ref->desc = ref->desc + 1;
//}
//上面的proc是binder_get_ref_for_node参数传进来的target_proc,即ServiceManager的binder_proc对象,
//ServiceManager进程的refs_by_desc是按引用的desc值为索引来组织的红黑树
//找到desc空闲值就分配给new_ref,然后将new_ref插入ServiceManager的refs_by_desc
//关键点:将flat_binder_object这个binder实体的handle值改为了ref->desc,这就是给服务端的binder分配的handle值
fp->handle = ref->desc;
//fp->type == BINDER_TYPE_HANDLE为ture
//thread为要注册的服务端的线程
//这里会增加ref的强引用计数 //由于是第一次有强引用,将ref对应的node加入thread->todo队列
binder_inc_ref(ref, fp->type == BINDER_TYPE_HANDLE, &thread->todo);
...
} break;
case BINDER_TYPE_HANDLE:
case BINDER_TYPE_WEAK_HANDLE: {
...
}
}
t->need_reply = ;
t->from_parent = thread->transaction_stack;
thread->transaction_stack = t;
t->work.type = BINDER_WORK_TRANSACTION;
list_add_tail(&t->work.entry, target_list);
struct binder_work *tcomplete;
tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL);
tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
list_add_tail(&tcomplete->entry, &thread->todo);
//注:target_wait = &target_proc->wait; 即为进程的等待队列
if (target_wait)
//ServiceManager正在binder_thread_read函数中调用wait_event_interruptible处于休眠状态
//这里就会唤醒等待的ServiceManager
wake_up_interruptible(target_wait);
//唤醒ServiceManager后的代码、ServiceManager被唤醒后的操作这里就省略了,
//感兴趣的可以去看http://blog.csdn.net/luoshengyang/article/details/6629298
到第5步时,我们做的操作应该和普通客户端调用服务器端的操作是相同的,我们构造了BBinder的子类对象,即本服务对应的实现(如MediaService的实现就是MediaPlayerService类)类的对象加入到bwr中,同时bwr中的加入类型为BINDER_TYPE_BINDER的命令,当然,这个对象的载体是flat_binder_object类型对象,同时handle值也会被包装传入至binder驱动中去,binder驱动将根据这个handle值找到内核空间对应的binder_node对象,而这个对象保存了对应服务端的进程信息,这样,就找到了对应服务端的进程信息,找到服务端在内存中拥有的内核空间块,然后将客户端传来的数据拷贝到这个内核空间块,而由于服务端处于循环监听的阻塞状态,这时它就将停止阻塞,按照前面说的将内核内存块对应的地址转换为对应用户空间服务端进程内存地址(tr.data.ptr.buffer = (void *)t->buffer->data + proc->user_buffer_offset,即内核空间地址+内核空间地址与用户空间地址的偏移量),并将其作为参数返回用户空间的服务进程,服务进程根据返回参数读取刚才从另一个进程拷贝进来内核空间的那部分数据,这就实现了IPC。
这里是服务端作为ServiceManager的客户端,通过IPC向ServiceManager发送注册服务请求,让ServiceManager帮这个服务端记住它在binder驱动拥有的binder_node的handle值,而其他客户端想访问本服务端时,就先通过IPC向ServiceManager查询需要服务的handle值,然后拿着这个handle值传入binder驱动,binder驱动再根据handle值查询对应服务器进程信息,拷贝对应信息到服务进程的内核内存块,完成传递。
整个传递过程可以描述为图3所示,可以结合图2理解:
图3 客户端使用BC_TRANSACTION命令向服务端传数据的过程
需要注意的是,图中的两个binder_transaction结构是一个对象,其余的同颜色的都只是一个类型,是不同的两个对象。
(原创)Android Binder设计与实现 - 实现篇(1)的更多相关文章
- (转)Android Binder设计与实现 – 设计篇
原文地址(貌似已打不开):Android Binder设计与实现 – 设计篇 ------------------------------------------------------------- ...
- [转]Android Binder设计与实现 - 设计篇
摘要 Binder是Android系统进程间通信(IPC)方式之一.Linux已经拥有管道,system V IPC,socket等IPC手段,却还要倚赖Binder来实现进程间通信,说明Binder ...
- Android Binder设计与实现 - 设计篇
要 Binder是Android系统进程间通信(IPC)方式之一.Linux已经拥有管道,system V IPC,socket等IPC手段,却还要倚赖Binder来实现进程间通信,说明Binder具 ...
- Android Binder 设计与实现 - 设计篇
关键词 Binder Android IPC Linux 内核 驱动 摘要 Binder是Android系统进程间通信(IPC)方式之一.Linux已经拥有管道,system V IPC,socket ...
- Android Binder设计与实现 – 设计篇
摘要 Binder是Android系统进程间通信(IPC)方式之一.Linux已经拥有管道,system V IPC,socket等IPC手段,却还要倚赖Binder来实现进程间通信,说明Binder ...
- ANDROID BINDER机制浅析
Binder是Android上一种IPC机制,重要且较难理解.由于Linux上标准IPC在灵活和可靠性存在一定不足,Google基于OpenBinder的设计和构想实现了Binder. 本文只简单介绍 ...
- 【转】Android - Binder机制
以下几篇文章是分析binder机制里讲得还算清楚的 目录 1. Android - Binder机制 - ServiceManager 2. Android - Binder机制 - 普通servic ...
- android Binder机制(一)架构设计
Binder 架构设计 Binder 被设计出来是解决 Android IPC(进程间通信) 问题的.Binder 将两个进程间交互的理解为 Client 向 Server 进行通信. 如下:bind ...
- 理解 Android Binder 机制(一):驱动篇
Binder的实现是比较复杂的,想要完全弄明白是怎么一回事,并不是一件容易的事情. 这里面牵涉到好几个层次,每一层都有一些模块和机制需要理解.这部分内容预计会分为三篇文章来讲解.本文是第一篇,首先会对 ...
随机推荐
- JDBC Statement对象执行批量处理实例
以下是使用Statement对象的批处理的典型步骤序列 - 使用createStatement()方法创建Statement对象. 使用setAutoCommit()将自动提交设置为false. 使用 ...
- erlang 二进制中 拼接 变量或者函数 报错
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVsAAACiCAIAAABgR/nfAAAM5ElEQVR4nO2dzZrcKBJF9Zjd/tnOdN
- mothur trim.seqs 去除PCR引物
trim.seqs 有以下几个主要应用: 1)根据barcode 拆分序列: 2)去除PCR引物 3) 去除低质量序列 trim.seqs 在使用时必须输入一个fasta 格式的序列,然后在加至少一个 ...
- Vue中计算属性与class,style绑定
var vm=new Vue({ el:'#app', data:{ a:2, }, computed:{ //这里的b是计算属性:默认getter b:{ get:function(){ retur ...
- iOS: 查看 UIView 的视图树
在想要查看的 UIView 附近打个断点,运行,直到停在断点处,在控制台键入:po [view recursiveDescription],回车. (lldb) po [self recursiveD ...
- Java华氏转摄氏
package test; import java.util.Scanner; public class temperature { public static void main(String[] ...
- hibernate.cfg配置mysql方言
hibernate自动建表,mysql高版本不支持 type=InnoDB 中的type关键字. 应该使用engine关键字 而非type 所以需要在hibernate.cfg.xml中配置方言.否则 ...
- PS1-4
export PS2="continue->" cat ps4.sh export PS4='$0.$LINENO+ ' set -x echo "PS4 demo ...
- MyBatis的深入原理分析之1-架构设计以及实例分析
MyBatis是目前非常流行的ORM框架,它的功能很强大,然而其实现却比较简单.优雅.本文主要讲述MyBatis的架构设计思路,并且讨论MyBatis的几个核心部件,然后结合一个select查询实例, ...
- Spring MVC异常处理详解 ExceptionHandler good
@ControllerAdvice(basePackageClasses = AcmeController.class) public class AcmeControllerAdvice exten ...