通过 org.springframework.boot.jdbc.DataSourceBuilder.MappedDataSourceProperties 学习适配器模式和策略模式。

背景介绍

适配器模式:让新接口适配老规范的一种设计思想,属于结构型设计模式,用于解决接口不兼容的问题。它通过创建一个 “适配器” 类,将一个类的接口转换为客户端期望的另一个接口,使原本不兼容的类可以一起工作。

策略模式:属于行为型设计模式,它定义了一系列算法(策略),将每个算法封装起来并使其可以互换。客户端可以在不修改算法实现的情况下,动态选择不同的算法。一般会有一个定义所有支持的算法的公共接口。

自 java1.4 之后,官方推荐使用 javax.sql.DataSource 接口连接任意物理数据库,这个接口主要定义了连接数据库的核心方法(Connection getConnection() throws SQLException;),所有现有的连接池设计都会实现这个接口。而在 java 生态的长期发展过程中,大多数成熟的连接池实现方案,都默认约定在自己的DataSource实现类中声明了username,password,driverClassName,url这些通用字段,以方便配置和使用。

以 Hikari 为例:

class HikariDataSource extends HikariConfig implements DataSource, Closeable

其中 HikariConfig 里定义了 jdbcUrl driverClassName user password 这些字段。

public class HikariConfig implements HikariConfigMXBean
{
   private static final Logger LOGGER = LoggerFactory.getLogger(HikariConfig.class);

    ......

   // Properties changeable at runtime through the HikariConfigMXBean
   //
   ...
   private volatile String username;
   private volatile String password;

   // Properties NOT changeable at runtime
   //
   ...
   private String driverClassName;
   ...
   private String jdbcUrl;
   ...

   ...   
}

问题介绍

假设你是一个 Spring 框架的开发者,在设计时,你希望能无缝接入一部分市面上主流的连接池。调研后你发现,这些连接池除了统一实现 DataSource 接口,还十分默契地约定了这些通用字段的配置,这种接口之外的约定自然也是我们需要统一适配的一部分。思考一下有没有比较合适的方案?

实现难点

接口规范和社区约定之间的差别

java 官方推荐接口是:

package javax.sql;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Wrapper;

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password)
    throws SQLException;
}

大部分 MySQL 连接池都会在自己的实现类中声明这些通用属性:

public class XXXDataSource implements DataSource {
    
    String userName;
    String password;
    String url;
    String driverClassName;
    
    ......
}

虽然大家声明了相同的字段,**但并没有继承同一个基类,所以这些字段无法通过运行时多态来适配。**同时,并非所有的 DataSource 实现都会声明这些字段,比如一些嵌入式数据库就完全不需要 driverClassName,这也是需要我们去考虑的地方。

相同的字段不同的命名

其实各个连接池对通用字段的命名也不一致(比如 url 在 Hikari 里声明为 jdbcUrl),这点在Spring源码中也有体现:

private enum DataSourceProperty {

    URL(false, "url", "URL"),

    DRIVER_CLASS_NAME(true, "driverClassName"),

    USERNAME(false, "username", "user"),

    PASSWORD(false, "password");
		
    ......
}

虽然可以通过反射来适配这些字段,但是反射的性能消耗大,字段不能稳定匹配,并且对已知的连接池的声明属性反复使用反射来 get 和 set 也并不优雅。在 Spring 中,其实有对未知连接池使用反射来匹配字段的兜底策略,兜底策略只是下策,不能作为首选方案。

反射来匹配字段见:org.springframework.boot.jdbc.DataSourceBuilder.ReflectionDataSourceProperties

代码实践

Spring 的核心思想是“约定先于配置”,希望开发者在使用过程中即插即用地引入依赖,所以采用了 SPI 和非现实配置的策略,这一点在整个DataSource的自动装配过程中都有体现,这个思想也是我们实现无缝接入一部分市面上主流的连接池的大前提,现在开始自己实现在 Spring 中装配 DataSource 的过程。

以下过程是Spring装配过程的简化版本,主要是方便理解,连接池以 Hikari 和 DBCP 为例。

编写SPI基本配置

首先我们需要设计一下属性配置类,这个属性配置类中约定了上文提到的一些通用配置属性:

@ConfigurationProperties(prefix = "my.datasource")
public class DataSourceProperties{
    
    private String url;
    private String username;
    private String password;
    private String driverClassName;
    
}

按照非显式配置的思想,我们设计一下 DataSource 的SPI自动配置类。这里的自动配置会和 Spring 里面的有所不同,Spring 默认是不推荐直接使用 DataSource 来进行数据库操作的,所以 Spring 强制要求只有在 org.springframework.jdbc 依赖被引入的时候才能触发 DataSource 的自动装配,但我们这里就不要求了。

@Configuration
@ConditionalOnClass(DataSource.class) // 只有在 DataSource.class 可以被加载到的时候才能触发这个自动配置
@EnableConfigurationProperties(DataSourceProperties.class) // 添加我们上文定义的属性配置类
public class DataSourceAutoConfiguration {
    
}

在这里启用 DataSourceProperties 方便后续其他数据源配置类自动注入,然后以 Hikari 和 DBCP 为例,我们分别编写两者的自动装配类

Maven 坐标:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>4.0.3</version>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.9.0</version>
    <optional>true</optional>
</dependency>

这是 Hikari 的装配类

@Configuration
@ConditionalOnClass(HikariDataSource.class) // com.zaxxer.hikari.HikariDataSource
@ConditionalOnMissingBean(DataSource.class)
public class HikariDataSourceConfiguration {
    
    @Bean
    HikariDataSource dataSource(DataSourceProperties properties) { // 自动注入
        // 这一段是伪代码
        return createDataSource(properties);
    }
}

这是 DBCP 的装配类

@Configuration
@ConditionalOnClass(BasicDataSource.class) // org.apache.commons.dbcp2.BasicDataSource
@ConditionalOnMissingBean(DataSource.class)
public class Dbcp2DataSourceConfiguration {

	@Bean
	BasicDataSource dataSource(DataSourceProperties properties) {
        // 这一段是伪代码
		return createDataSource(properties);
	}
}

这两者在创建 createDataSource 方法上有不少相似的地方,并且都是 DataSource 接口的实现类,这里就可以先整合在一起,让后再使用策略模式进行优化。

public class DataSourceConfiguration {
    
    /** 内部使用 **/
    protected static  <T extends DataSource> T createDataSource(DataSourceProperties properties, Class<T> type) {
        // TODO
        T dataSource = BeanUtils.instantiateClass(type);
        return dataSource;
    }
    
    /**
	 * Hikari DataSource configuration.
	 */
    @Configuration
	@ConditionalOnClass(HikariDataSource.class) // com.zaxxer.hikari.HikariDataSource
	@ConditionalOnMissingBean(DataSource.class)
	static class Hikari {
    
    	@Bean
    	HikariDataSource dataSource(DataSourceProperties properties) { // 自动注入
        	return createDataSource(properties, HikariDataSource.class);
    	}
	}
    
    /**
	 * DBCP DataSource configuration.
	 */
    @Configuration
	@ConditionalOnClass(BasicDataSource.class) // org.apache.commons.dbcp2.BasicDataSource
	@ConditionalOnMissingBean(DataSource.class)
	static class Dbcp2 {

		@Bean
		BasicDataSource dataSource(DataSourceProperties properties) {
			return createDataSource(properties, BasicDataSource.class);
		}
	}
    
}

让后我们将这两个配置类加入到DataSourceAutoConfiguration里:

@Configuration
@ConditionalOnClass(DataSource.class) // 只有在 DataSource.class 可以被加载到的时候才能触发这个自动配置
@EnableConfigurationProperties(DataSourceProperties.class) // 添加我们上文定义的属性配置类
@Import({
    DataSourceConfiguration.Hikari.class,
    DataSourceConfiguration.Dbcp2.class
})
public class DataSourceAutoConfiguration {
    
}

这里就将 Hikari 和 DBCP2 的 SPI 配置基本完成,Import 中规定了配置的顺序,也隐形定义了连接池的配置优先级(Hikari > DBCP),不过,当开发者引入多个连接池依赖的时候,也要允许开发者可以自己选择连接池,所以我们继续改造一下代码。

@ConfigurationProperties(prefix = "my.datasource")
public class DataSourceProperties{
    
    private String url;
    private String username;
    private String password;
    private String driverClassName;
    // type 可指定连接池
    private Class<? extends DataSource> type;
    
}
public class DataSourceConfiguration {
    
    /** 内部使用 **/
    protected static  <T extends DataSource> T createDataSource(DataSourceProperties properties, Class<T> type) {
        // TODO
        T dataSource = BeanUtils.instantiateClass(type);
        return dataSource;
    }
    
    /**
	 * Hikari DataSource configuration.
	 */
    @Configuration
	@ConditionalOnClass(HikariDataSource.class) // com.zaxxer.hikari.HikariDataSource
	@ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "my.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
			matchIfMissing = true)
	static class Hikari {
    
    	@Bean
    	HikariDataSource dataSource(DataSourceProperties properties) { // 自动注入
        	return createDataSource(properties, HikariDataSource);
    	}
	}
    
    /**
	 * DBCP DataSource configuration.
	 */
    @Configuration
	@ConditionalOnClass(BasicDataSource.class) // org.apache.commons.dbcp2.BasicDataSource
	@ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "my.datasource.type", havingValue = "org.apache.commons.dbcp2.BasicDataSource",
			matchIfMissing = true)
	static class Dbcp2 {

		@Bean
		BasicDataSource dataSource(DataSourceProperties properties) {
			return createDataSource(properties, BasicDataSource.class);
		}
	}
    
}

现在可以测试一下能不能自动配置好我们指定的 DataSource 了。

这里需要补充测试代码

策略模式实现配置不同连接池

我们通过策略模式来配置不同的 DataSource 里面的属性。现在只有两个连接池,我们首先实现逻辑。

public class DataSourceConfiguration {
    
    protected static  <T extends DataSource> T createDataSource(DataSourceProperties properties, Class<T> type) {
        T dataSource = BeanUtils.instantiateClass(type);
        if(HikariDataSource.class.isAssignableFrom(type)){
            ((HikariDataSource) dataSource).setJdbcUrl(properties.getUrl());
            ((HikariDataSource) dataSource).setUsername(properties.getUsername());
            ((HikariDataSource) dataSource).setPassword(properties.getPassword());
            ((HikariDataSource) dataSource).setDriverClassName(properties.getDriverClassName());
        } else if (BasicDataSource.class.isAssignableFrom(type)){
            // TODO
        }
        return (T) dataSource;
    }
    
    ......
        
}

这么写虽然可以,但是过于丑陋了,现在我们只是接入两个连接池,随着后续我们接入的连接池越来越多,这个代码的维护成本将不敢想象。

那么首先我们将不同的 DataSource 的属性映射策略抽象出来,我们将这个类命名为 MappedProperties

public interface MappedProperties<T extends DataSource> {
    
    void set(T target, DataSourcePropertyEnum property, String value);
    
    String get(T target, DataSourcePropertyEnum property)
    
}

定义通用属性的枚举类:

public enum DataSourcePropertyEnum {
    URL,
    USERNAME,
    DRIVER_CLASS_NAME,
    PASSWORD,
}

在这里先实现 Hikari 的属性映射策略:

public class HikariMappedProperties implements MappedProperties<HikariDataSource> {
    @Override
    public void set(HikariDataSource target, DataSourceProperty property, String value) {
        switch (property) {
            case URL:
                target.setJdbcUrl(value);
                break;
            case PASSWORD:
                target.setPassword(value);
                break;
            case USERNAME:
                target.setUsername(value);
                break;
            case DRIVER_CLASS_NAME:
                target.setDriverClassName(value);
                break;
        }
    }

    @Override
    public String get(HikariDataSource target, DataSourceProperty property) {
        switch (property){
            case URL: 
                return target.getJdbcUrl();
            case DRIVER_CLASS_NAME: 
                return target.getDriverClassName();
            case USERNAME: 
                return target.getUsername();
            case PASSWORD: 
                return target.getPassword();
        }
        return null;
    }
}

同样思路设计完 DBCP2 的属性映射策略:

public class HikariMappedProperties implements MappedProperties<BasicDataSource> {
	// 省略了
}

我们开始写策略的选择逻辑,显而易见,策略被选择的条件为当前 DataSource 的实际类型刚好和策略要求的类型匹配上,所以我们给策略接口加上返回支持类型的方法声明。

public interface MappedProperties<T extends DataSource> {
    
    void set(T target, DataSourcePropertyEnum property, String value);
    String get(T target, DataSourcePropertyEnum property);
    Class<?> getDataSourceType();
}

以 Hikari 为例,我们实现一下这个方法:

public class HikariMappedProperties implements MappedProperties<HikariDataSource> {
    @Override
    public void set(HikariDataSource target, DataSourceProperty property, String value) {
        ...
    }

    @Override
    public String get(HikariDataSource target, DataSourceProperty property) {
        ...
    }
    
    @Override
    Class<?> getDataSourceType(){
        return HikariDataSource.class;
    };
}

然后我们编写策略选择逻辑,这种选择可以有多种写法,我们采用短路链的写法(和Spring的实现思路一致):

@SuppressWarnings("unchecked")
public class DataSourceConfiguration {

    protected static <T extends DataSource> T createDataSource(DataSourceProperties properties, Class<T> type) {
        T dataSource = BeanUtils.instantiateClass(type);
        MappedProperties<T> mappedProperties = lookForMappedProperties(type);       
        // TODO
        return dataSource;
    }

    private static <T extends DataSource> MappedProperties<T> lookForMappedProperties(Class<T> type) {
        MappedProperties<T> result = null;
        result = lookUp(result, type, HikariMappedProperties::new);
        result = lookUp(result, type, Dbcp2MappedProperties::new);
        return result;
    }

    /**
     * 短路链的核心,若已经找到对应的策略则立刻返回,否则尝试寻找策略
     */
    private static <T extends DataSource> MappedProperties<T> lookUp(MappedProperties<T> existing, Class<T> type, Supplier<MappedProperties<? extends DataSource>> supplier) {
        if (Objects.nonNull(existing)) {
            return existing;
        }
        MappedProperties<?> properties = supplier.get();
        return properties.getDataSourceType().isAssignableFrom(type) ? (MappedProperties<T>) properties : null;
    }

然后我们就可以直接对 DataSource 进行属性映射操作了:

@SuppressWarnings("unchecked")
public class DataSourceConfiguration {

    protected static <T extends DataSource> T createDataSource(DataSourceProperties properties, Class<T> type) {
        T dataSource = BeanUtils.instantiateClass(type);
        MappedProperties<T> mappedProperties = lookForMappedProperties(type);
        
        mappedProperties.set(dataSource, DataSourceProperty.URL, properties.getUrl());
        mappedProperties.set(dataSource, DataSourceProperty.USERNAME, properties.getUsername());
        mappedProperties.set(dataSource, DataSourceProperty.PASSWORD, properties.getPassword());
        mappedProperties.set(dataSource, DataSourceProperty.DRIVER_CLASS_NAME, properties.getDriverClassName());
        
        return dataSource;
    }
	
    ...
}

这样就在保证高度扩展性的前提下完成了通用属性的配置工作。

适配器模式优化属性映射

基本逻辑完成之后,我们再看一下最初的映射属性的实现:

public class HikariMappedProperties implements MappedProperties<HikariDataSource> {
    @Override
    public void set(HikariDataSource target, DataSourceProperty property, String value) {
        switch (property) {
            case URL:
                target.setJdbcUrl(value);
                break;
            case PASSWORD:
                target.setPassword(value);
                break;
            case USERNAME:
                target.setUsername(value);
                break;
            case DRIVER_CLASS_NAME:
                target.setDriverClassName(value);
                break;
        }
    }

    @Override
    public String get(HikariDataSource target, DataSourceProperty property) {
        switch (property){
            case URL: 
                return target.getJdbcUrl();
            case DRIVER_CLASS_NAME: 
                return target.getDriverClassName();
            case USERNAME: 
                return target.getUsername();
            case PASSWORD: 
                return target.getPassword();
        }
        return null;
    }
}

可以发现,随着需要接入的连接池的增加,中间出现了大量的重复逻辑。如果我们再实现一个其他连接池的映射策略的话,新增的重复冗余代码量也会很大。所以这样写依然不够优雅。通过观察不难发现,重复的逻辑基本都在映射对应 property 的 get 和 set 方法上,我们可以尝试用函数式编程的适配器模式方案再次优化一下。

添加两个函数式接口 Getter 和 Setter

@FunctionalInterface
public interface Getter<T,V> {

    V get(T target);
}

@FunctionalInterface
public interface Setter<T,V> {

    void set(T target, V value);
}

然后设计一个映射属性的模板类

@SuppressWarnings("unchecked")
public abstract class BaseMappedProperties<T extends DataSource> implements MappedProperties<T> {

    protected EnumSet<DataSourceProperty> applied = EnumSet.noneOf(DataSourceProperty.class);
    protected HashMap<DataSourceProperty, Setter<T, String>> setters = new HashMap<>();
    protected HashMap<DataSourceProperty, Getter<T, String>> getters = new HashMap<>();

    protected void addMapping(DataSourceProperty property,
            Setter<T, String> setter, Getter<T, String> getter) {
        if (applied.contains(property)) {
            return;
        }
        applied.add(property);
        setters.put(property, setter);
        getters.put(property, getter);
    }

    @Override
    public void set(T target, DataSourceProperty property, String value) {
        if (applied.contains(property)) {
            setters.get(property).set(target, value);
        }
    }

    @Override
    public <V> V get(T target, DataSourceProperty property) {
        if (applied.contains(property)){
            return (V) getters.get(property).get(target);
        }
        return null;
    }
    
    @Override
    public Class<?> getDataSourceType() {
        return (Class<?>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }
}

这样我们再实现 Hikari 的配置映射策略:

public class HikariMappedProperties extends BaseMappedProperties<HikariDataSource> {

    public HikariMappedProperties() {
        addMapping(DataSourceProperty.URL, HikariDataSource::setJdbcUrl, HikariDataSource::getJdbcUrl);
        addMapping(DataSourceProperty.USERNAME, HikariDataSource::setUsername, HikariDataSource::getUsername);
        addMapping(DataSourceProperty.PASSWORD, HikariDataSource::setPassword, HikariDataSource::getPassword);
        addMapping(DataSourceProperty.DRIVER_CLASS_NAME, HikariDataSource::setDriverClassName, HikariDataSource::getDriverClassName);
    }
}

接下来再实现 DBCP2 的映射策略:

public class Dbcp2MappedProperties extends BaseMappedProperties<BasicDataSource> {

    public Dbcp2MappedProperties() {
        addMapping(DataSourceProperty.URL, BasicDataSource::setUrl, BasicDataSource::getUrl);
        addMapping(DataSourceProperty.USERNAME, BasicDataSource::setUsername, BasicDataSource::getUsername);
        addMapping(DataSourceProperty.PASSWORD, BasicDataSource::setPassword, BasicDataSource::getPassword);
        addMapping(DataSourceProperty.DRIVER_CLASS_NAME, BasicDataSource::setDriverClassName, BasicDataSource::getDriverClassName);
    }

}

这样,一个优雅的函数式的适配器就完成了。

测试方案:

package priv.dawn.normalspringenv;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;

@SpringBootApplication
public class NormalSpringEnvApplication implements CommandLineRunner {

    @Autowired
    private DataSource dataSource;

    public static void main(String[] args) {
        SpringApplication.run(NormalSpringEnvApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        System.out.println(dataSource.getClass().getName());
        Connection connection = dataSource.getConnection();
        ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM user;");
        while (resultSet.next()) {
            int id = resultSet.getInt("id");
            String name = resultSet.getString("name");
            int age = resultSet.getInt("age");
            System.out.println("ID: " + id + ", Name: " + name + ", Age: " + age);
        }
        connection.close();
    }
}