Mybatis之动态代理生成Mapper

一、前期准备

(1)实体对象

 
  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. public class User {
  5.  
  6. private Long id;
  7. private String name;
  8. private String createTime;
  9. private String password;
  10. private String phone;
  11. private String nickname;
  12. }
 

(2)mapper接口

 
  1. public interface UserMapper {
  2.  
  3. List<User> listAllUser();
  4.  
  5. @Select("select * from user where id=#{userId,jdbcType=INTEGER}")
  6. User getUserById(@Param("userId") String userId);
  7.  
  8.  
  9. }
 

(3)mapper的xml

 
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4. <mapper namespace="com.yxy.mapper.UserMapper">
  5. <sql id="userAllField">
  6. id,create_time, name, password, phone, nick_name
  7. </sql>
  8.  
  9. <select id="listAllUser" resultType="com.yxy.entity.User" >
  10. select
  11. <include refid="userAllField"/>
  12. from user
  13. </select>
  14. </mapper>
 

(4)mybatis的配置文件

 
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE configuration
  3. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  4. "http://mybatis.org/dtd/mybatis-3-config.dtd">
  5.  
  6. <configuration>
  7. <settings>
  8. <setting name="useGeneratedKeys" value="true"/>
  9. <setting name="mapUnderscoreToCamelCase" value="true"/>
  10. <setting name="logImpl" value="LOG4J"/>
  11. </settings>
  12.  
  13. <environments default="dev" >
  14. <environment id="dev">
  15. <transactionManager type="JDBC">
  16. <property name="" value="" />
  17. </transactionManager>
  18. <dataSource type="UNPOOLED">
  19. <property name="driver" value="org.hsqldb.jdbcDriver" />
  20. <property name="url" value="jdbc:hsqldb:mem:mybatis" />
  21. <property name="username" value="sa" />
  22. <property name="password" value="" />
  23. </dataSource>
  24. </environment>
  25. </environments>
  26.  
  27. <mappers>
  28. <mapper resource="mapper/UserMapper.xml"/>
  29. </mappers>
  30. </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> 节 点

 
  1. private void mapperElement(XNode parent) throws Exception {
  2. if (parent != null) {
  3. for (XNode child : parent.getChildren()) {
  4. if ("package".equals(child.getName())) {
  5. //扫描package标签
  6. String mapperPackage = child.getStringAttribute("name");
  7. configuration.addMappers(mapperPackage);
  8. } else {
  9. String resource = child.getStringAttribute("resource");
  10. String url = child.getStringAttribute("url");
  11. String mapperClass = child.getStringAttribute("class");
  12. if (resource != null && url == null && mapperClass == null) {
  13. ErrorContext.instance().resource(resource);
  14. try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
  15. //根据mapper标签的resource属性解析mapper
  16. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
  17. configuration.getSqlFragments());
  18. mapperParser.parse();
  19. }
  20. } else if (resource == null && url != null && mapperClass == null) {
  21. ErrorContext.instance().resource(url);
  22. try (InputStream inputStream = Resources.getUrlAsStream(url)) {
  23. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,
  24. configuration.getSqlFragments());
  25. //根据mapper标签的url属性解析mapper
  26. mapperParser.parse();
  27. }
  28. } else if (resource == null && url == null && mapperClass != null) {
  29. //根据mapper标签的class属性解析mapper
  30. Class<?> mapperInterface = Resources.classForName(mapperClass);
  31. configuration.addMapper(mapperInterface);
  32. } else {
  33. //<mapper>节点的resource、url、class属性,这三个属性互斥
  34. throw new BuilderException(
  35. "A mapper element may only specify a url, resource or class, but not more than one.");
  36. }
  37. }
  38. }
  39. }
  40. }
 

对于通过接口的方式来注册映射器的形式,最终会由MapperAnnotationBuilder.parse()来解析xml文件

 
  1. public void parse() {
  2. String resource = type.toString();
  3. if (!configuration.isResourceLoaded(resource)) {
  4. //检测是否加载过对应的映射配置文件,如果未加载,则创建
  5. //XMLMapperBuilder对象解析对应的映射文件
  6. loadXmlResource();
  7. configuration.addLoadedResource(resource);
  8. assistant.setCurrentNamespace(type.getName());
  9. //解析@CacheNamespace注解
  10. parseCache();
  11. //解析@CacheNamespaceRef注解
  12. parseCacheRef();
  13. for (Method method : type.getMethods()) {
  14. if (!canHaveStatement(method)) {
  15. continue;
  16. }
  17. if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
  18. && method.getAnnotation(ResultMap.class) == null) {
  19. parseResultMap(method);
  20. }
  21. try {
  22. //解析接口文件定义的注解,例如@Select,并创建MappedStatement对象
  23. parseStatement(method);
  24. } catch (IncompleteElementException e) {
  25. //出现异常的方法添加到Configuration.incompleteMethods集合中暂存,这样保证xml标签的定义顺序不分先后
  26. configuration.addIncompleteMethod(new MethodResolver(this, method));
  27. }
  28. }
  29. }
  30. // 遍历Configuration.incompleteMethods集合中记录的为解析的方法,并重新进行解析
  31. parsePendingMethods();
  32. }
 

在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等标签

 
  1. public void parse() {
  2. //判断是否加载过配置文件
  3. if (!configuration.isResourceLoaded(resource)) {
  4. //解析xml文件mapper节点
  5. configurationElement(parser.evalNode("/mapper"));
  6. //记录了已经加载过的映射文件。
  7. configuration.addLoadedResource(resource);
  8. //注册mapper接口
  9. bindMapperForNamespace();
  10. }
  11.  
  12. // 处理configurationElement()方法中解析失败的<resultMap>节点
  13. parsePendingResultMaps();
  14. //处理 configurationElement()方法中解析失败的<cache-ref>节点
  15. parsePendingCacheRefs();
  16. //处理configurationElement()方法中解析失败的SQL语句节点
  17. parsePendingStatements();
  18. }
 

(2)解析select|insert|update|delete标签

解析select|insert|update|delete标签,主要XMLStatementBuilder.parseStatementNode()来解析的。XMLMapperBuilder.buildStatementFromContext()会根据扫描到的sql节点,通过遍历的方式逐一解析。

 
  1. private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  2. for (XNode context : list) {
  3. final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context,
  4. requiredDatabaseId);
  5. try {
  6.  
  7. statementParser.parseStatementNode();
  8. } catch (IncompleteElementException e) {
  9. configuration.addIncompleteStatement(statementParser);
  10. }
  11. }
  12. }
 
 
  1. public void parseStatementNode() {
  2. //......前面代码省略
  3. //通过MapperBuilderAssistant创建MappedStatement对象,并添加到Configuration.mappedStatements集合中保存
  4. builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
  5. parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
  6. keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
  7. }
 

解析完成会生成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 接口的绑定。

 
  1. //映射配置文件与对应 Mapper 接口的绑定
  2. private void bindMapperForNamespace() {
  3. //获取映射配置文件的命名空间
  4. String namespace = builderAssistant.getCurrentNamespace();
  5. if (namespace != null) {
  6. Class<?> boundType = null;
  7. try {
  8. boundType = Resources.classForName(namespace);
  9. } catch (ClassNotFoundException e) {
  10. // ignore, bound type is not required
  11. }
  12. if (boundType != null && !configuration.hasMapper(boundType)) {
  13. // Spring may not know the real resource name so we set a flag
  14. // to prevent loading again this resource from the mapper interface
  15. // look at MapperAnnotationBuilder#loadXmlResource
  16. configuration.addLoadedResource("namespace:" + namespace);
  17. 调用
  18. //MapperRegistry.addMapper()方法,注册Mapper接口
  19. configuration.addMapper(boundType);
  20. }
  21. }
  22. }
 

在MapperRegistry维持一个map集合:

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();

这个集合可以是接口的Class类,值是MapperProxyFactory类型的对象,其中MapperProxyFactory对象用于创建Mapper动态代理对象。

 
  1. public <T> void addMapper(Class<T> type) {
  2. if (type.isInterface()) {
  3. if (hasMapper(type)) {
  4. throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
  5. }
  6. boolean loadCompleted = false;
  7. try {
  8.  
  9. knownMappers.put(type, new MapperProxyFactory<>(type));
  10. // 创建MapperAnnotationBuilder,并调用MapperAnnotationBuilder.parse()方法解析Mapper接口中的注解信息
  11. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
  12. parser.parse();
  13. loadCompleted = true;
  14. } finally {
  15. if (!loadCompleted) {
  16. knownMappers.remove(type);
  17. }
  18. }
  19. }
  20. }
 

2.3 生成Mapper代理对象

(1)获得SqlSession对象

     SqlSession是MyBatis中提供的与数据库交互的接口,SqlSession实例通过工厂模式创建。为了创建SqlSession对象,首先需要创建SqlSessionFactory对象,而SqlSessionFactory对象的创建依赖于SqlSessionFactoryBuilder类,该类提供了一系列重载的build()方法,我们需要以主配置文件的输入流作为参数调用SqlSessionFactoryBuilder对象的bulid()方法,该方法返回一个SqlSessionFactory对象。有了SqlSessionFactory对象之后,调用SqlSessionFactory对象的openSession()方法即可获取一个与数据库建立连接的SqlSession实例。

 
  1. // 读取配置文件
  2. InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
  3. // 创建SqlSessionFactory
  4. SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
  5. // 打开sqlSession
  6. SqlSession sqlSession = sqlSessionFactory.openSession();
  7. // 获取mapper接口对象
  8. 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)

 
  1. public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  2. //查找指定type对应的MapperProxyFactory对象
  3. final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  4. if (mapperProxyFactory == null) {
  5. throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  6. }
  7. try {
  8. // 创建实现了type接口的代理对象
  9. return mapperProxyFactory.newInstance(sqlSession);
  10. } catch (Exception e) {
  11. throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  12. }
  13. }
 

MapperProxyFactory主要负责创建代理对象

 
  1. public class MapperProxyFactory<T> {
  2.  
  3. //......
  4.  
  5. @SuppressWarnings("unchecked")
  6. protected T newInstance(MapperProxy<T> mapperProxy) {
  7. // 创建MapperProxy对象,每次调用都会创建新的MapperProxy对象
  8. return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  9. }
  10.  
  11. public T newInstance(SqlSession sqlSession) {
  12. // 创建实现了mapperInterface接口的代理对象
  13. final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  14. return newInstance(mapperProxy);
  15. }
  16.  
  17. }
 

生成Mapper接口的代理对象使用的jdk动态代理,MapperProxy实现了InvocationHandler接口,重写invoke方法。

 
  1. @Override
  2. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  3. try {
  4. if (Object.class.equals(method.getDeclaringClass())) {
  5. return method.invoke(this, args);
  6. }
  7. // 从缓存中获取MapperMethodInvoker对象,
  8. // 如果缓存中没有,则创建新的MapperMethodInvoker对象并添加到缓存中
  9. return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
  10. } catch (Throwable t) {
  11. throw ExceptionUtil.unwrapThrowable(t);
  12. }
  13. }
 

 (3)通过Mapper的代理对象执行sql语句

        cachedInvoker的返回值是MapperMethodInvoker类型,该接口有两个实现类

PlainMethodInvoker和DefaultMethodInvoker,PlainMethodInvoker用于执行接口里定义的普通方法,而DefaultMethodInvoker是通过方法句柄MethodHandle执行接口中的默认方法。PlainMethodInvoker类会调用MapperMethod.execute(SqlSession sqlSession, Object[] args) 方法执行sql语句。

 
  1. public Object execute(SqlSession sqlSession, Object[] args) {
  2. Object result;
  3. //根据SQL语句的类型调用SqlSession对应的方法
  4. switch (command.getType()) {
  5. case INSERT: {
  6. //使用 ParamNameResolver处理args[]数组(用户传入的实参列表),将用户传入的实参与指定参数名称关联起来
  7. Object param = method.convertArgsToSqlCommandParam(args);
  8. result = rowCountResult(sqlSession.insert(command.getName(), param));
  9. break;
  10. }
  11. case UPDATE: {
  12. Object param = method.convertArgsToSqlCommandParam(args);
  13. result = rowCountResult(sqlSession.update(command.getName(), param));
  14. break;
  15. }
  16. case DELETE: {
  17. Object param = method.convertArgsToSqlCommandParam(args);
  18. result = rowCountResult(sqlSession.delete(command.getName(), param));
  19. break;
  20. }
  21. case SELECT:
  22. if (method.returnsVoid() && method.hasResultHandler()) {
  23. executeWithResultHandler(sqlSession, args);
  24. result = null;
  25. } else if (method.returnsMany()) {
  26. result = executeForMany(sqlSession, args);
  27. } else if (method.returnsMap()) {
  28. result = executeForMap(sqlSession, args);
  29. } else if (method.returnsCursor()) {
  30. result = executeForCursor(sqlSession, args);
  31. } else {
  32. Object param = method.convertArgsToSqlCommandParam(args);
  33. result = sqlSession.selectOne(command.getName(), param);
  34. if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
  35. result = Optional.ofNullable(result);
  36. }
  37. }
  38. break;
  39. case FLUSH:
  40. result = sqlSession.flushStatements();
  41. break;
  42. default:
  43. throw new BindingException("Unknown execution method for: " + command.getName());
  44. }
  45. if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
  46. throw new BindingException("Mapper method '" + command.getName()
  47. + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  48. }
  49. return result;
  50. }
 

MapperMethod中封装了Mapper接口中对应方法的信息,以及对应SQL语句的信息。可以将MapperMethod 看作连接 Mapper 接口以及映射配置文件中定义的 SQL 语句的桥梁。

posted @   CharyGao  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有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]什么是柳比歇夫的时间事件记录法
点击右上角即可分享
微信分享提示