Mybatis-Plus实现单表CRUD的原理

导读

现在使用Mybatis-plus的人越来越多,基本上对于单表的增删改查是不需要写代码即可完成。使用的时候我们只需要在原来的Mapper接口文件上继承BaseMapper即可自动完成单表的CRUD。之前一直都是使用,并没有想过是如何实现的,通过一下午时间大致明白了Mybatis-plus是如何实现通用的单表CRUD了。

示例

在讲原理之前我们先来看简单的看一下如何Mybatis如何实现一个查询,示例代码如下:

@Slf4j
public class MybatisApp {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

        try (SqlSession session = factory.openSession()) {
            UserEntity user = (UserEntity) session.selectOne("com.buydeem.mapper.UserMapper.getById",1);
            log.info("{}",user);
        }
    }
}
  • 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>
    <properties resource="jdbc.properties">
    </properties>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
        <package name="com.buydeem.entity"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>

</configuration>
  • 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.buydeem.mapper.UserMapper">

    <select id="getById" resultType="UserEntity">
        select * from t_user where id = #{id}
    </select>
</mapper>

示例代码很简单,首先通过读取配置文件创建一个SqlSessionFactory,然后获取SqlSession执行查询。

Configuration

通过前面的简单示例,貌似还看不出来什么原因。先看SqlSession.selectOne()实现,跟踪其源码,最后的实现如下:

  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

该方法从Configuration中获取一个MappedStatement实例,然后将其交给Executor去执行。

首先我们得分析一下这个Configuration是个什么东西。在之前我们的示例代码中,它实际上就是读取mybatis-config.xml配置文件构建出一个配置类。从Configuration的源码中也有体现,这个配置类中的属性与mybatis-config.xml能够对应起来。

  MappedStatement ms = configuration.getMappedStatement(statement

该代码是从Configuration实例中获取MappedStatement实例,而其内部实现如下:

  public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
    if (validateIncompleteStatements) {
      buildAllStatements();
    }
    return mappedStatements.get(id);
  }

其实就是从Configuration中的属性mappedStatements获取MappedStatement实例。

  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());

可以看见Configuration中的mappedStatements类型是StrictMap,它继承了HashMap并重写了put和get方法。

    public V put(String key, V value) {
      if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key
            + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
      }
      if (key.contains(".")) {
        final String shortKey = getShortName(key);
        if (super.get(shortKey) == null) {
          super.put(shortKey, value);
        } else {
          super.put(shortKey, (V) new Ambiguity(shortKey));
        }
      }
      return super.put(key, value);
    } 
    
    
    public V get(Object key) {
      V value = super.get(key);
      if (value == null) {
        throw new IllegalArgumentException(name + " does not contain value for " + key);
      }
      if (value instanceof Ambiguity) {
        throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
            + " (try using the full name including the namespace, or rename one of the entries)");
      }
      return value;
    }

MappedStatement

通过前面我们了解到Configuration内部用一个Map来存储MappedStatement,现在的问题是它是何时被放进去的。

  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }

Configuration中只提供了一个方法用来添加MappedStatement,同时还可以知道一点Configuration中存储MappedStatement的Map它的key就是MappedStatement的ID属性。而调用Configuration.addMappedStatement()方法的也只有MapperBuilderAssistant了,该类通过名称可以看出来就是用来构建Mapper的工具类。而我们创建的Mapper.xml文件中的sql语句其实就是通过解析xml文件,然后通过该工具类将其转换成MappedStatement存储到Configuration实例中。关于如何解析xml相关内容本文就不多说了,有兴趣的可以自己去看源码。

现在的疑问是这个MappedStatement有什么用?

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
}

其实每个MappedStatement对应了我们自定义Mapper接口中的一个方法,它保存了我们定义SQL语句、参数结构、返回值结构、Mybatis对它的处理方式等等配置,其内容对应的就是我们Mapper.xml中定义的内容。

Mybatis-Plus实现单表CRUD的原理
MappedStatement

中途小结

从前面我们了解的内容我们现在知道了下面几点:

  • Mybatis首先会构建一个Configuration实例。

  • Configuration中通过一个mappedStatements属性存储MappedStatement实例,其类型就是一个Map,Map的key为MappedStatement的ID,value为MappedStatement实例。

  • Mapper.xml文件中的定义的每个增删改查被解析成MappedStatement实例并被保存在Configuration中。

到目前我们知道了,如果要让Mybatis执行一个SQL语句,我们必须将其转化成MappedStatement才能执行。

Mapper接口

而实际我们使用Mybatis的时候,我们更多的使用下面这种方式:

   try (SqlSession session = factory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            UserEntity user = mapper.getById(1);
            log.info("{}",user);
        }

我们通过一个Mapper接口,在接口中定义SQL执行方法,那这又是如何实现的呢?

MapperRegistry

跟踪SqlSession.getMapper方法其源码是通过调用MapperRegistry获取到一个代理类。首先我们明白一个道理,接口是无法实例化的,但是我们上面的示例代码中我们确实创建出了一个UserMapper实例,这又是为什么呢?

Java中我们可以通过JDK动态代理生成一个代理类,而我们之所以能够使用UserMapper实例就是因为Mybatis给我们创建了代理对象。关于代理可以看下面的示例:

public interface Animal {
    void say();



public class ProxyDemo {
    public static void main(String[] args) {
        InvocationHandler dogProxy = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("wang wang wang");
                return null;
            }
        };
        Animal dog = (Animal) Proxy.newProxyInstance(ProxyDemo.class.getClassLoader(), new Class[]{Animal.class}, dogProxy);
        dog.say();
        System.out.println(dog.getClass().getName());
    }
}

这就是一个简单的JDK动态代理生成实例的代码,Animal接口就是通过动态代理来生成一个代理的Dog实例。

  // MapperRegistry.getMapper()方法实现
  public <T> getMapper(Class<T> type, SqlSession sqlSession) {
    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);
    }
  }

MapperRegistry.getMapper()方法的主要逻辑就是获取接口的代理工厂类,然后通过代理工厂类创建代理类的实例对象。

public class MapperProxyFactory<T{

  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethodInvoker> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) 
    //调用JDK动态代理创建代理类
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

从源码可以看出代理工厂就是通过JDK动态代理来创建接口实例的,同时代理工厂类为了提高性能还使用Map来缓存创建的实例对象。而MapperProxy其实就是InvocationHandler的接口实现,这点可以从源码看出。

public class MapperProxy<Timplements InvocationHandlerSerializable {
 //省略内部代码   
}

我们重点看一下invoke方法内部实现:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      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);
    }
  }

第一个判断是用来处理Object对象的,这个里面什么都不用管直接使用其原来的逻辑。第二个分支则是通过缓存获取其实现。

  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        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);
          }
        } else {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

上面方法返回的MapperMethodInvoker有两种,其中DefaultMethodInvoker是用来处理Java8及以上JDK中接口默认方法的,这个我们不用管,我们要关心的是PlainMethodInvoker。该方法内部保存了一个MapperMethod属性,最后invoke方法实际上执行的逻辑是:

    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }

而方法该方法最后执行的逻辑还是回到了最初sqlSession.selectOne()方法。

通过该部分内容我们知道了下面几点:

  • Mybatis会为我们的Mapper接口生成代理实例。

  • 代理实例调用调用的方法最后还是会调用sqlSession.selectOne()方法来实现。

Mybatis-plus简单实用

@Slf4j
public class App2 {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-plus-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory  = new MybatisSqlSessionFactoryBuilder().build(inputStream);

        try (SqlSession sqlSession = sqlSessionFactory.openSession()){
            User2Mapper mapper = sqlSession.getMapper(User2Mapper.class);
            UserEntity user = mapper.selectById(1);
            log.info("{}",user);
        }
    }
}

使用Mybatis-plus与Mybatis类似,但是可以发现其创建SqlSessionFactory使用的是MybatisSqlSessionFactoryBuilder,难道就这点不一样么?其实不是,虽然都能用来创建SqlSessionFactory,但是其内存创建MybatisConfiguration会有差别。

MybatisConfiguration

该类是Mybatis-plus通过继承Configuration的一个子类,该类代码与Configuration基本相似,而增强Mybatis就是从此处开始的。在前面我们介绍了MapperRegistry,而在MybatisConfiguration中同样存在一个MybatisMapperRegistry,它的作用与MapperRegistry一样,但是其有一个方法需要特别关注。

    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // TODO 如果之前注入 直接返回
                return;
                // TODO 这里就不抛异常了
//                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                // TODO 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
                knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                // TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
                MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }

该方法与Mybatis源码最大的不同点就在此处了。此处不是使用的原有MapperAnnotationBuilder而是使用的自己的MybatisMapperAnnotationBuilder,而这个类的parse方法就是增强的关键点所在。

            try {
                // https://github.com/baomidou/mybatis-plus/issues/3038
                if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
                    parserInjector();
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new InjectorResolver(this));
            }

parse方法中有一个判断,判断该接口是不是BaseMapper的子类,如果是就进行CRUD SQL注入。

    public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
        Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
        if (modelClass != null) {
            String className = mapperClass.toString();
            Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
            if (!mapperRegistryCache.contains(className)) {
                TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
                if (CollectionUtils.isNotEmpty(methodList)) {
                    // 循环注入自定义方法
                    methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
                } else {
                    logger.debug(mapperClass.toString() + ", No effective injection method was found.");
                }
                mapperRegistryCache.add(className);
            }
        }
    }

该方法就是增强SQL的核心代码,该通过model实体类获取到数据库表的信息也就是TableInfo,然后通过获取到BaseMapper接口中的增强方法,然后通过MapperBuilderAssistant将其解析成MappedStatement并添加到Configuration中的mappedStatement中,最后完成了CURD方法增强。

注意这里面用到了一个模板方法的设计模式,对于AbstractMethod其实现了大多数逻辑,而且这些逻辑都是相同的,唯一不同的就是就是注入自定义的MappedStatement,该类也只有这个方法需要子类去继承实现。

总结

其实Mybatis-plus的单表CRUD就是通过自动添加MappedStatement去实现的。如果我们自己去写单表的CURD也就是将自己的SQL执行方法写到XML文件中,然后Mybatis再将我们写的XML文件解析成MappedStatement保存到Configuration已给后续使用。而Mybatis-plus通过Java的反射获取到表信息,然后动态的生成MappedStatement保存到Configuration中。

最后看了Mybatis-plus的增强实现感觉Mybatis很多东西都不能很好的扩展,Mybatis-plus有很多代码都是复制的Mybatis源代码然后增加自己的逻辑来实现的。这个主要问题还是因为Mybatis的扩展性的确不如Spring

最后说一句,Spring的设计确实比Mybatis好很多。


原文始发于微信公众号(一只菜鸟程序员):Mybatis-Plus实现单表CRUD的原理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72864.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!