Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Why MyBatis Mapper Interfaces Can Be @Autowired Like Ordinary Beans?

Tech 1

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.

  1. @MapperScan & MapperScannerRegistrar
    The @MapperScan annotation imports MapperScannerRegistrar (via @Import), which registers a MapperScannerConfigurer bean 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());
    }
}
  1. MapperScannerConfigurer & ClassPathMapperScanner
    MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, triggering ClassPathMapperScanner to 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, ","));
    }
}
  1. ClassPathMapperScanner & MapperFactoryBean
    ClassPathMapperScanner extends ClassPathBeanDefinitionScanner. In doScan(), it scans Mapper interfaces and modifies their bean definition to use MapperFactoryBean as the BeanClass.
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)
        }
    }
}
  1. MapperFactoryBean (FactoryBean Implementation)
    MapperFactoryBean implements FactoryBean<T>, and its getObject() method uses SqlSession.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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.