MyBatis动态代理与Mapper接口原理深度解析
“你的Mapper接口明明没有实现类为什么Spring容器里能注入进去调方法还能正常返回数据” —— 这是我带过的很多新人都会问的问题。其实答案不复杂你看到的Mapper接口在运行时被动态代理成了一个代理对象。真正干活的是代理对象里的InvocationHandler它拿着你的方法名去和SQL映射绑定再交给SqlSession执行。这整套机制就是MyBatis最核心的“动态代理与Mapper接口”联动。今天我把这块原理和实战拆开讲清楚既有底层的代理机制也有我自己踩过的坑。标题里的“动态代理”在Java里指两条技术线JDK动态代理和CGLIB。MyBatis用的是JDK动态代理因为Mapper本身就是接口JDK动态代理天生就是为接口服务的。明白了这一点很多设计上的“为什么”就通了。这篇文章适合两类人看一是被Mapper注入和调用搞糊涂的Java新手二是想把MyBatis底层看透、之后遇到诡异报错能快速定位的开发者。我会从整体设计、核心细节、手动模拟实现再到问题排查一步步带你走一遍。1. 项目整体设计与思路拆解1.1 为什么Mapper接口必须搭配动态代理如果你写过原生JDBC一定体验过那套繁琐流程注册驱动、拿Connection、拼Statement、填参数、遍历ResultSet、手动映射到对象。MyBatis把这些都封装了但对使用者来说最直观的改进是“只写接口 SQL映射文件/注解”就能完成数据库操作。这里的关键问题是Java的接口没有实现类Autowired注入的时候Spring到底给你注了个什么答案是一个Proxy对象。MyBatis启动时会把每个Mapper接口注册到MapperRegistry每个接口对应一个MapperProxyFactory。当你要获取某个Mapper实例时工厂调用Proxy.newProxyInstance()传入接口类、一个MapperProxy实现了InvocationHandler、还有一个类加载器从而生成一个实现该接口的代理对象。这个代理对象拦截你对接口方法的所有调用然后在MapperProxy.invoke()里把方法信息方法名、参数类型转换成一条SQL操作交给SqlSession执行。这样做的好处很明显你不需要写Mapper接口的实现类MyBatis自动替你完成了“方法调用 → SQL执行 → 结果映射”的链路。如果不用动态代理你就得自己写一堆模板代码比如为每个接口写一个UserMapperImpl里面调用SqlSession.selectOne(namespace.methodId, param)——这恰恰是MyBatis想要消灭的样板代码。所以动态代理不是花哨技巧而是这个框架的地基。1.2 整体方案选型背后的考量有人可能会问为什么MyBatis不用CGLIB动态代理CGLIB是通过生成子类来实现代理的它不要求目标对象是接口。但MyBatis的Mapper就是接口JDK动态代理直接基于接口生成Proxy类没有任何继承上的限制而且JDK动态代理是Java原生支持不需要引入额外的字节码库。更关键的是每个Mapper接口的“实现逻辑”并不在某个具体类里而是分布在SQL映射文件和注解中需要的是方法到SQL的映射能力JDK动态代理天然地能在invoke方法里统一处理这些逻辑。相比之下CGLIB适合代理那些没有接口的普通类比如Spring对Bean的AOP增强。在这类场景里选JDK动态代理既符合“面向接口编程”的设计也让框架核心更快更轻。此外MyBatis把SQL操作分成了两种模式XML映射和注解。注解模式下SQL直接写在接口方法上XML模式下SQL写在Mapper XML文件里通过namespace和id定位。这两种模式的统一入口都在MapperProxy.invoke()里方法被调用时MyBatis缓存了该方法的MapperMethod对象MapperMethod.execute()内部根据SQL类型增删改查调用SqlSession的不同方法执行完成后再用ResultSetHandler完成对象映射。这个设计非常巧妙把“接口方法层”和“SqlSession执行层”解耦了。1.3 一次Mapper方法调用到底经历了什么为了后续实操你脑子里必须先有一个时序概念。假设我有一个UserMapper接口里面定义了User selectById(Integer id)XML里对应了一条select语句。当你调用userMapper.selectById(1)时步骤拆开来看是这样的Spring容器里的userMapper实际上是一个JDK动态代理对象它实现了UserMapper接口。JVM把selectById(Integer)调用拦截到MapperProxy.invoke()方法传参包括代理对象、Method对象、参数数组。如果这个方法不是Object类的方法比如toString()MapperProxy会先从缓存中取出对应的MapperMethod。MapperMethod里有SqlCommand记录SQL类型和ID和MethodSignature记录参数和返回类型。MapperMethod.execute()判断SQL类型如果是SELECT调用sqlSession.selectList/selectOne等如果是INSERT/UPDATE/DELETE调用对应的sqlSession.update/insert/delete。SqlSession拿着语句ID去Configuration里找到对应的MappedStatement由执行器Executor通过JDBC执行SQL并处理参数和结果集。结果通过ResultSetHandler映射成User对象或ListUser返回。我之所以先把这个链路讲清楚是因为后面对“Invalid bound statement”这类问题的排查完全建立在它之上当你看到的报错是“找不到对应SQL”实际上就是第4步里的语句ID没有在Configuration里找到对应的MappedStatement也就是namespace.id和Mapper接口全限定名没有对齐。2. 核心细节解析与实操要点2.1 JDK动态代理的底层原理手写一个验证先动手写一个最简单的JDK动态代理Demo这能帮你理解MyBatis到底做了什么。假设我有个接口public interface UserMapper { User selectById(Integer id); }再写一个调用处理器import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class MapperProxyHandler implements InvocationHandler { Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getDeclaringClass().equals(Object.class)) { // 对Object的方法走原生逻辑 return method.invoke(this, args); } System.out.println(拦截到方法 method.getName()); System.out.println(参数是 (args null ? null : args[0])); // 这里模拟返回一个User对象 User user new User(); user.setId((Integer) args[0]); user.setName(模拟用户); return user; } }然后生成代理对象UserMapper userMapper (UserMapper) Proxy.newProxyInstance( UserMapper.class.getClassLoader(), new Class[]{UserMapper.class}, new MapperProxyHandler() ); User user userMapper.selectById(1); System.out.println(user.getName());运行后输出拦截到方法selectById 参数是1 模拟用户这个Demo虽然简单但它已经复现了MyBatis的核心机制接口本身没有实现类但动态代理对象实现了这个接口并在invoke里拦截方法调用。真正的MyBatis只不过是把“模拟返回一个User”替换成了“从配置里找SQL并让SqlSession去执行”。这里有个新人容易忽略的细节invoke方法里第一个参数proxy是代理对象本身千万别在代理方法里去调用proxy.xxx()否则会掉进无限递归。判断方法是不是Object自带的方法可以通过method.getDeclaringClass()来区分或者直接判断方法名比如放行toString、hashCode、equals。2.2 MapperProxyFactory和MapperProxy的源码级拆解明白了JDK动态代理之后再看MyBatis内的两个核心类就轻松了。在MyBatis的binding包下有四个相关的类MapperRegistryMyBatis启动时扫描Mapper接口往这个注册表里放ClassT到MapperProxyFactoryT的映射。它对外提供getMapper(ClassT type, SqlSession sqlSession)方法。MapperProxyFactoryT工厂类在newInstance(SqlSession sqlSession)里创建MapperProxy再调用Proxy.newProxyInstance生成最终代理实例。MapperProxyT实现了InvocationHandler持有SqlSession内部用MapMethod, MapperMethodInvoker缓存方法调用器。MapperMethod封装了方法对应的SQL命令和参数/返回签名信息。关键逻辑在MapperMethod的execute里它会对SqlCommand的SqlCommandType做switch判断。以SELECT为例如果返回类型是集合或数组就调用selectList如果返回类型是Cursor调用selectCursor否则调用selectOne。如果结果是Map且配置了mapKey则遍历结果封装为多个Map。INSERT/UPDATE/DELETE走的是sqlSession.insert/update/delete并且会先处理Options(useGeneratedKeystrue)等注解自动填充主键。MyBatis之所以把MapperMethod缓存起来是因为每次调用接口方法都要解析方法签名和SQL命令成本太高。你想想一个selectById可能需要判断返回类型、判断参数类型、是否有多结果等这些信息其实是静态不变的所以放在ConcurrentHashMap里做缓存最合适。这个缓存的key是Method对象value是调用了反射初始化的MapperMethodInvoker后者持有MapperMethod和SqlSession操作。2.3 参数绑定和ResultSet映射的细节动态代理的另一个核心工作就是参数绑定。普通方法调用里你传入的是一个对象或多个参数但JDBC执行时需要的是一组预编译参数。MyBatis的处理是这样如果Mapper方法只有一个参数那么这个参数对象本身作为参数上下文如果有多个参数MyBatis会把它们封装成ParamMap并额外添加两个键param1、param2……和arg0、arg1……旧版本是0、1。这就是为什么在XML里你可以写#{param1}或#{arg0}也可以配合Param(userId)来自定义键。来看一个常见的坑你写了个方法User selectByNameAndAge(String name, Integer age)XML里写#{name}和#{age}如果没加ParamMyBatis是解析不到name这个键的。因为多个参数被封装成了ParamMap键是arg0/param1没有name。正确做法是加Param(name)和Param(age)。这一点搞懂了你以后不会再被“Parameter name not found. Available parameters are [arg0, param1]”这类的报错卡半天。ResultSet映射这块MyBatis默认的策略叫autoMapping它会把SQL列的别名转换成属性名。比如数据库列名user_name如果你的实体属性是userName那么需要开启mapUnderscoreToCamelCase配置否则就要写resultMap做映射。这里我建议新手直接用resultMap对于复杂的连表查询、一对一、一对多更可控。动态代理里返回值除了普通POJO也能处理List、Map、Cursor、Optional这些都在MapperMethod的返回类型判断里做了分支。2.4 接口上的默认方法与lambda表达式官方MyBatis还允许你在Mapper接口里定义default方法或者静态方法。这对扩展接口通用逻辑很有用比如你想定义一个默认方法default ListUser selectAllOrdered() { return selectAll(create_time desc); }。但实际上MyBatis对接口方法是否代理也有自己的判断MapperProxy.invoke()里如果方法声明自Object直接放行如果是default方法Java 8则会调用InvocationHandler的默认方法逻辑MyBatis通过MethodHandle或者Method.invoke调用默认实现只有普通抽象方法才会进入MapperMethod逻辑。有一点需要注意在默认方法里如果调用了其他抽象Mapper方法会再次经过代理这个需要用普通的自调用方式通过this来调用实际上this是代理对象能正常转发。lambda这块MyBatis的Mapper接口本身不是函数式接口所以一般不会配合lambda直接用但你可以把Mapper方法作为方法引用传出去例如userMapper::selectById这时候方法引用绑定的是代理对象的方法调用时会走代理逻辑没有任何问题。3. 实操过程与核心环节实现3.1 动手搭一个最小可运行的MyBatis脚本纸上谈兵没意思我建议你自己在本地搭一个最小工程。不需要Spring Boot直接纯MyBatis 一个测试类把动态代理跑起来。工程依赖就两个MyBatis和对应数据库驱动。我用的是H2内存数据库方便演示。pom.xml里大致这样dependencies dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId version2.1.214/version /dependency /dependencies然后建表和数据CREATE TABLE user_info ( id INT PRIMARY KEY, name VARCHAR(32) ); INSERT INTO user_info (id, name) VALUES (1, 张三), (2, 李四);接着创建一个UserInfo实体public class UserInfo { private Integer id; private String name; // getter/setter 省略 }然后定义Mapper接口public interface UserInfoMapper { UserInfo selectById(Integer id); Integer insertUser(UserInfo userInfo); }再写对应的Mapper XML文件注意namespace必须写接口类的全限定名mapper namespacecom.example.mapper.UserInfoMapper select idselectById resultTypecom.example.entity.UserInfo SELECT id, name FROM user_info WHERE id #{id} /select insert idinsertUser parameterTypecom.example.entity.UserInfo INSERT INTO user_info (id, name) VALUES (#{id}, #{name}) /insert /mapper最后写一个测试类重点看SqlSession.getMapper()这一步public class MapperStartTest { public static void main(String[] args) throws Exception { String resource mybatis-config.xml; InputStream inputStream Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession session sqlSessionFactory.openSession(true)) { UserInfoMapper userInfoMapper session.getMapper(UserInfoMapper.class); System.out.println(代理类型 userInfoMapper.getClass()); UserInfo userInfo userInfoMapper.selectById(1); System.out.println(查询结果 userInfo.getName()); } } }运行后在控制台你会看到代理类型com.sun.proxy.$Proxy4或者jdk.proxy1.$Proxy0之类的输出这就说明你拿到的确实是JDK动态代理对象。3.2 手写一个简化版动态代理框架彻底吃透原理如果你觉得直接用MyBatis还是黑盒那就跟我一起实现一个极简版的“动态代理Mapper框架”。目的不是重新造轮子而是让你真正理解那几步是怎么粘起来的。先定义一个Select注解表示方法对应一条查询SQLRetention(RetentionPolicy.RUNTIME) Target(ElementType.METHOD) public interface Select { String value(); }再定义Mapper接口public interface UserMapper { Select(SELECT id, name FROM user WHERE id #{id}) User selectById(int id); }实现一个MapperFactory工具类核心逻辑如下public class SimpleMapperFactory { private final MapClass?, Object proxyCache new ConcurrentHashMap(); public T T getMapper(ClassT mapperInterface) { if (!mapperInterface.isInterface()) { throw new IllegalArgumentException(必须是接口); } return (T) proxyCache.computeIfAbsent(mapperInterface, this::createProxy); } private Object createProxy(Class? mapperInterface) { InvocationHandler handler (proxy, method, args) - { Select select method.getAnnotation(Select.class); if (select null) { throw new RuntimeException(没有SQL定义 method.getName()); } String sql select.value(); System.out.println(执行SQL: sql); // 这里简化参数绑定假设只有一个参数 Integer id (Integer) args[0]; System.out.println(绑定参数: id); // 模拟数据库返回 User user new User(); user.setId(id); user.setName(模拟用户 id); return user; }; return Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[]{mapperInterface}, handler ); } }测试运行SimpleMapperFactory factory new SimpleMapperFactory(); UserMapper userMapper factory.getMapper(UserMapper.class); User user userMapper.selectById(10);输出如下执行SQL: SELECT id, name FROM user WHERE id #{id} 绑定参数: 10 模拟用户10你看这个简化版和我之前第一段代码本质上是一样的。真正MyBatis的差异只是把执行SQL这段换成了真正用SqlSession去拿连接、预编译、绑定参数、执行查询、遍历ResultSet。通过这个练习你应该能真正意识到所谓Mapper接口有没有实现类这件事其实根本不存在障碍JVM早就用动态代理帮我们补了一个“隐形的实现”。3.3 在Spring Boot里跑起来并查看真实代理现状实际工作里大多数情况是在Spring Boot中整合MyBatis。你的UserMapper通过MapperScan扫描注册到Spring容器然后Autowired注入。如果你好奇注入的到底是什么可以在启动后随便找个地方打印classSpringBootTest public class MapperDebugTest { Autowired private UserMapper userMapper; Test public void printMapperClass() { System.out.println(userMapper.getClass()); // 输出类似com.sun.proxy.$Proxy61 或 jdk.proxy2.$Proxy13 } }如果某个Mapper方法被Spring的AOP也拦截了比如加了缓存、事务注解那么实际注入的可能是多层代理外层是Spring AOP的JDK动态代理内层才是MyBatis的MapperProxy。这也是为什么你在调试时看到Proxy对象一层套一层的原因。排查问题时如果用proxy调用toString()可能触发多层代理注意不要死循环。Spring Boot配置里的mapper-locations要指向你的XML目录比如mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置特别重要我建议能开就开可以少写很多resultMap。但开了之后要注意如果数据库字段本身就是驼峰命名而且你开了这个开关它依然会尝试把下划线转驼峰对没有下划线的字段没影响。3.4 参数计算与重载问题的工程决策在日常开发里Mapper方法重载是个高频坑。接口里写两个同名方法参数不同User selectByName(String name); User selectByName(String name, int age);XML里给这两个方法分别写selectByName和selectByName:1之类的MyBatis是不允许同一Mapper中同名方法有多个映射的因为statementId是namespace methodName无法区分重载方法。你在XML里如果写两个idselectByName运行时会直接报“ParameterMappedStatement already exists”。所以我在团队规范里明确要求Mapper接口禁止方法重载同名方法必须改名后加后缀。这个决策背后的原因是动态代理的方法签名缓存虽然能区分不同方法但SQL映射注册的key却只有方法名。MyBatis的MapperRegistry在注册MapperAnnotationBuilder阶段解析每个方法时会直接用method.getName()作为id用接口全限定名 方法名作为statementId。一旦重载第二个方法即使参数不同也会覆盖前一个MapperMethod缓存里的Method对象也会和MappedStatement的key对不上很容易出现“执行对了方法但SQL拿错”或者直接启动失败。所以在设计Mapper接口时命名时要带有业务语义比如selectByName和selectByNameAndAge而不是依赖重载。再有一个隐藏的点接口的返回类型和泛型。MyBatis在解析方法签名时会取泛型参数比如ListUser selectAll();如果你用继承父接口的方式定义泛型例如public interface BaseMapperT { T selectById(Integer id); } public interface UserMapper extends BaseMapperUser {}动态代理在运行时要正确推断T是User否则结果映射时会用Object处理。MyBatis实际上通过MapperMethod.MethodSignature里的TypeParameterResolver来解析泛型它会把泛型擦除之前的实际类型还原回来。这里涉及复杂泛型时偶尔有绕不开的坑建议新手不要过度设计通用Mapper等把泛型解析摸透了再抽象。4. 常见问题与排查技巧实录4.1 “Invalid bound statement (not found)”到底在说什么这个报错是Mapper动态代理相关里出现频率第一的问题。它的含义是代理调用了UserMapper.selectById但在Configuration里找不到对应的statement。说到底就是namespace.id对不上或者XML压根没有被加载。排查口诀我总结为三步看namespace是否等于Mapper接口的全限定名。例如接口是com.example.mapper.UserMappermapper namespacecom.example.mapper.UserMapper这样写才是对的。漏掉一个字符、写成了com.example.dao.UserMapper、或者只写了UserMapper都会导致找不到。看id是否等于Mapper接口的方法名。方法名拼写不一致、大小写不一致都会挂。尤其注意方法名拼写和XML里的id要保持完全一致包括大小写。看XML是否被扫描到。在Spring Boot配置里检查mybatis.mapper-locations确认路径是否正确比如classpath:mapper/*.xml并且XML文件确实在对应的目录下。如果用了Maven多模块别忘了资源路径是否包含xml后缀有时候还得在pom里做资源过滤配置。这类报错还有一个变种“Result Maps collection already contains value for ...”或者是“Mapper method xxx has an unsupported return type”这些其实也都是配置或方法签名层面的问题排查思路类似先说配置再看方法。4.2 为什么我拿到的代理对象直接调用toString会出现奇怪行为因为MapperProxy.invoke()对Object方法做了放行如果你没有正确放行就可能在调用toString时进入MapperMethod逻辑导致它尝试去查SQL然后报“Bound statement not found”。在MyBatis源码里invoke方法开头有一段很关键的判断if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); }这段的存在就是为了让代理对象的toString、hashCode、equals保持Object的默认行为。所以如果你自定义一个Mapper接口恰好定义了一个toString()接口方法这本身可以但意义不大那么该方法会被当做普通Mapper方法处理映射SQL时就会和Object类的toString冲突或者走代理逻辑极容易踩坑。我的建议是Mapper接口里除了数据库操作不要定义任何和Object类同名的方法也不要去重写toString。你可以在调试时打印代理对象的toString()但别指望它告诉你业务信息它只会返回类似com.example.mapper.UserMapper1234abcd。4.3 方法级缓存与SqlSession范围导致的坑动态代理生成的Mapper实例和SqlSession是绑定的。每一次session.getMapper()都会创建一个新的代理对象内部持有同一个SqlSession。所以如果你在Spring中配置了一级缓存默认同一个SqlSession内执行相同SQL、相同参数会命中本地缓存但如果两个不同线程各自拿到不同SqlSession那一级缓存互相不可见。这个和动态代理本身没有直接关系但容易造成“我同一个Mapper调同一个方法两次第二次没更新”的困惑。在Spring整合MyBatis时默认是SqlSessionTemplate来管理SqlSession的每次从容器里拿到的UserMapper代理对象虽然是同一个因为Spring把它做成单例但代理内部的SqlSession是通过SqlSessionTemplate动态获取当前事务绑定的SqlSession的。如果你在事务里执行两次查询命中一级缓存事务结束后再查询缓存失效。理解了代理和SqlSession的绑定关系这个行为就能顺理成章地解释。排查这种问题的时候我习惯直接把SqlSession换成SqlSessionTemplate或者用Transactional(propagation Propagation.NOT_SUPPORTED)来验证是否是缓存问题。在压测环境里遇到“数据不刷新”八成是一级缓存导致必要的时候可以设置localCacheScopeSTATEMENT关闭一级缓存。4.4 动态代理类怎么真切地看到它的字节码想知道代理类具体长什么样可以在JDK 8里用-Dsun.misc.ProxyGenerator.saveGeneratedFilestrue或者JDK 17里用-Djdk.proxy.ProxyGenerator.saveGeneratedFilestrue把代理类字节码落盘到指定的com/sun/proxy目录里。更简单的办法是复制下面的代码直接在测试里反编译Proxy.getInvocationHandler(userMapper)但要注意如果这个Mapper代理被Spring AOP再次包装过你拿到的handler是Spring的不是MyBatis的MapperProxy。想看到MyBatis真正的MapperProxy可以用(AopProxyUtils.getSingletonTarget(userMapper))或者在Spring AOP暴露目标对象的情况下拿原始目标对象。说这个不是让你去改框架源码而是当你们遇到“代理嵌套导致方法匹配异常”时能有一个清晰的定位工具。还有一个实用的调试技巧在MapperProxy的invoke方法上打断点然后调用Mapper方法你会在Idea的栈帧里看到非常完整的方法调用链。通过断点你可以看到MapperMethod、SqlCommand、MethodSignature等对象的具体内容。这是我排查动态代理问题最常用的一招远比看源码来得直观。4.5 自调用类的坑代理类方法内部调用另一个Mapper方法假设你在一个Service类里注入UserMapper然后在某个Service方法里执行了User user this.userMapper.selectById(1);这没问题。但如果你写了这样一个Service方法Transactional public void doSomething() { this.updateUser(user); this.updateLog(log); // 用的是同一个Mapper? }如果两个方法在同一个Mapper接口里你想在updateUser方法里面去调用同接口的updateLog方法写法一般是this.updateLog(log)这依然走动态代理吗取决于this是什么。如果你在Service里this是Spring Bean的实例而不是代理除非开启了AOP自调用那没有问题。如果你在Mapper接口的default方法里用this调用同接口抽象方法这时this实际上是代理对象所以会正常走代理逻辑。这个区别容易混淆记住一点所谓“自调用不走代理”只有在一个普通类内部通过this调用自身方法时才会发生Spring AOP场景下尤其典型对于MyBatis Mapperthis通常就是代理对象所以不存在这个问题。4.6 我使用过程中的一点补充心得下面列几个我组内新人常踩的问题做一个简洁速查表现象根本原因处理方案注入Mapper为null扫描路径不对容器没生成代理Bean检查MapperScan或Mapper注解位置调方法提示Invalid bound statementnamespace/id或XML扫描配置错误按4.1三步排查参数绑定报错Available parameters多参数没有Param加Param注解查询返回null但SQL能执行resultMap字段和属性不匹配开启驼峰映射或配置resultMap接口方法同名重载导致statement冲突避免重载方法改名默认方法没有走代理非抽象方法本身不映射SQL确认是否误用default方法最后补充一个我自己的习惯日常调试时不要为了省事把Mapper接口里的方法全部堆在XML里这是一件很危险的事。我更倾向给每个Mapper对应一个XML并且XML文件名和接口名保持一致。当动态代理逻辑出问题时一眼就能定位到对应的XML文件不用满项目搜索。用注解写SQL虽然省事但复杂SQL的可读性差团队也该有统一规范我这里就不再展开。说到底动态代理与Mapper接口这套组合真正的难点不在“代理”这个概念本身而在于它把“接口声明、SQL映射、参数绑定、结果映射”四件事串成了一条流水线。你在熟悉了JDK动态代理的基本原理之后再去看MyBatis的binding包源码会发现一切都是那么自然。我遇到过一次很奇特的线上问题某个Mapper方法偶尔报“Cannot invoke because this.mapperInstance is null”后来发现是有人直接序列化了Mapper代理对象反序列化后MapperProxy的SqlSession字段丢失了。这种属于极端情况但也说明一个道理代理对象不是一个普通的POJO它一定和SqlSession、Configuration有强关联不能随便脱离上下文使用。你在日常开发里只要把这条链路理解透就没有什么诡异报错能难住你。

相关新闻

基于YOLOv8的社区电动车进电梯预警系统:从模型训练到RK3588边缘部署全链路

基于YOLOv8的社区电动车进电梯预警系统:从模型训练到RK3588边缘部署全链路

简介:这份资源面向计算机视觉与人工智能方向的在校学生、教师及企业开发者,提供一套基于YOLOv8的社区电动车进电梯预警系统完整实现,可用于毕业设计、课程设计、大作业或项目初期立项演示。压缩包共97个文件,约24.21MB&#xff0c…

2026/10/12 1:50:00 阅读更多 →
YOLO垃圾四分类数据集训练前必查的细节与避坑指南

YOLO垃圾四分类数据集训练前必查的细节与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 1:50:00 阅读更多 →
分治三角剖分算法实战:从点集到网格,省掉两周试错

分治三角剖分算法实战:从点集到网格,省掉两周试错

简介:这份资源围绕三角剖分算法展开,重点讲解如何用分治法实现 Delaunay 三角剖分,面向计算机图形学、几何计算与科学计算方向的学习者和开发者,适合已具备一定数据结构与算法基础、希望深入理解剖分原理与工程实现的人群。压缩包…

2026/10/12 1:50:00 阅读更多 →

最新新闻

高校学籍管理系统落地实战:从需求文档到数据库设计与状态流转

高校学籍管理系统落地实战:从需求文档到数据库设计与状态流转

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:40:30 阅读更多 →
数据库课程设计航空订票系统:从ER图到MySQL事务与并发避坑指南

数据库课程设计航空订票系统:从ER图到MySQL事务与并发避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:40:30 阅读更多 →
串口转网络实战:TCP/UDP模式选型、心跳重连与粘包处理

串口转网络实战:TCP/UDP模式选型、心跳重连与粘包处理

简介:这份资源面向工业控制、嵌入式开发与物联网方向的工程师及学习者,聚焦串口通信与网络通信之间的双向转换问题。内容围绕RS-232、RS-485等串口标准与TCP/IP协议栈的对接展开,涵盖串口数据帧与网络数据包的互转原理、TCP与UDP在可靠性与实…

2026/10/12 2:40:30 阅读更多 →
Docker镜像导出为tar文件并跨服务器加载的实践指南

Docker镜像导出为tar文件并跨服务器加载的实践指南

直接开始写吧。见过太多人在这上面翻车了——不是导出的时候选错命令,就是另一台机器上加载完发现容器跑不起来。我搞运维这么多年,被这玩意儿坑过好多次,也帮不少人收拾过烂摊子。虽然标题里写的“Docker导出镜像为.tar文件并在另一台服务器…

2026/10/12 2:40:30 阅读更多 →
AVCaptureSession视频流捕获实战:设备枚举、帧回调与后台持续运行

AVCaptureSession视频流捕获实战:设备枚举、帧回调与后台持续运行

简介:本资源是一个基于FFmpeg API实现音视频采集与同步处理的完整C工程,面向多媒体开发初学者及iOS/macOS平台音视频应用开发者,解决摄像头图像与麦克风音频实时采集、OpenGL预览、H.264/AAC编码、MP4封装及视音频时间戳同步等核心问题。压缩…

2026/10/12 2:40:30 阅读更多 →
UIUC CS241中文讲义:系统编程从malloc到并发服务器实战指南

UIUC CS241中文讲义:系统编程从malloc到并发服务器实战指南

简介:《UIUC CS241系统编程中文讲义》是一份由ApacheCN社区翻译的开源学习资料,面向希望深入理解系统编程、操作系统底层机制和C语言与Linux交互的开发者。项目源自UIUC经典课程CS241,内容涵盖进程与线程、虚拟内存、并发同步、文件系统、网络…

2026/10/12 2:39:29 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →