做者:小傅哥
博客:https://bugstack.cn - 原創系列優質專題文章
html
沉澱、分享、成長,讓本身和他人都能有所收穫!
在前面一篇分析了 mybatis 源碼,從它爲何以後接口可是沒有實現類就能執行數據庫操做爲入口,整個源碼核心流程徹底解釋了一遍。對於一個3年以上的程序員來講,新知識的學習過程應該是從最開始 helloworld 到熟練使用 api 完成業務功能。下一步爲了深刻了解就須要閱讀部分核心源碼,從而在出問題後能夠快速定位,迅速排查。從而減小線上事故的持續時長,提高我的影響力。但!這不是學習終點,由於不管是任何一個框架的源碼,若是隻是看那麼就很難學習到它的實用技術。紙上得來終覺淺,惟有實戰和操練。java
那麼,本章節咱們去簡單實現一個基於jdbc的demo版本Mybatis,從而更加清楚這樣框架的設計。與此同時這份思想會讓你能夠在其餘場景使用,好比給ES查詢寫一個EsBatis。實現了心情也好了;node
擴展上一篇源碼分析工程;itstack-demo-mybatis,增長 like 包,模仿 Mybatis 工程。完整規程下載,關注公衆號:bugstack蟲洞棧 | 回覆:源碼分析mysql
itstack-demo-mybatis └── src ├── main │ ├── java │ │ └── org.itstack.demo │ │ ├── dao │ │ │ ├── ISchool.java │ │ │ └── IUserDao.java │ │ ├── like │ │ │ ├── Configuration.java │ │ │ ├── DefaultSqlSession.java │ │ │ ├── DefaultSqlSessionFactory.java │ │ │ ├── Resources.java │ │ │ ├── SqlSession.java │ │ │ ├── SqlSessionFactory.java │ │ │ ├── SqlSessionFactoryBuilder.java │ │ │ └── SqlSessionFactoryBuilder.java │ │ └── interfaces │ │ ├── School.java │ │ └── User.java │ ├── resources │ │ ├── mapper │ │ │ ├── School_Mapper.xml │ │ │ └── User_Mapper.xml │ │ ├── props │ │ │ └── jdbc.properties │ │ ├── spring │ │ │ ├── mybatis-config-datasource.xml │ │ │ └── spring-config-datasource.xml │ │ ├── logback.xml │ │ ├── mybatis-config.xml │ │ └── spring-config.xml │ └── webapp │ └── WEB-INF └── test └── java └── org.itstack.demo.test ├── ApiLikeTest.java ├── MybatisApiTest.java └── SpringApiTest.java
關於整個 Demo 版本,並非把全部 Mybatis 所有實現一遍,而是撥絲抽繭將最核心的內容展現給你,從使用上你會感覺如出一轍,可是實現類已經所有被替換,核心類包括;程序員
ApiLikeTest.test_queryUserInfoById()
@Test public void test_queryUserInfoById() { String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); SqlSession session = sqlMapper.openSession(); try { User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L); System.out.println(JSON.toJSONString(user)); } finally { session.close(); reader.close(); } } catch (Exception e) { e.printStackTrace(); } }
一切順利結果以下(新人每每會遇到各類問題);web
{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000} Process finished with exit code 0
可能乍一看這測試類徹底和 MybatisApiTest.java 測試的代碼如出一轍呀,也看不出區別。其實他們的引入的包是不同;spring
MybatisApiTest.java 裏面引入的包
import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder;
ApiLikeTest.java 裏面引入的包
import org.itstack.demo.like.Resources; import org.itstack.demo.like.SqlSession; import org.itstack.demo.like.SqlSessionFactory; import org.itstack.demo.like.SqlSessionFactoryBuilder;
好!接下來咱們開始分析這部分核心代碼。sql
這裏咱們採用 mybatis 的配置文件結構進行解析,在不破壞原有結構的狀況下,最大可能的貼近源碼。mybatis 單獨使用的使用的時候使用了兩個配置文件;數據源配置、Mapper 映射配置,以下;數據庫
mybatis-config-datasource.xml & 數據源配置
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/User_Mapper.xml"/> <mapper resource="mapper/School_Mapper.xml"/> </mappers> </configuration>
User_Mapper.xml & Mapper 映射配置
<?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="org.itstack.demo.dao.IUserDao"> <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where id = #{id} </select> <select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where age = #{age} </select> </mapper>
這裏的加載過程與 mybaits 不一樣,咱們採用 dom4j 方式。在案例中會看到最開始獲取資源,以下;apache
ApiLikeTest.test_queryUserInfoById() & 部分截取
String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); ...
從上能夠看到這是經過配置文件地址獲取到了讀取流的過程,從而爲後面解析作基礎。首先咱們先看 Resources 類,整個是咱們的資源類。
Resources.java & 資源類
/** * 公衆號 | bugstack蟲洞棧 * 博 客 | https://bugstack.cn * Create by 小傅哥 @2020 */ public class Resources { public static Reader getResourceAsReader(String resource) throws IOException { return new InputStreamReader(getResourceAsStream(resource)); } private static InputStream getResourceAsStream(String resource) throws IOException { ClassLoader[] classLoaders = getClassLoaders(); for (ClassLoader classLoader : classLoaders) { InputStream inputStream = classLoader.getResourceAsStream(resource); if (null != inputStream) { return inputStream; } } throw new IOException("Could not find resource " + resource); } private static ClassLoader[] getClassLoaders() { return new ClassLoader[]{ ClassLoader.getSystemClassLoader(), Thread.currentThread().getContextClassLoader()}; } }
這段代碼方法的入口是getResourceAsReader,直到往下以此作了;
配置文件加載後開始進行解析操做,這裏咱們也仿照 mybatis 但進行簡化,以下;
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactoryBuilder.build() & 入口構建類
public DefaultSqlSessionFactory build(Reader reader) { SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(new InputSource(reader)); Configuration configuration = parseConfiguration(document.getRootElement()); return new DefaultSqlSessionFactory(configuration); } catch (DocumentException e) { e.printStackTrace(); } return null; }
SqlSessionFactoryBuilder.parseConfiguration() & 解析過程
private Configuration parseConfiguration(Element root) { Configuration configuration = new Configuration(); configuration.setDataSource(dataSource(root.selectNodes("//dataSource"))); configuration.setConnection(connection(configuration.dataSource)); configuration.setMapperElement(mapperElement(root.selectNodes("mappers"))); return configuration; }
SqlSessionFactoryBuilder.dataSource() & 解析出數據源
private Map<String, String> dataSource(List<Element> list) { Map<String, String> dataSource = new HashMap<>(4); Element element = list.get(0); List content = element.content(); for (Object o : content) { Element e = (Element) o; String name = e.attributeValue("name"); String value = e.attributeValue("value"); dataSource.put(name, value); } return dataSource; }
SqlSessionFactoryBuilder.connection() & 獲取數據庫鏈接
private Connection connection(Map<String, String> dataSource) { try { Class.forName(dataSource.get("driver")); return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password")); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } return null; }
SqlSessionFactoryBuilder.mapperElement() & 解析SQL語句
private Map<String, XNode> mapperElement(List<Element> list) { Map<String, XNode> map = new HashMap<>(); Element element = list.get(0); List content = element.content(); for (Object o : content) { Element e = (Element) o; String resource = e.attributeValue("resource"); try { Reader reader = Resources.getResourceAsReader(resource); SAXReader saxReader = new SAXReader(); Document document = saxReader.read(new InputSource(reader)); Element root = document.getRootElement(); //命名空間 String namespace = root.attributeValue("namespace"); // SELECT List<Element> selectNodes = root.selectNodes("select"); for (Element node : selectNodes) { String id = node.attributeValue("id"); String parameterType = node.attributeValue("parameterType"); String resultType = node.attributeValue("resultType"); String sql = node.getText(); // ? 匹配 Map<Integer, String> parameter = new HashMap<>(); Pattern pattern = Pattern.compile("(#\\{(.*?)})"); Matcher matcher = pattern.matcher(sql); for (int i = 1; matcher.find(); i++) { String g1 = matcher.group(1); String g2 = matcher.group(2); parameter.put(i, g2); sql = sql.replace(g1, "?"); } XNode xNode = new XNode(); xNode.setNamespace(namespace); xNode.setId(id); xNode.setParameterType(parameterType); xNode.setResultType(resultType); xNode.setSql(sql); xNode.setParameter(parameter); map.put(namespace + "." + id, xNode); } } catch (Exception ex) { ex.printStackTrace(); } } return map; }
最後將初始化後的配置類 Configuration,做爲參數進行建立 DefaultSqlSessionFactory,以下;
public DefaultSqlSessionFactory build(Reader reader) { SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(new InputSource(reader)); Configuration configuration = parseConfiguration(document.getRootElement()); return new DefaultSqlSessionFactory(configuration); } catch (DocumentException e) { e.printStackTrace(); } return null; }
DefaultSqlSessionFactory.java & SqlSessionFactory的實現類
public class DefaultSqlSessionFactory implements SqlSessionFactory { private final Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } @Override public SqlSession openSession() { return new DefaultSqlSession(configuration.connection, configuration.mapperElement); } }
SqlSession session = sqlMapper.openSession();
上面這一步就是建立了DefaultSqlSession,比較簡單。以下;
@Override public SqlSession openSession() { return new DefaultSqlSession(configuration.connection, configuration.mapperElement); }
User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
在 DefaultSqlSession 中經過實現 SqlSession,提供數據庫語句查詢和關閉鏈接池,以下;
SqlSession.java & 定義
public interface SqlSession { <T> T selectOne(String statement); <T> T selectOne(String statement, Object parameter); <T> List<T> selectList(String statement); <T> List<T> selectList(String statement, Object parameter); void close(); }
接下來看具體的執行過程,session.selectOne
DefaultSqlSession.selectOne() & 執行查詢
public <T> T selectOne(String statement, Object parameter) { XNode xNode = mapperElement.get(statement); Map<Integer, String> parameterMap = xNode.getParameter(); try { PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql()); buildParameter(preparedStatement, parameter, parameterMap); ResultSet resultSet = preparedStatement.executeQuery(); List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType())); return objects.get(0); } catch (Exception e) { e.printStackTrace(); } return null; }
經過 statement 獲取最初解析 xml 時候的存儲的 select 標籤信息;
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where id = #{id} </select>
這裏還須要設置入參,咱們將入參設置進行抽取,以下;
private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException { int size = parameterMap.size(); // 單個參數 if (parameter instanceof Long) { for (int i = 1; i <= size; i++) { preparedStatement.setLong(i, Long.parseLong(parameter.toString())); } return; } if (parameter instanceof Integer) { for (int i = 1; i <= size; i++) { preparedStatement.setInt(i, Integer.parseInt(parameter.toString())); } return; } if (parameter instanceof String) { for (int i = 1; i <= size; i++) { preparedStatement.setString(i, parameter.toString()); } return; } Map<String, Object> fieldMap = new HashMap<>(); // 對象參數 Field[] declaredFields = parameter.getClass().getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); field.setAccessible(true); Object obj = field.get(parameter); field.setAccessible(false); fieldMap.put(name, obj); } for (int i = 1; i <= size; i++) { String parameterDefine = parameterMap.get(i); Object obj = fieldMap.get(parameterDefine); if (obj instanceof Short) { preparedStatement.setShort(i, Short.parseShort(obj.toString())); continue; } if (obj instanceof Integer) { preparedStatement.setInt(i, Integer.parseInt(obj.toString())); continue; } if (obj instanceof Long) { preparedStatement.setLong(i, Long.parseLong(obj.toString())); continue; } if (obj instanceof String) { preparedStatement.setString(i, obj.toString()); continue; } if (obj instanceof Date) { preparedStatement.setDate(i, (java.sql.Date) obj); } } }
接下來須要將查詢結果轉換爲咱們的類(主要是反射類的操做),resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) { List<T> list = new ArrayList<>(); try { ResultSetMetaData metaData = resultSet.getMetaData(); int columnCount = metaData.getColumnCount(); // 每次遍歷行值 while (resultSet.next()) { T obj = (T) clazz.newInstance(); for (int i = 1; i <= columnCount; i++) { Object value = resultSet.getObject(i); String columnName = metaData.getColumnName(i); String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1); Method method; if (value instanceof Timestamp) { method = clazz.getMethod(setMethod, Date.class); } else { method = clazz.getMethod(setMethod, value.getClass()); } method.invoke(obj, value); } list.add(obj); } } catch (Exception e) { e.printStackTrace(); } return list; }
sql 查詢有入參、有不須要入參、有查詢一個、有查詢集合,只須要合理包裝便可,例以下面的查詢集合,入參是對象類型;
ApiLikeTest.test_queryUserList()
@Test public void test_queryUserList() { String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); SqlSession session = sqlMapper.openSession(); try { User req = new User(); req.setAge(18); List<User> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req); System.out.println(JSON.toJSONString(userList)); } finally { session.close(); reader.close(); } } catch (Exception e) { e.printStackTrace(); } }
*測試結果:*
[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}] Process finished with exit code 0