在本节内容,我们通过自定义实现一些 MyBatis 框架的核心组件类和相关类,来实现自己的 MyBatis 框架,这里主要是模拟实现 MyBatis 框架的查询功能。
1、创建工程环境
为简便开发,我们直接在入门案例((一)MyBatis入门)的基础上进行开发,在 pom.xml 中删除 MyBatis 的依赖,并添加解析 XML 文件的依赖:
<dependencies>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.8</version>
<scope>runtime</scope>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- 解析 xml 的 dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- dom4j 的依赖包 jaxen -->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
</dependencies>
在移除 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">
在移除 MyBatis 的依赖后,可以看到以下都是需要我们自己实现的类和接口:
2、自定义实现
2.1 读取配置文件信息
- Resources:读取配置文件类,用类加载器读取配置文件,并获取输入字节流。
public class Resources {
public static InputStream getResourceAsStream(String filePath) {
return Resources.class.getClassLoader().getResourceAsStream(filePath);
}
}
2.2 封装配置文件信息
- Mapper:映射器类,用于封装执行的 sql 语句和结果类型的全限定类名。
public class Mapper {
private String queryString;
private String resultType;
public String getQueryString() {
return queryString;
}
public void setQueryString(String queryString) {
this.queryString = queryString;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
}
- Configuration:MyBatis 的配置类,封装数据库连接信息和映射器map。
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 String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Map<String, Mapper> getMappers() {
return mappers;
}
public void setMappers(Map<String, Mapper> mappers) {
// 使用put的方式,存放所有的映射器
this.mappers.putAll(mappers);
}
}
2.3 工具类
- DataSourceUtils:数据库工具类,负责创建与数据库的连接。
public class DataSourceUtils {
public static Connection getConnection(Configuration cfg) {
try {
Class.forName(cfg.getDriver());
return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
- Select:自定义查询注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
// 查询sql语句
String value();
}
- XMLConfigBuilder:解析工具类,用来解析 XML 配置文件和 Dao 接口的注解。
public class XMLConfigBuilder {
public static Configuration loadConfiguration(InputStream config){
try{
// 定义封装的配置对象
Configuration cfg = new Configuration();
// 1.获取SAXReader对象
SAXReader reader = new SAXReader();
// 2.根据字节输入流获取Document对象
Document document = reader.read(config);
// 3.获取根节点
Element root = document.getRootElement();
// 4.使用xpath中选择指定节点的方式,获取所有property节点
List<Element> propertyElements = root.selectNodes("//property");
// 5.遍历节点
for(Element propertyElement : propertyElements){
String name = propertyElement.attributeValue("name");
if("driver".equals(name)){
// 获取驱动信息
String driver = propertyElement.attributeValue("value");
cfg.setDriver(driver);
}
if("url".equals(name)){
// 获取连接 url 信息
String url = propertyElement.attributeValue("value");
cfg.setUrl(url);
}
if("username".equals(name)){
// 获取用户名信息
String username = propertyElement.attributeValue("value");
cfg.setUsername(username);
}
if("password".equals(name)){
// 获取密码信息
String password = propertyElement.attributeValue("value");
cfg.setPassword(password);
}
}
// 取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
List<Element> mapperElements = root.selectNodes("//mappers/mapper");
for(Element mapperElement : mapperElements){
Attribute attribute = mapperElement.attribute("resource");
if(attribute != null){
System.out.println("基于XML开发");
// 获取映射 xml 文件路径
String mapperPath = attribute.getValue();
// 获取映射配置文件的内容,封装成一个map
Map<String, Mapper> mappers = loadMapperConfiguration(mapperPath);
// 添加到configuration中的mappers
cfg.setMappers(mappers);
}else{
System.out.println("基于注解开发");
// 获取class属性的值
String daoClassPath = mapperElement.attributeValue("class");
// 根据daoClassPath获取封装的必要信息
Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath);
// 添加到configuration中的mappers
cfg.setMappers(mappers);
}
}
return cfg;
}catch(Exception e){
throw new RuntimeException(e);
}finally{
try {
config.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
/**
* 根据传入的参数,解析XML,并且封装到Map中
* @param mapperPath 映射配置文件的位置
* @return key是由dao的全限定类名和方法名组成,value是一个Mapper对象
*/
private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException {
InputStream in = null;
try{
Map<String,Mapper> mappers = new HashMap<String,Mapper>();
// 1.根据路径获取字节输入流
in = Resources.getResourceAsStream(mapperPath);
// 2.根据字节输入流获取Document对象
SAXReader reader = new SAXReader();
Document document = reader.read(in);
// 3.获取根节点
Element root = document.getRootElement();
// 4.获取根节点的namespace属性取值
String namespace = root.attributeValue("namespace");//是组成map中key的部分
// 5.获取所有的select节点
List<Element> selectElements = root.selectNodes("//select");
// 6.遍历select节点集合
for(Element selectElement : selectElements){
// 取出id属性的值 组成map中key的部分
String id = selectElement.attributeValue("id");
// 取出resultType属性的值 组成map中value的部分
String resultType = selectElement.attributeValue("resultType");
// 取出文本内容 组成map中value的部分
String queryString = selectElement.getText();
// 创建Key
String key = namespace+"."+id;
// 创建Value
Mapper mapper = new Mapper();
mapper.setQueryString(queryString);
mapper.setResultType(resultType);
// 把key和value存入mappers中
mappers.put(key,mapper);
}
return mappers;
}catch(Exception e){
throw new RuntimeException(e);
}finally{
in.close();
}
}
/**
* 根据传入的参数,得到dao中所有被select注解标注的方法。
* 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息
* @param daoClassPath
* @return
*/
private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{
Map<String,Mapper> mappers = new HashMap<String, Mapper>();
// 1.得到dao接口的字节码对象
Class daoClass = Class.forName(daoClassPath);
// 2.得到dao接口中的方法数组
Method[] methods = daoClass.getMethods();
// 3.遍历Method数组
for(Method method : methods){
// 取出每一个方法,判断是否有select注解
boolean isAnnotated = method.isAnnotationPresent(Select.class);
if(isAnnotated){
// 创建Mapper对象
Mapper mapper = new Mapper();
// 取出注解的value属性值
Select selectAnno = method.getAnnotation(Select.class);
String queryString = selectAnno.value();
mapper.setQueryString(queryString);
// 获取当前方法的返回值,还要求必须带有泛型信息
Type type = method.getGenericReturnType();//List<User>
// 判断type是不是参数化的类型
if(type instanceof ParameterizedType){
ParameterizedType ptype = (ParameterizedType)type;
// 得到参数化类型中的实际类型参数
Type[] types = ptype.getActualTypeArguments();
// 取出第一个,参数化类型可能为 <k,v> 形式
Class domainClass = (Class)types[0];
// 获取domainClass的类名
String resultType = domainClass.getName();
// 给Mapper赋值
mapper.setResultType(resultType);
}
// 组装key的信息
String methodName = method.getName();
String className = method.getDeclaringClass().getName();
String key = className+"."+methodName;
// 给map赋值
mappers.put(key,mapper);
}
}
return mappers;
}
}
- Executor:用于执行jdbc相关操作,负责执行SQL语句,并且封装结果集。
public class Executor {
public <E> List<E> selectList(Mapper mapper, Connection conn) {
PreparedStatement pstm = null;
ResultSet rs = null;
try {
// 1.取出mapper中的数据
String queryString = mapper.getQueryString();
String resultType = mapper.getResultType();
Class domainClass = Class.forName(resultType);
// 2.获取PreparedStatement对象
pstm = conn.prepareStatement(queryString);
// 3.执行SQL语句,获取结果集
rs = pstm.executeQuery();
// 4.封装结果集
List<E> list = new ArrayList<E>();
while(rs.next()) {
// 实例化要封装的实体类对象
E obj = (E)domainClass.newInstance();
// 取出结果集的元信息:ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
// 取出总列数
int columnCount = rsmd.getColumnCount();
// 遍历总列数
for (int i = 1; i <= columnCount; i++) {
// 获取每列的名称,列名的序号是从1开始的
String columnName = rsmd.getColumnName(i);
// 根据得到列名,获取每列的值
Object columnValue = rs.getObject(columnName);
// 给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
// 要求:实体类的属性和数据库表的列名保持一致
PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);
// 获取它的写入方法
Method writeMethod = pd.getWriteMethod();
// 把获取的列的值,给对象赋值
writeMethod.invoke(obj,columnValue);
}
// 把赋好值的对象加入到集合中
list.add(obj);
}
return list;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
release(pstm,rs);
}
}
private void release(PreparedStatement pstm, ResultSet rs){
if(rs != null){
try {
rs.close();
}catch(Exception e){
e.printStackTrace();
}
}
if(pstm != null){
try {
pstm.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
2.4 核心组件类
- MapperProxy:代理类,在调用 Dao 接口方法时进行增强。
public class MapperProxy implements InvocationHandler {
private Map<String, Mapper> mappers;
private Connection conn;
public MapperProxy(Map<String, Mapper> mappers, Connection conn) {
this.mappers = mappers;
this.conn = conn;
}
/**
* 对方法进行增强,其实就是调用工具类的selectList方法
*/
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;
// 获取Mapper
Mapper mapper = mappers.get(key);
// 判断是否有mapper
if (mapper == null) {
throw new IllegalArgumentException("传入的参数有误");
}
// 调用工具类进行查询
return new Executor().selectList(mapper, conn);
}
}
- SqlSession:自定义 Mybatis 中和数据库交互的核心类,可以创建dao接口的代理对象。
public interface SqlSession {
// 根据参数创建一个代理对象
<T> T getMapper(Class<T> daoInterfaceClass);
// 释放资源
void close();
}
- DefaultSqlSession:SqlSession接口的实现类
public class DefaultSqlSession implements SqlSession {
private Configuration cfg;
private Connection conn;
public DefaultSqlSession(Configuration cfg) {
this.cfg = cfg;
conn = DataSourceUtils.getConnection(cfg);
}
/**
* 创建代理对象
* @param daoInterfaceClass dao接口的字节码对象
*/
public <T> T getMapper(Class<T> daoInterfaceClass) {
return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(),
new Class[]{daoInterfaceClass}, new MapperProxy(cfg.getMappers(), conn));
}
// 释放资源
public void close() {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
- SqlSessionFactory
public interface SqlSessionFactory {
// 用于打开一个新的SqlSession对象
SqlSession openSession();
}
- DefaultSqlSessionFactory:SqlSessionFactory接口的实现类
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration cfg;
public DefaultSqlSessionFactory(Configuration cfg) {
this.cfg = cfg;
}
// 用于创建SqlSession对象
public SqlSession openSession() {
return new DefaultSqlSession(cfg);
}
}
- SqlSessionFactoryBuilder:构建者类 ,用于构建SqlSessionFactory工厂
public class SqlSessionFactoryBuilder {
// 通过字节输入流构建一个SqlSessionFactory工厂
public SqlSessionFactory build(InputStream config) {
Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
return new DefaultSqlSessionFactory(cfg);
}
}
2.5 测试类
- 测试类,重新导入需要的类后,报错消除
public class MybatisTest {
public static void main(String[] args) throws Exception{
// 1. 读取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 创建 SqlSessionFactory 工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
// 3. 获取 SqlSession 对象
SqlSession sqlSession = factory.openSession();
// 4. 使用 SqlSession 创建 Mapper 的代理对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 5. 使用代理对象执行查询
List<User> users = mapper.findAll();
for(User user : users){
System.out.println(user);
}
// 6. 释放资源
sqlSession.close();
in.close();
}
}
3、运行结果
- 工程目录:
- 基于xml测试:
- 基于注解测试: