Mybatis之动态代理生成Mapper
一、前期准备
(1)实体对象
-
-
-
-
public class User {
-
-
private Long id;
-
private String name;
-
private String createTime;
-
private String password;
-
private String phone;
-
private String nickname;
-
}
(2)mapper接口
-
public interface UserMapper {
-
-
List<User> listAllUser();
-
-
-
User getUserById(; String userId)
-
-
-
}
(3)mapper的xml
-
-
-
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-
<mapper namespace="com.yxy.mapper.UserMapper">
-
<sql id="userAllField">
-
id,create_time, name, password, phone, nick_name
-
</sql>
-
-
<select id="listAllUser" resultType="com.yxy.entity.User" >
-
select
-
<include refid="userAllField"/>
-
from user
-
</select>
-
</mapper>
(4)mybatis的配置文件
-
-
-
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
-
"http://mybatis.org/dtd/mybatis-3-config.dtd">
-
-
<configuration>
-
<settings>
-
<setting name="useGeneratedKeys" value="true"/>
-
<setting name="mapUnderscoreToCamelCase" value="true"/>
-
<setting name="logImpl" value="LOG4J"/>
-
</settings>
-
-
<environments default="dev" >
-
<environment id="dev">
-
<transactionManager type="JDBC">
-
<property name="" value="" />
-
</transactionManager>
-
<dataSource type="UNPOOLED">
-
<property name="driver" value="org.hsqldb.jdbcDriver" />
-
<property name="url" value="jdbc:hsqldb:mem:mybatis" />
-
<property name="username" value="sa" />
-
<property name="password" value="" />
-
</dataSource>
-
</environment>
-
</environments>
-
-
<mappers>
-
<mapper resource="mapper/UserMapper.xml"/>
-
</mappers>
-
</configuration>
二、源码解读
2.1 mybatis初始化解析配置文件的mapper标签
(1)mapper文件的配置方式
mapper标签在配置文件的配置方式一共有四种
(1)使用相对于类路径的资源引用
<mapper resource="com/yxy/mapper/UserMapper.xml"/>
(2)使用完全限定资源定位符(URL)
<mapper url="file:E:\mybatis-study\src\main\resources\UserMapper.xml"/>
(3)使用映射器接口实现类的完全限定类名
<mapper class="com.yxy.mapper.UserMapper"/>
(4)将包内的映射器接口全部注册为映射器
<package name="com.yxy.mapper"/>
注意:
前两种配置方式对于接口文件和resources上的xml文件不一定在同一包下,也不要求xml文件名和接口名相等,而后两种要求文件路径和文件名必须相等。
(2) 解析mybatis-config.xml的mappers标签
XmlConfigBuilder.mapperElement() 方法负责解析 <mappers> 节 点
-
private void mapperElement(XNode parent) throws Exception {
-
if (parent != null) {
-
for (XNode child : parent.getChildren()) {
-
if ("package".equals(child.getName())) {
-
//扫描package标签
-
String mapperPackage = child.getStringAttribute("name");
-
configuration.addMappers(mapperPackage);
-
} else {
-
String resource = child.getStringAttribute("resource");
-
String url = child.getStringAttribute("url");
-
String mapperClass = child.getStringAttribute("class");
-
if (resource != null && url == null && mapperClass == null) {
-
ErrorContext.instance().resource(resource);
-
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
-
//根据mapper标签的resource属性解析mapper
-
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
-
configuration.getSqlFragments());
-
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());
-
//根据mapper标签的url属性解析mapper
-
mapperParser.parse();
-
}
-
} else if (resource == null && url == null && mapperClass != null) {
-
//根据mapper标签的class属性解析mapper
-
Class<?> mapperInterface = Resources.classForName(mapperClass);
-
configuration.addMapper(mapperInterface);
-
} else {
-
//<mapper>节点的resource、url、class属性,这三个属性互斥
-
throw new BuilderException(
-
"A mapper element may only specify a url, resource or class, but not more than one.");
-
}
-
}
-
}
-
}
-
}
对于通过接口的方式来注册映射器的形式,最终会由MapperAnnotationBuilder.parse()来解析xml文件
-
public void parse() {
-
String resource = type.toString();
-
if (!configuration.isResourceLoaded(resource)) {
-
//检测是否加载过对应的映射配置文件,如果未加载,则创建
-
//XMLMapperBuilder对象解析对应的映射文件
-
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 {
-
//解析接口文件定义的注解,例如@Select,并创建MappedStatement对象
-
parseStatement(method);
-
} catch (IncompleteElementException e) {
-
//出现异常的方法添加到Configuration.incompleteMethods集合中暂存,这样保证xml标签的定义顺序不分先后
-
configuration.addIncompleteMethod(new MethodResolver(this, method));
-
}
-
}
-
}
-
// 遍历Configuration.incompleteMethods集合中记录的为解析的方法,并重新进行解析
-
parsePendingMethods();
-
}
在loadXmlResource()通过接口的完全限定类名,再拼接.xml来解析xml文件的,故前面讲到,对于配置mapper文件的配置方式,必须要求接口和xml文件的包名和文件名称必须相等。
loadXmlResource()函数中一句关键代码如下:
String xmlResource = type.getName().replace('.', '/') + ".xml";
2.2 解析xml文件
(1)解析xml文件mapper节点
XMLMapperBuilder.parse()负责解析xml文件,xml可以配置许多标签,比如cache,resultMap,这里我重点分析解析select等标签
-
public void parse() {
-
//判断是否加载过配置文件
-
if (!configuration.isResourceLoaded(resource)) {
-
//解析xml文件mapper节点
-
configurationElement(parser.evalNode("/mapper"));
-
//记录了已经加载过的映射文件。
-
configuration.addLoadedResource(resource);
-
//注册mapper接口
-
bindMapperForNamespace();
-
}
-
-
// 处理configurationElement()方法中解析失败的<resultMap>节点
-
parsePendingResultMaps();
-
//处理 configurationElement()方法中解析失败的<cache-ref>节点
-
parsePendingCacheRefs();
-
//处理configurationElement()方法中解析失败的SQL语句节点
-
parsePendingStatements();
-
}
(2)解析select|insert|update|delete标签
解析select|insert|update|delete标签,主要XMLStatementBuilder.parseStatementNode()来解析的。XMLMapperBuilder.buildStatementFromContext()会根据扫描到的sql节点,通过遍历的方式逐一解析。
-
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
-
for (XNode context : list) {
-
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context,
-
requiredDatabaseId);
-
try {
-
-
statementParser.parseStatementNode();
-
} catch (IncompleteElementException e) {
-
configuration.addIncompleteStatement(statementParser);
-
}
-
}
-
}
-
public void parseStatementNode() {
-
//......前面代码省略
-
//通过MapperBuilderAssistant创建MappedStatement对象,并添加到Configuration.mappedStatements集合中保存
-
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
-
parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
-
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
-
}
解析完成会生成MappedStatement,并保存到Configuration的成员变量里,其中key是Mapper的namespace.methodName
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>( "Mapped Statements collection")
(3) 绑定mapper接口
每个映射配置文件的命名空间可以绑定一个 Mapper 接口,并注册到 MapperRegistry 中。在XMLMapperBuilder.bindMapperForNamespace()方法中,完成了映射配置文件与对应 Mapper 接口的绑定。
-
//映射配置文件与对应 Mapper 接口的绑定
-
private void bindMapperForNamespace() {
-
//获取映射配置文件的命名空间
-
String namespace = builderAssistant.getCurrentNamespace();
-
if (namespace != null) {
-
Class<?> boundType = null;
-
try {
-
boundType = Resources.classForName(namespace);
-
} catch (ClassNotFoundException e) {
-
// ignore, bound type is not required
-
}
-
if (boundType != null && !configuration.hasMapper(boundType)) {
-
// Spring may not know the real resource name so we set a flag
-
// to prevent loading again this resource from the mapper interface
-
// look at MapperAnnotationBuilder#loadXmlResource
-
configuration.addLoadedResource("namespace:" + namespace);
-
调用
-
//MapperRegistry.addMapper()方法,注册Mapper接口
-
configuration.addMapper(boundType);
-
}
-
}
-
}
在MapperRegistry维持一个map集合:
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();
这个集合可以是接口的Class类,值是MapperProxyFactory类型的对象,其中MapperProxyFactory对象用于创建Mapper动态代理对象。
-
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.put(type, new MapperProxyFactory<>(type));
-
// 创建MapperAnnotationBuilder,并调用MapperAnnotationBuilder.parse()方法解析Mapper接口中的注解信息
-
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
-
parser.parse();
-
loadCompleted = true;
-
} finally {
-
if (!loadCompleted) {
-
knownMappers.remove(type);
-
}
-
}
-
}
-
}
2.3 生成Mapper代理对象
(1)获得SqlSession对象
SqlSession是MyBatis中提供的与数据库交互的接口,SqlSession实例通过工厂模式创建。为了创建SqlSession对象,首先需要创建SqlSessionFactory对象,而SqlSessionFactory对象的创建依赖于SqlSessionFactoryBuilder类,该类提供了一系列重载的build()方法,我们需要以主配置文件的输入流作为参数调用SqlSessionFactoryBuilder对象的bulid()方法,该方法返回一个SqlSessionFactory对象。有了SqlSessionFactory对象之后,调用SqlSessionFactory对象的openSession()方法即可获取一个与数据库建立连接的SqlSession实例。
-
// 读取配置文件
-
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
-
// 创建SqlSessionFactory
-
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
-
// 打开sqlSession
-
SqlSession sqlSession = sqlSessionFactory.openSession();
-
// 获取mapper接口对象
-
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
(2)获得Mapper的代理对象
获得Mapper的代理对象,调用链如下
(1)SqlSession.getMapper(Class<T> type)
(2)DefaultSqlSession.getMapper(Class<T> type)
(3)Configuration.getMapper(Class<T> type, SqlSession sqlSession)
(4)MapperRegistry.getMapper(Class<T> type, SqlSession sqlSession)
-
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
-
//查找指定type对应的MapperProxyFactory对象
-
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
-
if (mapperProxyFactory == null) {
-
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
-
}
-
try {
-
// 创建实现了type接口的代理对象
-
return mapperProxyFactory.newInstance(sqlSession);
-
} catch (Exception e) {
-
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
-
}
-
}
MapperProxyFactory主要负责创建代理对象
-
public class MapperProxyFactory<T> {
-
-
//......
-
-
-
protected T newInstance(MapperProxy<T> mapperProxy) {
-
// 创建MapperProxy对象,每次调用都会创建新的MapperProxy对象
-
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
-
}
-
-
public T newInstance(SqlSession sqlSession) {
-
// 创建实现了mapperInterface接口的代理对象
-
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
-
return newInstance(mapperProxy);
-
}
-
-
}
生成Mapper接口的代理对象使用的jdk动态代理,MapperProxy实现了InvocationHandler接口,重写invoke方法。
-
-
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
-
try {
-
if (Object.class.equals(method.getDeclaringClass())) {
-
return method.invoke(this, args);
-
}
-
// 从缓存中获取MapperMethodInvoker对象,
-
// 如果缓存中没有,则创建新的MapperMethodInvoker对象并添加到缓存中
-
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
-
} catch (Throwable t) {
-
throw ExceptionUtil.unwrapThrowable(t);
-
}
-
}
(3)通过Mapper的代理对象执行sql语句
cachedInvoker的返回值是MapperMethodInvoker类型,该接口有两个实现类
PlainMethodInvoker和DefaultMethodInvoker,PlainMethodInvoker用于执行接口里定义的普通方法,而DefaultMethodInvoker是通过方法句柄MethodHandle执行接口中的默认方法。PlainMethodInvoker类会调用MapperMethod.execute(SqlSession sqlSession, Object[] args) 方法执行sql语句。
-
public Object execute(SqlSession sqlSession, Object[] args) {
-
Object result;
-
//根据SQL语句的类型调用SqlSession对应的方法
-
switch (command.getType()) {
-
case INSERT: {
-
//使用 ParamNameResolver处理args[]数组(用户传入的实参列表),将用户传入的实参与指定参数名称关联起来
-
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中封装了Mapper接口中对应方法的信息,以及对应SQL语句的信息。可以将MapperMethod 看作连接 Mapper 接口以及映射配置文件中定义的 SQL 语句的桥梁。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2022-01-25 linux下nproc的作用,Linux句柄调优之nofile、nr_open、file-max
2018-01-25 [No0000119]什么是柳比歇夫的时间事件记录法