背景
不知道大家公司内部有没有这样的困惑, 不少部门常常会要求大家部门提供接口, 查询一些数据, 接口基本没有业务逻辑, 一条sql足以, 可是为了这个sql就不得不开发一个接口, 费时费力. 不少人也想过解决, 好比常常见到的, 会写一个包含不少字段的SQL, 而后经过不一样的入参拼接不一样的sql(mybatis中的<if>). 这种方式简单粗暴, 只能查询固定表, 若是换一个表的数据, 仍是要从新写, 并且返回无用大量字段.java
思路
怎么解决? 说说我和小伙伴D的思路: 回顾下需求场景, 提供无业务逻辑, 只返回sql查询结果的接口. 也就是说, 若是有这样一个接口, 能够每次执行我写的sql, 那问题就解决了, 因此咱们的目标就是: 把sql写到一个地方(DB), 而后接口获取sql, 并执行返回执行结果.mysql
实现
我和D开始以为并不难, 将sql存到DB, 而后读取, 利用mybatis执行. 可是在执行这步就卡住了, 若是是简单的sql, 好比git
select * from user where name = ? and age = ?
的确能够实现, 好比使用mybatis提供的@SelectProvider注解, 在方法selectUserSql中拼接参数, 而后执行.github
@SelectProvider(value = UserService.class, method = "selectUserSql") List<User> selectDyn(SQL sql, Map<String, Object> parameterMap);
可是若是稍微复杂一点, 好比name非必填, 那这的处理想一想就头大(开始还想着要不要本身实现一套解析工具)... 和D商量, 既然mybatis已经有一套完整的sql解析工具, 咱们直接拿来用就行了, 既省去了本身开发的工做量, 又可靠(是否是瞧不起我! 嗯~).sql
mybatis加载解析过程概述
说干就干, 从看mybatis源码着手, 发现了点门道. 通常使用mybatis代码以下apache
// 配置文件以流的形式加载到内存 InputStream inputStreamXML = Resources.getResourceAsStream("mybatis-config.xml"); // 构造工厂 SqlSessionFactory sqlSessionFactoryXML = new SqlSessionFactoryBuilder().build(inputStreamXML); // sqlSession SqlSession sqlSessionXML = sqlSessionFactoryXML.openSession(); // 获取对应Mapper UserMapper userMapper = sqlSessionXML.getMapper(UserMapper.class); // 执行 System.out.println("xml : " + userMapper.queryById(1));
看着代码咱们从加载配置文件唠起, 首先咱们测试代码的配置信息以下mybatis
<configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/xxx"/> <property name="username" value="xxx"/> <property name="password" value="xxxxxx"/> </dataSource> </environment> </environments> <mappers> <mapper resource="UserMapper.xml"/> </mappers> </configuration>
流程大概这样, 用于配置参数太多, 经过工厂的builder建立工厂类, 先构造一个解析配置文件的工具, 而后一点点解析, 将解析结果放到configuration对象中, 而后使用该对象构造工厂对象.app
因为咱们的目标是动态载入sql, 因此咱们重点看下Mapper的解析 解析分为两类, 一个是package标签, 一个是Mapper标签, 这里是Mapper标签. Mapper标签下又分为三种resource, url, class(就是加载方式不同), 接下来会加载Mapper标签指定的文件信息, 也就是UserMapper.xml, 内容以下:ide
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.togo.repository.UserMapper"> <resultMap type="com.togo.entity.User" id="UserMap"> <result property="id" column="id" jdbcType="INTEGER"/> <result property="xx" column="xx" jdbcType="VARCHAR"/> <result property="appid" column="appid" jdbcType="VARCHAR"/> <result property="nickname" column="nickname" jdbcType="VARCHAR"/> <result property="passtest" column="passtest" jdbcType="INTEGER"/> </resultMap> <select id="queryById" resultMap="UserMap"> select id, xx, appid, nickname, passtest from wx.user <where> <if test="id != null"> and id = #{id} </if> </where> </select> </mapper>
跟解析配置文件的套路一致, 也是挨个标签的解析, 由于咱们最初就是打算直接使用mybatis的解析工具, 因此不是很关心它是如何实现的, 咱们只要知道怎么载入Mapper就能够了, 在这里出现了关键代码工具
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement下 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); }
这里咱们彻底能够拿出来加载咱们的mapper,
// mapper就是xml中的字符串 InputStream inputStream = new ByteArrayInputStream(mapper.getBytes()); Configuration configuration = sqlSessionFactoryXML.getConfiguration(); ErrorContext.instance().resource("resource"); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, "resource", configuration.getSqlFragments()); mapperParser.parse();
debug中发现已经加载到configuration对象中了~
执行
加载完成后就是执行, 咱们在看下正常的执行代码
SqlSession sqlSessionXML = sqlSessionFactoryXML.openSession(); UserMapper userMapper = sqlSessionXML.getMapper(UserMapper.class); System.out.println("xml : " + userMapper.queryById(1));
额...这个UserMapper怎么获得? 咱们只是加载了一段字符串, 固然没有能够执行方法的Mapper类了, 那是否是说只要咱们有一个这样的类就能够了! 那么就动态生成一个吧~ 咱们这里使用的是asm, 配合idea插件使用简单.
dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>7.0</version> </dependency>
准备生成的类
public interface TestMapper { Map<String, Object> queryById(Integer id); }
生成代码
public class MyClassLoader extends ClassLoader { public static byte[] dump() throws Exception { ClassWriter cw = new ClassWriter(0); FieldVisitor fv; MethodVisitor mv; AnnotationVisitor av0; cw.visit(52, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "com/togo/asm/TestMapper", null, "java/lang/Object", null); cw.visitSource("TestMapper.java", null); { mv = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "queryById", "(Ljava/lang/Integer;)Ljava/util/Map;", "(Ljava/lang/Integer;)Ljava/util/Map<Ljava/lang/String;Ljava/lang/Object;>;", null); mv.visitEnd(); } cw.visitEnd(); return cw.toByteArray(); } public Class<?> defineClass(String name, byte[] b) { // ClassLoader是个抽象类,而ClassLoader.defineClass 方法是protected的 // 因此咱们须要定义一个子类将这个方法暴露出来 return super.defineClass(name, b, 0, b.length); } }
执行!!!
// 生成二进制字节码 byte[] bytes = MyClassLoader.dump(); // 使用自定义的ClassLoader MyClassLoader cl = new MyClassLoader(); // 加载咱们生成的 HelloWorld 类 Class<?> clazz = cl.defineClass("com.togo.asm.TestMapper", bytes); // 将生成的类对象加载到configuration中 configuration.addMapper(clazz); Method query = clazz.getMethod("queryById", Integer.class); // 这里就是经过类对象从configuration中获取对应的Mapper Object testMapper = sqlSessionXML.getMapper(clazz); Object result = query.invoke(testMapper, 1); System.out.println("dyn : " + result);
总结
本篇经过mybatis实现了动态加载执行外部sql的功能, 这里只是为你们提供一个实现思路, 在应用到项目前还有不少细节须要深刻研究. 加油加油~ demo地址