ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

你写的 Mapper 接口,MyBatis 到底是怎么把它变成 SQL 的?

你写的 Mapper 接口,MyBatis 到底是怎么把它变成 SQL 的? 你写的 Mapper 接口MyBatis 到底是怎么把它变成 SQL 的只写了一个接口没有实现类MyBatis 却能在运行时帮你执行 SQL——它是怎么做到的这篇从 SqlSession 工厂、Mapper 代理、SQL 解析到参数绑定把 MyBatis 的执行链路一次拆透。一、从最原始的用法说起大多数人第一次用 MyBatis都是先写一个 XML再写一个接口!-- UserMapper.xml --mappernamespacecom.example.mapper.UserMapperselectidselectByIdresultTypecom.example.entity.UserSELECT * FROM user WHERE id #{id}/select/mapper// UserMapper.java —— 只声明了接口没有实现类publicinterfaceUserMapper{UserselectById(Param(id)Longid);}然后神奇的事情发生了UserMappermappersqlSession.getMapper(UserMapper.class);Userusermapper.selectById(1L);接口没有实现类代码却能跑通。这就是 MyBatis 的核心魔法JDK 动态代理。下面从全局视角拆这条链路。二、全局架构MyBatis 执行链路总览┌──────────────────────────────────────────────────────────┐ │ 应用层 │ │ UserMapper.selectById() ← 你只写了接口 │ └──────────────────────────┬───────────────────────────────┘ ▼ ┌──────────────────────────────────────────────────────────┐ │ MapperProxy (JDK 动态代理) │ │ invoke() → 按方法找到 MappedStatement │ └──────────────────────────┬───────────────────────────────┘ ▼ ┌──────────────────────────────────────────────────────────┐ │ SqlSessionTemplate / DefaultSqlSession │ │ · 从 Configuration 拿 MappedStatement │ └──────────────────────────┬───────────────────────────────┘ ▼ ┌──────────────────────────────────────────────────────────┐ │ Executor执行器 │ │ · SimpleExecutor / ReuseExecutor / BatchExecutor │ │ · 二级缓存 → 一级缓存 → 查库 │ └──────────────────────────┬───────────────────────────────┘ ▼ ┌──────────────────────────────────────────────────────────┐ │ StatementHandler语句处理器 │ │ · ParameterHandler#{id} 参数绑定 │ │ · ResultSetHandler结果集 → 对象映射 │ └──────────────────────────────────────────────────────────┘ ▼ 数据库 JDBC一条 SQL 从接口到数据库经过 5 个环节代理 → SqlSession → Executor → StatementHandler → 参数/结果处理。下面逐个拆。三、Mapper 代理接口是怎么被实现的SqlSession.getMapper()的底层是MapperRegistry// MapperRegistry 源码核心逻辑publicclassMapperRegistry{privatefinalMapClass?,MapperProxyFactory?knownMappersnewHashMap();publicTTgetMapper(ClassTtype,SqlSessionsqlSession){// 1. 从 knownMappers 拿代理工厂MapperProxyFactoryTmapperProxyFactory(MapperProxyFactoryT)knownMappers.get(type);// 2. 用 JDK 动态代理创建实例returnmapperProxyFactory.newInstance(sqlSession);}}// MapperProxyFactorypublicclassMapperProxyFactoryT{publicTnewInstance(SqlSessionsqlSession){MapperProxyTmapperProxynewMapperProxy(sqlSession,mapperInterface,methodCache);return(T)Proxy.newProxyInstance(mapperInterface.getClassLoader(),newClass[]{mapperInterface},// 代理的接口mapperProxy);// InvocationHandler}}当你调用mapper.selectById(1L)时真正执行的是MapperProxy.invoke()publicclassMapperProxyTimplementsInvocationHandler{OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{// Object 自带的方法toString/hashCode 等不走 MyBatisif(Object.class.equals(method.getDeclaringClass())){returnmethod.invoke(this,args);}// 核心找到这个接口方法对应的 MapperMethod然后执行MapperMethodmapperMethodcachedMapperMethod(method);returnmapperMethod.execute(sqlSession,args);}}一句话总结MyBatis 用 JDK 动态代理伪造了接口的实现每个接口方法都对应一条已解析好的 SQLMappedStatement。四、MappedStatement接口方法 ↔ SQL 的绑定问题来了MapperProxy怎么知道selectById对应哪条 SQL答案是MappedStatement。在解析阶段MyBatis 把 XML 里的每个select/insert/update/delete解析成一个MappedStatement注册到Configuration// XMLMapperBuilder 解析 XML 时的核心逻辑简版publicvoidparse(){// 1. 解析 mapper namespace...Stringnamespaceparser.getStringAttribute(namespace);// 2. 逐个解析 select/insert/... 标签for(XNodestatementNode:parser.evalNodes(select|insert|update|delete)){// 生成 MappedStatementid namespace . 语句 idMappedStatementstatementbuildStatementFromContext(statementNode);configuration.addMappedStatement(statement);}}MapperProxy.invoke()里其实就是一条 lookup// MapperMethod 初始化时publicMapperMethod(Class?mapperInterface,Methodmethod,Configurationconfig){// key com.example.mapper.UserMapper.selectByIdStringstatementIdmapperInterface.getName().method.getName();// 从 Configuration 里找到对应 MappedStatementMappedStatementmsconfig.getMappedStatement(statementId);}这就解释了为什么 XML 里id必须等于接口方法名、namespace必须等于接口全限定名——MyBatis 靠这两个拼接出唯一 ID 来绑定。五、SQL 解析从 #{id} 到 JDBC 的 ?XML 里写的#{id}不是直接拼进 SQL 的MyBatis 会先把它解析成 JDBC 的?占位符// DynamicSqlSource / RawSqlSource 最终都会生成 BoundSqlpublicclassRawSqlSourceimplementsSqlSource{privatefinalSqlSourcesqlSource;publicRawSqlSource(Configurationconfiguration,Stringsql,Class?parameterType){// 1. 用 SqlSourceBuilder 解析 #{xxx}SqlSourceBuildersqlSourceParsernewSqlSourceBuilder(configuration);// 2. 替换成 ? 占位符 ParameterMapping 列表SqlSourcesqlSourcesqlSourceParser.parse(sql,parameterType,newHashMap());this.sqlSourcesqlSource;}}解析结果SELECT * FROM user WHERE id #{id} ↓ 解析 SELECT * FROM user WHERE id ? ↓ ParameterMapping [ParameterMapping{propertyid, jdbcTypenull, ...}]注意#{}是预编译占位符安全${}是字符串拼接有 SQL 注入风险。${}的场景是表名、排序字段等动态部分。五、ExecutorSQL 执行的三层过滤器Executor接口有 3 个默认实现实现类行为适用场景SimpleExecutor每次执行都新建 Statement默认通用ReuseExecutor复用 Statement同 SQL 复用 PreparedStatement循环执行同 SQLBatchExecutor批量执行攒一批再提交批量插入/更新Executor内部有两级缓存publicabstractclassBaseExecutorimplementsExecutor{// 一级缓存SqlSession 级默认开启protectedPerpetualCachelocalCache;OverridepublicEListEquery(MappedStatementms,Objectparameter,...){// 1. 先查一级缓存if(queryStack0ms.isFlushCacheRequired()){clearLocalCache();}// 2. 组装 CacheKey含 SQL、参数、分页信息CacheKeykeycreateCacheKey(ms,parameter,...);// 3. 查缓存命中直接返回ListElistlocalCache.getObject(key);if(listnull){listqueryFromDatabase(ms,parameter,rowBounds,key);// 查库}returnlist;}}// 二级缓存Mapper 级需配置 cache/// 入口CachingExecutor装饰器模式包在 Executor 外层publicclassCachingExecutorimplementsExecutor{privatefinalExecutordelegate;// 真正的执行器OverridepublicEListEquery(MappedStatementms,Objectparameter,...){// 1. 查二级缓存命名空间级跨 SqlSession 共享if(ms.isUseCache()){Cachecachems.getCache();if(cache!null){ListElistcache.getObject(key);if(list!null)returnlist;// 命中二级缓存}}// 2. 未命中走 delegate一级缓存 查库returndelegate.query(ms,parameter,rowBounds,resultHandler,key,boundSql);}}缓存层级二级缓存Mapper 级→ 一级缓存SqlSession 级→ 数据库。二级缓存默认关闭一查默认开启。六、参数绑定PreparedStatement 的赋值过程Executor最终通过PreparedStatementHandler创建PreparedStatement然后用ParameterHandler绑定参数publicclassDefaultParameterHandlerimplementsParameterHandler{OverridepublicvoidsetParameters(PreparedStatementps){// 遍历 MappedStatement 里解析好的 ParameterMapping 列表for(inti0;iparameterMappings.size();i){ParameterMappingpmparameterMappings.get(i);// 从参数对象里取出对应属性值ObjectvaluevalueFromParameter(parameterObject,pm.getProperty());// 设置 jdbcType 等元数据TypeHandlertypeHandlerpm.getTypeHandler();JdbcTypejdbcTypepm.getJdbcType();// 核心给 PreparedStatement 的第 i 个 ? 赋值typeHandler.setParameter(ps,i1,value,jdbcType);}}}绑定过程mapper.selectById(1L);// ↓ MapperProxy 把参数包成 ParamMap// ↓ Executor 把参数传给 StatementHandler// ↓ ParameterHandler.setParameters()SELECT*FROMuserWHEREid?←PreparedStatement.setLong(1,1L)七、结果映射ResultSet → Java 对象查出来的结果怎么变成User对象这是ResultSetHandler的活publicclassDefaultResultSetHandlerimplementsResultSetHandler{OverridepublicObjecthandleResultSets(Statementstmt){ListObjectmultipleResultsnewArrayList();ResultSetWrapperrswnewResultSetWrapper(stmt.getResultSet());// 获取 resultMap或自动映射ResultMapresultMapgetResultMap(rsw);handleResultSet(rsw,resultMap,multipleResults);returncollapseSingleResultList(multipleResults);// 单条时解包}privatevoidhandleRowValues(...){// 核心反射/构造器创建目标对象ObjectmappedObjectcreateResultObject(rsw,resultMap,columnPrefix);// 按列名映射到属性applyAutomaticMappings(rsw,resultMap,metaObject,columnPrefix);}}自动映射过程user_name→ 去掉下划线转驼峰 →userName→ 反射setUserName()。这就是mapUnderscoreToCamelCase配置的底层逻辑。八、完整链路串联一次 selectById 的旅程调用 UserMapper.selectById(1L) │ ▼ ① MapperProxyJDK 动态代理 └─ 找到 MappedStatement: com.example.mapper.UserMapper.selectById │ ▼ ② SqlSessionTemplate └─ 获取当前事务的 ExecutorCachingExecutor 包装 │ ▼ ③ CachingExecutor ├─ 查二级缓存 → 命中直接返回 └─ 未命中 → 交给 BaseExecutor │ ▼ ④ BaseExecutor ├─ 查一级缓存 → 命中直接返回 └─ 未命中 → queryFromDatabase() │ ▼ ⑤ SimpleExecutor └─ StatementHandler.prepare() → 创建 PreparedStatement │ ▼ ⑥ ParameterHandler └─ setParameters() → PreparedStatement.setLong(1, 1L) │ ▼ ⑦ JDBC executeQuery() │ ▼ ⑧ ResultSetHandler ├─ createResultObject() → 反射创建 User └─ 逐列 setter 赋值 → 返回 User 对象 │ ▼ ⑨ 回填一级缓存 → 返回结果九、高频面试追问9.1 为什么 MyBatis 的 Mapper 接口不能用方法重载接口里同名方法会生成相同的MappedStatement.idnamespace . 方法名后注册的覆盖先注册的第二个方法执行必然报错。这也是 MyBatis 设计上的一个限制。9.2 一级缓存失效的几种情况场景原因SqlSession 关闭/新建一级缓存绑定 SqlSession 生命周期手动 clearCache()主动清空两次查询中间有增删改增删改会 flushCache默认查询参数不同CacheKey 不同配置了 useLocalCachefalse全局关闭9.3 #{id} 和 ${id} 的区别对比项#{}${}处理方式PreparedStatement 占位符字符串拼接SQL 注入安全有风险适用场景值参数表名、排序字段等动态结构性能可复用预编译每次重新拼接9.4 为什么 MyBatis 查不出数据时会脏读到上一次的结果缓存穿透隐患同一个 SqlSession 内先查询出结果再执行一次返回空结果的查询如果参数和 SQL 完全一致CacheKey 相同会命中一级缓存返回旧数据。实践中批量处理大查询时要留意必要时调clearCache()或配置flushCache。十、总结MyBatis 的执行链路一句话概括代理MapperProxy 伪造接口实现→ MappedStatement接口方法 ↔ SQL 绑定→ Executor缓存 执行→ StatementHandler参数绑定→ ResultSetHandler结果映射记住这张图遇到接口方法没实现却能跑“SQL 怎么被替换成占位符”为什么查了缓存还慢这类问题就能直接定位到是哪个环节。框架并不神秘神秘的是你不去读它的源码。
返回列表