今天说说MyBatis3
和iBatis2
。大家现在正常的项目都应该使用MyBatis3
了。动态代理的Mapper
机制,只要考虑3个要素(xxxMapper.java
, xxxMapper.xml
, xxxPojo.java
),完全面向对象的开发方式,用起来相当爽。更重要的是,还有Mybatis Generator
工具可以自动生成DAO
层。
在MyBatis3
之前还有iBatis2
,有些比较年轻的程序员可能都没有用过这个东西。有些比较老的工程可能还在用,我之前在某银行的时候就用iBatis2
,写到DAO
层就想睡觉。吐槽下iBatis2
开发恶心的地方。
- 表的映射配置,表很大,如果有20个字段。都得逐个copy(调试的时候就怕哪个字段错了)
sql
的id
是随便起名字的,表与表之前也没有严格的sql范围划分。- 写调用代码时是面向过程的,
queryForObjec
t出来的对象需要强制转换。 java
代码中各种硬编码- 大家为了当下省事,在调用时常常使用
map
进,map
出。特别难看
iBatis2的示意代码
public Student findStudentById(int studentId) {
SqlMapClient sqlMapClient = MyBatisUtil.getSqlSqlMapClient();
SqlMapSession sqlMapSession = sqlMapClient.openSession();
try {
Student student = (Student) sqlMapSession.queryForObject("student.queryByPrimaryKey", studentId);
} finally {
sqlMapSession.close();
}
}
MyBatis的示意代码
public Student findStudentById(int studentId) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
try {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.queryByPrimaryKey(studentId);
return student;
} finally {
sqlSession.close();
}
}
对比之后会发现MyBatis3
是面向对象的,使用Mapper
机制,可以使得表之前的sql
有一定的隔离。入参和出餐都可以尽量使用面向对象的方式。硬编码也没有了。
苦逼程序员在代码还得写,ibatis2
还得用,怎么办?能不能在ibatis2
基础上做一个适配层,使得我们在业务代码中可以像使用MyBatis3
一样面向对象的方式去使用ibatis2
呢?
答案当然是可以,要不然我也不会写这文章了。不仅可以像MyBatis3
一样调用,连MyBatis Generator
的自动生成器也可以拿过来改吧改吧适配iBatis2
。下面我们来详细说说。
要干此事的前提是,要先了解MyBatis3
在Mapper
映射部分的原理。
MyBatis3 源代码分析
Mapper
机制从流程上分为3个步骤
1.启动时做初始化动作
2.运行时使用getMapper
方法生成动态代理
3.调用sql
执行过程(这部分的核型实际与iBatis
差不多)
Mapper机制初始化
系统启动首先会初始化Configuration
,我们从Configuration
的初始化位置开始看。请先找到XMLConfigBuilder
类。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
这段方法,是xml
解析过程。重点关注parseConfiguration
。
private void parseConfiguration(XNode root) {
try {
// 此处省略部分代码
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
继续跟进mapperElement
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 此处省略部分代码
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
继续跟进configuration.addMapper(mapperInterface);一路可以跟进到MapperRegistry
类
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
这个方法是要加载某个包下面的所有xxxMapper.java
(也就是我们所写的比如UserMapper.java
,这种DAO
行为接口),为后续运行时生成动态代理类做准备。(我们在业务代码中常写的sqlSession.getMapper
方法,就是在生成动态代理)
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// knownMappers<Mapper接口类型, Mapper代理的工场对象>
// 初始化时,会把这些东西缓存起来。为后续生成动态代理做准备。
knownMappers.put(type, new MapperProxyFactory<T>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
关注下knownMappers
,这里面的key
和value
分别是<Mapper接口类型, Mapper代理的工场对象>
初始化时,会把这些东西缓存起来。为后续生成动态代理做准备。
使用getMapper方法生成动态代理
还是在MapperRegistry
类中,调用getMapper
方法生成动态代理类。关于动态代理不太熟悉的同学,可以相关设计模式,这个模式非常重要。
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);
}
}
再来追踪下MapperProxyFactory
,我把其中不太重要的部分去掉了
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
// 去掉了一些不太重要的部分代码
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 实际使用了JDK的动态代理,接口类型就是初始化时候放进knownMappers中的某一个。
// 具体类型是在getMapper的时候传入的。
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] {
mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
实际使用了JDK
的动态代理,接口类型就是初始化时候放进knownMappers
中的某一个。具体类型是在getMapper
的时候传入的。具体我们再回顾下上面那个MyBatis的调用案例。
MyBatis的示意代码
public Student findStudentById(int studentId) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
try {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.queryByPrimaryKey(studentId);
return student;
} finally {
sqlSession.close();
}
}
从这边可以看到。在调用getMapper
时,会传入StudentMapper
接口的类型。MyBatis
会为这个接口生成动态代理类。下面我们看看这个动态代理实际执行了什么操作。找到MapperProxy类
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class