自己重写 MyBatis 框架(基础篇)。
MyBatis 框架入门请参阅
在以上工程的基础上,理解 MyBatis 框架的流程,我们可以自己尝试写一个“MyBatis 框架”,以便更深入了解 MyBatis。
源代码请移步:https://github.com/lyfGeek/MyBatis_my
新建 Maven 项目。
延用以上工程的 Maven 依赖。但这里请注意,我们的目的是自己重写 “MyBatis”,所以就不需要引入 MaBatis 的依赖,所以这里我注释掉了。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.geek</groupId>
<artifactId>mybatis_my</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
-->
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
</dependencies>
</project>
来看此时的项目结构。
需要我们自己重写的类:
class Resources {}
class SqlSessionFactoryBuilder { build() {} }
class SqlSessionFactory { openSession() }
class SqlSession { getMapper() }
我们来自己写读取配置文件的类。
package com.geek.mybatis.io;
import java.io.InputStream;
/**
* 使用类加载器读取配置文件的类。
*
* @author geek
*/
public class Resources {
/**
* 根据传入的参数,获取一个字节输入流。
*
* @param filePath
* @return
*/
public static InputStream getResourceAsStream(String filePath) {
return Resources.class.getClassLoader().getResourceAsStream(filePath);
}
}
然后根据 test 类中所缺失的类逐一创建。
最终项目结构。
package com.geek.mybatis.sqlSession;
import com.geek.mybatis.cfg.Configuration;
import com.geek.mybatis.sqlSession.defaults.DefaultSqlSessionFactory;
import com.geek.mybatis.utils.XMLConfigBuilder;
import java.io.InputStream;
/**
* 用于创建一个 SqlSessionFactoryBuilder 对象。
*
* @author geek
*/
public class SqlSessionFactoryBuilder {
/**
* 根据参数的字节输入流来构建一个 SqlSessionFactory 工厂。
*
* @param config
* @return
*/
public ISqlSessionFactory build(InputStream config) {
Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
return new DefaultSqlSessionFactory(cfg);
}
}
package com.geek.mybatis.sqlSession;
/**
* 用于打开一个新的 ISqlSession 对象。
*
* @author geek
*/
public interface ISqlSessionFactory {
/**
* 用于打开一新的 ISqlSession 对象。
*
* @return
*/
ISqlSession openSession();
}
package com.geek.mybatis.sqlSession;
/**
* 自定义 MyBatis 中和数据库交互的核心类。
* 创建 dao 接口的代理对象。
*
* @author geek
*/
public interface ISqlSession {
/**
* 根据参数创建一个代理对象。
*
* @param daoInterfaceClass dao 的接口字节码。
* @param <T>
* @return
*/
<T> T getMapper(Class<T> daoInterfaceClass);
// 泛形——> 先声明,再使用。<T>。
/**
* 释放资源。
*/
void close();
}
下一步,我们要解析 xml 文件,获取其中的 MySQL 连接数据以及 sql 语句。就需要 dom4j 来解析 xml。
在 pom.xml 中添加依赖。
<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
Class ConfigBuilder 中需要使用 XPath 就需要依赖。
<!-- https://mvnrepository.com/artifact/jaxen/jaxen -->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
需要读取配置文件,就需要实体类,用于封装。
package com.geek.mybatis.cfg;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义 MyBatis 的配置类。
*
* @author geek
*/
@Data
public class Configuration {
private String driver;
private String url;
private String username;
private String password;
private Map<String, Mapper> mappers = new HashMap<String, Mapper>();
public Map<String, Mapper> getMappers() {
return mappers;
}
public void setMappers(Map<String, Mapper> mappers) {
// this.mappers = mappers;
// 此处使用追加的方式。如果使用赋值方式,只能有一个<mapper>标签。前面的会被覆盖。
this.mappers.putAll(mappers);
}
}
package com.geek.mybatis.cfg;
import lombok.Data;
/**
* 用于封装执行 SQL 语句和结果类型的全限定类名。
*
* @author geek
*/
@Data
public class Mapper {
/**
* SQL。
*/
private String queryString;
/**
* 实体类的全限定类名。
*/
private String resultType;
}
- utils 工具类。
package com.geek.mybatis.utils;
import com.geek.mybatis.cfg.Configuration;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* 创建数据库连接的工具类。
*
* @author geek
*/
public class DataSourceUtil {
public static Connection getConnection(Configuration cfg) {
try {
Class.forName(cfg.getDriver());
return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new RuntimeException(e);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
现在需要
Interface SqlSession 的实现类 Class DefaultSqlSession {}
和
Interface SqlSessionFactory 的实现类 Class DefaultSqlSessionFactory {}
package com.geek.mybatis.sqlSession.defaults;
import com.geek.mybatis.cfg.Configuration;
import com.geek.mybatis.sqlSession.ISqlSession;
import com.geek.mybatis.sqlSession.ISqlSessionFactory;
/**
* ISqlSessionFactory 接口的实现类。
*
* @author geek
*/
public class DefaultSqlSessionFactory implements ISqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
/**
* 用于创建一个新的操作数据库对象。
*
* @return
*/
@Override
public ISqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
public class MapperProxy implements InvocationHandler {}
// 此处需要实现 InvocationHandler。
package com.geek.mybatis.sqlSession.proxy;
import com.geek.mybatis.cfg.Mapper;
import com.geek.mybatis.utils.Executor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;
/**
* @author geek
*/
public class MapperProxy implements InvocationHandler {
// map 的 key 是全限定类名 + 方法名。
private Map<String, Mapper> mappers;
private Connection connection;
public MapperProxy(Map<String, Mapper> mappers, Connection connection) {
this.mappers = mappers;
this.connection = connection;
}
/**
* 用于对方法进行增强。
* 我们的增强其实就是调用 selectList(); 方法。
*
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取方法名。
String methodName = method.getName();
// 获取方法所在的类名。
String className = method.getDeclaringClass().getName();
// 组合 key。
String key = className + "." + methodName;
// 获取 mappers 中的 Mapper 对象。
Mapper mapper = mappers.get(key);
// 判断是否有 mapper。
if (mapper == null) {
throw new IllegalArgumentException("传入的参数有误。");
}
// 调用工具类,执行查询所有。
return new Executor().selectList(mapper, connection);
}
}
现在需要执行查询的工具类。
utils 包下。
package com.geek.mybatis.utils;
import com.geek.mybatis.cfg.Mapper;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* 负责执行 Sql 语句,并且封装结果。
*/
public class Executor {
/**
* 执行查询。
*
* @param mapper
* @param connection
* @param <E>
* @return
*/
public <E> List<E> selectList(Mapper mapper, Connection connection) {
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
// 取出 mapper 中的数据。
String queryString = mapper.getQueryString();// select * from user;
String resultType = mapper.getResultType();// com.geek.domain.User。
Class<?> aClass = null;
try {
aClass = Class.forName(resultType);
// 获取 PreparedStatement 对象。
preparedStatement = connection.prepareStatement(queryString);
// 执行 SQL 语句,获取结果集。
resultSet = preparedStatement.executeQuery();
// 封装结果集。
List<E> list = new ArrayList<E>();// 定义返回值。
while (resultSet.next()) {
// 实例化要封装的实体类对象。
E o = (E) aClass.newInstance();
// 取出结果集的元信息,ResultSetMetaData。
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
// 取出总列数。
int columnCount = resultSetMetaData.getColumnCount();
// 遍历总列数。
for (int i = 1; i <= columnCount; i++) {
// 获取列名。从 1 开始。
String columnName = resultSetMetaData.getColumnName(i);
// 根据得到的列名,获取每列的值。
Object columnValue = resultSet.getObject(columnName);
// 给 Object 赋值,使用 java 内省机制(借助 PropertyDescription 实现属性的封装)。
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, aClass);// 要求:实体类
// 获取 ta 的写入方法。
Method writeMethod = propertyDescriptor.getWriteMethod();
// 把获取的列的值赋值给对象。
writeMethod.invoke(o, columnValue);
}
// 把赋好值的对象加入到集合中。
list.add(o);
}
return list;
} catch (ClassNotFoundException | InstantiationException | InvocationTargetException | IntrospectionException | SQLException | IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
release(preparedStatement, resultSet);
}
}
/**
* 释放资源。
*
* @param preparedStatement
* @param resultSet
*/
private void release(PreparedStatement preparedStatement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
现在需要连接数据库的工具类。
utils 包下。
package com.geek.mybatis.utils;
import com.geek.mybatis.cfg.Configuration;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* 创建数据库连接的工具类。
*
* @author geek
*/
public class DataSourceUtil {
public static Connection getConnection(Configuration cfg) {
try {
Class.forName(cfg.getDriver());
return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new RuntimeException(e);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}