MyBatis接口注入的实现原理
在SpringBoot与MyBatis的日常开发中,Mapper接口没有任何实现类,却能被Spring通过@Autowired直接注入并正常调用。原因就在于Spring的FactoryBean扩展机制与MyBatis动态代理技术的结合。
一、问题:为何接口能被直接注入?
典型的业务代码:
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(Long id);
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // 注入无实现类的接口
public User getUser(Long id) {
return userMapper.findById(id);
}
}
UserMapper是纯接口,没有任何类实现其方法。
原因就在于:Spring注入的是MyBatis通过动态代理生成的接口实现类实例,而这一实例的创建与注册,则依赖于FactoryBean机制。
二、底层原理:FactoryBean机制与动态代理的协同
1. FactoryBean:Spring的对象创建工厂
FactoryBean是Spring框架提供的一个特殊扩展接口,专门用于创建复杂对象或动态生成的对象。当Bean的创建逻辑复杂(如需要依赖多个资源、需要代理增强)时,使用FactoryBean比普通Bean定义更灵活。
接口定义:
public interface FactoryBean<T> {
T getObject() throws Exception; // 核心方法:返回最终要注入的对象实例
Class<?> getObjectType(); // 返回对象的类型
boolean isSingleton(); // 是否为单例对象
}
关键特性:当Spring容器中注册的是FactoryBean实现类时,通过@Autowired注入的不是FactoryBean本身,而是getObject()方法返回的对象实例。
2. MyBatis对FactoryBean的利用
MyBatis与Spring的整合包mybatis-spring中,核心类MapperFactoryBean实现了FactoryBean接口,完成了Mapper接口实例的创建。
其简化逻辑如下:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface; // 要代理的Mapper接口类型
@Override
public T getObject() throws Exception {
// 关键逻辑:通过SqlSession生成接口的动态代理对象
return getSqlSession().getMapper(this.mapperInterface);
}
@Override
public Class<?> getObjectType() {
return this.mapperInterface;
}
}
3. 完整实现流程:从扫描到注入
整个接口注入的流程可总结为以下5个核心步骤:
- 扫描接口:Spring Boot通过
@MapperScan注解扫描指定包下的@Mapper接口 - 注册FactoryBean:Spring为每个Mapper接口,注册一个
MapperFactoryBean到容器,并传入接口类型 - 调用工厂方法:Spring初始化Bean时,调用
MapperFactoryBean.getObject()方法 - 生成动态代理:MyBatis通过JDK动态代理,为Mapper接口生成代理实例,封装SQL执行逻辑
- 注入代理对象:Spring将代理实例返回,最终注入到业务类中
链路简化描述:
扫描@Mapper接口 → 注册MapperFactoryBean → 调用getObject() → 生成JDK动态代理 → 注入代理对象
三、手动实现:自定义FactoryBean模拟MyBatis机制
为了更深入理解,我们自己实现一个简化版的“接口注入”机制,模拟注解扫描 + FactoryBean + 动态代理的完整流程。
1. 定义核心注解
首先定义两个自定义注解,用于标记Mapper接口和SQL语句:
// 标记Mapper接口
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyMapper {
}
// 标记查询SQL
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MySelect {
String value();
}
2. 实现自定义FactoryBean
自定义MyMapperFactoryBean,核心逻辑是解析注解SQL并生成动态代理对象:
public class MyMapperFactoryBean<T> implements FactoryBean<T> {
private Class<T> mapperInterface;
private Map<String, String> sqlMap = new HashMap<>(); // 方法名 -> SQL映射
public MyMapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
this.parseAnnotations(); // 解析@MySelect注解
}
private void parseAnnotations() {
for (Method method : mapperInterface.getMethods()) {
if (method.isAnnotationPresent(MySelect.class)) {
sqlMap.put(method.getName(), method.getAnnotation(MySelect.class).value());
}
}
}
@Override
public T getObject() throws Exception {
// 核心:JDK动态代理生成接口实例
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
new MapperInvocationHandler(sqlMap)
);
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
@Override
public boolean isSingleton() {
return true;
}
// 代理调用处理器:模拟SQL执行逻辑
private static class MapperInvocationHandler implements InvocationHandler {
private Map<String, String> sqlMap;
public MapperInvocationHandler(Map<String, String> sqlMap) {
this.sqlMap = sqlMap;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String sql = sqlMap.get(method.getName());
System.out.println("执行SQL:" + sql + ",参数:" + Arrays.toString(args));
// 返回结果
return execute(sql);
}
}
}
3. 注册FactoryBean:扫描接口并绑定
通过BeanDefinitionRegistryPostProcessor实现接口扫描,并将扫描到的接口与MyMapperFactoryBean绑定:
@Configuration
public class MyMapperAutoConfig {
@Bean
public BeanDefinitionRegistryPostProcessor mapperScanner() {
return new BeanDefinitionRegistryPostProcessor() {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.scan("com.example.mapper"); // 扫描指定包
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {}
};
}
static class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
public ClassPathMapperScanner(BeanDefinitionRegistry registry) {
super(registry, false);
addIncludeFilter(new AnnotationTypeFilter(MyMapper.class));
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> holders = super.doScan(basePackages);
for (BeanDefinitionHolder holder : holders) {
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
// 关键:将Bean类型改为MyMapperFactoryBean
definition.setBeanClass(MyMapperFactoryBean.class);
// 传入接口全类名作为构造参数
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
}
return holders;
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface(); // 只扫描接口
}
}
}
4. 测试验证
测试:
@MyMapper
public interface UserMapper {
@MySelect("SELECT * FROM users WHERE id = #{id}")
User findById(Long id);
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void test() {
User user = userMapper.findById(1L);
System.out.println("查询结果:" + user);
}
}
四、Spring Boot自动配置的加持
在实际开发中,我们无需手动配置扫描器和FactoryBean,这归功于SpringBoot的自动配置机制。
MyBatis的自动配置核心类MybatisAutoConfiguration,通过条件注解完成了核心Bean的自动注册:
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer scanner = new MapperScannerConfigurer();
scanner.setBasePackage("com.example.mapper");
return scanner;
}
}
五、借鉴:FactoryBean模式的实际应用场景
MyBatis结合FactoryBean的设计,在我们日常开发中也有借鉴意义。
1. RPC接口本地透明注入
在微服务架构中,可通过该模式实现RPC调用的“本地接口化”,让业务代码感知不到远程调用:
public class RpcClientFactoryBean implements FactoryBean<Object> {
private Class<?> serviceInterface;
private String serviceName;
@Override
public Object getObject() {
return Proxy.newProxyInstance(
serviceInterface.getClassLoader(),
new Class[]{serviceInterface},
// 比如底层利用http协议发送请求
(proxy, method, args) -> RpcClient.invoke(serviceName, method.getName(), args)
);
}
@Override
public Class<?> getObjectType() {
return serviceInterface;
}
}
业务层使用时,只需注入接口,完全无需关注远程调用细节。
2. 功能开关与多版本兼容
在灰度发布、多版本功能并存的场景下,可通过FactoryBean根据配置动态返回不同实现类:
public class FeatureToggleFactoryBean implements FactoryBean<PaymentService> {
@Value("${payment.new.version:false}")
private boolean useNewVersion;
@Override
public PaymentService getObject() {
return useNewVersion ? new NewPaymentServiceImpl() : new OldPaymentServiceImpl();
}
@Override
public Class<?> getObjectType() {
return PaymentService.class;
}
}
六、总结
SpringBoot集成MyBatis时,接口能被直接注入的核心原理,是Spring FactoryBean机制与MyBatis动态代理技术的协同:
FactoryBean作为桥梁,将Mapper接口的实例化逻辑委托给MapperFactoryBean- MyBatis通过JDK动态代理,为接口生成包含SQL执行逻辑的代理实例
- SpringBoot的自动配置机制,简化了扫描和注册流程,实现开箱即用