前言
mybaits 在 ORM 框架中,可算是半壁江山了,由於它是輕量級,半自動載入,靈活性和易拓展性。深受廣大公司的喜愛,所以我們程式開發也離不開 mybatis 。但是我們有對 mabtis 原始碼進行研究嗎?或者想看但是不知道怎麼看的苦惱嗎?
歸根結底,我們還是需要知道為什麼會有 mybatis ,mybatis 解決了什麼問題? 想要知道 mybatis 解決了什麼問題,就要知道傳統的 JDBC 操作存在哪些痛點才促使 mybatis 的誕生。 我們帶著這些疑問,再來一步步學習吧。
原始 JDBC 存在的問題所以我們先來來看下原始 JDBC 的操作
我們知道最原始的資料庫操作。分為以下幾步
1、獲取 connection 連線 2、獲取 preparedStatement 3、引數替代佔位符 4、獲取執行結果 resultSet 5、解析封裝 resultSet 到物件中返回。
如下是原始 JDBC 的查詢程式碼,存在哪些問題?
public static void main(String[] args) { String dirver="com.mysql.jdbc.Driver"; String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"; String userName="root"; String password="123456"; Connection connection=null; List<User> userList=new ArrayList<>(); try { Class.forName(dirver); connection= DriverManager.getConnection(url,userName,password); String sql="select * from user where username=?"; PreparedStatement preparedStatement=connection.prepareStatement(sql); preparedStatement.setString(1,"張三"); System.out.println(sql); ResultSet resultSet=preparedStatement.executeQuery(); User user=null; while(resultSet.next()){ user=new User(); user.setId(resultSet.getInt("id")); user.setUsername(resultSet.getString("username")); user.setPassword(resultSet.getString("password")); userList.add(user); } } catch (Exception e) { e.printStackTrace(); }finally { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } if (!userList.isEmpty()) { for (User user : userList) { System.out.println(user.toString()); } } }
小夥伴們發現了上面有哪些不友好的地方?
我這裡總結了以下幾點:
1、資料庫的連線資訊存在硬編碼,即是寫死在程式碼中的。 2、每次操作都會建立和釋放 connection 連線,操作資源的不必要的浪費。 3、sql 和引數存在硬編碼。 4、將返回結果集封裝成實體類麻煩,要建立不同的實體類,並透過 set 方法一個個的注入。
存在上面的問題,所以 mybatis 就對上述問題進行了改進。 對於硬編碼,我們很容易就想到配置檔案來解決。mybatis 也是這麼解決的。 對於資源浪費,我們想到使用連線池,mybatis 也是這個解決的。 對於封裝結果集麻煩,我們想到是用 JDK 的反射機制,好巧,mybatis 也是這麼解決的。
設計思路既然如此,我們就來寫一個自定義持久層框架,來解決上述問題,當然是參照 mybatis 的設計思路,這樣我們在寫完之後,再來看 mybatis 的原始碼就恍然大悟,這個地方這樣配置原來是因為這樣啊。
我們分為使用端和框架端兩部分。
使用端我們在使用 mybatis 的時候是不是需要使用 SqlMapConfig.xml 配置檔案,用來存放資料庫的連線資訊,以及 mapper.xml 的指向資訊。mapper.xml 配置檔案用來存放 sql 資訊。
所以我們在使用端來建立兩個檔案 SqlMapConfig.xml 和 mapper.xml。
框架端框架端要做哪些事情呢?如下:
1、獲取配置檔案。也就是獲取到使用端的 SqlMapConfig.xml 以及 mapper.xml 的檔案 2、解析配置檔案。對獲取到的檔案進行解析,獲取到連線資訊,sql,引數,返回型別等等。這些資訊都會儲存在 configuration 這個物件中。 3、建立 SqlSessionFactory,目的是建立 SqlSession 的一個例項。 4、建立 SqlSession ,用來完成上面原始 JDBC 的那些操作。
那在 SqlSession 中 進行了哪些操作呢?
1、獲取資料庫連線 2、獲取 sql ,並對 sql 進行解析 3、透過內省,將引數注入到 preparedStatement 中 4、執行 sql 5、透過反射將結果集封裝成物件
使用端實現好了,上面說了一下,大概的設計思路,主要也是仿照 mybatis 主要的類實現的,保證類名一致,方便我們後面閱讀原始碼。我們先來配置好使用端吧,我們建立一個 maven 專案。
在專案中,我們建立一個 User 實體類
public class User { private Integer id; private String username; private String password; private String birthday; //getter()和 setter()方法}
建立 SqlMapConfig.xml 和 Mapper.xml SqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?><configuration> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false"></property> <property name="userName" value="root"></property> <property name="password" value="123456"></property> <mapper resource="UserMapper.xml"> </mapper></configuration>
可以看到我們 xml 中就配置了資料庫的連線資訊,以及 mapper 一個索引。mybatis 中的 SqlMapConfig.xml 中還包含其他的標籤,只是豐富了功能而已,所以我們只用最主要的。
mapper.xml 是每個類的 sql 都會生成一個對應的 mapper.xml 。我們這裡就用 User 類來說吧,所以我們就建立一個 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?><mapper namespace="cn.quellanan.dao.UserDao"> <select id="selectAll" resultType="cn.quellanan.pojo.User"> select * from user </select> <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User"> select * from user where username=#{username} </select></mapper>
可以看到有點 mybatis 裡面檔案的味道,有 namespace 表示名稱空間,id 唯一標識,resultType 返回結果集的型別,paramType 引數的型別。 我們使用端先建立到這,主要是兩個配置檔案,我們接下來看看框架端是怎麼實現的。
加油哈哈。
框架端實現框架端,我們按照上面的設計思路一步一步來。
獲取配置怎麼樣獲取配置檔案呢?我們可以使用 JDK 自帶自帶的類 Resources 載入器來獲取檔案。我們建立一個自定義 Resource 類來封裝一下:
import java.io.InputStream;public class Resources { public static InputStream getResources(String path){ //使用系統自帶的類 Resources 載入器來獲取檔案。 return Resources.class.getClassLoader().getResourceAsStream(path); }}
這樣透過傳入路徑,就可以獲取到對應的檔案流啦。
解析配置檔案上面獲取到了 SqlMapConfig.xml 配置檔案,我們現在來解析它。 不過在此之前,我們需要做一點準備工作,就是解析的記憶體放到什麼地方?
所以我們來建立兩個實體類 Mapper 和 Configuration 。
Mapper Mapper 實體類用來存放使用端寫的 mapper.xml 檔案的內容,我們前面說了裡面有 id、sql、resultType 和 paramType .所以我們建立的 Mapper 實體如下:
public class Mapper { private String id; private Class<?> resultType; private Class<?> parmType; private String sql; //getter()和 setter()方法}
這裡我們為什麼不新增 namespace 的值呢? 聰明的你肯定發現了,因為 mapper 裡面這些屬性表明每個 sql 都對應一個 mapper , 而 namespace 是一個名稱空間,算是 sql 的上一層,所以在 mapper 中暫時使用不到,就沒有添加了。
Configuration Configuration 實體用來儲存 SqlMapConfig 中的資訊。所以需要儲存資料庫連線,我們這裡直接用 JDK 提供的 DataSource 。還有一個就是 mapper 的資訊。每個 mapper 有自己的標識,所以這裡採用 hashMap 來儲存。如下:
public class Configuration { private DataSource dataSource; HashMap <String,Mapper> mapperMap=new HashMap<>(); //getter()和 setter 方法 }
XmlMapperBuilder做好了上面的準備工作,我們先來解析 mapper 吧。我們建立一個 XmlMapperBuilder 類來解析。透過 dom4j 的工具類來解析 XML 檔案。我這裡用的 dom4j 依賴為:
<dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>2.1.3</version> </dependency>
思路:
1、獲取檔案流,轉成 document。 2、獲取根節點,也就是 mapper。獲取根節點的 namespace 屬性值 3、獲取 select 節點,獲取其 id,sql , resultType ,paramType 4、將 select 節點的屬性封裝到 Mapper 實體類中。 5、同理獲取 update/insert/delete 節點的屬性值封裝到 Mapper 中 6、透過 namespace.id 生成 key 值將 mapper 物件儲存到 Configuration 實體中的 HashMap 中。 7、返回 Configuration 實體 程式碼如下:
public class XmlMapperBuilder { private Configuration configuration; public XmlMapperBuilder(Configuration configuration){ this.configuration=configuration; } public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException { Document document=new SAXReader().read(in); Element rootElement=document.getRootElement(); String namespace=rootElement.attributeValue("namespace"); List<Node> list=rootElement.selectNodes("//select"); for (int i = 0; i < list.size(); i++) { Mapper mapper=new Mapper(); Element element= (Element) list.get(i); String id=element.attributeValue("id"); mapper.setId(id); String paramType = element.attributeValue("paramType"); if(paramType!=null && !paramType.isEmpty()){ mapper.setParmType(Class.forName(paramType)); } String resultType = element.attributeValue("resultType"); if (resultType != null && !resultType.isEmpty()) { mapper.setResultType(Class.forName(resultType)); } mapper.setSql(element.getTextTrim()); String key=namespace+"."+id; configuration.getMapperMap().put(key,mapper); } return configuration; }}
上面我只解析了 select 標籤。大家可以解析對應 insert/delete/uupdate 標籤,操作都是一樣的。
XmlConfigBuilder我們再來解析一下 SqlMapConfig.xml 配置資訊思路是一樣的, 1、獲取檔案流,轉成 document。 2、獲取根節點,也就是 configuration。 3、獲取根節點中所有的 property 節點,並獲取值,也就是獲取資料庫連線資訊 4、建立一個 dataSource 連線池 5、將連線池資訊儲存到 Configuration 實體中 6、獲取根節點的所有 mapper 節點 7、呼叫 XmlMapperBuilder 類解析對應 mapper 並封裝到 Configuration 實體中 8、完 程式碼如下:
public class XmlConfigBuilder { private Configuration configuration; public XmlConfigBuilder(Configuration configuration){ this.configuration=configuration; } public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { Document document=new SAXReader().read(in); Element rootElement=document.getRootElement(); //獲取連線資訊 List<Node> propertyList=rootElement.selectNodes("//property"); Properties properties=new Properties(); for (int i = 0; i < propertyList.size(); i++) { Element element = (Element) propertyList.get(i); properties.setProperty(element.attributeValue("name"),element.attributeValue("value")); } //是用連線池 ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setDriverClass(properties.getProperty("driverClass")); dataSource.setJdbcUrl(properties.getProperty("jdbcUrl")); dataSource.setUser(properties.getProperty("userName")); dataSource.setPassword(properties.getProperty("password")); configuration.setDataSource(dataSource); //獲取 mapper 資訊 List<Node> mapperList=rootElement.selectNodes("//mapper"); for (int i = 0; i < mapperList.size(); i++) { Element element= (Element) mapperList.get(i); String mapperPath=element.attributeValue("resource"); XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration); configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath)); } return configuration; }}
建立 SqlSessionFactory完成解析後我們建立 SqlSessionFactory 用來建立 Sqlseesion 的實體,這裡為了儘量還原 mybatis 設計思路,也也採用的工廠設計模式。 SqlSessionFactory 是一個介面,裡面就一個用來建立 SqlSessionf 的方法。 如下:
public interface SqlSessionFactory { public SqlSession openSqlSession();}
單單這個介面是不夠的,我們還得寫一個介面的實現類,所以我們建立一個 DefaultSqlSessionFactory。 如下:
public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } public SqlSession openSqlSession() { return new DefaultSqlSeeion(configuration); }}
可以看到就是建立一個 DefaultSqlSeeion 並將包含配置資訊的 configuration 傳遞下去。DefaultSqlSeeion 就是 SqlSession 的一個實現類。
建立 SqlSession在 SqlSession 中我們就要來處理各種操作了,比如 selectList,selectOne,insert, update , delete 等等。 如下:
public interface SqlSession { /** * 條件查詢 * @param statementid 唯一標識,namespace.selectid * @param parm 傳參,可以不傳也可以一個,也可以多個 * @param <E> * @return */ public <E> List<E> selectList(String statementid,Object...parm) throws Exception; public <T> T selectOne(String statementid, Object...parm) throws Exception; public int insert(String statementid, Object...parm) throws Exception; public int update(String statementid, Object...parm) throws Exception; public int delete(String statementid, Object...parm) throws Exception; public void commit() throws Exception; /** * 使用代理模式來建立介面的代理物件 * @param mapperClass * @param <T> * @return */ public <T> T getMapper(Class<T> mapperClass);
然後我們建立 DefaultSqlSeeion 來實現 SqlSeesion 。
public class DefaultSqlSeeion implements SqlSession { private Configuration configuration; private Executer executer=new SimpleExecuter(); public DefaultSqlSeeion(Configuration configuration) { this.configuration = configuration; } @Override public <E> List<E> selectList(String statementid, Object... parm) throws Exception { Mapper mapper=configuration.getMapperMap().get(statementid); List<E> query = executer.query(configuration, mapper, parm); return query; } @Override public <T> T selectOne(String statementid, Object... parm) throws Exception { List<Object> list =selectList(statementid, parm); if(list.size()==1){ return (T) list.get(0); }else{ throw new RuntimeException("返回結果過多"); } } @Override public int insert(String statementid, Object... parm) throws Exception { return update(statementid,parm); } @Override public int update(String statementid, Object... parm) throws Exception { Mapper mapper=configuration.getMapperMap().get(statementid); int update = executer.update(configuration, mapper, parm); return update; } @Override public int delete(String statementid, Object... parm) throws Exception { return update(statementid,parm); } @Override public void commit() throws Exception { executer.commit(); } @Override public <T> T getMapper(Class<T> mapperClass) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //獲取到方法名 String name = method.getName(); //型別 String className = method.getDeclaringClass().getName(); String statementid=className+"."+name; Mapper mapper = configuration.getMapperMap().get(statementid); SqlCommandType sqlType = mapper.getSqlCommandType(); Type genericReturnType = method.getGenericReturnType(); switch (sqlType){ case SELECT: //判斷是否實現泛型型別引數化 if(genericReturnType instanceof ParameterizedType){ return selectList(statementid,args); }else { return selectOne(statementid,args); } case INSERT:return insert(statementid,args); case DELETE:return delete(statementid,args); case UPDATE:return update(statementid,args); default:break; } return null; } }); return (T) proxyInstance; }}
我們可以看到 DefaultSqlSeeion 獲取到了 configuration,並透過 statementid 從 configuration 中獲取 mapper。 然後具體實現交給了 Executer 類來實現。我們這裡先不管 Executer 是怎麼實現的,就假裝已經實現了。那麼整個框架端就完成了。透過呼叫 Sqlsession.selectList() 方法,來獲取結果。
感覺我們都還沒有處理,就框架搭建好了?騙鬼呢,確實前面我們從獲取檔案解析檔案,然後建立工廠。都是做好準備工作。下面開始我們 JDBC 的實現。
SqlSession 具體實現我們前面說 SqlSeesion 的具體實現有下面 5 步
1、獲取資料庫連線 2、獲取 sql,並對 sql 進行解析 3、透過內省,將引數注入到 preparedStatement 中 4、執行 sql 5、透過反射將結果集封裝成物件
但是我們在 DefaultSqlSeeion 中將實現交給了 Executer 來執行。所以我們就要在 Executer 中來實現這些操作。
我們首先來建立一個 Executer 介面,並寫一個 DefaultSqlSeeion 中呼叫的 query 方法。
public interface Executer { <E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception;}
接著我們寫一個 SimpleExecuter 類來實現 Executer 。 然後 SimpleExecuter.query() 方法中,我們一步一步的實現。
獲取資料庫連線因為資料庫連線資訊儲存在 configuration,所以直接獲取就好了。
//獲取連線 connection=configuration.getDataSource().getConnection();
獲取 sql,並對 sql 進行解析
我們這裡想一下,我們在 Usermapper.xml 寫的 sql 是什麼樣子?
select * from user where username=#{username}
{username} 這樣的 sql 我們該怎麼解析呢?
分兩步 1、將 sql 找到 #{***} ,並將這部分替換成 ?號
2、對 #{***} 進行解析獲取到裡面的引數對應的 paramType 中的值。
具體實現用到下面幾個類。 GenericTokenParser 類,可以看到有三個引數,開始標記,就是我們的 “#{” ,結束標記就是 “}” , 標記處理器就是處理標記裡面的內容也就是 username。
public class GenericTokenParser { private final String openToken; //開始標記 private final String closeToken; //結束標記 private final TokenHandler handler; //標記處理器 public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } /** * 解析${}和#{} * @param text * @return * 該方法主要實現了配置檔案、指令碼等片段中佔位符的解析、處理工作,並返回最終需要的資料。 * 其中,解析工作由該方法完成,處理工作是由處理器 handler 的 handleToken()方法來實現 */ public String parse(String text) { //具體實現 } }
主要的就是 parse() 方法,用來獲取操作 1 的 sql。獲取結果例如:
select * from user where username=?
那上面用到 TokenHandler 來處理引數。 ParameterMappingTokenHandler 實現 TokenHandler 的類
public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); // context 是引數名稱 #{id} #{username} @Override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; } private ParameterMapping buildParameterMapping(String content) { ParameterMapping parameterMapping = new ParameterMapping(content); return parameterMapping; } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } public void setParameterMappings(List<ParameterMapping> parameterMappings) { this.parameterMappings = parameterMappings; }}
可以看到將引數名稱存放 ParameterMapping 的集合中了。 ParameterMapping 類就是一個實體,用來儲存引數名稱的。
public class ParameterMapping { private String content; public ParameterMapping(String content) { this.content = content; } //getter()和 setter() 方法。}
所以我們在我們透過 GenericTokenParser 類,就可以獲取到解析後的 sql,以及引數名稱。我們將這些資訊封裝到 BoundSql 實體類中。
public class BoundSql { private String sqlText; private List<ParameterMapping> parameterMappingList=new ArrayList<>(); public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) { this.sqlText = sqlText; this.parameterMappingList = parameterMappingList; } ////getter()和 setter() 方法。 }
好了,那麼分兩步走,先獲取,後解析 獲取 獲取原始 sql 很簡單,sql 資訊就存在 mapper 物件中,直接獲取就好了。
String sql=mapper.getSql()
解析 1、建立一個 ParameterMappingTokenHandler 處理器 2、建立一個 GenericTokenParser 類,並初始化開始標記,結束標記,處理器 3、執行 genericTokenParser.parse(sql) ;獲取解析後的 sql‘’,以及在 parameterMappingTokenHandler 中存放了引數名稱的集合。 4、將解析後的 sql 和引數封裝到 BoundSql 實體類中。
/** * 解析自定義佔位符 * @param sql * @return */ private BoundSql getBoundSql(String sql){ ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler); String parse = genericTokenParser.parse(sql); return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings()); }
將引數注入到 preparedStatement 中上面的就完成了 sql 的解析,但是我們知道上面得到的 sql 還是包含 JDBC 的 佔位符,所以我們需要將引數注入到 preparedStatement 中。
1、透過 boundSql.getSqlText() 獲取帶有佔位符的 sql . 2、接收引數名稱集合 parameterMappingList 3、透過 mapper.getParmType() 獲取到引數的類。 4、透過 getDeclaredField(content) 方法獲取到引數類的 Field。 5、透過 Field.get() 從引數類中獲取對應的值 6、注入到 preparedStatement 中
BoundSql boundSql=getBoundSql(mapper.getSql()); String sql=boundSql.getSqlText(); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); //獲取 preparedStatement,並傳遞引數值 PreparedStatement preparedStatement=connection.prepareStatement(sql); Class<?> parmType = mapper.getParmType(); for (int i = 0; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String content = parameterMapping.getContent(); Field declaredField = parmType.getDeclaredField(content); declaredField.setAccessible(true); Object o = declaredField.get(parm[0]); preparedStatement.setObject(i+1,o); } System.out.println(sql); return preparedStatement;
執行 sql其實還是呼叫 JDBC 的 executeQuery() 方法或者 execute() 方法
//執行 sql ResultSet resultSet = preparedStatement.executeQuery();
透過反射將結果集封裝成物件
在獲取到 resultSet 後,我們進行封裝處理,和引數處理是類似的。
1、建立一個 ArrayList 2、獲取返回型別的類 3、迴圈從 resultSet 中取資料 4、獲取屬性名和屬性值 5、建立屬性生成器 6、為屬性生成寫方法,並將屬性值寫入到屬性中 7、將這條記錄新增到 list 中 8、返回 list
/** * 封裝結果集 * @param mapper * @param resultSet * @param <E> * @return * @throws Exception */ private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{ ArrayList<E> list=new ArrayList<>(); //封裝結果集 Class<?> resultType = mapper.getResultType(); while (resultSet.next()) { ResultSetMetaData metaData = resultSet.getMetaData(); Object o = resultType.newInstance(); int columnCount = metaData.getColumnCount(); for (int i = 1; i <= columnCount; i++) { //屬性名 String columnName = metaData.getColumnName(i); //屬性值 Object value = resultSet.getObject(columnName); //建立屬性描述器,為屬性生成讀寫方法 PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType); Method writeMethod = propertyDescriptor.getWriteMethod(); writeMethod.invoke(o,value); } list.add((E) o); } return list; }
建立 SqlSessionFactoryBuilder我們現在來建立一個 SqlSessionFactoryBuilder 類,來為使用端提供一個人口。
public class SqlSessionFactoryBuilder { private Configuration configuration; public SqlSessionFactoryBuilder(){ configuration=new Configuration(); } public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration); configuration=xmlConfigBuilder.loadXmlConfig(in); SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration); return sqlSessionFactory; }}
可以看到就一個 build 方法,透過 SqlMapConfig 的檔案流將資訊解析到 configuration ,建立並返回一個 sqlSessionFactory 。
到此,整個框架端已經搭建完成了,但是我們可以看到,只實現了 select 的操作, update 、inster 、delete 的操作我們在我後面提供的原始碼中會有實現,這裡只是將整體的設計思路和流程。
測試終於到了測試的環節啦。我們前面寫了自定義的持久層,我們現在來測試一下能不能正常的使用吧。 見證奇蹟的時刻到啦
我們先引入我們自定義的框架依賴。以及資料庫和單元測試
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> <dependency> <groupId>cn.quellanan</groupId> <artifactId>myself-mybatis</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> </dependency>
然後我們寫一個測試類 1、獲取 SqlMapperConfig.xml 的檔案流 2、獲取 Sqlsession 3、執行查詢操作
@org.junit.Test public void test() throws Exception{ InputStream inputStream= Resources.getResources("SqlMapperConfig.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession(); List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll"); for (User parm : list) { System.out.println(parm.toString()); } System.out.println(); User user=new User(); user.setUsername("張三"); List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user); for (User user1 : list1) { System.out.println(user1); } }
可以看到已經可以了,看來我們自定義的持久層框架生效啦。
最佳化但是不要高興的太早哈哈,我們看上面的測試方法,是不是感覺和平時用的不一樣,每次都都寫死 statementId ,這樣不太友好,所以我們接下來來點騷操作,通用 mapper 配置。 我們在 SqlSession 中增加一個 getMapper 方法,接收的引數是一個類。我們透過這個類就可以知道 statementId .
/** * 使用代理模式來建立介面的代理物件 * @param mapperClass * @param <T> * @return */ public <T> T getMapper(Class<T> mapperClass);
具體實現就是利用 JDK 的動態代理機制。 1、透過 Proxy.newProxyInstance() 獲取一個代理物件 2、返回代理物件 那代理物件執行了哪些操作呢? 建立代理物件的時候,會實現一個 InvocationHandler 介面,重寫 invoke() 方法,讓所有走這個代理的方法都會執行這個 i nvoke() 方法。那這個方法做了什麼操作? 這個方法就是透過傳入的類物件,獲取到物件的類名和方法名。用來生成 statementid 。所以我們在 mapper.xml 配置檔案中的 namespace 就需要制定為類路徑,以及 id 為方法名。 實現方法:
@Override public <T> T getMapper(Class<T> mapperClass) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //獲取到方法名 String name = method.getName(); //型別 String className = method.getDeclaringClass().getName(); String statementid=className+"."+name; return selectList(statementid,args); } }); return (T) proxyInstance; }
我們寫一個 UserDao
public interface UserDao { List<User> selectAll(); List<User> selectByName(User user);}
這個是不是我們熟悉的味道哈哈,就是 mapper 層的介面。 然後我們在 mapper.xml 中指定 namespace 和 id
接下來我們在寫一個測試方法
@org.junit.Test public void test2() throws Exception{ InputStream inputStream= Resources.getResources("SqlMapperConfig.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession(); UserDao mapper = sqlSession.getMapper(UserDao.class); List<User> users = mapper.selectAll(); for (User user1 : users) { System.out.println(user1); } User user=new User(); user.setUsername("張三"); List<User> users1 = mapper.selectByName(user); for (User user1 : users1) { System.out.println(user1); } }
番外自定義的持久層框架,我們就寫完了。這個實際上就是 mybatis 的雛形,我們透過自己手動寫一個持久層框架,然後在來看 mybatis 的原始碼,就會清晰很多。下面這些類名在 mybatis 中都有體現。
這裡拋磚引玉,祝君閱讀原始碼愉快。 覺得有用的兄弟們記得收藏啊。