仿Mybatis实现查询功能

2023-10-04 源码探究Mybatis

可以先来看一张大致的类图,然后先看文章底部的测试代码,看看是如何使用的,然后进行开发,这样思路可能会更加清晰。

点击查看自研框架整体目录结构
|-- com.xk857
    |-- config
    |   |-- BoundSql.java                # 处理后的SQL对象,包含SQL语句(参数使用问号)和参数名称集合
    |   |-- XMLConfigBuilder.java    # 将配置文件转换成Configuration对象
    |   |-- XMLMapperBuilder.java  # 将单个mapper.xml配置文件转换成MappedStatement对象存储到Configuration集合中
    |-- executor
    |   |-- Executor.java                # 执行器接口
    |   |-- SimpleExecutor.java       # 实现执行器,使用JDBC完成数据库操作
    |-- io
    |   |-- Resources.java               # 加载resources目录下的xml配置文件,转换内容为输入流形式
    |-- pojo
    |   |-- Configuration.java           # 全局配置类,存放核心配置文件解析出来的内容
    |   |-- MappedStatement.java     # 单个mapper转换成的java对象,包含SQL语句、返回值类型、唯一标识……
    |-- session
    |   |-- DefaultSqlSession.java      # SqlSession实现类,实现selectList、selectOne等
    |   |-- DefaultSqlSessionFactory.java # SqlSessionFactory的实现类,获取执行器,返回DefaultSqlSession
    |   |-- SqlSession.java                # 定义查询、修改接口
    |   |-- SqlSessionFactory.java      # 接口,用于生产提供操作数据库的对象
    |   |-- SqlSessionFactoryBuilder.java # 构造最基本的操作对象,解析配置文件,创建SqlSessionFactory工厂对象
    |-- utils                                    # 这个包下的工具方法,都是用于将#{参数},改成?并记录参数集合的工具类
        |-- GenericTokenParser.java 
        |-- ParameterMapping.java
        |-- ParameterMappingTokenHandler.java
        |-- TokenHandler.java

自研Mybatis框架类图分析

# 创建执行器接口及默认实现

执行器是执行真正的JDBC代码,这里暂时不做具体显示,先定义方便后续调用;为什么要单独写个执行器,而不是直接写到SqlSession中,首先是单一职责原则,其次则是模仿Mybatis。

package com.xk857.executor;
import com.xk857.pojo.Configuration;
import com.xk857.pojo.MappedStatement;

public interface Executor { 

    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception;

    void close();
}

具体实现看后文,现在暂时先不写,创建SimpleExecutor防止报错

package com.xk857.executor;
import com.xk857.pojo.Configuration;
import com.xk857.pojo.MappedStatement;

public class SimpleExecutor implements Executor {

    private Connection connection = null;
    private PreparedStatement preparedStatement = null;
    private ResultSet resultSet = null;

    @Override                                                                               // user
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {
        ArrayList<E> list = new ArrayList<>();
        return list;
    }

    @Override
    public void close() {
        // 释放资源
    }
}

# 处理SQL语句

想一下Mapper里面的SQL语句是什么形式的?①我们需要将#{}替换成?;②保存#{}中的参数

select * from user where id = #{id} and username = #{username}

# 复制Mybatis中的工具类

TokenHandler定义标记处理接口
package com.xk857.utils;

public interface TokenHandler {
  String handleToken(String content);
}
ParameterMappingTokenHandler创建标记处理器,需配合标记解析器使用
public class ParameterMappingTokenHandler implements TokenHandler {
    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();

    // context是参数名称 #{id} #{username}

    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
        ParameterMapping parameterMapping = new ParameterMapping(content);
        return parameterMapping;
    }

    public List<ParameterMapping> getParameterMappings() {
        return parameterMappings;
    }

    public void setParameterMappings(List<ParameterMapping> parameterMappings) {
        this.parameterMappings = parameterMappings;
    }
}
GenericTokenParser标记解析器,将开始到结束变成问号,并将标记值保存下来
package com.xk857.utils;

/**
 * @author Clinton Begin
 */
public class GenericTokenParser {

    private final String openToken; //开始标记
    private final String closeToken; //结束标记
    private final TokenHandler handler; //标记处理器

    public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
    }

    /**
     * 解析${}和#{}
     * @param text
     * @return
     * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
     * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
     */
    public String parse(String text) {
        // 验证参数问题,如果是null,就返回空字符串。
        if (text == null || text.isEmpty()) {
            return "";
        }

        // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
        int start = text.indexOf(openToken, 0);
        if (start == -1) {
            return text;
        }

        // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
        // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
        char[] src = text.toCharArray();
        int offset = 0;
        final StringBuilder builder = new StringBuilder();
        StringBuilder expression = null;
        while (start > -1) {
            // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
            if (start > 0 && src[start - 1] == '\\') {
                builder.append(src, offset, start - offset - 1).append(openToken);
                offset = start + openToken.length();
            } else {
                //重置expression变量,避免空指针或者老数据干扰。
                if (expression == null) {
                    expression = new StringBuilder();
                } else {
                    expression.setLength(0);
                }
                builder.append(src, offset, start - offset);
                offset = start + openToken.length();
                int end = text.indexOf(closeToken, offset);
                while (end > -1) {////存在结束标记时
                    if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时
                        // this close token is escaped. remove the backslash and continue.
                        expression.append(src, offset, end - offset - 1).append(closeToken);
                        offset = end + closeToken.length();
                        end = text.indexOf(closeToken, offset);
                    } else {//不存在转义字符,即需要作为参数进行处理
                        expression.append(src, offset, end - offset);
                        offset = end + closeToken.length();
                        break;
                    }
                }
                if (end == -1) {
                    // close token was not found.
                    builder.append(src, start, src.length - start);
                    offset = src.length;
                } else {
                    //首先根据参数的key(即expression)进行参数处理,返回?作为占位符
                    builder.append(handler.handleToken(expression.toString()));
                    offset = end + closeToken.length();
                }
            }
            start = text.indexOf(openToken, offset);
        }
        if (offset < src.length) {
            builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
    }
}

# 创建BoundSQL对象

package com.xk857.config;

@Data
@AllArgsConstructor
public class BoundSql {

    // 处理后的SQL语句(带?号)
    private String finalSql;
    // 参数列表(#{}的内容,不是实际传递的参数值)
    private List<ParameterMapping> parameterMappingList;
}

ParameterMapping的content就是存放#{}中的值,比如id、nickName等;

package com.xk857.utils;

@Data
@AllArgsConstructor
public class ParameterMapping {
    private String content;
}

# 处理SQL语句解析成BoundSql对象

package com.xk857.executor;
import com.xk857.config.BoundSql;
import com.xk857.pojo.Configuration;
import com.xk857.pojo.MappedStatement;
import com.xk857.utils.GenericTokenParser;
import com.xk857.utils.ParameterMapping;
import com.xk857.utils.ParameterMappingTokenHandler;

public class SimpleExecutor implements Executor {
    
    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {
        ArrayList<E> list = new ArrayList<>();
        return list;
    }

    /**
     * #{}占位符替换成?并将#{}里面的值保存下来
     * @param sql 处理前的SQL
     * @return BoundSql对象
     */
    private BoundSql getBoundSql(String sql) {
        // 1.创建标记处理器:配合标记解析器完成标记的处理解析工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();

        // 2.创建标记解析器
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);

        // 2.1 #{}占位符替换成? 2.解析替换的过程中 将#{}里面的值保存下来 ParameterMapping
        String finalSql = genericTokenParser.parse(sql);

        // 2.2 #{}里面的值的一个集合 id username,注意此时是值就是"id"而不是替换成的值,值还没传过来
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
       
        // 3.处理成BoundSql对象
        return new BoundSql(finalSql, parameterMappings);
    }
}

# 实现执行器查询操作

  1. 加载驱动,获取数据库连接
  2. 获取preparedStatement预编译对象(需要将#{参数}处理成?,并将参数保存)
  3. 设置参数,将SQL语句的参数传递进去
    • 传递过来的JavaBean,通过反射根据参数名称获取对象中的属性内容
  4. 执行SQL语句
  5. 处理返回的结果集,先遍历结果集,然后根据数据库字段设置数据到JavaBean中,用List集合存储
public class SimpleExecutor implements Executor {

    private Connection connection = null;
    private PreparedStatement preparedStatement = null;
    private ResultSet resultSet = null;

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {
        // 1.加载驱动,获取数据库连接
        connection = configuration.getDataSource().getConnection();

        // 2.获取preparedStatement预编译对象(需要将#{参数}处理成?,并将参数保存)
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        String finalSql = boundSql.getFinalSql();
        preparedStatement = connection.prepareStatement(finalSql);

        // 3.设置参数
        String parameterType = mappedStatement.getParameterType();
        // 3.1 如果参数类型不为空,则开始解析设置参数(parameterType是根据xml的标签获取的)
        if (parameterType != null) {
            // 3.2 获取到参数类型的Clas对象
            Class<?> parameterTypeClass = Class.forName(parameterType);
            // 3.3 遍历#{}的参数列表
            List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
            for (int i = 0; i < parameterMappingList.size(); i++) {
                ParameterMapping parameterMapping = parameterMappingList.get(i);
                // 根据参数名称,获取对象的属性
                Field declaredField = parameterTypeClass.getDeclaredField(parameterMapping.getContent());
                // 暴力访问,否则私有属性获取不到值
                declaredField.setAccessible(true);
                Object value = declaredField.get(param);
                // 赋值占位符
                preparedStatement.setObject(i + 1, value);
            }
        }

        // 4.执行sql,发起查询
        resultSet = preparedStatement.executeQuery();

        // 5.处理返回结果集
        ArrayList<E> list = new ArrayList<>();
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = Class.forName(resultType);
        while (resultSet.next()) {
            // 元数据信息包含了字段名、字段的值
            ResultSetMetaData metaData = resultSet.getMetaData();
            Object o = resultTypeClass.newInstance();

            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                // 字段名
                String columnName = metaData.getColumnName(i);
                // 字段的值
                Object value = resultSet.getObject(columnName);
                // 通过反射设置参数的值
                Field declaredField = resultTypeClass.getDeclaredField(columnName);
                declaredField.setAccessible(true);
                declaredField.set(o, value);
            }
            list.add((E) o);
        }
        return list;
    }
}

# 创建SqlSession接口

package com.xk857.session;

public interface SqlSession {

    /**
     * 查询多个
     * @param statementId 根据statementId可找到Map对应的sql语句
     * @param param 查询参数
     */
    <E> List<E> selectList(String statementId, Object param) throws Exception;

    /** 查询单个结果 */
    <T> T selectOne(String statementId, Object param) throws Exception;

    /** 清除资源 */
    void close();

    /** 生成代理对象 */
    <T> T getMapper(Class<?> mapperClass);
}

# 创建SqlSession默认实现

public class DefaultSqlSession implements SqlSession {

    private final Configuration configuration;
    private final Executor executor;

    public DefaultSqlSession(Configuration configuration, Executor executor) {
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public <E> List<E> selectList(String statementId, Object param) throws Exception {
        // 1.获取MappedStatement对象
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        // 2.调用执行器,使用JDBC操作数据库
        return executor.query(configuration, mappedStatement, param);
    }

    @Override
    public <T> T selectOne(String statementId, Object param) throws Exception {
        // 去调用selectList();
        List<Object> list = this.selectList(statementId, param);
        if (list.size() == 1) {
            return (T) list.get(0);
        } else if (list.size() > 1) {
            throw new RuntimeException("返回结果过多");
        } else {
            return null;
        }
    }
}

# 创建DefaultSqlSessionFactory

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;
    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        // 1.创建执行器对象
        Executor simpleExecutor = new SimpleExecutor();

        // 2.生产sqlSession对象
        return new DefaultSqlSession(configuration, simpleExecutor);
    }
}

# 测试查询功能

@Test
public void test() throws Exception {
    // 1.读取配置文件信息
    InputStream is = Resources.getResourceAsSteam("sqlMapConfig.xml");
    // 2.解析配置文件封装成JavaBean对象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
    // 3.配置执行器及默认实现DefaultSqlSessionFactory
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 4.通过namespace+id,找到MapperStatement(其中存放SQL语句),再调用执行器完成查询操作
    List<User> userList = sqlSession.selectList("user.selectList", null);
    for (User user : userList) {
        System.out.println(user);
    }
}

# 整理回顾

自研Mybatis框架类图分析

上次更新: 4 个月前