mybatis-spring:@MapperScan註解的使用
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<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; /** * 這個註解指定的接口也要被掃描 */ Class<? extends Annotation> annotationClass() default Annotation.class; /** * 繼承這個接口的接口也要被掃描 */ Class<?> markerInterface() default Class.class; /** * 多數據源的時候可能用到這個,後面單獨說明這個 */ String sqlSessionTemplateRef() default ""; /** * 多數據源的時候可能用到這個,後面單獨說明這個 */ String sqlSessionFactoryRef() default ""; /** * 多數據源的時候可能用到這個,後面單獨說明這個 */ Class<? extends MapperFactoryBean> 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這個類的。
可以debug,看這個參數的信息。
@Override public 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.1 if (resourceLoader != null) { scanner.setResourceLoader(resourceLoader); } Class<? extends Annotation> 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<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator"); if (!BeanNameGenerator.class.equals(generatorClass)) { scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass)); } Class<? extends MapperFactoryBean> 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<String> basePackages = new ArrayList<String>(); 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 #59 definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig);
在ClassPathMapperScanner類的processBeanDefinitions方法內看到這裡註冊的一個spring的工廠bean:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { ... @Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); } @Override public Class<T> getObjectType() { return this.mapperInterface; } @Override public 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;
工程結構
按工程結構,列一下文件代碼:
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; } @Override public String toString() { return "User{" + "id=" + id + ", age=" + age + ", username='" + username + '\'' + '}'; } }
MybatisConfig.java
@Configuration @MapperScan(basePackageClasses = {UserDao.class}) //@EnableTransactionManagement //啟用spring事務 public class MybatisConfig { @Autowired private Environment environment; // 數據源配置 @Bean public 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,事務部分會用到 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } @Bean public 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 { @Autowired IUserService 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
@Repository public interface UserDao { boolean saveUser(User user); List<User> getUsers(); boolean deleteUser(); }
UserServiceImpl.java
@Service public class UserServiceImpl implements IUserService { @Autowired UserDao userDao; //@Transactional //指定這個方法的事務屬性 @Override public boolean saveUser(User user) throws Exception { boolean success = userDao.saveUser(user); // spring事務能力測試的時候,使用下面這段代碼 /*if (true) { throw new RuntimeException(); }*/ return success; } @Override public List<User> getUsers() throws Exception { return userDao.getUsers(); } @Override public boolean deleteUser() throws Exception { return userDao.deleteUser(); } }
IUserService.java
public interface IUserService { boolean saveUser(User user) throws Exception; List<User> getUsers() throws Exception; boolean deleteUser() throws Exception; }
WebApplication.java
@SpringBootApplication public class WebApplication { public static void main(String[] args) { SpringApplication.run(WebApplication.class, args); } }
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xuxd.demo.dao.UserDao"> <sql id="user_column"> id,age,username </sql> <select id="getUsers" resultType="com.xuxd.demo.beans.User"> select * from user </select> <!-- 增加用戶 --> <insert id="saveUser" parameterType="com.xuxd.demo.beans.User"> insert into user (<include refid="user_column"/>) values (#{id},#{age},#{username}) </insert> <delete id="deleteUser"> DELETE from USER where id = 10 </delete> </mapper>
application.properties
#data source config mysql.driver=com.mysql.jdbc.Driver mysql.username=root mysql.passwd=123456 mysql.url=jdbc:mysql://localhost:3306/testdb?useSSL=false
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.xuxd</groupId> <artifactId>spring-mybatis.demo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.3.1.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.43</version> </dependency> </dependencies> </project>
後續就用這個工程debug源碼瞭。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 解決Spring使用@MapperScan問題
- Spring Dao層@Repository與@Mapper的使用
- 搭建MyBatis-Plus框架並進行數據庫增刪改查功能
- Spring整合Mybatis思路梳理總結
- Spring框架基於註解開發CRUD詳解