Mybatis15-Mapper接口的代理+MappedStatement

发布时间:2026/7/24 0:36:50

Mybatis15-Mapper接口的代理+MappedStatement 一、Mapper接口的代理对象的意义第一步没有Mapper接口时我们怎么写代码在MyBatis早期或者说如果你不用Mapper接口你的代码长这样// 1. 拿到SqlSession SqlSession session sqlSessionFactory.openSession(); // 2. 直接调用CRUD方法 // 参数1SQL的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里配置的返回类型和你要转的类型不匹配又是运行时才能发现。弊端4IDE无法提供有效支持因为一切都是字符串和Object你的IDE无法做方法跳转、参数提示、重构重命名。你想改个SQL的ID全局搜索替换很容易漏改或改错。第二步于是Mapper接口被设计出来了为了解决上面的问题MyBatis引入了Mapper接口public interface UserMapper { User selectById(int id); ListUser 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 ListUser 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方法而是把这个调用拦截下来转交给了MapperProxy的invoke方法。MapperProxy.invoke里大概做了这些事获取方法信息知道你调用的是selectById参数是1解析SQL定位根据接口全限定名 方法名找到XML或注解中对应的SQL语句调用SqlSession最终还是会走到sqlSession.selectOne(...)但这个过程对你是透明的结果映射把查询结果转换成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 IDcom.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支持手写实现类样板代码爆炸、维护困难、没有解决本质问题代理模式是唯一的出路因为它同时解决了类型安全你操作的是UserMapper接口方法参数和返回值都是强类型的编译器会检查。无字符串硬编码方法名就是SQL ID通过反射自动获取写错方法名编译期就能发现。零样板代码你不需要写实现类框架动态生成。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。总结逻辑链条因为直接调用SqlSession有字符串硬编码、类型不安全、维护困难等弊端所以引入了Mapper接口利用Java的类型系统提供编译期检查但是接口不能实例化需要有人来实现接口里的方法如果让开发者手写实现类会写大量样板代码且本质上还是调用SqlSession没有解决问题因此MyBatis使用JDK动态代理由框架在运行时自动生成接口的实现代理对象最终MapperProxy拦截所有方法调用自动解析方法名、找到SQL、执行并返回结果。你拿到的mapper对象本质上是一个由MyBatis自动生成的、会帮你转发请求到SqlSession的接口实现。你表面上在调用接口方法实际上MyBatis在背后帮你完成了找SQL → 传参数 → 执行 → 转结果的一整套流程。这就是代理的意义让你用优雅的方式调用接口方法去做原本繁琐且易错的事情操作SqlSession。二、MappedStatement 类的讲解第一步如果没有MappedStatement执行SQL会面临什么困境假设MyBatis没有MappedStatement这个概念最原始的执行方式可能是这样// 伪代码直接传SQL字符串 session.execute(SELECT * FROM user WHERE id ?, 1);但这远远不够。一条SQL要正确执行并返回正确结果框架至少需要知道参数类型?对应的Java类型是什么是int、String还是User对象这决定了JDBC的setXxx方法用哪个。返回类型查询结果要封装成User对象还是Map还是ListStringSQL命令类型这是SELECT还是INSERT如果是INSERT可能需要获取数据库自增的主键如果是SELECT需要返回结果集。结果映射规则数据库字段名叫user_nameJava属性叫userName这个映射关系怎么告诉框架动态SQL这条SQL可能包含if标签需要根据参数动态拼接不是固定的字符串。缓存配置这条SQL的结果要不要走二级缓存执行这条SQL前要不要清空缓存超时时间这条SQL最多执行多久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 idselectById parameterTypeint resultTypeUser useCachetrue timeout5000 SELECT * FROM user WHERE id #{id} /select这个标签里包含了idselectById唯一标识SQL文本SELECT * FROM user WHERE id ?参数类型int返回类型User缓存配置useCachetrue超时配置timeout5000这些信息是内聚的——它们只和根据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 ListResultMap resultMaps; // 结果映射配置 private boolean flushCacheRequired; // 执行前是否清空二级缓存 private boolean useCache; // 是否使用二级缓存 private Integer timeout; // 超时时间 private StatementType statementType; // STATEMENT / PREPARED / CALLABLE // ... 还有其他属性 }注意这里有一个关键设计sqlSource的类型不是String而是SqlSource。因为MyBatis支持动态SQLif、foreach等SQL文本不是固定的。SqlSource的职责是根据运行时传入的参数生成最终可执行的SQL生成一个叫BoundSql的对象。所以MappedStatement不仅封装了静态配置还封装了动态SQL的解析逻辑。第四步MappedStatement解决了哪些具体问题1. 信息聚合告别散弹枪式传参以前执行SQL需要传10个分散的参数现在只需要传一个MappedStatement对象。所有和这条SQL相关的配置都在它内部调用方和执行方都轻松。2. 启动时解析运行时复用MappedStatement在MyBatis初始化阶段解析XML或扫描注解时就被创建好了然后存入内存。运行时执行SQL时直接取出来用不需要重复解析XML性能上完全可控。3. 统一执行器的调度入口SqlSession底层委托给Executor执行。Executor的所有执行方法都接收MappedStatement作为参数// Executor接口定义 E ListE 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 StatementMappedStatement让执行器具备了自解释的能力——执行器不需要外部再告诉它任何额外信息。4. 动态SQL的天然载体如果没有MappedStatement动态SQL的逻辑放在哪里MappedStatement内部的SqlSource完美解决了这个问题。它把根据参数生成SQL的逻辑封装了起来对外只暴露一个getBoundSql(parameter)方法。第五步MappedStatement在MyBatis中的完整生命周期阶段1解析阶段应用启动时当你写了一个Mapper XMLmapper namespacecom.example.mapper.UserMapper select idselectById resultTypeUser SELECT * FROM user WHERE id #{id} /select /mapperMyBatis的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 MapString, MappedStatement mappedStatements new StrictMapMappedStatement(); }所有解析好的MappedStatement都存在这个Map里。这就是为什么SQL ID不能重复——它是Map的key。阶段3执行阶段运行时当你调用User user mapper.selectById(1);内部的调用链是这样的MapperProxy.invoke()拦截到selectById方法调用组合出IDcom.example.mapper.UserMapper.selectById从Configuration.mappedStatements中根据ID取出对应的MappedStatement调用sqlSession.selectOne(mappedStatement, 1)——注意这里传的是对象不是字符串Executor从MappedStatement中提取所有信息完成JDBC操作第六步为什么不能直接用字符串ID非要包装成对象你可能会问既然最终也是根据namespace.id找到SQL为什么不能直接传字符串ID让框架每次执行时去XML里查答案是三个层面的问题层面原因性能每次执行都解析XML是不可接受的。MappedStatement在启动时解析一次后续内存中直接复用。信息完整性字符串ID只能定位到SQL文本但无法携带参数类型、返回类型、缓存配置等元信息。动态SQL动态SQL需要根据参数实时生成最终SQL这个逻辑封装在MappedStatement持有的SqlSource中不是简单的字符串查找。总结逻辑链条因为执行一条SQL需要大量元信息参数类型、返回类型、SQL类型、缓存配置、超时设置等如果分散传递会导致API臃肿、调用混乱、极易出错所以MyBatis设计了MappedStatement把一条SQL的所有执行元信息封装成一个对象因为这些信息是固定不变的可以在应用启动时解析XML/注解一次性准备好所以MappedStatement被创建后存入Configuration的Map中以namespace.id为键运行时直接复用因为执行时只需要一个MappedStatement对象就能拿到SQL文本、参数映射、结果映射、缓存策略等全部信息所以SqlSession和Executor的执行方法都接收MappedStatement作为核心参数内部从中提取所有配置完成完整的数据库操作。MappedStatement就是MyBatis中一条SQL的完整执行档案。它让SQL的执行从临时拼凑参数变成了有备而来、一应俱全。

相关新闻