作者 | 小傅哥 博客 | https://
微信公眾號:bugstack蟲洞棧 | 博客:https:// 沉淀、分享、成長,專注于原創(chuàng)專題案例,以最易學(xué)習(xí)編程的方式分享知識,讓自己和他人都能有所收獲。目前已完成的專題有;Netty4.x實(shí)戰(zhàn)專題案例、用Java實(shí)現(xiàn)JVM、基于JavaAgent的全鏈路監(jiān)控、手寫RPC框架、架構(gòu)設(shè)計(jì)專題案例、源碼分析等。 你用劍🗡、我用刀🔪,好的代碼都很燒😏,望你不吝出招💨!
一、前言介紹
在前面一篇分析了 mybatis 源碼,從它為什么之后接口但是沒有實(shí)現(xiàn)類就能執(zhí)行數(shù)據(jù)庫操作為入口,整個源碼核心流程完全解釋了一遍。對于一個3年以上的程序員來說,新知識的學(xué)習(xí)過程應(yīng)該是從最開始 helloworld 到熟練使用 api 完成業(yè)務(wù)功能。下一步為了深入了解就需要閱讀部分核心源碼,從而在出問題后可以快速定位,迅速排查。從而減少線上事故的持續(xù)時長,提升個人影響力。但!這不是學(xué)習(xí)終點(diǎn),因?yàn)闊o論是任何一個框架的源碼,如果只是看那么就很難學(xué)習(xí)到它的實(shí)用技術(shù)。紙上得來終覺淺,唯有實(shí)戰(zhàn)和操練。
那么,本章節(jié)我們?nèi)ズ唵螌?shí)現(xiàn)一個基于jdbc的demo版本Mybatis,從而更加清楚這樣框架的設(shè)計(jì)。與此同時這份思想會讓你可以在其他場景使用,比如給ES查詢寫一個EsBatis。實(shí)現(xiàn)了心情也好了;
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-8Ai2paSY-1578920519920)(https://fuzhengwei./assets/images/pic-content/2019/11/itstack-demo-mybatis-07.png)]
二、案例工程
擴(kuò)展上一篇源碼分析工程;itstack-demo-mybatis,增加 like 包,模仿 Mybatis 工程。完整規(guī)程下載,關(guān)注公眾號:bugstack蟲洞棧 | 回復(fù):源碼分析
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
三、環(huán)境配置
JDK1.8 IDEA 2019.3.1 dom4j 1.6.1
四、代碼講述
關(guān)于整個 Demo 版本,并不是把所有 Mybatis 全部實(shí)現(xiàn)一遍,而是撥絲抽繭將最核心的內(nèi)容展示給你,從使用上你會感受一模一樣,但是實(shí)現(xiàn)類已經(jīng)全部被替換,核心類包括;
Configuration DefaultSqlSession DefaultSqlSessionFactory Resources SqlSession SqlSessionFactory SqlSessionFactoryBuilder XNode
1. 先測試下整個DemoJdbc框架
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" , 1 L) ;
System. out. println ( JSON. toJSONString ( user) ) ;
} finally {
session. close ( ) ;
reader. close ( ) ;
}
} catch ( Exception e) {
e. printStackTrace ( ) ;
}
}
一切順利結(jié)果如下(新人往往會遇到各種問題);
{ "age" : 18 , "createTime" : 1576944000000 , "id" : 1 , "name" : "水水" , "updateTime" : 1576944000000 }
Process finished with exit code 0
可能乍一看這測試類完全和 MybatisApiTest.java 測試的代碼一模一樣呀,也看不出區(qū)別。其實(shí)他們的引入的包是不一樣;
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;
好!接下來我們開始分析這部分核心代碼。
2. 加載XML配置文件
這里我們采用 mybatis 的配置文件結(jié)構(gòu)進(jìn)行解析,在不破壞原有結(jié)構(gòu)的情況下,最大可能的貼近源碼。mybatis 單獨(dú)使用的使用的時候使用了兩個配置文件;數(shù)據(jù)源配置、Mapper 映射配置,如下;
mybatis-config-datasource.xml & 數(shù)據(jù)源配置
? xml version= "1.0" encoding= "UTF-8" ? >
! DOCTYPE configuration PUBLIC "-////DTD Config 3.0//EN"
"http:///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 "-////DTD Mapper 3.0//EN" "http:///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 方式。在案例中會看到最開始獲取資源,如下;
ApiLikeTest.test_queryUserInfoById() & 部分截取
String resource = "spring/mybatis-config-datasource.xml" ;
Reader reader;
try {
reader = Resources. getResourceAsReader ( resource) ;
. . .
從上可以看到這是通過配置文件地址獲取到了讀取流的過程,從而為后面解析做基礎(chǔ)。首先我們先看 Resources 類,整個是我們的資源類。
Resources.java & 資源類
/**
* 公眾號 | bugstack蟲洞棧
* 博 客 | https://
* 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,直到往下以此做了;
獲取 ClassLoader 集合,最大限度搜索配置文件 通過 classLoader.getResourceAsStream 讀取配置資源,找到后立即返回,否則拋出異常
3. 解析XML配置文件
配置文件加載后開始進(jìn)行解析操作,這里我們也仿照 mybatis 但進(jìn)行簡化,如下;
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder ( ) . build ( reader) ;
SqlSessionFactoryBuilder.build() & 入口構(gòu)建類
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;
}
通過讀取流創(chuàng)建 xml 解析的 Document 類 parseConfiguration 進(jìn)行解析 xml 文件,并將結(jié)果設(shè)置到配置類中,包括;連接池、數(shù)據(jù)源、mapper關(guān)系
SqlSessionFactoryBuilder.parseConfiguration() & 解析過程
private Configuration parseConfiguration ( Element root) {
Configuration configuration = new Configuration ( ) ;
configuration. setDataSource ( dataSource ( root. selectNodes ( "http://dataSource" ) ) ) ;
configuration. setConnection ( connection ( configuration. dataSource) ) ;
configuration. setMapperElement ( mapperElement ( root. selectNodes ( "mappers" ) ) ) ;
return configuration;
}
在前面的 xml 內(nèi)容中可以看到,我們需要解析出數(shù)據(jù)庫連接池信息 datasource,還有數(shù)據(jù)庫語句映射關(guān)系 mappers
SqlSessionFactoryBuilder.dataSource() & 解析出數(shù)據(jù)源
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;
}
這個過程比較簡單,只需要將數(shù)據(jù)源信息獲取即可
SqlSessionFactoryBuilder.connection() & 獲取數(shù)據(jù)庫連接
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;
}
這個就是jdbc最原始的代碼,獲取了數(shù)據(jù)庫連接池
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;
}
這個過程首先包括是解析所有的sql語句,目前為了測試只解析 select 相關(guān) 所有的 sql 語句為了確認(rèn)唯一,都是使用;namespace + select中的id進(jìn)行拼接,作為 key,之后與sql一起存放到 map 中。 在 mybaits 的 sql 語句配置中,都有占位符,用于傳參。where id = #{id} 所以我們需要將占位符設(shè)置為問號,另外需要將占位符的順序信息與名稱存放到 map 結(jié)構(gòu),方便后續(xù)設(shè)置查詢時候的入?yún)ⅰ?/li>
4. 創(chuàng)建DefaultSqlSessionFactory
最后將初始化后的配置類 Configuration,作為參數(shù)進(jìn)行創(chuàng)建 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的實(shí)現(xiàn)類
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) ;
}
}
這個過程比較簡單,構(gòu)造函數(shù)只提供了配置類入?yún)?/li> 實(shí)現(xiàn) SqlSessionFactory 的 openSession(),用于創(chuàng)建 DefaultSqlSession,也就可以執(zhí)行 sql 操作
5. 開啟SqlSession
SqlSession session = sqlMapper. openSession ( ) ;
上面這一步就是創(chuàng)建了DefaultSqlSession,比較簡單。如下;
@Override
public SqlSession openSession ( ) {
return new DefaultSqlSession ( configuration. connection, configuration. mapperElement) ;
}
6. 執(zhí)行SQL語句
User user = session. selectOne ( "org.itstack.demo.dao.IUserDao.queryUserInfoById" , 1 L) ;
在 DefaultSqlSession 中通過實(shí)現(xiàn) SqlSession,提供數(shù)據(jù)庫語句查詢和關(guān)閉連接池,如下;
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 ( ) ;
}
接下來看具體的執(zhí)行過程,session.selectOne
DefaultSqlSession.selectOne() & 執(zhí)行查詢
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;
}
selectOne 就objects.get(0);,selectList 就全部返回
通過 statement 獲取最初解析 xml 時候的存儲的 select 標(biāo)簽信息;
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>
獲取 sql 語句后交給 jdbc 的 PreparedStatement 類進(jìn)行執(zhí)行
這里還需要設(shè)置入?yún)?,我們將入?yún)⒃O(shè)置進(jìn)行抽取,如下;
private void buildParameter ( PreparedStatement preparedStatement, Object parameter, Map Integer, String> parameterMap) throws SQLException, IllegalAccessException {
int size = parameterMap. size ( ) ;
// 單個參數(shù)
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 > ( ) ;
// 對象參數(shù)
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) ;
}
}
}
單個參數(shù)比較簡單直接設(shè)置值即可,Long、Integer、String … 如果是一個類對象,需要通過獲取 Field 屬性,與參數(shù) Map 進(jìn)行匹配設(shè)置 設(shè)置參數(shù)后執(zhí)行查詢 preparedStatement.executeQuery()
接下來需要將查詢結(jié)果轉(zhuǎn)換為我們的類(主要是反射類的操作),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 標(biāo)簽上 時間類型需要判斷后處理,Timestamp,與 java 不是一個類型
7. Sql查詢補(bǔ)充說明
sql 查詢有入?yún)?、有不需要入?yún)?、有查詢一個、有查詢集合,只需要合理包裝即可,例如下面的查詢集合,入?yún)⑹菍ο箢愋停?/p>
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 ( ) ;
}
}
**測試結(jié)果:
[ { "age" : 18 , "createTime" : 1576944000000 , "id" : 1 , "name" : "水水" , "updateTime" : 1576944000000 } , { "age" : 18 , "createTime" : 1576944000000 , "id" : 2 , "name" : "豆豆" , "updateTime" : 1576944000000 } ]
Process finished with exit code 0
五、綜上總結(jié)
學(xué)習(xí)完 Mybaits 核心源碼,再實(shí)現(xiàn)一下核心過程,那么就會很清晰這個過程是怎么個流程,也就不會覺得自己知識棧有漏洞 只有深入的學(xué)習(xí)才能將這樣的技術(shù)賦能于其他開發(fā)上,例如給ES增加這樣查詢包,讓ES更加容易操作。其實(shí)還可以有很多創(chuàng)造 知識往往是綜合的使用,將各個知識點(diǎn)綜合起來使用,才能更加熟練。不要總看不做,否則全套的流程不能在自己腦子流程下什么印象
六、文末驚喜
小傅哥 | 沉淀、分享、成長,讓自己和他人都能有所收獲!