解密MyBatis Mapper接口的Spring注入机制

解密MyBatis Mapper接口的Spring注入机制
1. 为什么MyBatis Mapper接口能被Autowired注入第一次在Spring项目里写MyBatis代码时发现一个神奇的现象我们定义的Mapper接口既没有实现类也没有用Component等注解标记却能被Autowired直接注入使用。这背后到底发生了什么今天我们就来彻底解密这个看似简单却暗藏玄机的机制。提示这个问题看似基础但能考察对Spring和MyBatis整合原理的理解深度是面试中的高频考点1.1 从现象看本质先看一个典型的使用场景RestController public class UserController { Autowired // 这里注入的是接口 private UserMapper userMapper; GetMapping(/users/{id}) public User getUser(PathVariable Long id) { return userMapper.selectById(id); } }UserMapper只是一个接口public interface UserMapper { Select(SELECT * FROM user WHERE id #{id}) User selectById(Long id); }这里至少有三大疑问接口为什么能被实例化实例化过程发生在哪个阶段SQL语句是如何被执行的1.2 MyBatis-Spring整合的核心机制答案藏在MyBatis-Spring整合包的MapperFactoryBean中。这个类实现了Spring的FactoryBean接口是关键所在。当我们配置了MapperScan或者声明了MapperScannerConfigurer时Spring会为每个Mapper接口创建一个MapperFactoryBean。具体工作流程Spring容器启动时扫描到Mapper接口为每个接口创建对应的MapperFactoryBeanMapperFactoryBean的getObject()方法返回的是MyBatis动态代理生成的实现类这个代理对象最终被注册到Spring容器中// 简化版的MapperFactoryBean核心逻辑 public class MapperFactoryBeanT implements FactoryBeanT { private ClassT mapperInterface; public T getObject() { return sqlSession.getMapper(this.mapperInterface); } public ClassT getObjectType() { return this.mapperInterface; } }2. 动态代理的魔法2.1 JDK动态代理的实现MyBatis使用的是JDK动态代理技术。当调用sqlSession.getMapper()时MyBatis会创建一个实现了Mapper接口的代理对象。这个代理对象的方法调用会被MapperProxy拦截public class MapperProxyT implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) { // 1. 解析方法对应的SQL语句 // 2. 处理参数绑定 // 3. 执行SQL并处理结果 } }2.2 方法调用全过程以一个简单的查询为例userMapper.selectById(1L);实际执行流程代理对象接收到方法调用根据方法签名找到对应的MappedStatement创建BoundSql对象处理参数和SQL模板通过Executor执行数据库操作使用ResultHandler处理返回结果注意这里的方法调用链涉及多个关键组件包括SqlSession、Executor、StatementHandler等3. Spring的依赖注入机制3.1 Autowired的工作原理Spring处理Autowired注入时的大致流程根据类型查找候选Bean如果找到唯一候选则直接注入如果有多个候选则根据名称等条件筛选如果找不到则根据required属性决定是否报错对于Mapper接口类型是接口类型实际Bean是MapperFactoryBean创建的代理对象Spring会处理好这个转换关系3.2 与普通Bean的区别普通Bean的注入流程graph TD A[定义Bean] -- B[实例化] B -- C[属性填充] C -- D[初始化] D -- E[放入容器]Mapper Bean的注入流程graph TD A[定义接口] -- B[创建MapperFactoryBean] B -- C[getObject返回代理] C -- D[注册代理对象]关键区别在于普通Bean是直接实例化类Mapper Bean是通过工厂模式间接创建代理4. 常见问题与解决方案4.1 注入失败的可能原因未配置Mapper扫描SpringBootApplication MapperScan(com.example.mapper) // 这个注解不能少 public class Application {}多数据源冲突 当配置多个数据源时需要明确指定每个Mapper使用的SqlSessionTemplateBean public MapperScannerConfigurer mapperScannerConfigurer() { MapperScannerConfigurer configurer new MapperScannerConfigurer(); configurer.setSqlSessionTemplateBeanName(primarySqlSessionTemplate); configurer.setBasePackage(com.example.mapper.primary); return configurer; }接口方法定义问题方法名与XML中的id不匹配参数类型不匹配返回类型不匹配4.2 性能优化建议批量操作Insert(script INSERT INTO user(name,age) VALUES foreach collectionlist itemitem separator, (#{item.name},#{item.age}) /foreach /script) void batchInsert(ListUser users);二级缓存配置cache evictionLRU flushInterval60000 size512 readOnlytrue/延迟加载resultMap iduserMap typeUser collection propertyorders columnid selectcom.example.mapper.OrderMapper.findByUserId fetchTypelazy/ /resultMap5. 深入原理从启动到执行5.1 Spring启动阶段MapperScan处理解析basePackage注册MapperScannerConfigurer接口扫描扫描指定包下的接口过滤出符合条件的Mapper接口Bean定义注册为每个Mapper接口创建MapperFactoryBean定义设置必要的属性sqlSessionFactory等5.2 首次调用过程当第一次调用Mapper方法时触发MapperFactoryBean.getObject()通过SqlSession.getMapper()获取代理MyBatis创建MapperProxy实例解析对应的XML或注解SQL构建MappedStatement并缓存5.3 SQL执行流程完整的方法调用链MapperProxy.invoke()MapperMethod.execute()SqlSession.selectOne()Executor.query()StatementHandler.prepare()ParameterHandler.setParameters()ResultSetHandler.handleResultSets()6. 高级话题自定义扩展6.1 自定义拦截器实现一个执行时间统计拦截器Intercepts({ Signature(type Executor.class, methodquery, args{MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}) }) public class PerformanceInterceptor implements Interceptor { public Object intercept(Invocation invocation) { long start System.currentTimeMillis(); Object result invocation.proceed(); long end System.currentTimeMillis(); System.out.println(执行耗时 (end - start) ms); return result; } }注册拦截器plugins plugin interceptorcom.example.PerformanceInterceptor/ /plugins6.2 动态数据源切换结合AbstractRoutingDataSource实现public class DynamicDataSource extends AbstractRoutingDataSource { protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceType(); } }配合AOP实现Mapper层切换Aspect Component public class DataSourceAspect { Before(annotation(targetDataSource)) public void switchDataSource(JoinPoint point, TargetDataSource targetDataSource) { DataSourceContextHolder.setDataSourceType(targetDataSource.value()); } }7. 最佳实践与避坑指南7.1 接口设计规范方法命名查询select/find/get 条件插入insert/save更新update删除delete/remove参数设计简单查询直接使用基本类型参数复杂查询使用DTO对象批量操作使用List或数组返回类型单条记录直接返回实体类型多条记录返回List分页查询返回Page7.2 常见坑点参数绑定问题// 错误示范 Select(SELECT * FROM user WHERE name #{name} AND age #{age}) User findByNameAndAge(String name, int age); // 正确做法使用Param User findByNameAndAge(Param(name) String name, Param(age) int age);动态SQL注入风险// 危险存在SQL注入风险 Select(SELECT * FROM user WHERE ${column} #{value}) User findByColumn(Param(column) String column, Param(value) String value); // 安全做法避免使用${}或者严格过滤输入懒加载问题// 在Service层调用 User user userMapper.findById(1L); // 这里可能会抛出异常因为Session已关闭 System.out.println(user.getOrders().size()); // 解决方案 // 1. 使用join查询一次性获取 // 2. 在事务范围内访问关联对象 // 3. 配置OpenSessionInViewFilter8. 性能调优实战8.1 批量操作优化原始方式性能差for (User user : userList) { userMapper.insert(user); }优化方案1使用BatchExecutor// 配置方式 Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH); } // 使用方式 try (SqlSession session sqlSessionFactory.openSession(ExecutorType.BATCH)) { UserMapper mapper session.getMapper(UserMapper.class); for (User user : userList) { mapper.insert(user); } session.commit(); }优化方案2使用foreach动态SQL见4.2节8.2 缓存配置技巧本地缓存配置!-- 开启二级缓存 -- settings setting namecacheEnabled valuetrue/ /settings !-- Mapper级别缓存 -- mapper namespacecom.example.mapper.UserMapper cache typeorg.mybatis.caches.ehcache.EhcacheCache/ /mapper分布式缓存集成Redispublic class RedisCache implements Cache { private final ReadWriteLock readWriteLock new ReentrantReadWriteLock(); private String id; private JedisPool jedisPool; public RedisCache(String id) { this.id id; this.jedisPool new JedisPool(redis-server); } // 实现Cache接口的各种方法 }9. 与MyBatis-Plus的对比9.1 基础CRUD差异MyBatis原生方式public interface UserMapper { Insert(INSERT INTO user(name,age) VALUES(#{name},#{age})) int insert(User user); Select(SELECT * FROM user WHERE id #{id}) User selectById(Long id); }MyBatis-Plus方式public interface UserMapper extends BaseMapperUser { // 基础CRUD方法已自动继承 } // 使用示例 userMapper.selectById(1L); userMapper.insert(new User(Tom, 20));9.2 条件构造器对比MyBatis条件查询Select(script SELECT * FROM user WHERE if testname ! nullname #{name}/if if testage ! nullAND age #{age}/if /script) ListUser selectByCondition(Param(name) String name, Param(age) Integer age);MyBatis-Plus条件查询// 使用QueryWrapper QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(name ! null, name, name) .eq(age ! null, age, age); userMapper.selectList(wrapper); // 使用Lambda表达式 LambdaQueryWrapperUser lambdaWrapper Wrappers.lambdaQuery(); lambdaWrapper.eq(User::getName, name) .eq(age ! null, User::getAge, age); userMapper.selectList(lambdaWrapper);10. 源码解析关键点10.1 MapperRegistry核心注册逻辑public class MapperRegistry { private final MapClass?, MapperProxyFactory? knownMappers new HashMap(); public T T getMapper(ClassT type, SqlSession sqlSession) { MapperProxyFactoryT mapperProxyFactory (MapperProxyFactoryT) knownMappers.get(type); return mapperProxyFactory.newInstance(sqlSession); } }10.2 MappedStatementSQL语句的运行时表示public final class MappedStatement { private String resource; private SqlSource sqlSource; private SqlCommandType sqlCommandType; private Class? parameterType; private Class? resultType; // 其他重要字段... }10.3 SqlSessionTemplateSpring集成关键类public class SqlSessionTemplate implements SqlSession { private final SqlSessionFactory sqlSessionFactory; private final ExecutorType executorType; private final SqlSessionInterceptor interceptor; // 代理所有SqlSession方法 public T T selectOne(String statement) { return this.sqlSessionProxy.selectOne(statement); } private class SqlSessionInterceptor implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) { SqlSession sqlSession getSqlSession(); try { return method.invoke(sqlSession, args); } finally { closeSqlSession(sqlSession); } } } }在实际项目中理解这些核心类的交互方式能帮助我们更好地排查问题并进行定制开发。比如当遇到性能问题时可以检查Executor的类型是否正确当需要扩展功能时可以基于拦截器机制实现自定义逻辑。