背景:

有些时候, 由于机器原因或者是环境原因, 我们希望将微服务架构离散的可运行组件打包到一起运行. 也就是说 ,一个进程运中行多个容器或者运行多个Application.

由于组件间的调用都是rpc调用. 那么怎样在不动组件代码的情况下, 且保证进程中的多容器中的组件功能完全隔离呢?

apollo assembly:

我们看一下初始示例, 来源于携程配置中心 apollo assembly, 先看一下apollo的工程依赖:
在这里插入图片描述

assembly(装配)

只有一个类,就是一个main函数,同时启动了common、configservice、adminservice、portal组件。

buildtools(构建工具)

只有些脚本工具,和一些规范

demo

apollo的一些使用方法和示例

assembly 源码(改造后):

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
    HibernateJpaAutoConfiguration.class})
public class ApolloApplication {

  private static final Logger logger = LoggerFactory.getLogger(ApolloApplication.class);

  public static void main(String[] args) throws Exception {
    /**
     * Common
     */
    ConfigurableApplicationContext commonContext =
        new SpringApplicationBuilder(ApolloApplication.class).web(WebApplicationType.NONE).run(args);
    logger.info(commonContext.getId() + " isActive: " + commonContext.isActive());

    /**
     * ConfigService
     */
    if (commonContext.getEnvironment().containsProperty("configservice")) {
      ConfigurableApplicationContext configContext =
          new SpringApplicationBuilder(ConfigServiceApplication.class).parent(commonContext)
              .sources(RefreshScope.class).run(args);
      logger.info(configContext.getId() + " isActive: " + configContext.isActive());
    }

    /**
     * AdminService
     */
    if (commonContext.getEnvironment().containsProperty("adminservice")) {
      ConfigurableApplicationContext adminContext =
          new SpringApplicationBuilder(AdminServiceApplication.class).parent(commonContext)
              .sources(RefreshScope.class).run(args);
      logger.info(adminContext.getId() + " isActive: " + adminContext.isActive());
    }

    /**
     * Portal
     */
    ConfigurableEnvironment environment = commonContext.getEnvironment();
    String name = "Config resource 'class path resource [application-github.properties]' via location 'optional:classpath:/'";
    OriginTrackedMapPropertySource propertySource = (OriginTrackedMapPropertySource) environment.getPropertySources().get(name);
    Map<String, Object> source = propertySource.getSource();
    Map map = new HashMap();
    map.putAll(source);
    map.put("spring.datasource.url", "jdbc:mysql://127.0.0.1:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true");
    environment.getPropertySources().replace(name, new OriginTrackedMapPropertySource(name, map));
    // 一些配置bean在父上下文 commonContext 就加载了, 如 DataSourceProperties 的配置, 死活在 portalContext 中更新profiles配置无效, 要启动 PortalApplication, 只能改bean的属性值,若希望通过设置配置改变值必须 publishEvent 刷新bean
    commonContext.publishEvent(new EnvironmentChangeEvent(new HashSet<String>(){{add("spring.datasource.url");}}));
    logger.info(commonContext.getBean(DataSourceProperties.class).getUrl());

    // 一些配置bean在父上下文 commonContext 就加载了, 如 DataSourceProperties 的配置, 死活在 portalContext 中更新profiles配置无效, 要启动 PortalApplication, 只能改bean的属性值,
//    commonContext.getBean(DataSourceProperties.class).setUrl("jdbc:mysql://127.0.0.1:3306/ApolloPortalDB?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true");
    if (commonContext.getEnvironment().containsProperty("portal")) {
      ConfigurableApplicationContext portalContext =
          new SpringApplicationBuilder(PortalApplication.class).parent(commonContext).profiles("portal")
                  //.profiles("portal")
              .sources(RefreshScope.class).run(args);
      logger.info(portalContext.getId() + " isActive: " + portalContext.isActive());
    }
  }
}  

注意: 代码改造前, portal 不能同时 和 configservice、adminservice运行. 原因是 portal 的数据源配置不能被正确加载.

运行:

在这里插入图片描述

springboot assembly:

基于 apollo assembly 示例, 验证配置的加载和优先级关系.

A1源码:

A1Application.java

package com.test.a1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.PropertySource;

@EnableDiscoveryClient
@EnableAspectJAutoProxy
@Configuration
@PropertySource(value = {"classpath:a1.properties"})
@EnableAutoConfiguration
@ComponentScan(basePackageClasses = {A1Application.class})
public class A1Application {

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

}

TestController.java

package com.test.a1;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;

@Configuration
@RestController
@Slf4j
public class TestController {

    @Value("${public.p1}")
    private String publicA1;

    @Value("${public.p1}")
    private String publicA2;

    @Value("${private.a1}")
    private String privateA1;

    @Value("${private.a2}")
    private String privateA2;

    @Value("${spring.application.name}")
    private String appName;

    @GetMapping("/a1")
    public String a1() {
        return "a1";
    }

    @Autowired
    private ConfigurableApplicationContext configurableApplicationContext;

    @PostConstruct
    public void log() {
        log.info("application-a1, publicA1: {}, publicA2: {}, privateA1: {}, privateA2: {}, appName: {}, configurableApplicationContext: {}",
                publicA1, publicA2, privateA1, privateA2, appName, configurableApplicationContext);
    }

}

A2源码:

A2Application.java

package com.test.a2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.PropertySource;

@EnableDiscoveryClient
@EnableAspectJAutoProxy
@Configuration
@PropertySource(value = {"classpath:a2.properties"})
@EnableAutoConfiguration
@ComponentScan(basePackageClasses = {A2Application.class})
public class A2Application {

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

}

TestController.java

package com.test.a2;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;

@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@RestController
@Slf4j
public class TestController {

    @Value("${public.p1}")
    private String publicA1;

    @Value("${public.p1}")
    private String publicA2;

    @Value("${private.a3:null}")
    private String privateA3;

    @Value("${private.a4}")
    private String privateA4;

    @Value("${spring.application.name}")
    private String appName;

    @GetMapping("/a2")
    public String a1() {
        return "a2";
    }

    @Autowired
    private ConfigurableApplicationContext configurableApplicationContext;

    @PostConstruct
    public void log() {
        log.info("application-a1, publicA1: {}, publicA2: {}, privateA3: {}, privateA4: {}, appName: {}, configurableApplicationContext: {}",
                publicA1, publicA2, privateA3, privateA4, appName, configurableApplicationContext);
    }
}

Assembly源码:

AssemblyApplication.java

package com.test.assembly;

import com.test.a1.A1Application;
import com.test.a2.A2Application;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.context.ConfigurableApplicationContext;

@Slf4j
@SpringBootApplication(scanBasePackages = "com.test.assembly")
public class AssemblyApplication {

    public static void main(String[] args) {
        System.setProperty("spring.cloud.service-registry.auto-registration.enabled", "false");
        final ConfigurableApplicationContext commonContext =
                new SpringApplicationBuilder(AssemblyApplication.class).web(WebApplicationType.NONE).run(args);
        log.info(commonContext.getId() + " isActive: " + commonContext.isActive());
        log.info(commonContext.getId() + " env: " + commonContext.getEnvironment().toString());

        System.setProperty("spring.cloud.service-registry.auto-registration.enabled", "true");
//        System.setProperty("spring.cloud.consul.discovery.instance-id", "${spring.application.name}-${spring.cloud.client.ip-address}-${server.port}");

        // a1
        if (commonContext.getEnvironment().containsProperty("a1")) {
            final ConfigurableApplicationContext context =
                    new SpringApplicationBuilder(A1Application.class)
                            .parent(commonContext)
                            .properties("server.port=9060")
                            .properties("spring.application.name=a1-ms")
                            .properties("spring.cloud.consul.discovery.instance-id=a1")
//                            .properties("spring.cloud.consul.service-registry.auto-registration.enabled=true")
                            .sources(RefreshScope.class)
                            .run(args);
            log.info(context.getId() + " isActive: " + context.isActive());
            log.info(commonContext.getId() + " env: " + context.getEnvironment().toString());
        }

        // a2
        if (commonContext.getEnvironment().containsProperty("a2")) {
            final ConfigurableApplicationContext context =
                    new SpringApplicationBuilder(A2Application.class)
                            .parent(commonContext)
                            .properties("server.port=9070")
                            .properties("spring.application.name=a2-ms")
                            .properties("spring.cloud.consul.discovery.instance-id=a2")
                            .sources(RefreshScope.class)
                            .run(args);
            log.info(context.getId() + " isActive: " + context.isActive());
            log.info(commonContext.getId() + " env: " + context.getEnvironment().toString());
        }
    }
}

运行

在这里插入图片描述

配置和依赖图:

在这里插入图片描述
其中 application-additional.properties 是通过 SpringApplicationBuilder.profiles(“additional”) 动态加入的配置

总结:
  • 1、同名配置文件先加载上层配置, 即 application.yaml 和 application-sit.properties 只会加载assembly的. 尽管每个子context都会重新初始化 Environment, 依然只能加载到上层同名配置文件.
  • 2、同名配置项, profiles 定义的配置优先于默认的配置[即application.yaml] , 优先于 @PropertySource中配置
  • 3、通过 SpringApplicationBuilder.properties() 设置的配置, 属于默认配置. 优先级低.
  • 4、通过 SpringApplicationBuilder.profiles(“portal”)加载到配置, 优先级最高. 见apollo assembly源码改造.
  • 5、此外 子context前, 还可以通过 System.setProperty() 设置配置.
  • 6、一些配置bean在父 Context 就加载到env中了, 如 DataSourceProperties 配置, 若希望通过设置配置改变值必须 publishEvent 刷新bean, 否则只能通过getBean()去改bean的属性值. 或者最外层排除一切与DataSourceProperties相关的自动装备类: DataSourceAutoConfiguration、DataSourceTransactionManagerAutoConfiguration. 换句话说, 子context 装配bean时, 先到父 context 上找, 没有再注册bean. 因此 A1 和 A2模块注意定义不同的包路径, 在装配bean时, 通过扫描不同包路径隔离.
  • @PropertySource的优先级最低

以上总结并非100%正确. 有兴趣的同学可以先阅读spring boot 启动源码, 并且自行测试. 本例子代码见: GITHUB

附: 配置优秀级顺序
在这里插入图片描述
附: 启动日志
在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐