1

I have made a small rest api with Spring-boot and i am reading certain things from a config file. Since i cant hardcode the path to the config file, since it changes place in production, i decided to get it from runtime arguments. I use the path when instantiating my ConfigService.Now the problem is that all my tests are failing because it needs to instantiate the ConfigService, but it hasnt reached the main (where it gets the path) when running the tests. My main looks like this:

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

I load the config before starting the Spring application because i need to set SSL settings etc, before the server starts. Now when it runs the SpringApplication it instantiates all the classes, including ConfigService again. The ConfigService looks like this:

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

When trying to run as JAR file it spits a bunch of nullpointerexceptions because args[0] is null because it hasnt actually gotten the arguments yet.

Can i somehow work around this? Like giving it the path src/config.xml first, and then overwriting it to the runtime args path later, when it actually starts?

One of my test classes look like this:

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

}

Configurations 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;
  }
}

And my config.xml that is mapped to the configurations 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>
Betlista
  • 10,327
  • 13
  • 69
  • 110
Mike Nor
  • 227
  • 1
  • 9
  • 18

3 Answers3

1

Your motivation and goal is not very clear to me (you didn't provide MCVE also), but I'd like to show you, what I believe is a proper Spring way...

I'll start from the end, for you to show you results first and then how to achieve that...

You want to have so logic based on parameter, let the Spring inject the value of the parameter for you

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
    }

}

and now, as you maybe saw, you pass args to SpringApplication.run(SecurityService.class, args);, so it is available to Spring, in SecurityService you do not need you code, just this simple

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

You can delete implementation of ApplicationRunner it's there just to be able override run() and it's used for logging only...

At the end you have to run the application as:

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

Let me know if I missed some of your requirements from comments or initial question, I can add additional info.


As discussed in comment. It seems, you need to read the properties in I'd say non-standard way before Spring starts. I didn't test that, but this should be the direction how to do that

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
}

if you want to reuse in Configuration class, you should be able to do so, but let the Spring to inject it same way I shown for configPath

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

you have to use expected property keys.

Betlista
  • 10,327
  • 13
  • 69
  • 110
  • Okay so the @value automatically overwrites defaultpath if it finds runtime arguments? And can you explain what the publi void run(ApplicationArguments args) does? besides print stuff :p – Mike Nor Feb 21 '18 at 08:41
  • 1
    Correct, but if you want to do some logic based on value you cannot do that in constructor (it's not set yet). Nothing, just logging, you can remove it. It might be helpful in the beggining, once you are familiar with a concept, you can delete it. You need so called option argument, to be able to map it in `@Value`. – Betlista Feb 21 '18 at 08:43
  • Okay. But correct me if im wrong. I dont have a constructor now? – Mike Nor Feb 21 '18 at 08:44
  • 1
    Of course you have. In Java constructor is always there (at least the one without parameters), but Spring should be creating your services not you. `new` can be used technically as you tried, but it breaks the framework and is causing problems only. You can use `new` for your data holding instances and other non-Spring stuff. – Betlista Feb 21 '18 at 08:47
  • Okay so i tested al this now, and it looks alot better, but it doesnt actually load the configservice before starting the main, meaning it doesnt set the ssl settings and profiles and other stuff. I need some getBean before the main i think? i need the if(configService.getConfigurations().getEnableHttps()){ set ssl } somehow. – Mike Nor Feb 21 '18 at 08:57
  • Not really, but I got your point. All Spring components are created on line `SpringApplication.run(SecurityService.class, args);`, but I got your point. – Betlista Feb 21 '18 at 08:59
  • It seems to me, that reading properties (logic from `setConfig`) and the run your application would be a solution for you, check that code https://stackoverflow.com/a/36067066/384674 – Betlista Feb 21 '18 at 09:04
  • I started out with it that way, but because of the way my xml file is build, it has to be done this way. So i basically need the setConfig() to be run before my main, or it wont start on https : – Mike Nor Feb 21 '18 at 09:09
  • I added additional info to my answer, please try. Your `Configuration` class was not in a question, that's why I just added an idea... – Betlista Feb 21 '18 at 09:12
  • The Configurations class is just a simple class with fields and gettters/setters that is used for mapping the config.xml to. So i think i want to do props.put("enable.ssl", configService.getConfigurations.getEnableHttps()); But how will i be able to use the configService? and i need to make sure the configService.setConfig is called first, or the getConfigurations will be null, since the setConfig is the one mapping the xml to the class – Mike Nor Feb 21 '18 at 09:26
  • Read data for Configuration separately (out of Spring, before it is started), the question is if you need some of the properties in Spring components too... – Betlista Feb 21 '18 at 09:29
  • I added the idea, how to use those properties in Spring if needed... You will prepare properties on your own basically... I'm not sure if you are using standard application.properties or not... So I didn't test, that half of the values will be loaded from property file, which should be possible too... – Betlista Feb 21 '18 at 09:36
  • I added the configurations class and the config.xml to the question up top. The way im using the properties makes this harder, but it has to be done like this im afraid. – Mike Nor Feb 21 '18 at 09:52
  • 1
    The biggest question is, whether you need all those properties later on, maybe that token time only, so pass that one to Spring as I shown above, the rest remap to proper keys (that Spring expects), like `server.port` and `server.ssl.enabled` and so on... (after that you do not need the values anymore it seems to me). – Betlista Feb 21 '18 at 09:58
  • 1
    Isnt that what setSSL() does in the ConfigService? And it should only do that if enableHttps is set to true, because if it sets those 3 properties, it will start on https. And port is set on the customcontainers. – Mike Nor Feb 21 '18 at 10:04
  • i think what i want is something like: ConfigService configService; configService.setConfig(); // to map configurations if(configService.getConfigurations.getEnableHttps()){ configService.setSSL();} SpringApplication.run(SeurityService.class, args); Except that i cant actually load COnfigService like that – Mike Nor Feb 21 '18 at 10:08
  • I put all these things into the afterPropertiesSet(). So everything appears to work now, except it doesnt :p sprinkled some log statements around and it says it uses the right port and the secure profile, but it doesnt actually do any of those things. I think the server starts before i actually do those things. Is there a way to restart the server after the SpringApplication.run in the main, so it starts on default, loads my settings and then restarts with those? @Betlista – Mike Nor Feb 21 '18 at 10:54
  • I didn't check with Tomcat, but `afterPropertiesSet()` seems to be to late. I'm not sure what you meant by "Except that i cant actually load ConfigService like"... – Betlista Feb 21 '18 at 11:12
  • When I replaced `props.put("custom", cp.getCustom());` with `System.setProperty("custom", cp.getCustom());` it works the same way... So you can reuse the code you have, just do not expect your ConfigService be loaded by Spring, it's too late. – Betlista Feb 21 '18 at 11:15
  • Erh i mean that ConfigService configService; doesnt initialise the object properly. Everything is null if i do it like that in the SecurityService. It would be nice to make a constructor of ConfigService, that called setConfig and setSSL, and then call that constructor in the main before it starts the server. – Mike Nor Feb 21 '18 at 11:25
  • 1
    Don't do it in `SecurityService`, why? If you want it before tomcat start replace my `ConfigProperties` with your ConfigService. It will be POJO. Spring doesn't need to be aware of all your classes. I shown you how to have **other** component holding some of your properties if you want to, but still I'll update the code... – Betlista Feb 21 '18 at 11:30
  • 1
    Jesus christ okay that does exactly what i want i think... Thank you so much for putting up with me. I put ConfigService configService = new ConfigService(); right before SpringApplication.run and now it actually starts the server on the specified port and ssl settings. Its not pretty Spring, but it works. – Mike Nor Feb 21 '18 at 11:37
  • As I stated before, make a decision - do you need those values in Spring bean or not? If not, set it up (using `System.setProperty()` is ok too) and forget that you ever had Configuration instance. If you do need it elsewhere, use **different** class (this time Spring bean) to hold the values as I shown. I'm happy we finally made it working for you ;-) You were so close all the time... – Betlista Feb 21 '18 at 11:38
0

You can use VM argument instead of command line argument for SSL property. i.e. -Dspring.profiles.active=Secure

and can access the same within program using System.getProperty("spring.profiles.active").

0

As I can understand the task you would like to have different beans (with SSL and without) depends on property (enable.https for example).

For this case you can you conditional wiring. For example:

@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...) 
  }
}
Vlad Bochenin
  • 3,007
  • 1
  • 20
  • 33
  • This would work aswell, but not fix my probem. Also the config.xml has to be the way it is, not using properties, so im using unmarshalling for that. And it consists of more than just ssl settings. This works in my code, it uses the profile it should, depending on wether or not enableHttps is true. So im sure your code would work, its just not really relevant to my problem, being that i am not instantiating configService twice, once in the main, before i start the server, and then Spring does it again while starting the server, so i can autowire it into other classes i guess. – Mike Nor Feb 21 '18 at 07:16