0%

深入理解 MyBatis

MyBatis(前身是 iBatis)的出现,主要是为了解决传统 JDBC 编程中存在的繁琐性、维护困难以及SQL与代码耦合度高等问题。

具体来说,MyBatis 旨在解决以下痛点:

  • SQL语句与Java代码的紧密耦合: 在传统的 JDBC 编程中,开发者通常需要在 Java 代码中直接拼接 SQL 语句。这导致 SQL 语句的修改需要重新编译 Java 代码,增加了开发和维护成本。尤其当 SQL 语句复杂时,拼接字符串的方式容易出错且可读性差。

  • 资源管理和异常处理的复杂性: JDBC 编程需要手动管理数据库连接、PreparedStatement、ResultSet 等资源,并且需要编写大量的 try-catch-finally 块来处理异常和关闭资源。这部分代码重复且冗余,容易出现资源泄露。

  • 参数设置和结果映射的繁琐: 将 Java 对象属性映射到 SQL 参数,以及将 ResultSet 中的数据映射到 Java 对象,需要大量的手动代码。对于复杂的对象关系,这个过程会变得非常繁琐和枯燥。

  • 可移植性问题: 不同数据库厂商的 SQL 语法可能存在差异,直接在 Java 代码中硬编码 SQL 语句会导致应用程序难以在不同数据库之间移植。

  • 开发效率低下: 上述种种问题导致 JDBC 编程效率低下,开发者需要花费大量时间在编写和维护重复性的代码上,而不是专注于业务逻辑。

MyBatis 通过以下方式有效地解决了这些问题:

  • SQL与Java代码分离: MyBatis 允许将 SQL 语句独立地定义在 XML 文件或注解中,从而将 SQL 语句从 Java 代码中解耦。这使得 SQL 语句的修改无需改动 Java 代码,提高了可维护性,消除了大量 JDBC 冗余代码。
  • 自动化参数映射和结果映射: MyBatis 提供了强大的参数映射和结果映射机制,开发者可以通过简单的配置实现 Java 对象与 SQL 参数、SQL 结果集之间的自动转换,大大减少了手动编写映射代码的工作量。
  • 统一的API和简化资源管理: MyBatis 提供了简洁的 API,封装了 JDBC 的底层操作,自动管理数据库连接、预编译语句等资源,并处理异常,大大简化了开发者的工作。
  • 动态SQL: MyBatis 提供了强大的动态 SQL 功能,允许开发者根据不同的条件构建不同的 SQL 语句,这对于处理复杂的查询逻辑非常有用,并且避免了大量的条件判断代码。
  • 更好的可移植性: 由于 SQL 语句是外部化的,可以根据不同的数据库方言编写不同的 SQL 文件,从而增强了应用程序在不同数据库之间的可移植性。
操作 原生JDBC MyBatis JPA/Hibernate
获取连接 DriverManager/DataSource 由框架管理 由框架管理
SQL执行 手动拼写SQL字符串 XML/注解定义SQL 自动生成SQL
结果映射 手动遍历ResultSet 自动映射到对象 全自动对象映射
事务控制 手动commit/rollback 声明式事务(结合Spring) 声明式事务

原生 JDBC 开发流程

JDBC (Java Database Connectivity) 是 Java 语言访问关系型数据库的标准 API。它提供了一套统一的接口,让开发者能够用 Java 代码来执行 SQL 语句,并处理数据库返回的结果。

JDBC API 中的核心类与接口

JDBC API 主要由以下几个核心组件构成:

  • DriverManager: 这是一个管理 JDBC 驱动程序的类。它负责加载和注册不同的数据库驱动,并根据应用程序提供的 URL 获取数据库连接。

  • Connection: 代表与特定数据库的连接。通过 Connection 对象,你可以创建 Statement 对象,并管理事务。

  • Statement: 用于执行 SQL 语句。Statement 有几个子接口,用于处理不同类型的 SQL:

    • Statement: 用于执行不带参数的静态 SQL 语句。
    • PreparedStatement: 用于执行预编译的 SQL 语句,它支持参数化查询,能有效防止 SQL 注入,并提高执行效率。

SQL 注入:SQL 注入 (SQL Injection) 是一种常见的网络安全漏洞,它发生在应用程序与数据库层。简单来说,就是攻击者通过在应用程序的输入字段中插入恶意的 SQL 代码,欺骗数据库执行非预期的命令,从而窃取、篡改或破坏数据。一个常见的登录查询可能是: SELECT * FROM users WHERE username = '用户输入的用户名' AND password = '用户输入的密码' 如果用户输入 admin' OR '1'='1 作为用户名,而密码为空,那么最终的 SQL 语句可能变成: SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '' 由于 '1'='1' 永远为真,这个查询就会返回 admin 用户的信息,从而绕过密码验证。

  • CallableStatement: 用于执行数据库的存储过程。

  • ResultSet: 表示 SQL 查询的结果集。当你执行 SELECT 语句时,数据库会返回一个结果集,ResultSet 对象允许你逐行、逐列地遍历这些数据。

  • DatabaseMetaData: 提供了关于数据库本身的信息,例如数据库的名称、版本、支持的 SQL 类型等。

  • ResultSetMetaData: 提供了关于 ResultSet 对象中列的信息,例如列名、数据类型等。

MyBatis 核心组件介绍

  • Configuration:Configuration 类承载着 MyBatis 运行时的所有重要配置和元数据。主要作用:管理核心配置、管理 SQL 映射信息、管理结果映射、管理 Mapper 接口、构建 SqlSessionFactory。

  • SqlSessionFactoryBuilder

    • 作用:负责构建 SqlSessionFactory 实例。它通常从 XML 配置文件或 Java 代码中获取配置信息。SqlSessionFactoryBuilder 的生命周期很短,一旦 SqlSessionFactory 被创建,它就可以被销毁。
  • SqlSessionFactory

    • 作用:是创建 SqlSession 的工厂。它是线程安全的,并且在 MyBatis 应用中通常只有一个实例。SqlSessionFactory 一旦被创建,在整个应用运行时都应该存在。
  • SqlSession

    • 作用:是 MyBatis 与数据库交互的会话接口。它包含了执行 SQL 查询、提交事务、回滚事务等方法。SqlSession 不是线程安全的,因此每个线程都应该拥有自己的 SqlSession 实例。它的生命周期应该与请求或业务操作的生命周期保持一致。
  • Executor

    • 作用:是 MyBatis 的内部执行器,负责具体的 SQL 语句的执行和结果集的映射。MyBatis 提供了多种执行器,如简单执行器(SimpleExecutor)、重用执行器(ReuseExecutor)和批量执行器(BatchExecutor)。
  • StatementHandler

    • 作用:负责封装 JDBC Statement 操作,包括设置参数和执行 SQL 语句。它处理预编译语句(PreparedStatement)和可调用语句(CallableStatement)的创建和执行。
  • ParameterHandler

    • 作用:负责处理 SQL 参数的设置。它根据映射文件中定义的参数类型,将 Java 对象属性的值设置到 JDBC PreparedStatement 中。
  • ResultSetHandler

    • 作用:负责处理 SQL 查询结果集的映射。它将 JDBC ResultSet 中的数据映射到 Java 对象中,并处理结果集中的类型转换。
  • TypeHandler

    • 作用:负责 Java 类型和 JDBC 类型之间的转换。MyBatis 内置了许多常用的类型处理器,同时开发者也可以自定义类型处理器来处理特殊类型。
  • MappedStatement

    • 作用:是 MyBatis 内部的配置对象,它封装了 SQL 映射文件中的一条 SQL 语句(包括 SQL ID、SQL 语句、输入参数类型、输出结果类型等信息)。
  • Configuration

    • 作用:MyBatis 的所有配置信息都存储在这个对象中,包括数据源、事务管理器、类型处理器、映射器(Mapper)等。SqlSessionFactory 在构建时会加载这些配置。
  • Mapper(映射器接口)

    • 作用:MyBatis 推荐使用接口编程的方式来操作数据库。Mapper 接口中的方法与 SQL 映射文件中的 SQL 语句一一对应,通过动态代理的方式,开发者可以直接调用 Mapper 接口的方法来执行 SQL。

MyBatis 运行流程

  • 构建 SqlSessionFactory: 应用程序启动时,通常会通过 SqlSessionFactoryBuilder 读取 mybatis-config.xml 配置文件来构建一个 SqlSessionFactory 实例。这个工厂是线程安全的,一个应用通常只需要一个。

  • 加载配置 (Configuration) : SqlSessionFactory 在构建过程中会解析配置文件,将所有 Mapper.xml 中定义的 SQL 语句、ResultMap 等信息加载到内存中的 Configuration 对象中。

  • 打开 SqlSession: 应用程序通过 SqlSessionFactoryopenSession() 方法获取一个 SqlSession 实例。SqlSession 类似于 JDBC 中的 Connection,它不是线程安全的,每次数据库操作都应该获取新的 SqlSession

  • 获取 Mapper 代理: 开发者通常通过 SqlSession.getMapper(YourMapper.class) 方法获取 Mapper 接口的代理对象。MyBatis 会为该接口生成一个动态代理,所有对 Mapper 接口方法的调用都会被代理拦截。

  • 调用 Mapper 方法: 应用程序直接调用 Mapper 接口中定义的方法(例如 selectUserById(1))。

  • 委托 SqlSession 执行: Mapper 代理拦截到方法调用后,会将方法名、参数等信息传递给 SqlSession,由 SqlSession 决定调用底层的执行方法(如 selectOneinsert 等)。

  • 委托 Executor 执行: SqlSession 会将 SQL 执行的职责委托给内部的 Executor (执行器)。MyBatis 有几种内置的 Executor 类型,如 SimpleExecutorReuseExecutorBatchExecutor 等。

  • StatementHandler 处理 SQL: Executor 会创建或获取一个 StatementHandler 来处理 SQL 语句。StatementHandler 负责准备 SQL 语句(包括 PreparedStatement 的创建)、设置参数以及执行 SQL。

  • ParameterHandler 设置参数: StatementHandler 会委托 ParameterHandler 来处理 SQL 中的参数。ParameterHandler 负责将 Java 对象或基本类型的数据设置到 JDBC 的 PreparedStatement 中。

  • 执行 SQL: StatementHandler 通过 JDBC API 向数据库发送并执行 SQL 语句。

  • ResultSetHandler 封装结果: 当 SQL 查询执行并返回结果集(ResultSet)后,StatementHandler 会委托 ResultSetHandler 来处理结果。ResultSetHandler 根据 Mapper XML 中定义的 resultTyperesultMap,将 JDBC 返回的 ResultSet 中的数据映射并封装成 Java 对象。

  • 返回结果: 封装好的 Java 对象会层层返回,最终通过 Mapper 代理返回给应用程序。

  • 关闭 SqlSession: 最后,应用程序需要手动关闭 SqlSession 来释放数据库连接和相关资源。通常在 finally 块中完成此操作,以确保资源被正确释放。

SqlSession 执行 Mapper 的过程

动态代理

动态代理是一种在程序运行时(而非编译时)动态地创建代理类及其对象的技术。这个“代理”对象会作为目标对象的替身,拦截对目标对象方法的调用。你可以把它想象成一个“门卫”,任何想进入特定房间(调用目标方法)的人,都必须先经过这个门卫。这个门卫可以在你进入房间前和离开房间后做一些额外的事情。
动态代理主要有两种实现方式:

  1. JDK 动态代理:这是 Java 语言自带的代理机制,通过反射来实现。它要求目标对象必须实现一个接口,代理类会实现相同的接口。
  2. CGLIB 动态代理:这是一个第三方库(Code Generation Library),它通过继承目标类来创建代理。因此,它不要求目标对象实现接口,但目标类不能是 final 类或 final 方法。

为什么要使用动态代理?

使用动态代理的核心目的是在不修改原有代码(即目标对象)的情况下,对其功能进行增强扩展。它遵循了软件设计的开闭原则(对扩展开放,对修改关闭),以及单一职责原则(业务逻辑专注于业务本身,非业务逻辑交给代理处理)。

想象一下,如果你有很多个业务方法都需要在执行前打印日志、开启事务,执行后提交事务、关闭日志。如果没有动态代理,你可能需要在每个业务方法的开头和结尾都手动添加这些重复的代码。一旦需求变更,比如日志格式变了,你就要修改所有相关的方法,这会非常麻烦且容易出错。

MyBatis 缓存

MyBatis 提供了两级缓存机制来优化数据库访问性能,减少不必要的数据库查询,提高系统响应速度。这两级缓存分别是一级缓存(本地缓存)和二级缓存(全局缓存)

一级缓存(本地缓存)

  • 作用域SqlSession 级别。
  • 生命周期:与 SqlSession 的生命周期保持一致。当 SqlSession 开启时,一级缓存也随之创建;当 SqlSession 关闭时,一级缓存也会被清空。
  • 默认开启:MyBatis 默认开启一级缓存,无需额外配置。

二级缓存(全局缓存)

  • 作用域SqlSessionFactory 级别。
  • 生命周期:与 SqlSessionFactory 的生命周期保持一致,可以跨多个 SqlSession 共享。
  • 默认关闭:MyBatis 默认不开启二级缓存,需要手动配置开启。

MyBatis 日志实现

MyBatis 作为一个持久层框架,其日志功能对于开发和调试至关重要。它能帮助我们查看 MyBatis 内部的执行细节,比如:

  • SQL 语句:MyBatis 生成的完整 SQL 语句,包括参数绑定后的内容。
  • 参数:SQL 语句中实际绑定的参数值。
  • 结果集:从数据库返回的结果集数据。
  • 连接池信息:连接的获取与释放。
  • 内部处理流程:MyBatis 在执行 SQL 时的内部方法调用等。

MyBatis 并没有自己实现一套日志框架,而是通过适配器模式(或称作桥接模式)集成了业界主流的日志框架。这意味着 MyBatis 自身不产生日志,它只是将日志输出的工作委托给了其他日志框架。

动态 SQL 的实现原理

MyBatis 动态 SQL 的实现原理主要基于其内置的OGNL (Object-Graph Navigation Language) 表达式语言和一套动态 SQL 节点解析器

插件原理及其应用

MyBatis 插件提供了一个强大的机制,允许开发者在不修改 MyBatis 源码的情况下,介入 SQL 执行的四大核心环节,从而实现对 SQL 执行过程的定制化增强。这种“即插即用”的特性使其在许多场景下都大有可为。以下是一些最常见和实用的应用场景:

  • 1、通用分页查询 (Most Common)

    • 场景描述: 几乎所有后台管理系统都需要列表分页。不同数据库(如 MySQL 的 LIMIT,Oracle 的 ROWNUM)的分页 SQL 语法各不相同。如果在业务代码或 XML 中硬编码分页逻辑,会导致代码冗余、难以维护,且无法轻松切换数据库。
    • 插件实现: 创建一个分页插件,拦截 StatementHandlerprepare 方法。在 SQL 执行前,插件会动态地将原始的查询 SQL(如 select * from user)改写成对应数据库方言的分页 SQL(如 select * from user LIMIT ?, ?),并自动设置分页参数。著名的分页插件如 PageHelper 就是基于此原理。
  • 2、SQL 性能监控与慢查询日志

    • 场景描述: 在开发和生产环境中,需要精确追踪每条 SQL 的执行耗时,以便定位性能瓶颈。对于执行时间超过阈值的“慢查询”,需要记录详细的日志(包括 SQL、参数、耗时)以便后续分析和优化。
    • 插件实现: 拦截 Executorqueryupdate 方法。在方法调用前记录一个时间点,在方法执行后(invocation.proceed())再记录一个时间点,两者之差即为 SQL 执行耗时。然后可以将这些信息输出到日志系统或性能监控平台(APM)。
  • 数据脱敏与加解密

    • 场景描述: 很多业务数据(如用户手机号、身份证号、银行卡号)是敏感信息。在存入数据库时需要加密,在查询出来返回给前端或打印日志时,则需要进行脱敏(例如,将 “13812345678” 脱敏为 “138****5678”)。

    • 插件实现:

      • 加密(入库) : 拦截 ParameterHandlersetParameters 方法。在设置参数到 PreparedStatement 之前,检查参数对象的字段是否带有自定义的加密注解(如 @Encrypt),如果有,则对该字段值进行加密处理后再设置。
      • 解密/脱敏(出库) : 拦截 ResultSetHandlerhandleResultSets 方法。在 MyBatis 将结果集 ResultSet 映射成 Java 对象后,检查返回对象的字段是否带有解密/脱敏注解(如 @Decrypt / @Mask),然后对相应字段的值进行处理。
  • 公共字段自动填充

    • 场景描述: 在执行 INSERTUPDATE 操作时,我们希望自动填充一些通用字段,如 create_timecreate_byupdate_timeupdate_by 等,而不需要在每个业务方法的代码里手动设置。
    • 插件实现: 拦截 Executorupdate 方法。在执行 SQL 前,通过 MappedStatement 判断当前是 INSERT 还是 UPDATE 操作,然后通过反射获取参数对象,为其公共字段赋上当前时间或当前用户信息。
  • 多租户(SaaS)数据隔离

    • 场景描述: 在 SaaS 应用中,多个租户共享同一套数据库和表。为了保证数据安全,所有的数据操作(SELECT, UPDATE, DELETE)都必须自动带上租户 ID(tenant_id)作为过滤条件。
    • 插件实现: 拦截 StatementHandlerprepare 方法。从 ThreadLocal 或其他上下文中获取当前操作的 tenant_id,然后使用 JSqlParser 等 SQL 解析工具,为原始 SQL 动态地、智能地追加上 WHERE tenant_id = ? 条件。对于 INSERT 语句,则自动添加 tenant_id 字段及其值。
  • 初始化: MyBatis 启动时,会读取配置文件中的所有 <plugin> 标签,并将其实例化后保存在一个 InterceptorChain(拦截器链)中。

  • 对象创建: 当 MyBatis 需要创建四大核心对象 (Executor, StatementHandler, ParameterHandler, ResultSetHandler) 时,它不会直接返回实例,而是调用 interceptorChain.pluginAll(target) 方法。

  • 动态代理: pluginAll 方法会遍历链上的所有插件。每个插件都有机会通过 Plugin.wrap(target, interceptor) 方法为目标对象创建一个 JDK 动态代理。这个过程像“套娃”一样,如果配置了多个插件,对象会被层层代理。

  • 方法拦截: 当应用程序调用 Mapper 方法,最终会触发到被代理对象(如 Executor)的方法时,实际上调用的是代理对象的 invoke 方法。

  • 逻辑执行: 代理对象的 invoke 方法会转而调用插件的 intercept(Invocation invocation) 方法。在这里,开发者可以执行自己的增强逻辑(如修改 SQL、记录日志等)。

  • 链式调用: 在 intercept 方法内部,通过调用 invocation.proceed(),会将控制权交还给责任链中的下一个插件或最终的原始目标对象,从而完成整个调用链。

MyBatis Spring 的实现原理

MyBatis-Spring 是 MyBatis 框架与 Spring 框架的集成,它允许你在 Spring 环境中更方便地使用 MyBatis。其核心原理在于利用 Spring 的 IoC 容器来管理 MyBatis 的 SqlSessionFactorySqlSessionTemplate,并提供了将 MyBatis Mapper 接口注入到 Spring Bean 中的机制。

参考