6. SpringBoot数据访问 6.1 数据源自动配置源码剖析 1. 数据源配置方式 选择数据库驱动的库文件 在maven中配置数据库驱动 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency >
配置数据库连接 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root # spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
配置spring-boot-starter-jdbc <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency >
编写测试类 @RunWith(SpringRunner.class) @SpringBootTest(classes = SpringBootDemoApplication.class) class SpringBootDemoApplicationTests { @Autowired DataSource dataSource; @Test public void contextLoads () throws SQLException { Connection connection = dataSource.getConnection(); } }
2. 连接池配置方式 选择数据库连接池的库文件 SpringBoot提供了三种数据库连接池:
其中spring boot2.x版本默认使用HikariCP,maven中配置如下:
<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency >
如果不使用HikariCP,而改用Commons DBCP2,则配置如下:
<dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-dbcp2</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > <exclusions > <exclusion > <groupId > com.zaxxer</groupId > <artifactId > HikariCP</artifactId > </exclusion > </exclusions > </dependency >
如果不使用HikariCP,而改用Tomcat JDBC Connection Pool,则配置如下:
<dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-jdbc</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > <exclusions > <exclusion > <groupId > com.zaxxer</groupId > <artifactId > HikariCP</artifactId > </exclusion > </exclusions > </dependency >
思考:为什么说springboot默认使用的连接池类型是HikariCP,在哪指定的?
3. 数据源自动配置 spring.factories中找到数据源的配置类:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @EnableConfigurationProperties(DataSourceProperties.class) @Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class }) public class DataSourceAutoConfiguration { @Configuration(proxyBeanMethods = false) @Conditional(EmbeddedDatabaseCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import(EmbeddedDataSourceConfiguration.class) protected static class EmbeddedDatabaseConfiguration { } @Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration { } }
@Conditional(PooledDataSourceCondition.class) 根据判断条件,实例化这个类,指定了配置文件中,必须有type这个属性。
另外springboot 默认支持 type 类型设置的数据源;
abstract class DataSourceConfiguration { @SuppressWarnings("unchecked") protected static <T> T createDataSource (DataSourceProperties properties, Class<? extends DataSource> type) { return (T) properties.initializeDataSourceBuilder().type(type).build(); } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource", matchIfMissing = true) static class Tomcat { @Bean @ConfigurationProperties(prefix = "spring.datasource.tomcat") org.apache.tomcat.jdbc.pool.DataSource dataSource (DataSourceProperties properties) { org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(properties, org.apache.tomcat.jdbc.pool.DataSource.class); DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl()); String validationQuery = databaseDriver.getValidationQuery(); if (validationQuery != null ) { dataSource.setTestOnBorrow(true ); dataSource.setValidationQuery(validationQuery); } return dataSource; } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HikariDataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true) static class Hikari { @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") HikariDataSource dataSource (DataSourceProperties properties) { HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(org.apache.commons.dbcp2.BasicDataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.commons.dbcp2.BasicDataSource", matchIfMissing = true) static class Dbcp2 { @Bean @ConfigurationProperties(prefix = "spring.datasource.dbcp2") org.apache.commons.dbcp2.BasicDataSource dataSource (DataSourceProperties properties) { return createDataSource(properties, org.apache.commons.dbcp2.BasicDataSource.class); } } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type") static class Generic { @Bean DataSource dataSource (DataSourceProperties properties) { return properties.initializeDataSourceBuilder().build(); } } }
如果在类路径没有找到 jar包 则会跑出异常:
配置文件中没有指定数据源时候 会根据注解判断然后选择相应的实例化数据源对象!则 type 为空。
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(HikariDataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true) static class Hikari { @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") HikariDataSource dataSource (DataSourceProperties properties) { HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } }
createDataSource 方法
@SuppressWarnings("unchecked") protected static <T> T createDataSource (DataSourceProperties properties, Class<? extends DataSource> type) { return (T) properties.initializeDataSourceBuilder().type(type).build(); }
DataSourceBuilder 类
设置type
public <D extends DataSource> DataSourceBuilder<D> type (Class<D> type) { this .type = type; return this ; }
根据设置type的选择类型
private Class<? extends DataSource> getType() { Class<? extends DataSource> type = this .type != null ? this .type : findType(this .classLoader); if (type != null ) { return type; } else { throw new IllegalStateException("No supported DataSource type found" ); } }
public static Class<? extends DataSource> findType(ClassLoader classLoader) { String[] var1 = DATA_SOURCE_TYPE_NAMES; int var2 = var1.length; int var3 = 0 ; while (var3 < var2) { String name = var1[var3]; try { return ClassUtils.forName(name, classLoader); } catch (Exception var6) { ++var3; } } return null ; }
private static final String[] DATA_SOURCE_TYPE_NAMES = new String[]{"com.zaxxer.hikari.HikariDataSource" , "org.apache.tomcat.jdbc.pool.DataSource" ,"org.apache.commons.dbcp2.BasicDataSource" };
取出来的第一个值就是com.zaxxer.hikari.HikariDataSource,那么证实在没有指定Type的情况下,默认类型为com.zaxxer.hikari.HikariDataSource
4. Druid连接池的配置 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.1.10</version > </dependency >
在application.yml中引入druid的相关配置
spring: datasource: username: root password: root url: jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver initialization-mode: always type: com.alibaba.druid.pool.DruidDataSource initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
但是测试Debug查看DataSource的值,会发现有些属性是没有生效的
这是因为:如果单纯在yml文件中编写如上的配置,SpringBoot肯定是读取不到druid的相关配置的。因为它并不像我们原生的jdbc,系统默认就使用DataSourceProperties与其属性进行了绑定。所以我们应该编写一个类与其属性进行绑定
编写整合druid的配置类DruidConfig
public class DruidConfig { @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource druid () { return new DruidDataSource(); } }
测试的时候,突然发现控制台报错了。经过查找发现是yml文件里的filters: stat,wall,log4j
因为我们springBoot2.0以后使用的日志框架已经不再使用log4j了。此时应该引入相应的适配器。 我们可以在pom.xml文件上加入:
<dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-log4j12</artifactId > </dependency >
6.2 SpringBoot整合Mybatis MyBatis 是一款优秀的持久层框架,Spring Boot官方虽然没有对MyBatis进行整合,但是MyBatis团队自行适配了对应的启动器,进一步简化了使用MyBatis进行数据的操作 因为Spring Boot框架开发的便利性,所以实现Spring Boot与数据访问层框架(例如MyBatis)的整合非常简单,主要是引入对应的依赖启动器,并进行数据库相关参数设置即可。
1. 新建springboot项目,并导入mybatis的pom配置 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 1.3.2</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency >
2. application.yml配置 spring: datasource: username: root password: root url: jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource
3. 代码实现 Bean对象
@Data public class Account { private String name; private Integer money; private String cardNo; }
mapper接口
@Mapper public interface UserDao { @Select("SELECT * FROM USER") List<User> getUser () ; }
service类
@Service public class UserService { Logger logger = LoggerFactory.getLogger(UserService.class); @Autowired private UserDao userDao; public List<User> getUser () { List<User> userList = userDao.getUser(); logger.info("查询出来的用户信息,{}" ,userList.toString()); return userList; } }
测试类
@RunWith(SpringRunner.class) @SpringBootTest public class DemoApplicationTests { @Test public void contextLoads () { } } public class UserServiceTest extends DemoApplicationTests { @Autowired private UserService userService; @Test public void getUser () { userService.getUser(); } }
6.3 Mybatis自动配置源码分析 springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplication
中的@EnableAutoConfiguration
EnableAutoConfiguration主要是通过AutoConfigurationImportSelector类来加载 以mybatis为例,*selector通过反射加载spring.factories中指定的java类,也就是加载MybatisAutoConfiguration类(该类有Configuration注解,属于配置类)
@org .springframework.context.annotation.Configuration@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties(MybatisProperties.class) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class); private final MybatisProperties properties; private final Interceptor[] interceptors; private final TypeHandler[] typeHandlers; private final LanguageDriver[] languageDrivers; private final ResourceLoader resourceLoader; private final DatabaseIdProvider databaseIdProvider; private final List<ConfigurationCustomizer> configurationCustomizers; public MybatisAutoConfiguration (MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) { this .properties = properties; this .interceptors = interceptorsProvider.getIfAvailable(); this .typeHandlers = typeHandlersProvider.getIfAvailable(); this .languageDrivers = languageDriversProvider.getIfAvailable(); this .resourceLoader = resourceLoader; this .databaseIdProvider = databaseIdProvider.getIfAvailable(); this .configurationCustomizers = configurationCustomizersProvider.getIfAvailable(); } @Override public void afterPropertiesSet () { checkConfigFileExists(); } private void checkConfigFileExists () { if (this .properties.isCheckConfigLocation() && StringUtils.hasText(this .properties.getConfigLocation())) { Resource resource = this .resourceLoader.getResource(this .properties.getConfigLocation()); Assert.state(resource.exists(), "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)" ); } } @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setVfs(SpringBootVFS.class); if (StringUtils.hasText(this .properties.getConfigLocation())) { factory.setConfigLocation(this .resourceLoader.getResource(this .properties.getConfigLocation())); } applyConfiguration(factory); if (this .properties.getConfigurationProperties() != null ) { factory.setConfigurationProperties(this .properties.getConfigurationProperties()); } if (!ObjectUtils.isEmpty(this .interceptors)) { factory.setPlugins(this .interceptors); } if (this .databaseIdProvider != null ) { factory.setDatabaseIdProvider(this .databaseIdProvider); } if (StringUtils.hasLength(this .properties.getTypeAliasesPackage())) { factory.setTypeAliasesPackage(this .properties.getTypeAliasesPackage()); } if (this .properties.getTypeAliasesSuperType() != null ) { factory.setTypeAliasesSuperType(this .properties.getTypeAliasesSuperType()); } if (StringUtils.hasLength(this .properties.getTypeHandlersPackage())) { factory.setTypeHandlersPackage(this .properties.getTypeHandlersPackage()); } if (!ObjectUtils.isEmpty(this .typeHandlers)) { factory.setTypeHandlers(this .typeHandlers); } if (!ObjectUtils.isEmpty(this .properties.resolveMapperLocations())) { factory.setMapperLocations(this .properties.resolveMapperLocations()); } Set<String> factoryPropertyNames = Stream .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName) .collect(Collectors.toSet()); Class<? extends LanguageDriver> defaultLanguageDriver = this .properties.getDefaultScriptingLanguageDriver(); if (factoryPropertyNames.contains("scriptingLanguageDrivers" ) && !ObjectUtils.isEmpty(this .languageDrivers)) { factory.setScriptingLanguageDrivers(this .languageDrivers); if (defaultLanguageDriver == null && this .languageDrivers.length == 1 ) { defaultLanguageDriver = this .languageDrivers[0 ].getClass(); } } if (factoryPropertyNames.contains("defaultScriptingLanguageDriver" )) { factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver); } return factory.getObject(); } private void applyConfiguration (SqlSessionFactoryBean factory) { Configuration configuration = this .properties.getConfiguration(); if (configuration == null && !StringUtils.hasText(this .properties.getConfigLocation())) { configuration = new Configuration(); } if (configuration != null && !CollectionUtils.isEmpty(this .configurationCustomizers)) { for (ConfigurationCustomizer customizer : this .configurationCustomizers) { customizer.customize(configuration); } } factory.setConfiguration(configuration); } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate (SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this .properties.getExecutorType(); if (executorType != null ) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } } public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware , ImportBeanDefinitionRegistrar { private BeanFactory beanFactory; @Override public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { if (!AutoConfigurationPackages.has(this .beanFactory)) { logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled." ); return ; } logger.debug("Searching for mappers annotated with @Mapper" ); List<String> packages = AutoConfigurationPackages.get(this .beanFactory); if (logger.isDebugEnabled()) { packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'" , pkg)); } BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); builder.addPropertyValue("processPropertyPlaceHolders" , true ); builder.addPropertyValue("annotationClass" , Mapper.class); builder.addPropertyValue("basePackage" , StringUtils.collectionToCommaDelimitedString(packages)); BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class); Set<String> propertyNames = Stream.of(beanWrapper.getPropertyDescriptors()).map(PropertyDescriptor::getName) .collect(Collectors.toSet()); if (propertyNames.contains("lazyInitialization" )) { builder.addPropertyValue("lazyInitialization" , "${mybatis.lazy-initialization:false}" ); } if (propertyNames.contains("defaultScope" )) { builder.addPropertyValue("defaultScope" , "${mybatis.mapper-default-scope:}" ); } registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition()); } @Override public void setBeanFactory (BeanFactory beanFactory) { this .beanFactory = beanFactory; } } @org .springframework.context.annotation.Configuration @Import(AutoConfiguredMapperScannerRegistrar.class) @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class }) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { @Override public void afterPropertiesSet () { logger.debug( "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer." ); } } }
MybatisAutoConfiguration:
①类中有个MybatisProperties类,该类对应的是mybatis的配置文件
②类中有个sqlSessionFactory方法,作用是创建SqlSessionFactory类、Configuration类(mybatis最主要的类,保存着与mybatis相关的东西)
③SelSessionTemplate,作用是与mapperProoxy代理类有关sqlSessionFactory主要是通过创建了一个SqlSessionFactoryBean,这个类实现了FactoryBean接口,所以在Spring容器就会注入这个类中定义的getObject方法返回的对象。
看一下getObject()方法做了什么?
@Override public SqlSessionFactory getObject () throws Exception { if (this .sqlSessionFactory == null ) { afterPropertiesSet(); } return this .sqlSessionFactory; } @Override public void afterPropertiesSet () throws Exception { notNull(dataSource, "Property 'dataSource' is required" ); notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required" ); state((configuration == null && configLocation == null ) || !(configuration != null && configLocation != null ), "Property 'configuration' and 'configLocation' can not specified with together" ); this .sqlSessionFactory = buildSqlSessionFactory(); } protected SqlSessionFactory buildSqlSessionFactory () throws IOException { Configuration configuration; XMLConfigBuilder xmlConfigBuilder = null ; if (this .configuration != null ) { configuration = this .configuration; if (configuration.getVariables() == null ) { configuration.setVariables(this .configurationProperties); } else if (this .configurationProperties != null ) { configuration.getVariables().putAll(this .configurationProperties); } } else if (this .configLocation != null ) { xmlConfigBuilder = new XMLConfigBuilder(this .configLocation.getInputStream(), null , this .configurationProperties); configuration = xmlConfigBuilder.getConfiguration(); } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration" ); } configuration = new Configuration(); if (this .configurationProperties != null ) { configuration.setVariables(this .configurationProperties); } } if (this .objectFactory != null ) { configuration.setObjectFactory(this .objectFactory); } if (this .objectWrapperFactory != null ) { configuration.setObjectWrapperFactory(this .objectWrapperFactory); } if (this .vfs != null ) { configuration.setVfsImpl(this .vfs); } if (hasLength(this .typeAliasesPackage)) { String[] typeAliasPackageArray = tokenizeToStringArray(this .typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeAliasPackageArray) { configuration.getTypeAliasRegistry().registerAliases(packageToScan, typeAliasesSuperType == null ? Object.class : typeAliasesSuperType); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases" ); } } } if (!isEmpty(this .typeAliases)) { for (Class<?> typeAlias : this .typeAliases) { configuration.getTypeAliasRegistry().registerAlias(typeAlias); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type alias: '" + typeAlias + "'" ); } } } if (!isEmpty(this .plugins)) { for (Interceptor plugin : this .plugins) { configuration.addInterceptor(plugin); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered plugin: '" + plugin + "'" ); } } } if (hasLength(this .typeHandlersPackage)) { String[] typeHandlersPackageArray = tokenizeToStringArray(this .typeHandlersPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeHandlersPackageArray) { configuration.getTypeHandlerRegistry().register(packageToScan); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers" ); } } } if (!isEmpty(this .typeHandlers)) { for (TypeHandler<?> typeHandler : this .typeHandlers) { configuration.getTypeHandlerRegistry().register(typeHandler); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type handler: '" + typeHandler + "'" ); } } } if (this .databaseIdProvider != null ) { try { configuration.setDatabaseId(this .databaseIdProvider.getDatabaseId(this .dataSource)); } catch (SQLException e) { throw new NestedIOException("Failed getting a databaseId" , e); } } if (this .cache != null ) { configuration.addCache(this .cache); } if (xmlConfigBuilder != null ) { try { xmlConfigBuilder.parse(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed configuration file: '" + this .configLocation + "'" ); } } catch (Exception ex) { throw new NestedIOException("Failed to parse config resource: " + this .configLocation, ex); } finally { ErrorContext.instance().reset(); } } if (this .transactionFactory == null ) { this .transactionFactory = new SpringManagedTransactionFactory(); } configuration.setEnvironment(new Environment(this .environment, this .transactionFactory, this .dataSource)); if (!isEmpty(this .mapperLocations)) { for (Resource mapperLocation : this .mapperLocations) { if (mapperLocation == null ) { continue ; } try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'" , e); } finally { ErrorContext.instance().reset(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'" ); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found" ); } } return this .sqlSessionFactoryBuilder.build(configuration); }
这个已经很明显了,实际上就是调用了MyBatis的初始化流程现在已经得到了SqlSessionFactory了,接下来就是如何扫描到相关的Mapper接口了。这个需要看这个注解@MapperScan(basePackages = “com.mybatis.mapper”)
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(MapperScannerRegistrar.class) @Repeatable(MapperScans.class) public @interface MapperScan {}
通过@Import的方式会扫描到MapperScannerRegistrar类。
MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,那么在spring实例化之前就会调用到registerBeanDefinitions方法
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar , ResourceLoaderAware {}
@Override public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AnnotationAttributes mapperScanAttrs = AnnotationAttributes .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); if (mapperScanAttrs != null ) { registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0 )); } }
void registerBeanDefinitions (AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); builder.addPropertyValue("processPropertyPlaceHolders" , true ); Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass" ); if (!Annotation.class.equals(annotationClass)) { builder.addPropertyValue("annotationClass" , annotationClass); } Class<?> markerInterface = annoAttrs.getClass("markerInterface" ); if (!Class.class.equals(markerInterface)) { builder.addPropertyValue("markerInterface" , markerInterface); } Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator" ); if (!BeanNameGenerator.class.equals(generatorClass)) { builder.addPropertyValue("nameGenerator" , BeanUtils.instantiateClass(generatorClass)); } Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean" ); if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) { builder.addPropertyValue("mapperFactoryBeanClass" , mapperFactoryBeanClass); } String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef" ); if (StringUtils.hasText(sqlSessionTemplateRef)) { builder.addPropertyValue("sqlSessionTemplateBeanName" , annoAttrs.getString("sqlSessionTemplateRef" )); } String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef" ); if (StringUtils.hasText(sqlSessionFactoryRef)) { builder.addPropertyValue("sqlSessionFactoryBeanName" , annoAttrs.getString("sqlSessionFactoryRef" )); } List<String> basePackages = new ArrayList<>(); basePackages.addAll( Arrays.stream(annoAttrs.getStringArray("value" )).filter(StringUtils::hasText).collect(Collectors.toList())); basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages" )).filter(StringUtils::hasText) .collect(Collectors.toList())); basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses" )).map(ClassUtils::getPackageName) .collect(Collectors.toList())); if (basePackages.isEmpty()) { basePackages.add(getDefaultBasePackage(annoMeta)); } String lazyInitialization = annoAttrs.getString("lazyInitialization" ); if (StringUtils.hasText(lazyInitialization)) { builder.addPropertyValue("lazyInitialization" , lazyInitialization); } String defaultScope = annoAttrs.getString("defaultScope" ); if (!AbstractBeanDefinition.SCOPE_DEFAULT.equals(defaultScope)) { builder.addPropertyValue("defaultScope" , defaultScope); } builder.addPropertyValue("basePackage" , StringUtils.collectionToCommaDelimitedString(basePackages)); registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); }
MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,所以接着又会扫描并调用到postProcessBeanDefinitionRegistry方法。
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor , InitializingBean , ApplicationContextAware , BeanNameAware
@Override public void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry registry) { if (this .processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this .addToConfig); scanner.setAnnotationClass(this .annotationClass); scanner.setMarkerInterface(this .markerInterface); scanner.setSqlSessionFactory(this .sqlSessionFactory); scanner.setSqlSessionTemplate(this .sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this .sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this .sqlSessionTemplateBeanName); scanner.setResourceLoader(this .applicationContext); scanner.setBeanNameGenerator(this .nameGenerator); scanner.setMapperFactoryBeanClass(this .mapperFactoryBeanClass); if (StringUtils.hasText(lazyInitialization)) { scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization)); } if (StringUtils.hasText(defaultScope)) { scanner.setDefaultScope(defaultScope); } scanner.registerFilters(); scanner.scan( StringUtils.tokenizeToStringArray(this .basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#scan
public int scan (String... basePackages) { int beanCountAtScanStart = this .registry.getBeanDefinitionCount(); doScan(basePackages); if (this .includeAnnotationConfig) { AnnotationConfigUtils.registerAnnotationConfigProcessors(this .registry); } return (this .registry.getBeanDefinitionCount() - beanCountAtScanStart); }
org.mybatis.spring.mapper.ClassPathMapperScanner#doScan
@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; }
修改了mapper的beanClass类型为MapperFactoryBean
private void processBeanDefinitions (Set<BeanDefinitionHolder> beanDefinitions) { AbstractBeanDefinition definition; BeanDefinitionRegistry registry = getRegistry(); for (BeanDefinitionHolder holder : beanDefinitions) { definition = (AbstractBeanDefinition) holder.getBeanDefinition(); boolean scopedProxy = false ; if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) { definition = (AbstractBeanDefinition) Optional .ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition()) .map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> new IllegalStateException( "The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]" )); scopedProxy = true ; } String beanClassName = definition.getBeanClassName(); LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName + "' mapperInterface" ); definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); definition.setBeanClass(this .mapperFactoryBeanClass); definition.getPropertyValues().add("addToConfig" , this .addToConfig); definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName); boolean explicitFactoryUsed = false ; if (StringUtils.hasText(this .sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory" , new RuntimeBeanReference(this .sqlSessionFactoryBeanName)); explicitFactoryUsed = true ; } else if (this .sqlSessionFactory != null ) { definition.getPropertyValues().add("sqlSessionFactory" , this .sqlSessionFactory); explicitFactoryUsed = true ; } if (StringUtils.hasText(this .sqlSessionTemplateBeanName)) { if (explicitFactoryUsed) { LOGGER.warn( () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored." ); } definition.getPropertyValues().add("sqlSessionTemplate" , new RuntimeBeanReference(this .sqlSessionTemplateBeanName)); explicitFactoryUsed = true ; } else if (this .sqlSessionTemplate != null ) { if (explicitFactoryUsed) { LOGGER.warn( () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored." ); } definition.getPropertyValues().add("sqlSessionTemplate" , this .sqlSessionTemplate); explicitFactoryUsed = true ; } if (!explicitFactoryUsed) { LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'." ); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } definition.setLazyInit(lazyInitialization); if (scopedProxy) { continue ; } if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null ) { definition.setScope(defaultScope); } if (!definition.isSingleton()) { BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true ); if (registry.containsBeanDefinition(proxyHolder.getBeanName())) { registry.removeBeanDefinition(proxyHolder.getBeanName()); } registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition()); } } }
上述几步主要是完成通过
@MapperScan(basePackages = “com.mybatis.mapper”)
这个定义,扫描指定包下的mapper接口,然后设置每个mapper接口的beanClass属性为MapperFactoryBean类型并加入到spring的bean容器中。
MapperFactoryBean实现了FactoryBean接口,所以当spring从待实例化的bean容器中遍历到这个bean并开始执行实例化时返回的对象实际上是getObject方法中返回的对象。
public class MapperFactoryBean <T > extends SqlSessionDaoSupport implements FactoryBean <T >
最后看一下MapperFactoryBean的getObject方法,实际上返回的就是mybatis中通过getMapper拿到的对象,熟悉mybatis源码的就应该清楚,这个就是mybatis通过动态代理生成的mapper接口实现类:
@Override public T getObject () throws Exception { return getSqlSession().getMapper(this .mapperInterface); }
到此,mapper接口现在也通过动态代理生成了实现类,并且注入到spring的bean容器中了,之后使用者就可以通过@Autowired或者getBean等方式,从spring容器中获取到了。
6.4 SpringBoot + Mybatis实现动态数据源切换 1. 动态数据源介绍 **业务背景 **:
电商订单项目分正向和逆向两个部分:其中正向数据库记录了订单的基本信息,包括订单基本信息、订单商品信息、优惠卷信息、发票信息、账期信息、结算信息、订单备注信息、收货人信息等;逆向数据库主要包含了商品的退货信息和维修信息。数据量超过500万行就要考虑分库分表和读写分离,那么我们在正向操作和逆向操作的时候,就需要动态的切换到相应的数据库,进行相关的操作。
**解决思路 **:
现在项目的结构设计基本上是基于MVC的,那么数据库的操作集中在dao层完成,主要业务逻辑在service层处理,controller层处理请求。假设在执行dao层代码之前能够将数据源(DataSource)换成我们想要执行操作的数据源,那么这个问题就解决了
原理图 :
Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。
查看AbstractRoutingDataSource类:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { public void setTargetDataSources (Map<Object, Object> targetDataSources) { this .targetDataSources = targetDataSources; } protected abstract Object determineCurrentLookupKey () ; }
上面源码中还有另外一个核心的方法setTargetDataSources(Map<Object, Object>targetDataSources)
,它需要一个Map,在方法注释中我们可以得知,这个Map存储的就是我们配置的多个数据源的键值对。我们整理一下这个类切换数据源的运作方式,这个类在连接数据库之前会执行determineCurrentLookupKey()
方法,这个方法返回的数据将作为key去targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接。
它是一个abstract类,所以我们使用的话,推荐的方式是创建一个类来继承它并且实现它的determineCurrentLookupKey() 方法,这个方法介绍上面也进行了说明,就是通过这个方法进行数据源的切换。
2. 代码实现 实体类 @Data public class Product { private Integer id; private String name; private Double price; }
ProductMapper public interface ProductMapper { @Select("select * from product") public List<Product> findAllProductM () ; @Select("select * from product") public List<Product> findAllProductS () ; }
ProductService @Service public class ProductService { @Autowired private ProductMapper productMapper; public void findAllProductM () { List<Product> allProductM = productMapper.findAllProductM(); System.out.println(allProductM); } public void findAllProductS () { List<Product> allProductS = productMapper.findAllProductS(); System.out.println(allProductS); } }
ProductController @RestController public class ProductController { @Autowired private ProductService productService; @GetMapping("/findAllProductM") public String findAllProductM () { productService.findAllProductM(); return "master" ; } @GetMapping("/findAllProductS") public String findAllProductS () { productService.findAllProductS(); return "slave" ; } }
配置多数据源 首先,我们在application.properties中配置两个数据源 spring.druid.datasource.master.password=root spring.druid.datasource.master.username=root spring.druid.datasource.master.jdbcurl= jdbc:mysql://localhost:3306/product_master?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC spring.druid.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver spring.druid.datasource.slave.password=root spring.druid.datasource.slave.username=root spring.druid.datasource.slave.jdbcurl= jdbc:mysql://localhost:3306/product_slave?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC spring.druid.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
在SpringBoot的配置代码中,我们初始化两个数据源:
@Configuration public class MyDataSourceConfiguratioin { Logger logger = LoggerFactory.getLogger(MyDataSourceConfiguratioin.class); @Bean("masterDataSource") @ConfigurationProperties(prefix = "spring.druid.datasource.master") DataSource masterDataSource () { logger.info("create master datasource..." ); return DataSourceBuilder.create().build(); } @Bean("slaveDataSource") @ConfigurationProperties(prefix = "spring.druid.datasource.slave") DataSource slaveDataSource () { logger.info("create slave datasource..." ); return DataSourceBuilder.create().build(); } }
编写RoutingDataSource 然后,我们用Spring内置的RoutingDataSource,把两个真实的数据源代理为一个动态数据源: public class RoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey () { return "masterDataSource" ; } }
对这个RoutingDataSource ,需要在SpringBoot中配置好并设置为主数据源:
@Bean @Primary DataSource primaryDataSource ( @Autowired @Qualifier("masterDataSource") DataSource masterDataSource, @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource) { logger.info("create routing datasource..." ); Map<Object, Object> map = new HashMap<>(); map.put("masterDataSource" , masterDataSource); map.put("slaveDataSource" , slaveDataSource); RoutingDataSource routing = new RoutingDataSource(); routing.setTargetDataSources(map); routing.setDefaultTargetDataSource(masterDataSource); return routing; }
现在, RoutingDataSource 配置好了,但是,路由的选择是写死的,即永远返回”masterDataSource”, 那么问题来了:如何存储动态选择的key以及在哪设置key? 在Servlet的线程模型中,使用ThreadLocal存储key最合适,因此,我们编写一个 RoutingDataSourceContext ,来设置并动态存储key:
public class RoutingDataSourceContext { static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>(); public static String getDataSourceRoutingKey () { String key = threadLocalDataSourceKey.get(); return key == null ? "masterDataSource" : key; } public RoutingDataSourceContext (String key) { threadLocalDataSourceKey.set(key); } public void close () { threadLocalDataSourceKey.remove(); } }
然后,修改RoutingDataSource ,获取key的代码如下
public class RoutingDataSource extends AbstractRoutingDataSource { protected Object determineCurrentLookupKey () { return RoutingDataSourceContext.getDataSourceRoutingKey(); } }
这样,在某个地方,例如一个Controller的方法内部,就可以动态设置DataSource的Key:
@GetMapping("/findAllProductM") public String findAllProductM () { String key = "masterDataSource" ; RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key); productService.findAllProductM(); return "master" ; } @GetMapping("/findAllProductS") public String findAllProductS () { String key = "slaveDataSource" ; RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key); productService.findAllProductS(); return "slave" ; }
到此为止,我们已经成功实现了数据库的动态路由访问。
3. 优化 以上代码是可行的,但是,需要读数据库的地方,就需要加上一大段RoutingDataSourceContext
Spring提供的声明式事务管理,就只需要一个@Transactional() 注解,放在某个Java方法上,这个方法就自动具有了事务。
我们也可以编写一个类似的@RoutingWith(“slaveDataSource”) 注解,放到某个Controller的方法上,这个方法内部就自动选择了对应的数据源。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RoutingWith { String value () default "master" ; }
编译前需要添加一个Maven依赖:
<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
切面类:
@Aspect @Component public class RoutingAspect { @Around("@annotation(routingWith)") public Object routingWithDataSource (ProceedingJoinPoint joinPoint, RoutingWith routingWith) throws Throwable { String key = routingWith.value(); RoutingDataSourceContext ctx = new RoutingDataSourceContext(key); return joinPoint.proceed(); } }
注意方法的第二个参数RoutingWith是Spring传入的注解实例,我们根据注解的value()获取配置的key。
改造方法:
@RoutingWith("masterDataSource") @GetMapping("/findAllProductM") public String findAllProductM () { productService.findAllProductM(); return "lagou" ; } @RoutingWith("slaveDataSource") @GetMapping("/findAllProductS") public String findAllProductS () { productService.findAllProductS(); return "lagou" ; }
到此为止,我们就实现了用注解动态选择数据源的功能。