此篇记录自定义的一个简易mybatis,也算是架构师入门了吧
什么是mybatis
mybatis是一个框架。通常我们自己写dao层,都会把数据库的信息以及sql语句直接写到代码中,后期维护起来代价是相当昂贵的,于是mybatis就帮我们简化dao的开发复杂度,将代码和sql语句等进行解耦。我们只需要进行一些配置,就可以很方便的操作数据库,更多精力集中在数据库的语句编写当中,提升查询效率,而不再需要纠结返回结果集的处理、驱动安装等等工作。
mybatis为什么可以解耦
为了我们阅读方便,我们会用一个或多个xml用来记录sql语句以及返回值类型。我们通常命名为XXXMapper.xml。
mybatis从一个xml文件中读取相关配置信息,你的数据库url、用户名、密码以及数据库驱动,为了程序读取的时候只需要找一个xml配置文件,我们会把XXXMapper.xml也一起放到这个配置文件中,mybatis会自动将我们的sql语句装载到一个叫Configuration的pojo集合中。我们通常命名为该配置文件为SqlMapConfig.xml。
有了这两个配置文件,就把sql语句和配置信息从代码当中解耦出来,以后要修改数据库的字段或者说将mysql改为orcale,就不需要重新编译代码,只需要修改两个xml的相关信息即可。
源码分析阶段
接下来,先贴上使用mybatis的简单案例代码
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
public class TestMybatis {
@Test
public void testFindAll() {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory build = sqlSessionFactoryBuilder.build(inputStream);
SqlSession sqlSession = build.openSession();
List list = sqlSession.selectList("com.itheima.dao.UserDao.findAll");
for (Object o : list) {
System.out.println(o);
}
sqlSession.close();
}
}
从上面代码可以看到,首先需要一个SqlSessionFactoryBuilder来传入一个InputStream创建SqlSessionFactory。InputStream就是读取的配置文件。使用构建者模式来创建工厂。创建出来之后调用SqlSessionFactory的openSession方法,来加载配置文件,将配置文件信息存放到Configuration对象中,然后返回一个SqlSession。
Sqlsession的作用就类似于我们自己写的dao层,用来处理各种sql语句。
由于整个机制必须采用反射来搞定sql语句、返回值、参数等内容,直接把sql的执行语句放到Sqlsession中必定会很使代码的可读性很差,于是又创建了一个Executor工具类,用了执行sql语句,SqlSession的作用就是将sql语句进一步解析,具体的待会儿看代码再详细解释。
根据上图,首先我们看看SqlSessionFactoryBuilder.java的代码。这里有三个不同参数的build,当然都是为了获取流文件,然后返回一个工厂对象。
package cn.itclass.factory;
import java.io.InputStream;
public class SqlSessionFactoryBuilder {
// 如果给的是一个inputStream当然是最好不过
public SqlSessionFactory build(InputStream inputStream){
return new SqlSessionFactory(inputStream);
}
// 如果直接传过来一个文件地址,就根据地址去获得一个工厂
public SqlSessionFactory build(String path){
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(path);
return new SqlSessionFactory(inputStream);
}
// 如果用户什么都不给,就到默认地址找默认名称的配置文件,再创建一个流,
public SqlSessionFactory build(){
String path = "SqlMapConfig.xml";
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(path);
return new SqlSessionFactory(inputStream);
}
}
接下来是SQLSessionFactory.java获得一个工厂对象之后,工厂对象开始对我们的流文件进行解析,并且返回一个SqlSession对象供我们调用sql执行方法。此方法需要loadConfiguration()来解析SqlMapConfig.xml文件,loadXmlConfiguration()解析XXXMapper.xml文件。使用dom4j来解析xml。
package cn.itclass.factory;
import cn.itclass.dao.SqlSession;
import cn.itclass.dao.impl.SqlSessionImpl;
import cn.itclass.domain.Configuration;
import cn.itclass.domain.Mapper;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;
public class SqlSessionFactory {
private InputStream inputStream;
private Configuration cfg;
// 创建工厂必须传入一个inputStream,否则无法进行解析
public SqlSessionFactory(InputStream inputStream) {
this.inputStream = inputStream;
}
// 要调用sql的执行方法,首先就必须要获取到配置信息,所以创建一个Configuration对象,来存储配置信息
public SqlSession openSession() {
cfg = new Configuration();
// 开始解析基础配置文件
loadConfiguration();
SqlSession sqlSession = new SqlSessionImpl(cfg);
return sqlSession;
}
public void loadConfiguration() {
SAXReader reader = new SAXReader();
Document doc = null;
try {
doc = reader.read(inputStream);
Element root = doc.getRootElement();
List<Element> list = root.selectNodes("//property");
for (Element element : list) {
String value = element.attributeValue("value");
String name = element.attributeValue("name");
if (name.equals("driver")) {
cfg.setDriver(value);
}
if (name.equals("url")) {
cfg.setUrl(value);
}
if (name.equals("username")) {
cfg.setUsername(value);
}
if (name.equals("password")) {
cfg.setPassword(value);
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 除了获取基础配置信息,还需要获取所有的sql语句相关信息,并且存到一个list当中,以方便使用
Element root = doc.getRootElement();
Element mappers = root.element("mappers");
List<Element> mapperList = mappers.elements("mapper");
for (Element element : mapperList) {
String path = element.attributeValue("resource");
loadXmlConfiguration(path);
}
}
// 解析XXXMapper.xml文件
public void loadXmlConfiguration(String path) {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(path);
SAXReader reader = new SAXReader();
try {
Document doc = reader.read(inputStream);
Element root = doc.getRootElement();
String namespace = root.attributeValue("namespace");
List<Element> elements = root.elements();
for (Element element : elements) {
String resultType = element.attributeValue("resultType");
String id = element.attributeValue("id");
String sql = element.getTextTrim();
String key = namespace + "." + id;
Mapper value = new Mapper();
value.setResultType(resultType);
value.setSql(sql);
cfg.getXmlMap().put(key, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
由第一张图可以看出,我们的sql和resultType是另外写了一个Mapper类来存储的,Configuration用一个map来存储,这样我们要用哪一个语句就能很方便的找到。Map的key用namespace+"."+id的形式来存,value就用Mapper即可。
当配置文件读取完毕之后,就会将Configuration传给SqlSessionImpl(SqlSession的实现类)来创建一个SqlSession对象,然后执行sql语句。
为了关闭时关闭的是同一个链接,这里必须要定义一个构造函数接受Configuration对象。然后调用执行器来执行sql语句。
package cn.itclass.dao.impl;
import cn.itclass.dao.SqlSession;
import cn.itclass.domain.Configuration;
import cn.itclass.domain.Mapper;
import cn.itclass.utils.Executor;
import java.util.List;
public class SqlSessionImpl implements SqlSession {
private Configuration cfg;
private Executor executor;
public SqlSessionImpl(Configuration cfg) {
this.cfg = cfg;
executor = new Executor(cfg);
}
@Override
public List selectList(String mapperId) {
Mapper mapper = cfg.getXmlMap().get(mapperId);
String sql = mapper.getSql();
String resultType = mapper.getResultType();
return executor.executeQuery(sql,resultType);
}
@Override
public void close() {
executor.close();
}
}
为了阅读性,将sql的执行单独拿出来写,创建了一个Executor执行器,因为内容都在Configuration当中,需要采用反射机制来完成,所以会比较复杂。
package cn.itclass.utils;
import cn.itclass.domain.Configuration;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class Executor {
private Configuration cfg;
private Connection conn = null;
private PreparedStatement pst = null;
private ResultSet rs = null;
List list = new ArrayList();
public Executor(Configuration cfg) {
this.cfg = cfg;
}
public List executeQuery(String sql, String resultType) {
// 1、获取驱动
try {
Class.forName(cfg.getDriver());
} catch (Exception e) {
e.printStackTrace();
}
try {
// 2、获取连接
conn = DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
// 3、创建statement对象
pst = conn.prepareStatement(sql);
// 4、执行sql
rs = pst.executeQuery();
// 5、处理结果集
List<String> columnNames = new ArrayList<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i);
columnNames.add(columnName);
}
Class clazz = Class.forName(resultType);
while (rs.next()) {
Object o = clazz.newInstance();
Method[] methods = clazz.getMethods();
for (Method method : methods) {
for (String columnName : columnNames) {
if (("set" + columnName).equalsIgnoreCase(method.getName())) {
Object columnValue = rs.getObject(columnName);
method.invoke(o, columnValue);
}
}
}
list.add(o);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
public void close() {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (pst != null) {
try {
pst.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
整体总结执行流程:
刚刚入门,如有错误请大神指出。