MyBatis官网:https://mybatis.org/mybatis-3/zh/index.html
MyBatis的测试代码如下:
//解析mybatis-config.xml配置文件和Mapper文件,保存到Configuration对象中
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
//通过SqlSessionFactory创建SqlSession对象
SqlSession session = sqlSessionFactory.openSession();
//生成Mapper接口的代理类
BlogMapper mapper = session.getMapper(BlogMapper.class);
//调用代理类的方法
Blog blog = mapper.selectBlogById(2,"Bally Slog");
解析配置文件
执行第一行代码时,会去加载并解析 mybatis-config.xml 配置文件,配置文件的每一个节点在Configuration类里的都有一个属性与之对应,会把解析的值存放到对应的属性中,然后根据配置文件配置的Mapper,去解析Mapper文件。
SqlSession
执行第二行代码时,SqlSessionFactory当然是负责创建SqlSession了,具体就是DefaultSqlSessionFactory负责DefaultSqlSession的创建。最终也就是执行下面这段代码创建了DefaultSqlSession。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//获取Environment对象,也就是mybatis-config.xml中的<environment>节点
final Environment environment = configuration.getEnvironment();
//获取TransactionFactory,创建 Transaction 对象
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//创建执行器
final Executor executor = configuration.newExecutor(tx, execType);
//上面这些都是为创建DefaultSqlSession而准备的
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();
}
}
SqlSession是暴露给客户端操作数据库的接口,调用者无需了解内部复杂的执行流程,就可以轻松操作数据库执行CRUD操作。这应该就是门面模式了。
Mapper接口是如何被调用的?
执行第三行代码时,就会生成一个Mapper接口的代理类,为什么要生成代理类呢?当然是增强方法了。但由于Mapper接口并没有实现类,所以也谈不上增强,这里主要是为了调用Mapper接口后,接下来的流程能够继续执行。
动态代理
MyBatis 中大量使用了动态代理,先来看一个动态代理的例子 - 动态地给房东生成一个中介,代替房东出租房屋。
// 1.定义接口,JDK动态代理支持接口类型,所以先得准备一个接口
public interface IHouse {
//房屋出租
void chuZuHouse();
}
// 2.准备一个实现类,也就是房东
public class HouseOwner implements IHouse{
@Override
public void chuZuHouse() {
System.out.println("我有房子要出租");
}
}
// 3.动态生成一个代理类,也就是中介
public class TestDynamicProxy {
public static void main(String[] args) {
// ClassLoader loader:类加载器,这个类加载器他是用于加载代理对象字节码的,写的是被代理对象的类加载器,也就是和被代理对象使用相同的类加载器,固定写法,代理谁用谁的
// Class<?>[] interfaces:它是用于让代理对象和被代理对象有相同的方法。写被代理对象实现的接口的字节码,固定写法代理谁就获取谁实现的接口
// InvocationHandler h:用于提供增强的代码。它是让我们写如何代理,写InvocationHandlder接口的实现类
IHouse houseOwner = new HouseOwner();
IHouse houseAgent = (IHouse)Proxy.newProxyInstance(HouseOwner.class.getClassLoader(), HouseOwner.class.getInterfaces(), new InvocationHandler() {
/**
* @param proxy 代理对象的引用,在方法中如果想使用代理对象,就可以使用它,一般不用
* @param method 表示代理对象执行的被代理对象的方法
* @param args 表示执行当前被代理对象的方法所需要的参数
* @return 返回值: 和被代理对象有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//method.invoke(proxy, args);不能这样写,否则会出现自己调自己
Object invoke = method.invoke(houseOwner, args);
System.out.println("我是房产中介,让我来替你租吧");
return invoke;
}
});
//调用代理类的方法,就能执行invoke方法里的代码逻辑了
houseAgent.chuZuHouse();
}
}
上面就是很简单的动态代理,通过中介调用chuZuHouse()方法时,就会执行增强的逻辑,动态代理的好处就是动态生成代理类,假如有大量的类的方法需要被增强,如果用静态代理,有多少个类就得写多少个代理类,而使用动态代理只需实现InvocationHandler接口,不需要自己动手写代理类。
那如果不需要调用被代理对象的方法,那就可以不要实现类了,这就是MyBatis动态代理Mapper接口的原理。比如:
public interface IHouse {
//房屋出租
void chuZuHouse();
}
public class Test2DynamicProxy {
public static void main(String[] args) {
//由于不需要实现类,所以只需要定义接口就可以了
IHouse houseAgent = (IHouse)Proxy.newProxyInstance(IHouse.class.getClassLoader(), new Class[]{IHouse.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//此处不需要实现类(房东)
System.out.println("我是房产中介,让我来替你租吧");
return null;
}
});
//调用代理类的方法,就能执行invoke方法里的代码逻辑了
houseAgent.chuZuHouse();
}
}
生成Mapper接口的代理类
有了动态代理的基础,接下来看看 BlogMapper mapper = session.getMapper(BlogMapper.class);做了什么?
//Configuration.java
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//从Configuration中根据类的全类名获取
return mapperRegistry.getMapper(type, sqlSession);
}
//MapperRegistry.java
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 从knownMappers中根据类的全类名获取
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.java
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
//此处就是Mapper接口被动态代理的关键,返回代理类对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
从以上代码可以看到,首先会从Configuration中根据类的全类名获取,然后交给了MapperRegistry,该类中有一个名叫knownMappers的map,保存了需要的Mapper,那什么时候存进去的呢?答案是在解析Mapper文件之后,就会把当前Mapper文件对应的Mapper接口封装到MapperProxyFactory对象,存到名叫knownMappers的HashMap中。然后进入MapperProxyFactory类可以看到,最终是通过Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);生成了Mapper的代理类。
有了代理类对象,就可以调用Mapper接口中的方法了。
调用Mapper接口方法
接下来看看 Blog blog = mapper.selectBlogById(2,"Bally Slog");这句代码做了什么?我们先分析一下:
1、mapper xml文件中写的参数是#{XXX},执行SQL前,参数肯定得处理;
2、返回了最终的执行结果,说明对数据库进行了操作,肯定会有JDBC相应的代码,比如PreparedStatement对象;
3、在查询的时候,MyBatis是有缓存功能的,所以还会对一级二级缓存进行处理;
4、MyBatis直接返回了我们需要的对象,所以还会对数据库的返回结果进行解析;
//MapperProxy.java
//调用代理类的方法,也就会执行InvocationHandler接口的实现类(MapperProxy)的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//如果method.getDeclaringClass()是Object,则调用method.invoke(this, args);不知道干嘛的,不过对正常流程无影响
//method.getDeclaringClass()正常情况下是Mapper接口的Class,这里是yyb.useful.start01.BlogMapper
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
//如果methodCache里面存在method对应的MapperMethod,则取出,否则会创建并存放到methodCache中,并返回
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
通过以上代码,又把接下来的流程交给了MapperMethod来处理了,接下来看看MapperMethod做了什么?
//MapperMethod.java
/**
* 解析参数和返回值(此处的返回值已经是java类型了),具体执行交给sqlSession来做
*/
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);
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;
}
可以看到,MapperMethod的功能有三个:
1、获取SQL的类型,用来判断接下来该调用CRUD中的哪一个,类型判断主要是借助于MappedStatement,MappedStatement里面封装了<select|update|insert|delete>标签对应的信息,其中有一个SqlCommandType枚举类型,就是用来保存SQL的类型的。
2、解析参数,该功能由ParamNameResolver类来完成,逻辑如下:
public class ParamNameResolver {
private static final String GENERIC_NAME_PREFIX = "param";
/**
* key是参数的索引,值是参数的名称
*
* 如果用@Param指定了名称,名称就从@Param中获取
* 如果未指定,使用参数的索引。注意,当方法具有特殊参数(RowBounds,ResultHandler)时,此索引可能与实际索引不同
* 比如:
* aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}
* aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}
* aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}
*/
private final SortedMap<Integer, String> names;
private boolean hasParamAnnotation;
public ParamNameResolver(Configuration config, Method method) {
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
//如果参数是RowBounds,ResultHandler时,跳过
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
//如果用@Param指定了名称,名称就从@Param中获取
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
//使用方法签名中的名称作为语句参数名称。 要使用该特性,项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1)
//编译不加上 -parameters,则获取的值是arg0
name = getActualParamName(method, paramIndex);
}
//如果未指定@Param,使用参数的索引
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
/**
* 单个参数且没加@Param注解,直接返回值
* 多个参数除了使用命名规则(可能是argX,真实的name,或者索引)来命名外,此方法还添加了通用名称(param1, param2,...)
*/
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
//没有参数,返回null
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
//只有一个参数,且没加@Param注解,直接返回值
return args[names.firstKey()];
} else {
//只要参数个数大于1,或者参数只有1个但加了@Param,返回hashMap
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//把names中的数据添加到map中(可能是argX,真实的name,或者索引)
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
//添加(param1, param2, ...)到map中
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
}
3、解析返回值类型
解析返回值(这里只是简单的处理,比如把int转换Integer,Long,Boolean等,实际的ResultSet解析过程有相应的类进行处理),具体执行又交给SqlSession来做,所以本质上,以下两种调用方式其实没什么区别,只不过mapper的方式多了一层转换,但对开发中更友好,避免写错类名的情况。
Blog blog = mapper.selectBlogById(2);
Blog blog1 = session.selectOne("yyb.useful.start01.BlogMapper.selectBlogById", 2);