测试所需的运行时参数,弹簧启动,配置路径

时间:2018-02-20 14:13:49

标签: java spring spring-boot filepath

我用Spring-boot做了一个小的休息api,我正在从配置文件中读取某些东西。由于我无法硬编码配置文件的路径,因为它改变了生产中的位置,我决定从运行时参数中获取它。我在实例化我的ConfigService时使用了路径。现在的问题是我的所有测试都失败了,因为它需要实例化ConfigService,但是在运行测试时它没有到达main(它获取路径)。 我的主要看起来像这样:

@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时,它会再次实例化所有类,包括ConfigService。 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文件运行时,它会吐出一堆nullpointerexceptions,因为args [0]为null,因为它实际上还没有获得参数。

我可以以某种方式解决这个问题吗?比如首先给它路径src / config.xml,然后在它实际启动时将它覆盖到运行时args路径?

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

@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"));
  }

}

配置类:

// 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映射到配置类:

<?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>

3 个答案:

答案 0 :(得分:1)

你的动机和目标对我来说不是很清楚(你也没有提供MCVE),但我想告诉你,我相信春天是正确的......

我将从最后开始,首先向您展示结果然后如何实现这一目标......

你想拥有基于参数的逻辑,让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
    }

}

现在,正如您所看到的,您将args传递给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开始之前以非标准的方式阅读属性。我没有测试过,但这应该是如何做到的方向

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
}

如果你想在Configuration类中重用,你应该能够这样做,但让Spring以我为configPath显示的方式注入它

Configuration {
    @Value("${custom}") String custom;
    // holding value for serverPort is not needed in application, so it's not here
}

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

答案 1 :(得分:0)

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

并且可以使用System.getProperty(“spring.profiles.active”)在程序中访问它。

答案 2 :(得分:0)

我可以理解您希望拥有不同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...) 
  }
}