深度自定義mybatis
回顧mybatis的操作的核心步驟
編寫核心類SqlSessionFacotryBuild進行解析配置檔案
深度分析解析SqlSessionFacotryBuild幹的核心工作
編寫核心類SqlSessionFacotry
深度分析解析SqlSessionFacotry幹的核心工作
編寫核心類SqlSession
深度分析解析SqlSession幹的核心工作
總結自定義mybatis用的技術點
一. 回顧mybatis的操作的核心步驟
聲明一點我們本篇主要探讨的是mybatis的注解方式的操作, 完全從頭開始都是小編從頭開搞的, 如果與其他大神的代碼思維有出入請多指教
我們首先需要準備mybatis的核心配置檔案(當然導入相關的坐标這裡不在啰嗦)
<?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">
<!--資料庫連接配接資訊-->
<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>
準備好結果的實體類以及在mapper接口上編寫需要執行的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語句的執行以及結果集的封裝
//1.關聯主配置檔案
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
//2.解析配置檔案
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = builder.build(in);
//3.建立會話對象
SqlSession sqlSession = sqlSessionFactory.openSession();
//4.可以采用接口代理的方式
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAll();
System.out.println(all);
//5.釋放資源
sqlSession.close();
思考: mybatis大緻是如何幫我們完成相關操作的 ?
我們通過Resources的getResourceAsStream告訴了mybatis我們編寫的核心配置檔案的位置, mybatis就可以找到我們資料庫的連接配接資訊, 也同時找到我們編寫的sql語句的地方, 然後可以将其解析按照某種規則存放起來, 我們通過調用接口代理的方式執行方法時, 可以找到對應方法上的sql語句然後執行将結果封裝傳回給我們
二. 編寫核心類SqlSessionFacotryBuild進行解析配置檔案
那麼我們廢話不多說開始我們自定義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來對配置檔案進行解析分發
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) 方法做的工作
①借助Dom4j的來解析了xml檔案, 将environments解析工作分發給了parseEnvironment(Element environments)
②将mappers的解析工作分發給了parseMapper(Element mapper)
2. parseEnvironment(Element environments)方法做的工作
①主要解析了連接配接資料庫的參數們, 并且建立了資料庫連接配接池
自定義連接配接池非本章節的重點,是以這裡内部本質采用的Druid連接配接池來做了簡化
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對象中,mappers成員變量先别糾結下一章節會講解
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;
}
詳細圖解如下圖
3.parseMapper(Element mapper) 方法做的工作
①解析出使用者配置的package找到sql語句所在接口的檔案夾, 交給initMapper來處理
②遞歸找到這個包下所有的.class檔案,并且擷取到接口的全類名, 然後交給initMapper來處理
③initMapper通過反射擷取類中的每一個方法,将方法交給一個專門解析方法上的注解的工具類ParseMapper的parse方法處理,處理完後将其放到configuration中的mappers的集合中
④ParseMapper的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語句#{參數}的這些資料, 然後将參數的順序儲存起來, 用來後期設定參數的資料做準備, 一個
方法對應一個Mapper對象
然後再根據結果類型, 判斷是什麼類型相關的操作,友善後期執行對應的sql語句
四. 編寫核心類SqlSessionFacotry
1.回顧那個地方建立的SqlSessionFacotry對象
經過SqlSessionFacotryBuilder的努力, 我們成功的将配置檔案中核心的資訊解析出來并放入了configuration對象中了, 然後我們此時将解析好的configuration傳入到SqlSessionFacotry中
SqlSessionFactory的實作類如下:
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.添加事務管理器
事務管理是一個小的功能, 裡面希望使用ThreadLocal集合來保證一個使用者拿到的連結是同一個
事務管理的代碼如下:
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() 方法做的工作
可以看的出來我們在這個方法建立了DefaultSqlSession對象,并傳入封裝好的configuration,預設的事務管理器
預設通過openSession事務是開啟的等等相關的參數
六.編寫核心類SqlSession
其實有SqlSession的接口,我們使用的實作類是DefaultSession, 這裡記錄了解析的配置對象configuration
預設事務管理器對象transactionManagement, 預設事務開啟的狀态tx标記
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映射,請檢查");
}
return mapper;
}
}
七.深度分析解析SqlSession幹的核心工作
1.selectOne & selectList做的工作
主要是分發了下功能, 執行sql語句避免不了有參數和無參數的, 都讓調用有參數的友善管理
在執行前, 考慮還有一種情況, 使用者不是通過接口代理的方式來執行以上方法, 這樣手動輸入sqlId容易造成錯誤
這裡做一個健壯性判斷
BaseExecutor中的query以及queryList做的核心工作
首先這兩個方法的特點都是查詢, 其步驟基本類似, 是以這裡可以合并一起轉調query0功能
這裡需要對參數進行設定, 還根據最後isOne的參數決定傳回值是否是單個
參數設定這裡比較複雜我們通過圖解的方式來解釋, (注: 參數是List集合類型的和數組類型的沒有做!!!)
對結果的封裝主要用到内省技術和資料庫中繼資料等等知識點
2.update&delete&insert做的工作
BaseExecutor中的update做的核心工作
還是和query&queryList一樣需要設定參數, 不管是增删改其本質其結果都是一緻
3.getMapper代理模式開發的原理
主要使用的動态代理的技術建立接口的實作類, 内部主要整合了sqlId和參數, 省去使用者自己拼sqlId拼錯的風險
也同時解決使用者手動合參數的麻煩, 但是最終工作的還是selectOne,selectList以及update0這些方法
總結自定義mybatis用的技術點
一款架構的誕生肯定不是一蹴而就的, 随着時間慢慢推進逐漸更新出來, 是以一款好的架構肯定要經過
很多考驗才能夠穩定靠譜, 但是縱觀整篇用的技術點, 不難發現架構也是由基礎代碼編寫而來,解決大量重複
的工作, 提供擴充性等等機制,比如本篇用核心的技術點有
① 反射
② 内省
③ 解析xml
④ 動态代理
⑤ 工廠設計模式
等等, 感謝大家耐心閱覽, 附件有本篇的原碼, 如果有更好的建議和想法歡迎和小編一起探讨交流