I have a few classes that need some environment specific configuration. I turned to using properties files, which are loaded in the constructor of the class.
public class MyClass {
public MyClass() {
try {
ValidatedEnvironmentProperties props = new ValidatedEnvironmentProperties();
props.load(MyClass.class.getResourceAsStream("/myclass.properties"));
ValidatedEnvironmentProperties extends Properties. Basically, it uses a Java System Property to set a key prefix. I set the System property to, say, production, and in the properties files, I have staging.url=... and production.url=.... This allows me build/runtime selection of which configuration is used while not needing to change property file name.
myclass.properties is stored in src/main/resources.
That works fine, and I rather like the how it works. My problem is that I'm sort of stuck with TestNG. I want to test a bunch of other properties in TestNG unit tests. This led me to create src/test/resources/myclass.properties. Instead of "environment" keys, I use test names like bad_url_test.url=this_ain't_a_url.
I was thinking that src/test/resources would get a higher priority in the classpath/classloader (terminology?), causing the test-specific properties to load. Then, for my various tests, I just set the Java System property to bad_url_test, instantiate MyClass, test my assertions, and then set the System property to a new test, instantiate a new object, and repeat.
I believe the source of the problem is this line in MyClass's constructor:
props.load(MyClass.class.getResourceAsStream("/myclass.properties"));
In MyClassTest, I put these lines to try to understand what's happening:
File f = new File(MyClass.class.getProtectionDomain().getCodeSource().getLocation().getPath());
System.out.println("MyClass classpath: " + f.toString());
f = new File(MyClassTest.class.getProtectionDomain().getCodeSource().getLocation().getPath());
System.out.println("MyClassTest classpath: " + f.toString());
Output is:
MyClass classpath: /home/fandingo/code/project/build/classes/main
MyClassTest classpath: /home/fandingo/code/project/build/classes/test
MyClassTest is correct, but I need something accessible within MyClass's constructor that will automatically prefer /src/test/resources/ when running tests but /src/main/resources/ when running normally.
You need inversion of control. i.e. MyClass needs to be told where to get its properties from and not decide on its own.
There are lots of ways to do this but they all come down to the same idea: MyClass should not know at compile time where its properties come from.
e.g.
In MyClass.java
ValidatedEnvironmentProperties props = new ValidatedEnvironmentProperties();
String resourceSupplierClassName = System.getProperty("resource-supplier-class-name",
MyClass.class.getName());
Class<?> resourceSupplierClass = Class.forName(resourceSupplierClassName);
props.load(resourceSupplierClass.getResourceAsStream("/myclass.properties"));
In MyClassTest.java
System.setProperty("resource-supplier-class-name", MyClassTest.class.getName());
Again, there are many ways to do this. Instead of passing around system properties, etc. you can also use dependency injection (a form of inversion of control). You can change the MyClass constructor to take your props as an argument and then put the onus on the instantiators of MyClass to provide the props or you can use a dependency injection framework such as Spring or Guice to manage creating the appropriate props instance for main/test execution and provide it to objects that need it as needed.
Related
We have a spring boot application with configuration being driven from application.yml file. In this configuration file we use the feature of defining a property by referring to another property inside the same application.yml file:
my-games-app:
base-property: foo
games:
- game-one:
game-name: ${my-games-app.base-property}one
game-location: ${my-games-app.base-property}/one
- game-two:
game-name: ${my-games-app.base-property}two
game-location: ${my-games-app.base-property}/two
And we have a #ConfigurationProperties bean loading games configuration:
#Configuration
#ConfigurationProperties(prefix = "my-games-app.games")
public class GamesConfig {
private Map<String, Game> games;
...
}
Useless to say the above is just an example, in reality it is a very complex setup with GamesConfig bean being used as a constructor argument for many other beans inside our application:
#Component
public class GamesRunner {
private final GamesConfig gamesConfig;
...
}
Everything works as expected. The problem we have is related to testing the beans where GamesConfig is injected; in the above example GamesRunner. At the moment we use #SpringBootTest to get hold of the beans we want to test. This again, works OK but the main inconvenient is that the whole application needs to be started in order to access the GamesConfig bean. This means setting up a lot of infrastructure such as a Database a JMS message broker and a Kafka broker. This takes time and makes our CI builds longer to run which started to become a bit of an inconvenient. Because the beans we want to test don't need any other setup than having the GamesConfig constructor argument provided we would prefer to have unit tests in place rather than integration tests as they are much faster to run.
In other words, we want to be able to recreate GamesConfig by hand by parsing our application.yml with a test helper method. To do this we use snakeyaml library:
public final class TestHelper {
public static GamesConfig getGamesConfig() {
var yaml = new Yaml();
var applicationYaml = (Map<String, Object>) yaml.load(readResourceAsString("application.yml");
return createGamesConfig(applicationYaml.get("games");
}
private static GamesConfig createGamesConfig(Object config) {
// The config Object passed here is a `Map<String, Map<String, String>>`
// as defeined in our `games` entry in our `application.yml`.
// The issue is that game name and game locations are loaded exactly like
// configured without property place holders being resolved
return gamesConfig;
}
}
We resolved the issue by manually parsing the property placeholders and looking up their values in the application.yml file. Even if our own property placeholder implementation is quite generic, my feeling is that this extra work is not needed as it should be a basic expectation the library would have some specific set up to do this out of the box. Being very new to snakeyaml I hope someone else hit the same problem and knows how to do it.
We use snakeyaml because it just happened to be in the class path as a transitive dependency, we are open to any suggestions that would achieve the same thing.
Thank you in advance.
To my knowledge, SnakeYAML only supports substitution of environment variables, which is why what you want is not possible as far as I know. What you can do instead, of course, is simply use Spring's classes without setting up a full ApplicationContext.
For example, assuming your game config from above, you could use:
final var loader = new YamlPropertySourceLoader();
final var sources = loader.load(
"games-config.yml",
new ClassPathResource("games-config.yml")
);
final var mutablePropertySources = new MutablePropertySources();
sources.forEach(mutablePropertySources::addFirst);
final var resolver = new PropertySourcesPropertyResolver(mutablePropertySources);
resolver.setIgnoreUnresolvableNestedPlaceholders(true);
System.out.println(resolver.getProperty("my-games-app.games[0].game-one.game-name"));
System.out.println(resolver.getProperty("my-games-app.games[0].game-one.game-location"));
System.out.println(resolver.getProperty("my-games-app.games[1].game-two.game-name"));
System.out.println(resolver.getProperty("my-games-app.games[1].game-two.game-location"));
which outputs:
fooone
foo/one
footwo
foo/two
If you are actually interested in how Spring does it, a good starting point is the source code of the PropertySourcesPlaceholderConfigurer class.
I am trying to do one example with ArchUnit where passing the AnalyzeClasses can be dynamic based on for which Adapter Application the test need run.
For Example:
#AnalyzeClasses(packages = "${archtest.scan.package}", importOptions = { ImportOption.DoNotIncludeTests.class, ImportOption.DoNotIncludeJars.class })
public class ArchitectureTests {
}
And from application.properties file it should allow to pass the packages to analyze dynamically, so any application using this Application as Jar library can provide the scan classes in its properties file. As below.
archtest.scan.package=com.example.pkgname
I am not sure what is the right way to pick up the dynamic value from property and pass that into #AnalyzeClasses Annotation. I am looking for some help or any example in this regard.
I don't think that ArchUnit's JUnit 4 & 5 support – in the current version 0.23.1 – allows for dynamic packages configured via an application.properties.
But instead of using #AnalyzeClasses, you can always just invoke new ClassFileImporter().import… and pass any dynamic runtime values you like.
(Note that ArchUnit's JUnit support also introduces a clever cache to reuse imported JavaClasses by multiple #ArchTests, but storing JavaClasses in a static field may be also good enough.)
This actually should be possible using a custom LocationProvider within #AnalyzeClasses. E.g.
#AnalyzeClasses(locations = ApplicationPropertiesLocationProvider.class)
public class ExampleTest {
// ...
}
class ApplicationPropertiesLocationProvider implements LocationProvider {
#Override
public Set<Location> get(Class<?> testClass) {
String packageToScan = readFromApplicationProperties();
return Locations.ofPackage(packageToScan);
}
}
But be aware of caching limitations! The caching mechanism assumes that your LocationProvider is "idempotent", i.e. it always returns the same locations. The caching mechanism will only take the type of the LocationProvider into consideration as cache key. This should not be a problem for a static application.properties as source though.
I would like to have properties, that I can reference via #Value in spring beans, that can only be created dependend on other properties.
In particular I am having a property, that describes the file system location of a directory.
myDir=/path/to/mydir
And by convention, there is a file in that directory, that is always called myfile.txt.
Now i want to have access to both, the directory and the file, via #Value annotations inside my beans. And sometimes I want to access them as Strings, sometimes as java.io.Files and sometimes as org.springframework.core.io.FileSystemResource (which by the way works very well out of the box!). But because of that concatenating Strings on demand is not an option.
So what I of course could do is just declare both, but I would end up with
myDir=/path/to/mydir
myFile/path/to/mydir/myfile.txt
and I would like to avoid that.
So I came up with an #Configuration class, that takes the property and adds it as new PropertySource:
#Autowired
private ConfigurableEnvironment environment;
#Value("${myDir}")
private void addCompleteFilenameAsProperty(Path myDir) {
Path absoluteFilePath = myDir.resolve("myfile.txt");
Map<String, Object> props = new HashMap<>();
props.put("myFile, absoluteFilePath.toString());
environment.getPropertySources().addFirst(new MapPropertySource("additional", props));
}
As you can see, in my context I even created a PropertyEditor, that can convert to java.nio.file.Paths.
Now the problem is, that for some reason, this "works on my machine" (in my IDE), but does not run on the intended target environment. There I get
java.lang.IllegalArgumentException: Could not resolve placeholder 'myFile' in string value "${myFile}"
Spring can combine properties
myDir=/path/to/mydir
myFile=${myDir}/myfile.txt
You can also use a default value without defining your myFile in the properties at first:
Properties file
myDir=/path/to/mydir
In class:
#Value("#{myFile:${myDir}/myfile.txt}")
private String myFileName;
Spring expressions can be used to refer the properties.
In my example it was
query-parm=QueryParam1=
query-value=MyParamaterValue
Now while binding them in Spring Bean.
#Configuration
public class MyConfig {
#Value("${query-param}${query-value}")
private String queryString;
}
Above code will inject QueryParam1=MyParamaterValue to the variable queryString.
I want to use Groovy scripts to utilize some java classes that are have spring annotations along the lines of the following:
#Component
class SomeUtility {
#Value("${foo}")
public string String fooValue;
}
Ideally I would like to configure foo in an application.properties file in the same manner as done in spring-boot
I have tried adding spring to the classpath and running something like this in a groovy script:
def ctx = new GenericApplicationContext()
new ClassPathBeanDefinitionScanner(ctx).scan('dylan')
ctx.refresh()
def b = ctx.getBean(SomeUtility)
println b.fooValue
but the output is ${foo} whether a properties file is in place or not - so I guess the #Value value processing is not happening.
I have also tried adding spring-boot to the classpath and running the above script - but I suspect I am not actually triggering spring-boot in that case.
Is there any way that I can do this - what I want is essentially to be able to configure the values easily then get hold of the components in a groovy script.
Since you are using Groovy, you need to do single quote #Value('${foo}'), as double quotes will be picked up by Groovy as a GString before Spring has a chance to look at its own context.
Is there a way to have different environment variables for different war files in tomcat? I am using a 3rd party war and need to have multiple deployments of the same war but with different environment variables (so it loads different configs).
Its easy if you run two instances of Tomcat independently. I'm assuming here you are talking about the OS environment variables.
You can also set properties in Tomcat for each war/web app. That would let you run two wars in one Tomcat instance. But that's not what you asked.
Ok, total crazy hack idea:
Implement a PropertyPlayHolderConfigurer (or use web.xml from Tomcat) for each app instance and load properties same name as you have for System.properties().
Then, create a delegate Properties class that contains both sets of properties. Then
Properties props = new DelegatingProperties(app1Props,app2Props)
System.setProperties(delegate);
public class DelegatingProperties extends Properties {
private Properties app1Props;
private Properties app2Props;
public DelegatingProperties(Properties app1Props, Properties app2Props) {
this.app1Props = app1Props;
this.app2Props = app2Props;
}
public String getProperty(String prop) {
// begin crazy science
String caller = new Throwable().getStackTrace()[0].getClassName();
// this is where you get creative.
// Do the System.setProperties() above so you can intercept calls to
//getProperty()
// and find out the FQCN of the library class(es) that need these variable
// (use your debugger).
// then implement the logic here to decide which of the 2 property sets you have
// you will query to get the correct results
}
}
These are SYSTEM properties we are talking about and they are meant to apply system wide. Your library was probably developed when it was 1-app-1-jvm (or the developer is a tard which is also likely).
Can I atleast get props for creativity? :)