本次目标:手写Mybatis,理解运行流程
前置知识
- JDBC的相关操作
- XML文件的解析
- Mybatis的基本使用
- 反射基本知识
Mybatis基本运行流程
- 读取解析配置文件
–db.properties
–mybatis-config.xml
–mapper.xml- 执行SQL
– 参数的解析,生成动态SQL
– SQL执行
– 结果集映射(封装成实体对象/集合)
运行流程代码分析
/**
* 解析配置文件,生成SqlSessionFactory
*/
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
/**
* 执行SQL阶段
*/
SqlSession sqlSession = sqlSessionFactory.openSession();
//创建StudentDao的代理对象
StudentDao studentDao = sqlSession.getMapper(StudentDao.class);
Student student = studentDao.queryById(902);
System.out.println(student);
重要对象
Java是面向对象的语言,将xml中的数据信息解析封装成一个个对象也是合情合理的。
- Configuration:全局配置类,包含了很多重要的信息,如数据源、mappedStatement对象等。
- MappedStatement:封装了每一个INSERT|SELECT|UPDATE|DELETE标签中信息,保存了SQL的全部信息,最后存存储在Configuration的mappedStatements(Map)中,namespace+"."+SQL标签的id作为key,MappedStatement对象作为value。
- Executor:真正执行SQL的类的接口。
基本环境搭建
导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
配置文件
db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/studentdb
jdbc.username=root
jdbc.password=123456
mapper.xml
<mapper namespace="com.customer.mybatis.StudentDao">
<select id="selectOne" resultType="com.customer.mybatis.bean.Student" parameterType="java.lang.Integer">
select * from student where id = ?
</select>
</mapper>
具体实现代码
根据Mybatis源码的设计,定义相关主要的类,保留本次测试能用到的属性
Configuration.java
Data
public class Configuration {
//数据源
private String jdbcDriver;
private String jdbcUrl;
private String jdbcUsername;
private String jdbcPassword;
private Map<String,MappedStatement> mappedStatements = new HashMap<String, MappedStatement>();
}
MappedStatement.java
@Data
public class MappedStatement {
private String namespace;
private String sourceId; // namespace+"."+id(key值)
private String resultType; // 返回结果类型
private String sql; //sql语句
}
- 根据mybatis运行流程以及源码支持,我们知道首先需要读取配置文件,创建SqlSessionFactory。
- 如下是源码中创建SqlSessinFactory的build方法,在经过一系列解析配置后,将Configuration对象以构造方法的形式注入到实现类DefaultSqlSessionFactory中。
- public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
SqlSessionFactory.java
/**
* SqlSessinFactory两个主要任务
* 1、实例化时加载Configuration
* 2、生成SqlSession
*/
public class SqlSessionFactory {
private final Configuration configuration = new Configuration();
public static final String DB_CONFIG = "db.properties";
public static final String MAPPER_CONFIG = "com/customer/mybatis/mapper";
//初始化加载配置文件并解析
public SqlSessionFactory() {
loadDbInfo();
loadMappersInfo();
}
//openSession方法加载Configuration对象
public SqlSession openSqlSession(){
return new DefaultSqlSession(configuration);
}
//加载mapper.xml的信息。然后将存储在Configuration对象中
private void loadMappersInfo() {
URL resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_CONFIG);
//mapper文件夹下的全部mapper.xml文件
File mapperXmls = new File(resource.getFile());
if(mapperXmls.isDirectory()){
File[] mapperXmlList = mapperXmls.listFiles();
for (File file : mapperXmlList) {
if(file.getName().indexOf(".xml") > 0){
loadMappersInfo(file);
}
}
}
}
//Dom4j解析Xml文件,获取namespace、id、resultType、sql
private void loadMappersInfo(File file) {
SAXReader saxReader = new SAXReader();
Document document = null;
try {
//读取xml文件,转换成Doucment对象
document = saxReader.read(file);
} catch (DocumentException e) {
e.printStackTrace();
}
//获取根节点元素<mapper></mapper>
Element rootElement = document.getRootElement();
//获取命名空间
String namespace = rootElement.attribute("namespace").getValue();
//获取根节点下的全部子节点<INSERT/SELECT/UPDATE/DELETE>
//本次测试只读取<select></select>节点
List<Element> elements = rootElement.elements("select");
//遍历select节点,读取id、resultType、sql,写入MappedStatement,然后以键值对的方式存储在Configuration中
for (Element element : elements) {
MappedStatement mappedStatement = new MappedStatement();
String id = element.attribute("id").getValue();
String resultType = element.attribute("resultType").getValue();
String sql = element.getStringValue();
String sourceId = namespace+"."+id;
//向MappedStatement中添加属性
mappedStatement.setNamespace(namespace);
mappedStatement.setResultType(resultType);
mappedStatement.setSql(sql);
mappedStatement.setSourceId(sourceId);
configuration.getMappedStatements().put(sourceId,mappedStatement);
}
}
//加载数据库配置信息,最后加载到Configuration对象中
private void loadDbInfo() {
//加载数据库配置文件
InputStream inputStream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG);
Properties properties = new Properties();
try {
//将配置配置信息写入properties
properties.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
//将数据库配置信息写入configruation对象
configuration.setJdbcDriver(properties.get("jdbc.driver").toString());
configuration.setJdbcUrl(properties.get("jdbc.url").toString());
configuration.setJdbcUsername(properties.get("jdbc.username").toString());
configuration.setJdbcPassword(properties.get("jdbc.password").toString());
}
}
在生成了SqlSessionFactory后,便可以生成Sqlsession,SqlSession对外提供了访问数据库的API,而且将数据库的操作分配给了Executor,因此SqlSession中必须包含CRUD方法以及Executor对象以及创建Mapper代理对象的getMapper方法。
SqlSession.java
public interface SqlSession {
<T> T selectOne(String statement, Object parameter);
<T> List<T> selectList(String statement,Object parameter);
<T> T getMapper(Class<T> clazz);
}
DefaultSqlSession.java
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
this.executor = new DefaultExecutor(configuration);
}
//查询单个对象方法
public <T> T selectOne(String statement, Object parameter) {
List<Object> objects = this.selectList(statement, parameter);
if(objects.size() == 0){
return null;
}
if (objects.size() == 1){
return (T) objects.get(0);
}else{
throw new RuntimeException("except one,but found "+objects.size());
}
}
public <T> List<T> selectList(String statement, Object parameter) {
return executor.query(configuration.getMappedStatements().get(statement), parameter);
}
public <T> T getMapper(Class<T> clazz) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(),new Class[]{
clazz},new MapperProxy(this));
}
}
MapperProxy.java
public class MapperProxy implements InvocationHandler {
private SqlSession sqlSession;
public MapperProxy(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String namespace = method.getDeclaringClass().getName();
String id = method.getName();
//判断返回类型是否是集合
if(Collection.class.isAssignableFrom(method.getReturnType())) {
return sqlSession.selectList(namespace + "." + id, args == null ? null : args[0]);
}
return sqlSession.selectOne(namespace + "." + id, args == null ? null : args[0]);
}
}
有了SqlSession,就可以通过Mapper接口创建代理对象调用方法操作数据库了。
mapper.java
public interface StudentDao {
Student selectOne(Integer id);
}
但是最终的CURD方法还是要交给Executor执行器。在Mybatis中,一共有三种执行器,本次测试只实现一个DefaultExecutor
Executor.java
public interface Executor {
<T> List<T> query(MappedStatement ms, Object parameter);
<T> T queryOne(MappedStatement ms, Object parameter);
}
public class DefaultExecutor implements Executor {
private final Configuration configuration;
public DefaultExecutor(Configuration configuration) {
this.configuration = configuration;
}
//查询集合方法
public <T> List<T> query(MappedStatement ms, Object parameter) {
//定义一个最终的结果集List
List<T> resultList = new ArrayList<T>();
try {
Class.forName(configuration.getJdbcDriver());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//获取数据库连接
connection = DriverManager.getConnection(configuration.getJdbcUrl(), configuration.getJdbcUsername(), configuration.getJdbcPassword());
//获取preparedStatement对象
preparedStatement = connection.prepareStatement(ms.getSql());
//参数赋值
parameterize(preparedStatement, parameter);
//获取结果集
resultSet = preparedStatement.executeQuery();
//结果集映射
handleResultSet(resultSet, resultList, ms.getResultType());
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
try {
resultSet.close();
preparedStatement.close();
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return resultList;
}
//结果集映射,利用反射简单实现结果集到对象的转化
private <T> void handleResultSet(ResultSet resultSet, List<T> resultList, String resultType) {
try {
Class<?> clazz = Class.forName(resultType);
//获取声明的全部属性
Field[] fileds = clazz.getDeclaredFields();
while (resultSet.next()){
T object = (T) clazz.newInstance();
for (Field filed : fileds) {
//属性名称
String name = filed.getName();
//结果集中的数据
Object value = resultSet.getObject(name);
if (value != null) {
filed.setAccessible(true);
filed.set(object, value);
}
}
resultList.add(object);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//参数赋值,动态SQL的生成,参数赋值是关键,也挺复杂,本次测试就简单做做样子
private void parameterize(PreparedStatement preparedStatement, Object parameter) {
try {
preparedStatement.setObject(1, parameter);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
public <T> T queryOne(MappedStatement ms, Object parameter) {
System.out.println(ms.getSql());
return null;
}
}
测试
public class Test {
public static void main(String[] args) {
//创建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
//获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSqlSession();
//创建mapper代理对象
StudentDao studentDao = sqlSession.getMapper(StudentDao.class);
//执行sql
Student student = studentDao.selectOne(914);
//打印结果
System.out.println(student);
}
}
结果
Student(id=903, name=张三, password=123456, sex=女, birth=1989-03-12, address=湖南省永州市)