美文网首页Spring技术Spring源码分析
十、SpringBoot数据源属性注入与创建

十、SpringBoot数据源属性注入与创建

作者: 木石前盟Caychen | 来源:发表于2019-04-24 11:42 被阅读2次

    一、 前言

    ​ 在Spring Boot中常常会用到配置文件,最常见的莫过于数据源,基本上都使用配置文件的方式进行配置了,即在application.properties/yml中配置连接数据库的基本信息。

    #driver配置不需要配置,Spring Boot会从配置的url中试图获取driver名
    spring.datasourc.driver=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql:///springboot
    spring.datasource.username=root
    spring.datasource.password=admin
    

    具体在DataSourceBuilder类的build()方法中。

    DataSourceBuilder.build()

    public DataSource build() {
        Class<? extends DataSource> type = getType();
        DataSource result = BeanUtils.instantiate(type);
        //试图尝试获取driver名
        maybeGetDriverClassName();
        bind(result);
        return result;
    }
    

    DataSourceBuilder.maybeGetDriverClassName():

    private void maybeGetDriverClassName() {
        if (!this.properties.containsKey("driverClassName")
            && this.properties.containsKey("url")) {
            String url = this.properties.get("url");
            //从url中获取driver
            String driverClass = DatabaseDriver.fromJdbcUrl(url).getDriverClassName();
            this.properties.put("driverClassName", driverClass);
        }
    }
    

    DatabaseDriver.fromJdbcUrl():

    DatabaseDriver是个Enum枚举类,列举了所有数据库的驱动。

    public static DatabaseDriver fromJdbcUrl(String url) {
        if (StringUtils.hasLength(url)) {
            Assert.isTrue(url.startsWith("jdbc"), "URL must start with 'jdbc'");
            String urlWithoutPrefix = url.substring("jdbc".length()).toLowerCase();
            //values()返回Spring Boot配置的所有数据库驱动
            for (DatabaseDriver driver : values()) {
                for (String urlPrefix : driver.getUrlPrefixes()) {
                    String prefix = ":" + urlPrefix + ":";
                    if (driver != UNKNOWN && urlWithoutPrefix.startsWith(prefix)) {
                        return driver;
                    }
                }
            }
        }
        return UNKNOWN;
    }
    

    DatabaseDriver:

    public enum DatabaseDriver {
    
        UNKNOWN(null, null),
    
        DERBY("Apache Derby", "org.apache.derby.jdbc.EmbeddedDriver",
                "org.apache.derby.jdbc.EmbeddedXADataSource",
                "SELECT 1 FROM SYSIBM.SYSDUMMY1"),
    
        H2("H2", "org.h2.Driver", "org.h2.jdbcx.JdbcDataSource", "SELECT 1"),
    
        HSQLDB("HSQL Database Engine", "org.hsqldb.jdbc.JDBCDriver",
                "org.hsqldb.jdbc.pool.JDBCXADataSource",
                "SELECT COUNT(*) FROM INFORMATION_SCHEMA.SYSTEM_USERS"),
        
        SQLITE("SQLite", "org.sqlite.JDBC"),
    
        MYSQL("MySQL", "com.mysql.jdbc.Driver",
                "com.mysql.jdbc.jdbc2.optional.MysqlXADataSource", "SELECT 1"),
    
        MARIADB("MySQL", "org.mariadb.jdbc.Driver", "org.mariadb.jdbc.MariaDbDataSource",
                "SELECT 1") {
    
            //other code...
        },
    
        GAE(null, "com.google.appengine.api.rdbms.AppEngineDriver"),
    
    
        ORACLE("Oracle", "oracle.jdbc.OracleDriver",
                "oracle.jdbc.xa.client.OracleXADataSource", "SELECT 'Hello' from DUAL"),
    
        POSTGRESQL("PostgreSQL", "org.postgresql.Driver", "org.postgresql.xa.PGXADataSource",
                "SELECT 1"),
    
        JTDS(null, "net.sourceforge.jtds.jdbc.Driver"),
    
        SQLSERVER("Microsoft SQL Server", "com.microsoft.sqlserver.jdbc.SQLServerDriver",
                "com.microsoft.sqlserver.jdbc.SQLServerXADataSource", "SELECT 1") {
    
            //other code...
        },
    
        FIREBIRD("Firebird", "org.firebirdsql.jdbc.FBDriver",
                "org.firebirdsql.ds.FBXADataSource",
                "SELECT 1 FROM RDB$DATABASE") {
    
            //other code...
        },
    
        DB2("DB2", "com.ibm.db2.jcc.DB2Driver", "com.ibm.db2.jcc.DB2XADataSource",
                "SELECT 1 FROM SYSIBM.SYSDUMMY1") {
    
            //other code...
        },
    
        DB2_AS400("DB2 UDB for AS/400", "com.ibm.as400.access.AS400JDBCDriver",
                "com.ibm.as400.access.AS400JDBCXADataSource",
                "SELECT 1 FROM SYSIBM.SYSDUMMY1") {
    
            //other code...
        },
    
        TERADATA("Teradata", "com.teradata.jdbc.TeraDriver"),
    
        INFORMIX("Informix Dynamic Server", "com.informix.jdbc.IfxDriver", null,
                "select count(*) from systables") {
        
            //other code...
        };
        
        //other code...
    }
    

    二、属性注入与Bean创建

    ​ 在[【Spring Boot】(4)、配置文件值注入][(https://www.jianshu.com/p/83f8e9dae8ba
    )]文中简单地讲述了配置文件中属性值的注入,本文重新讲述几种属性注入的方法:

    • @Value注解
    • Environment环境变量
    • @ConfigurationProperties和@PropertySource注解
      • 注入全局配置
      • 注入非全局(即外部)配置

    在讲述各种属性注入方式的同时,顺便使用该注入方式进行Bean的创建,本文以创建DataSource数据源为例。

    2.1、@Value注解

    ​ 在【Spring Boot】(二)、配置文件值注入文中也讲了使用@Value注解的优缺点。

    #dbcp2.properties
    dbcp2.driver=com.mysql.jdbc.Driver
    dbcp2.url=jdbc:mysql:///springboot
    dbcp2.username=root
    dbcp2.password=admin
    
    @Configuration
    @PropertySource("classpath:dbcp2.properties")//如果配置信息不在全局配置文件application.properties/yml中,则需要显式加载外部properties
    public class Dbcp2Config {
    
        @Value("${dbcp2.driver}")
        private String driver;
    
        @Value("${dbcp2.url}")
        private String url;
    
        @Value("${dbcp2.username}")
        private String username;
    
        @Value("${dbcp2.password}")
        private String password;
        
        //...
    }
    

    注意点:如果dbcp2的配置放置在外部配置文件中,必须显式地加载外部配置文件;否则的话,使用@Value就会提示java.lang.IllegalArgumentException: Could not resolve placeholder 'dbcp2.driver' in value "${dbcp2.driver}"的异常信息。

    最后创建dbcp2的DataSource的时候,可以直接使用最基本的四个参数值就可以完成Bean的创建。

    @Configuration
    @PropertySource("classpath:dbcp2.properties")//如果配置信息不在全局配置文件application.properties/yml中,则需要显式加载外部properties
    public class Dbcp2Config {
    
        @Value("${dbcp2.driver}")
        private String driver;
    
        @Value("${dbcp2.url}")
        private String url;
    
        @Value("${dbcp2.username}")
        private String username;
    
        @Value("${dbcp2.password}")
        private String password;
        
        @Bean("dbcp2")
        public DataSource dataSource(){
            BasicDataSource dataSource = new BasicDataSource();
            System.out.println("@Value ===================:" + driver);
    
            dataSource.setDriverClassName(driver);
            dataSource.setUrl(url);
            dataSource.setUsername(username);
            dataSource.setPassword(password);
    
            return dataSource;
        }
    }
    

    2.2、Environment环境变量

    ​ 其实使用Environment变量和使用@Value注解类似,不同的是:@Value是直接将值注入到具体的属性上,而Environment是把所有的配置项添加到环境变量中,然后在取值的时候使用getProperty(key)方法即可。

    #c3p0.properties
    c3p0.driver=com.mysql.jdbc.Driver
    c3p0.url=jdbc:mysql:///springboot
    c3p0.username=root
    c3p0.password=admin
    
    @Configuration
    @PropertySource("classpath:c3p0.properties")//引入外部配置文件
    public class C3p0Config {
    
        @Autowired
        private Environment env;
    
    }
    

    注意点:使用Environment环境变量和使用@Value注解一样,对于外部配置文件需要显式加载。

    @Configuration
    @PropertySource("classpath:c3p0.properties")//引入外部配置文件
    public class C3p0Config {
    
        @Autowired
        private Environment env;
        
        @Bean("c3p0")
        public DataSource dataSource() throws PropertyVetoException {
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            System.out.println("env ==========:" + env.getProperty("c3p0.driver"));
    
            dataSource.setDriverClass(env.getProperty("c3p0.driver"));
            dataSource.setJdbcUrl(env.getProperty("c3p0.url"));
            dataSource.setUser(env.getProperty("c3p0.username"));
            dataSource.setPassword(env.getProperty("c3p0.password"));
            return dataSource;
        }
    }
    

    EnvironmentgetProperty()有个重载的方法:

    String getProperty(String key);
    String getProperty(String key, String defaultValue);
    

    如果使用getProperty(key),如果key不存在的时候,方法返回null,而不会报异常,这跟@Value有些区别。如果使用getProperty(key, defaultValue),见名思意,即当key不存在的时候,可以使用指定的默认值代替。

    2.3、@ConfigurationProperties和@PropertySource注解

    2.3.1、注入全局配置属性

    #application.properties
    druid.driver-class-name=com.mysql.jdbc.Driver
    druid.url=jdbc:mysql:///springboot
    druid.username=root
    druid.password=admin
    
    @ConfigurationProperties(prefix = "druid")
    public class DruidProperties {
    
        private String driverClassName;
        private String url;
        private String username;
        private String password;
    
        //getter和setter
    }
    
    @Configuration
    @EnableConfigurationProperties(DruidProperties.class)
    public class DruidConfig {
    
        private DruidProperties properties;
    
        public DruidConfig(DruidProperties properties){
            this.properties = properties;
        }
    }
    

    @EnableConfigurationProperties启动配置功能,将对应的DruidProperties配置类与全局配置文件(默认)中的以druid为前缀的配置项进行绑定并注入,并将DruidProperties配置类注入到Spring的IOC环境中。这个时候就可以创建一个DruidDataSource的Bean。

    @Configuration
    @EnableConfigurationProperties(DruidProperties.class)
    public class DruidConfig {
    
        private DruidProperties properties;
    
        public DruidConfig(DruidProperties properties){
            this.properties = properties;
        }
        
        @Bean("druid")
        public DruidDataSource dataSource(){
            System.out.println("properties =====================:" + properties.getDriverClassName());
    
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName(properties.getDriverClassName());
            dataSource.setUrl(properties.getUrl());
            dataSource.setUsername(properties.getUsername());
            dataSource.setPassword(properties.getPassword());
            return dataSource;
        }
    }
    

    此时创建的DataSource中包含了基本的四个属性值,就能执行数据库操作了。

    2.3.2、注入外部配置属性

    还是同全局配置中的属性值一样,只是将其提取到druid.properties中了。

    #druid.properties
    druid.driver-class-name=com.mysql.jdbc.Driver
    druid.url=jdbc:mysql:///springboot
    druid.username=root
    druid.password=admin
    

    在DruidProperties中加入@PropertySource("classpath:druid.properties")一行,提示Spring Boot从外部druid.properties中读取属性值。

    @ConfigurationProperties(prefix = "druid")
    @PropertySource("classpath:druid.properties")
    public class DruidProperties {
    
        private String driverClassName;
        private String url;
        private String username;
        private String password;
    
        //getter和setter
    }
    

    DruidConfig.java同上小节的一样,保持不变。

    ​ 在运行的时候,会发现DruidProperties并未注入,即数据源的四个属性值都未正常注入。在调试的时候,发现DruidProperties的属性setter方法并未进入。

    ​ 其实注入外部配置属性的时候,需要手动将该类注册为组件,即使用@Component或者@Configuration注解才行。与此同时,由于DruidConfig类上有个@EnableConfigurationProperties(DruidProperties.class)注解,既然已经手动在DruidProperties上添加了@Component或者@Configuration注解使其成为组件,那么就无需再在DruidConfig上通过@EnableConfigurationProperties(DruidProperties.class)再将DruidProperties注册为组件,否则会在自动注入使用DruidProperties的时候会找到两个Bean,导致出现org.springframework.beans.factory.NoUniqueBeanDefinitionException唯一性异常。

    代码如下:

    @Component
    @ConfigurationProperties(prefix = "druid")
    @PropertySource("classpath:druid.properties")
    public class DruidProperties {
    
        private String driverClassName;
        private String url;
        private String username;
        private String password;
    
        //other code...
    }
    
    
    @Configuration
    @EnableConfigurationProperties//(DruidProperties.class)
    public class DruidConfig {
    
        private DruidProperties properties;
    
        public DruidConfig(DruidProperties properties){
            this.properties = properties;
        }
    
        @Bean("druid")
        public DruidDataSource dataSource(){
            System.out.println("properties =====================:" + properties.getDriverClassName());
    
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName(properties.getDriverClassName());
            dataSource.setUrl(properties.getUrl());
            dataSource.setUsername(properties.getUsername());
            dataSource.setPassword(properties.getPassword());
            return dataSource;
        }
    }
    

    三、疑问?

    ​ 在第2.3.1小节中讲述到,使用@EnableConfigurationProperties会将其属性value对应的配置类与配置文件绑定,并将其注入到Spring IOC容器中。而对于第2.3.2小节中,既然有这个功能,为什么还要在配置类上加上组件注解吗?

    其实博主在调试的时候,发现Spring IOC容器中确实有配置类的Bean,但是就是没办法进入到属性注入的setter方法中!!!

    ​ 在博主历经N次调试后才发现,在使用@PropertySource(s)注解引入外部配置文件的时候,如果不在配置类上手动添加组件注解的话,该配置文件就无法加载到Spring环境中,导致想要给配置类属性进行注入的时候就不能成功执行,只能是null了。

    下图是不使用注解时,环境中的数据:

    不使用注解时环境中的变量信息.png

    如果使用注解的话,环境中就多了外部配置文件信息:


    使用变量时环境中的变量信息.png

    总结:要想使用外部配置文件进行注入的话,必须要使用组件注解才能成功。

    相关文章

      网友评论

        本文标题:十、SpringBoot数据源属性注入与创建

        本文链接:https://www.haomeiwen.com/subject/ljgwgqtx.html