背景
不知道大家公司內部有沒有這樣的困惑, 不少部門常常會要求大家部門提供接口, 查詢一些數據, 接口基本沒有業務邏輯, 一條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地址