我们在使用 MyBatis时,经常会用到一些单表查询,但是不得不在 *Mapper.xml 中写简单 sql,而如 selectUserBy(Long id) 这种情况,也需要在 Mapper.xml文件中写select * from user where id = #{id} ,我觉得这是不必要的,也是多余的,不外乎现在也有一些mybatis 插件来帮助我们避免 sql 的编写,如 baomidou 插件,但是对于 baomidou 插件,我也不是很满意,我们来看如下代码:
ListruleOperateResultList = ruleOperateResultService .selectList(new EntityWrapper () .eq("order_no", orderNo) .eq("rule_log_id", id) .orderBy("id",false));
在上述代码中,我觉得蓝色字体的代码是猢多余的,黑色字体如order_no,在java代码中写的变量竟然不是驼峰,让我很难受,如果在 service 层写一个查询方法占用这么多行,让我的业务代码看起来那么臃肿,可读性太差了。我们再来看看插入和更新方法。
在idea中,点击 insert 方法,看哪里引用了 insert,发现这么多的地方使用了,假如user表被插入了一条错误数据,现在我想在代码中找到插入位置,这没法找了。更新方法也存在同样的问题,如 user 表的 status 字段被更新了,想找到代码中字段更新位置,这也找起来也异常艰难。
每一张表中都对应一个 Mapper.java,在 Mapper 方法中定义所有插入和更新的方法,如果想看这张表在哪里插入的,只需要在方法中按住Control+右键,直接查看该方法在代码中引用的位置,如下图
通过上述方法就能找到在方法在代码中引用的位置了,从而找到表插入或更新的业务逻辑,但是这样写带来的一个问题就是,即使写简单的SQL查询或更新方法,都要在 Mapper.xml 中去写 sql,开发效率不高,今天我们的博客就是讲如何解决上述问题。
首先来看看在插件的加驰下,我们是怎样开发的:
文章的未尾提到git 地址 ,lz_mybatis_plugin_project是应用项目。lz_mybatis_plugin主要是插件包项目。
首先在lz_mybatis_plugin项目中运行 mvn clean install
本地打包。
再在lz_mybatis_plugin_project中引用该包。
a.如果你使用的是原生的 mybatis,引入ResolverBeanPostProcessor
@Bean public ResolverBeanPostProcessor resolverBeanPostProcessor (){ return new ResolverBeanPostProcessor(); }
b.如果你引用了baomidou 2.1.9的包的话,需要下面这种写法
@Bean public ResolverBeanPostProcessor resolverBeanPostProcessor() { MyBatisBaomidouServiceImpl impl = new MyBatisBaomidouServiceImpl(); return new ResolverBeanPostProcessor(impl); }
import com.baomidou.mybatisplus.MybatisMapperRegistry; import com.lz.mybatis.plugin.service.MyBatisBaomidouService; import com.lz.mybatis.plugin.utils.SqlParseUtils; import com.lz.mybatis.plugin.utils.t.PluginTuple; import com.lz.mybatis.plugin.utils.t.Tuple2; import org.apache.ibatis.binding.MapperMethod; import org.apache.ibatis.binding.MapperProxyFactory; import org.apache.ibatis.reflection.ParamNameResolver; import java.lang.reflect.Method; import java.util.List; import java.util.Map; public class MyBatisBaomidouServiceImpl implements MyBatisBaomidouService { @Override public void init(ListpluginTuples, org.apache.ibatis.session.Configuration configuration, Class type) { try { MybatisMapperRegistry mapperRegistry = (MybatisMapperRegistry) configuration.getMapperRegistry(); Map , MapperProxyFactory>> knownMappers = SqlParseUtils.getFieldValue(mapperRegistry, "knownMappers"); MapperProxyFactory mapperProxyFactory = knownMappers.get(type); Map methodCache = mapperProxyFactory.getMethodCache(); for (PluginTuple pluginTuple : pluginTuples) { Tuple2 data = pluginTuple.getData(); Method method = data.getSecond(); MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { if (mapperProxyFactory.getMapperInterface() != null) { mapperMethod = new MapperMethod(mapperProxyFactory.getMapperInterface(), method, configuration); } else { mapperMethod = new MapperMethod(type, method, configuration); } MapperMethod.MethodSignature methodSignature = SqlParseUtils.getFieldValue(mapperMethod, "method"); ParamNameResolver paramNameResolver = SqlParseUtils.getFieldValue(methodSignature, "paramNameResolver"); SqlParseUtils.setFieldValue(paramNameResolver, "hasParamAnnotation", data.getFirst()); methodCache.put(method, mapperMethod); } } } catch (Exception e) { e.printStackTrace(); } } }
c.如果你引用了baomidou 3.3.1插件的包的话,需要下面这种写法
@Bean public ResolverBeanPostProcessor resolverBeanPostProcessor() { MyBatisBaomidouServiceImpl myBatisBaomidouService = new MyBatisBaomidouServiceImpl(); return new ResolverBeanPostProcessor(myBatisBaomidouService); }
import com.baomidou.mybatisplus.core.MybatisMapperRegistry; import com.baomidou.mybatisplus.core.override.MybatisMapperMethod; import com.baomidou.mybatisplus.core.override.MybatisMapperProxyFactory; import com.lz.mybatis.plugin.service.MyBatisBaomidouService; import com.lz.mybatis.plugin.utils.SqlParseUtils; import com.lz.mybatis.plugin.utils.t.PluginTuple; import com.lz.mybatis.plugin.utils.t.Tuple2; import org.apache.ibatis.binding.MapperMethod; import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.session.Configuration; import java.lang.reflect.Method; import java.util.List; import java.util.Map; public class MyBatisBaomidouServiceImpl implements MyBatisBaomidouService { @Override public void init(ListpluginTuples, Configuration configuration, Class type) { try { MybatisMapperRegistry mapperRegistry = (MybatisMapperRegistry) configuration.getMapperRegistry(); Map , MybatisMapperProxyFactory>> knownMappers = SqlParseUtils.getFieldValue(mapperRegistry, "knownMappers"); MybatisMapperProxyFactory mapperProxyFactory = knownMappers.get(type); Map methodCache = mapperProxyFactory.getMethodCache(); for (PluginTuple pluginTuple : pluginTuples) { Tuple2 data = pluginTuple.getData(); Method method = data.getSecond(); MybatisMapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { if (mapperProxyFactory.getMapperInterface() != null) { mapperMethod = new MybatisMapperMethod(mapperProxyFactory.getMapperInterface(), method, configuration); } else { mapperMethod = new MybatisMapperMethod(type, method, configuration); } MapperMethod.MethodSignature methodSignature = SqlParseUtils.getFieldValue(mapperMethod, "method"); ParamNameResolver paramNameResolver = SqlParseUtils.getFieldValue(methodSignature, "paramNameResolver"); SqlParseUtils.setFieldValue(paramNameResolver, "hasParamAnnotation", data.getFirst()); methodCache.put(method, mapperMethod); } } } catch (Exception e) { e.printStackTrace(); } } }
为什么不同的 baomidou 版本需要使用不同的 service 呢?原因在于MybatisMapperRegistry,MybatisMapperMethod,MybatisMapperProxyFactory类所在的包不同。因此需要对不同的插件版本做兼容。
可能大家对 baomidou是什么有些疑惑,如下图:
就是引用com.baomidou.mybatis-plus插件而已,我们公司都称其为包米豆。那我也叫他包米豆吧,公司目前只用了这个 mybatis 插件,因此我只对这个插件的两个版本做了兼容,如果你移植到你们公司代码,可能会有其他问题,到时候需要你根据实际情况再做兼容,后面我会对插件做详细解析。希望会对聪明的读者有用。
pom.xml 中加入插件,主要是为了减少 Mapper.java 中的@Param 注解的编写
org.apache.maven.plugins maven-compiler-plugin 3.5.1 1.8 true lines,vars,source -parameters
上述己经将如何配置插件演示过了,下面来看看,在实际开发中如何使用。
CREATE TABLE `lz_test_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生成时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `type` int(11) DEFAULT '0' COMMENT '0', `branch_id` int(11) DEFAULT NULL COMMENT '版本号', `real_name` varchar(256) DEFAULT NULL COMMENT '真实名称', `mobile` varchar(256) DEFAULT NULL COMMENT '手机号码', `username` varchar(256) DEFAULT NULL COMMENT '用户名', `task_id` int(11) DEFAULT NULL COMMENT '任务 id', `staff_id` int(11) DEFAULT '0' COMMENT '员工 id', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=57 DEFAULT CHARSET=utf8mb4 COMMENT='项目用户';
为了保持可读性,简单性,目前只支持单表操作,如果想支持多表操作,聪明的读者,在你看完这篇博客以后,你自己去开发吧。
INSERT INTO `lz_test_user` (`id`, `is_delete`, `gmt_create`, `gmt_modified`, `type`, `branch_id`, `real_name`, `mobile`, `username`, `task_id`, `staff_id`) VALUES (13, 1, '2021-01-19 11:39:49', '2021-01-19 11:39:49', 0, 1, '金x', '1575xxx', '1xxxx', 1, 311), (14, 1, '2021-01-19 11:39:49', '2021-01-19 11:39:49', 0, 1, '李x', '1826xxx', 'zhangsan', 1, 323), (45, 1, '2021-01-19 15:41:11', '2021-01-19 15:41:11', 4, 1, 'x甜', '1575xxx', '1xxx76952', 1, 310), (46, 1, '2021-01-19 15:41:11', '2021-01-19 15:41:11', 4, 1, 'x海', '1526xxx', '1xxx89338', 1, 325), (47, 1, '2021-01-19 15:41:11', '2021-01-19 15:41:11', 5, 1, 'x金', '1760xxx', '1xxx50797', 1, 317), (48, 1, '2021-02-02 14:58:57', '2021-02-02 14:58:57', 0, 10, '张三', '18458195149', 'zhangsan', NULL, 10), (49, 1, '2021-02-02 15:07:08', '2021-02-02 15:07:08', 4, 1, '张三', '18458195149', '18458195149', NULL, 10), (50, 1, '2021-02-02 15:24:22', '2021-02-02 15:24:22', 0, 1, '张三aaa', '18458195149', 'zhangsan', NULL, 10), (51, 1, '2021-02-02 15:25:07', '2021-03-12 15:15:27', 0, 10, '张三xxxxxxx', '123', '张', 10, 10), (52, 1, '2021-03-12 15:14:42', '2021-03-12 15:14:42', 0, 10, '张三', '18458195149', 'zhangsan', NULL, 10), (53, 1, '2021-03-12 15:15:01', '2021-03-12 15:15:01', NULL, 10, '张三', '18458195149', 'zhangsan', NULL, 10), (54, 1, '2021-03-12 15:15:01', '2021-03-12 15:15:01', NULL, 10, '张三', '18258136007', 'zhangsan', NULL, 10), (55, 1, '2021-03-12 15:15:14', '2021-03-12 15:15:14', NULL, 10, '张三', '18458195149', 'zhangsan', NULL, 10), (56, 1, '2021-03-12 15:15:14', '2021-03-12 15:15:14', NULL, 10, '张三', '18258136007', 'zhangsan', NULL, 10);
public interface TestUserMapper extends MyBaseMapper{ //所有的查询条件,默认是 AND 和 = 关系,如果想在其他的关系,可以写相关的注解@OR ,或@Like TestUser selectTestUserById(Long id); @OrderBy({ @By(value = {"id"}, type = OrderType.DESC), }) TestUser selectUserByCondition(Long branchId, @GT int type, @LIKE String realName, UserInfo userInfo); @LIMIT(10) List selectUserByRealName(@LIKE String realName, @LLIKE String mobile); //对于这种情况 taskId 和 staffId 传入的值可以是 null List selectByTaskId(@IsNull Long taskId, @IsNotNull Long staffId); List selectByTaskRealNameMobile(@IsNotEmpty String mobile, @IsEmpty String realName); int countUser(@LIKE String realName); Long insertTestUser(TestUser testUser); Long insertBatchTestUser(List testUsers); Long insertTestUserBatch(TestUser[] testUsers); //目前不支持批量更新 int updateTestUserById(TestUser testUser); //默认使用最后一个作为更新条件 int updateRealNameById(String realName, Long id); //如果想写多个更新条件,在字段前面加 @by注解,值得注意的是,所有的方法参数名称都应该和数据库中的字段对应,在自动生成 sql时,会将驼峰参数名转化为数据库字段 void updateTestUserUserNamePassword(String username, String mobile, @By Long id, @By Long taskId); @Realy int deleteTestUserById(Long id); // @In注解中的值,对应数据库列字段 int deleteTestUserByIds(@IN("id") List ids); //【注意】千万不能这样写,这样写的话,是删除所有的数据 void deleteBatch(); }
@RestController public class TestUserController { @Autowired private TestUserMapper testUserMapper; @RequestMapping("/selectTestUserById") public String test() { TestUser processUser = testUserMapper.selectTestUserById(14l); System.out.println(processUser); return "测试成功"; } @RequestMapping("/selectUserByCondition") public String selectUserByCondition() { UserInfo userInfo = new UserInfo(); userInfo.setStaffId(10l); userInfo.setUsername("18458195149"); TestUser testUser = testUserMapper.selectUserByCondition(1l,1,"张三",userInfo); System.out.println(testUser); return "测试成功"; } @RequestMapping("/selectUserByRealName") public String selectUserByRealName() { ListtestUser = testUserMapper.selectUserByRealName("张三","184"); System.out.println(testUser); return "测试成功"; } @RequestMapping("/selectByTaskId") public String selectByTaskId() { List testUser = testUserMapper.selectByTaskId(null,null); System.out.println(testUser); return "测试成功"; } @RequestMapping("/selectByTaskRealNameMobile") public String selectByTaskRealNameMobile() { List testUser = testUserMapper.selectByTaskRealNameMobile(null,null); System.out.println(testUser); return "测试成功"; } @RequestMapping("/countUser") public String countUser() { int a = testUserMapper.countUser("张"); System.out.println(a); return "测试成功"; } @RequestMapping("/testInsert") public String testInsert() { TestUser testUser = new TestUser(); testUser.setBranchId(10l); testUser.setMobile("18458195149"); testUser.setRealName("张三"); testUser.setStaffId(10l); testUser.setUsername("zhangsan"); testUserMapper.insertTestUser(testUser); System.out.println(testUser); return "测试成功"; } @RequestMapping("/insertTestUserBatch") public String insertBatchTestUser() { TestUser testUser = new TestUser(); testUser.setBranchId(10l); testUser.setMobile("18458195149"); testUser.setRealName("张三"); testUser.setStaffId(10l); testUser.setUsername("zhangsan"); TestUser testUser2 = new TestUser(); testUser2.setBranchId(10l); testUser2.setMobile("18258136007"); testUser2.setRealName("张三"); testUser2.setStaffId(10l); testUser2.setUsername("zhangsan"); List testUsers = new ArrayList<>(); testUsers.add(testUser); testUsers.add(testUser2); testUserMapper.insertBatchTestUser(testUsers); for (TestUser testUser1 : testUsers) { System.out.println(testUser1); } return "测试成功"; } @RequestMapping("/insertTestUserBatchByArray") public String insertTestUserBatchByArray() { TestUser testUser = new TestUser(); testUser.setBranchId(10l); testUser.setMobile("18458195149"); testUser.setRealName("张三"); testUser.setStaffId(10l); testUser.setUsername("zhangsan"); TestUser testUser2 = new TestUser(); testUser2.setBranchId(10l); testUser2.setMobile("18258136007"); testUser2.setRealName("张三"); testUser2.setStaffId(10l); testUser2.setUsername("zhangsan"); TestUser[] testUsers = new TestUser[2]; testUsers[0] = testUser; testUsers[1] = testUser2; testUserMapper.insertTestUserBatch(testUsers); for (TestUser testUser1 : testUsers) { System.out.println(testUser1); } return "测试成功"; } @RequestMapping("/testUpdate1") public String testUpdate1() { TestUser testUser = new TestUser(); testUser.setBranchId(10l); testUser.setMobile("18458195149"); testUser.setRealName("张三xxxxxxx"); testUser.setStaffId(10l); testUser.setUsername("zhangsan"); testUser.setId(51l); testUserMapper.updateTestUserById(testUser); System.out.println(testUser); return "测试成功"; } @RequestMapping("/updateTestUserUserNamePassword") public String updateTestUserUserNamePassword() { testUserMapper.updateTestUserUserNamePassword("张","123",51l,10l); return "测试成功"; } @RequestMapping("/updateRealNameById") public String updateRealNameById() { testUserMapper.updateRealNameById("张三aaa", 50l); return "测试成功"; } @RequestMapping("/deleteTestUserById") public String deleteTestUserById() { testUserMapper.deleteTestUserById(50l); return "测试成功"; } @RequestMapping("/deleteTestUserByIds") public String deleteTestUserByIds() { List ids = new ArrayList<>(); ids.add(43l); ids.add(44l); testUserMapper.deleteTestUserByIds(ids); return "测试成功"; } @RequestMapping("/deleteBatch") public String deleteBatch() { List ids = new ArrayList<>(); ids.add(43l); ids.add(44l); testUserMapper.deleteBatch(); return "测试成功"; } }
接下来,我们对每一个方法分析
TestUser selectTestUserById(Long id);
默认情况下所有的查询条件之间以 AND 拼接,如 selectByUserNamePassword(String userName,String password) ; 插件动态生成的 SQL 是
select * from lz_test_user where user_name = #{userName} and password = #{password}
插件会自动将方法参数的驼峰名转化为数据库中以下划线分割的列名,如上述的userName 被转化为 user_name。如果查询条件之间的关系不是and 关系,是OR 关系,方法该如何编写呢?
List
默认情况下生成的 sql条件参数和方法参数的顺序一致,因此,我们只需要在第二个方法参数前加上@OR 注解即可。上述生成的 sql 是
SELECT * FROM lz_test_user WHERE username = ‘zhangsan’ OR mobile = ‘184581149’ AND IS_DELETE = 0 ,运行结果如下图
如果现在的需要是,username = #{username} and (mobile = #{mobile} or real_name = #{real_name}) 这种情况怎么办呢?我们继续看例子。
@OrderBy({ @By(value = {"id"}, type = OrderType.DESC), }) TestUser selectUserByCondition(Long branchId, @GT int type, @LIKE String realName, UserInfo userInfo); @OR public class UserInfo { @LIKE @AND private String username; @EQ @OR private Long staffId; ... get set 方法省略 }
根据 branchId 查找,并且 type > #{type} and realName like #{realName} and (userInfo) ,我相信大家从字面意思中就能理解上述方法所代码的 sql,我们来测试一下。
SELECT * FROM lz_test_user WHERE branch_id = 1 AND type > 1 AND real_name LIKE CONCAT(’%’,‘张三’,’%’) AND ( username LIKE CONCAT(’%’,‘18458195149’,’%’) OR staff_id = 10 ) AND IS_DELETE = 0 ORDER BY id DESC
大家可能比较困惑,为什么UserInfo对象标明了@OR注解,但是sql () 外还是以 AND 拼接呢?这里声明一下,对象上的注解,只对对象内部的条件控制,如staffId不加@OR 注解,只要UserInfo对象上加了OR 注解,内部的条件都以OR 来连接,这样做的好处就是,当遇到对象内部有很多属性需要以 OR 来连接的话,只需要在对象上加上注解 OR 即可。如果对象内部的条件和外面条件也想以 OR 来连接怎么办呢?只需要在Mapper 方法的对象参数前加上@OR 注解即可,如下
聪明的读者可能现在己经发现了,其实对象的第一个属性加AND或OR注解毫无意义,因为在 sql语句中的()内的第一个条件前是不允许加AND或OR的。又有说伙伴会提问了,你这些都懂,如果查询条件要左like怎么办呢?你不用担心,这也提供了。
List
我们来看看 SQL是怎样的,
SELECT * FROM lz_test_user WHERE real_name LIKE CONCAT(’%’,‘张三’,’%’) AND mobile LIKE CONCAT(’’,‘184’,’%’)AND IS_DELETE = 0
肯定又有机灵的读者会提问,你这些我都知道了,如果需要判断一些字段为空,另外一些字段不是空,那怎么办呢?
List
List selectByTaskRealNameMobile(@IsNotEmpty String mobile, @IsEmpty String realName),那我们来看看sql的区别。
SELECT * FROM lz_test_user WHERE (mobile IS NOT NULL OR mobile != ‘’ ) AND (real_name IS NULL OR real_name = ‘’ ) AND IS_DELETE = 0,细心的读者会发现IsNull和IsEmpty的区别了,主要是在空串的区别,isNull 不对空串处理,IsEmpty对空串处理,IsNotNull和IsNotEmpty的区别类似,接下来肯定又有读者会想了,你这些我知道了,但是对于排序,你又是怎样处理的呢?你不可能写到方法参数中吧,因为排序不一定作为查询条件,你不用担心,我们来继续看下图
看完上图以后,肯定又有人会想,如果先按 id降序,再按 mobile降序呢?
这个时间又有读者在想,如果我还想按username升序怎么办呢?
接下来我们来看生成的 sql
这个时候,读者又会想,如果通过字段来排序,我也不知道,这需要根据前端传过来的来确定是升序还是降序。这个你也不用担心,请看:
在方法的参数中新增了一个 OrderBy 注解,注解的默认值是需要排序的字段,上图中是以 id来排序,String asc 是一个字符串,如果前端传过来是 "ASC"字符串那么,就以 id升序排序,如果前端传过来是"DESC"字符串,以 id 降序排序。
肯定有读者会想,排序好了,我只要要前面的10条,怎么办呢?
假如我想实现手动分页,那该怎么办呢?
可能又有人会想,如果想实现分页,这个时候,limit里的 index 和 offset 不是写死在方法注解之上,而是通过方法参数控制,这个你也不用担心,在我们的方法中,提供了分页方法。
这个方法的使用需要注意,Page后面的泛型可写可不写,同时方法参数中一定要有两个参数分别被@CurrPage ,@PageSize 注解修饰,因为分页一定需要传当前页和 pageSize 两个参数。
接下来我们来谈谈分页这样写的好处。
这是我们之前的代码中使用分页,从这个代码中我们能够看到,除了能看到实现了分页,没有看到任何业务逻辑信息,同时还占用3行代码。还有一种写法如下,使用 PageHelper插件,但是有一个问题,就是在我们查询方法的前面需要写PageHelper.startPage(pn, 5);一行代码,如果有人不小心,在我们的分页方法查询方法前面又写了一个查询方法,这个时候分页地逻辑就不是我们想要的了。
基于上述问题,我之前想过另外一种情况来封装,达到不会被同事不小心,在分页查询代码之前加一行其他的业务方法,如下:
上图写法终究还是没有那么简捷,因此我们这个插件提供了终级方案,接下来,来看如何调用
测试结果:
当你看到了这个方法的使用时,你肯定会想,原来分页是如此简单的事情 。
可能又有读者在想了,如果我想要查询某个条件有多少条数据,那怎么办呢?
int countUser(@LIKE String realName);
那生成的 sql 是怎样的呢?
此时此刻,我相信大部分单表查询都能满足了。至少我用这个插件对于单表查询还没有写过 sql,哈哈。接下来我们来看SQL 的插入操作。
插入数据,主键数据回显,是插件帮我们实现了,后面我们再来分析插件是如何实现。有人可能会问,如果是批量插入数据,那该怎么办?
批量插入数据主键也回显了。可能又有读者会问了,你传入的是集合,如果传入的是数组怎么办呢?
我相信大部分插入操作都是上述两种情况,其他的情况,感兴趣的小伙伴可以去改插件源码 。
上述情况,我们大部分更新操作既是如此,根据id 查询出对象,设置对象数据,再更新到数据库,但这样可能会导致数据问题,如果用户1查询出 User 对象数据返回给前端页面,此时user的 username = “zhangsan”,用户2查询user的数据返回前端username=“zhangsan”,此时用户2将user数据 username改成"lisi" 提交,数据库中的username变成了 lisi,但是用户1修改user 的 mobile 数据为185xxx,此时前端页面的username依然是zhangsan 提交给后台,后台根据 id 查询出用户数据,将 username继续设置成"zhangsan",此时用户2来看,发现之前 user 数据不是设置成 lisi 了吗,怎么又变成了 zhangsan 了呢?莫名其妙,但是在开发的过程中,这种情况时有发生,你说程序错也吗?也没有,只是偶然出现这种情况而已。为了减少这种问题的发生,我们只能,如果用户只更新 username那么就只更新 username 字段,而不是将user的username 和mobile字段都更新一次。那么我们来看在插件的加驰下,如何实现。
此时生成的 sql是UPDATE lz_test_user SET real_name = ‘张三aaa’ where id = 50,机灵的小伙伴肯定晕了,这是怎么回事,我又没有设置其他的选项,他怎么知道 where 后面的条件是 id,而不是 real_name 呢?这是这个插件的规定,如果用户没有设置@By 注解,默认以方法的最后一个参数作为更新条件,那么使用@By注解是如何使用的呢?
我相信开发丰富的小伙伴肯定觉得这个操作很有意思,如果我们每更新一个字段,就需要到 Mapper.xml中写一条 SQL 也太麻烦了,不得已用千篇一律的updateXXXById 方法,但是这个方法也带来不好的一面,就是并发问题。此时,我相信这样操作对你很有帮助,可能有些小伙伴想到批量更新的问题,而我们现实中很少在 mybatis的 xml 文件中写批量更新代码,要么使用插件批量更新,要么一条一条的更新,还有一些像如下写法。
我不是很喜欢这种写法,因为要实现批量更新,首先得设置mysql支持批量操作,在jdbc链接中需要附加&allowMultiQueries=true属性才行 ,同时Mysql 需要执行多条语句,性能上我觉得应该比不上一条语句就实现的所有数据的更新。而我们这个插件原理采用的是第二种方式,利用mysql的自身语法 case…when…then语法来实现。
接下来,我们来看看如何使用
是不是很简单,只需要传一个一 list 集合即可,但是需要注意的一点是,目前批量更新只支持基于主键的批量更新,如果根据对象的多个条件来更新的话,还需要用户自己去写 sql 来实现。
更新的使用说完了,下面我们来看删除如何使用
删除分上述三种情况,根据 ids批量删除,或根据id删除以及删除所有数据,第三种情况,千万不能这样写,因为太危险了,一不小心将表中的数据全部删除了,但是细心的读者可能会发现,为什么deleteTestUserById方法有一个@Realy注解,因为我们公司的所有的数据库表中都有三个字段是固定的is_delete,gmt_create,gmt_modified,因此删除时,如果表中有字段 is_delete,那么默认情况下是逻辑删除,将 is_delete 字段置为1,但是有时候,我们明确知道这条数据不要了,存在表中,占用空间,因此,我们可以在方法上加@Realy注解,表示直接物理删除数据 。根据 id删除多条数据,可以使用 in 注解,但是 in 注解的值需要填写,值为对应的数据库字段。
关于插件的使用,看完上面的说明,大家肯定都了解了,但是这么好用的插件,他的原理是什么呢?接下来,我们将对插件原码进行解析。
首先,插件帮我们动态增加 sql 的入口ResolverBeanPostProcessor。
public class ResolverBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { private Listmappers = new ArrayList<>(); public ApplicationContext ac; private MyBatisBaomidouService myBatisBaomidouService; public ResolverBeanPostProcessor() { } public ResolverBeanPostProcessor(MyBatisBaomidouService myBatisBaomidouService) { this.myBatisBaomidouService = myBatisBaomidouService; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ac = applicationContext; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // 只对 MapperFactoryBean 作处理,非MapperFactoryBean 略过 if (!(bean instanceof MapperFactoryBean)) { return bean; } // 解决多次调用问题,如在项目启动过程中,一个MapperFactoryBean可能会多次调用postProcessAfterInitialization方法 // Mapper 动态生成 sql只需要调用一次就可以了,如果容器多次调用,后面的调用直接略过 if (!check(mappers, beanName)) { return bean; } try { mappers.add(beanName); // 从 Spring 源码中得知,获取bean 的工厂方法,只需要用 & + beanName ,就能从容器中获取创建 bean 的工厂bean Object factoryBean = ac.getBean("&" + beanName); //获取 Mapper的工厂方法 if (factoryBean != null && factoryBean instanceof MapperFactoryBean) { MapperFactoryBean mapperFactoryBean = (MapperFactoryBean) factoryBean; SqlSession sqlSession = mapperFactoryBean.getSqlSession(); Configuration configuration = sqlSession.getConfiguration(); // myBatisBaomidouService 主要是解析版本兼容问题,交给引入包的项目来解决不同版本兼容性问题 CustomerMapperBuilder customerMapperBuilder = new CustomerMapperBuilder(configuration, mapperFactoryBean.getObjectType(), myBatisBaomidouService); // 调用 parse 方法,解析 *Mapper.java中的方法,动态生成sql并保存到org.apache.ibatis.session.Configuration中 customerMapperBuilder.parse(); } } catch (BeansException e) { e.printStackTrace(); } finally { mappers.add(beanName); } return bean; } public synchronized boolean check(List mappers, String beanName) { if (mappers.contains(beanName)) { return false; } return true; } }
ResolverBeanPostProcessor实现了ApplicationContextAware接口和BeanPostProcessor接口。
首先我们来看看ApplicationContextAware。
public interface ApplicationContextAware extends Aware { void setApplicationContext(ApplicationContext var1) throws BeansException; }
那么setApplicationContext何时调用呢?
上图一系列的调用中得知,在resolverBeanPostProcessor实例化时,当属性设置完成,将调用其initializeBean,接着调用applyBeanPostProcessorsBeforeInitialization方法,获取容器中所有的BeanPostProcessor,再调用BeanPostProcessor的postProcessBeforeInitialization方法,ResolverBeanPostProcessor类的setApplicationContext方法是在调用 ApplicationContextAwareProcessor的postProcessBeforeInitialization方法时调用。在ApplicationContextAwareProcessor中继续调用invokeAwareInterfaces方法,接下来我们来看看invokeAwareInterfaces方法的内部实现。
private void invokeAwareInterfaces(Object bean) { if (bean instanceof Aware) { if (bean instanceof EnvironmentAware) { ((EnvironmentAware)bean).setEnvironment(this.applicationContext.getEnvironment()); } if (bean instanceof EmbeddedValueResolverAware) { ((EmbeddedValueResolverAware)bean).setEmbeddedValueResolver(this.embeddedValueResolver); } if (bean instanceof ResourceLoaderAware) { ((ResourceLoaderAware)bean).setResourceLoader(this.applicationContext); } if (bean instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware)bean).setApplicationEventPublisher(this.applicationContext); } if (bean instanceof MessageSourceAware) { ((MessageSourceAware)bean).setMessageSource(this.applicationContext); } if (bean instanceof ApplicationContextAware) { ((ApplicationContextAware)bean).setApplicationContext(this.applicationContext); } } }
在invokeAwareInterfaces方法中有一个非常重要的条件,如果 bean 实现了ApplicationContextAware,则将当前容器【ApplicationContext】通过setApplicationContext方法注入bean中,在 bean中就得到了当前容器的引用,拿到当前容器引用,我们就可以对当前容器中所有的 bean 进行"为所欲为"的查询及修改了。
接下来我们继续看BeanPostProcessor接口。
public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; }
这个接口分别提供了两个接口方法 postProcessBeforeInitialization和postProcessAfterInitialization,这两个方法有什么用呢?在 bean的生命周期中起到什么样的作用呢?我们来看一张 bean的生命周期图,这个图非常重要,几乎 Spring 所有Bean的生命周期都遵循这张图。
从上图中得知,在容器中的任何 Bean 在创建过程中,都会被实现了BeanPostProcessor接口的 bean拦截到,ResolverBeanPostProcessor实现BeanPostProcessor接口,主要是为了拦截到Spring 容器中所有被创建的 Bean,当发现是我们要找的Mapper.java时,我们就可以对其"动手动脚"了。
可能有读者会问了,你为什么不在InitializingBean 的 AfterPropertiesSet()方法中去增强Mapper而是在BeanPostProcessor的 postProcessAfterInitialization中去拦截呢?请听我尾尾道来。
首先我们来看看testUserMapper的BeanDefinition注册,
在代码中寻寻觅觅,我们发现,最终是调用ClassPathMapperScanner的 doScan 方法,才将我们的 Mapper.java 的BeanDefinition注册到容器中。
而又有人会问,basePackages,又是在我们的业务代码中哪里设置的呢?
相信 MapperScan注解大家不陌生吧,我们再来看看SpringBooot启动类。
在启动类中配置了MapperScan注解,我们来看看 MapperScan注解的内容。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(MapperScannerRegistrar.class) public @interface MapperScan { String[] value() default {}; String[] basePackages() default {}; Class>[] basePackageClasses() default {}; Class extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; Class extends Annotation> annotationClass() default Annotation.class; Class> markerInterface() default Class.class; String sqlSessionTemplateRef() default ""; String sqlSessionFactoryRef() default ""; Class extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class; }
从 MapperScan 的内容看,最常用的就是指定Mapper.java的包路径,但是有一个factoryBean属性引起了我的注意,我觉得可以在 MapperScan指定一个自定义CustomerMapperFactoryBean,并继承MapperFactoryBean从而来对 Mapper.java类增强
发现MapperFactoryBean继承了 DaoSupport,并且实现了InitializingBean接口,在 bean 的生命周期中肯定会调用afterPropertiesSet方法,那我们在CustomerMapperFactoryBean中重写afterPropertiesSet方法
但很遗憾的是,DaoSupport中的afterPropertiesSet方法是 final修饰的,不能被覆盖。
但是细心的读者肯定会发现在DaoSupport中有一个方法initDao是提供给子类覆盖的,那在这里来实现我们的Mapper增强,结果会怎样。
public abstract class DaoSupport implements InitializingBean { protected final Log logger = LogFactory.getLog(getClass()); @Override public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException { checkDaoConfig(); try { initDao(); } catch (Exception ex) { throw new BeanInitializationException("Initialization of DAO failed", ex); } } protected abstract void checkDaoConfig() throws IllegalArgumentException; protected void initDao() throws Exception { } }
public class CustomerMapperFactoryBean extends MapperFactoryBean { protected void initDao() throws Exception { SqlSession sqlSession = getSqlSession(); Configuration configuration = sqlSession.getConfiguration(); // myBatisBaomidouService 主要是解析版本兼容问题,交给引入包的项目来解决不同版本兼容性问题 MyBatisBaomidouService myBatisBaomidouService = new MyBatisBaomidouServiceImpl(); CustomerMapperBuilder customerMapperBuilder = new CustomerMapperBuilder(configuration, this.getObjectType(), myBatisBaomidouService); // 调用 parse 方法,解析 *Mapper.java中的方法,动态生成sql并保存到org.apache.ibatis.session.Configuration中 customerMapperBuilder.parse(); } }
此时,我觉得代码非常的优雅,我们来看看测试结果。
但遗憾的是程序出错了,解决了一会问题,没有解决掉,感兴趣的小伙伴可以根据我的思路来尝试一下,说不定你就解决了呢?如果有解决方案,请留言告诉我,我们一起学习,一起进步。那问题先放到这里吧,以后有时间再来研究。
恢复一下代码。我们继续来调试,我们从下图中看到了ClassPathMapperScanner类的继承关系,ClassPathMapperScanner继承ClassPathBeanDefinitionScanner,而在ClassPathMapperScanner中并没有自己实现 doScan 方法,而是调用父类的 doScan 方法。
接下来,我们来看看 doScan的内部实现,从下面代码中得知原理其实很简单,就是遍历所有包,并且获取每一个包下所有Bean的 BeanDefinition,但是不要小看下面的代码,findCandidateComponents方法内部实现极其复杂,在我之前的博客中专门写了一篇博客就是为了讲解这个方法,因为在 Spring内部需要通过 ASM 技术,解析java类字节码才能得到 Bean 的定义,包括注解解析,常量池解析,方法解析,等等,因此,在这里就不做深入研究了。
protected SetdoScan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); 创建bean定义的holder对象用于保存扫描后生成的bean定义对象 // 创建一个集合,存入扫描到的Bean 定义的封装类 Set beanDefinitions = new LinkedHashSet (); //循环我们的包路径集合 // 遍历扫描所给定的包 for (String basePackage : basePackages) { // 找到候选的 @Component // 调用父类的 ClassPathScanningCandidateComponentProvider 的 findCandidateComponents 方法 // 扫描给定类路径的,获取符合条件的Bean的定义 // 类路径的Bean定义扫描 ClassPathBeanDefinitionScanner 主要通过 findCandidateComponents() 方法调用其父类 ClassPathScanningCandidateComponentProvider // 来扫描获取给定包及其子包的类 Set candidates = findCandidateComponents(basePackage); // 遍历扫描得到的Bean for (BeanDefinition candidate : candidates) { // 获取Bean定义类中的@Scope注解的值,即获取Bean的作用域 ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); // 为Bean设置注解配置的作用域 candidate.setScope(scopeMetadata.getScopeName()); //设置我们的beanName,为Bean生成名称 String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); // 处理@AutoWired相关的 // 如果扫描到Bean不是Spring的注解Bean,则为Bean设置默认值 // 设置Bean的自动依赖注入装配属性等 if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } //处理jsr250相关的组件,如果扫描到的Bean是Spring的注解的Bean,则处理其通用的注解 if (candidate instanceof AnnotatedBeanDefinition) { // 处理注解Bean中通过的注解,在分析注解Bean定义类读取器时已经分析过了 AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } //把我们解析出来的组件bean定义注册到Spring IoC容器中,根据Bean名称检查指定的Bean是否需要在容器注册,或者是否是容器中 // 有冲突。 if (checkCandidate(beanName, candidate)) { LogUtils.info(" scan bean Name :" + beanName); BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); // 根据注解中的配置的作用域,为Bean的应用的代理模式 definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); beanDefinitions.add(definitionHolder); //注册到Spring IoC容器中,向容器注册扫描到的Bean registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; }
在ClassPathBeanDefinitionScanner解析中,我们只得到了普通的 BeanDefinition定义,如下图所示:
那么在ClassPathMapperScanner中,又对BeanDefinition 做了哪些"添油加醋"的操作呢?我们接下来看processBeanDefinitions方法
private void processBeanDefinitions(SetbeanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); if (logger.isDebugEnabled()) { logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface"); } // mapper接口是bean的原始类,但是,bean的实际类是MapperFactoryBean definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59 //设置 bean的实际类型为mapperFactoryBean definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); boolean explicitFactoryUsed = false; //如果 MapperScan 中设置了sqlSessionFactoryRef,添加sqlSessionFactory属性引用 if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionFactory != null) { definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory); explicitFactoryUsed = true; } //如果 MapperScan的sqlSessionTemplateRef属性不为空,设置sqlSessionTemplate的引用 if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionTemplate != null) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); explicitFactoryUsed = true; } if (!explicitFactoryUsed) { if (logger.isDebugEnabled()) { logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); } definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
可能有读者不知道mapperFactoryBean属性是中哪里赋值的,看下面代码
private MapperFactoryBean> mapperFactoryBean = new MapperFactoryBean
如果MapperScan注解中没有配置factoryBean,默认情况下使用MapperFactoryBean作为 bean 类型,如果指定了,则使用指定类型。上述代码中还有一个非常重要方法调用,getConstructorArgumentValues.addGenericArgumentValue(),将当前Mapper的名称赋值给构造函数参数,这样做的用意是什么呢?我们来看看MapperFactoryBean的构造函数。
在MapperFactoryBean中有两个构造函数,一个无参数的构造函数,一个有参数的构造函数,有参数的构造函数接收一个接口参数,因此,我们将当前 Mapper的类名称添加到BeanDefinition 的genericArgumentValues属性中,在创建MapperFactoryBean 时,会调用有参数的构造方法,同时将 Mapper 接口作为参数传入。
理解了processBeanDefinitions方法,我们又来看看 MapperFactoryBean 的继承关系。如下图
我们发现MapperFactoryBean竟然继承 FactoryBean,那么说明此时的 Mapper是一个工厂Bean,在创建 Mapper 实例时,会调用工厂方法的getObject()方法来创建实例存储于容器中。我们再来看MapperFactoryBean中的方法。
上图中的每个方法都很重要。我们先来看看setSqlSessionFactory和setSqlSessionTemplate的实现,这两个方法是 MapperFactoryBean 从SqlSessionDaoSupport中继承而来。方法的实现很简单,就是给 MapperFactoryBean 的sqlSession属性赋值。那么可能又会有小伙伴会问sqlSessionFactory又是从哪里来,又是何时创建的呢?
小伙伴别急,请听我娓娓道来,在 Spring中任何一个 Bean的创建都依赖于Bean的 BeanDefinition,那么我们要找到sqlSessionFactory的创建过程,先要找到在哪里创建sqlSessionFactory的 BeanDefinition。
经过我在代码中寻寻觅觅,终于找到了sqlSessionFactory的 BeanDefinition 的创建。
首先,我们来看ConfigurationClassParser的doProcessConfigurationClass方法。
在ConfigurationClassParser的retrieveBeanMethodMetadata方法中。
private SetretrieveBeanMethodMetadata(SourceClass sourceClass) { AnnotationMetadata original = sourceClass.getMetadata(); Set beanMethods = original.getAnnotatedMethods(Bean.class.getName()); if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) { //尝试通过ASM读取类文件以获得确定性声明顺序... ,不幸的是, //JVM的标准反射以任意顺序返回方法,即使在同一JVM上同一应用程序的不同运行之间也是如此。 try { AnnotationMetadata asm = this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata(); //使用ASM读取类字节码获取注解了@Bean 注解的方法注解元数据 Set asmMethods = asm.getAnnotatedMethods(Bean.class.getName()); if (asmMethods.size() >= beanMethods.size()) { Set selectedMethods = new LinkedHashSet (asmMethods.size()); for (MethodMetadata asmMethod : asmMethods) { for (MethodMetadata beanMethod : beanMethods) { if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) { selectedMethods.add(beanMethod); break; } } } if (selectedMethods.size() == beanMethods.size()) { //在ASM方法集中找到的所有反射检测方法->继续 beanMethods = selectedMethods; } } } catch (IOException ex) { logger.debug("Failed to read class file via ASM for determining @Bean method order", ex); // No worries, let's continue with the reflection metadata we started with... } } return beanMethods; }
将配置了@Bean 注解的方法元数据构建成BeanMethod对象添加到configClass的beanMethods属性中,我们再来看看MybatisAutoConfiguration的两个方法sqlSessionFactory(),sqlSessionTemplate()方法,这两个方法分别返回了SqlSessionFactory和SqlSessionTemplate对象,而这两个方法的注解元数据被加入到configClass的beanMethods属性中,接下来我们继续看是如何构建sqlSessionFactory的 BeanDefinition。
在invokeBeanFactoryPostProcessors方法中,所有配置了@Configuration 注解的 Bean ,并遍历其beanMethods,为 Bean构建 BeanDefinition。
调用所有配置了@Configuration注解的类,并调用loadBeanDefinitionsForConfigurationClass方法来对配置了@Bean 注解的方法构建BeanDefinition。
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) { if (trackedConditionEvaluator.shouldSkip(configClass)) { String beanName = configClass.getBeanName(); if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { this.registry.removeBeanDefinition(beanName); } this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName()); return; } if (configClass.isImported()) { registerBeanDefinitionForImportedConfigurationClass(configClass); } //遍历configClass的所有 BeanMethod对象,为其方法返回的类型添加 BeanDefinition for (BeanMethod beanMethod : configClass.getBeanMethods()) { loadBeanDefinitionsForBeanMethod(beanMethod); } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); }![在这里插入图片描述](https://img-blog.csdnimg.cn/20210317194157456.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3F1eWl4aWFv,size_16,color_FFFFFF,t_70)
相信此此刻,大家非常清楚 MapperFactoryBean中的sqlSessionFactory和sqlSessionTemplate的来源了,来自于MybatisAutoConfiguration的sqlSessionFactory()和sqlSessionTemplate()方法的调用,而关于sqlSessionFactory需要传入dataSource,这里就不再深究了,感兴趣的小伙伴可以去研究一下,sqlSessionFactory中的 DataSource又是何时注入的呢?
@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setVfs(SpringBootVFS.class); if (StringUtils.hasText(this.properties.getConfigLocation())) { factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation())); } Configuration configuration = this.properties.getConfiguration(); if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) { configuration = new Configuration(); } if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { for (ConfigurationCustomizer customizer : this.configurationCustomizers) { customizer.customize(configuration); } } //设置数据源 databaseId,如 MySql,Oracle 等 factory.setConfiguration(configuration); if (this.properties.getConfigurationProperties() != null) { factory.setConfigurationProperties(this.properties.getConfigurationProperties()); } //设置 MyBatis中拦截器插件,如本项目中使用到的DataScopeInterceptor拦截器 if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); } if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); } //设置type-aliases-package if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); } //设置 type-handlers-package if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { factory.setTypeHandlersPackage(this.properties// 设置mapper-locations if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { factory.setMapperLocations(this.properties.resolveMapperLocations()); } return factory.getObject(); } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } }
在上述方法中主要是一些MyBatis 的 Configuration 的属性设置,可能又会有小伙伴会想了,properties对象MybatisProperties参数值是从哪里来的呢?我们先来看看
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX) public class MybatisProperties { public static final String MYBATIS_PREFIX = "mybatis"; //MyBatis xml配置文件的位置 private String configLocation; //MyBatis Mapper 文件位置 private String[] mapperLocations; //MyBatis 类型别名所在包,可以以,; \t\n 隔开 private String typeAliasesPackage; //MyBatis 类型处理器所有包 ,可以以 ,; \t\n 隔开 private String typeHandlersPackage; //指示是否执行MyBatis xml配置文件的存在性检查。 private boolean checkConfigLocation = false; private ExecutorType executorType; //MyBatis配置的外部化属性。 private Properties configurationProperties; //用于自定义默认设置的Configuration对象。如果指定了{@link #configLocation} *,则不使用此属性 @NestedConfigurationProperty private Configuration configuration; ... get set 方法省略 public Resource[] resolveMapperLocations() { ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); Listresources = new ArrayList (); if (this.mapperLocations != null) { for (String mapperLocation : this.mapperLocations) { try { Resource[] mappers = resourceResolver.getResources(mapperLocation); resources.addAll(Arrays.asList(mappers)); } catch (IOException e) { // ignore } } } return resources.toArray(new Resource[resources.size()]); } }
public SqlSessionFactory getObject() throws Exception { if (this.sqlSessionFactory == null) { afterPropertiesSet(); } return this.sqlSessionFactory; }
public void afterPropertiesSet() throws Exception { notNull(dataSource, "Property 'dataSource' is required"); notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), "Property 'configuration' and 'configLocation' can not specified with together"); this.sqlSessionFactory = buildSqlSessionFactory(); }
protected SqlSessionFactory buildSqlSessionFactory() throws IOException { Configuration configuration; XMLConfigBuilder xmlConfigBuilder = null; if (this.configuration != null) { configuration = this.configuration; if (configuration.getVariables() == null) { configuration.setVariables(this.configurationProperties); } else if (this.configurationProperties != null) { configuration.getVariables().putAll(this.configurationProperties); } } else if (this.configLocation != null) { xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties); configuration = xmlConfigBuilder.getConfiguration(); } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration"); } configuration = new Configuration(); if (this.configurationProperties != null) { configuration.setVariables(this.configurationProperties); } } if (this.objectFactory != null) { configuration.setObjectFactory(this.objectFactory); } if (this.objectWrapperFactory != null) { configuration.setObjectWrapperFactory(this.objectWrapperFactory); } if (this.vfs != null) { configuration.setVfsImpl(this.vfs); } if (hasLength(this.typeAliasesPackage)) { String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeAliasPackageArray) { configuration.getTypeAliasRegistry().registerAliases(packageToScan, typeAliasesSuperType == null ? Object.class : typeAliasesSuperType); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases"); } } } if (!isEmpty(this.typeAliases)) { for (Class> typeAlias : this.typeAliases) { configuration.getTypeAliasRegistry().registerAlias(typeAlias); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type alias: '" + typeAlias + "'"); } } } if (!isEmpty(this.plugins)) { for (Interceptor plugin : this.plugins) { configuration.addInterceptor(plugin); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered plugin: '" + plugin + "'"); } } } if (hasLength(this.typeHandlersPackage)) { String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeHandlersPackageArray) { configuration.getTypeHandlerRegistry().register(packageToScan); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers"); } } } if (!isEmpty(this.typeHandlers)) { for (TypeHandler> typeHandler : this.typeHandlers) { configuration.getTypeHandlerRegistry().register(typeHandler); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type handler: '" + typeHandler + "'"); } } } if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls try { configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource)); } catch (SQLException e) { throw new NestedIOException("Failed getting a databaseId", e); } } if (this.cache != null) { configuration.addCache(this.cache); } if (xmlConfigBuilder != null) { try { xmlConfigBuilder.parse(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'"); } } catch (Exception ex) { throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex); } finally { ErrorContext.instance().reset(); } } if (this.transactionFactory == null) { this.transactionFactory = new SpringManagedTransactionFactory(); } configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource)); if (!isEmpty(this.mapperLocations)) { for (Resource mapperLocation : this.mapperLocations) { if (mapperLocation == null) { continue; } try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'"); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found"); } } return this.sqlSessionFactoryBuilder.build(configuration); }
上述代码我相信大家并不陌生,只是将 mybatis 配置在 xml 文件中的配置搬到了 mybatis-spring包中实现。在mybatis.xml配置如下
如果我们配置了configLocation,也就是Spring中配置了mybatis-config.xml 的路径,Spring会调用xmlConfigBuilder.parse();方法解析 mybatis配置文件。
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; } private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
相信上面的代码大家很熟悉了,Spring调用了MyBatis的源码来解析用户自己配置的mybatis-config.xml文件。关于内部源码的解析,之前写过一篇博客专门解析 MyBatis 是如何解析配置文件及Mapper.xml文件的,这里就不再赘述。
那么Spring又会怎样做呢?请看
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); //解析 XML 对应的 Mapper.java 文件中的mybatis标签注解 bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } }
相信细心的小伙伴肯定会发现,上面的标签好熟悉,你想对了,这不正是我们的Mapper.xml 文件中的标签吗?
接下来我们继续看SqlSessionFactory的构建。最终我们发现是创建了DefaultSqlSessionFactory作为SqlSessionFactory对象。sqlSessionTemplate对象的创建,和SqlSession 一样,这里就不再赘述。
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
关于setSqlSessionFactory()和setSqlSessionTemplate()方法我们终于分析完毕,下面我们来看看其他方法。我们先来看看MapperFactoryBean的afterPropertiesSet方法。
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException { checkDaoConfig(); try { initDao(); } catch (Exception ex) { throw new BeanInitializationException("Initialization of DAO failed", ex); } } protected abstract void checkDaoConfig() throws IllegalArgumentException; protected void initDao() throws Exception { }
protected void checkDaoConfig() { super.checkDaoConfig(); notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration(); //addToConfig默认为 true,如果Configuration的mapperRegistry对象的 //knownMappers集合中并没有 Mapper,则加入到集合中 if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { configuration.addMapper(this.mapperInterface); } catch (Exception e) { logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e); throw new IllegalArgumentException(e); } finally { ErrorContext.instance().reset(); } } }
在MapperFactoryBean的afterPropertiesSet方法实际上只调用了checkDaoConfig方法,因为initDao默认是空实现,并没有什么业务逻辑,而在checkDaoConfig方法中,将接口 Mapper 加入到configuration的mapperRegistry对象的knownMappers属性中,我们来看看内部实现。
publicvoid addMapper(Class type) { mapperRegistry.addMapper(type); }
publicvoid addMapper(Class 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 parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
因为之前所有的*.Mapper.xml 己经被MyBatis解析,此时只能基于 Mapper. java配置了注解方法进行解析。我们继续来看parse方法。
public MapperAnnotationBuilder(Configuration configuration, Class> type) { String resource = type.getName().replace('.', '/') + ".java (best guess)"; this.assistant = new MapperBuilderAssistant(configuration, resource); this.configuration = configuration; this.type = type; ... } public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { //如果有 Mapper对应的 Xml 没有被加载,加载 Mapper.java 包下的 Mapper.xml文件 loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
isResourceLoaded和addLoadedResource方法,我相信很好理解,如 TestUserMapper.java 文件在解析的过程中,如果外部继续调用parse方法,则不再进行解析。loadXmlResource方法的用意就是在解析 Mapper.java中的注解时,如果 Mapper.xml没有被解析,则先解析 Mapper.xml。但是Mapper.xml文件的路径有要求,必需是和 Mapper.java 是在同一个路径下。
private void loadXmlResource() { if (!configuration.isResourceLoaded("namespace:" + type.getName())) { String xmlResource = type.getName().replace('.', '/') + ".xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource); } catch (IOException e) { // ignore, resource is not required } if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); xmlParser.parse(); } } }
我们又看到了isResourceLoaded方法,意思就是如果 Mapper.xml 己经被解析了,就不再解析 Mapper.java 对应的 Mapper.xml 文件,但是"namespace:" + type.getName()是在何时被加入到Configuration对象的loadedResources中的呢?带着疑问,我们在代码中寻寻觅觅。发现在解析 Mapper.xml 文件后,调用了如下方法,在如下方法中添加了"namespace:" + type.getName()到loadedResources中
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) { if (!configuration.hasMapper(boundType)) { configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType); } } } }
其实从上述源码解析中,我们得出一个结论,在解析完 xml 后,MyBatis 会找到命名空间对应的 Mapper.java 接口,并解析接口方法中配置的 MyBatis 注解标签。其实只要我们配置了 mapper-locations
并且 Mapper.xml 文件中命名空间配置正确,其实@MapperScan注解配置或者不配置都不重要,因为 Mapper.xml解析完成以后,肯定会找到命名空间对应的 Mapper.java 并解析其配置的 Mybatis 注解标签。
上述中己经将 Mapper.xml 文件及 Mapper.java 文件都己经被解析完了,因此我们来看看MapperFactoryBean的getObject方法。
public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); }
@Override publicT getMapper(Class type) { return configuration. getMapper(type, this); }
publicT getMapper(Class type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }
publicT getMapper(Class type, SqlSession sqlSession) { final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory ) 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); } }
public T newInstance(SqlSession sqlSession) { final MapperProxymapperProxy = new MapperProxy return newInstance(mapperProxy); } protected T newInstance(MapperProxy(sqlSession, mapperInterface, methodCache); mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
代码跟进到这里,我相信大家己经明白了,最终Spring 容器中存储的TestUserMapper,竟然是一个JDK代理对象,而代理工厂是MapperProxy,那么我们继续来看MapperProxy的 invoke 方法。
public class MapperProxyimplements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class mapperInterface; private final Map methodCache; public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { //如果是 Object 中的方法,直接反射调用,如 getClass,hashCode,equals,clone方法等 return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } .... }
上述中真正重要的是execute方法,接下来看看execute方法的内部实现。
public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { 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); } 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; } }
因此我们在用TestUser testUser = testUserMapper.selectUserByCondition(1l, 1, “张三”, userInfo);调用selectUserByCondition方法,实际上调用的是JDK代理对象的 invoke 方法。在代理对象中实现了复杂的逻辑,并将结果值返回到业务代码中来,关于 execute 方法内部是如何实现的,在之前的博客中也己经详细的解析过了,这里也不再赘述,其实这篇博客对 mybatis原理的解析,之前的博客中己经或多或者的提及过了,现在只不过是为了更加好的理解插件的开发原理,才详细的说明,我相信此时此刻,我们对 mybatis 的原理及 SpringBoot中 mybatis整合有了一定的了解了,接下来我们再来看我们插件的实现。在了解插件之前,我们再次来看看Spring中Bean 的生命周期。
为了避免对己有的 Mapper.xml及 Mapper.java 代码的影响,我们只能将代码加到 MapperFactoryBean的 checkDaoConfig()方法之后,本来我们可以加到initDao方法中的,但是为了避免对MapperFactoryBean的修改,还是加到postProcessAfterInitialization中吧,之前也测试过,报错,我这边也不去究其原因了,感兴趣的小伙伴可以去研究一下,我觉得加到 initDao方法中,代码会更加优雅。
接下来我们继续看ResolverBeanPostProcessor的postProcessAfterInitialization方法的内部实现。
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // 只对 MapperFactoryBean 作处理,非MapperFactoryBean 略过 if (!(bean instanceof MapperFactoryBean)) { return bean; } // 解决多次调用问题,如在项目启动过程中,一个MapperFactoryBean可能会多次调用postProcessAfterInitialization方法 // Mapper 动态生成 sql只需要调用一次就可以了,如果容器多次调用,后面的调用直接略过 if (!check(mappers, beanName)) { return bean; } try { mappers.add(beanName); // 从 Spring 源码中得知,获取bean 的工厂方法,只需要用 & + beanName ,就能从容器中获取创建 bean 的工厂bean Object factoryBean = ac.getBean("&" + beanName); //获取 Mapper的工厂方法 if (factoryBean != null && factoryBean instanceof MapperFactoryBean) { MapperFactoryBean mapperFactoryBean = (MapperFactoryBean) factoryBean; SqlSession sqlSession = mapperFactoryBean.getSqlSession(); Configuration configuration = sqlSession.getConfiguration(); // myBatisBaomidouService 主要是解析版本兼容问题,交给引入包的项目来解决不同版本兼容性问题 CustomerMapperBuilder customerMapperBuilder = new CustomerMapperBuilder(configuration, mapperFactoryBean.getObjectType(), myBatisBaomidouService); // 调用 parse 方法,解析 *Mapper.java中的方法,动态生成sql并保存到org.apache.ibatis.session.Configuration中 customerMapperBuilder.parse(); } } catch (BeansException e) { e.printStackTrace(); } finally { mappers.add(beanName); } return bean; }
上述代码其实很简单,我们想获取 bean的工厂bean,只需要在 bean的名称前加上&,就能从容器中获取工厂 bean 了,Spring 内部就是这样写死的,MapperFactoryBean中通过setSqlSessionFactory()方法,己经将DefaultSqlSessionFactory对象设置到sqlSession中,因此我们就可以通过sqlSession得到configuration,及 DataSource。
上述 parse()方法是不是和下面的方法很相似,你猜得没错,就是一样的,接下来,我们来看看parse方法的内部实现。
public class CustomerMapperBuilder extends MapperAnnotationBuilder { private final Set> customerAnnotationTypes = new HashSet >(); private Configuration configuration = null; private MapperBuilderAssistant assistant = null; private Class> type = null; private static JdbcTemplate jdbcTemplate = null; private String tableName; private List tableColumns; private List primaryColumns; public final static String TABLENAME = "TableName"; public MyBatisBaomidouService myBatisBaomidouService; protected final TypeAliasRegistry typeAliasRegistry; private Class entityType; public CustomerMapperBuilder(Configuration configuration, Class> type, MyBatisBaomidouService myBatisBaomidouService) { super(configuration, type); String resource = type.getName().replace('.', '/') + ".java (best guess)"; this.assistant = new MapperBuilderAssistant(configuration, resource); this.configuration = configuration; this.type = type; this.myBatisBaomidouService = myBatisBaomidouService; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); try { // 有些 mybatis Configuration 的 useGeneratedKeys 字段没有默认设置为true,为了保险起见,还是调用一下setUseGeneratedKeys // 设置其默认值为 true Method method = Configuration.class.getMethod("setUseGeneratedKeys", boolean.class); if (method != null) { // invoke setUseGeneratedKeys set value true method.invoke(configuration, true); //默认不需要写@Param注解,就能在 sql 中引用方法参数名称 } } catch (Exception e) { e.printStackTrace(); } // 获取 Datasource 构建jdbcTemplate ,主要用途是用来在项目启动时获取数据库表中的所有字段 if (jdbcTemplate == null) { final Environment environment = configuration.getEnvironment(); DataSource dataSource = environment.getDataSource(); jdbcTemplate = new JdbcTemplate(dataSource); } // 获取表名,看Mapper的继承类中有没有配置泛型,如果配置泛型,看泛型对象是否有@TableName注解,如果有@TableName注解 // 获取@TableName注解的 value 作为表名称 tableName = SqlParseUtils.findTableName(type); entityType = SqlParseUtils.findEntityType(type); //找到实体名称 if (StringUtils.isEmpty(tableName)) { // tableName = SqlParseUtils.getAnnotationValueByTypeName(type, TABLENAME); } //如果表名为空,则直接退出 if (StringUtils.isEmpty(tableName)) { return; } //通过 jdbc 获取表信息,主要是表的主键列 和 表的所有列 Tuple2 , List
> tableInfos = getTableInfo(jdbcTemplate, tableName).getData(); primaryColumns = tableInfos.getFirst(); tableColumns = tableInfos.getSecond(); // 初始化 Select ,Insert ,Update,Delete 注解,如: // 我们在 Mapper 中定义了一个方法 selectUser(String username) ; 但是方法上确配置了@Delete注解 // 那么最后动态生成的 sql 是 delete from user where username = #{username} // 如果selectUser没有配置 @Delete 注解,那么生成的 sql 是 select * from user where username = #{username} // Select ,Insert ,Update,Delete 注解的作用主要是指定方法的是查询 ,更新,插入,还是删除 // 默认情况下,方法名以: // select 开头的方法是 查询操作 // update 开头的方法是 更新操作 // insert 开头的方法是 插入操作 // delete 开头的方法是 删除操作 customerAnnotationTypes.add(Select.class); customerAnnotationTypes.add(Insert.class); customerAnnotationTypes.add(Update.class); customerAnnotationTypes.add(Delete.class); }
相信读者看了上述代码以后,觉得也不难,无非就是根据表名获取表中的主键列和表中所有的列而已,而需要注意的是,默认情况下,select 开头的方法,表示 select 操作,delete 开头的方法,表示 delete操作,在customerAnnotationTypes集合中添加了插件自定义的注解Select,Insert,Update,Delete,主要是为了解决,如果 delete 开头的方法,我想要 select 操作,怎么办,直接在 delete 方法上加上@Select 注解即可,PluginTuple对象只是一个元组而已,java 中不支持元组,但是我想用元组,只能自定义了。
public void parse() { // 获取 *Mapper.java 中的所有方法 Method[] methods = type.getMethods(); // 设置名称空间 assistant.setCurrentNamespace(type.getName()); // PluginTuple 是一个java元组,类似于 python 中的元组 ListpluginTuples = new ArrayList<>(); for (Method method : methods) { try { String methodName = method.getName(); // 以currentNamespace + . + methodName 构建 id ,在 mybatis 源码中也是这样构建 id的 // 这个 id 在 configuration 中唯一,以 id为key,保存 mappedStatement String id = assistant.applyCurrentNamespace(methodName, false); MappedStatement mappedStatement = null; try { mappedStatement = configuration.getMappedStatement(id); } catch (Exception e) { } // 如果 Mapper 中的方法没有对应的mappedStatement, 并且方法不是桥接方法,则为此方法生成 mappedStatement if (!method.isBridge() && mappedStatement == null) { Tuple1 one = parseStatement(method).getData(); pluginTuples.add(new PluginTuple(one.getFirst(), method)); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); }catch (Exception e ){ e.printStackTrace(); } } // 这个方法的目的主要是解决: // 如: 在 A.xml 的 selectXXX 方法引用了一个 B.xml 的 的 id,但是在解析 A.xml 时,B.xml还没有被解析 // 这个时候,解析器会将selectXXX方法的信息存储于 configuration 的 incompleteMethods属性中。 // 当 C.xml 己经解析完了,会查看configuration 的 incompleteMethods属性中是否有未初始化完成的 method // 如果有,则继续去完成 selectXXX 的 mappedStatement 的创建,如果还是失败,将抛出 IncompleteElementException异常, // selectXXX信息会继续留incompleteMethods中等待下一个 *.xml 的解析, // 直到selectXXX引用的 resultMap所在的 B.xml 被解析完成 ,此时创建selectXXX的mappedStatement将不会抛出异常, // 并且将selectXXX方法的信息从从configuration 的 incompleteMethods属性移除 // selectXXX的mappedStatement就己经创建完成,并将创建好的mappedStatement保存到configuration的mappedStatements属性中 parsePendingMethods(); Object mapperRegistry = configuration.getMapperRegistry(); if ("MybatisMapperRegistry".equals(mapperRegistry.getClass().getSimpleName())) { //对于不同版本的 baonidou 插件支持 myBatisBaomidouService.init(pluginTuples, configuration, type); } else if (mapperRegistry instanceof MapperRegistry) { //没有使用 baomidou插件情况下,原生 mybatis 处理方式 originMyBatis(pluginTuples); } } }
在上述中,所有己经被解析的 Mapper中的方法会被生成一个个mappedStatement,存储于Configuration 的mappedStatements属性中,而mappedStatements是一个 Map类型,因此,我们根据存储的 id从mappedStatements获取相应的mappedStatement,如果mappedStatement为空,说明该方法没有配置@Select(“select * from tableName … “) 注解或在 Mapper.xml 中没有生成如相应的标签,这个方法就是我们需要为其动态添加sql的方法。
在解析Statement 之前,我们先来看看Options注解。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Options { public enum FlushCachePolicy { DEFAULT, TRUE, FALSE } // useCache = true表示本次查询结果被缓存以提高下次查询速度, boolean useCache() default true; // flushCache = false表示下次查询时不刷新缓存, FlushCachePolicy flushCache() default FlushCachePolicy.DEFAULT; ResultSetType resultSetType() default ResultSetType.FORWARD_ONLY; StatementType statementType() default StatementType.PREPARED; int fetchSize() default -1; //timeout = 10000表示查询结果缓存10000秒。 int timeout() default -1; // 主键是否自增 boolean useGeneratedKeys() default false; // 设置的主键对应的字段名 String keyProperty() default "id"; // 设置的主键对应的数据库列 String keyColumn() default ""; //可以自定义resultSet结果集处理方法 String resultSets() default ""; }
了解了Options注解的参数之后,我们再来看看parseStatement()方法的解析。
PluginTuple parseStatement(Method method) throws Exception { Class> parameterTypeClass = getParameterType(method); LanguageDriver languageDriver = getLanguageDriver(method); SqlCommandType sqlCommandType = getSqlCommandType(method); PluginTuple data = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver, sqlCommandType); Tuple5tupleInfo = data.getData(); //如果返回了ResultMap标签,则需解析resultMap ,并添加到configuration的resultMaps属性中 if (StringUtils.isNotEmpty(tupleInfo.getFourth())) { XPathParser xPathParser = new XPathParser(tupleInfo.getFifth()); List xNodeList = xPathParser.evalNodes("/resultMap"); resultMapElement(xNodeList.get(0)); } SqlSource sqlSource = tupleInfo.getSecond(); String keyPropertyPre = ""; if (StringUtils.isNotEmpty(tupleInfo.getThird())) { keyPropertyPre = tupleInfo.getThird(); } if (sqlSource != null) { Options options = method.getAnnotation(Options.class); final String mappedStatementId = type.getName() + "." + method.getName(); Integer fetchSize = null; Integer timeout = null; StatementType statementType = StatementType.PREPARED; ResultSetType resultSetType = ResultSetType.FORWARD_ONLY; boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = !isSelect; boolean useCache = isSelect; KeyGenerator keyGenerator; String keyProperty = keyPropertyPre + "id"; String keyColumn = null; //如果是 insert 或 update,需要回显主键 if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) { SelectKey selectKey = method.getAnnotation(SelectKey.class); KeyGenerator jdbc3KeyGenerator = SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : CustomerJdbc3KeyGenerator.INSTANCE; if (selectKey != null) { keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? jdbc3KeyGenerator : NoKeyGenerator.INSTANCE; } else { keyGenerator = options.useGeneratedKeys() ? jdbc3KeyGenerator : NoKeyGenerator.INSTANCE; keyProperty = options.keyProperty(); keyColumn = options.keyColumn(); } } else { keyGenerator = NoKeyGenerator.INSTANCE; } if (options != null) { if (Options.FlushCachePolicy.TRUE.equals(options.flushCache())) { flushCache = true; } else if (Options.FlushCachePolicy.FALSE.equals(options.flushCache())) { flushCache = false; } useCache = options.useCache(); fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348 timeout = options.timeout() > -1 ? options.timeout() : null; statementType = options.statementType(); resultSetType = options.resultSetType(); } String resultMapId = null; if(StringUtils.isEmpty(tupleInfo.getFourth())){ ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class); if (resultMapAnnotation != null) { String[] resultMaps = resultMapAnnotation.value(); StringBuilder sb = new StringBuilder(); for (String resultMap : resultMaps) { if (sb.length() > 0) { sb.append(","); } sb.append(resultMap); } resultMapId = sb.toString(); } else if (isSelect) { resultMapId = parseResultMap(method); } }else{ resultMapId = tupleInfo.getFourth(); } assistant.addMappedStatement( mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout, // ParameterMapID null, parameterTypeClass, resultMapId, getReturnType(method), resultSetType, flushCache, useCache, // TODO gcode issue #577 false, keyGenerator, keyProperty, keyColumn, // DatabaseID null, languageDriver, // ResultSets options != null ? nullOrEmpty(options.resultSets()) : null); } return data; }
上述代码主要是对Options,SelectKey,ResultMap注解属性的获取入封装到 MappedStatement 中去,在上述代码中需要注意的是,当元组返回第5个参数不为空时,说明在 sql 解析时需要添加 resultMap映射,这个时候调用resultMapElement方法解析resultMap 映射的标签,解析好以后,保存到 configuration 的 resultMaps 中。并且设置当前方法的resultMap 的 id 为元组返回的第4个参数,这么做的用处是什么呢?在插件中主要是为了分页Mapper方法的使用。下面我们来看看如果是分页,元组将返回什么信息给我们。
来看看执行结果
而我们生成 sql关键代码getSqlSourceFromAnnotations方法,在这个方法中,为我们的方法动态生成 sql。我们来看看其内部实现。
private PluginTuple getSqlSourceFromAnnotations(Method method, Class> parameterType, LanguageDriver languageDriver, SqlCommandType sqlCommandType) { try { return buildSqlSourceFromStrings(method, parameterType, languageDriver, sqlCommandType); } catch (Exception e) { e.printStackTrace(); throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e); } } private PluginTuple buildSqlSourceFromStrings(Method method, Class> parameterTypeClass, LanguageDriver languageDriver, SqlCommandType sqlCommandType) { //解析 Mapper.java的方法,获取sql Tuple5data = SqlParseUtils.parse(tableName, primaryColumns, tableColumns, sqlCommandType, method, entityType).getData(); SqlSource sqlSource = languageDriver.createSqlSource(configuration, data.getSecond().trim(), parameterTypeClass); return new PluginTuple(data.getFirst(), sqlSource, data.getThird(),data.getFourth(),data.getFifth()); }
public class SqlParseUtils { public final static String IS_DELETE = "is_delete"; public final static String GMT_MODIFIED = "gmtModified"; public final static String TABLE_ID = "TableId"; public final static String BY = "By"; public static String TAB = " "; private static final List> primitiveTypes = new ArrayList<>(8); ... private static List tableColumns = Arrays.asList(new String[]{"id", "is_delete", "gmt_create", "gmt_modified", "type", "branch_id", "real_name", "mobile", "username", "task_id", "staff_id"}); private static List primaryC = Arrays.asList(new String[]{"id"}); public static PluginTuple testSelect(Class clazz, String methodName) { SqlCommandType sqlCommandType = SqlCommandType.SELECT; return parse("lz_test_user", primaryC, tableColumns, sqlCommandType, getMethod(clazz, methodName), null); } public static PluginTuple testInsert(Class clazz, String methodName) { SqlCommandType sqlCommandType = SqlCommandType.INSERT; return parse("lz_test_user", primaryC, tableColumns, sqlCommandType, getMethod(clazz, methodName), null); } public static PluginTuple testUpdate(Class clazz, String methodName) { SqlCommandType sqlCommandType = SqlCommandType.UPDATE; return parse("lz_test_user", primaryC, tableColumns, sqlCommandType, getMethod(clazz, methodName), null); } public static PluginTuple testDelete(Class clazz, String methodName) { SqlCommandType sqlCommandType = SqlCommandType.DELETE; return parse("lz_test_user", primaryC, tableColumns, sqlCommandType, getMethod(clazz, methodName), null); } public static PluginTuple testCount(Class clazz, String methodName) { SqlCommandType sqlCommandType = SqlCommandType.UNKNOWN; return parse("lz_test_user", primaryC, tableColumns, sqlCommandType, getMethod(clazz, methodName), null); } public static PluginTuple parse(String tableName, List primaryColumns, List tableColumns, SqlCommandType sqlCommandType, Method method, Class entityType) { DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); StringBuilder sb = new StringBuilder(); //select方法解析生成 sql 逻辑 if (SqlCommandType.SELECT.equals(sqlCommandType)) { //如果方法名以 count 开头,表示查询 count if (method.getName().startsWith("count")) { return parseCount(tableName, tableColumns, parameterNames, method); } else if (method.getReturnType().equals(Page.class)) { //如果返回值是 Page 类型,表示分页,使用时需要注意,Page 是我们插件中的 Page 类 return parseSelectPage(tableName, tableColumns, parameterNames, method, entityType); } else { //其他的情况的select查询 return parseSelect(tableName, tableColumns, parameterNames, method); } //insert方法解析生成 sql 逻辑 } else if (SqlCommandType.INSERT.equals(sqlCommandType)) { //如果方法名以insertOrUpdate开头,则表示如果对象有 id,则是更新操作,如果没有 id,则表示插入操作 if (method.getName().startsWith("insertOrUpdate")) { Tuple2 tupleInsert = parseInsert(tableName, parameterNames, method).getData(); String insertSql = removeScript(tupleInsert.getSecond()); Tuple2 tupleUpdate = parseUpdate(tableName, parameterNames, method).getData(); String updateSql = removeScript(tupleUpdate.getSecond()); StringBuilder sBuild = new StringBuilder(); sBuild.append("").append("\n"); return new PluginTuple(false, sBuild.toString()); } //插入操作 return parseInsert(tableName, parameterNames, method); //update方法解析生成sql逻辑 } else if (SqlCommandType.UPDATE.equals(sqlCommandType)) { //更新操作,分为批量更新和单个对象更新 return parseUpdate(tableName, parameterNames, method); //delete方法解析生成sql逻辑 } else if (SqlCommandType.DELETE.equals(sqlCommandType)) { //批量删除,和根据查询条件删除 return parseDelete(tableName, tableColumns, parameterNames, method); } return new PluginTuple(true, sb.toString()); } ... }
关于SqlParseUtils类是如何帮我们生成 sql ,这里也不再赘述,其实你来看SqlParseUtils类中的parseCount,parseSelectPage,parseSelect,parseInsert,parseUpdate,parseDelete方法,内部实现非常简单,就是解析方法参数及方法上和注解,将解析注解得到的值拼成sql,也没有什么多的技术含量。但是为了方便测试你写的方法最终生成的sql是怎样的,在SqlParseUtils类中提供了testSelect,testInsert,testUpdate,testDelete,testCount方法方便测试,如何测试,如下图所示。
但小伙伴需要注意,SqlParseUtils生成 sql解析器是基于我们的表中一定有三个字段is_delete,gmt_create,gmt_modified
因此,如果你的业务表没有这三个字段,或者是其他的字段代表是否删除,创建时间,更新时间,你需要根据你们的业务表来修改SqlParseUtils内的部分方法内容,才能投入生产使用,我们公司都是用 jdk1.8来开发了,因此没有做1.7的兼容,如果你们的代码是运行在1.7之上的,不好意思,插件也没有提供支持,但是感兴趣的小伙伴也可以自己去开发 ,无非将我们生成的 sql的#{username} 这种改成#{param1},#{param2}而已。
插件源码解析完了吗?细心的小伙伴肯定觉得还没有解析完,在插件中还有一个非常重要的部分没有解析,就是手动指定方法是否使用@Param参数,为什么要控制方法的参数是否使用@Param注解呢?我们先来看一个例子。对于insert 方法,如果我们没有加@Param 注解
我们的 sql 是如下写法。
insert into lz_test_user( )values( id, is_delete, gmt_create, gmt_modified, type, branch_id, real_name, mobile, username, task_id, staff_id, ) #{id}, #{isDelete}, #{gmtCreate}, #{gmtModified}, #{type}, #{branchId}, #{realName}, #{mobile}, #{username}, #{taskId}, #{staffId},
但是如果我们方法参数中添加了@Param 注解。
我们的sql 就必需这样写了。
user.id" > insert into lz_test_user( )values( user.id != null">id, user.isDelete != null">is_delete, user.gmtCreate != null">gmt_create, user.gmtModified != null">gmt_modified, user.type != null">type, user.branchId != null">branch_id, user.realName != null and realName != '' ">real_name, user.mobile != null and mobile != '' ">mobile, user.username != null and username != '' ">username, user.taskId != null">task_id, user.staffId != null">staff_id, ) user.id != null">#{user.id}, user.isDelete != null">#{user.isDelete}, user.gmtCreate != null">#{user.gmtCreate}, user.gmtModified != null">#{user.gmtModified}, user.type != null">#{user.type}, user.branchId != null">#{user.branchId}, user.realName != null and user.realName != '' ">#{user.realName}, user.mobile != null and user.mobile != '' ">#{user.mobile}, user.username != null and user.username != '' ">#{user.username}, user.taskId != null">#{user.taskId}, user.staffId != null">#{user.staffId},
因此,加不加@Param 注解,我们的 sql 方法写法是不一样的,所以我们需要生成的 sql手动来控制方法是否加@Param 注解。
public void originMyBatis(ListpluginTuples) { try { MapperRegistry mapperRegistry = configuration.getMapperRegistry(); // 获取MapperRegistry的knownMappers属性值 Map , MapperProxyFactory>> knownMappers = SqlParseUtils.getFieldValue(mapperRegistry, "knownMappers"); //根据 Mapper 类型获取MapperProxyFactory MapperProxyFactory mapperProxyFactory = knownMappers.get(type); // 从mapperProxyFactory的缓存中获取Map Map methodCache = mapperProxyFactory.getMethodCache(); for (PluginTuple pluginTuple : pluginTuples) { Tuple2 data = pluginTuple.getData(); Method method = data.getSecond(); MapperMethod mapperMethod = methodCache.get(method); // 如果方法对应的 mapperMethod 为空,则创建MapperMethod if (mapperMethod == null) { if (mapperProxyFactory.getMapperInterface() != null) { mapperMethod = new MapperMethod(mapperProxyFactory.getMapperInterface(), method, configuration); } else { mapperMethod = new MapperMethod(type, method, configuration); } // 获取mapperMethod的method方法属性 MapperMethod.MethodSignature methodSignature = SqlParseUtils.getFieldValue(mapperMethod, "method"); // 反射调用,获取methodSignature的paramNameResolver属性 ParamNameResolver paramNameResolver = SqlParseUtils.getFieldValue(methodSignature, "paramNameResolver"); // 调用 paramNameResolver的hasParamAnnotation方法, data.getFirst()为 boolean 类型 // hasParamAnnotation 的作用,设置当前方法有没有使用@Param 注解 // 1. 对于普通的查询方法如 selectUserById(Long id) 这种情况,默认data.getFirst(),表示使用@Param注解 // 最后生成的 sql 为 select * from user where username = #{username},可以设置hasParamAnnotation值为true // 的好处就是省去了在 selectUserById(Long id) 方法中@Param注解的书写。 // 本来要写成 selectUserById(@Param Long id)的,现在只需要写成selectUserById(Long id) // 2. 而对于insert(User user )方法,MyBatis本身是支持这样写的,因此,我们这里设置hasParamAnnotation为 false // 设置 true 和 false 由生成sql时控制 // 如果在insert(User user )中也设置hasParamAnnotation为 true // insert(id ,username ,password ) values (user.id,user.username,user.password) // hasParamAnnotation为设置为 false,则生成的 sql 是 // insert (id ,username,password ) values (id,username,password),签于方便,我选择了后者 SqlParseUtils.setFieldValue(paramNameResolver, "hasParamAnnotation", data.getFirst()); methodCache.put(method, mapperMethod); } } } catch (Exception e) { e.printStackTrace(); } }
上述方法,大家看到的只是一个结论性的东西,肯定有小伙伴心里会想,为什么要这样做,请听我细细讲来。在MapperFactoryBean 的checkDaoConfig方法中,己经将当前Mapper 接口添加到了MapperRegistry的knownMappers Map 中,
因此在originMyBatis方法中,我们能从knownMappers中获取MapperProxyFactory对象,获取到MapperProxyFactory有什么用处呢?我们先来看看MapperProxyFactory。
public class MapperProxyFactory{ private final Class mapperInterface; private final Map methodCache = new ConcurrentHashMap (); public MapperProxyFactory(Class mapperInterface) { this.mapperInterface = mapperInterface; } public Class getMapperInterface() { return mapperInterface; } public Map getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy mapperProxy = new MapperProxy (sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
单从类内容中看不出什么所以然,只知道在创建MapperProxy 时,将methodCache传入其中,同时我们知道,当 Spring 容器启动完成时,存储于容器的是由Proxy.newProxyInstance()创建的一个 JDK 代理对象,当业务调用 Mapper接口的方法时,这个时候会调用MapperProxy的 invoke 方法,从而从数据库中查找数据封装并返回,那么我们来看看MapperProxy的内部实现。
public class MapperProxyimplements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class mapperInterface; private final Map methodCache; public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); //调用MapperMethod的execute方法执行 sql 并返回值 return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { //如果缓存中存在,则从缓存中获取,如果缓存中不存在,则创建MapperMethod MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { //创建MapperMethod mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); //创建好的MapperMethod存储于methodCache中 methodCache.put(method, mapperMethod); } return mapperMethod; } @UsesJava7 private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { final Constructor constructor = MethodHandles.Lookup.class .getDeclaredConstructor(Class.class, int.class); if (!constructor.isAccessible()) { constructor.setAccessible(true); } final Class> declaringClass = method.getDeclaringClass(); return constructor .newInstance(declaringClass, MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC) .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args); } private boolean isDefaultMethod(Method method) { return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC && method.getDeclaringClass().isInterface(); } }
真正的执行是调用mapperMethod的 execute方法,因此mapperMethod这个类肯定封装了
mybatis 属性,我们继续进入MapperMethod的构造方法。
public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } ... }
在上述方法中,好像没有什么东西嘛,无非创建了一个MethodSignature对象,SqlCommand对象,无非标识当前方法是 select,update 还是其他操作,感觉到了山穷水尽的地步,我们进入MethodSignature构造方法看看。
public static class MethodSignature { private final boolean returnsMany; private final boolean returnsMap; private final boolean returnsVoid; private final boolean returnsCursor; private final Class> returnType; private final String mapKey; private final Integer resultHandlerIndex; private final Integer rowBoundsIndex; private final ParamNameResolver paramNameResolver; public MethodSignature(Configuration configuration, Class> mapperInterface, Method method) { Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); //设置方法返回值类型 if (resolvedReturnType instanceof Class>) { this.returnType = (Class>) resolvedReturnType; } else if (resolvedReturnType instanceof ParameterizedType) { this.returnType = (Class>) ((ParameterizedType) resolvedReturnType).getRawType(); } else { this.returnType = method.getReturnType(); } //如果方法返回值是 void,则returnsVoid为 true this.returnsVoid = void.class.equals(this.returnType); //如果返回值是数组类型或 Collection 集合类型,则returnsMany为 true this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); this.returnsCursor = Cursor.class.equals(this.returnType); this.mapKey = getMapKey(method); this.returnsMap = this.mapKey != null; //如果方法参数中有RowBounds对象,则获取其在方法参数的索引位置 this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); //如果方法参数中有ResultHandler对象,则获取其在方法参数的索引位置,如果位于方法参数的第一个位置,则resultHandlerIndex为0 this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); //参数名称解析器 this.paramNameResolver = new ParamNameResolver(configuration, method); } ... }
终于柳暗花明了,看到了参数名称解析器,终于看到了希望的曙光,我们进入ParamNameResolver类。
public class ParamNameResolver { private static final String GENERIC_NAME_PREFIX = "param"; private final SortedMapnames; //如果配置了@Param注解,则hasParamAnnotation为 true private boolean hasParamAnnotation; public ParamNameResolver(Configuration config, Method method) { final Class>[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap map = new TreeMap (); int paramCount = paramAnnotations.length; for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { //如果方法参数是RowBounds或ResultHandler类,则跳过 if (isSpecialParameter(paramTypes[paramIndex])) { continue; } String name = null; for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { //如果配置了Param注解,则hasParamAnnotation为 true hasParamAnnotation = true; name = ((Param) annotation).value(); break; } } if (name == null) { //如果config中的useGeneratedKeys属性为 true if (config.isUseActualParamName()) { //从 ASM 中获取方法参数名称 name = getActualParamName(method, paramIndex); } if (name == null) { // 如果参数名依然为空,只能使用 "0", "1", ...来代替了 name = String.valueOf(map.size()); } } map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map); } private String getActualParamName(Method method, int paramIndex) { if (Jdk.parameterExists) { return ParamNameUtil.getParamNames(method).get(paramIndex); } return null; } private static boolean isSpecialParameter(Class> clazz) { return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz); } public String[] getNames() { return names.values().toArray(new String[0]); } public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { return args[names.firstKey()]; } else { final Map param = new ParamMap (); int i = 0; for (Map.Entry entry : names.entrySet()) { // 添加方法参数名 username,password 等到 map 中 param.put(entry.getValue(), args[entry.getKey()]); // 添加普通的参数名 param1, param2, ...到 Map 中 final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } } }
在上述getNamedParams方法中,我们发现,当方法只有一个参数时,hasParamAnnotation就起作用了,如果配置了@Param 参数,Map 对象,而不配置@Param 参数,方法返回的是一个 Object对象。那么两者有什么区别呢?我们来测试一下。
经过我的一波测试,发现hasParamAnnotation一直为 false 也不会报错,不过在之前的测试中确实因为返回值是普通对象还是Map对象时,Ognl解析报错,不一样的处理方式导致报错,我才想到去控制hasParamAnnotation的值,从而解析错误问题,不过现在都没有这个问题了,解析方案还是留在这里吧,说不定小伙伴也遇到一样的问题,到时候就有解决方案了。
而originMyBatis方法的目的就是,控制hasParamAnnotation为 true或 false,当只有一个参数时,控制getNamedParams方法返回的是一个普通的参数对象还是一个被Map包裹的方法参数对象。不过刚刚也测试过了,其实只要useActualParamName=true,hasParamAnnotation为 false 也没有关系,而默认情况下Configuration的useActualParamName为 true。
到这里,我们的MyBatis 小插件的使用及源码解析己经完成了,感兴趣的小伙伴可以尝试着将包加入到你们的业务开发中去,己经支持了大部分单表操作,省去了写 SQL 的操作,提高了我们代码的可阅读性,减少 bug 出错的机率,相信你们会喜欢,当然,发现有问题,及时进出,我们一起学习,一起进步。
目前只支持 jdk1.8 ,jdk1.7 目前不支持,感兴趣的小伙伴可以自己去开发一下。
对于idea版本比低的小伙伴,在配置 maven 插件的-parameters参数时,需要手动加一下,万一不行,自己升级一下 idea 的版本。
当然也有同事建义集成 JPA的方法提示功能,如下
不过我觉得意义不大,因为我们写 Mapper方法时,一般在开发的时候,直接在业务代码中写好需要添加的 Mapper 方法,按快捷键直接创建方法。而不是先到 Mapper 中创建方法,再到业务方法来引用,因此我觉得意义不大。所以就不开发了。提供jpa 代码的git 地址在这,感兴趣的小伙伴可以去研究一下,如何在 Mapper中提供方法名创建时提示功能。https://github.com/quyixiao/jpa_webwork
希望小插件对你的工作,开发有帮助。
应用项目 github 地址
https://github.com/quyixiao/lz_mybatis_plugin_project
插件 github 地址
https://github.com/quyixiao/lz_mybatis_plugin