2019-03-28 · Develop

SpringBoot 集成 Apollo

由于 Disconf 已经不再维护且和 Spring5.x 不兼容。考虑换成 Apollo。本文就是基于官方文档做的一个极简 DEMO

部署

官网上提供了多种部署方式的文档,为了快速体验,本文基于Docker方式部署Quick Start 进行简单部署。

首先拉取代码,切换到最新稳定版 V1.3.0 进行 Docker 部署。

git clone https://github.com/ctripcorp/apollo.git
git checkout -b 1.3.0 v1.3.0

cd apollo/scripts/docker-quick-start/
docker-compose up

然后访问 http://192.168.113.130:8070 使用 apollo/admin 进行登录就可以看见如下的页面

apollo-index

创建 App/Cluster/Namespace

参考Apollo使用指南 添加 App/Cluster/Namespace

如果已有 yml 格式的配置可以使用 YamlPropertiesFactoryBean.getObject 转成 properties 格式,当然也可以直接使用 yml 格式, Apollo 1.3.0 对 yml 有很好的支持。 为了测试 yml 文件,所以创建两个 Namespace 文件如下:

management:
  endpoints:
    web:
      exposure:
        include: "*"
spring:
  datasource:
    url: jdbc:mysql://192.168.19.124:3306/lo?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: lo
    password: lo
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      maximum-pool-size: 15
      auto-commit: true
      idle-timeout: 30000
      pool-name: LoHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
  jpa:
    show-sql: true
    database: mysql
    hibernate:
      ddl-auto: update

Spring Boot 集成

添加客户端依赖

<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.3.0</version>
</dependency>

项目中使用

在项目中使用起来就非常方便了,可以参考Java客户端使用指南 的详细文档。

@EnableApolloConfig({"application.yml", "business.yml"})
@SpringBootApplication
public class ApolloApplication {

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

在项目的启动类上使用 EnableApolloConfig 注解,注解可以指定加载的 namespaces 默认是加载 application.properties。 下面通过一个自动切换数据源的栗子来说明在 Java 中的多种调用配置的方式

@Configuration
@Slf4j
public class DataSourceConfig {

    @Value("${spring.datasource.driver-class-name:}")
    private String driverClassName;
    @Value("${spring.datasource.url:}")
    private String url;
    @Value("${spring.datasource.username:}")
    private String username;
    @Value("${spring.datasource.password:}")
    private String password;

    @Autowired
    private Environment environment;

    @Getter @Setter
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    @Configuration
    public class HikariConfig {

        private Long connectionTimeout;

        private String connectionTestQuery;
    }

    @Autowired
    private HikariConfig hikariConfig;

    // 默认是 application 非 properties 文件需要加后缀
    @ApolloConfig("application.yml")
    private Config config;

    private DataSource buildDataSource() {

        HikariDataSource hikariDataSource = new HikariDataSource();

        // @Value

        hikariDataSource.setJdbcUrl(url);
        hikariDataSource.setUsername(username);
        hikariDataSource.setPassword(password);
        hikariDataSource.setDriverClassName(driverClassName);

        // @ApolloConfig

        hikariDataSource.setMinimumIdle(config.getIntProperty("spring.datasource.hikari.minimum-idle", 5));
        hikariDataSource.setMaximumPoolSize(config.getIntProperty("spring.datasource.hikari.maximum-pool-size", 15));
        hikariDataSource.setAutoCommit(config.getBooleanProperty("spring.datasource.hikari.auto-commit", true));
        hikariDataSource.setIdleTimeout(config.getLongProperty("spring.datasource.hikari.idle-timeout", 30000L));

        // Environment

        hikariDataSource.setPoolName(environment.getProperty("spring.datasource.hikari.pool-name", "PmsHikariCP"));
        hikariDataSource.setMaxLifetime(Long.valueOf(environment.getProperty("spring.datasource.hikari.max-lifetime", "1800000")));

        // @ConfigurationProperties

        hikariDataSource.setConnectionTimeout(hikariConfig.getConnectionTimeout());
        hikariDataSource.setConnectionTestQuery(hikariConfig.getConnectionTestQuery());

        return hikariDataSource;
    }

    @ApolloConfigChangeListener
    private void dataSourceRebuild(ConfigChangeEvent changeEvent) {
        Set<String> changedKeys = changeEvent.changedKeys();
        // 如果数据源的 url 变化,则变换数据源
        if (changedKeys.contains("spring.datasource.url")) {
            DynamicDataSource dynamicDataSource = SpringContextUtils.getBean(DynamicDataSource.class);
            dynamicDataSource.setTargetDataSources(Collections.singletonMap("db", buildDataSource()));
            dynamicDataSource.afterPropertiesSet();
            log.info("切换数据源从[{}]到[{}]", changeEvent.getChange("spring.datasource.url").getOldValue(), changeEvent.getChange("spring.datasource.url").getNewValue());
        }
    }

    @Bean
    public DynamicDataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(Collections.singletonMap("db", buildDataSource()));
        return dynamicDataSource;
    }

    class DynamicDataSource extends AbstractRoutingDataSource {

        @Nullable
        @Override
        protected Object determineCurrentLookupKey() {
            return "db";
        }
    }
}

其中使用的工具类 SpringContextUtils 的代码如下

@Component
public class SpringContextUtils implements ApplicationContextAware {
    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }

    public static <T> T getBean(String name, Class<T> requiredType) {
        return applicationContext.getBean(name, requiredType);
    }

    public static boolean containsBean(String name) {
        return applicationContext.containsBean(name);
    }

    public static boolean isSingleton(String name) {
        return applicationContext.isSingleton(name);
    }

    public static Class<? extends Object> getType(String name) {
        return applicationContext.getType(name);
    }
}

除了上述四种方式外,还有使用比较多的通过 ConfigUtils 等工具类来获取配置的方式, Apollo 也提供了可以进行静态调用的方式

Config config = ConfigService.getConfig(namespace);
String value = config.getProperty(key, null);

根据 ConfigService 类我们来构造 ConfigUtils 工具类

@Slf4j
public final class ConfigUtils {

    private static final List<Config> configList;

    static {
        configList = new ArrayList<>();
        Stream.of("application.yml", "business.yml").forEach(namespace -> {
            Config config = ConfigService.getConfig(namespace);
            // 添加配置变化的日志记录 Listener
            config.addChangeListener(configChangeEvent -> {
                for (String key : configChangeEvent.changedKeys()) {
                    ConfigChange change = configChangeEvent.getChange(key);
                    log.info("Found config changed => " + change.toString());
                }
            });
            configList.add(config);
        });

    }

    public static String getProperty(String key, String defaultValue) {
        String value = System.getProperty(key, null);
        if (value != null) {
            return value;
        }
        for (Config config : configList) {
            value = config.getProperty(key, null);
            if (value != null) {
                return value;
            }
        }
        return defaultValue;
    }

    // 省略获取 Interget,Long,Array,Boolean,等格式的配置

    public Set<String> getPropertyNames() {
        Set<String> propertyNames = new HashSet<>();
        for (Config config : configList) {
            propertyNames.addAll(config.getPropertyNames());
        }
        return propertyNames;
    }
}

启动项目的时候将 Apollo 的服务器信息通过 VM 参数传递给 Java 客户端

-Denv=DEV
-Dapp.id=SampleApp
-Dapollo.meta=http://192.168.113.130:8080
-Dapollo.cluster=SampleCluster

其中 env 是环境列表的值, app.id 是项目名, appllo.meta 可以通过系统信息查询到, apollo.cluster 是集群的名称,如下面两张图

apollo-app-demo

apollo-system-info

现在可以在 IDEA 中配置上面的 VM 参数,编写测试类后,启动项目测试下

apollo-run-vm

简单的测试代码如下

@RestController
public class HelloController {

    @GetMapping("/key")
    public String getProperties(String key) {
        return "key -> " + ConfigUtils.getProperty(key, null);
    }

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/user")
    public User save() {
        User user = User.builder().name("ZUOJL-PC").build();
        user = userRepository.save(user);
        return user;
    }
}
@Entity
@Data
@Builder
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String name;
}
public interface UserRepository extends JpaRepository<User, Long> {
}

现在访问接口 http://localhost:8080/user 并在 Apollo 上改变数据库地址,看看数据库是否切换。

Spring 和 Apollo 的集成

在 Spring4.x 等使用 xml 配置的方式下,可以只需要简单的注入 namespace 就可以使用占位符 ${key:defaultValue} 进行调用。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:apollo="http://www.ctrip.com/schema/apollo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">
    <apollo:config namespaces="appication.yml,business.yml" />
    <bean class="">
        <property name="username" value="${spring.datasource.username:}"/>
        <property name="password" value="${spring.datasource.password:}"/>
    </bean>
</beans>