2021年3月18日星期四

浅析MyBatis(三):聊一聊MyBatis的实用插件与自定义插件

在前面的文章中,笔者详细介绍了 🔗MyBatis 框架的底层框架与运行流程,并且在理解运行流程的基础上手写了一个自己的 MyBatis 框架。看完前两篇文章后,相信读者对 MyBatis 的偏底层原理和执行流程已经有了自己的认知,并且对其在实际开发过程中使用步骤也已是轻车熟路。所谓实践是检验真理的唯一标准,本文将为大家介绍一些 MyBatis 使用中的一些实用插件与自定义插件。本文涉及到的代码已上传至 GitHub: 🔗mypagehelper-demo 。

话不多说,现在开始🔛🔛🔛!

1. Lombok插件

1.1 Lombok简介

在编写 Java 程序时经常会用到很多实体类对象 ,其创建的一般流程就是定义成员变量,然后定义对应的 Constructor(有参/无参构造方法)、Getter and Setter 方法、toString() 方法等等。在 IDEA 中,可以通过 alt + insert 快捷键来快速插入这些方法,操作起来感觉还是很方便的。但是,在实际的业务开发中这些实体对象的属性可能经常发生变化(成员变量命名变化、成员变量个数变化等等),比如在 Web 项目的开发中入参和出参的 DTO 类的属性经常会有所变化。这样在每次发生属性变化时,都需要去修改成员变量对应的构造方法、 Getter and Setter 方法以及 toString() 方法等等,这些操作既繁琐又浪费时间还没有技术含量,降低了实际的开发效率。对于这些问题,Lombok 给出了完美的解决方案。

Lombok 是一种 Java 实用工具,它通过注解方式来帮助开发人员消除代码的冗长。在 IDEA 的插件库搜索 Lombok 便可完成插件的安装,同时在项目里引入 Lombok 依赖可以提供编译支持。

<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> <scope>provided</scope></dependency>

1.2 Lombok的使用

在 Lombok 的包中会提供很多注解,有的作用在类上,有的作用在变量上,而有的注解在方法上,下面会对实际开发中常用的注解进行介绍。

注解名称作用位置作用注解效果
@Data为类提供 Getter and Setter、equals、canEqual、hashCode、toString 方法@Date 效果
@Value为类提供全参构造、equals、hashCode、toString 方法,为类的属性提供 Getter 方法@Value 效果
@AllArgsConstructor为类提供一个全参构造@AllArgsConstructor 效果
@NoArgsConstructor为类提供一个无参构造@NoArgsConstructor 效果
@EqualsAndHashCode为类提供 equals、canEqual 以及hashcode 方法@EqualsAndHashCode 效果
@toString为类提供 toString 方法@toString 效果
@Getter类或者属性为类的所有属性或单个属性提供 Getter 方法@Getter 效果
@Setter类或者属性为类的所有属性或单个属性提供 Setter 方法@Setter 效果
@NonNull属性为属性提供非空检查,如果为空则抛出空指针异常@NonNull 效果
@RequiredArgsConstructor使用 带@NonNull 或 final 修饰的属性来构造类的构造方法@RequiredArgsConstructor 效果

在 MyBatis 的使用中灵活搭配 Lombok 的各种注解能够很大程度上简化代码,提高开发效率,关于更多 Lombok 插件的使用可参见其官网:https://projectlombok.org/ 。

2. PageHelper插件

2.1 PageHelper简介

实际开发中遇到查询数据库表给前端返回信息时,常需要对查询结果进行分页,使用 PageHelper 插件能够方便快捷地实现分页要求。 PageHelper 是开源的分页插件,支持任何单表或多表的分页。在实际开发中,推荐使用 maven 添加依赖的方式引入插件:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version></dependency>

2.2 PageHelper的使用

2.2.1 添加PageHelper的配置

在 MyBatis 框架的配置文件中提供了 plugins 标签用于配置 MyBatis 插件,因此在使用 PageHelper 时需要把插件的相关配置写到 MyBatis 的配置文件 mybatis-config.

<plugins> <!-- com.github.pagehelper为PageHelper类所在包名 --> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="param1" value="value1"/> </plugin> </plugins>

这里需要注意的是:在 MyBatis 配置文件中,各个标签的顺序有严格的要求,务必在正确的位置添加 PageHelper 的配置。相应的标签顺序见下方:

properties, settings, typeAliases, typeHandlers, objectFactory, objectWrapperFactory, plugins, environments, databaseIdProvided, mappers

2.2.2 PageHelper的两种使用方式

在添加了 PageHelper 的配置后,就可以在实际开发中利用该插件来实现分页。还是采用之前的学生表案例来编写相应的测试方法,如下所示:

public class StudentTest { private InputStream in; private SqlSession sqlSession; @Before public void init() throws IOException { // 读取MyBatis的配置文件 in = Resources.getResourceAsStream("mybatis-config.

可以采用 PageHelper.startPage(int pageNum, int pageSize) 来快速实现分页操作,其中 int pageNum 用于指定当前的页码,而 int pageSize 用于指定每一页展示的记录数。这里需要注意的是:只有紧跟在 PageHelper.startPage 方法后的第一个 Mybatis 的查询(Select)方法会被分页。因此在调用 PageHelper.startPage 方法后需要紧跟 Mapper 接口中定义的查询方法,否则分页插件将失效。对于上面的测试方法,在运行后得到如下结果:

https://cdn.jsdelivr.net/gh/IzumiChiaki/CDN/dellImages/PageHelper.png

通过打印的日志发现原本 SELECT * FROM student 的 SQL 语句在配置了 PageHelper 插件后会在语句末尾加入 LIMIT 的分页操作,同时传入指定的 pageSize 参数。在最后的查询结果输出中也可以看出实际的总记录数为 total = 4 ,而查询结果只显示了第一页的三条记录,成功实现了对查询结果分页的操作。

上面介绍的 PageHelper.startPage() 方法最大的局限在于只能对紧跟在其后的 MyBatis 的查询操作的结果进行分页。然而,在实际的后端开发中经常需要对多表进行查询并对结果进行聚合,然后给前端传一个结果集合,这种时候如何实现分页操作呢?在这种情况下,仍然可以利用 PageHelper 插件进行手工分页,定义用于分页请求的 pageRequest() 方法以及相应的测试类,如下所示:

/** * 分页请求 * @param pageNum 指定页码 * @param pageSize 指定每页的记录数 * @param list 待分页的集合 * @param <T> 集合类型 * @return 分页后的集合 */private <T> List<T> pageRequest(int pageNum, int pageSize, List<T> list){ // 根据pageNum和pageSize构建Page类 Page<T> page = new Page<T>(pageNum, pageSize); // 设置page对象的总记录数属性 page.setTotal(list.size()); // 计算分页的开始和结束索引 int startIndex = (pageNum - 1) * pageSize; int endIndex = Math.min(startIndex + pageSize, list.size()); // 从待分页的集合获取需要展示的内容添加到page对象 page.addAll(list.subList(startIndex, endIndex)); // 返回分页后的集合 return page.getResult();}@Testpublic void testPage() { // 定义分页相关参数 int pageNum = 2, pageSize = 3; // 准备List<Student>集合 List<Student> students = new ArrayList<Student>(); students.add(new Student(1, "张A","男")); students.add(new Student(2, "张B","男")); students.add(new Student(3, "张C","男")); students.add(new Student(4, "张D","男")); students.add(new Student(5, "张E","男")); // 分页 List<Student> results = pageRequest(pageNum, pageSize, students); System.out.println(results);}

通过构建上面的 pageRequest 方法,我们实现了一个简单的手工分页,通过调用该方法就能够实现对已有集合的分页,通过运行测试方法可以得到如下结果。

https://cdn.jsdelivr.net/gh/IzumiChiaki/CDN/dellImages/PageRequest.png

从结果易知在面对已存在的 List 集合时,我们基于 PageHelper 插件构建的 pageRequest 方法仍起到了分页的作用。这里指定的页码 pageNum = 2,pageSize = 3 ,待分页的集合总记录为五条,结果显示了集合中的最后两条记录,分页结果正确。

在本小节中介绍了 PageHelper.startPage 方法以及手工定义 pageRequest 方法的两种基于 PageHelper 插件的分页方式,能够根据不同的情况实现分页需求。

3. MyBatis插件的运行原理简介

在第一篇文章中已经对 MyBatis 框架的运行流程进行了讲解,相信读者都已知晓 MyBatis 是利用

3.2 Interceptor接口

找到上面方法中涉及到的 Interceptor 类对于的源码如下所示:

public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties);}

可以看出这是一个接口,其中定义了 intercept()、plugin() 以及 setProperties() 三个方法:

  • intercept() 方法:覆盖所拦截对象原有的方法,也是插件的核心方法。其入参是 invocation,可以利用反射调原对象的方法;
  • plugin() 方法:入参 target 是被拦截的对象,该方法的作用是生成一个被拦截对象的代理实例;
  • setProperties() 方法:方便把 MyBatis 配置文件中 plugin 标签下的内容解析后设置到 Configuration 对象。

对于一个插件来说,必须要先实现 Intercept 接口中的三个方法才能在 MyBatis 框架中进行配置并使用。

3.3 从PageHelper插件源码来看插件的实现流程

本节中会从 PageHelper 的源代码来分析 MyBatis 插件的实现流程,找到关键类 PageInterceptor 的源码如下:

// 压制警告注解@SuppressWarnings({"rawtypes", "unchecked"})// 拦截器的注解@Intercepts( {				// 注册拦截器签名:指定需要被拦截的类型(type)、方法(method)和参数(args)需要被拦截				// 只包含4个参数  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),				// 只包含6个参数  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), })public class PageInterceptor implements Interceptor {// 缓存count查询的ms protected Cache<String, MappedStatement> msCountMap = null; private Dialect dialect; private String default_dialect_class = "com.github.pagehelper.PageHelper"; private Field additionalParametersField; private String countSuffix = "_COUNT"; @Override public Object intercept(Invocation invocation) throws Throwable { try {  // 解析拦截到的参数  Object[] args = invocation.getArgs();  MappedStatement ms = (MappedStatement) args[0];  Object parameter = args[1];  RowBounds rowBounds = (RowBounds) args[2];  ResultHandler resultHandler = (ResultHandler) args[3];  Executor executor = (Executor) invocation.getTarget();  CacheKey cacheKey;  BoundSql boundSql;  // 由于逻辑关系,只会进入一次  if(args.length == 4){  // 4个参数时  boundSql = ms.getBoundSql(parameter);  cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);  } else {  // 6个参数时  cacheKey = (CacheKey) args[4];  boundSql = (BoundSql) args[5];  }  List resultList;  // 调用方法判断是否需要进行分页,如果不需要,直接返回结果  if (!dialect.skip(ms, parameter, rowBounds)) {  // 反射获取动态参数  String msId = ms.getId();  Configuration configuration = ms.getConfiguration();  Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);  // 判断是否需要进行count查询  if (dialect.beforeCount(ms, parameter, rowBounds)) {   String countMsId = msId + countSuffix;   Long count;   // 先判断是否存在手写的count查询   MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);   if(countMs != null){   count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);   } else {   countMs = msCountMap.get(countMsId);   // 自动创建   if (countMs == null) {    // 根据当前的ms创建一个返回值为Long类型的ms    countMs = MSUtils.newCountMappedStatement(ms, countMsId);    msCountMap.put(countMsId, countMs);   }   count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);   }   // 处理查询总数   // 返回true时继续分页查询,false时直接返回   if (!dialect.afterCount(count, parameter, rowBounds)) {   // 当查询总数为0时,直接返回空的结果   return dialect.afterPage(new ArrayList(), parameter, rowBounds);   }  }  // 判断是否需要进行分页查询  if (dialect.beforePage(ms, parameter, rowBounds)) {   // 生成分页的缓存key   CacheKey pageKey = cacheKey;   // 处理参数对象   parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);   // 调用方言获取分页sql   String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);   BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);   // 设置动态参数   for (String key : additionalParameters.keySet()) {   pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));   }   // 执行分页查询   resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);  } else {   // 不执行分页的情况下,也不执行内存分页   resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);  }  } else {  // rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页  resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);  }  return dialect.afterPage(resultList, parameter, rowBounds); } finally {  dialect.afterAll(); } } // 省略……	 @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 缓存count ms msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties); String dialectClass = properties.getProperty("dialect"); if (StringUtil.isEmpty(dialectClass)) {  dialectClass = default_dialect_class; } try {  Class<?> aClass = Class.forName(dialectClass);  dialect = (Dialect) aClass.newInstance(); } catch (Exception e) {  throw new PageException(e); } dialect.setProperties(properties); String countSuffix = properties.getProperty("countSuffix"); if (StringUtil.isNotEmpty(countSuffix)) {  this.countSuffix = countSuffix; } try {  // 反射获取BoundSql中的additionalParameters属性  additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");  additionalParametersField.setAccessible(true); } catch (NoSuchFieldException e) {  throw new PageException(e); } }}

简单看完 PageHelper 插件的实现代码之后,总结一下 MyBatis 框架中自定义插件的步骤:

  • ✅确定要被拦截的签名:根据 @Intercepts 以及 @Signature 注解指定需要拦截的参数✅;
  • ✅实现 Intercept 接口的 intercept()、plugin() 以及 setProperties() 方法✅。

4. 自定义一个MyPageHelper分页插件

本节中会借鉴 PageHelper 插件的实现思路来实现自定义一个 MyPageHelper 插件,以更好地理解 MyBatis 框架运行插件的流程。

4.1 MyPage类

本小节中实现了自定义的 MyPage 类,这是一个分页返回对象,封装了分页的相关信息以及分页的列表数据,如下所示:

@Getterpublic class MyPage<E> extends ArrayList<E> { private static final long serialVersionUID = 2630741492557235098L; /** 指定页码,从1开始 **/ @Setter private Integer pageNum; /** 指定每页记录数 **/ @Setter private Integer pageSize; /** 起始行 **/ @Setter private Integer startIndex; /** 末行 **/ @Setter private Integer endIndex; /** 总记录数 **/ private Long total; /** 总页数 **/ @Setter private Integer pages;	 // 根据pageNum、pageSize以及total设置其它属性 public void setTotal(Long total) { this.total = total; this.pages = (int)(total / pageSize + (total % pageSize == 0 ? 0 : 1)); if (pageNum > pages) {  pageNum = pages; } this.startIndex = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0; this.endIndex = this.startIndex + this.pageSize * (this.pageNum > 0 ? 1 : 0); } // 获取分页后的结果 public List<E> getResults() { return this; }}

4.2 MyPageHelper类

进一步定义一个 MyPageHelper 类来辅助分页,该类的核心是利用 ThreadLocal 线程遍历存储分页信息,代码如下所示:

/** * 分页帮助类 * @author chenliang258 * @date 2021/3/17 17:23 */@SuppressWarnings("rawtypes")public class MyPageHelper { private static final ThreadLocal<MyPage> MY_PAGE_THREAD_LOCAL = new ThreadLocal<>(); public static void setMyPageThreadLocal(MyPage myPage) { MY_PAGE_THREAD_LOCAL.set(myPage); } public static MyPage geyMyPageThreadLocal() { return MY_PAGE_THREAD_LOCAL.get(); } public static void clearMyPageThreadLocal() { MY_PAGE_THREAD_LOCAL.remove(); } public static void startPage(Integer pageNum, Integer pageSize) { MyPage myPage = new MyPage(); myPage.setPageNum(pageNum); myPage.setPageSize(pageSize); setMyPageThreadLocal(myPage); }}

4.3 MyPageInterceptor类

接下来就需要编写 Interceptor 接口的实现类来实现相应方法,这里定义了 MyPageInterceptor 类,代码如下:

/** * 分页拦截器实现 * @author chenliang258 * @date 2021/3/17 17:30 */@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})public class MyPageInterceptor implements Interceptor { private Field field; @SuppressWarnings({"rawtypes", "unchecked"}) @Override public Object intercept(Invocation invocation) throws Throwable { Executor executor = (Executor)invocation.getTarget(); Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement)args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds)args[2]; ResultHandler resultHandler = (ResultHandler)args[3]; CacheKey cacheKey; BoundSql boundSql; // 4个参数 if (args.length == 4) {  boundSql = ms.getBoundSql(parameter);  cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } // 6个参数 else {  cacheKey = (CacheKey)args[4];  boundSql = (BoundSql)args[5]; } // 判断是否需要分页 MyPage myPage = MyPageHelper.geyMyPageThreadLocal(); // 不执行分页 if (myPage.getPageNum() <= 0) {  return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } // count查询 MappedStatement countMs = newCountMappedStatement(ms); String sql = boundSql.getSql(); String countSql = "select count(1) from (" + sql + ") _count"; BoundSql countBoundSql =  new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter); Map<String, Object> additionalParameters = (Map<String, Object>) field.get(boundSql); for (Map.Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {  countBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue()); } CacheKey countCacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countBoundSql); Object countResult = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countCacheKey, countBoundSql); Long count = (Long)((List)countResult).get(0); myPage.setTotal(count); // 分页查询 String pageSql = sql + " limit " + myPage.getStartIndex() + "," + myPage.getPageSize(); BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter); for (Map.Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {  pageBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue()); } CacheKey pageCacheKey = executor.createCacheKey(ms, parameter, rowBounds, pageBoundSql); List listResult = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageCacheKey, pageBoundSql); myPage.addAll(listResult); // 清空线程局部变量分页信息 MyPageHelper.clearMyPageThreadLocal(); return myPage; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { try {  field = BoundSql.class.getDeclaredField("additionalParameters");  field.setAccessible(true); } catch (NoSuchFieldException | SecurityException e) {  e.printStackTrace(); } } /** * 创建count的MappedStatement * * @param ms 原始MappedStatement * @return 新的带有分页信息的MappedStatement */ private MappedStatement newCountMappedStatement(MappedStatement ms) { MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_count",  ms.getSqlSource(), ms.getSqlCommandType()); builder.resource(ms.getResource()); builder.fetchSize(ms.getFetchSize()); builder.statementType(ms.getStatementType()); builder.keyGenerator(ms.getKeyGenerator()); if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {  StringBuilder keyProperties = new StringBuilder();  for (String keyProperty : ms.getKeyProperties()) {  keyProperties.append(keyProperty).append(",");  }  keyProperties.delete(keyProperties.length() - 1, keyProperties.length());  builder.keyProperty(keyProperties.toString()); } builder.timeout(ms.getTimeout()); builder.parameterMap(ms.getParameterMap()); // count查询返回值int List<ResultMap> resultMaps = new ArrayList<>(); ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId() + "_count", Long.class,  new ArrayList<>(0)).build(); resultMaps.add(resultMap); builder.resultMaps(resultMaps); builder.resultSetType(ms.getResultSetType()); builder.cache(ms.getCache()); builder.flushCacheRequired(ms.isFlushCacheRequired()); builder.useCache(ms.isUseCache()); return builder.build(); }}

4.4 MyPageHelper插件测试

要测试自定义的 MyPageHelper 插件,首先必须要在 mybatis-config.

<plugins> <!-- 使用自定义插件MyPageHelper --> <plugin interceptor="com.chiaki.mypagehelper.MyPageInterceptor" /></plugins>

然后编写 MyPageHelper 插件的测试方法,如下所示:

@Testpublic void testMyPageHelper() { Integer pageNum = 2, pageSize = 3; StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); MyPageHelper.startPage(pageNum, pageSize); List<Student> students = studentMapper.findAll(); System.out.println(students);}

运行测试方法后,其结果如下图所示。可以看出在数据库表中共有 4 条记录,在设置了 pageNum = 2, pageSize = 3 参数后,查询结果显示的是第二页的数据,仅此 1 条,结果符合预期,这也验证了本节中自定义 MyPageHelper 分页插件的正确性。

https://cdn.jsdelivr.net/gh/IzumiChiaki/CDN/dellImages/MyPageHelper.png

总结

本文首先介绍了 MyBatis 框架使用中比较实用的 Lombok 以及 PageHelper 插件,然后从 PageHelper 插件出发简单介绍了 MyBatis 框架中插件的解析与运行流程,并在此基础上实现了一个自定义的 MyPageHelper 分页插件。笔者认为在实际应用中不必重复造轮子,有好用的插件直接使用就行,大大提高开发效率。但是从另外一个角度看,所谓知其然也要知其所以然,从源码去理解实现原理并能够自己动手实现一遍对于个人的进步是非常有用的。本文中只是很简略地介绍了下 MyBatis 的插件解析与运行过程,实现的 MyPageHelper 插件也处于模仿的层面。读者感兴趣的话可以自行去探究源码,能够在模仿中创新是最好不过了!

参考资料

Lombok 官方社区:https://projectlombok.org/

PageHelper 官方社区:https://pagehelper.github.io/

MyBatis 官网:https://mybatis.org/mybatis-3/

MyBatis 源码仓库:https://github.com/mybatis/mybatis-3

浅析MyBatis(一):由一个快速案例剖析MyBatis的整体架构与运行流程

浅析MyBatis(二):手写一个自己的MyBatis简单框架

🔚🔚🔚

觉得有用的话就点个推荐吧~









原文转载:http://www.shaoqun.com/a/633745.html

跨境电商:https://www.ikjzd.com/

bestbuy:https://www.ikjzd.com/w/394

extra:https://www.ikjzd.com/w/1736


在前面的文章中,笔者详细介绍了🔗MyBatis框架的底层框架与运行流程,并且在理解运行流程的基础上手写了一个自己的MyBatis框架。看完前两篇文章后,相信读者对MyBatis的偏底层原理和执行流程已经有了自己的认知,并且对其在实际开发过程中使用步骤也已是轻车熟路。所谓实践是检验真理的唯一标准,本文将为大家介绍一些MyBatis使用中的一些实用插件与自定义插件。本文涉及到的代码已上传至GitHu
环球b2b:https://www.ikjzd.com/w/1762
邓白氏集团:https://www.ikjzd.com/w/582
拍拍网服装:https://www.ikjzd.com/w/2205
12亿人口,54个国家,比肩英国的新经济体竟无人问津?中企如何快速抢占:https://www.ikjzd.com/home/138411
跨境电商卖家如何理解和选择海外中转仓?:https://www.ikjzd.com/home/21037
深圳检测排长队了?其实,就地过年也很香……:https://www.ikjzd.com/home/141278

没有评论:

发表评论