目录
- 1、环境搭建
- 2、动态代理类的生成
- 3、MapperProxy 增强 mapper 接口
- 3.1、cachedInvoker(method)
- 3.2、MapperMethod
- 3.2.1、参数转换 method.convertArgsToSqlCommandParam(args)
- 3.2.2、调用 sqlSession 的方法获取结果
- 4、小结
- 总结
使用过 mybatis 框架的小伙伴们都知道,mybatis 是个半 orm 框架,通过写 mapper 接口就能自动实现数据库的增删改查,但是对其中的原理一知半解,接下来就让我们深入框架的底层一探究竟
1、环境搭建
首先引入 mybatis 的依赖,在 resources 目录下创建 mybatis 核心配置文件 mybatis-config.xml
PUBLIC "-//mybatis.org//DTD Config.0//EN" | |
"https://mybatis.org/dtd/mybatis--config.dtd"> | |
<configuration> | |
<!-- 环境、事务工厂、数据源 --> | |
<environments default="dev"> | |
<environment id="dev"> | |
<transactionManager type="JDBC"/> | |
<dataSource type="UNPOOLED"> | |
<property name="driver" value="org.apache.derby.jdbc.EmbeddedDriver"/> | |
<property name="url" value="jdbc:derby:db-user;create=true"/> | |
</dataSource> | |
</environment> | |
</environments> | |
<!-- 指定 mapper 接口--> | |
<mappers> | |
<mapper class="com.myboy.demo.mapper.user.UserMapper"/> | |
</mappers> | |
</configuration> |
在 com.myboy.demo.mapper.user 包下新建一个接口 UserMapper
public interface UserMapper { | |
UserEntity getById(Long id); | |
void insertOne(@Param("id") Long id, @Param("name") String name, @Param("json") List<String> json); | |
} |
在 resources 的 com.myboy.demo.mapper.user 包下创建 UserMapper.xml
PUBLIC "-//mybatis.org//DTD Mapper.0//EN" | |
"https://mybatis.org/dtd/mybatis--mapper.dtd"> | |
<mapper namespace="com.myboy.demo.mapper.user.UserMapper"> | |
<select id="getById" resultType="com.myboy.demo.db.entity.UserEntity"> | |
select * from demo_user where id = #{id} | |
</select> | |
<insert id="insertOne"> | |
insert into demo_user (id, name, json) values (#{id}, #{name}, #{json}) | |
</insert> | |
</mapper> |
创建 main 方法测试
try(InputStream in = Resources.getResourceAsStream("com/myboy/demo/sqlsession/mybatis-config.xml")){ | |
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in); | |
sqlSession = sqlSessionFactory.openSession(); | |
UserMapper mapper = sqlSession.getMapper(UserMapper.class); | |
UserEntity userEntity = mapper.getById(L); | |
System.out.println(userEntity); | |
sqlSession.close(); | |
}catch (Exception e){ | |
e.printStackTrace(); | |
} |
2、动态代理类的生成
🤔 通过上面的示例,我们需要思考两个问题:
- mybatis 如何生成 mapper 的动态代理类?
- 通过 sqlSession.getMapper 获取到的动态代理类是什么内容?
通过查看源码,sqlSession.getMapper() 底层调用的是 mapperRegistry 的 getMapper 方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { | |
// sqlSessionFactory build 的时候,就已经扫描了所有的 mapper 接口,并生成了一个 MapperProxyFactory 对象 | |
// 这里根据 mapper 接口类获取 MapperProxyFactory 对象,这个对象可以用于生成 mapper 的代理对象 | |
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 接口在解析时会对应生成一个 MapperProxyFactory,保存到 knownMappers 中,mapper 接口的实现类(也就是动态代理类)通过这个 MapperProxyFactory 生成,mapperProxyFactory.newInstance(sqlSession) 代码如下:
/** | |
* 根据 sqlSession 创建 mapper 的动态代理对象 | |
* @param sqlSession sqlSession | |
* @return 代理类 | |
*/ | |
public T newInstance(SqlSession sqlSession) { | |
// 创建 MapperProxy 对象,这个对象实现 InvocationHandler 接口,里面封装类 mapper 动态代理方法的执行的核心逻辑 | |
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); | |
return newInstance(mapperProxy); | |
} | |
protected T newInstance(MapperProxy<T> mapperProxy) { | |
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); | |
} |
代码一目了然,通过 jdk 动态代理技术创建了 mapper 接口的代理对象,其 InvocationHandler 的实现是 MapperProxy,那么 mapper 接口中方法的执行,最终都会被 MapperProxy 增强
3、MapperProxy 增强 mapper 接口
MapperProxy 类实现了 InvocationHandler 接口,那么其核心方法必然是在其 invoke 方法内部
/** | |
* 所有 mapper 代理对象的方法的核心逻辑 | |
*/ | |
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { | |
try { | |
// 如果执行的方法是 Object 类的方法,则直接反射执行 | |
if (Object.class.equals(method.getDeclaringClass())) { | |
return method.invoke(this, args); | |
} else { | |
//、根据method创建方法执行器对象 MapperMethodInvoker,用于适配不同的方法执行过程 | |
//、执行方法逻辑 | |
return cachedInvoker(method).invoke(proxy, method, args, sqlSession); | |
} | |
} catch (Throwable t) { | |
throw ExceptionUtil.unwrapThrowable(t); | |
} | |
} |
3.1、cachedInvoker(method)
由于 jdk8 对接口增加了 default 关键字,使接口中的方法也可以有方法体,但是默认方法和普通方法的反射执行方式不同,需要用适配器适配一下才能统一执行,具体代码如下
/** | |
* 适配器模式,由于默认方法和普通方法反射执行的方式不同,所以用 MapperMethodInvoker 接口适配下 | |
* DefaultMethodInvoker 用于执行默认方法 | |
* PlainMethodInvoker 用于执行普通方法 | |
*/ | |
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { | |
try { | |
return MapUtil.computeIfAbsent(methodCache, method, m -> { | |
// 返回默认方法执行器 DefaultMethodInvoker | |
if (m.isDefault()) { | |
try { | |
if (privateLookupInMethod == null) { | |
return new DefaultMethodInvoker(getMethodHandleJava(method)); | |
} else { | |
return new DefaultMethodInvoker(getMethodHandleJava(method)); | |
} | |
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | |
| NoSuchMethodException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
// 返回普通方法执行器,只有一个 invoke 执行方法,实际上就是调用 MapperMethod 的执行方法 | |
else { | |
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); | |
} | |
}); | |
} catch (RuntimeException re) { | |
Throwable cause = re.getCause(); | |
throw cause == null ? re : cause; | |
} | |
} |
如果判定执行的是接口的默认方法,则原始方法封装成 DefaultMethodInvoker,这个类的 invoke 方法就是利用反射调用原始方法,没什么好说的
如果是普通的接口方法,则将方法封装成封装成 MapperMethod,然后再将 MapperMethod 封装到 PlainMethodInvoker 中,PlainMethodInvoker 没什么好看的,底层的执行方法还是调用 MapperMethod 的执行方法,至于 MapperMethod,咱们放到下一章来看
3.2、MapperMethod
首先看下构造方法
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { | |
// 通过这个 SqlCommand 可以拿到 sql 类型和sql 对应的 MappedStatement | |
this.command = new SqlCommand(config, mapperInterface, method); | |
// 包装了 mapper 接口的一个方法,可以拿到方法的信息,比如方法返回值类型、返回是否集合、返回是否为空 | |
this.method = new MethodSignature(config, mapperInterface, method); | |
} |
代码里的注释写的很清楚了,MapperMethod 构造方法创建了两个对象 SqlCommand 和 MethodSignature
mapper 接口的执行核心逻辑在其 execute() 方法中:
/** | |
* 执行 mapper 方法的核心逻辑 | |
* @param sqlSession sqlSession | |
* @param args 方法入参数组 | |
* @return 接口方法返回值 | |
*/ | |
public Object execute(SqlSession sqlSession, Object[] args) { | |
Object result; | |
switch (command.getType()) { | |
case INSERT: { | |
// 参数处理,单个参数直接返回,多个参数封装成 map | |
Object param = method.convertArgsToSqlCommandParam(args); | |
// 调用 sqlSession 的插入方法 | |
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()) { | |
// 方法返回值为 void,但是参数里有 ResultHandler | |
executeWithResultHandler(sqlSession, args); | |
result = null; | |
} else if (method.returnsMany()) { | |
// 方法返回集合 | |
result = executeForMany(sqlSession, args); | |
} else if (method.returnsMap()) { | |
// 方法返回 map | |
result = executeForMap(sqlSession, args); | |
} else if (method.returnsCursor()) { | |
// 方法返回指针 | |
result = executeForCursor(sqlSession, args); | |
} else { | |
// 方法返回单个对象 | |
// 将参数进行转换,如果是一个参数,则原样返回,如果多个参数,则返回map,key是参数name(@Param注解指定 或 arg、arg1 或 param1、param2 ),value 是参数值 | |
Object param = method.convertArgsToSqlCommandParam(args); | |
// selectOne 从数据库获取数据,封装成返回值类型,取出第一个 | |
result = sqlSession.selectOne(command.getName(), param); | |
// 如果返回值为空,并且返回值类型是 Optional,则将返回值用 Optional.ofNullable 包装 | |
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 方法来看,他只做了两件事
- 参数转换
- 调用 sqlSession 对应的 insert 方法
3.2.1、参数转换 method.convertArgsToSqlCommandParam(args)
在 mapper 接口中,假设我们定义了一个 user 的查询方法
List<User> find(@Param("name")String name, @Param("age")Integer age)
在我们的 mapper.xml 中,写出来的 sql 可以是这样的:
select * from user where name = #{name} and age > #{age}
当然不使用 @Param 注解也可以的,按参数顺序来
select * from user where name = | |
或 | |
select * from user where name = |
因此如果要通过占位符匹配到具体参数,就要将接口参数封装成 map 了,如下所示
{arg=12, arg0="abc", param1="abc", param2=12} | |
或 | |
{name="abc", age=, param1="abc", param2=12} | |
复制代码 |
这里的这个 method.convertArgsToSqlCommandParam(args) 就是这个作用,当然只有一个参数的话就不用转成 map 了, 直接就能匹配
3.2.2、调用 sqlSession 的方法获取结果
真正要操作数据库还是要借助 sqlSession,因此很快就看到了 sqlSession.insert(command.getName(), param) 方法的执行,其第一个参数是 statement 的 id,就是 mpper.xml 中 namespace 和 insert 标签的 id的组合,如 com.myboy.demo.mapper.MoonAppMapper.getAppById,第二个参数就是上面转换过的参数,至于 sqlSession 内部处理逻辑,不在本章叙述范畴
sqlSession 方法执行完后的执行结果交给 rowCountResult 方法处理,这个方法很简单,就是将数据库返回的数据处理成接口返回类型,代码很简单,如下
private Object rowCountResult(int rowCount) { | |
final Object result; | |
if (method.returnsVoid()) { | |
result = null; | |
} else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) { | |
result = rowCount; | |
} else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) { | |
result = (long) rowCount; | |
} else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) { | |
result = rowCount >; | |
} else { | |
throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType()); | |
} | |
return result; | |
} |
4、小结
到目前为止,我们已经搞清楚了通过 mapper 接口生成动态代理对象,以及代理对象调用 sqlSession 操作数据库的逻辑,我总结出执行逻辑图如下: