mybatis-spring:@MapperScan注解的使用

目录

  • mybatis-spring:@MapperScan注解
    • @MapperScan源码
    • MapperScannerRegistrar.class
  • demo: springboot+mybatis
    • 工程代码

mybatis-spring:@MapperScan注解 在demo: springboot+mybatis的示例中,dao层接口使用了注解@MapperScan:指定扫描com.xuxd.demo.dao.UserDao所在包路径下的所有接口类。
本文分析下@MapperScan注解做了哪些动作。

@MapperScan源码
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@Documented@Import(MapperScannerRegistrar.class)public @interface MapperScan { /***缺省属性(==basePackages),basePackages的别名*/String[] value() default {}; /*** 哪些包路径下的接口被扫描注册(接口至少有一个方法),具体实现类(非接口)忽略*/String[] basePackages() default {}; /*** 指定类所在包下所有接口被扫描注册(接口至少有一个方法),具体实现类(非接口)忽略*/Class[] basePackageClasses() default {}; /*** 扫描到的满足条件的接口,首先要把它们相关bean定义注册到spring容器中吧,注册bean定义* 的时候,需要定义bean名称,这个是用来自定方生成bean名称的策略组件,个人觉得很少用*/Class nameGenerator() default BeanNameGenerator.class; /*** 这个注解指定的接口也要被扫描*/Class annotationClass() default Annotation.class; /*** 继承这个接口的接口也要被扫描*/Class markerInterface() default Class.class; /*** 多数据源的时候可能用到这个,后面单独说明这个*/String sqlSessionTemplateRef() default ""; /*** 多数据源的时候可能用到这个,后面单独说明这个*/String sqlSessionFactoryRef() default ""; /*** 多数据源的时候可能用到这个,后面单独说明这个*/Class factoryBean() default MapperFactoryBean.class; }

这个注解的重点是@Import(MapperScannerRegistrar.class)
使用这个注解导入MapperScannerRegistrar主要完成两件事:
1. 扫描指定接口
2. 注册这些接口的bean定义到spring容器
接下来进入MapperScannerRegistrar类看下是如何完成这两动作:

MapperScannerRegistrar.class
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {}

这个类实现了ImportBeanDefinitionRegistrar接口:
public interface ImportBeanDefinitionRegistrar { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry); }

@MapperScan注解类上使用了@Import注解导入了这个接口的实现类(MapperScannerRegistrar.class),因此spring解析MybatisConfig(源码:demo: springboot+mybatis)这个类的时候,解析到这个类上使用了注解@MapperScan,从MapperScan注解类上(注解都是一个接口,java会创建代理类)发现了@Import注解及MapperScannerRegistrar类(因为Import注解是导入配置类的)。
在加载MybatisConfig配置类的bean定义时候,找到了ImportBeanDefinitionRegistrar 的实现类MapperScannerRegistrar,便会回调这个MapperScannerRegistrar的registerBeanDefinitions方法。
总之一句话:
在加载配置类MybatisConfig的bean定义的时候,会调用与之看起来有点关系的MapperScannerRegistrar的registerBeanDefinitions方法。
MapperScannerRegistrar的registerBeanDefinitions方法第一个参数importingClassMetadata指的是MybatisConfig这个类的。
mybatis-spring:@MapperScan注解的使用
文章图片

可以debug,看这个参数的信息。
@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); // this check is needed in Spring 3.1if (resourceLoader != null) {scanner.setResourceLoader(resourceLoader); } Class annotationClass = annoAttrs.getClass("annotationClass"); if (!Annotation.class.equals(annotationClass)) {scanner.setAnnotationClass(annotationClass); } Class markerInterface = annoAttrs.getClass("markerInterface"); if (!Class.class.equals(markerInterface)) {scanner.setMarkerInterface(markerInterface); } Class generatorClass = annoAttrs.getClass("nameGenerator"); if (!BeanNameGenerator.class.equals(generatorClass)) {scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass)); } Class mapperFactoryBeanClass = annoAttrs.getClass("factoryBean"); if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass)); } scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef")); scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef")); List basePackages = new ArrayList(); for (String pkg : annoAttrs.getStringArray("value")) {if (StringUtils.hasText(pkg)) {basePackages.add(pkg); }}for (String pkg : annoAttrs.getStringArray("basePackages")) {if (StringUtils.hasText(pkg)) {basePackages.add(pkg); }}for (Class clazz : annoAttrs.getClassArray("basePackageClasses")) {basePackages.add(ClassUtils.getPackageName(clazz)); }scanner.registerFilters(); scanner.doScan(StringUtils.toStringArray(basePackages)); }

看这个方法的源码,主要完成2件事:
1. 解析MapperScan注解的各个字段的值 ,用以初始化类路径扫描器
2. 确定扫描类路径下哪些接口,如指定的包路径、指定的类所在包路径。上面倒数第2行代码,注册过滤器,用来指定包含哪些注解或接口的扫描(@MapperScan的annotationClass的markerInterface属性,如果设置的话)
因此,重点是最后一行代码doScan的调用。
这里不贴源码了,前文提到,MapperScannerRegistrar主要完成两件事,都会在这里完成,解析包路径,扫描指定接口并注册bean定义到spring容器。
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig);

在ClassPathMapperScanner类的processBeanDefinitions方法内看到这里注册的一个spring的工厂bean:
public class MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean { ...@Overridepublic T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface); } @Overridepublic Class getObjectType() {return this.mapperInterface; } @Overridepublic boolean isSingleton() {return true; }...}

大部分代码删除了, 只留下这几个说明。
不了解 spring的FactoryBean的建议查看下相关文档。
这里用直白的话说,就是:
我在service层需要注入这个Dao层接口的bean(比如demo: springboot+mybatis中UserServiceImpl类的UserDao字段的自动注入),依据类型注入。
spring在自己的容器里翻呀翻,如果是普通bean,一看和这个接口类型(UserDao)都不匹配就换一个,找到了这个工厂bean,一看是工厂bean,就不能直接做类型匹配了,而是调用getObjectType方法,把返回的类型和需要被注入字段的类型一比较,正好匹配(都是UserDao类型),就调用这个工厂bean的getObject方法返回这个对象,然后通过反射等操作,把这个值注入到这个字段中。而调用getObject方法,其实就是我们平常直接用mybatis的接口返回的一个MapperProxy的代理对象的操作了。

demo: springboot+mybatis 最近因工作原因,需要研究下spring的事务部分和mybatis的多数据源的源码实现,这样才能更容易的在代码层面通过扩展/重写等方式去定制自己的实现。
以前虽然用过几次mybatis,但是却一直没抽出时间认真翻看下源码,趁这次机会,花点时间研究下,顺便做个笔记。
关于看源码,我向来是觉得只有一步步去debug整个流程,查看每一步的数据流向和数据状态,才会有个更清晰的深知。如果只是看的话,有些源码中各种继承、适配、代理、装饰等,会分不清当前使用的到底是哪个类。
于是乎,所谓工欲善其事,必先利其器。先搭建个极简单的mybatis的工程环境,用来调试源码。
这个工程用了spring boot+mybatis。mybatis采用java config的形式(是真心不喜欢xml配置,所以源码研究上也会避开xml的加载)
后面博文关于分析描述就会针对这个工程的配置来说了。
另外,代码中关于spring事务的注解先注释了。
说了这么多,是希望缓解自己又写了篇这么没技术含量的博客的尴尬,哎,最近这段时间写的博客确实有些凑数了。

工程代码
数据库脚本:
CREATE DATABASE `testdb` /*!40100 DEFAULT CHARACTER SET utf8 */ -- ------------------------------ Table structure for user-- ----------------------------DROP TABLE IF EXISTS `user`; CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT,`age` int(11) NOT NULL,`username` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

工程结构
mybatis-spring:@MapperScan注解的使用
文章图片

按工程结构,列一下文件代码:
User.java
public class User {int id; int age; String username; public User() {} public User(int id, int age, String username) {this.id = id; this.age = age; this.username = username; } public int getId() {return id; } public void setId(int id) {this.id = id; } public int getAge() {return age; } public void setAge(int age) {this.age = age; } public String getUsername() {return username; } public void setUsername(String username) {this.username = username; } @Overridepublic String toString() {return "User{" +"id=" + id +", age=" + age +", username='" + username + '\'' +'}'; }}

MybatisConfig.java
@Configuration@MapperScan(basePackageClasses = {UserDao.class})//@EnableTransactionManagement //启用spring事务public class MybatisConfig { @Autowiredprivate Environment environment; // 数据源配置@Beanpublic DataSource dataSource() {// mybatis自带的一个简易数据库连接池,只是为了debug代码,这个就不关心了PooledDataSource pooledDataSource = new PooledDataSource(); pooledDataSource.setDriver(environment.getProperty("mysql.driver")); pooledDataSource.setUsername(environment.getProperty("mysql.username")); pooledDataSource.setPassword(environment.getProperty("mysql.passwd")); pooledDataSource.setUrl(environment.getProperty("mysql.url")); return pooledDataSource; } // spring事务管理的基础bean,事务部分会用到@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource) {DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } @Beanpublic SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:/mapper/*.xml")); return sqlSessionFactoryBean; }}

UserController.java
@RestController@RequestMapping("/user")public class UserController { @AutowiredIUserService userService; @GetMapping("/save")public String saveUser() {User user = new User(10, 100, "test user"); try {return userService.saveUser(user) ? "save success" : "save fail"; } catch (Exception ignore) {ignore.printStackTrace(); // 不打印日志了,堆栈信息直接打到控制台看return "save error: " + ignore.getMessage(); } } @GetMapping("/list")public String getUsers() throws Exception {return userService.getUsers().toString(); } @GetMapping("/delete")public String deleteUser() throws Exception {return userService.deleteUser() ? "delete success" : "delete fail"; }}

UserDao.java
@Repositorypublic interface UserDao { boolean saveUser(User user); List getUsers(); boolean deleteUser(); }

UserServiceImpl.java
@Servicepublic class UserServiceImpl implements IUserService { @AutowiredUserDao userDao; //@Transactional //指定这个方法的事务属性@Overridepublic boolean saveUser(User user) throws Exception {boolean success = userDao.saveUser(user); // spring事务能力测试的时候,使用下面这段代码/*if (true) {throw new RuntimeException(); }*/return success; } @Overridepublic List getUsers() throws Exception {return userDao.getUsers(); } @Overridepublic boolean deleteUser() throws Exception {return userDao.deleteUser(); }}

IUserService.java
public interface IUserService { boolean saveUser(User user) throws Exception; List getUsers() throws Exception; boolean deleteUser() throws Exception; }

WebApplication.java
@SpringBootApplicationpublic class WebApplication { public static void main(String[] args) {SpringApplication.run(WebApplication.class, args); }}

UserMapper.xml
id,age,username select * from user insert into user()values(#{id},#{age},#{username}) DELETE from USER where id = 10

application.properties
#data source configmysql.driver=com.mysql.jdbc.Drivermysql.username=rootmysql.passwd=123456mysql.url=jdbc:mysql://localhost:3306/testdb?useSSL=false

pom.xml
4.0.0 com.xuxdspring-mybatis.demo1.0-SNAPSHOT org.springframework.bootspring-boot-starter-parent1.5.6.RELEASE org.springframework.bootspring-boot-starter-web org.springframeworkspring-jdbc4.3.1.RELEASE org.mybatismybatis3.3.1 org.mybatismybatis-spring1.2.5 mysqlmysql-connector-java5.1.43

后续就用这个工程debug源码了。
【mybatis-spring:@MapperScan注解的使用】以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

    推荐阅读