使用过 mybatis 结构的小伙伴们都知道,mybatis 是个半 orm 结构,经过写 mapper 接口就能自动完成数据库的增修改查,但是对其中的原理一知半解,接下来就让咱们深化结构的底层一探究竟
1、环境搭建
首先引进 mybatis 的依赖,在 resources 目录下创立 mybatis 中心配置文件 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-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(2L);
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 署理目标的办法的中心逻辑
*/
@Override
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 {
// 1、依据method创立办法履行器目标 MapperMethodInvoker,用于适配不同的办法履行进程
// 2、履行办法逻辑
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(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(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注解指定 或 arg0、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 = #{arg0} and age > #{arg1}
或
select * from user where name = #{param1} and age > #{param2}
因而假如要经过占位符匹配到具体参数,就要将接口参数封装成 map 了,如下所示
{arg1=12, arg0="abc", param1="abc", param2=12}
或
{name="abc", age=12, 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 > 0;
} else {
throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
}
return result;
}
4、小结
到目前为止,咱们现已搞清楚了经过 mapper 接口生成动态署理目标,以及署理目标调用 sqlSession 操作数据库的逻辑,我总结出履行逻辑图如下: