测试所需的运行时参数,spring 引导,配置路径

runtime arguments needed for tests, spring boot, path to config

我用 Spring-boot 做了一小段休息 api,我正在从配置文件中读取某些内容。因为我不能硬编码配置文件的路径,因为它在生产中改变了位置,所以我决定从 运行time 参数中获取它。我在实例化我的 ConfigService.Now 时使用了路径 问题是我的所有测试都失败了,因为它需要实例化 ConfigService,但是当 运行测试。 我的主图是这样的:

@SpringBootApplication
public class SecurityService {

  private static final Logger LOG = LogManager.getLogger(SecurityService.class);

  private static String[] savedArgs;
  public static String[] getArgs(){
    return savedArgs;
  }

  public static void main(String[] args) throws IOException {

    savedArgs = args;
    final String configPath = savedArgs[0];
    // final String configPath = "src/config.xml";
    ConfigService configService = new ConfigService(configPath);

    if (configService.getConfigurations().getEnableHttps()) {
      LOG.info("Using HTTPS on port {}", configService.getConfigurations().getPort());
      configService.setSSL();
    }

    SpringApplication.run(SecurityService.class, args);
  }
}

我在启动 Spring 应用程序之前加载配置,因为我需要在服务器启动之前设置 SSL 设置等。现在,当它 运行 是 SpringApplication 时,它会再次实例化所有 classes,包括 ConfigService。配置服务如下所示:

@Configuration
@Service
public class ConfigService {

  private static final Logger LOG = LogManager.getLogger(ConfigService.class);
  private static String[] args = SecurityService.getArgs();
  private static final String CONFIG_PATH = args[0];
  private Configurations configurations;

  public ConfigService() {
    this(CONFIG_PATH);
  }

  public ConfigService(String configPath) {
    configurations = setConfig(configPath);
  }

  // Reads config and assigns values to an object, configurations
  private Configurations setConfig(String configPath) {
    Configurations configurations = new Configurations();
    try {
      ApplicationContext appContext =
        new ClassPathXmlApplicationContext("applicationContext.xml");
      XMLConverter converter = (XMLConverter) appContext.getBean("XMLConverter");
      configurations = (Configurations) converter.convertFromXMLToObject(configPath);

    } catch (IOException e) {
      e.printStackTrace();
    }
    LOG.info("Loaded settings from config.xml");
    return configurations;
  }

  // Checks if EnableHttps is true in config.xml and then sets profile to secure and sets SSL settings
  public void setSSL() {
    System.setProperty("spring.profiles.active", "Secure");
    System.setProperty("server.ssl.key-password", configurations.getKeyPass());
    System.setProperty("server.ssl.key-store", configurations.getKeyStorePath());
  }

  // Spring profiles
  // If EnableHttps is false it uses the Default profile, also sets the port before starting tomcat
  @Component
  @Profile({"Default"})
  public class CustomContainer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
    @Override
    public void customize(ConfigurableServletWebServerFactory container) {
      container.setPort(configurations.getPort());
    }
  }

  // If EnableHttps is True it will use the "Secure" profile, also sets the port before starting tomcat
  @Component
  @Profile({"Secure"})
  public class SecureCustomContainer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
    @Override
    public void customize(ConfigurableServletWebServerFactory container) {
      container.setPort(configurations.getPort());
    }
  }

  public Configurations getConfigurations() {
    return configurations;
  }
}

当尝试 运行 作为 JAR 文件时,它会吐出一堆空指针异常,因为 args[0] 是空的,因为它实际上还没有得到参数。

我能以某种方式解决这个问题吗?比如先给它 src/config.xml 路径,然后在它真正开始时将它覆盖到 运行time args 路径?

我的一个测试 class 看起来像这样:

@RunWith(SpringRunner.class)
@SpringBootTest
public class SecurityServiceTests {

  @Test
  public void contextLoads() {
  }

  @Test
  public void testGetPort() {
    String configPath = "src/config.xml";
    ConfigService configService = new ConfigService(configPath);
    int actualPort = configService.getConfigurations().getPort();
    int expectedPort = 8443;
    assertEquals(expectedPort, actualPort);
  }
  @Test
  public void testGetTTL(){
    String configPath = "src/config.xml";
    ConfigService configService = new ConfigService(configPath);
    int actualTTL = configService.getConfigurations().getTTL();
    int expectedTTL = 15000;
    assertEquals(expectedTTL, actualTTL);
  }
  @Test
  public void testSSL(){
    String configPath = "src/config.xml";
    ConfigService configService = new ConfigService(configPath);
    String expectedKeyPass = "changeit";
    String expectedKeyStore = "classpath:ssl-server.jks";
    configService.setSSL();
    assertEquals(expectedKeyPass,System.getProperty("server.ssl.key-password"));
    assertEquals(expectedKeyStore,System.getProperty("server.ssl.key-store"));
  }

}

配置class:

// Model class that we map config.xml to
@Component
public class Configurations {
  private int port;
  private boolean enableHttps;
  private String keyStorePath;
  private String keyPass;
  private int TokenTtlMillis;

  public int getPort() {
    return port;
  }

  public void setPort(int port) {
    this.port = port;
  }

  public boolean getEnableHttps() {
    return enableHttps;
  }

  public void setEnableHttps(boolean enableHttps) {
    this.enableHttps = enableHttps;
  }

  public String getKeyStorePath() {
    return keyStorePath;
  }

  public void setKeyStorePath(String keyStorePath) {
    this.keyStorePath = keyStorePath;
  }

  public String getKeyPass() {
    return keyPass;
  }

  public void setKeyPass(String keyPass) {
    this.keyPass = keyPass;
  }

  public int getTTL() {
    return TokenTtlMillis;
  }

  public void setTTL(int TTL) {
    this.TokenTtlMillis = TTL;
  }
}

我的 config.xml 映射到配置 class:

<?xml version="1.0" encoding="UTF-8"?>
<Configurations>
  <Port>8443</Port>
  <EnableHttps>true</EnableHttps>
  <KeyStorePath>classpath:ssl-server.jks</KeyStorePath>
  <KeyPass>changeit</KeyPass>
  <TokenTtlMillis>15000</TokenTtlMillis>
</Configurations>

您可以使用 VM 参数代替 SSL 的命令行参数 属性。 即 -Dspring.profiles.active=安全

并且可以使用 System.getProperty("spring.profiles.active").

在程序中访问相同的内容

据我了解,您希望拥有不同 bean(使用和不使用 SSL)的任务取决于 属性(例如 enable.https)。

对于这种情况,您可以进行条件接线。例如:

@Component
@ConditionalOnProperty(value = "enable.https", havingValue = "false")
public class CustomContainer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

  @Value("${port}")
  private int port;

  @Override
  public void customize(ConfigurableServletWebServerFactory container) {
    container.setPort(port);
  }
}


@Component
@ConditionalOnProperty(value = "enable.https", havingValue = "true")
public class SecureCustomContainer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

   @Value("${port}")
   private int port;    

   @Value("${ssl.key.store}")
   private String sslKeyStore;
   .....

   @Override
   public void customize(ConfigurableServletWebServerFactory container) {
      container.setPort(port);
       container.setSsl(... configure ssl here...) 
  }
}

我不太清楚你的动机和目标(你也没有提供MCVE),但我想告诉你,我认为是正确的Spring方式...

我将从头开始,先给你展示结果,然后再展示如何实现...

你想要这样基于参数的逻辑,让Spring为你注入参数的值

package com.example.args;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

@Configuration
@Service
public class ConfigService implements InitializingBean {

    private Configurations configurations;

    @Value("${configPath:defaultPath}") // "defaultPath" is used if not specified as arg from command line
    private String configPath;

    // you can use also @PostConstruct and not interface, up to you
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("configPath: " + configPath);
        configurations = setConfig(configPath); // you original setConfig
    }

}

现在,正如您可能看到的那样,您将参数传递给 SpringApplication.run(SecurityService.class, args);,因此它可用于 Spring,在 SecurityService 中您不需要代码,就这么简单

package com.example.args;

import java.util.Set;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecurityService 
        implements ApplicationRunner { // not really needed, just for logging in run

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

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // for logging only
        System.out.println("NonOptionArgs: " + args.getNonOptionArgs());
        Set<String> optionNames = args.getOptionNames();
        System.out.println("OptionArgs: " + optionNames);
        for (String optionName : optionNames) {
            System.out.println(optionName + ": " + args.getOptionValues(optionName));
        }
    }
}

您可以删除 ApplicationRunner 的实现,它只是为了能够覆盖 run() 并且它仅用于记录...

最后你必须运行申请为:

java -jar target\args-0.0.1-SNAPSHOT.jar --configPath=somePath

如果我在评论或最初的问题中遗漏了您的一些要求,请告诉我,我可以添加其他信息。


如评论中所述。看来,您需要在 Spring 开始之前阅读我所说的 non-standard 中的属性。我没有对此进行测试,但这应该是如何做到这一点的方向

public static void main(String[] args) {
    HashMap<String, Object> props = new HashMap<>();

    ConfigProperties cp = ... // some magic to load, cp is not Spring bean
    // server.port and server.ssl.enabled are properties Spring is aware of - will use it
    props.put("server.port", cp.getPort());
    props.put("server.ssl.enabled", cp.isSslEnabled()); // where you read properties from config.xml
    props.put("custom", cp.getCustom());

    ConfigurableApplicationContext context = new SpringApplicationBuilder()
        .sources(SecurityService.class)                
        .properties(props)
        .run(args);

    SecurityService ss = context.getBean(SecurityService.class);
    // configuration is not Spring managed, so there is not @Autowired in SecurityService 
    ss.setConfiguration(configuration); // problem is, that while it is not Spring bean, you cannot reference it from multiple services
}

如果你想在配置 class 中重用,你应该可以这样做,但是让 Spring 以我为 configPath

显示的方式注入它
Configuration {
    @Value("${custom}") String custom;
    // holding value for serverPort is not needed in application, so it's not here
}

您必须使用预期的 属性 键。