Mybatis自定義持久層框架
Mybatis自定義持久層框架
概述
Mybatis使我們常用的持久層框架,他的本質是通過反射和動態代理的方式對JDBC(這裡以jdbc為例,當然mybatis不只是jdbc)的進一步封裝,所以我們通過反射和動態代理來嘗試自己實現一個建議的持久層框架。
思路
我們在開始之前先整理一下,我們的思路,根據我們使用mybatis的經驗,來總結我們可能需要用到的東西。
- 資料庫連線的xml配置檔案和我們寫sql的xml配置檔案
- 我們需要一個類解析xml配置檔案
- 還需要有一個介面類,其中包括,查詢插入等方法,來以供我們調取
- 還需要有一個具體的執行類
大概的思路就是這樣,然後我們開始吧
解析XML配置檔案
首先我們在客戶端建立兩個xml配置檔案,一個叫SqlConnectionMapper.xml用來資料庫連線,一個叫UserMapper.xml用來寫sql。
SqlConnectionMapper.xml
<configuration>
<dataresource>
<property name="driver" value="com.mysql.jdbc.Driver" ></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatisdemo"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"> </property>
</dataresource>
<mapper name="UserMapper.xml"></mapper>
</configuration>
UserMapper.xml
<configuration namespace="Dao.UserDao">
<select id="queryAll" paramtertype="mode.User" resulttype="mode.User">
select * from user;
</select>
</configuration>
注意的是我們在SqlConnectionMapper.xml裡面寫入了UserMapper.xml的地址,這樣的話,我們在解析xml配置檔案的時候,只需要傳遞SqlConnectionMapper.xml這一個配置檔案就能獲取到所有xml檔案,同時我們在實際的生產過程中,也不可能只有一個UserMapper.xml配置檔案,而是多個xml配置檔案,所以我們需要一個公共的入口,去載入所有的配置檔案。
接下來我們去解析配置檔案
首先我們建立一個SqlSessionFactoryBuild類,將傳入的配置檔案通過我們新建的build方法進行解析。
解析完的配置檔案,我們需要把他們儲存到類裡面,方便我們操作。
新建ConfigBean存放資料庫連線資訊也就是SqlConnectionMapper.xml
新建SqlMpperBean存放sql語句資訊也就是UserMapper.xml中的方法
ConfigBean
public class ConfigBean {
private DataSource dataSource;
private Map<String,SqlMpperBean> sqlMpperBeanMap=new HashMap<String,SqlMpperBean>();
dataSource:存放jdbc連線屬性
sqlMpperBeanMap:是一個map,其中key是statementid,value是SqlMpperBean
statementid:用來標記要執行的方法,有UserMapper的namespace+要執行的方法的id組成
SqlMpperBean
public class SqlMpperBean {
// 對應xml中的namespace
private String namespace;
// 對應方法標籤的id
private String id;
// 對應傳入的引數型別
private String paramtertype;
// 對應返回引數型別
private String resulttype;
// sql語句
private String sqlText;
// 方法型別 例如:<select> <insert>...
private String type;
那麼我們如何去解析xml配置檔案呢?
這裡我們使用dom4j工具來解析配置檔案
新建XmlConfigBuild.class用來解析資料庫連線資訊
新建XmlSqlMapperBuild用來解析sql語句資訊
然後在SqlSessionFactoryBuild中呼叫這兩個方法
SqlSessionFactoryBuild.class
public class SqlSessionFactoryBuild {
public SqlSessionFactory build(InputStream inputStream) throws PropertyVetoException, DocumentException {
// 配置檔案解析
XmlConfigBuild xmlConfigBuild = new XmlConfigBuild();
ConfigBean configBean = xmlConfigBuild.build(inputStream);
SqlSessionFactory defaultSqlSession = new DefaultSqlSession(configBean);
return defaultSqlSession;
}
}
XmlConfigBuild.class
public class XmlConfigBuild {
private ConfigBean configBean;
public XmlConfigBuild(){
this.configBean=new ConfigBean();
}
public ConfigBean build(InputStream inputStream) throws DocumentException, PropertyVetoException {
// 讀取配置檔案
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
// 獲取根標籤
Element rootElement = document.getRootElement();
// 獲取所有property標籤
List<Element> propertys = rootElement.selectNodes("//property");
HashMap<String, String> jdbcMap = new HashMap<>();
// 將所有的property中的name屬性存入map中,方便我們接下來使用
propertys.forEach(proper->{
jdbcMap.put(proper.attributeValue("name"),proper.attributeValue("value"));
});
// 使用c3p0資料連線池,保證資料連線的持久化
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
// 獲取資料庫連線資訊
comboPooledDataSource.setDriverClass(jdbcMap.get("driver"));
comboPooledDataSource.setJdbcUrl(jdbcMap.get("jdbcUrl"));
comboPooledDataSource.setUser(jdbcMap.get("user"));
comboPooledDataSource.setPassword(jdbcMap.get("password"));
this.configBean.setDataSource(comboPooledDataSource);
// 獲取sqlMapper.xml
List<Element> mapperList = rootElement.selectNodes("//mapper");
mapperList.forEach(mapper->{
String path = mapper.attributeValue("name");
// XmlSqlMapperBuild解析對應的sql語句
XmlSqlMapperBuild xmlSqlMapperBuild = new XmlSqlMapperBuild(this.configBean);
try {
xmlSqlMapperBuild.build(path);
} catch (DocumentException e) {
e.printStackTrace();
}
});
return this.configBean;
}
}
XmlSqlMapperBuild.class
public class XmlSqlMapperBuild {
private ConfigBean configBean;
public XmlSqlMapperBuild(ConfigBean configBean){
this.configBean=configBean;
}
public void build(String path) throws DocumentException {
InputStream resourceAsStream = XmlSqlMapperBuild.class.getClassLoader().getResourceAsStream(path);
// 同樣使用dom4j讀取配置檔案
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(resourceAsStream);
// 獲取根標籤
Element rootElement = document.getRootElement();
// namespace唯一標識一個Mapper.xml
String namespace = rootElement.attributeValue("namespace");
List<Element> elements = rootElement.elements();
elements.forEach(node->{
SqlMpperBean sqlMpperBean = new SqlMpperBean();
// 要執行的方法id,唯一標識
sqlMpperBean.setId(node.attributeValue("id"));
sqlMpperBean.setNamespace(namespace);
// 獲取傳入引數型別
sqlMpperBean.setParamtertype(node.attributeValue("paramtertype"));
//獲取返回引數型別
sqlMpperBean.setResulttype(node.attributeValue("resulttype"));
//獲取sql
sqlMpperBean.setSqlText(node.getText());
//獲取方法標籤
sqlMpperBean.setType(node.getName());
//組成statementid,標識那個sqlmapper.xml中的那個方法
String key=namespace+"."+node.attributeValue("id");
this.configBean.addSqlMapperBeanMap(key,sqlMpperBean);
});
}
}
這樣我們的xml配置檔案就解析完成了,然後我們回過頭來看,發現我們在SqlSessionFactoryBuild的build方法中除了呼叫解析配置檔案方法,我們還例項化了一個DefaultSqlSession。
DefaultSqlSession是 SqlSessionFactory介面的實現,而SqlSessionFactory提供了我們要執行的方法,包括查詢,插入等。
構建Session,呼叫執行方法
SQLSessionFactory
public interface SqlSessionFactory {
public <E> List<E> selectList(String statementid,Object...params);
public <T> T getMapper(Class<?> mapperClass);
public <T> T addOne(String statementid,Object...params);
}
DefaultSqlSession
public class DefaultSqlSession implements SqlSessionFactory {
private ConfigBean configBean;
public DefaultSqlSession(ConfigBean configBean){
this.configBean=configBean;
}
@Override
public <T> T getMapper(Class<?> mapperClass) {
// 使用動態代理
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
// 方法名
String name = method.getName();
// 該方法對應的類名
String namespace = method.getDeclaringClass().getName();
// 組成statementid
String key = namespace + "." + name;
// 獲取對應sqlmapper,得到該方法對應的sql資訊
SqlMpperBean sqlMpperBean = getSqlMpperBean(key);
// 根據該方法的標籤名,選擇對應的方法
switch (sqlMpperBean.getType()){
case "select":{
List<Object> objectList = selectList(key, objects);
return objectList;
}
case "insert":{
Object addOne = addOne(key, objects);
return addOne;
}
}
return new Exception("沒有找到對應標籤!請檢查sql");
}
});
return (T)proxyInstance;
}
private SqlMpperBean getSqlMpperBean(String statementid){
Map<String, SqlMpperBean> sqlMpperBeanMap = this.configBean.getSqlMpperBeanMap();
SqlMpperBean sqlMpperBean = sqlMpperBeanMap.get(statementid);
return sqlMpperBean;
}
@Override
public <E> List<E> selectList(String statementid, Object... params) {
SqlMpperBean sqlMpperBean = getSqlMpperBean(statementid);
Executor executor = new Executor();
// 執行器執行該方法
List<Object> query = executor.query(this.configBean, sqlMpperBean, params);
return (List<E>) query;
}
@Override
public <T> T addOne(String statementid, Object... params) throws SQLException, ClassNotFoundException {
SqlMpperBean sqlMpperBean = getSqlMpperBean(statementid);
Executor executor = new Executor();
// 執行器執行該方法
Boolean aBoolean = executor.addOne(this.configBean, sqlMpperBean, params);
if(aBoolean){
return (T)aBoolean;
}else{
throw new RuntimeException("執行錯誤,檢查sql程式碼是否正確");
}
}
}
執行器Executor
executor執行器是實際執行sql的類,對傳入的引數進行解析,對放回的值進行封裝
Executor.class
public class Executor {
// 解析入參
private PreparedStatement getprepare(ConfigBean configBean,SqlMpperBean sqlMpperBean,Object...params) {
// 獲取資料庫連線資訊
DataSource dataSource = configBean.getDataSource();
Connection connection = dataSource.getConnection();
// 解析sql語句,對#{}進行替換成?
BoundSql boundSql = this.boundSQL(sqlMpperBean.getSqlText());
// boundsql.getsql 是解析完的sql語句
// preparedStatement 預處理物件,傳入sql和對應引數就可以執行sql獲取資料庫返回值
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql());
// 獲取引數路徑
String paramtertype = sqlMpperBean.getParamtertype();
// 獲取入參對應的類
Class<?> paramteClass = Class.forName(paramtertype);
// 獲取解析完的要執行的入參欄位名,比如id,name等
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
if(!parameterMappingList.isEmpty()){
for (int i =0; i < parameterMappingList.size(); i++) {
ParameterMapping paramter = parameterMappingList.get(i);
String content = paramter.getContent();
try {
// 通過反射和欄位名,獲取到類中對應的屬性
Field declaredField = paramteClass.getDeclaredField(content);
// 暴力訪問
declaredField.setAccessible(true);
// 獲取該屬性在指定物件上面的值
Object o = declaredField.get(params[0]);
// 將引數存入preparedStatement
preparedStatement.setObject(i+1,o);
} catch (NoSuchFieldException | IllegalAccessException | SQLException e) {
e.printStackTrace();
}
}
}
return preparedStatement;
}
// 查詢方法
public <E> List<E> query(ConfigBean configBean, SqlMpperBean sqlMpperBean,Object...params) {
PreparedStatement preparedStatement = getprepare(configBean, sqlMpperBean, params);
// 執行sql
ResultSet resultSet = preparedStatement.executeQuery();
// 封裝返回結果
String resulttype = sqlMpperBean.getResulttype();
Class<?> resultClass = Class.forName(resulttype);
ArrayList<Object> objects = new ArrayList<>();
while (resultSet.next()){
Object instance = resultClass.newInstance();
ResultSetMetaData metaData = resultSet.getMetaData();
if (metaData != null) {
for (int j = 1; j <=metaData.getColumnCount(); j++) {
// 欄位名
String columnName = metaData.getColumnName(j);
// 欄位值
Object object = resultSet.getObject(columnName);
// 使用反射,來進行對實體的封裝
// 獲取屬性
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass);
// 得到對應的寫方法
Method writeMethod = propertyDescriptor.getWriteMethod();
// 對屬性進行賦值
writeMethod.invoke(instance, object);
}
}
objects.add(instance);
}
return (List<E>) objects;
}
// sql解析
private BoundSql boundSQL(String sql){
// 這裡藉助了mybatis的ParameterMappingTokenHandler和GenericTokenParser來解析sql
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
// 把{#}替換為?
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
// 解析完的sql
String parse = genericTokenParser.parse(sql);
// 入參名
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
// 存入boundSql實體中,方便我們呼叫
BoundSql boundSql = new BoundSql(parse, parameterMappings);
return boundSql;
}
// 新增方法
public Boolean addOne(ConfigBean configBean, SqlMpperBean sqlMpperBean,Object...params) {
PreparedStatement getprepare = getprepare(configBean, sqlMpperBean, params);
try{
getprepare.executeUpdate();
return true;
}catch (Exception e){
System.out.println("請檢查sql或資料已存在,請不要重複插入!");
return false;
}
}
}
客戶端呼叫
首先我們新建一個Dao和實體
UserDao
public interface UserDao {
public List<User> queryAll();
public Boolean addOne(User user);
}
User
public class User implements Serializable {
private Integer id;
private String name;
}
最後我們寫一個查詢和新增測試一下
查詢
@Test
public void test() throws PropertyVetoException, DocumentException {
InputStream resourceAsStream = mybatis_test.class.getClassLoader().getResourceAsStream("SqlConnectionMapper.xml");
SqlSessionFactoryBuild sqlSessionFactoryBuild = new SqlSessionFactoryBuild();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuild.build(resourceAsStream);
UserDao userDao = sqlSessionFactory.getMapper(UserDao.class);
List<User> users = userDao.queryAll();
System.out.println("--------" + users);
}
結果:
新增
@Test
public void addTest() throws PropertyVetoException, DocumentException {
InputStream resourceAsStream = mybatis_test.class.getClassLoader().getResourceAsStream("SqlConnectionMapper.xml");
SqlSessionFactoryBuild sqlSessionFactoryBuild = new SqlSessionFactoryBuild();
SqlSessionFactory build = sqlSessionFactoryBuild.build(resourceAsStream);
UserDao mapper = build.getMapper(UserDao.class);
User user = new User();
user.setId(3);
user.setName("wangwu");
Boolean aBoolean = mapper.addOne(user);
System.out.println(aBoolean);
}
結果:
最後一問
該專案中用了幾種設計模式呢?