MyBatis 是一款優秀的持久層框架,它支持定製化 SQL、存儲過程以及高級映射。java
MyBatis 避免了幾乎全部的 JDBC 代碼和手動設置參數以及獲取結果集。mysql
MyBatis 可使用簡單的 XML 或註解來配置和映射原生信息,將接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java對象)映射成數據庫中的記錄。(這是官網解釋)git
當框架啓動時,經過configuration解析config.xml配置文件和mapper.xml映射文件,映射文件可使用xml方式或者註解方式,而後由configuration得到sqlsessionfactory對象,再由sqlsessionfactory得到sqlsession數據庫訪問會話對象,經過會話對象得到對應DAO層的mapper對象,經過調用mapper對象相應方法,框架就會自動執行SQL語句從而得到結果。github
其實總體流程就是這麼簡單,咱們來一塊兒實現一個簡單版本的 mybatis。sql
(1)深刻學習 mybatis 的原理數據庫
一千個讀者就有一千個哈姆雷特,一千個做者就有一千個莎士比亞。——老馬session
(2)實現屬於本身的 mybatis 工具。mybatis
數據庫的種類實際上有幾百種,好比工做中就用到過 GreenPlum 這種相對小衆的數據庫,這時候 mybatis 可能就不能使用了。app
感受大可沒必要,符合 SQL 標準都應該統一支持下,這樣更加方便實用。框架
本系列目前共計 17 個迭代版本,基本完成了 mybatis 的核心特性。
耗時大概十天左右,相對實現的方式比較簡單。
採用 mvp 的開發策略,逐漸添加新的特性。
本系列將對核心代碼進行講解,完整代碼已經所有開源
不是本系列重點,請自行找資料。
版本:使用的是 v5.7 版本,v8.0 以後依賴的驅動包會有所不一樣。
-- auto-generated definition create table user ( id int auto_increment primary key, name varchar(100) not null, password varchar(100) not null ); insert into user (name, password) value ('ryo', '123456');
<dependency> <groupId>com.github.houbb</groupId> <artifactId>mybatis</artifactId> <version>0.0.1</version> </dependency>
<?xml version="1.0" encoding="UTF-8"?> <configuration> <dataSource> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers> </configuration>
Config config = new XmlConfig("mybatis-config-5-7.xml"); SqlSession sqlSession = new DefaultSessionFactory(config).openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.selectById(1L); System.out.println(user);
輸出結果:
User{id=1, name='ryo', password='123456'}
是否是有種 mybatis 初戀般的感受呢?
到這裏都是引子,下面咱們來說述下一些核心實現。
這裏咱們須要訪問 mysql,也須要解析 xml。
須要引入以下的依賴:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.29</version> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency>
上述的測試代碼中,咱們演示用到的幾個核心接口以下:
配置接口
/** * 配置信息 * @author binbin.hou * @since 0.0.1 */ public interface Config { /** * 獲取數據源信息 * @return 數據源配置 * @since 0.0.1 */ DataSource getDataSource(); /** * 獲取映射類信息 * @param clazz 類信息 * @return 結果 * @since 0.0.1 */ MapperClass getMapperData(final Class clazz); /** * 獲取映射類信息 * @param clazz 類信息 * @param methodName 方法名稱 * @return 結果 * @since 0.0.1 */ MapperMethod getMapperMethod(final Class clazz, final String methodName); /** * 數據庫鏈接信息 * @return 鏈接信息 * @since 0.0.1 */ Connection getConnection(); }
public interface SqlSession { /** * 查詢單個 * @param mapperMethod 方法 * @param args 參數 * @param <T> 泛型 * @return 結果 * @since 0.0.1 */ <T> T selectOne(final MapperMethod mapperMethod, Object[] args); /** * Retrieves a mapper. * @param <T> the mapper type * @param type Mapper interface class * @return a mapper bound to this SqlSession * @since 0.0.1 */ <T> T getMapper(Class<T> type); /** * 獲取配置信息 * @return 配置 * @since 0.0.1 */ Config getConfig(); }
UserMapper 就是咱們常常定義的 mapper
public interface UserMapper { User selectById(final long id); }
下面咱們來看看對應的幾個比較重要的實現。
咱們的不少配置放在 config.xml 文件中,確定是經過解析 xml 實現的。
public class XmlConfig extends ConfigAdaptor { /** * 文件配置路徑 * * @since 0.0.1 */ private final String configPath; /** * 配置文件信息 * * @since 0.0.1 */ private Element root; /** * 數據源信息 * * @since 0.0.1 */ private DataSource dataSource; /** * mapper 註冊類 * * @since 0.0.1 */ private final MapperRegister mapperRegister = new MapperRegister(); public XmlConfig(String configPath) { this.configPath = configPath; // 配置初始化 initProperties(); // 初始化數據鏈接信息 initDataSource(); // mapper 信息 initMapper(); } @Override public DataSource getDataSource() { return this.dataSource; } @Override public Connection getConnection() { try { Class.forName(dataSource.driver()); return DriverManager.getConnection(dataSource.url(), dataSource.username(), dataSource.password()); } catch (ClassNotFoundException | SQLException e) { throw new MybatisException(e); } } @Override public MapperMethod getMapperMethod(Class clazz, String methodName) { return this.mapperRegister.getMapperMethod(clazz, methodName); } }
這裏就是解析 xml 文件的 root 節點,便於後續使用:
root 節點的初始化以下:
/** * 獲取根節點 * @param path 配置路徑 * @return 元素 * @since 0.0.1 */ public static Element getRoot(final String path) { try { // 初始化數據庫鏈接信息 InputStream inputStream = StreamUtil.getInputStream(path); SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); return document.getRootElement(); } catch (DocumentException e) { throw new MybatisException(e); } }
這就是解析 xml 中對於 dataSource 的配置信息:
/** * 初始化數據源 * * @since 0.0.1 */ private void initDataSource() { // 根據配置初始化鏈接信息 this.dataSource = new DataSource(); Element dsElem = root.element("dataSource"); Map<String, String> map = new HashMap<>(4); for (Object property : dsElem.elements("property")) { Element element = (Element) property; String name = element.attributeValue("name"); String value = element.attributeValue("value"); map.put("jdbc." + name, value); } dataSource.username(map.get(DataSourceConst.USERNAME)) .password(map.get(DataSourceConst.PASSWORD)) .driver(map.get(DataSourceConst.DRIVER)) .url(map.get(DataSourceConst.URL)); }
解析 xml 中的 mapper 配置。
/** * 初始化 mapper 信息 * * @since 0.0.1 */ private void initMapper() { Element mappers = root.element("mappers"); // 遍歷全部須要初始化的 mapper 文件路徑 for (Object item : mappers.elements("mapper")) { Element mapper = (Element) item; String path = mapper.attributeValue("resource"); mapperRegister.addMapper(path); } }
mapperRegister 就是對方法的元數據進行一些構建,好比出參,入參的類型,等等,便於後期使用。
好比咱們的 UserMapper.xml 方法內容以下:
<select id = "selectById" paramType="java.lang.Long" resultType = "com.github.houbb.mybatis.domain.User"> select * from user where id = ? </select>
sql 就是:select * from user where id = ?
方法標識:selectById
入參:Long
出參:User
SqlSession sqlSession = new DefaultSessionFactory(config).openSession();
這句話實際執行的是:
@Override public SqlSession openSession() { return new DefaultSqlSession(config, new SimpleExecutor()); }
UserMapper userMapper = sqlSession.getMapper(UserMapper.class)
這裏獲取 mapper,實際獲取的是什麼呢?
實際上獲取到的是一個代理。
mybatis 將咱們的接口,和實際 xml 中的 sql 兩者經過動態代理結合,讓咱們調用 xml 中的 sql 和使用接口方法同樣天然。
getMapper 其實是一個動態代理。
@Override @SuppressWarnings("all") public <T> T getMapper(Class<T> clazz) { MapperProxy proxy = new MapperProxy(clazz, this); return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, proxy); }
MapperProxy 的實現以下:
public class MapperProxy implements InvocationHandler { /** * 類信息 * * @since 0.0.1 */ private final Class clazz; /** * sql session * * @since 0.0.1 */ private final SqlSession sqlSession; public MapperProxy(Class clazz, SqlSession sqlSession) { this.clazz = clazz; this.sqlSession = sqlSession; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MapperMethod mapperMethod = this.sqlSession.getConfig() .getMapperMethod(clazz, method.getName()); if (mapperMethod != null) { return this.sqlSession.selectOne(mapperMethod, args); } return method.invoke(proxy, args); } }
當咱們執行 userMapper.selectById(1L)
時,實際執行的是什麼?
實際執行的是 sqlSession.selectOne(mapperMethod, args)
selectOne 是比較核心的內容了。
總體以下
public <T> T query(final Config config, MapperMethod method, Object[] args) { try(Connection connection = config.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(method.getSql());) { // 2. 處理參數 parameterHandle(preparedStatement, args); // 3. 執行方法 preparedStatement.execute(); // 4. 處理結果 final Class resultType = method.getResultType(); ResultSet resultSet = preparedStatement.getResultSet(); ResultHandler resultHandler = new ResultHandler(resultType); Object result = resultHandler.buildResult(resultSet); return (T) result; } catch (SQLException ex) { throw new MybatisException(ex); } }
咱們獲取到 xml 中的 sql,而後構建 jdbc 中你們比較熟悉的 PreparedStatement。
而後對出參和入參進行處理,最後返回結果。
public void setParams(final Object[] objects) { try { for(int i = 0; i < objects.length; i++) { Object value = objects[i]; // 目標類型,這個後期能夠根據 jdbcType 獲取 // jdbc 下標從1開始 statement.setObject(i+1, value); } } catch (SQLException throwables) { throw new MybatisException(throwables); } }
針對咱們很是簡單的例子:
select * from user where id = ?
那就是直接把入參中的 1L 設置到佔位符 ?
便可。
這裏主要用到反射,將查詢結果和 javaBean 作一一映射。
/** * 構建結果 * @param resultSet 結果集合 * @return 結果 * @since 0.0.1 */ public Object buildResult(final ResultSet resultSet) { try { // 基本類型,非 java 對象,直接返回便可。 // 能夠進行抽象 Object instance = resultType.newInstance(); // 結果大小的判斷 // 爲空直接返回,大於1則報錯 if(resultSet.next()) { List<Field> fieldList = ClassUtil.getAllFieldList(resultType); for(Field field : fieldList) { Object value = getResult(field, resultSet); ReflectFieldUtil.setValue(field, instance, value); } // 返回設置值後的結果 return instance; } return null; } catch (InstantiationException | IllegalAccessException | SQLException e) { throw new MybatisException(e); } }
到這裏,一個簡易版的 myabtis 就能夠跑起來了。
固然這裏還有不少的不足之處,咱們後續都會一一優化。
爲了便於學習,完整版本代碼以開源: