原始碼學習之路----mybatis
在開始本文之前呢,我們首先需要了解一下傳統的jdbc存在的問題
傳統的jdbc的程式碼是這樣的:
程式碼 1
public class Test { public static final String URL = "jdbc:mysql://localhost:3306/test"; public static final String USER = "root"; public static final String PASSWORD = "root"; public static void main(String[] args) throws Exception {//1.載入驅動程式 Class.forName("com.mysql.jdbc.Driver"); //2. 獲得資料庫連線 Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); //3.操作資料庫,實現增刪改查 Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT user_name, age FROM imooc_goddess");//如果有資料,rs.next()返回true while(rs.next()){ System.out.println(rs.getString("user_name")+" 年齡:"+rs.getInt("age")); } }
觀察如上程式碼發現 ,傳統的jdbc存在如下問題:
1.資料庫配置資訊存在硬編碼。 2.每次執行都要開啟資料庫連線,頻繁建立釋放資料庫連線。 3.sql語句存在硬編碼。 4.需要手動封裝返回結果集,較為繁瑣。 那麼就上述問題,我們給出如下的解決方案:1)資料庫配置資訊,以及sql語句的編寫,我們都放入配置檔案中,由此以來,方便維護。 2)對於每次都要建立釋放資料庫連線,我們使用池技術來避免頻繁建立釋放資料庫連線帶來的資源浪費。 3)返回結果我們可以使用反射,內省的方式進行封裝。 接下來,我們開始手動封裝一個框架來解決傳統的jdbc問題 對於使用者來說,我們需要提供資料庫的配置資訊 和 sql配置資訊,所以,需要提供兩個檔案(使用xml來進行存放),sqlMapConfig.xml,mapper.xml來存放資料庫的配置資訊以及sql的配置資訊。 程式碼 2
<configuration> <!--資料庫配置資訊--> <dataSource> <property name="dirverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </dataSource> <!-- 為了一次性將所有的資源全部讀入,此處我們用來存放mapper.xml全路徑--> <mapper resource="UserMapper.xml"></mapper> </configuration>
程式碼 3
mapper.xml
說明:
為了防止我們在呼叫方法時,不同類中方法名重複,我們設定 sql的唯一標識namespace.id =》statementId 來避免這個問題,
比如 UserMapper.class中與Order.class中都存在findAll方法,我們就要在userMapper.xml和OrderMapper.xml中都設定一下namespace屬性,
這樣 mapper標籤的namespace屬性加上select標籤的id屬性就可以唯一的確定一個sql語句了。
<mapper namespace="user"> <!-- 為了防止我們在呼叫方法時,不同類中方法名重複,我們設定 sql的唯一標識namespace.id =》statementId 來避免這個問題
比如 UserMapper.class中與Order.class中都存在findAll方法,我們就要在userMapper.xml和OrderMapper.xml中都設定一下namespace屬性,
這樣 mapper標籤的namespace屬性加上select標籤的id屬性就可以唯一的確定一個sql語句了--> <select id="findAll" resultType="com.hg.pojo.User"> select * from user </select> <select id="findByCondition" resultType="com.hg.pojo.User" paramterType="com.hg.pojo.User"> select * from user where id = #{id} and username = #{username} </select> </mapper>
這樣,配置檔案就寫好了。
接下來,我們分析下框架中需要的類,以及實現方法:
首先,我們需要載入使用者的配置檔案:根據配置檔案的路徑載入配置檔案成位元組輸入流,儲存在記憶體中。
於是,我們建立一個Resource類來實現這個功能
我們建立一個maven工程,建立一個包com.xx.io來存放我們的Resource類,該類只有一個方法,就是讀取配置資訊成位元組流到記憶體中。
引入依賴
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.17</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <dependency> <groupId>jaxen</groupId> <artifactId>jaxen</artifactId> <version>1.1.6</version> </dependency>
程式碼 4
public class Resource { //根據配置檔案路徑,將配置檔案ji載入成位元組流,儲存在記憶體中 public static InputStream getResourceAsStream(String path){ InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }
然後我們需要建立兩個Bean來存放解析出來的資料庫配置資訊和sql配置資訊:
Configuration:核心配置類:存放sqlMapConfig.xml解析出來的內容 MappedStatement:對映配置類:存放mapper.xml解析出來的內容 建立一個包com.xx.pojo來存放我們的Configuration類以及MappedStatement類 程式碼 5import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
public class Configuration { private DataSource dataSource; public DataSource getDataSource() { return dataSource; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Map<String, MappedStatement> getMappedStatementMap() { return mappedStatementMap; } public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) { this.mappedStatementMap = mappedStatementMap; } //key statementId value:封裝好的 mappedStatement Map<String,MappedStatement> mappedStatementMap = new HashMap<>(); }
public class MappedStatement {
//id標識
private String id;
// 返回值型別
private String resultType;
// 引數值型別
private String paramterType;
// sql語句
private String sql;
public String getId() { return id; } public void setId(String id) { this.id = id; } public String getResultType() { return resultType; } public void setResultType(String resultType) { this.resultType = resultType; } public String getParamterType() { return paramterType; } public void setParamterType(String paramterType) { this.paramterType = paramterType; } public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } }
MappedStatement類中,我們定義了四個屬性,分別對應我們將來解析出來的mapper.xml檔案中的id、resultType、paramterType和sql。
Configuration類中,我們設定兩個屬性,分別是dataSource和mappedStatementMap ,dataSource屬性存放的是資料庫的配置資訊,mappedStatementMap存放的是sql的資訊,這裡可以看到,我們的mappedStatementMap是一個map型別,因為我們每個mapper.xml中都會存在多個sql語句,所以,我們在解析時,將會把我們前面提到的用來唯一定位一條sql語句的statementid來當做map的key,而解析出來的MappedStatement就是map的value。
至此,我們就把兩個配置檔案解析好後存放配置資訊的類都建立好了,接下來,我們來看一下,這個配置檔案是如何解析的呢?
我們需要建立類:SQLSessionFactoryBuilder類來解析Resource類讀取來的位元組流中的資訊 首先,我們在com.xx.sqlsession包下建立一個SqlSession介面,和一個SqlSessionFactory介面,在SqlSessionFactory介面中我們建立一個openSession方法用來建立SqlSession 程式碼 6public interface SqlSession { // 查詢所有 public <E> List<E> selectList(String statementId,Object... params) throws Exception; // 根據條件查詢一條 public <T> T selectOne(String statementId,Object... params) throws Exception; public int insert(String statementId,Object... params) throws Exception; public int delete(String statementId,Object... params) throws Exception; public int update(String statementId,Object... params) throws Exception; //為dao介面生成代理實現類 public <T> T getMapper(Class<?> mapperClass); }
public interface SqlSessionFactory { public SqlSession openSession(); }接下來我們在com.xx.sqlsession包下建立SQLSessionFactoryBuilder類用來構建一個SqlSessionFactory: 程式碼 7
import com.hg.config.XMLConfigBuilder; import com.hg.pojo.Configuration; import org.dom4j.DocumentException; import java.beans.PropertyVetoException; import java.io.InputStream; public class SqlSessionFactoryBuilder { public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException { // 第一:使用dom4j解析配置檔案,將解析出來的內容封裝到Configuration中 XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(); Configuration configuration = xmlConfigBuilder.parseConfig(in); // 第二:建立sqlSessionFactory物件: 工廠類:生產sqlSession:會話物件 DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration); return defaultSqlSessionFactory; } }這個類主要有兩大作用: 第一:我們將使用dom4j解析配置檔案,將解析出來的內容封裝到容器物件中(Configuration,MappedStatement) 第二:建立SqlSessionFactory物件;生產sqlSession:會話物件(此處使用的是工廠模式) 接下來我們詳細介紹SqlSessionFactoryBuilder的具體實現 首先我們建立一個XMLConfigBuilder類來對sqlMapConfig.xml進行解析 程式碼 8
import com.hg.io.Resource; import com.hg.pojo.Configuration; import com.mchange.v2.c3p0.ComboPooledDataSource; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.beans.PropertyVetoException; import java.io.InputStream; import java.util.List; import java.util.Properties; public class XMLConfigBuilder { private Configuration configuration; public XMLConfigBuilder() { this.configuration = new Configuration(); } //該方法是使用dom4j將配置檔案踐行解析,封裝Configuration public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException { Document document = new SAXReader().read(inputStream); //拿到 <configuration> Element rootElement = document.getRootElement(); List<Element> list = rootElement.selectNodes("//property"); Properties properties = new Properties(); for (Element element : list) { String name = element.attributeValue("name"); String value = element.attributeValue("value"); properties.setProperty(name,value); } ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); comboPooledDataSource.setDriverClass(properties.getProperty("dirverClass")); comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl")); comboPooledDataSource.setUser(properties.getProperty("username")); comboPooledDataSource.setPassword(properties.getProperty("password")); configuration.setDataSource(comboPooledDataSource); // mapper.xml解析 拿到路徑 獲取位元組輸入流 進行解析 List<Element> mapperList = rootElement.selectNodes("//mapper"); for (Element element : mapperList) { String mapperPath = element.attributeValue("resource"); InputStream resourceAsStream = Resource.getResourceAsStream(mapperPath); XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration); xmlMapperBuilder.parse(resourceAsStream); } return configuration; } }
這邊我們詳細講解一下這個類:
首先我們建立一個午餐的構造方法來初始化一個Configuration類用來儲存接下來解析的所有配置資訊。
然後是建立一個parseConfig方法來進行sqlMapConfig.xml的內容解析,這裡傳入的引數就是我們Resource類中獲取到的位元組流,然後通過dom4j的SAXReader開始對內容進行解析。 程式碼中的document就是獲取到的配置檔案內容,通過呼叫document.getRootElement()獲取到<configuration>中的所有內容,然後通過rootElement.selectNodes("//property");獲取到所有property標籤下的內容,我們建立一個Properties來儲存我們接下來解析出來的資料庫配置資訊,簡單的來講此處其實也可以用map來操作; 程式碼中迴圈遍歷property並通過element.attributeValue("name")和element.attributeValue("value")將property標籤中的內容取出存放到Properties中,這樣資料庫的配置資訊就全部都解析完成了。 接下來我們建立一個ComboPooledDataSource連線池,並將解析出來的資料庫配置資訊配置到連線池中,然後將連線池配置到我們的Configuration類的DataSource中。上面我們提到為了一次性將所有的資源全部讀入我們將mapper.xml檔案的的全路徑也配置到了sqlMapConfig.xml中,接下來我們將進行mapper.xml 的解析
我們通過rootElement.selectNodes("//mapper");來獲取到配置資訊中的所有mapper標籤,上文程式碼中層提到,裡面包含著所有的mapper.xml 的路徑資訊(因為我們可能會有多個mapper檔案)。
接下來我們通過迴圈遍歷element.attributeValue("resource")獲取到每一個mapper.xml的路徑,並使用Resource類進行位元組流的讀取。
然後我們需要建立一個XMLMapperBuilder類來對每個mapper.xml進行處理。我們將Configuration作為引數傳遞進去是為了將解析後的mapper.xml資訊儲存到Configuration裡面的mappedStatementMap集合中。
然後我們呼叫XMLMapperBuilder的parse方法來處理讀取到的mapper.xml檔案位元組流。
好了,接下來我們開始研究下如何將mapper.xml檔案的資訊解析出來。
我們先建立XMLMapperBuilder類
程式碼 9
package com.hg.config; import com.hg.pojo.Configuration; import com.hg.pojo.MappedStatement; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; public class XMLMapperBuilder { private Configuration configuration; public XMLMapperBuilder(Configuration configuration) { this.configuration = configuration; } public void parse(InputStream inputStream) throws DocumentException { Document document = new SAXReader().read(inputStream); Element rootElement = document.getRootElement(); String namespace = rootElement.attributeValue("namespace"); List<Element> list = rootElement.selectNodes("//select"); for (Element element : list) { String id = element.attributeValue("id"); String resultType = element.attributeValue("resultType"); String paramterType = element.attributeValue("paramterType"); String sqlText = element.getTextTrim(); MappedStatement mappedStatement = new MappedStatement(); mappedStatement.setId(id); mappedStatement.setParamterType(paramterType); mappedStatement.setResultType(resultType); mappedStatement.setSql(sqlText); String key = namespace+"."+id; configuration.getMappedStatementMap().put(key,mappedStatement); } } }
上面解析sqlMapConfig.xml 的程式碼中我們曾提到,將 Configuration 作為引數傳遞給 XMLMapperBuilder ,那麼XMLMapperBuilder程式碼中,我們建立了一個有參的構造方法來進行引數的傳遞,接下來我們建立一個parse方法來進行mapper.xml檔案的解析,這裡我們接收到的引數是 XMLConfigBuilder 中傳遞過來的mapper.xml位元組流,同樣的使用dom4j來進行解析,重複的程式碼我不再贅述,這邊解析的是mapper.xml中所有select(insert,update,delete)標籤下的所有屬性,包括id,resultType,paramterType以及sql(後面我們還要在MappedStatement中加入一個標記屬性,來判斷我們這個MappedStatement是增,刪,改,查中的哪種);
上文我們提到要想準確的定位一個sql需要通過statementid(namespace.id)來進行唯一標示,所以這邊使用statementid作為mappedStatementMap的key,而獲取到的select標籤下的所有資訊封裝成MappedStatement作為mappedStatementMap的value,這樣這個parse方法就把mapper.xml中的配置資訊傳遞到了Configuration中的mappedStatementMap裡。最終XMLConfigBuilder中的parseConfig方法將所有配置資訊都讀取到了Configuration中並將Configuration返回。這樣上文中的 程式碼7SQLSessionFactoryBuilder類的第一部分就完成了,有了配置資訊,接下來,我們繼續探討如何來進行SQLSession的建立以及如何進行查詢,對查詢結果的解析以及封裝。
我們建立一個DefaultSqlSessionFactory來實現SqlSessionFactory介面
程式碼 10
public class DefaultSqlSessionFactory implements SqlSessionFactory{ private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } @Override public SqlSession openSession() { return new DefaultSqlSession(configuration); } }
此時,我們要建立一個建構函式來傳遞我們剛才解析出來的Configuration,然後我們再建立一個DefaultSqlSession類來實現SqlSession介面的方法,同使這個DefaultSqlSession類也要建立一個有參的構造方法用來傳遞我們的Configuration引數(上面我們分析過,這個Configuration引數很重要,資料庫的配置資訊以及sql的資訊全都儲存在這個Configuration中),然後我們在DefaultSqlSessionFactory類的openSession方法中建立這個DefaultSqlSession例項物件。
接下來我們一起研究一下這個DefaultSqlSession都完成了什麼功能:
程式碼 11
public class DefaultSqlSession implements SqlSession { private Configuration configuration; public DefaultSqlSession(Configuration configuration) { this.configuration = configuration; } @Override public <E> List<E> selectList(String statementId, Object... params) throws Exception { // 將要完成對simpleExcutor裡的query方法 SimpleExecutor simpleExecutor = new SimpleExecutor(); MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); List<Object> list = simpleExecutor.query(configuration, mappedStatement, params); return (List<E>) list; } @Override public <T> T selectOne(String statementId, Object... params) throws Exception { List<Object> objects = selectList(statementId, params); if(objects.size()==1){ return (T) objects.get(0); }else{ throw new RuntimeException("查詢結果為空或返回結果過多"); } } @Override public int insert(String statementId, Object... params) throws Exception { return this.update(statementId,params); } @Override public int delete(String statementId, Object... params) throws Exception { return this.update(statementId,params); } @Override public int update(String statementId, Object... params) throws Exception { SimpleExecutor simpleExecutor = new SimpleExecutor(); MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); int rows = simpleExecutor.update(configuration, mappedStatement, params); return rows; } @Override public <T> T getMapper(Class<?> mapperClass) { //使用動態代理為DAO介面生成代理物件;並返回 Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, (proxy, method, args) -> { // 底層就是執行jdbc 根據不同情況,呼叫 selectList和selectOne // 準備引數 1statementid sql的唯一標識 namespace.id=介面許可權定名.方法名 System.out.println(DefaultSqlSession.class.getClassLoader()); System.out.println(mapperClass.getClassLoader()); String methodName = method.getName(); String className = method.getDeclaringClass().getName(); String statementId = className+"."+methodName; MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); switch (sqlCommandType){ case SELECT: //準備引數2:params:args //獲取被呼叫方法的返回值型別 Type genericReturnType = method.getGenericReturnType(); //判斷是否進行了泛型型別引數化 if(genericReturnType instanceof ParameterizedType){ List<Object> objects = selectList(statementId, args); return objects; } return selectOne(statementId,args); case DELETE: return delete(statementId,args); case INSERT: return insert(statementId,args); case UPDATE: return update(statementId,args); default: return null; } }); return (T) proxyInstance; } }
我們實現了SqlSession介面中的所有方法,接下來我們逐步介紹這個類:
可以看到,我們的增刪改查方法中都用到一個SimpleExecutor類,這個類就是用來實現我們的核心功能:
1.資料庫的連線
2.獲取sql語句並進行轉換(把我們寫的sql語句中的#{}替換成?)
3.獲取預處理物件
4.設定引數(將我們的傳值設定到預處理物件中)
5.執行sql
6.封裝返回結果(將查詢到的資料庫資料封裝成我們的pojo物件)
接下來我們將一步一步完成這些功能。
我們建立一個執行器介面Executor,這裡我們提供兩個方法,query和update,query我們用來查資料,update我們用來 增 刪 改 資料。
程式碼 12
public interface Executor { public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception; public int update(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception; }
然後我們建立一個SimpleExecutor類來實現這個介面中的方法,後面我們將重點的分析一下查詢方法的實現
程式碼 13
public class SimpleExecutor implements Executor { @Override public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception { //1註冊驅動,獲取連線 Connection connection = configuration.getDataSource().getConnection(); //2獲取sql語句 轉換sql語句 #{} 轉換成 ? 轉換的過程對#{}的值解析出來並存儲 String sql = mappedStatement.getSql(); BoundSql boundSql = getBoundSql(sql); //3獲取預處理物件 PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText()); //4設定引數 // 獲取到引數的全路徑 String paramterType = mappedStatement.getParamterType(); Class<?> paramterTypeClass = getClassType(paramterType); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); for (int i = 0; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String content = parameterMapping.getContent(); //反射 Field declaredField = paramterTypeClass.getDeclaredField(content); // 設定暴力訪問 declaredField.setAccessible(true); Object o = declaredField.get(params[0]); preparedStatement.setObject(i+1,o); } //5執行sql ResultSet resultSet = preparedStatement.executeQuery(); String resultType = mappedStatement.getResultType(); Class<?> resultTypeClass = getClassType(resultType); ArrayList<Object> objects = new ArrayList<>(); //6封裝返回結果集 while(resultSet.next()){ Object o = resultTypeClass.newInstance(); //元資料 ResultSetMetaData metaData = resultSet.getMetaData(); // getColumnCount 獲取總列數 就是屬性的個數 for (int i = 1; i <= metaData.getColumnCount(); i++) { //欄位名 String columnName = metaData.getColumnName(i); //欄位的值 Object value = resultSet.getObject(columnName); //使用反射或者內省,根據資料庫表和實體的對應關係,完成封裝 PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultTypeClass); Method writeMethod = propertyDescriptor.getWriteMethod(); writeMethod.invoke(o,value); } objects.add(o); } return (List<E>) objects; } @Override public int update(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception { //1註冊驅動,獲取連線 Connection connection = configuration.getDataSource().getConnection(); //2獲取sql語句 轉換sql語句 #{} 轉換成 ? 轉換的過程對#{}的值解析出來並存儲 String sql = mappedStatement.getSql(); BoundSql boundSql = getBoundSql(sql); //3獲取預處理物件 PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText()); //4設定引數 // 獲取到引數的全路徑 String paramterType = mappedStatement.getParamterType(); Class<?> paramterTypeClass = getClassType(paramterType); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); for (int i = 0; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String content = parameterMapping.getContent(); //反射 Field declaredField = paramterTypeClass.getDeclaredField(content); // 設定暴力訪問 declaredField.setAccessible(true); Object o = declaredField.get(params[0]); preparedStatement.setObject(i+1,o); } //5執行sql int rows = preparedStatement.executeUpdate(); return rows; } private Class<?> getClassType(String paramterType) throws ClassNotFoundException { if (paramterType!=null){ Class<?> aClass = Class.forName(paramterType); return aClass; } return null; } //對#{}進行解析,將#{}使用?代替,解析#{}中的值進行儲存 private BoundSql getBoundSql(String sql) { //標記處理類:配置標記解析器來完成對佔位符的解析處理工作 ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); //標記解析器 GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler); //用標記解析器解析出來的sql String parseSql = genericTokenParser.parse(sql); // #{}解析出來的引數名稱 List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BoundSql boundSql = new BoundSql(parseSql, parameterMappings); return boundSql; } }
我們具體來分析下這個query方法,首先我們註冊驅動,獲取資料庫連線,然後,根據從MappedStatement中獲取sql語句,然後通過getBoundSql方法來解析我們的sql,實際上這一步就是來將我們sql中的#{}替換成?並將#{}中的屬性取出並存儲到parameterMappingList,也就是程式碼中getBoundSql方法的作用,該方法用到的工具類我後面會貼出來。
將sql處理好後,我們獲取預處理物件,然後開始設定引數,這裡我們將會用到反射。
首先我們獲取到入參的全路徑,比如com.xx.User,然後我們通過getClassType這個方法初始化這個類,然後我們迴圈遍歷parameterMappingList 取出裡面的每一個引數,並通過反射將我們傳遞進來的引數中的值取出來,並對應到?相應的位置。而後,我們執行查詢操作,獲取到查詢的結果。
既然獲取到結果,我們就要根據我們設定的resultType來對結果集進行封裝,我們同樣根據反射來取得我們resultType中的類,因為我們查詢的時候,可能是多條,可能是一條,所以我們建立一個Object集合來進行結果集的儲存和返回。
這裡我們通過while來遍歷結果集,每一次迴圈都是一條資料,所以我們沒迴圈一次結果集都要建立一個Object物件進行接收, 每遍歷一次結果集我們都要取出資料來源再進行每一列,也就是每個欄位的遍歷,此處我們for迴圈的 i 是從1開始的,因為我們在資料來源中取列名是從腳標1開始的,我們獲取到列名再通過列名獲取到列值,而後我們通過內省來將列的值賦值給我們建立的Object物件,每一次for迴圈就遍歷賦值一個列,這樣我們就將每個結果集的每一條資料都對映給了Object物件,最終生成一個集合最終返回給使用者。這樣,我們的Query方法就實現了。至此,傳統(後面我們還講分析一下傳統方法的不足,以及如何優化,程式碼中已經給出了優化的部分)的查詢方法就完成了,我們可以寫測試類進行測試。我將工具類貼上在下面,方便我們接下來的測試。
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) { // 驗證引數問題,如果是null,就返回空字串。 if (text == null || text.isEmpty()) { return ""; } // 下面繼續驗證是否包含開始標籤,如果不包含,預設不是佔位符,直接原樣返回即可,否則繼續執行。 int start = text.indexOf(openToken, 0); if (start == -1) { return text; } // 把text轉成字元陣列src,並且定義預設偏移量offset=0、儲存最終需要返回字串的變數builder, // text變數中佔位符對應的變數名expression。判斷start是否大於-1(即text中是否存在openToken),如果存在就執行下面程式碼 char[] src = text.toCharArray(); int offset = 0; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null; while (start > -1) { // 判斷如果開始標記前如果有轉義字元,就不作為openToken進行處理,否則繼續處理 if (start > 0 && src[start - 1] == '\\') { builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); } else { //重置expression變數,避免空指標或者老資料干擾。 if (expression == null) { expression = new StringBuilder(); } else { expression.setLength(0); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1) {////存在結束標記時 if (end > offset && src[end - 1] == '\\') {//如果結束標記前面有轉義字元時 // this close token is escaped. remove the backslash and continue. expression.append(src, offset, end - offset - 1).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else {//不存在轉義字元,即需要作為引數進行處理 expression.append(src, offset, end - offset); offset = end + closeToken.length(); break; } } if (end == -1) { // close token was not found. builder.append(src, start, src.length - start); offset = src.length; } else { //首先根據引數的key(即expression)進行引數處理,返回?作為佔位符 builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } } public class ParameterMapping { private String content; public ParameterMapping(String content) { this.content = content; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); // context是引數名稱 #{id} #{username} 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; } } public interface TokenHandler { String handleToken(String content); }
整個工程結構如下
再次建立一個maven工程, 把我們建立的框架的工程install,然後引入到我們的測試工程中<dependency> <groupId>com.xx</groupId> <artifactId>框架工程名</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
然後我們建立一個包com.xx.pojo來儲存我們的User類
public class User { private Integer id; @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + '}'; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } private String username; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } }
我們再建立一個 com.xx.dao包,然後在該包下建立UserDao介面以及他的實現類UserDaoImpl
public interface IUserDao { // 查詢所有使用者 public List<User> findAll() throws Exception; // 根素條件進行查詢 public User findByCondition(User user) throws Exception; public int save(User user) throws PropertyVetoException, DocumentException, Exception; public int delete(User user); public int update(User user); }
public class UserDaoImpl implements IUserDao{ @Override public List<User> findAll() throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); List<User> users = sqlSession.selectList("user.selectList"); for (User user : users) { System.out.println(user); } return users; } @Override public User findByCondition(User user) throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); User user2 = sqlSession.selectOne("user.selectOne", user); System.out.println(user2); return user2; } @Override public int save(User user) throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); int rows = sqlSession.insert("user.insert", user); return rows; } @Override public int delete(User user) { return 0; } @Override public int update(User user) { return 0; } }
我們再建立一個com.xx.test包,在該包下建立Test類
public class Test { @Test public void test() throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); User user = new User(); user.setId(1); user.setUsername("lucy"); User user2 = sqlSession.selectOne("com.hg.dao.IUserDao.findByCondition", user); System.out.println(user2); // List<User> users = sqlSession.selectList("user.selectList"); // for (User user : users) { // System.out.println(user); // } // IUserDao userDao = sqlSession.getMapper(IUserDao.class); // List<User> all = userDao.findAll(); // User user1 = userDao.findByCondition(user); // System.out.println(user1); // System.out.println(all); } @Test public void save() throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); User user = new User(); user.setId(5); user.setUsername("張三"); IUserDao userDao = sqlSession.getMapper(IUserDao.class); int rows = userDao.save(user); System.out.println(rows); } @Test public void delete() throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); IUserDao userDao = sqlSession.getMapper(IUserDao.class); User user = new User(); user.setId(3); int rows = userDao.delete(user); System.out.println(rows); } @Test public void update() throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); IUserDao userDao = sqlSession.getMapper(IUserDao.class); User user = new User(); user.setId(4); user.setUsername("李四"); int rows = userDao.update(user); System.out.println(rows); } }
我們還要將上面起初建立的兩個客戶端的配置檔案配置到該工程的resource下
整個工程目錄如下:
為了測試方便,我們的資料庫中的user表只建立id和username兩個欄位,
準備完畢,我們開始測試程式碼,因為該篇文章只是介紹了通過statementid的定位方式進行查詢。所以我們採用test方法進行測試,後面我還會介紹這個手寫框架的不足,以及給出解決方案(其實程式碼已經是優化過的程式碼了),有興趣的小夥伴可以研究一下,其實這個就是mybatis的基本雛形了。