springboot情操陶冶-web配置(六)

本文则针对数据库的连接配置作下简单的分析,方便笔者理解以及后续的查阅

栗子当先

以我们经常用的mybatis数据库持久框架来操作mysql服务为例


环境依赖

1.JDK v1.8+
2.springboot v2.0.3.RELEASE
3.mybatis v3.4.6
4.mysql v10.2.8-MarialDB

配置类步骤

1.pom.xml

<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-autoconfigure</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.40</version>
</dependency>

2.springboot配置 application-datasource.properties

#datasource config
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/boot?useSSL=false&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimeZone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

#mybatis config
mybatis.check-config-location=false
mybatis.mapper-locations=classpath*:database/mybatis/mapper/*.xml
mybatis.executor-type=reuse

代码栗子步骤

1.实体类User.java

package com.example.demo.database.entity;

/**
 * @author nanco
 * -------------
 * demo-springboot
 * -------------
 * @create 2018/10/17 16:52
 **/
public class User {
    private Long id ;

    private String name ;

    private Integer age ;

    private String email ;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                '}';
    }
}

2.mapper配置文件 UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.demo.database.mysql.dao.UserDao">

    <insert id="saveUser" parameterType="com.example.demo.database.entity.User">
        insert into tbl_user(name,age,email)
        values(#{name},#{age},#{email})
    </insert>
</mapper>

3.为了使上述配置生效,则须定义扫描入口(@MapperScan)

package com.example.demo.database.config;

import com.mysql.jdbc.Driver;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;

/**
 * @author nanco
 * -------------
 * demo-springboot
 * -------------
 * @create 2018/10/17 17:09
 **/
@Configuration
public class DatabaseConfig {

    @Configuration
    @ConditionalOnClass(Driver.class)
    @MapperScan("com.example.demo.database.mysql.dao")
    static class MysqlInterfaceScanner {

    }
}

4.来一个测试类

package com.example.demo.database.mysql;

import com.example.demo.database.DatabaseApplication;
import com.example.demo.database.entity.User;
import com.example.demo.database.mysql.dao.UserDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.Assert;

import javax.annotation.Resource;

/**
 * @author nanco
 * -------------
 * demo-springboot
 * -------------
 * @create 2018/10/17 17:13
 **/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DatabaseApplication.class})
public class MysqlDaoTest {

    @Resource
    private UserDao userDao;

    @Test
    public void testSave() {
        User user = new User();
        user.setName("nanco");
        user.setAge(18);
        user.setEmail("nancoasky@gmail.com");
        System.out.println(userDao.saveUser(user));
    }
}

运行上述的测试案例便完成了简单的插入功能,其他的功能读者可自行编写

源码层

查阅了spring-boot-autoconfigure包下的spring.factories,发现对于数据源的配置是通过DataSourceAutoConfiguration类来进行的,由此简单的展开下

DataSourceAutoConfiguration

本类的注册是有条件的,其类上的注解是

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
		DataSourceInitializationConfiguration.class })

其中DataSource类是JDK自带的,EmbeddedDatabaseType类则是依赖spring-jdbc包,本例中引入mybatis则默认带入了上述包。笔者按照@Configuration的加载顺序来对此类作下简单的分析

1.静态内部类注册解析


数据源检测

	@Configuration
	@Conditional(EmbeddedDatabaseCondition.class)
	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
	@Import(EmbeddedDataSourceConfiguration.class)
	protected static class EmbeddedDatabaseConfiguration {

	}

具体的代码读者可翻阅相应的文档,这里作下总结

  • 用户配置了spring.datasource.type属性或者classpath下存在springboot默认支持的数据源则该配置略过
  1. com.zaxxer.hikari.HikariDataSource
  2. org.apache.tomcat.jdbc.pool.DataSource
  3. org.apache.commons.dbcp2.BasicDataSource
  • 如果上述的条件不满足则会在classpath下找寻springboot默认支持的数据库驱动,存在则会创建SimpleDriverDataSource数据源用来创建数据库连接
  1. H2 Database
  2. Derby Database
  3. HSQL Database

本例中引入mybatis-spring-boot-starter便会引入spring-jdbc包,则会采用HikariDataSource数据源来获取数据库连接


数据源创建

	@Configuration
	@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 {

	}

根据用户配置的spring.datasource.type属性或者springboot默认支持的数据源(见上)来进行数据源对象的创建,具体的读者可自行阅读,本例中则会采取HikariDataSource数据源,并注入至bean工厂中,当然也可以通过配置项给予更多的属性配置

spring.datasource.hikari.max-wait-millis=10000
spring.datasource.hikari.min-idle=5
spring.datasource.hikari.initial-size=5
spring.datasource.hikari.validation-query=SELECT 1

2.导入类解析


数据库连接池状态类初始化

@Configuration
public class DataSourcePoolMetadataProvidersConfiguration {

	@Configuration
	@ConditionalOnClass(HikariDataSource.class)
	static class HikariPoolDataSourceMetadataProviderConfiguration {

		@Bean
		public DataSourcePoolMetadataProvider hikariPoolDataSourceMetadataProvider() {
			return (dataSource) -> {
				if (dataSource instanceof HikariDataSource) {
					return new HikariDataSourcePoolMetadata(
							(HikariDataSource) dataSource);
				}
				return null;
			};
		}

	}
}

HikariDataSource为例,则会创建HikariDataSourcePoolMetadata对象,主要是用来获取连接池的相关信息,看下DataSourcePoolMetadata接口就行,具体如下

public interface DataSourcePoolMetadata {

	/**
	 * Return the usage of the pool as value between 0 and 1 (or -1 if the pool is not
	 * limited).
	 * <ul>
	 * <li>1 means that the maximum number of connections have been allocated</li>
	 * <li>0 means that no connection is currently active</li>
	 * <li>-1 means there is not limit to the number of connections that can be allocated
	 * </li>
	 * </ul>
	 * This may also return {@code null} if the data source does not provide the necessary
	 * information to compute the poll usage.
	 * @return the usage value or {@code null}
	 */
	Float getUsage();

	/**
	 * Return the current number of active connections that have been allocated from the
	 * data source or {@code null} if that information is not available.
	 * @return the number of active connections or {@code null}
	 */
	Integer getActive();

	/**
	 * Return the maximum number of active connections that can be allocated at the same
	 * time or {@code -1} if there is no limit. Can also return {@code null} if that
	 * information is not available.
	 * @return the maximum number of active connections or {@code null}
	 */
	Integer getMax();

	/**
	 * Return the minimum number of idle connections in the pool or {@code null} if that
	 * information is not available.
	 * @return the minimum number of active connections or {@code null}
	 */
	Integer getMin();

	/**
	 * Return the query to use to validate that a connection is valid or {@code null} if
	 * that information is not available.
	 * @return the validation query or {@code null}
	 */
	String getValidationQuery();

	/**
	 * The default auto-commit state of connections created by this pool. If not set
	 * ({@code null}), default is JDBC driver default (If set to null then the
	 * java.sql.Connection.setAutoCommit(boolean) method will not be called.)
	 * @return the default auto-commit state or {@code null}
	 */
	Boolean getDefaultAutoCommit();

}

sql脚本执行加载

@Configuration
@Import({ DataSourceInitializerInvoker.class,
		DataSourceInitializationConfiguration.Registrar.class })
class DataSourceInitializationConfiguration {
}

主要通过DataSourceInitializerInvoker类来进行sql脚本的执行加载,具体笔者就不贴代码了,作下简单的总结

  1. 如果spring.datasource.schema属性已指定相应的sql文件,则优先读取,并支持classpath路径查找
  2. 如果上述无配置,则默认读取classpath*:schema.sql/classpath*:schema-${platform}.sql文件(其中${platform}可用spring.datasource.platform指定)
  3. 如果没有上述文件,则不执行

温馨提示:如果想在环境运行的时候执行相应的sql语句,则仍需要另外配置用户名(spring.datasource.schema-username)与密码(spring.datasource.schema-password),方可执行

MybatisAutoConfiguration

其加载顺序是在上述的DataSourceAutoConfiguration之后的,看它头上的注解便可得知。读者在这之前最好已经了解了mybatis在spring中的相关用法,比如SqlsessionFactoryMappedStatement等基本概念,不了解也可直接戳笔者的先前文章Spring mybatis源码学习指引目录

springboot支持用户使用@MapperScan或者@Mapper注解来注册扫描相应的dao接口。后者只能应用于单个的数据源,一般推荐使用前者来进行扫描注册,因为前者有更多的属性配置。

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

  /**
   * Alias for the {@link #basePackages()} attribute. Allows for more concise
   * annotation declarations e.g.:
   * {@code @EnableMyBatisMapperScanner("org.my.pkg")} instead of {@code
   * @EnableMyBatisMapperScanner(basePackages= "org.my.pkg"})}.
   */
  String[] value() default {};

  /**
   * Base packages to scan for MyBatis interfaces. Note that only interfaces
   * with at least one method will be registered; concrete classes will be
   * ignored.
   */
  String[] basePackages() default {};

  /**
   * Type-safe alternative to {@link #basePackages()} for specifying the packages
   * to scan for annotated components. The package of each class specified will be scanned.
   * <p>Consider creating a special no-op marker class or interface in each package
   * that serves no purpose other than being referenced by this attribute.
   */
  Class<?>[] basePackageClasses() default {};

  /**
   * The {@link BeanNameGenerator} class to be used for naming detected components
   * within the Spring container.
   */
  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

  /**
   * This property specifies the annotation that the scanner will search for.
   * <p>
   * The scanner will register all interfaces in the base package that also have
   * the specified annotation.
   * <p>
   * Note this can be combined with markerInterface.
   */
  Class<? extends Annotation> annotationClass() default Annotation.class;

  /**
   * This property specifies the parent that the scanner will search for.
   * <p>
   * The scanner will register all interfaces in the base package that also have
   * the specified interface class as a parent.
   * <p>
   * Note this can be combined with annotationClass.
   */
  Class<?> markerInterface() default Class.class;

  /**
   * Specifies which {@code SqlSessionTemplate} to use in the case that there is
   * more than one in the spring context. Usually this is only needed when you
   * have more than one datasource.
   */
  String sqlSessionTemplateRef() default "";

  /**
   * Specifies which {@code SqlSessionFactory} to use in the case that there is
   * more than one in the spring context. Usually this is only needed when you
   * have more than one datasource.
   */
  String sqlSessionFactoryRef() default "";

  /**
   * Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.
   *
   */
  Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;

}

详细的属性注释也拷贝过来了,其可以针对不同的数据源扫描注册相应的dao接口,适用于多数据源应用。具体使用笔者就不展开了,具体看下其@MapperScan注解是如何被解析的,直接看MapperScannerRegistrar#registerBeanDefinitions()注册方法

  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));
  }

说白了也就是通过mybatis的接口扫描类ClassPathMapperScanner类进行具体的处理,很简单。具体的解析步骤可详细戳上述的文章链接

小结

mybatis属性在springboot的使用,笔者并没有展开,读者可详看MybatisProperties类便可。笔者发现过多冗长的代码会影响笔者乃至读者的回看,于是笔者决定后续的文章,笔者尽可能详细记录自己的思考步骤,至于代码就贴出相应的关键部分即可方便后续回看~

posted @ 2018-10-19 15:47  南柯问天  阅读(415)  评论(0编辑  收藏  举报