閉關修煉180天--手寫持久層框架(mybatis簡易版)
拋磚引玉
首先先看一段傳統的JDBC編碼的程式碼實現:
//傳統的JDBC實現
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//載入資料庫驅動
Class.forName("com.mysql.jdbc.Driver");
//通過驅動管理類獲取資料庫管理
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root","root");
//定義sql語句
String sql = "select * from user where username = ?";
//獲取預處理物件statement
preparedStatement = connection.prepareStatement(sql);
//設定引數,第一個引數為sql語句中的引數的序號(從1開始),第二個引數為設定的引數值
preparedStatement.setString(1,"tom");
//像資料庫發出sql執行查詢,查詢出結果集
resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
//將查詢出的結果集封裝進實體中
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//釋放資源
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
通過以上傳統的JDBC運算元據庫的程式碼可以發現,我們能總結出來以下幾條問題:
-
每執行一次sql都要建立一次資料庫連線,資料庫連線建立、釋放頻繁造成系統資源浪費,從而影響系統效能。
-
Sql編寫在了程式碼中存在硬編碼問題,實際上工作中sql變化是比較大的,每次都要修改程式碼,sql語句不易維護。
-
使用preparedStatement向佔有位符號傳引數存在硬編碼,因為sql語句的where條件不一定,可能 多也可能少,修改sql還要修改程式碼,系統不易維護。
-
對結果集解析存在硬編碼(查詢列名),sql變化導致解析程式碼變化,系統不易維護,如果能將資料庫 記錄自動封裝成pojo物件解析比較方便。
針對以上的幾條JDBC問題,我們可以大致的延伸出以下幾點的解決思路:
-
使用資料庫連線池初始化連線資源。
-
將sql語句寫在xml檔案中,在程式碼中剝離出來單獨維護。
-
使用反射內省等技術,完成資料庫的表欄位和實體的屬性的自動對映。
深入剖析
本次完成持久層框架的自定義編寫便是從以上幾個方面入手,來解決傳統的JDBC存在的問題,在編寫之前,我們首先要明白,框架屬於開發的一個半成品,是我們在開發過程中可以直接拿來用的東西,我們在自定義編寫時,什麼程式碼是框架中所有的,什麼程式碼屬於使用端(一般開發人員)提供的,這點要想明白。經分析,大體劃分如下:
使用端
- 提供核心配置檔案:
- sqlMapConfig.xml檔案,配置資料來源等資訊,同時引入Mapper.xml。
- Mapper.xml檔案,配置sql語句等資訊。
框架端
- 讀取配置檔案,將配置檔案載入成位元組輸入流存放在記憶體中,準備好兩個javaBean用來儲存以後解析配置檔案出來的資料。
- Configuration:存放資料來源資訊dataBase、Map<String,MappedStatement>、key為唯一標識:namespace+id,value是sql相關資訊實體。
- MappedStatement:存放sql相關資訊,包含id,sql語句,入參型別,返回值型別等。
-
解析配置檔案,建立SqlSessionFactoryBuild類,提供build(InputStream in)方法,用於構建SqlSessionFactory。
- 使用dom4j技術解析xml配置檔案,將解析出來的資料存放到javaBean中。
- 建立SqlSessionFactory的實現類DefaultSqlSessionFactory
-
生產會話物件。在SqlSessionFactory中提供openSession()方法,用於生產SqlSession。
-
建立SqlSession介面及其實現類,用於封裝crud方法。
- selectList(String statementId,Object... param) 查詢全部
- selectOne(String statementId,Object... param) 查詢單個
-
執行實際的JDBC操作。建立Executor介面及其實現類SimpleExecutor,提供query(Configuration configuration, MappedStatement mappedStatement, Object... params)方法,完成實際的與資料庫互動的工作。
- 從連線池中獲取連線
- 處理sql語句
- 設定引數
- 封裝結果集
涉及到的設計模式
- 構建者模式
- 工廠模式
- 代理模式
程式碼實現
這裡只貼出核心程式碼,全部程式碼請看我的碼雲:https://gitee.com/zang-chuanlei/FrameMyBatis.git
在這裡需要建立兩個專案:FrameMyBatistest和FrameMyBatis,其中FrameMyBatis_test代表的是使用端,FrameMyBatis代表的是框架端,專案結構如下:
1.在FrameMyBatis_test下面的resources目錄下新增兩個配置檔案sqlMapConfig.xml和UserMapper.xml
<configuration> <dataSource> <property name="dataDriver" value="com.mysql.jdbc.Driver"></property> <property name="dataUrl" value="jdbc:mysql:///mybatis"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </dataSource> <!--載入UserMapper.xml配置檔案--> <mapper resource="UserMapper.xml"></mapper> </configuration>
<mapper namespace="com.zae.dao.UserDao"> <select id="findAll" resultType="com.zae.entity.User"> select * from users </select> <select id="findOne" resultType="com.zae.entity.User" paramterType="com.zae.entity.User"> select * from users where id=#{id} and username=#{username} </select> </mapper>
2.給FrameMyBatis引入一些需要的座標
<dependencies> <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> </dependencies>
3.在FrameMyBatis下建立Resources類,用於載入位元組輸入流。
import java.io.InputStream; public class Resources { /** * 根據xml路徑將xml檔案載入成位元組流,存放在記憶體中 * @param path * @return */ public static InputStream getInputStreamByXml(String path){ InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }
4.建立兩個bean,Configuration和MappedStatement
import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * javaBean之一:用來裝sqlMapConfig.xml檔案的內容 */ public class Configuration { /** * 儲存資料來源連線資訊 */ private DataSource dataSource; /** * 儲存載入進來的mapper.xml裡面的資料 * key = statementId = namespace+"."+id * value = mapperStatement */ private Map<String,MapperStatement> mapperStatementMap = new HashMap<String, MapperStatement>(); public DataSource getDataSource() { return dataSource; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Map<String, MapperStatement> getMapperStatementMap() { return mapperStatementMap; } public void setMapperStatementMap(Map<String, MapperStatement> mapperStatementMap) { this.mapperStatementMap = mapperStatementMap; } }
/** * javaBean-2用來裝載mapper.xml的資料 * 一條sql語句資訊封裝在一個MapperStatement物件中 */ public class MapperStatement { private String id; private String resultType; private String paramterType; 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; } }
5.建立SqlSessionFactoryBuild
import com.zae.config.XmlConfigBuilder; import com.zae.pojo.Configuration; import java.io.InputStream; public class SqlSessionFactoryBuilder { /** * 構建SqlSessionFactory工廠 * @param inputStream * @return */ public SqlSessionFactory build(InputStream inputStream) throws Exception{ //完成xml檔案的解析 XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(); Configuration configuration = xmlConfigBuilder.parasConfig(inputStream); DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration); return defaultSqlSessionFactory; } }
6.建立解析XML檔案的兩個專屬類XmlConfigBuilder和XmlMapperBuilder
import com.mchange.v2.c3p0.ComboPooledDataSource; import com.zae.io.Resources; import com.zae.pojo.Configuration; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; import java.util.Properties; /** * 解析sqlMapConfig.xml檔案存放在javaBean中 */ public class XmlConfigBuilder { private Configuration configuration; public XmlConfigBuilder(){ this.configuration = new Configuration(); } /** * 解析sqlMapConfig.xml檔案 * @param inputStream * @return */ public Configuration parasConfig(InputStream inputStream) throws Exception{ SAXReader saxReader = new SAXReader(); Document document = saxReader.read(inputStream); //獲取根標籤裡面的內容 Element rootElement = document.getRootElement(); //獲取所有的property標籤裡面的內容 List<Element> list = rootElement.selectNodes("//property"); //將資料庫連線資訊讀取到properties檔案中 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("dataDriver")); comboPooledDataSource.setJdbcUrl(properties.getProperty("dataUrl")); comboPooledDataSource.setUser(properties.getProperty("username")); comboPooledDataSource.setPassword(properties.getProperty("password")); //給configuration裡面的資料來源屬性賦值 configuration.setDataSource(comboPooledDataSource); //解析xl檔案的資料 List<Element> mapperList = rootElement.selectNodes("//mapper"); for (Element element : mapperList) { //獲取到mapper.xml檔案的路徑 String resource = element.attributeValue("resource"); //獲取mapper檔案的輸入流 InputStream mapperInputStream = Resources.getInputStreamByXml(resource); XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration); xmlMapperBuilder.parasMapper(mapperInputStream); } return configuration; } }
import com.zae.pojo.Configuration; import com.zae.pojo.MapperStatement; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; /** * 解析mapper.xml資料存放到javaBean中 */ public class XmlMapperBuilder { private Configuration configuration; public XmlMapperBuilder(Configuration configuration){ this.configuration = configuration; } public void parasMapper(InputStream mapperInputStream) throws Exception { SAXReader saxReader = new SAXReader(); Document document = saxReader.read(mapperInputStream); //獲取根標籤的資料-mapper Element rootElement = document.getRootElement(); //獲取名稱空間 String namespace = rootElement.attributeValue("namespace"); //獲取所有的select標籤的內容 List<Element> elementList = rootElement.selectNodes("//select"); for (Element element : elementList) { String id = element.attributeValue("id"); String resultType = element.attributeValue("resultType"); String paramterType = element.attributeValue("paramterType"); String sql = element.getTextTrim(); MapperStatement mapperStatement = new MapperStatement(); mapperStatement.setId(id); mapperStatement.setResultType(resultType); mapperStatement.setParamterType(paramterType); mapperStatement.setSql(sql); String key = namespace+"."+id; configuration.getMapperStatementMap().put(key,mapperStatement); } } }
7.建立sqlSessionFactory介面及DefaultSqlSessionFactory 實現類
public interface SqlSessionFactory { SqlSession openSqlSession(); }
import com.zae.pojo.Configuration; public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration){ this.configuration = configuration; } public SqlSession openSqlSession() { return new DefaultSqlSession(configuration); } }
8.建立sqlSession 介面及DefaultSqlSession 實現類
import java.util.List; public interface SqlSession { <E> List<E> findAll(String statementId,Object ... params) throws Exception; <T> T findOne(String statement,Object ...params)throws Exception; <T> T getMapper(Class<?> mapperClass); }
import com.zae.pojo.Configuration; import com.zae.pojo.MapperStatement; import java.lang.reflect.*; import java.util.List; import java.util.Map; public class DefaultSqlSession implements SqlSession { private Configuration configuration; public DefaultSqlSession(Configuration configuration){ this.configuration = configuration; } public <E> List<E> findAll(String statementId, Object... params) throws Exception{ Map<String, MapperStatement> statementMap = configuration.getMapperStatementMap(); MapperStatement mapperStatement = statementMap.get(statementId); Executor executor = new SimpleExecutor(); List<Object> execute = executor.execute(configuration,mapperStatement,params); return (List<E>) execute; } public <T> T findOne(String statement, Object... params) throws Exception{ List<Object> objectList = findAll(statement, params); if(objectList.size() == 1){ return (T) objectList.get(0); }else{ throw new RuntimeException("返回引數不唯一或者為空"); } } public <T> T getMapper(Class<?> aclass) { Object object = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{aclass}, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //獲取執行方法名 String methodName = method.getName(); //獲取全限類名 String className = method.getDeclaringClass().getName(); //statementId String statementId = className+"."+methodName; //引數泛型化 Type type = method.getGenericReturnType(); if(type instanceof ParameterizedType){ //存在泛型,則呼叫findAll List<Object> all = findAll(statementId, args); return all; } return findOne(statementId,args); } }); return (T) object; } }
9.建立Executor介面及SimpleExecutor實現類,建立BoundSql實體,引入工具類。
import com.zae.pojo.Configuration; import com.zae.pojo.MapperStatement; import java.util.List; public interface Executor { /** * jdbc處理方法 * @param configuration * @param mapperStatement * @param params * @param <E> * @return */ <E> List<E> execute(Configuration configuration, MapperStatement mapperStatement, Object ...params) throws Exception; }
import com.zae.pojo.BorundSql; import com.zae.pojo.Configuration; import com.zae.pojo.MapperStatement; import com.zae.utils.GenericTokenParser; import com.zae.utils.ParameterMapping; import com.zae.utils.ParameterMappingTokenHandler; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.List; public class SimpleExecutor implements Executor { public <E> List<E> execute(Configuration configuration, MapperStatement mapperStatement, Object... params) throws Exception{ //1.獲取連線物件 Connection connection = configuration.getDataSource().getConnection(); //2.處理sql語句 String sql = mapperStatement.getSql(); BorundSql borundSql = dealSql(sql); String sqlNow = borundSql.getSql(); List<ParameterMapping> mappingList = borundSql.getParameterMappingList(); //3.獲取預處理物件 PreparedStatement preparedStatement = connection.prepareStatement(sqlNow); //4.設定引數 Class<?> classByType = getClassByType(mapperStatement.getParamterType()); for(int i = 0;i<mappingList.size();i++){ String content = mappingList.get(i).getContent(); //根據屬性名獲取該屬性資訊 Field declaredField = classByType.getDeclaredField(content); //設定暴力訪問 declaredField.setAccessible(true); //獲取引數裡面的值 Object value = declaredField.get(params[0]); //設定引數 preparedStatement.setObject(i+1,value); } //5.處理返回結果集 ResultSet resultSet = preparedStatement.executeQuery(); Class<?> resultClass = getClassByType(mapperStatement.getResultType()); List resultList = new ArrayList(); while (resultSet.next()){ //生成該類的例項物件 Object object = resultClass.newInstance(); ResultSetMetaData metaData = resultSet.getMetaData(); for(int i = 1;i<=metaData.getColumnCount();i++){ //獲取列的名稱 String columnName = metaData.getColumnName(i); //根據列的名稱獲取內容 Object value = resultSet.getObject(columnName); //使用反射或內省,完成欄位和資料庫的對映 PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultClass); //獲取寫入方法 Method writeMethod = propertyDescriptor.getWriteMethod(); //呼叫invoke方法,將資料寫入實體 writeMethod.invoke(object,value); } resultList.add(object); } return resultList; } /** * 處理sql,將sql中的#{}處理成?,並把#{}裡面的欄位儲存下來 * @param sourceSql * @return */ private BorundSql dealSql(String sourceSql){ ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler); String targetSql = genericTokenParser.parse(sourceSql); List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BorundSql borundSql = new BorundSql(); borundSql.setSql(targetSql); borundSql.setParameterMappingList(parameterMappings); return borundSql; } /** * 根據類的全路徑獲取類物件 * @param path * @return */ private Class<?> getClassByType(String path) throws ClassNotFoundException { if(path!=null){ Class<?> aClass = Class.forName(path); return aClass; }else{ return null; } } }
import com.zae.utils.ParameterMapping; import java.util.List; public class BorundSql { private String sql; private List<ParameterMapping> parameterMappingList; public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } public List<ParameterMapping> getParameterMappingList() { return parameterMappingList; } public void setParameterMappingList(List<ParameterMapping> parameterMappingList) { this.parameterMappingList = parameterMappingList; } }
工具類如下:
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; } }
import java.util.ArrayList; import java.util.List; 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); }
10.在FrameMyBatis_test專案的pom檔案中引入FrameMyBatis的座標,在準備好的FrameMyBatis_test下編寫測試類FrameTest
import com.zae.dao.UserDao; import com.zae.entity.User; import com.zae.io.Resources; import com.zae.session.SqlSession; import com.zae.session.SqlSessionFactory; import com.zae.session.SqlSessionFactoryBuilder; import org.junit.Before; import org.junit.Test; import java.io.InputStream; import java.util.List; public class FrameTest { private SqlSession sqlSession; private UserDao userDao; @Before public void test1() throws Exception{ //獲取位元組輸入流 InputStream inputStream = Resources.getInputStreamByXml("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); sqlSession = sqlSessionFactory.openSqlSession(); userDao = sqlSession.getMapper(UserDao.class); } /** * 全查 * @throws Exception */ @Test public void test2()throws Exception{ List<User> all = sqlSession.findAll("com.zae.dao.UserDao.findAll"); for (User user : all) { System.out.println(user); } } /** * 單條 * @throws Exception */ @Test public void test3() throws Exception{ User user = new User(); user.setId("1"); user.setUsername("sss"); User one = sqlSession.findOne("user.findOne", user); System.out.println(one); } /** * 代理物件查詢 * @throws Exception */ @Test public void test4() throws Exception{ List<User> userList = userDao.findAll(null); for (User user : userList) { System.out.println(user); } } }
我是帝莘,期待與你的技術交流和思想碰撞。