Spring:MyBatis框架@MapperScan注解流程和疑惑

折腾了老久,原来是一个@MapperScan注解放的位置不对,导致对于Mapper接口实例化十分疑惑,记录一下过程

MyBatis和Hibernate不太一样,SQL是需要自行配置,而不像JPA里都已经封装好了,如果SQL比较简单,直接用封装好的接口即可,但是如果十分复杂的SQL,还需要审核,校验,可以通过MyBatis框架来实现持久层

图来自:https://oscimg.oschina.net/oscnet/cefa901a81228be243c28cd1af13c039a00.jpg

用MyBatis-Spring会将MyBatis整合到Spring当中,我这里用的版本Spring 5.2.1,MyBatis  3.5.3,MyBatis-Spring 2.0.3,满足官方的要求,用的注解而不是XML

具体的使用,需要配置两样东西,一个是SqlSessionFactory,用来创建SqlSession的Factory,SqlSession用来对接数据库;另一个是数据映射器Mapper接口

1、在MyBatis-Spring中,可以通过SqlSessionFactoryBean来创建SqlSessionFactory,这里还需要一个DataSource,和JDBC等其它DataSource一样即可

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource());
    return factoryBean.getObject();
}

大致流程SqlSessionFactoryBean->getObject()->afterPropertiesSet()->buildSqlSessionFactory()->Configuration->SqlSessionFactory

2、映射器是一个接口,定义一个Mapper接口,一个简单的查询方法,@Select指定具体的SQL

package com.lihuia.mybatis.mapper;

import com.lihuia.mybatis.model.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

/**
 * Copyright (C), 2018-2019
 * FileName: UserMapper
 * Author:   lihui
 * Date:     2019/11/15
 */

public interface UserMapper {
    
    @Select("select * from user_info where id = #{id}")
    User getUserById(@Param("id") int id);
}

3、对应ORM实体类

package com.lihuia.mybatis.model;

import lombok.Builder;

/**
 * Copyright (C), 2018-2019
 * FileName: User
 * Author:   lihui
 * Date:     2019/11/10
 */

@Builder
public class User {
    
    private int id;
    private String name;
    private String phone;
    private int age;
    
    @Override
    public String toString() {
        return "id=" + id + ", name=" + name + ", phone=" + phone + ", age=" + age;
    }
}

4、映射器的注册,通过MapperFactoryBean可以将映射器注册到Spring

package com.lihuia.mybatis.bean;

import com.lihuia.mybatis.mapper.UserMapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperFactoryBean;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

/**
 * Copyright (C), 2018-2019
 * FileName: MyBatisConfig
 * Author:   lihui
 * Date:     2019/11/15
 */

@PropertySource(value = {"classpath:config/db.properties"})
@Configuration
public class MyBatisConfig {
    
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
    
    @Bean
    public DriverManagerDataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        return factoryBean.getObject();
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }
    
    @Bean
    public MapperFactoryBean<UserMapper> userMapper() throws Exception {
        MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class);
        factoryBean.setSqlSessionFactory(sqlSessionFactory());
        return factoryBean;
    }

}

5、测试类

package com.lihuia.mybatis;

import javax.annotation.Resource;

import com.lihuia.mybatis.bean.MyBatisConfig;
import com.lihuia.mybatis.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Test;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;

/**
 * Copyright (C), 2018-2019
 * FileName: UserTest
 * Author:   lihui
 * Date:     2019/11/15
 */

@ContextConfiguration(classes = {MyBatisConfig.class})
@Slf4j
public class UserTest extends AbstractTestNGSpringContextTests {

    @Resource
    private JdbcTemplate jdbcTemplate;
    
    @Resource
    private UserMapper userMapper;
    
    
    @Test(description = "测试JDBC")
    public void jdbcTest() {
        String sql = "select * from user_info";
        System.out.println(jdbcTemplate.queryForList(sql));
    }
    
    @Test(description = "测试MyBatis")
    public void myBatisTest() {
        log.info(userMapper.getUserById(1).toString());
    }
}

如果是直接通过注入Bean的方式注入UserMapper,那么假如有一大堆的映射器,一个一个的注册注入十分麻烦,因此就和@ComponentScan注解一样有一个扫描映射器的注解@MapperScan,大致如下

@PropertySource(value = {"classpath:config/db.properties"})
@Configuration
@MapperScan("com.lihuia.mybatis.mapper")
public class MyBatisConfig {

@MapperScan指定Mapper接口类的路径放在配置类上即可,如果还有其它Mapper接口,也能够一并扫描到;但是我之前将这个扫描注解放在了测试类UserTest上,结果程序就会有误,弄了挺久的

首先看下@MapperScan注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {

里面的@Import注解,可以导入一个类,定义如下

/**
 * A {@link ImportBeanDefinitionRegistrar} to allow annotation configuration of MyBatis mapper scanning. Using
 * an @Enable annotation allows beans to be registered via @Component configuration, whereas implementing
 * {@code BeanDefinitionRegistryPostProcessor} will work for XML configuration.
 *
 * @author Michael Lanyon
 * @author Eduardo Macarron
 * @author Putthiphong Boonphong
 *
 * @see MapperFactoryBean
 * @see ClassPathMapperScanner
 * @since 1.2.0
 */
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

根据注释里可以看到ClassPathMapperScanner,继承了ClassPathBeanDefinitionScanner类是Spring提供的一个用于扫描Bean定义配置的基础类,这里覆盖了基类的doScan()方法

/**
 * Calls the parent search that will search and register all the candidates. Then the registered objects are post
 * processed to set them as MapperFactoryBeans
 */
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
  Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

  if (beanDefinitions.isEmpty()) {
    LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
        + "' package. Please check your configuration.");
  } else {
    processBeanDefinitions(beanDefinitions);
  }

  return beanDefinitions;
}

DEBUG一下,可以看到这个就是扫描Mapper接口的方法,返回@MapperScan注解的value

NewImage

具体流程看的迷糊,不太懂,@MapperScan注解扫描必须定义在配置类上面

有空再学习学习,最终Mapper接口的实例化是通过Java动态代理来实现的

参考链接:

https://cofcool.github.io/tech/2018/06/20/mybatis-sourcecode-1#21-%E9%85%8D%E7%BD%AE%E7%B1%BB

https://objcoding.com/2018/06/12/mybatis-spring/

http://www.songshuiyang.com/2018/12/18/backend/framework/mybatis/sourceCodeAnalysis/Mybatis%E6%BA%90%E7%A0%81(%E5%8D%81%E4%B9%9D)Spring%20Mybatis%E9%9B%86%E6%88%90%E4%B9%8B%E5%9F%BA%E4%BA%8E%E6%B3%A8%E8%A7%A3%E7%9A%84%E9%85%8D%E7%BD%AE%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/

发表回复