【问题标题】:runtime arguments needed for tests, spring boot, path to config测试所需的运行时参数、spring boot、配置路径
【发布时间】:2018-02-20 14:13:49
【问题描述】:

我用 Spring-boot 制作了一个小的 rest 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 文件运行时,它会吐出一堆空指针异常,因为 args[0] 为空,因为它实际上还没有得到参数。

我能以某种方式解决这个问题吗?就像先给它路径 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>

【问题讨论】:

  • 您应该在测试之前运行的特殊方法之一中实例化 configService。
  • 如果您在 bean 中使用 /resources/application.properties 和 Value 注释,一切都会变得简单得多。 docs.spring.io/spring/docs/current/javadoc-api/org/…
  • 你不应该使用new启动你的服务...
  • 是的,春天无处可去。不要使用new,而是context.getBean或类似的,它必须由Spring管理...
  • 我添加了我的答案,请检查;-)

标签: java spring spring-boot filepath


【解决方案1】:

你的动机和目标对我来说不是很清楚(你也没有提供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
    }

}

现在,正如您可能看到的,您将 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

如果我错过了您对 cme​​ts 或初始问题的一些要求,请告诉我,我可以添加其他信息。


如评论中所述。看来,您需要在 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
}

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

【讨论】:

  • 正确,但是如果你想根据值做一些逻辑,你不能在构造函数中这样做(它还没有设置)。没什么,只是记录,你可以删除它。刚开始可能会有所帮助,一旦您熟悉了一个概念,就可以将其删除。您需要所谓的选项参数,以便能够将其映射到 @Value。
  • 当然有。在 Java 构造函数中总是存在的(至少是没有参数的),但是 Spring 应该创建你的服务而不是你。 new 可以按照您的尝试在技术上使用,但它会破坏框架并且只会导致问题。您可以将 new 用于您的数据保存实例和其他非 Spring 内容。
  • 最大的问题是,你以后是否需要所有这些属性,也许只需要那个令牌时间,所以如上所示将那个传递给 Spring,其余的重新映射到正确的键(Spring 期望的) ,比如server.port 和server.ssl.enabled 等等......(在我看来,你不再需要这些值了)。
  • 这不是setSSL() 在ConfigService 中的作用吗?只有在 enableHttps 设置为 true 时才应该这样做,因为如果它设置了这 3 个属性,它将在 https 上启动。并且端口是在自定义容器上设置的。
  • 不要在SecurityService这样做,为什么?如果你想在 tomcat 开始之前用你的 ConfigService 替换我的ConfigProperties。它将是 POJO。 Spring 不需要知道你所有的类。如果您愿意,我向您展示了如何让 other 组件保存您的一些属性,但我仍然会更新代码...
【解决方案2】:

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

并且可以使用 System.getProperty("spring.profiles.active") 在程序中访问相同的内容。

【讨论】:

    【解决方案3】:

    据我所知,您希望拥有不同 bean(使用 SSL 和不使用 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...) 
      }
    }
    

    【讨论】:

    • 这也可以,但不能解决我的问题。此外 config.xml 必须是它的方式,不使用属性,所以我使用解组。它不仅包含 ssl 设置。这在我的代码中有效,它使用它应该使用的配置文件,这取决于 enableHttps 是否为真。所以我确定你的代码可以工作,它只是与我的问题无关,因为我没有实例化 configService 两次,一次是在主要的,在我启动服务器之前,然后 Spring 在启动服务器时再次执行它,所以我我猜可以将它自动装配到其他类中。
    猜你喜欢
    • 2022-07-22
    • 1970-01-01
    • 1970-01-01
    • 2016-02-22
    • 2017-12-25
    • 2015-03-21
    • 1970-01-01
    • 2021-02-16
    • 2012-08-13
    相关资源
    最近更新 更多