当前位置: 首页 > news >正文

Mybatis15-Mapper接口的代理+MappedStatement

一、Mapper接口的代理对象的意义

第一步:没有Mapper接口时,我们怎么写代码?

在MyBatis早期(或者说如果你不用Mapper接口),你的代码长这样:

// 1. 拿到SqlSession SqlSession session = sqlSessionFactory.openSession(); // 2. 直接调用CRUD方法 // 参数1:SQL的ID(字符串),参数2:传入的参数 User user = session.selectOne("com.example.mapper.UserMapper.selectById", 1);

这段代码能跑,但有几个很严重的设计缺陷

弊端1:字符串硬编码,编译器不检查

"com.example.mapper.UserMapper.selectById"是一个字符串。如果你手滑写错了,比如把selectById写成了selectByID编译时完全不会报错,只有运行到这行代码时才会抛出异常。


弊端2:参数类型不安全

selectOne的第二个参数是Object类型。你可以传一个String进去,也可以传一个Date进去,编译器都不会阻止你。但如果XML里期望的是int,运行时就可能类型转换失败。


弊端3:返回类型需要强制转换

selectOne返回的是Object,你需要自己强转成User。如果XML里配置的返回类型和你要转的类型不匹配,又是运行时才能发现。


弊端4:IDE无法提供有效支持

因为一切都是字符串和Object,你的IDE无法做方法跳转参数提示重构重命名。你想改个SQL的ID,全局搜索替换很容易漏改或改错。


第二步:于是,Mapper接口被设计出来了

为了解决上面的问题,MyBatis引入了Mapper接口

public interface UserMapper { User selectById(int id); List<User> selectAll(); int insertUser(User user); }

这个接口定义了方法名参数类型返回值类型

但这里立刻出现了一个关键问题:

接口只有定义,没有实现。

UserMapper是一个接口,你不能new UserMapper()

那谁来执行真正的SQL逻辑呢?


第三步:如果让你写实现类,问题又回来了

假设MyBatis要求你自己写实现类:

public class UserMapperImpl implements UserMapper { private SqlSession sqlSession; public UserMapperImpl(SqlSession sqlSession) { this.sqlSession = sqlSession; } @Override public User selectById(int id) { // 绕了一圈,又回到了字符串调用! return sqlSession.selectOne( "com.example.mapper.UserMapper.selectById", id); } @Override public List<User> selectAll() { return sqlSession.selectList( "com.example.mapper.UserMapper.selectAll"); } // ... 每个方法都如此 }

你发现了吗?如果让你手写实现类,之前所有弊端一个都没解决——你还是在写字符串、还是在做类型转换、代码还是臃肿。

而且每增加一个Mapper接口,你就要多写一个实现类,全是样板代码(Boilerplate)。

所以MyBatis的设计者想:既然每个实现类的逻辑都是固定的——根据方法名找到SQL,执行,返回结果——那为什么不交给框架自动生成呢?


第四步:代理对象登场——框架帮你"实现"接口

这就是sqlSession.getMapper(UserMapper.class)做的事情。

它不会返回null,也不会返回一个你手写的UserMapperImpl。它返回的是一个代理对象(Proxy Object)。

这个代理对象在运行时被动态创建,它实现了UserMapper接口,所以你完全可以把它当成UserMapper来用:

UserMapper mapper = sqlSession.getMapper(UserMapper.class); // 这行代码能编译通过,因为返回的对象确实实现了UserMapper接口 User user = mapper.selectById(1);

核心问题:代理对象里并没有真正的业务逻辑,那selectById(1)是怎么执行的?


第五步:JDK动态代理 + MapperProxy 的工作机制

MyBatis使用的是JDK动态代理。要理解它,你需要知道三个角色:

1. 接口(UserMapper)

定义了"长什么样"——有哪些方法,参数和返回值是什么。

2. 代理对象(运行时动态生成的类)

它实现了UserMapper接口,所以你可以把它赋值给UserMapper类型的变量。

3. 调用处理器(MapperProxy)

这是真正干活的人。它实现了InvocationHandler接口。

当你调用:

mapper.selectById(1);

实际上,JVM并没有执行某个类里写好的selectById方法,而是把这个调用拦截下来,转交给了MapperProxyinvoke方法


MapperProxy.invoke里大概做了这些事:

  1. 获取方法信息:知道你调用的是selectById,参数是1

  2. 解析SQL定位:根据接口全限定名 + 方法名,找到XML或注解中对应的SQL语句

  3. 调用SqlSession最终还是会走到sqlSession.selectOne(...),但这个过程对你是透明的

  4. 结果映射:把查询结果转换成User类型返回

// 伪代码,帮你理解流程 public Object invoke(Object proxy, Method method, Object[] args) { // 1. 拿到方法名:selectById String methodName = method.getName(); // 2. 拿到接口名:com.example.mapper.UserMapper String interfaceName = method.getDeclaringClass().getName(); // 3. 组合成SQL ID:com.example.mapper.UserMapper.selectById String statementId = interfaceName + "." + methodName; // 4. 根据返回类型决定调用selectOne还是selectList if (method.getReturnType() == List.class) { return sqlSession.selectList(statementId, args[0]); } else { return sqlSession.selectOne(statementId, args[0]); } }

第六步:为什么必须用代理?不用行不行?

不用代理,你有两个选择,但都有致命缺陷:

方案缺陷
直接调用SqlSession字符串硬编码、无类型安全、无IDE支持
手写实现类样板代码爆炸、维护困难、没有解决本质问题

代理模式是唯一的出路,因为它同时解决了:

  1. 类型安全:你操作的是UserMapper接口,方法参数和返回值都是强类型的,编译器会检查。

  2. 无字符串硬编码:方法名就是SQL ID,通过反射自动获取,写错方法名编译期就能发现。

  3. 零样板代码:你不需要写实现类,框架动态生成。

  4. IDE友好:你可以Ctrl+点击跳转,可以安全重构重命名。


第七步:为什么是JDK动态代理?

JDK动态代理有一个硬性要求:被代理的必须是接口

MyBatis的Mapper天然就是接口,所以完美契合。

它的本质是java.lang.reflect.Proxy.newProxyInstance(...)在运行时生成一个类,大概长这样(伪代码):

// 这是JVM在内存中动态生成的类,你看不到源码 public class $Proxy0 implements UserMapper { private InvocationHandler handler; // 这就是MapperProxy public User selectById(int id) { // 所有方法调用都转发给handler return (User) handler.invoke(this, selectById方法对象, new Object[]{id}); } }

$Proxy0这个类是运行时临时生成的字节码,它实现了UserMapper,并把每个方法调用都委托给MapperProxy


总结:逻辑链条

  1. 因为直接调用SqlSession有字符串硬编码、类型不安全、维护困难等弊端;

  2. 所以引入了Mapper接口,利用Java的类型系统提供编译期检查;

  3. 但是接口不能实例化,需要有人来实现接口里的方法;

  4. 如果让开发者手写实现类,会写大量样板代码,且本质上还是调用SqlSession,没有解决问题;

  5. 因此MyBatis使用JDK动态代理,由框架在运行时自动生成接口的实现(代理对象);

  6. 最终MapperProxy拦截所有方法调用,自动解析方法名、找到SQL、执行并返回结果

你拿到的mapper对象,本质上是一个由MyBatis自动生成的、会帮你转发请求到SqlSession的接口实现。你表面上在调用接口方法,实际上MyBatis在背后帮你完成了"找SQL → 传参数 → 执行 → 转结果"的一整套流程。

这就是代理的意义:让你用优雅的方式(调用接口方法),去做原本繁琐且易错的事情(操作SqlSession)

二、MappedStatement 类的讲解

第一步:如果没有MappedStatement,执行SQL会面临什么困境?

假设MyBatis没有MappedStatement这个概念,最原始的执行方式可能是这样:

// 伪代码:直接传SQL字符串 session.execute("SELECT * FROM user WHERE id = ?", 1);

但这远远不够。一条SQL要正确执行并返回正确结果,框架至少需要知道:

  1. 参数类型?对应的Java类型是什么?是intString还是User对象?这决定了JDBC的setXxx方法用哪个。

  2. 返回类型:查询结果要封装成User对象,还是Map,还是List<String>

  3. SQL命令类型:这是SELECT还是INSERT?如果是INSERT,可能需要获取数据库自增的主键;如果是SELECT,需要返回结果集。

  4. 结果映射规则:数据库字段名叫user_name,Java属性叫userName,这个映射关系怎么告诉框架?

  5. 动态SQL:这条SQL可能包含<if>标签,需要根据参数动态拼接,不是固定的字符串。

  6. 缓存配置:这条SQL的结果要不要走二级缓存?执行这条SQL前要不要清空缓存?

  7. 超时时间:这条SQL最多执行多久?

  8. Statement类型:用普通的Statement、预编译的PreparedStatement,还是存储过程的CallableStatement


如果把这些信息都作为execute方法的参数,API会变成这样:

session.execute( "SELECT * FROM user WHERE id = ?", // SQL 1, // 参数 Integer.class, // 参数类型 User.class, // 返回类型 resultMap, // 结果映射规则 SqlCommandType.SELECT, // 命令类型 true, // 是否使用缓存 false, // 是否刷新缓存 5000, // 超时毫秒 StatementType.PREPARED // Statement类型 );

弊端非常明显:

  • 每次执行SQL都要传一堆参数,极易遗漏、顺序搞错

  • 这些信息其实是固定不变的(写在XML里就不会变),但每次执行都要重复传递

  • 动态SQL的解析逻辑无处安放,不可能每次执行都重新解析XML


第二步:一条SQL的所有信息,本质上是一个"整体"

让我们换个角度思考:你在UserMapper.xml里写的每一个<select><insert><update><delete>标签,其实都在描述同一件事——如何执行一条特定的SQL

比如:

<select id="selectById" parameterType="int" resultType="User" useCache="true" timeout="5000"> SELECT * FROM user WHERE id = #{id} </select>

这个标签里包含了:

  • idselectById(唯一标识)

  • SQL文本SELECT * FROM user WHERE id = ?

  • 参数类型int

  • 返回类型User

  • 缓存配置useCache="true"

  • 超时配置timeout="5000"

这些信息是内聚的——它们只和"根据ID查询用户"这一条SQL相关。既然它们是内聚的,代码设计上就应该把它们封装成一个对象

这就是MappedStatement的设计来源:

一个MappedStatement对象 = 一条SQL语句 + 执行这条SQL所需的全部元信息


第三步:MappedStatement里面到底装了什么?

MappedStatement是一个普通的Java类(虽然名字叫"Statement",但它不是JDBC的Statement),核心属性如下:

public final class MappedStatement { private String id; // 唯一标识,格式:namespace + "." + id private SqlSource sqlSource; // SQL源(封装了动态SQL解析逻辑) private SqlCommandType sqlCommandType; // SELECT / INSERT / UPDATE / DELETE private Class<?> parameterType; // 参数Java类型 private List<ResultMap> resultMaps; // 结果映射配置 private boolean flushCacheRequired; // 执行前是否清空二级缓存 private boolean useCache; // 是否使用二级缓存 private Integer timeout; // 超时时间 private StatementType statementType; // STATEMENT / PREPARED / CALLABLE // ... 还有其他属性 }

注意这里有一个关键设计:sqlSource的类型不是String,而是SqlSource

因为MyBatis支持动态SQL<if><foreach>等),SQL文本不是固定的。

SqlSource的职责是:根据运行时传入的参数,生成最终可执行的SQL(生成一个叫BoundSql的对象)。

所以MappedStatement不仅封装了静态配置,还封装了动态SQL的解析逻辑


第四步:MappedStatement解决了哪些具体问题?

1. 信息聚合,告别"散弹枪式"传参

以前执行SQL需要传10个分散的参数,现在只需要传一个MappedStatement对象。所有和这条SQL相关的配置都在它内部,调用方和执行方都轻松。

2. 启动时解析,运行时复用

MappedStatementMyBatis初始化阶段(解析XML或扫描注解时)就被创建好了,然后存入内存。运行时执行SQL时直接取出来用,不需要重复解析XML,性能上完全可控。

3. 统一执行器的调度入口

SqlSession底层委托给Executor执行。Executor的所有执行方法都接收MappedStatement作为参数:

// Executor接口定义 <E> List<E> query(MappedStatement ms, Object parameter, ...); int update(MappedStatement ms, Object parameter);

Executor拿到MappedStatement后,就能独立完成全部操作:

  • 调用ms.getSqlSource().getBoundSql(parameter)→ 得到最终SQL

  • 查看ms.getSqlCommandType()→ 知道是查询还是更新

  • 查看ms.getResultMaps()→ 知道怎么把ResultSet转成Java对象

  • 查看ms.getStatementType()→ 知道创建哪种JDBC Statement

MappedStatement让执行器具备了"自解释"的能力——执行器不需要外部再告诉它任何额外信息。

4. 动态SQL的天然载体

如果没有MappedStatement,动态SQL的逻辑放在哪里?

MappedStatement内部的SqlSource完美解决了这个问题。它把"根据参数生成SQL"的逻辑封装了起来,对外只暴露一个getBoundSql(parameter)方法。


第五步:MappedStatement在MyBatis中的完整生命周期

阶段1:解析阶段(应用启动时)

当你写了一个Mapper XML:

<mapper namespace="com.example.mapper.UserMapper"> <select id="selectById" resultType="User"> SELECT * FROM user WHERE id = #{id} </select> </mapper>

MyBatis的XMLMapperBuilder会解析这个<select>标签:

// 伪代码 MappedStatement.Builder builder = new MappedStatement.Builder( configuration, // 全局配置 "com.example.mapper.UserMapper.selectById", // id sqlSource, // 解析出的SQL源 SqlCommandType.SELECT // 命令类型 ); builder.resultType(User.class); // ... 设置其他属性 MappedStatement ms = builder.build(); configuration.addMappedStatement(ms); // 注册到Configuration

阶段2:存储阶段(在Configuration中)

public class Configuration { // key: "namespace.id" value: MappedStatement protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(); }

所有解析好的MappedStatement都存在这个Map里。这就是为什么SQL ID不能重复——它是Map的key。

阶段3:执行阶段(运行时)

当你调用:

User user = mapper.selectById(1);

内部的调用链是这样的:

  1. MapperProxy.invoke()拦截到selectById方法调用

  2. 组合出ID:"com.example.mapper.UserMapper.selectById"

  3. Configuration.mappedStatements根据ID取出对应的MappedStatement

  4. 调用sqlSession.selectOne(mappedStatement, 1)——注意这里传的是对象,不是字符串

  5. ExecutorMappedStatement中提取所有信息,完成JDBC操作


第六步:为什么不能直接用字符串ID,非要包装成对象?

你可能会问:既然最终也是根据"namespace.id"找到SQL,为什么不能直接传字符串ID,让框架每次执行时去XML里查?

答案是三个层面的问题:

层面原因
性能每次执行都解析XML是不可接受的。MappedStatement在启动时解析一次,后续内存中直接复用。
信息完整性字符串ID只能定位到SQL文本,但无法携带参数类型、返回类型、缓存配置等元信息。
动态SQL动态SQL需要根据参数实时生成最终SQL,这个逻辑封装在MappedStatement持有的SqlSource中,不是简单的字符串查找。

总结:逻辑链条

  1. 因为执行一条SQL需要大量元信息(参数类型、返回类型、SQL类型、缓存配置、超时设置等),如果分散传递会导致API臃肿、调用混乱、极易出错;

  2. 所以MyBatis设计了MappedStatement,把一条SQL的所有执行元信息封装成一个对象;

  3. 因为这些信息是固定不变的,可以在应用启动时解析XML/注解一次性准备好;

  4. 所以MappedStatement被创建后存入Configuration的Map中"namespace.id"为键,运行时直接复用;

  5. 因为执行时只需要一个MappedStatement对象,就能拿到SQL文本、参数映射、结果映射、缓存策略等全部信息;

  6. 所以SqlSessionExecutor的执行方法都接收MappedStatement作为核心参数,内部从中提取所有配置,完成完整的数据库操作。

MappedStatement就是MyBatis中一条SQL的完整执行档案。它让SQL的执行从"临时拼凑参数"变成了"有备而来、一应俱全"。

http://www.jsqmd.com/news/1251056/

相关文章:

  • 报表做得再溜,上线也崩:数据分析转 Agent 的权限与日志生死线
  • Django计算机毕设之基于 Web 的大学生评优评先测评系统 基于 Django 的学生综合素质数据统计与分析系统(完整前后端 代码+说明文档+LW,调试定制等)
  • 微信去水印小程序风险提醒:2026抖音快手小红书免费去水印实测 - AI测评专家
  • 2026年彩钢开平机定制厂家哪家靠谱 实用选型攻略分享 - 热点品牌推荐
  • 2026年鄂西北原厂风光车发动机靠谱供应商选购参考指南 - 热点品牌推荐
  • 基于Java的校园物品交易平台(Java+SSM+MySQL)| 计算机毕业设计 附源码论文PPT
  • 卡地亚东莞网点地址与客服热线最新公示信息(2026年7月版) - 卡地亚官方售后中心
  • STELLA自进化LLM在生物医学研究的应用与实现
  • 2026年新疆区域实力变电站鹅卵石批发定制厂家哪家靠谱 - 热点品牌推荐
  • 手机自制电子证件照,从拍摄到换底出图的全流程方法 - 软件小管家
  • 手机制作小二寸照片的几种办法,亲测这几种工具用着顺手 - AI测评专家
  • 雷达福州2026年7月最新网点地址与客服热线及售后公示信息 - 亨得利官方服务中心
  • 萧邦中国售后服务中心热线与门店地址实地考察报告_多信源验证(2027年7月最新) - 萧邦中国官方服务中心
  • 金水区下水管道根部防水公司 本地防水服务选择实用攻略 - 热点品牌推荐
  • 想找靠谱的中国谷歌SEO公司?大鱼营销用真实数据帮你提升海外排名。
  • 2026年生鲜冰袋机制造厂哪个厂便宜实用选型参考 - 热点品牌推荐
  • 手机换证件照衣服不求人:三种方法让你省下跑照相馆的时间 - 办公小帮手
  • 数据驱动的LQR自适应控制:DeePO算法实现与Matlab复现
  • 龙岩CMA甲醛检测公司怎么选:只测不除的专业实验室——国康CMA检测及公共卫生检测 - 信誉隆金银铂奢回收
  • OpenClaw:本地化开源AI助手的部署与应用指南
  • 2026年聚氨酯跑道厂家推荐 专业运动场地服务商选型参考 - 热点品牌推荐
  • FoundationMotion:自监督学习颠覆动作识别技术
  • 免费一寸照片生成器怎么用?2026年微信小程序、在线工具、电脑软件全教程 - 办公小帮手
  • 不用跑照相馆了:一部手机搞定所有证件照,从拍到印全流程指南 - AI测评专家
  • 亨得利服务项目及价格查询|电话及详细维修地址权威信息通知(2026年7月更新) - 亨得利官方博客
  • 2026年福州家长关心故事表演费用哪家可靠 - 热点品牌推荐
  • 2026 最新泰安防水补漏实操指南:山区自建房与景区民宿防渗施工标准.doc - 资讯报道
  • 证件照换底色工具与操作全攻略(2026年7月版) - 软件小管家
  • AI写小说软件哪个好?10款国内外热门AI写作工具深度测评与详解
  • 爱彼中国售后服务中心|最新电话和维修地址权威信息公告(2026年7月更新) - 爱彼中国官方服务中心