Why MyBatis Mapper Interfaces Can Be @Autowired Like Ordinary Beans?
Case 1: Using MyBatis Alone
Create a new configuration file mybatis-solo.xml in the resources directory with the fololwing content:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<settings>
<setting name="logImpl" value="LOG4J2"/>
</settings>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/testdb"/>
<property name="username" value="root"/>
<property name="password" value="******"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/demo/mybatis/AccountMapper.xml"/>
</mappers>
</configuration>
In the resources/com/demo/mybatis folder, create AccountMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.demo.mybatis.mapper.AccountMapper">
<select id="fetchAccountById" resultType="com.demo.mybatis.entity.Account">
select * from Account where id = #{id}
</select>
</mapper>
Create the AccountMapper interface in the corresponding package:
@Mapper
public interface AccountMapper {
Account fetchAccountById(@Param("id") Long id);
}
In Java code, build SqlSessionFactory, obtain SqlSession, and then the Mapper proxy:
public static void main(String[] args) throws IOException {
String configFile = "mybatis-solo.xml";
InputStream is = Resources.getResourceAsStream(configFile);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
try (SqlSession session = factory.openSession()) {
AccountMapper mapper = session.getMapper(AccountMapper.class);
Account account = mapper.fetchAccountById(1L);
System.out.println(JSON.toJSONString(account));
}
}
Case 2: Using MyBatis with Spring
@Configuration
@MapperScan("com.demo.mapper")
public class MyBatisSpringConfig {
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/testdb");
ds.setUsername("root");
ds.setPassword("******");
return ds;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource ds) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(ds);
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
);
return factoryBean.getObject();
}
}
@RestController
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private AccountMapper accountMapper;
@GetMapping("/{id}")
@ResponseBody
public Account getAccountById(@PathVariable Long id) {
return accountMapper.fetchAccountById(id);
}
}
From the two cases, when using MyBatis alone, you manually obtain the Mapper proxy via SqlSession. When integrated with Spring, Mapper interfaces act as ordinary Spring beans (injectable via @Autowired).
Under the Hood (Source Code Analysis)
Spring scans Mapper interfaces under the @MapperScan-specified package, registers them as bean definitions, and uses MapperFactoryBean to gneerate the proxy.
- @MapperScan & MapperScannerRegistrar
The@MapperScanannotation importsMapperScannerRegistrar(via@Import), which registers aMapperScannerConfigurerbean definition.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan { /* ... */ }
In MapperScannerRegistrar.registerBeanDefinitions(), it parses @MapperScan attributes (e.g., basePackages, sqlSessionFactoryRef) and registers MapperScannerConfigurer.
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata meta, BeanDefinitionRegistry registry) {
AnnotationAttributes attrs = AnnotationAttributes.fromMap(
meta.getAnnotationAttributes(MapperScan.class.getName())
);
if (attrs != null) {
registerBeanDefs(meta, attrs, registry, generateBeanName(meta, 0));
}
}
private void registerBeanDefs(AnnotationMetadata meta, AnnotationAttributes attrs,
BeanDefinitionRegistry registry, String beanName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
// Parse attributes (e.g., basePackages, sqlSessionFactoryRef)
String sqlSessionFactoryRef = attrs.getString("sqlSessionFactoryRef");
if (StringUtils.hasText(sqlSessionFactoryRef)) {
builder.addPropertyValue("sqlSessionFactoryBeanName", sqlSessionFactoryRef);
}
// Resolve base packages
List<String> basePackages = new ArrayList<>();
basePackages.addAll(Arrays.stream(attrs.getStringArray("basePackages"))
.filter(StringUtils::hasText)
.collect(Collectors.toList()));
// ... (handle basePackageClasses)
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
}
- MapperScannerConfigurer & ClassPathMapperScanner
MapperScannerConfigurerimplementsBeanDefinitionRegistryPostProcessor, triggeringClassPathMapperScannerto scan Mapper enterfaces.
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// Configure scanner (e.g., sqlSessionFactory, resourceLoader)
scanner.setMapperFactoryBeanClass(MapperFactoryBean.class);
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ","));
}
}
- ClassPathMapperScanner & MapperFactoryBean
ClassPathMapperScannerextendsClassPathBeanDefinitionScanner. IndoScan(), it scans Mapper interfaces and modifies their bean definition to useMapperFactoryBeanas theBeanClass.
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> holders = super.doScan(basePackages);
if (!holders.isEmpty()) {
processBeanDefs(holders);
}
return holders;
}
private void processBeanDefs(Set<BeanDefinitionHolder> holders) {
for (BeanDefinitionHolder holder : holders) {
AbstractBeanDefinition def = (AbstractBeanDefinition) holder.getBeanDefinition();
def.setBeanClass(this.mapperFactoryBeanClass); // Set to MapperFactoryBean
// ... (set constructor arguments, e.g., mapperInterface)
}
}
}
- MapperFactoryBean (FactoryBean Implementation)
MapperFactoryBeanimplementsFactoryBean<T>, and itsgetObject()method usesSqlSession.getMapper()to create the dynamic proxy:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
}
This way, when Spring initializes the bean, it calls MapperFactoryBean.getObject(), which internally uses MyBatis’ SqlSession to generate the Mapper proxy—making Mapper interfaces behave like ordinary Spring beans.