手写简易 MyBatis 框架(mini-mybatis)—— 完善版架构设计与核心实现

文档版本 :v2.0
目标 :从零构建轻量级 ORM 框架,深度复现 MyBatis 核心运作机制,并集成 Spring Boot 自动装配。
适用读者:掌握 MyBatis 源码原理、Spring 扩展机制的 Java 高级开发者。


文档导航

  1. 引言与目标
  2. 总体架构设计
  3. 模块分包与依赖关系
  4. 配置与元信息设计
  5. 注解与双轨解析合并策略
  6. [Mapper 代理层设计](#Mapper 代理层设计 "#6-mapper-%E4%BB%A3%E7%90%86%E5%B1%82%E8%AE%BE%E8%AE%A1")
  7. [SqlSession 与 Executor 执行器体系](#SqlSession 与 Executor 执行器体系 "#7-sqlsession-%E4%B8%8E-executor-%E6%89%A7%E8%A1%8C%E5%99%A8%E4%BD%93%E7%B3%BB")
  8. [StatementHandler 与参数/结果处理器](#StatementHandler 与参数/结果处理器 "#8-statementhandler-%E4%B8%8E%E5%8F%82%E6%95%B0%E7%BB%93%E6%9E%9C%E5%A4%84%E7%90%86%E5%99%A8")
  9. 一级缓存设计与缓存键
  10. 插件拦截链实现
  11. 类型处理器体系
  12. 异常体系与资源安全
  13. [Spring Boot 自动配置整合](#Spring Boot 自动配置整合 "#13-spring-boot-%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE%E6%95%B4%E5%90%88")
  14. 单元测试设计
  15. 关键流程时序图(文字描述)
  16. 总结与扩展空间

1. 引言与目标

在原学习系列中,我们系统掌握了 MyBatis 的核心架构:SqlSession 门面、Executor 执行引擎、StatementHandler JDBC 封装、ParameterHandler 参数映射、ResultSetHandler 结果映射、一级/二级缓存、插件拦截链、TypeHandler 类型转换以及 Spring Boot 无缝整合原理。本项目的目标是以"建造者 + 代理 + 模板方法 + 责任链 + 策略"的设计模式组合,亲手实现一个名为 mini-mybatis 的简化框架,并通过与 Spring 容器的集成,打通从接口调用到数据库交互的完整链路。

项目提供两大模块:

  • mini-mybatis-core:核心框架,纯 Java 实现,无任何 Spring 依赖。
  • mini-mybatis-spring-boot-starter :基于 Spring Boot 2.x/3.x 自动配置,提供 @MyMapperScanFactoryBean 动态代理注入。

2. 总体架构设计

2.1 核心分层与职责

text 复制代码
┌──────────────────────────────────────┐
│          Mapper 接口                 │
│ (项目定义,@Select,@Insert等)        │
└────────────┬─────────────────────────┘
             │ 代理拦截
      ┌──────▼──────────────────────┐
      │  MapperProxy               │  动态代理层:方法→StatementId→SqlSession
      └──────┬──────────────────────┘
             │ 获取MappedStatement
      ┌──────▼──────────────────────┐
      │  SqlSession                │  会话门面:query/update/commit/rollback
      └──────┬──────────────────────┘
             │ 委托Executor
      ┌──────▼──────────────────────┐
      │  Executor (插件包装)       │  执行器:一级缓存、事务、批处理
      └──────┬──────────────────────┘
             │ 创建StatementHandler
      ┌──────▼──────────────────────┐
      │  StatementHandler          │  JDBC封装:prepare/parameterize/query/update
      └──┬──────────┬──────────────┘
         │          │
   ┌─────▼──┐  ┌───▼──────────┐
   │Parameter│  │ResultSet     │
   │Handler  │  │Handler       │
   └─────────┘  └──────────────┘
        │              │
   ┌────▼──┐    ┌─────▼──────┐
   │TypeHandler│  │TypeHandler│
   └──────────┘  └─────────────┘

2.2 核心类图(概要)

sql 复制代码
+-------------------+        +---------------------+
|   Configuration   |<>------|  MappedStatement    |
| - dataSource      |        | - id                |
| - mappedStatements|        | - sql               |
| - interceptorChain|        | - sqlCommandType    |
| - typeHandlerReg  |        | - resultType        |
+-------------------+        | - parameterMappings |
                             +---------------------+
+-------------------+        +---------------------+
|    SqlSession     |<>------|      Executor       |
+-------------------+        +---------------------+
| - configuration   |        | + query()           |
| - executor        |        | + update()          |
+-------------------+        +---------------------+
                                      |
                 +--------------------+-----------------+
                 |                    |                 |
         SimpleExecutor      BaseExecutor       BatchExecutor
               |
   +-----------+------------+
   |  StatementHandler      |
   |  - prepare()           |
   |  - parameterize()      |
   |  - query() / update()  |
   +-----------+------------+
               |
   +-----------+------------+
   | PreparedStatementHandler|
   +-------------------------+

整个系统以 Configuration 为中心,保存所有解析结果,并由它创建经过插件链包装的 ExecutorStatementHandler


3. 模块分包与依赖关系

mini-mybatis-core 包结构:

go 复制代码
com.mini.mybatis
├── binding          // MapperProxy、MapperProxyFactory
├── builder          // XMLConfigBuilder、XMLMapperBuilder、MapperAnnotationBuilder
├── cache            // 缓存key、CacheKey(后续扩展二级缓存)
├── datasource       // 内置简单数据源
├── executor         // Executor、BaseExecutor、SimpleExecutor、BatchExecutor
│   ├── parameter    // ParameterHandler接口及默认实现
│   ├── resultset    // ResultSetHandler接口及默认实现
│   └── statement    // StatementHandler接口及PreparedStatementHandler
├── io               // 资源读取工具
├── mapping          // MappedStatement、ParameterMapping、SqlCommandType
├── plugin           // Interceptor、@Intercepts、@Signature、Plugin、InterceptorChain
├── session          // SqlSession、SqlSessionFactory、DefaultSqlSession、DefaultSqlSessionFactory
├── transaction      // Transaction接口及JDBC实现
└── type             // TypeHandler接口及各实现类

mini-mybatis-spring-boot-starter 包结构:

arduino 复制代码
com.mini.mybatis.spring
├── annotation        // @MyMapperScan
├── config            // MiniMybatisAutoConfiguration
├── scanner           // ClassPathMapperScanner
├── registrar         // MiniMybatisRegistrar
├── factory           // MiniMapperFactoryBean、MiniSqlSessionFactoryBean
└── properties        // MiniMybatisProperties

Maven 依赖关系:starter 模块依赖 core 模块,并引入 spring-boot-starterspring-boot-autoconfigure,可选 spring-boot-configuration-processor


4. 配置与元信息设计

4.1 MappedStatement 完整定义

java 复制代码
public class MappedStatement {
    private String id;                        // namespace.methodName
    private String sql;                       // 原始SQL
    private SqlCommandType sqlCommandType;    // SELECT/INSERT/UPDATE/DELETE
    private Class<?> resultType;              // 简单返回类型
    private String resultMapId;               // 关联ResultMap的id(后续扩展)
    private List<ParameterMapping> parameterMappings; // 参数映射
    // getters & setters...
}

public enum SqlCommandType {
    SELECT, INSERT, UPDATE, DELETE
}

4.2 ParameterMapping

java 复制代码
public class ParameterMapping {
    private String property;          // 属性名(如 "id")
    // 暂未实现完整的 TypeHandler 指定,默认使用自动推断的类型处理器
}

4.3 Configuration 增加细节

java 复制代码
public class Configuration {
    private DataSource dataSource;
    private Map<String, MappedStatement> mappedStatements = new HashMap<>();
    private InterceptorChain interceptorChain = new InterceptorChain();
    private Map<Class<?>, TypeHandler<?>> typeHandlerRegistry = new HashMap<>();
    private boolean mapUnderscoreToCamelCase = true; // 下划线转驼峰开关

    public Configuration() {
        // 注册默认类型处理器
        registerDefaultTypeHandlers();
    }

    // 核心工厂方法:创建Executor并应用插件
    public Executor newExecutor(Transaction transaction) {
        Executor executor = new SimpleExecutor(this, transaction);
        // 如果启用批量,可以在全局配置指定,此处简化为每次都创建SimpleExecutor
        return (Executor) interceptorChain.pluginAll(executor);
    }

    // 创建StatementHandler,同样应用插件
    public StatementHandler newStatementHandler(MappedStatement ms, Object parameter) {
        StatementHandler handler = new PreparedStatementHandler(this, ms, parameter);
        return (StatementHandler) interceptorChain.pluginAll(handler);
    }

    // 注册默认类型处理器
    private void registerDefaultTypeHandlers() {
        typeHandlerRegistry.put(Integer.class, new IntegerTypeHandler());
        typeHandlerRegistry.put(int.class, new IntegerTypeHandler());
        typeHandlerRegistry.put(String.class, new StringTypeHandler());
        typeHandlerRegistry.put(Date.class, new DateTypeHandler());
        // 其他...
    }
}

5. 注解与双轨解析合并策略

5.1 注解定义(补充 @Param)

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {
    String value();
}

5.2 XML 映射文件格式示例

xml 复制代码
<mapper namespace="com.example.mapper.UserMapper">
    <select id="selectById" resultType="com.example.domain.User">
        SELECT id, user_name, age FROM users WHERE id = ?
    </select>
    <insert id="insertUser" parameterType="com.example.domain.User">
        INSERT INTO users (id, user_name, age) VALUES (?, ?, ?)
    </insert>
</mapper>

5.3 XMLMapperBuilder 解析细节

解析 resultType 时,将字符串转为 Class。对于 insert/update/delete,可以配置 parameterType,构造 ParameterMapping 列表(此处暂时按顺序映射,后续完善)。

5.4 MapperAnnotationBuilder 解析与合并策略

合并规则定义

  • statementId(namespace + "." + 方法名)为唯一标识。
  • 如果 XML 中已存在相同的 statementId,则以 XML 的配置为基础,注解仅作为补充,且 SQL 语句以注解为准(业务场景中注解更方便修改)。也可以反过来,需要明确设计。
  • 假设采用 注解 SQL 优先,XML 补全其它元信息 的策略(更符合现代开发习惯)。
  • 具体合并原则:
    • id:两者一致。
    • sql:注解中的 SQL 直接设置(覆盖 XML 中的)。
    • sqlCommandType:由注解类型决定。
    • resultType:如果注解方法返回类型不是 void/List 且 XML 未设置,则从方法返回值推导;如果 XML 已设置,保留 XML 的结果类型(因为 XML 可能用 resultMap,高于简单 resultType)。
    • parameterType:根据方法参数和 @Param 注解构建 parameterMappings,但 XML 中的 parameterType 作为备用。

合并代码示例见 MapperAnnotationBuilder

java 复制代码
public void parse(Class<?> mapperInterface, Configuration config) {
    String namespace = mapperInterface.getName();
    for (Method method : mapperInterface.getMethods()) {
        String id = namespace + "." + method.getName();
        MappedStatement ms = config.getMappedStatement(id);
        boolean isNew = (ms == null);
        if (isNew) {
            ms = new MappedStatement();
            ms.setId(id);
        }
        // 解析注解
        if (method.isAnnotationPresent(Select.class)) {
            String sql = method.getAnnotation(Select.class).value().trim();
            ms.setSql(sql);   // 覆盖XML中的SQL
            ms.setSqlCommandType(SqlCommandType.SELECT);
        } else if (method.isAnnotationPresent(Insert.class)) {
            ms.setSql(method.getAnnotation(Insert.class).value().trim());
            ms.setSqlCommandType(SqlCommandType.INSERT);
        } // Update, Delete 类似...
        
        // 处理结果类型:如果XML未设,从方法返回类型推导
        if (ms.getResultType() == null) {
            Class<?> returnType = method.getReturnType();
            if (returnType != void.class && !List.class.isAssignableFrom(returnType)) {
                ms.setResultType(returnType);
            } else if (List.class.isAssignableFrom(returnType)) {
                // 可通过泛型推导,此处简化忽略
            }
        }
        // 构建参数映射(根据@Param注解和方法参数名)
        if (ms.getParameterMappings() == null || ms.getParameterMappings().isEmpty()) {
            ms.setParameterMappings(buildParameterMappings(method));
        }
        config.addMappedStatement(ms);
    }
}

这种方法使框架同时支持"XML 为主,注解为辅"和"纯注解"模式,并明确了冲突解决规则。


6. Mapper 代理层设计

6.1 MapperProxy 增加异常处理与事务控制

java 复制代码
public class MapperProxy implements InvocationHandler {
    private Configuration configuration;
    private SqlSession sqlSession;

    // 通过工厂方法创建,sqlSession可以在每次调用时新创建,也可以复用
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        SqlSession session = null;
        try {
            session = new DefaultSqlSession(configuration);
            // 参数封装(支持无参、单参、多参@Param)
            Object param = wrapParameter(method, args);
            String statementId = configuration.getMapperStatementId(method);
            MappedStatement ms = configuration.getMappedStatement(statementId);
            if (ms == null) {
                throw new MiniMybatisException("No statement found for " + statementId);
            }
            switch (ms.getSqlCommandType()) {
                case SELECT:
                    if (method.getReturnType().isAssignableFrom(List.class)) {
                        return session.selectList(statementId, param);
                    } else {
                        return session.selectOne(statementId, param);
                    }
                case INSERT:
                case UPDATE:
                case DELETE:
                    return session.update(statementId, param);
                default:
                    throw new MiniMybatisException("Unsupported command type");
            }
        } catch (Exception e) {
            throw new MiniMybatisException("Error executing mapper method: " + method.getName(), e);
        } finally {
            if (session != null) session.close();
        }
    }

    // 参数包装:若多个参数,根据@Param名称组成Map;单参数直接传递
    private Object wrapParameter(Method method, Object[] args) {
        if (args == null || args.length == 0) return null;
        if (args.length == 1) return args[0];
        Map<String, Object> paramMap = new HashMap<>();
        Annotation[][] annos = method.getParameterAnnotations();
        for (int i = 0; i < args.length; i++) {
            String name = "arg" + i; // 默认名
            for (Annotation a : annos[i]) {
                if (a instanceof Param) name = ((Param) a).value();
            }
            paramMap.put(name, args[i]);
        }
        return paramMap;
    }
}

MapperProxyFactory 保持不变,提供静态方法创建代理。


7. SqlSession 与 Executor 执行器体系

7.1 Transaction 接口与连接管理

为彻底解决"连接从哪来"的问题,引入 Transaction

java 复制代码
public interface Transaction {
    Connection getConnection() throws SQLException;
    void commit() throws SQLException;
    void rollback() throws SQLException;
    void close() throws SQLException;
}

public class JdbcTransaction implements Transaction {
    private Connection connection;
    private DataSource dataSource;
    private boolean autoCommit;

    public JdbcTransaction(DataSource ds, boolean autoCommit) {
        this.dataSource = ds;
        this.autoCommit = autoCommit;
    }

    @Override
    public Connection getConnection() throws SQLException {
        if (connection == null) {
            connection = dataSource.getConnection();
            connection.setAutoCommit(autoCommit);
        }
        return connection;
    }

    @Override
    public void commit() throws SQLException {
        if (connection != null && !autoCommit) connection.commit();
    }

    @Override
    public void rollback() throws SQLException {
        if (connection != null && !autoCommit) connection.rollback();
    }

    @Override
    public void close() throws SQLException {
        if (connection != null) connection.close();
    }
}

7.2 Executor 实现调整

BaseExecutor 除了缓存,还持有 Transaction,子类通过 transaction.getConnection() 获取连接。

java 复制代码
public abstract class BaseExecutor implements Executor {
    protected Configuration configuration;
    protected Transaction transaction;
    private Map<String, Object> localCache = new HashMap<>();

    protected abstract <E> List<E> doQuery(String statementId, Object parameter) throws Exception;
    protected abstract int doUpdate(String statementId, Object parameter) throws Exception;

    @Override
    public <E> List<E> query(String statementId, Object parameter) throws Exception {
        // 生成缓存键
        CacheKey cacheKey = createCacheKey(statementId, parameter);
        if (localCache.containsKey(cacheKey)) {
            System.out.println("一级缓存命中: " + cacheKey);
            return (List<E>) localCache.get(cacheKey);
        }
        List<E> result = doQuery(statementId, parameter);
        localCache.put(cacheKey, result);
        return result;
    }

    @Override
    public int update(String statementId, Object parameter) throws Exception {
        localCache.clear();
        return doUpdate(statementId, parameter);
    }

    @Override
    public void commit(boolean required) throws SQLException {
        if (required) transaction.commit();
    }

    @Override
    public void rollback(boolean required) throws SQLException {
        if (required) transaction.rollback();
    }

    @Override
    public void close(boolean forceRollback) throws SQLException {
        if (forceRollback) transaction.rollback();
        transaction.close();
    }

    private CacheKey createCacheKey(String statementId, Object parameter) {
        // 简化的哈希键实现
        return new CacheKey(statementId, parameter);
    }
}

7.3 CacheKey 设计

避免直接用 statementId + parameter 字符串拼接导致的不准确问题:

java 复制代码
public class CacheKey {
    private String statementId;
    private Object parameter;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CacheKey)) return false;
        CacheKey cacheKey = (CacheKey) o;
        return Objects.equals(statementId, cacheKey.statementId) &&
               Objects.equals(parameter, cacheKey.parameter);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(statementId, parameter);
    }
    // 构造器、getter
}

注意 :若参数为可变对象且包含业务数据,需保证其 equals/hashCode 被正确重写,否则缓存可能失效或误命中。实际 MyBatis 使用 CacheKey 组合多个值。

SimpleExecutordoQuery 中获取连接,创建 StatementHandler,设置参数并执行:

java 复制代码
@Override
protected <E> List<E> doQuery(String statementId, Object parameter) throws Exception {
    MappedStatement ms = configuration.getMappedStatement(statementId);
    StatementHandler handler = configuration.newStatementHandler(ms, parameter);
    handler.prepare(transaction.getConnection());
    handler.parameterize(parameter);
    return handler.query();
}

8. StatementHandler 与参数/结果处理器

8.1 PreparedStatementHandler 完善资源释放

java 复制代码
public class PreparedStatementHandler implements StatementHandler {
    private Configuration configuration;
    private MappedStatement mappedStatement;
    private Object parameter;
    private PreparedStatement ps;

    @Override
    public void prepare(Connection conn) throws SQLException {
        this.ps = conn.prepareStatement(mappedStatement.getSql());
    }

    @Override
    public void parameterize(Object param) {
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, param);
        parameterHandler.setParameters(ps);
    }

    @Override
    public <E> List<E> query() throws SQLException {
        ResultSet rs = ps.executeQuery();
        try {
            ResultSetHandler resultSetHandler = new DefaultResultSetHandler(configuration, mappedStatement);
            return resultSetHandler.handleResultSets(rs);
        } finally {
            if (rs != null) rs.close();
            if (ps != null) ps.close();
        }
    }

    @Override
    public int update() throws SQLException {
        try {
            return ps.executeUpdate();
        } finally {
            if (ps != null) ps.close();
        }
    }
}

8.2 DefaultParameterHandler 支持 @Param 和 Map 参数

java 复制代码
public class DefaultParameterHandler implements ParameterHandler {
    private MappedStatement mappedStatement;
    private Object parameter;

    @Override
    public void setParameters(PreparedStatement ps) {
        if (parameter == null) return;
        List<ParameterMapping> mappings = mappedStatement.getParameterMappings();
        if (mappings != null && !mappings.isEmpty()) {
            // 根据映射设置参数
            for (int i = 0; i < mappings.size(); i++) {
                ParameterMapping pm = mappings.get(i);
                Object value = getValueByProperty(pm.getProperty());
                TypeHandler handler = getTypeHandler(value);
                handler.setParameter(ps, i + 1, value);
            }
        } else {
            // 没有映射信息,根据参数类型简单处理
            if (parameter instanceof Map) {
                // 按占位符顺序?这里暂时忽略
            } else {
                // 单个参数自动匹配
                TypeHandler handler = getTypeHandler(parameter);
                handler.setParameter(ps, 1, parameter);
            }
        }
    }

    private Object getValueByProperty(String property) {
        // 如果是Map,直接get;如果是JavaBean,使用反射获取字段值
        if (parameter instanceof Map) return ((Map) parameter).get(property);
        try {
            Field f = parameter.getClass().getDeclaredField(property);
            f.setAccessible(true);
            return f.get(parameter);
        } catch (Exception e) {
            return null;
        }
    }
}

8.3 DefaultResultSetHandler 下划线转驼峰与类型处理

java 复制代码
public class DefaultResultSetHandler implements ResultSetHandler {
    private Configuration configuration;
    private MappedStatement mappedStatement;

    @Override
    public <E> List<E> handleResultSets(ResultSet rs) throws SQLException {
        List<E> results = new ArrayList<>();
        Class<?> resultClass = mappedStatement.getResultType();
        while (rs.next()) {
            E obj = (E) resultClass.getDeclaredConstructor().newInstance();
            ResultSetMetaData meta = rs.getMetaData();
            int cols = meta.getColumnCount();
            for (int i = 1; i <= cols; i++) {
                String columnName = meta.getColumnLabel(i);
                String propertyName = columnToProperty(columnName);
                try {
                    Field field = findFieldRecursive(resultClass, propertyName);
                    if (field != null) {
                        field.setAccessible(true);
                        TypeHandler<?> handler = configuration.getTypeHandler(field.getType());
                        Object value = handler.getResult(rs, columnName);
                        field.set(obj, value);
                    }
                } catch (NoSuchFieldException e) {
                    // 字段不存在,忽略
                }
            }
            results.add(obj);
        }
        return results;
    }

    private String columnToProperty(String column) {
        if (configuration.isMapUnderscoreToCamelCase()) {
            // 简单实现下划线转驼峰:user_name => userName
            StringBuilder sb = new StringBuilder();
            boolean nextUpper = false;
            for (char c : column.toCharArray()) {
                if (c == '_') { nextUpper = true; continue; }
                sb.append(nextUpper ? Character.toUpperCase(c) : c);
                nextUpper = false;
            }
            return sb.toString();
        }
        return column;
    }
}

9. 一级缓存设计与缓存键

一级缓存是 MyBatis 会话级缓存的核心实现,位于 BaseExecutor 中,生命周期与 SqlSession 绑定。它的设计目标是在同一个数据库会话中,对于相同 SQL 和参数的查询操作,能够直接返回缓存结果,从而减少数据库访问,提升性能。同时,缓存必须保证在数据发生变更(INSERT/UPDATE/DELETE)时被及时清空,以避免脏读。

9.1 缓存的生命周期与作用域

在 mini-mybatis 中,每个 SqlSession 都会创建一个新的 Executor(通过 Configuration.newExecutor() 实例化)。BaseExecutor 内部维护一个 Map<CacheKey, Object> 类型的 localCache,该缓存在 Executor 关闭前一直有效,因此一级缓存的作用范围严格限制在一个 SqlSession 内,不同 SqlSession 之间的缓存相互隔离。

当用户执行查询时,Executor 会首先尝试从 localCache 中获取结果。若命中,则直接返回缓存的对象;若未命中,则执行实际的数据库查询,并将查询结果存入缓存。当用户在同一会话内执行任何更新操作(INSERT、UPDATE、DELETE)时,BaseExecutor.update 方法会首先清空 localCache(调用 localCache.clear()),然后再执行实际的数据库写操作。这种"修改即清空"的策略确保了缓存的强一致性。

9.2 缓存键 CacheKey 的设计

准确生成缓存键是一级缓存正确工作的关键。如果缓存键不能唯一标识一次查询,就可能导致缓存命中错误或缓存失效。原始版本中使用 statementId + ":" + parameter.toString() 的方式过于粗糙,存在以下问题:

  1. 哈希碰撞与相等性 :参数对象的 toString() 方法不一定能体现对象内容的相等性。例如,两个不同的对象可能产生相同的 toString() 结果,或者本应相等的对象由于没有重写 toString() 而产生不同的字符串。
  2. 参数顺序与类型:如果 SQL 包含多个参数,仅靠字符串拼接无法正确处理参数顺序和类型差异。
  3. 空值处理null 参数的字符串表示容易引起歧义。

在 MyBatis 中,CacheKey 是一个精心设计的类,它会组合 StatementId、参数值、分页信息等多个要素,并使用严格的 equalshashCode 方法。在 mini-mybatis 中,我们简化实现,但依然要保证缓存键的准确性和高效性。

CacheKey 实现

java 复制代码
public class CacheKey {
    private final String statementId;
    private final Object parameter;
    private final int hashCode;

    public CacheKey(String statementId, Object parameter) {
        this.statementId = statementId;
        this.parameter = parameter;
        this.hashCode = Objects.hash(statementId, parameter);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CacheKey)) return false;
        CacheKey cacheKey = (CacheKey) o;
        return Objects.equals(statementId, cacheKey.statementId) &&
               Objects.equals(parameter, cacheKey.parameter);
    }

    @Override
    public int hashCode() {
        return this.hashCode;
    }

    @Override
    public String toString() {
        return "CacheKey{" + statementId + ":" + parameter + "}";
    }
}

设计要点

  • immutableCacheKey 被设计为不可变对象,hashCode 在构造时就计算完成并缓存,这显著提高了作为 HashMap 键时的性能。
  • 基于 Objects.equals/hash :利用 Java 工具方法处理任意对象的相等性和哈希,包括 null 安全。
  • 参数对象的契约 :要求作为 parameter 的对象必须正确实现 equalshashCode。如果参数是简单类型(IntegerStringDate 等),它们已经满足这一要求。如果参数是自定义的 JavaBean,则需要使用者确保其正确性,否则可能影响缓存命中。

在实际使用中,对于多参数查询,我们已经在 MapperProxy 中将多个参数包装为一个 Map<String, Object>(按 @Param 命名),而 HashMapequals/hashCode 是基于其条目内容的,因此只要参数键值对相同,就能生成相同的缓存键,这一点符合预期。

9.3 缓存读写流程

查询时

java 复制代码
@Override
public <E> List<E> query(String statementId, Object parameter) throws Exception {
    CacheKey cacheKey = new CacheKey(statementId, parameter);
    @SuppressWarnings("unchecked")
    List<E> cached = (List<E>) localCache.get(cacheKey);
    if (cached != null) {
        System.out.println("一级缓存命中: " + cacheKey);
        return cached;
    }
    List<E> result = doQuery(statementId, parameter);
    localCache.put(cacheKey, result);
    return result;
}

更新时

java 复制代码
@Override
public int update(String statementId, Object parameter) throws Exception {
    localCache.clear();  // 清空整个一级缓存
    return doUpdate(statementId, parameter);
}

为什么是清空整个缓存而不是移除对应的键?

一个更新操作可能会影响多张表,而 mini-mybatis 还无法追踪哪些缓存键与哪些表相关(这需要元数据映射,成本极高)。直接清空整个缓存是最简单且绝对安全的做法,代价是降低了缓存的利用率。真实 MyBatis 也是如此处理,除非用户显式配置了二级缓存并开启了细粒度的失效机制。

9.4 一级缓存与事务隔离性

一级缓存是会话级别的,它不会跨会话共享,因此天然满足"可重复读"的隔离性:同一会话内多次读取同一行数据,只要没有在此期间执行更新操作,返回的结果一致。但需要注意,如果另一个会话修改了数据库,当前会话的缓存并不会自动失效,这可能导致读取到陈旧数据。在 MyBatis 的标准使用中,这是可以接受的,因为一级缓存的寿命通常很短(一次请求的范围),而请求结束 SqlSession 即关闭。在批量操作或长时间运行的会话中,应避免过度依赖一级缓存,或者在关键查询后手动清除缓存。

9.5 缓存统计与调试

为了方便开发者观察缓存效果,可以在 BaseExecutor 中添加统计信息(命中次数、未命中次数),并通过日志或 JMX 暴露。鉴于 mini-mybatis 是学习型框架,当前只通过控制台输出来提示缓存命中,但实际生产框架通常会集成到统一的日志框架中。

9.6 扩展:向二级缓存演进的思考

一级缓存是请求级别的,而二级缓存是进程级别的,可以跨 SqlSession 共享。在 mini-mybatis 中,我们可以在 BaseExecutor 中预留装饰器接口,当开启了二级缓存时,用 CachingExecutor 包装 SimpleExecutor,在 query 方法中先查二级缓存,再查一级缓存,最后查数据库,而 update 时同时刷新二级缓存和清空一级缓存。这一扩展点已通过 Executor 的装饰器模式(插件也是装饰器)提供了可能。


10. 插件拦截链实现

10.1 Plugin 包装优化

MyBatis 中的 Plugin.wrap 会为目标对象创建一个代理,代理对象实现目标对象的所有接口。之前的实现只取了 type.getInterfaces(),如果目标对象是具体的 SimpleExecutor,其实现的 Executor 接口在 type.getInterfaces() 中通常会出现(因为 SimpleExecutor 直接实现了 Executor),但更安全的方式是收集所有父接口。

java 复制代码
public static Object wrap(Object target, Interceptor interceptor) {
    Class<?> targetClass = target.getClass();
    Intercepts intercepts = interceptor.getClass().getAnnotation(Intercepts.class);
    if (intercepts == null) return target;
    
    // 收集所有接口
    Set<Class<?>> interfaces = new HashSet<>();
    getAllInterfaces(targetClass, interfaces);
    
    for (Signature sig : intercepts.value()) {
        if (sig.type().isAssignableFrom(targetClass)) {
            try {
                Method method = targetClass.getMethod(sig.method(), sig.args());
                Plugin plugin = new Plugin(target, interceptor);
                plugin.signatureMap.put(method, true);
                return Proxy.newProxyInstance(
                        targetClass.getClassLoader(),
                        interfaces.toArray(new Class<?>[0]),
                        plugin);
            } catch (NoSuchMethodException ignored) {}
        }
    }
    return target;
}

private static void getAllInterfaces(Class<?> clazz, Set<Class<?>> result) {
    if (clazz == null || clazz == Object.class) return;
    for (Class<?> iface : clazz.getInterfaces()) {
        result.add(iface);
    }
    getAllInterfaces(clazz.getSuperclass(), result);
}

10.2 示例拦截器

LoggingInterceptorPerformanceInterceptor(测量执行时间),演示多拦截器嵌套:

java 复制代码
@Intercepts(@Signature(type = Executor.class, method = "query", args = {String.class, Object.class}))
public class PerformanceInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        long time = System.currentTimeMillis() - start;
        System.out.println("方法执行耗时:" + time + "ms");
        return result;
    }
}

通过 InterceptorChain 链式包装,最终生成的 Executor 对象实际上是 Plugin(Plugin(SimpleExecutor, Logging), Performance) 这样的嵌套结构。


11. 类型处理器体系

完整展示一个类型处理器的注册与使用。除了基本类型,增加对 java.util.Date 的支持,内部分别处理 java.sql.Date

java 复制代码
public class DateTypeHandler implements TypeHandler<Date> {
    @Override
    public void setParameter(PreparedStatement ps, int i, Date parameter) throws SQLException {
        ps.setTimestamp(i, new java.sql.Timestamp(parameter.getTime()));
    }

    @Override
    public Date getResult(ResultSet rs, String columnName) throws SQLException {
        Timestamp ts = rs.getTimestamp(columnName);
        return ts != null ? new Date(ts.getTime()) : null;
    }
}

Configuration 提供 getTypeHandler(Class<?> type) 方法,根据类型返回对应的处理器。若未找到,可抛出异常或使用默认的 Object 序列化处理器(此处省略)。


12. 异常体系与资源安全

定义统一的异常 MiniMybatisException extends RuntimeException,包装底层异常,避免向调用方暴露 SQLException

所有涉及到资源开启的地方(Connection、PreparedStatement、ResultSet)均使用 try-finally 确保关闭。在 StatementHandler 执行完毕后立即释放语句和结果集,Executor 关闭时释放 Connection(通过 Transaction 的关闭)。


13. Spring Boot 自动配置整合

13.1 MiniMybatisRegistrar 与扫描器

ClassPathMapperScanner 扩展自 ClassPathBeanDefinitionScanner,覆盖 isCandidateComponent 只接受接口;并重写 doScan,在扫描到的 BeanDefinition 中设置 beanClassMiniMapperFactoryBean,并通过构造器参数传入接口类型。

java 复制代码
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
    for (BeanDefinitionHolder holder : beanDefinitions) {
        GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
        // 将原来的接口作为构造参数传给 MiniMapperFactoryBean
        definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
        definition.setBeanClass(MiniMapperFactoryBean.class);
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
    return beanDefinitions;
}

13.2 MiniMapperFactoryBean 注入 SqlSession

java 复制代码
public class MiniMapperFactoryBean<T> implements FactoryBean<T>, ApplicationContextAware {
    private Class<T> mapperInterface;
    private SqlSession sqlSession;

    public MiniMapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        return sqlSession.getMapper(mapperInterface);
    }

    @Override
    public Class<?> getObjectType() { return mapperInterface; }

    @Override
    public void setApplicationContext(ApplicationContext ctx) {
        // 从容器中获取 SqlSession,通常为单例
        this.sqlSession = ctx.getBean(SqlSession.class);
    }
}

13.3 MiniSqlSessionFactoryBean 与 InitializingBean

afterPropertiesSet() 中解析 xml 配置、注解,构建 Configuration,实例化 SqlSessionFactory。Spring 容器中再定义 @Bean 方法生成 SqlSessionDefaultSqlSession 打开一个会话,但通常 Mapper 代理每次请求新会话,所以此处需要设计为 SqlSessionTemplate 线程安全变体,或扫描器注入的 Mapper 每次调用创建新 SqlSession。为简化,MiniMapperFactoryBean 中获取的 SqlSession 可以是一个"会话工厂",getMapper 返回的代理在内部每次创建新会话。我们调整设计:在 Spring 环境中,Mapper 代理的方法每次执行时都新建 SqlSession,这需要修改 MapperProxy 使其每次从 SqlSessionFactory 获取会话。可将 MapperProxy 改为持有 SqlSessionFactory 而不是 SqlSession

完善:MapperProxy 每次调用时创建 SqlSession(通过 sqlSessionFactory.openSession()),在 finally 中关闭。Spring 容器中暴露 SqlSessionFactory bean,MiniMapperFactoryBean 不再直接持有 SqlSession,而是注入 SqlSessionFactory,在 getObject 返回代理时,MapperProxy 内部使用该工厂。

13.4 自动配置类调整

java 复制代码
@Configuration
@ConditionalOnClass({DataSource.class, SqlSessionFactory.class})
@EnableConfigurationProperties(MiniMybatisProperties.class)
public class MiniMybatisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MiniSqlSessionFactoryBean sqlSessionFactory(DataSource dataSource, MiniMybatisProperties props) {
        MiniSqlSessionFactoryBean factory = new MiniSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setConfigLocation(props.getConfigLocation());
        factory.setMapperLocations(props.getMapperLocations());
        return factory;
    }

    // 注册一个SqlSessionFactory,便于其他地方获取
    @Bean
    public SqlSessionFactory sqlSessionFactoryObj(MiniSqlSessionFactoryBean factoryBean) throws Exception {
        return factoryBean.getObject();
    }
}

MiniMybatisProperties 读取 mini.mybatis 前缀下的配置。

spring.factories / AutoConfiguration.imports 文件中添加:

复制代码
com.mini.mybatis.spring.MiniMybatisAutoConfiguration

14. 单元测试设计

14.1 Mapper 代理与 SQL 执行测试(H2 内存数据库)

java 复制代码
@Test
public void testSelectById() throws Exception {
    // 构建Configuration
    Configuration config = new Configuration();
    config.setDataSource(createH2DataSource());
    // 注册Mapper接口和对应的MappedStatement(可由解析器生成)
    MappedStatement ms = new MappedStatement();
    ms.setId("com.test.mapper.UserMapper.selectById");
    ms.setSql("SELECT id, user_name, age FROM users WHERE id = ?");
    ms.setSqlCommandType(SqlCommandType.SELECT);
    ms.setResultType(User.class);
    // 设置参数映射
    ms.setParameterMappings(Collections.singletonList(new ParameterMapping("id")));
    config.addMappedStatement(ms);

    SqlSessionFactory factory = new DefaultSqlSessionFactory(config);
    UserMapper mapper = factory.openSession().getMapper(UserMapper.class);
    User user = mapper.selectById(1);
    assertNotNull(user);
    assertEquals("张三", user.getUserName());
}

14.2 一级缓存测试

java 复制代码
@Test
public void testFirstLevelCache() throws Exception {
    // 以SimpleExecutor执行两次相同查询,第二次应命中缓存
    Executor executor = new SimpleExecutor(config, transaction);
    List<User> list1 = executor.query("...selectById", 1);
    List<User> list2 = executor.query("...selectById", 1);
    assertSame(list1, list2); // 缓存命中,返回同一对象
}

14.3 插件拦截测试

java 复制代码
@Test
public void testPlugin() throws Throwable {
    Configuration config = new Configuration();
    config.getInterceptorChain().addInterceptor(new LoggingInterceptor());
    Executor executor = config.newExecutor(transaction);
    // 捕获输出日志,验证拦截器执行
}

14.4 Spring 整合测试

java 复制代码
@SpringBootTest
@MyMapperScan("com.test.mapper")
public class SpringIntegrationTest {
    @Autowired
    UserMapper userMapper;
    
    @Test
    public void testAutowiredMapper() {
        User user = userMapper.selectById(1);
        assertNotNull(user);
    }
}

15. 关键流程时序图

15.1 一次 SELECT 查询的完整时序图(Mermaid 语法)

sequenceDiagram actor C as 调用者 participant MP as MapperProxy participant SS as DefaultSqlSession participant EX as BaseExecutor
(含插件包装) participant SH as PreparedStatementHandler
(StatementHandler) participant PH as DefaultParameterHandler
(ParameterHandler) participant RH as DefaultResultSetHandler
(ResultSetHandler) participant TX as JdbcTransaction participant DB as Database C->>MP: selectById(1) MP->>MP: wrapParameter(method, args) → param MP->>MP: 获取 statementId MP->>SS: new DefaultSqlSession(configuration) MP->>SS: selectOne(statementId, param) SS->>EX: query(statementId, param) Note over EX: 生成 CacheKey EX->>EX: 查本地缓存 localCache.get(cacheKey) alt 缓存命中 EX-->>SS: 返回缓存结果列表 else 缓存未命中 EX->>EX: doQuery(statementId, param) EX->>SH: newStatementHandler → handler = new PreparedStatementHandler
(经过插件包装) EX->>TX: getConnection() TX->>DB: 获取连接 DB-->>TX: 返回 Connection TX-->>EX: Connection EX->>SH: prepare(Connection) SH->>SH: conn.prepareStatement(sql) → PreparedStatement EX->>SH: parameterize(param) SH->>PH: new DefaultParameterHandler(statement, param) PH->>PH: 解析 parameterMappings loop 对于每个参数 PH->>PH: 使用 TypeHandler 设置占位符 PH->>DB: ps.setInt/ps.setString等 end EX->>SH: query() SH->>SH: ps.executeQuery() → ResultSet SH->>RH: new DefaultResultSetHandler(config, statement) RH->>RH: handleResultSets(rs) loop 遍历 ResultSet 行 RH->>RH: 反射创建结果对象 loop 遍历列 RH->>RH: columnToProperty(下划线转驼峰) RH->>RH: TypeHandler.getResult(rs, columnName) end end RH-->>SH: 返回结果列表 SH-->>EX: 返回结果列表 EX->>EX: localCache.put(cacheKey, result) end EX-->>SS: 返回结果列表 SS-->>MP: 返回结果(selectOne 取第一条) MP->>SS: close() SS->>EX: close(false) EX->>TX: close() TX->>DB: 关闭连接 MP-->>C: 返回 User 对象

15.2 详细流程分步说明

下面从调用者视角,逐步拆解一次 UserMapper.selectById(1) 的完整执行路径,与上述时序图严格对应。

第一步:动态代理拦截方法调用

当调用 userMapper.selectById(1) 时,由于 userMapper 实际上是 MapperProxyFactory 生成的 JDK 动态代理对象,JVM 会将此调用转发给 MapperProxy.invoke 方法。

第二步:参数归一化与 StatementId 组装
MapperProxy 通过反射获取调用方法的 Method 对象和参数数组 args

  • 如果方法没有参数,paramnull
  • 如果只有一个参数且没有 @Param 注解,参数直接作为对象传递。
  • 如果有多个参数或使用了 @Param,则根据注解将参数值封装为一个 Map<String, Object>,键为 @Param 的值或默认的 arg0arg1 等。
    最终形成 param 对象,并得到 statementId = 接口全限定名 + "." + 方法名(例如 "com.example.mapper.UserMapper.selectById")。

第三步:创建 SqlSession
MapperProxy 创建一个新的 DefaultSqlSession 实例,此时 DefaultSqlSession 的构造器会调用 configuration.newExecutor(transaction),该方法内部:

  • 创建 SimpleExecutor 实例,传入 Configuration 和一个 JdbcTransaction
  • SimpleExecutor 传入 interceptorChain.pluginAll(),依次遍历已注册的拦截器。如果某个拦截器的 @Signature 匹配 Executor.query,则生成一层 Plugin 代理包装。最终返回的 Executor 对象可能被多层动态代理包裹。

DefaultSqlSession 持有这个经过插件包装的 Executor

第四步:SqlSession 委托 Executor 执行查询
sqlSession.selectOne(statementId, param) 内部调用 executor.query(statementId, param),并由结果列表中取出第一个元素(若结果多于一条则抛异常)。

第五步:Executor 一级缓存检查
BaseExecutor.query 的调用被插件代理拦截,如果有日志或性能监控拦截器,会在此触发拦截逻辑。最终进入 BaseExecutorquery 实现:

  • statementIdparam 构造 CacheKey
  • 调用 localCache.get(cacheKey) 查找一级缓存。
    • 若命中 :直接返回缓存的 List<E>,跳过数据库访问。
    • 若未命中 :执行 doQuery

第六步:SimpleExecutor.doQuery 构建 StatementHandler
SimpleExecutor.doQuery 负责真正的数据库交互:

  1. Configuration 获取 MappedStatement
  2. 调用 configuration.newStatementHandler(ms, param),内部创建 PreparedStatementHandler 实例,并同样经过 interceptorChain.pluginAll() 进行插件包装(拦截器可能拦截 StatementHandler.prepare 等方法)。
  3. 通过 transaction.getConnection() 获取数据库连接。JdbcTransaction 会检查是否已持有连接,若无则从 DataSource 获取并设置 autoCommit
  4. 调用 handler.prepare(connection),创建 PreparedStatement 对象。
  5. 调用 handler.parameterize(param),进行参数设置。

第七步:参数处理(ParameterHandler)
PreparedStatementHandler.parameterize 创建 DefaultParameterHandler,并调用 setParameters(ps)

  • 如果 MappedStatement 中存在 parameterMappings,遍历每个映射:通过反射(或从 Map 中获取)取出对应的参数值,并使用 TypeHandler 设置到 PreparedStatement 的对应占位符上。
  • 如果不存在映射,说明是单参数简单查询,直接通过 TypeHandler 设置第一个占位符。

TypeHandler 根据参数类型,调用 ps.setInt(index, value)ps.setString(index, value) 等 JDBC 方法。

第八步:执行查询并处理结果集
PreparedStatementHandler.query

  1. 执行 ps.executeQuery(),返回 ResultSet
  2. 创建 DefaultResultSetHandler,传入 ConfigurationMappedStatement
  3. 调用 resultSetHandler.handleResultSets(rs)
    • 获取 MappedStatement.resultType,通过反射创建该类型的实例。
    • 遍历 ResultSet 的每一行:
      • 获取 ResultSetMetaData,得到列名。
      • 对每个列名,先转换为驼峰属性名(若配置了 mapUnderscoreToCamelCase)。
      • 在结果类型及其父类中递归查找对应字段。
      • 通过 TypeHandler.getResult(rs, columnName) 提取数据库中的值,并设置到对象的字段上。
    • 将所有对象收集到 List 中返回。
  4. 关闭 ResultSetPreparedStatement

第九步:缓存结果并返回
BaseExecutor.querydoQuery 返回的结果列表存入 localCache(键为 CacheKey),然后返回给 SqlSession

第十步:关闭资源,返回用户代码
SqlSession.selectOne 获取结果并返回到 MapperProxyMapperProxy 随后在 finally 块中关闭 SqlSession

  • sqlSession.close() 实际调用 executor.close(false)
  • executor.close 调用 transaction.close(),进而关闭数据库连接。

以上内容为第 9 部分和第 15 部分的详尽展开,完整覆盖了一级缓存设计细节和一次查询请求的全链路时序与文字说明,并与前文架构设计无缝衔接。

16. 总结与扩展空间

本设计通过 mini-mybatis 的完整实现,深入剖析了 MyBatis 核心组件协作的全过程。框架具备了基本的 ORM 能力、缓存、插件、类型转换和 Spring 整合,虽然与工业级 MyBatis 仍有差距(缺少动态 SQL、二级缓存、ResultMap 嵌套、ScriptRunner、日志系统等),但已覆盖最关键的架构思想。

后续扩展可加入:

  • 动态 SQL 引擎 :解析 <if>, <foreach> 等标签,结合 OGNL 表达式。
  • 二级缓存 :引入 Cache 接口及装饰器(LRU、FIFO),实现跨会话缓存。
  • ResultMap 关联映射<resultMap> 嵌套对象/集合。
  • 更精致的 Spring 整合SqlSessionTemplate 实现线程安全会话管理,支持 @Mapper 注解直接扫描(不依赖 @MyMapperScan)。
相关推荐
敖正炀3 小时前
反模式与排查宝典:MyBatis 常见陷阱与排错指南
mybatis
掘金者阿豪3 小时前
R3play让听歌不再费劲,想咋就咋!
后端
EthanYuan3 小时前
🦴不是MCP害了我,是这个阻塞害了我啊
后端
用户298698530143 小时前
Java 文档处理:在 Word 中插入分页符与分节符
java·后端
fliter3 小时前
分布式聚合查询的工程内幕:Cloudflare R2 SQL 如何实现 GROUP BY
后端
无限进步_3 小时前
【C++】红黑树完全解析:从概念到插入与平衡维护
java·c语言·开发语言·数据结构·c++·后端·算法
MacroZheng3 小时前
狂揽34k star!一款AI编程必不可少的神器,和Claude Code/Codex绝配!
人工智能·后端·claude
阿聪谈架构4 小时前
第09章:AI Skills 技能系统 —— 用能力包管理 Agent 的技能库
人工智能·后端