Mybatis源码系列文章
手写源码(了解源码整体流程及重要组件)
Mybatis源码解析(一):环境搭建
Mybatis源码解析(二):全局配置文件的解析
Mybatis源码解析(三):映射配置文件的解析
Mybatis源码解析(四):sql语句及#{}、${}的解析
Mybatis源码解析(五):SqlSession会话的创建
Mybatis源码解析(六):缓存执行器操作流程
Mybatis源码解析(六):查询数据库主流程
Mybatis源码解析(七):Mapper代理原理
目录
- 前言
- 一、环境准备
- 二、引入映射配置文件方式
- 三、\<package name="com.xxx.mapper"/>标签的解析
- 1、通过包路径获取Mapper接口
- 2、注解方式mapper接口的解析
- 3、xml和mapper接口需要同包同名的原因?
- 四、Mapper接口代理对象的生成
- 五、代理对象执行接口方法的流程
- 总结
前言
- 文章主要围绕着如下几个点,展开源码解析:
- <package name=“com.xxx.mapper”/>;是如何进行解析的?
- sqlSession.getMapper(UserMapper.class);是如何生成的代理对象?
- mapperProxy.findById(1);是怎么完成的增删改查操作?
一、环境准备
- java代码
@Test
public void test2() throws IOException {
// 1. 通过类加载器对配置文件进行加载,加载成了字节输入流,存到内存中 注意:配置文件并没有被解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. (1)解析了配置文件,封装configuration对象 (2)创建了DefaultSqlSessionFactory工厂对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3. (1)创建事务对象 (2)创建了执行器对象cachingExecutor (3)创建了DefaultSqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. JDK动态代理生成代理对象
UserMapper mapperProxy = sqlSession.getMapper(UserMapper.class);
// 5.代理对象调用方法
User user = mapperProxy.findUserById(100);
System.out.println("MyBatis源码环境搭建成功....");
sqlSession.close();
}
- 核心配置文件sqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--第一部分:数据源配置-->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事务管理 -->
<transactionManager type="JDBC"/>
<!-- 数据库连接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis"/>
<property name="username" value="root"/>
<property name="password" value="123456789"/>
</dataSource>
</environment>
</environments>
<!--第二部分:引入映射配置文件-->
<mappers>
<!--使用相对路径注册映射文件-->
<!-- <mapper resource="mapper/UserMapper.xml"/>-->
<!--使用绝对路径注册映射文件-->
<!-- <mapper url="file:///D:\javaCode\mybatis-3.5.7\src\test\resources\mapper\UserMapper.xml"/>-->
<!--注册持久层接口-->
<!-- <mapper class="com.xc.mapper.UserMapper"/>-->
<!--注册一个包下的所有持久层接口-->
<package name="com.xc.mapper"/>
</mappers>
</configuration>
- 实体映射配置文件UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xc.mapper.UserMapper">
<select id="findUserById" parameterType="int" resultType="com.xc.pojo.User" >
SELECT id,username FROM user WHERE id = #{id}
</select>
</mapper>
二、引入映射配置文件方式
先说个结论,后续源码验证:如果不指定xml,则会在Mapper接口同目录下寻找
- 方式一:<mapper resource=“mapper/UserMapper.xml”/> 指定xml,缺点需要每个映射xml都要手动添加
- 方式二:<mapper class=“com.xc.mapper.UserMapper”/> 没有指定xml,会从同目录下寻找xml,缺点也是需要每个Mapper接口都要手动添加
- 方式三:<package name=“com.xc.mapper”/> 没有指定xml,会从同目录下寻找xml,会遍历此包下所有Mappe接口
三、<package name=“com.xxx.mapper”/>标签的解析
- 为什么单独讲这个标签?
- 这个标签是多种引入映射文件的最佳选,也是工作中必用的
- 创建代理类工厂,为以后通过Mapper接口类生成代理实现类做准备
- <package>标签在核心配置文件的<mappers>标签下
- 方式一也会创建代理类工厂,不过是在解析xml文件后,方式三是先创建代理类工厂,再解析xml
- Mybatis源码解析(三):映射配置文件的解析:这篇单独讲了<mapper resource=“mapper/UserMapper.xml”/>指定配置文件的解析
进入解析<mappers>标签方法
- <mapper>子标签的解析在Mybatis源码解析(三):映射配置文件的解析里详细讲了,这里讲下<package>子标签的解析
- 获取包名,调用addMappers方法
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 获取<mappers>标签的子标签
for (XNode child : parent.getChildren()) {
// <package>子标签
if ("package".equals(child.getName())) {
// 获取mapper接口和mapper映射文件对应的package包名
String mapperPackage = child.getStringAttribute("name");
// 将包下所有的mapper接口以及它的代理工厂对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
configuration.addMappers(mapperPackage);
} else {// <mapper>子标签
// 获取<mapper>子标签的resource属性
String resource = child.getStringAttribute("resource");
// 获取<mapper>子标签的url属性
String url = child.getStringAttribute("url");
// 获取<mapper>子标签的class属性
String mapperClass = child.getStringAttribute("class");
// 它们是互斥的
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
// 专门用来解析mapper映射文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 通过XMLMapperBuilder解析mapper映射文件
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 通过XMLMapperBuilder解析mapper映射文件
mapperParser.parse();
}
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 将指定mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
进入configuration的addMappers方法
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
- mapperRegistry对象中核心属性就是knownMappers
- key:Mapper接口的Class对象
- value:Mapper接口代理类工厂
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
...
}
进入mapperRegistry的addMappers方法
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
1、通过包路径获取Mapper接口
- resolverUtil.find方法:加载包路径下Mapper接口
- mapperSet:mapper接口Class对象集合
- addMapper方法:将Mapper接口添加到上面所说的Map集合knownMappers中
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 根据package名称,加载该包下Mapper接口文件(不是映射文件)
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 获取加载的Mapper接口
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
// 将Mapper接口添加到MapperRegistry中
addMapper(mapperClass);
}
}
resolverUtil.find方法
- getPackagePath方法:将包名-com.xc.mapper转换为资源路径-com/xc/mapper(.替换成/)
- children:获取资源路径下的资源,如下
- addIfMatching方法:将Mapper接口的Class对象添加到matches集合中
public ResolverUtil<T> find(Test test, String packageName) {
String path = getPackagePath(packageName);
try {
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
if (child.endsWith(".class")) {
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
resolverUtil.getClasses()
private Set<Class<? extends T>> matches = new HashSet<>();
...
public Set<Class<? extends T>> getClasses() {
return matches;
}
addMapper方法
- 循环遍历matches集合,将所有Mapper接口Class对象添加到knownMappers
- key:Mapper接口的Class对象
- value:Mapper接口代理类工厂
- 创建注解解析Builder,调用parse解析方法(xml的解析也包含在内)
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
// 如果Map集合中已经有该mapper接口的映射,就不需要再存储了
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 将mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
// 用来解析注解方式的mapper接口
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析注解方式的mapper接口
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
2、注解方式mapper接口的解析
- loadXmlResource方法:xml文件的解析
- parseStatement方法:原理其实和Mybatis源码解析(三):映射配置文件的解析差不多
- 不同点:xml解析<select>标签内的属性,注解解析@select注解里的属性
- 相同点:最终目的都是解析成MappedStatement对象
public void parse() {
// 获取mapper接口的全路径
String resource = type.toString();
// 是否解析过该mapper接口
if (!configuration.isResourceLoaded(resource)) {
// 先解析mapper映射文件
loadXmlResource();
// 设置解析标识
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
// 解析CacheNamespace注解
parseCache();
// 解析CacheNamespaceRef注解
parseCacheRef();
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// 每个mapper接口中的方法,都解析成MappedStatement对象
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
3、xml和mapper接口需要同包同名的原因?
进入上步骤的xml解析方法loadXmlResource方法
- type:Mapper接口的Class对象
- 通过Mapper接口名字(com.xc.UserMapper),.替换/转换成资源路径,再添加后缀.xml获取mapper对应的xml
- 通过资源路径加载为输入流
- 然后创建xml解析Builder对象,再调用解析方法,就是Mybatis源码解析(三):映射配置文件的解析的内容了
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
四、Mapper接口代理对象的生成
sqlSession.getMapper(UserMapper.class)通过Mapper接口Class对象生成代理对象
- 其实就是通过Mapper接口Class对象,获取上面说的接口代理类工厂
- 代理类工厂调用.newInstance创建接口代理类
@Override
public <T> T getMapper(Class<T> type) {
// 从Configuration对象中,根据Mapper接口,获取Mapper代理对象
return configuration.getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 根据Mapper接口的类型,从Map集合中获取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 {
// 通过MapperProxyFactory生产MapperProxy,通过MapperProxy产生Mapper代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
进入newInstance方法
- Proxy.newProxyInstance:jdk动态代理,Mapper接口的代理类,从这里创建
- 代理方法第三个参数是InvocationHandler的实现类,invoke方法就是代理类实现接口类方法的内容
public T newInstance(SqlSession sqlSession) {
// InvocationHandler接口的实现类
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 使用JDK动态代理方式,生成代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
五、代理对象执行接口方法的流程
根据jdk动态代理可知,调用接口方法则会进入invoke方法,里面会有接口方法的实现内容
- 如果是Object定义方法,则MapperProxy类直接调用方法
- mapperProxy.findUserById(100):代理类调用接口方法,debug则会进入invoke方法
@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 {
// 代理逻辑在这
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
进入cachedInvoker(method).invoke方法(代理逻辑)
- command.getType():增删改查类型标记
- command.getName():statementId(namespace:id)
- sqlSession.selectOne(command.getName(), param):Mybatis源码解析(六):查询数据库主流程
- sqlSession.insert、sqlSession.update、sqlSession.delete调用方法相同,都是executor.update,所有xml中<insert><update><delete>三个标签的作用一样,只是为了看上去区分一下
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 判断mapper中的方法类型
switch (command.getType()) {
// 添加
case INSERT: {
// 转换参数
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:
// 无返回结果,并且有ResultHandler方法参数,将查询结果交给ResultHandler进行处理
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
// 执行查询、返回列表
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
// 执行查询、返回Map
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
// 执行查询、返回Cursor
} 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;
}
补充说明下SqlCommand: command对象
- 通过接口方法名获取对应xml的MappedStatement对象(每一个赠送改查标签对应一个),这里也说明了为啥接口名要与<insert><update><delete><select>标签内的id一致,就是通过方法名匹配标签id获取MappedStatement
- command.getType():是<insert><update><delete><select>标签解析出增删改查类型
- command.getName():MappedStatement的id,statementId
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 当前调用的方法名称
final String methodName = method.getName();
// 当前执行的方法对应的Class
final Class<?> declaringClass = method.getDeclaringClass();
// 获取对应的MappedStatement
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
总结
- <package>标签配置的包名下的Mapper接口文件都会被加载成对应的代理类工厂
- 通过Mapper接口获取同包同名的xml文件,并解析
- Mapper接口通过jdk代理创建代理类,接口方法匹配xml中标签的id值,执行增删改查