更新時(shí)間:2022-11-11 來源:黑馬程序員 瀏覽量:
> 回顧mybatis的操作的核心步驟
>
> 編寫核心類SqlSessionFacotryBuild進(jìn)行解析配置文件
> 深度分析解析SqlSessionFacotryBuild干的核心工作
>
> 編寫核心類SqlSessionFacotry
> 深度分析解析SqlSessionFacotry干的核心工作
> 編寫核心類SqlSession
> 深度分析解析SqlSession干的核心工作
> 總結(jié)自定義mybatis用的技術(shù)點(diǎn)
一. 回顧mybatis的操作的核心步驟
聲明一點(diǎn)我們本篇主要探討的是mybatis的注解方式的操作, 完全從頭開始都是小編從頭開搞的, 如果與其他大神的代碼思維有出入請(qǐng)多指教。
我們首先需要準(zhǔn)備mybatis的核心配置文件(當(dāng)然導(dǎo)入相關(guān)的坐標(biāo)這里不在啰嗦)
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <!--數(shù)據(jù)庫(kù)連接信息--> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///db6?useSSL=false"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <!-- 配置sql語句編寫的位置 --> <package name="cn.itcast.mapper"/> </mappers> </configuration>
準(zhǔn)備好結(jié)果的實(shí)體類以及在mapper接口上編寫需要執(zhí)行的sql語句
public class User { private Integer uid; private String username; private String password; private String nickname; }
package cn.itcast.mapper; import cn.itcast.pojo.User; import org.apache.ibatis.annotations.Select; import java.util.List; public interface UserMapper { @Select("select * from users") List<User> findAll(); }
使用mybatis的api來幫助我們完成sql語句的執(zhí)行以及結(jié)果集的封裝
//1.關(guān)聯(lián)主配置文件 InputStream in = Resources.getResourceAsStream("mybatis-config.xml"); //2.解析配置文件 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = builder.build(in); //3.創(chuàng)建會(huì)話對(duì)象 SqlSession sqlSession = sqlSessionFactory.openSession(); //4.可以采用接口代理的方式 UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> all = mapper.findAll(); System.out.println(all); //5.釋放資源 sqlSession.close();
思考: mybatis大致是如何幫我們完成相關(guān)操作的 ?
我們通過Resources的getResourceAsStream告訴了mybatis我們編寫的核心配置文件的位置, mybatis就可以找到我們數(shù)據(jù)庫(kù)的連接信息, 也同時(shí)找到我們編寫的sql語句的地方, 然后可以將其解析按照某種規(guī)則存放起來, 我們通過調(diào)用接口代理的方式執(zhí)行方法時(shí), 可以找到對(duì)應(yīng)方法上的sql語句然后執(zhí)行將結(jié)果封裝返回給我們
二. 編寫核心類SqlSessionFacotryBuild進(jìn)行解析配置文件
那么我們廢話不多說開始我們自定義mybatis的旅程,
1.首先我們需要用戶編寫配置文件, 然后通過我們自己的Resources來告訴我們配置文件所在位置。
package com.itheima.ibatis.configuration; import java.io.InputStream; public class Resources { public static InputStream getResourceAsStream(String path) { return ClassLoader.getSystemClassLoader().getResourceAsStream(path); } }
2. 然后需要定義SqlSessionFacotryBuild來對(duì)配置文件進(jìn)行解析分發(fā)
package com.itheima.ibatis.configuration; import com.itheima.ibatis.core.session.SqlSessionFactory; import com.itheima.ibatis.core.session.impl.DefaultSqlSessionFactory; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.Node; import org.dom4j.io.SAXReader; import javax.sql.DataSource; import java.io.File; import java.io.InputStream; import java.lang.reflect.Method; import java.util.List; import java.util.Properties; public class SqlSessionFactoryBuilder { private Configuration configuration = new Configuration(); public SqlSessionFactory build(InputStream in) { SAXReader saxReader = new SAXReader(); Document document = null; try { document = saxReader.read(in); } catch (DocumentException e) { e.printStackTrace(); } Element rootElement = document.getRootElement(); parseEnvironment(rootElement.element("environments")); parseMapper(rootElement.element("mappers")); return new DefaultSqlSessionFactory(configuration); } private void parseMapper(Element mapper) { String pack = mapper.element("package").attributeValue("name"); String directory = pack.replace(".", "/"); String path = ClassLoader.getSystemClassLoader().getResource("").getPath(); File mapperDir = new File(path, directory); if (!mapperDir.exists()) { throw new RuntimeException("找不到mapper映射"); } findMapper(mapperDir, pack); // System.out.println(configuration.getSql()); } private void findMapper(File mapperDir, String base) { File[] files = mapperDir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile()) { if (file.getName().endsWith(".class")) { String name = file.getName(); name = name.substring(0, name.lastIndexOf(".")); String className = base + "." + name; initMapper(className); } } else { findMapper(file, base + "." + file.getName()); } } } } private void initMapper(String className) { try { Class<?> clazz = Class.forName(className); Method[] methods = clazz.getMethods(); for (Method method : methods) { if(method.getAnnotations().length>0){ Mapper mapper = ParseMapper.parse(method); this.configuration.getMappers().put(className + "." + method.getName(), mapper); } } } catch (Exception e) { e.printStackTrace(); } } private void parseEnvironment(Element environments) { String defEnv = environments.attributeValue("default"); Node node = environments.selectSingleNode("//environment[@id='" + defEnv + "']"); List<Element> list = node.selectNodes("//property"); Properties properties = new Properties(); for (Element element : list) { String name = element.attributeValue("name"); String value = element.attributeValue("value"); properties.put(name, value); } DataSource dataSource = new DefaultDataSource().getDataSource(properties); configuration.setDataSource(dataSource); } }
三. 深度分析解析SqlSessionFacotryBuild干的核心工作
1.build(InputStream in) 方法做的工作
?、俳柚鶧om4j的來解析了xml文件, 將environments解析工作分發(fā)給了parseEnvironment(Element environments)
?、趯appers的解析工作分發(fā)給了parseMapper(Element mapper)
2. parseEnvironment(Element environments)方法做的工作
?、僦饕馕隽诉B接數(shù)據(jù)庫(kù)的參數(shù)們, 并且創(chuàng)建了數(shù)據(jù)庫(kù)連接池
自定義連接池非本章節(jié)的重點(diǎn),所以這里內(nèi)部本質(zhì)采用的Druid連接池來做了簡(jiǎn)化
package com.itheima.ibatis.configuration; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.util.Properties; public class DefaultDataSource { public DataSource getDataSource(Properties properties) { try { return DruidDataSourceFactory.createDataSource(properties); } catch (Exception e) { e.printStackTrace(); } return null; } }
②將解析好的連接池放入configuration對(duì)象中,mappers成員變量先別糾結(jié)下一章節(jié)會(huì)講解
package com.itheima.ibatis.configuration; import lombok.Data; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Data public class Configuration { private Map<String, Mapper> mappers = new HashMap<>(); private DataSource dataSource; }
詳細(xì)圖解如下圖
3.parseMapper(Element mapper) 方法做的工作
①解析出用戶配置的package找到sql語句所在接口的文件夾, 交給initMapper來處理
?、谶f歸找到這個(gè)包下所有的.class文件,并且獲取到接口的全類名, 然后交給initMapper來處理
?、踚nitMapper通過反射獲取類中的每一個(gè)方法,將方法交給一個(gè)專門解析方法上的注解的工具類ParseMapper的parse方法處理,處理完后將其放到configuration中的mappers的集合中
?、躊arseMapper的parse方法做的工作, 這是解析配置的核心地方
package com.itheima.ibatis.configuration; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ParseMapper { public static Mapper parse(Method method) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Annotation[] annotations = method.getAnnotations(); Object value = annotations[0].getClass().getMethod("value").invoke(annotations[0]); Mapper mapper = new Mapper(); Class<?> resultType = method.getReturnType(); String val = (String) value; Pattern pattern = Pattern.compile("\\#\\{\\s*\\w+\\s*\\}"); Matcher matcher = pattern.matcher(val); List<String> paramNames = new ArrayList<>(); while (matcher.find()) { String group = matcher.group(); String fieldName = group.substring(2, group.length() - 1).trim(); paramNames.add(fieldName); } String sql = val.replaceAll("\\#\\{\\s*\\w+\\s*\\}", "?"); mapper.setSql(sql); mapper.setParameterNames(paramNames); mapper.setSql(sql); if (resultType == List.class) { mapper.setSelectList(true); Type genericReturnType = method.getGenericReturnType(); ParameterizedType parameterizedType = (ParameterizedType) genericReturnType; Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0]; mapper.setResultType(actualTypeArgument.getTypeName()); mapper.setType("SELECT"); } else if (resultType == Integer.class || resultType == int.class) { mapper.setType("UPDATE"); } else { mapper.setType("SELECT"); mapper.setResultType(resultType.getName()); } return mapper; } }
首先拿到方法上的注解,得到用戶填入的sql語句
然后處理sql語句#{參數(shù)}的這些數(shù)據(jù), 然后將參數(shù)的順序保存起來, 用來后期設(shè)置參數(shù)的數(shù)據(jù)做準(zhǔn)備, 一個(gè)方法對(duì)應(yīng)一個(gè)Mapper對(duì)象
然后再根據(jù)結(jié)果類型, 判斷是什么類型相關(guān)的操作,方便后期執(zhí)行對(duì)應(yīng)的sql語句
四. 編寫核心類SqlSessionFacotry
1.回顧那個(gè)地方創(chuàng)建的SqlSessionFacotry對(duì)象
經(jīng)過SqlSessionFacotryBuilder的努力, 我們成功的將配置文件中核心的信息解析出來并放入了configuration對(duì)象中了, 然后我們此時(shí)將解析好的configuration傳入到SqlSessionFacotry中
SqlSessionFactory的實(shí)現(xiàn)類如下:
public class DefaultSqlSessionFactory implements SqlSessionFactory { private final Configuration configuration; private TransactionManagement defaultTransactionManagement; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration =configuration; defaultTransactionManagement = new DefaultTransactionManagement(configuration.getDataSource()); } @Override public SqlSession openSession() { return new DefaultSqlSession(configuration,defaultTransactionManagement,false); } }
2.添加事務(wù)管理器
事務(wù)管理是一個(gè)小的功能, 里面希望使用ThreadLocal集合來保證一個(gè)用戶拿到的鏈接是同一個(gè)
事務(wù)管理的代碼如下:
public class DefaultTransactionManagement implements TransactionManagement { private ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); private DataSource dataSource; public DefaultTransactionManagement(DataSource dataSource) { this.dataSource = dataSource; } public Connection getConnection() { Connection connection = threadLocal.get(); if (connection == null) { try { connection = dataSource.getConnection(); } catch (SQLException e) { e.printStackTrace(); } threadLocal.set(connection); } return connection; } @Override public void commit() { Connection connection = threadLocal.get(); if (connection != null ) { try { connection.commit(); } catch (Exception e) { e.printStackTrace(); } } } @Override public void rollback() { Connection connection = threadLocal.get(); if (connection != null) { try { connection.rollback(); } catch (SQLException e) { e.printStackTrace(); } } } public void close() { Connection connection = threadLocal.get(); if (connection != null) { try { connection.close(); threadLocal.remove(); } catch (SQLException e) { e.printStackTrace(); } } } @Override public void begin() { Connection connection = threadLocal.get(); if (connection != null) { try { connection.setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); } } } }
五. 深度分析解析SqlSessionFacotry干的核心工作
1.SqlSession openSession() 方法做的工作
可以看的出來我們?cè)谶@個(gè)方法創(chuàng)建了DefaultSqlSession對(duì)象,并傳入封裝好的configuration,默認(rèn)的事務(wù)管理器
默認(rèn)通過openSession事務(wù)是開啟的等等相關(guān)的參數(shù)
六.編寫核心類SqlSession
其實(shí)有SqlSession的接口,我們使用的實(shí)現(xiàn)類是DefaultSession, 這里記錄了解析的配置對(duì)象configuration
默認(rèn)事務(wù)管理器對(duì)象transactionManagement, 默認(rèn)事務(wù)開啟的狀態(tài)tx標(biāo)記
package com.itheima.ibatis.core.session.impl; import com.itheima.ibatis.configuration.Configuration; import com.itheima.ibatis.configuration.Mapper; import com.itheima.ibatis.core.BaseExecutor; import com.itheima.ibatis.core.annotation.Param; import com.itheima.ibatis.core.session.SqlSession; import com.itheima.ibatis.core.transaction.TransactionManagement; import java.lang.reflect.*; import java.util.HashMap; import java.util.List; import java.util.Map; public class DefaultSqlSession implements SqlSession { private final Configuration configuration; private final boolean tx; private TransactionManagement transactionManagement; public DefaultSqlSession(Configuration configuration, TransactionManagement transactionManagement, boolean tx) { this.configuration = configuration; this.transactionManagement = transactionManagement; this.tx = tx; } public void close() { transactionManagement.close(); } @Override public void commit() { transactionManagement.commit(); } @Override public void rollback() { transactionManagement.rollback(); } @Override public <T> List<T> selectList(String sqlId) { return selectList(sqlId, null); } @Override public <T> List<T> selectList(String sqlId, Object param) { List<Object> list = new BaseExecutor(transactionManagement, tx).queryList(getMapper(sqlId), param); return (List<T>) list; } @Override public <T> T selectOne(String sqlId) { return selectOne(sqlId, null); } @Override public <T> T selectOne(String sqlId, Object param) { return new BaseExecutor(transactionManagement, tx).query(getMapper(sqlId), param); } @Override public int delete(String sqlId) { return update0(sqlId, null); } @Override public int delete(String sqlId, Object param) { return update0(sqlId, param); } @Override public int update(String sqlId) { return update0(sqlId, null); } @Override public int update(String sqlId, Object param) { return update0(sqlId, param); } @Override public int insert(String sqlId) { return update0(sqlId, null); } @Override public int insert(String sqlId, Object param) { return update0(sqlId, param); } @Override public <T> T getMapper(Class<T> clazz) { Object o = Proxy.newProxyInstance( clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String sqlId = clazz.getName() + "." + method.getName(); Mapper mapper = configuration.getMappers().get(sqlId); String type = mapper.getType(); Object findParam = null; if (args != null) { if (args.length == 1) { Object param = args[0]; boolean isArray = param.getClass().isArray(); if (!isArray) { findParam = param; } } else { Map<String, Object> map = new HashMap<>(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { Param param = parameters[i].getAnnotation(Param.class); String key = "arg"+i; if(param !=null){ key = param.value(); } map.put(key, args[i]); } findParam = map; } } if (type.equals("SELECT")) { boolean selectList = mapper.isSelectList(); if (selectList) return selectList(sqlId, findParam); else return selectOne(sqlId, findParam); } else { return update0(sqlId, findParam); } } }); return (T) o; } private int update0(String sqlId, Object param) { return new BaseExecutor(transactionManagement, tx).update(getMapper(sqlId), param); } public Mapper getMapper(String sqlId) { Mapper mapper = configuration.getMappers().get(sqlId); if (mapper == null) { throw new RuntimeException("沒有找到sql映射,請(qǐng)檢查"); } return mapper; } }
七.深度分析解析SqlSession干的核心工作
1.selectOne & selectList做的工作
主要是分發(fā)了下功能, 執(zhí)行sql語句避免不了有參數(shù)和無參數(shù)的, 都讓調(diào)用有參數(shù)的方便管理
在執(zhí)行前, 考慮還有一種情況, 用戶不是通過接口代理的方式來執(zhí)行以上方法, 這樣手動(dòng)輸入sqlId容易造成錯(cuò)誤
這里做一個(gè)健壯性判斷
BaseExecutor中的query以及queryList做的核心工作
首先這兩個(gè)方法的特點(diǎn)都是查詢, 其步驟基本類似, 所以這里可以合并一起轉(zhuǎn)調(diào)query0功能
這里需要對(duì)參數(shù)進(jìn)行設(shè)定, 還根據(jù)最后isOne的參數(shù)決定返回值是否是單個(gè)
參數(shù)設(shè)置這里比較復(fù)雜我們通過圖解的方式來解釋, (注: 參數(shù)是List集合類型的和數(shù)組類型的沒有做!!!)
對(duì)結(jié)果的封裝主要用到內(nèi)省技術(shù)和數(shù)據(jù)庫(kù)元數(shù)據(jù)等等知識(shí)點(diǎn)
2.update&delete&insert做的工作
BaseExecutor中的update做的核心工作
還是和query&queryList一樣需要設(shè)置參數(shù), 不管是增刪改其本質(zhì)其結(jié)果都是一致
3.getMapper代理模式開發(fā)的原理
主要使用的動(dòng)態(tài)代理的技術(shù)創(chuàng)建接口的實(shí)現(xiàn)類, 內(nèi)部主要整合了sqlId和參數(shù), 省去用戶自己拼sqlId拼錯(cuò)的風(fēng)險(xiǎn)
也同時(shí)解決用戶手動(dòng)合參數(shù)的麻煩, 但是最終工作的還是selectOne,selectList以及update0這些方法
總結(jié)自定義mybatis用的技術(shù)點(diǎn)
一款框架的誕生肯定不是一蹴而就的, 隨著時(shí)間慢慢推進(jìn)逐步更新出來, 所以一款好的框架肯定要經(jīng)過很多考驗(yàn)才能夠穩(wěn)定靠譜, 但是縱觀整篇用的技術(shù)點(diǎn), 不難發(fā)現(xiàn)框架也是由基礎(chǔ)代碼編寫而來,解決大量重復(fù)的工作, 提供擴(kuò)展性等等機(jī)制,比如本篇用核心的技術(shù)點(diǎn)有。
① 反射
?、?內(nèi)省
③ 解析xml
?、?動(dòng)態(tài)代理
⑤ 工廠設(shè)計(jì)模式
等等, 感謝大家耐心閱覽, 附件有本篇的原碼, 如果有更好的建議和想法歡迎和小編一起探討交流。