Considering a Spring Boot CommandLineRunner Application, I would like to know how to filter the "switch" options passed to Spring Boot as externalized configuration.
For example, with:
#Component
public class FileProcessingCommandLine implements CommandLineRunner {
#Override
public void run(String... strings) throws Exception {
for (String filename: strings) {
File file = new File(filename);
service.doSomething(file);
}
}
}
I can call java -jar myJar.jar /tmp/file1 /tmp/file2 and the service will be called for both files.
But if I add a Spring parameter, like java -jar myJar.jar /tmp/file1 /tmp/file2 --spring.config.name=myproject then the configuration name is updated (right!) but the service is also called for file ./--spring.config.name=myproject which of course doesn't exist.
I know I can filter manually on the filename with something like
if (!filename.startsWith("--")) ...
But as all of this components came from Spring, I wonder if there is not a option somewhere to let it manage it, and to ensure the strings parameter passed to the run method will not contain at all the properties options already parsed at the Application level.
Thanks to #AndyWilkinson enhancement report, ApplicationRunner interface was added in Spring Boot 1.3.0 (still in Milestones at the moment, but will soon be released I hope)
Here the way to use it and solve the issue:
#Component
public class FileProcessingCommandLine implements ApplicationRunner {
#Override
public void run(ApplicationArguments applicationArguments) throws Exception {
for (String filename : applicationArguments.getNonOptionArgs())
File file = new File(filename);
service.doSomething(file);
}
}
}
There's no support for this in Spring Boot at the moment. I've opened an enhancement issue so that we can consider it for a future release.
One option is to use Commons CLI in the run() of your CommandLineRunner impl.
There is a related question that you may be interested.
Here is another solution :
#Component
public class FileProcessingCommandLine implements CommandLineRunner {
#Autowired
private ApplicationConfig config;
#Override
public void run(String... strings) throws Exception {
for (String filename: config.getFiles()) {
File file = new File(filename);
service.doSomething(file);
}
}
}
#Configuration
#EnableConfigurationProperties
public class ApplicationConfig {
private String[] files;
public String[] getFiles() {
return files;
}
public void setFiles(String[] files) {
this.files = files;
}
}
Then run the program :
java -jar myJar.jar --files=/tmp/file1,/tmp/file2 --spring.config.name=myproject
Related
So there's a lot of hits on this topic, but none of them have worked for me.
I have a very simple configuration class:
#Configuration
#ConfigurationProperties(prefix = "props")
public class TagIncluder {
private static final String PARAMETER_NAME = "tags";
private List<String> tags;
public TagIncluder() {
tags = new ArrayList<>();
}
public List<String> getTags() {
return tags;
}
#Handler
public void attachIncludedTags(Exchange exchange) {
exchange.getIn().setHeader(PARAMETER_NAME, tags);
}
}
I want this class to be able to load different property files. I am using yaml, and my file is named application-tag_test.yml. I have tried placing this file in src/main/resources, src/test/resources and src/test/resources/config, but it is never picked up.
This is the contents of the property file:
props:
tags:
- test
And finally, the test case:
#SpringBootTest
#ActiveProfiles("tag_test")
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = TagIncluder.class)
public class TagIncluderTest extends ExchangeTestSupport {
#Autowired
private TagIncluder sut;
#Test
public void attachIncludedTags_shouldUseTagsInFileIfFileSpecified() {
Exchange testExchange = createExchange();
sut.attachIncludedTags(testExchange);
Assertions.assertThat(testExchange.getIn().getHeader("tags", List.class))
.size().isGreaterThanOrEqualTo(1);
}
}
Additionally, I have tried placing an application.properties file in the above described locations with the following content:
spring.profiles.active=tag_test
What is required for Spring to set my yaml file as the desired configuration for my test class under test?
UPDATE
So after some exploration and trial and error, I have found that the following works:
#SpringBootTest
#ActiveProfiles("tag_test")
#RunWith(SpringJUnit4ClassRunner.class)
public class TagIncluderTest extends ExchangeTestSupport {
#Autowired
private TagIncluder sut;
#Test
public void attachIncludedTags_shouldUseTagsInFileIfFileSpecified() {
Exchange testExchange = createExchange();
sut.attachIncludedTags(testExchange);
Assertions.assertThat(testExchange.getIn().getHeader("tags", List.class))
.size().isGreaterThanOrEqualTo(1);
}
}
The difference here is that I've removed the #ContextConfiguration annotation and I let Spring take care of all of that.
It is a lot slower, and I would prefer specifying what is needed. I think this might break in the future, for instance if I add another configuration class that will start with the entire context and throw errors because those properties are not included in my application-tag_test.yml configuration.
Finally, any of the above locations I tried for the configuration is valid with the above annotations. The application.properties to specify a profile is not needed.
If anyone knows a way to specify what should be loaded into the context instead, I'd be very grateful for another solution.
With some guidance of Jans suggestion above, I've managed to isolate the test to a slice. Auto configured testing is written about here, however that only touches on Springs predefined #..Test annotations.
If you dive deeper into the #WebMvcTest, for instance, you will find the #ImportAutoConfiguration annotation.
Using this, we can tell our test class to enable auto configuration for a single slice of our application. A tutorial is available here. The full list of factories available for auto configuration can be found in the spring-boot repository.
Finally, this is the entire test class:
#ActiveProfiles("tag_test")
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(classes = TagIncluder.class)
#ImportAutoConfiguration(classes = ConfigurationPropertiesAutoConfiguration.class)
public class TagIncluderTest extends ExchangeTestSupport {
#Autowired
private TagIncluder sut;
#Test
public void attachIncludedTags_shouldUseTagsInFileIfFileSpecified() {
Exchange testExchange = createExchange();
sut.attachIncludedTags(testExchange);
Assertions.assertThat(testExchange.getIn().getHeader("tags", List.class))
.size().isGreaterThanOrEqualTo(1);
}
}
The class under test is untouched.
So now we can:
Use profiles
Use yaml
Test only our desired class in Spring Context
This has been very enlightening :)
The Spring Boot Test documentations states that
External properties, logging, and other features of Spring Boot are installed in the context by default only if you use SpringApplication to create it.
This means that you need to have a working Spring Boot Application in order to test anything related to property loading in a test case.
Also, setting a list from properties needs a setter. This works:
#Configuration
#ConfigurationProperties(prefix = "props")
public class TagIncluder {
private List<String> tags;
public void setTags(List<String> tags) {
this.tags = tags;
}
public List<String> getTags() {
return tags;
}
}
#Component
public class MyComponent {
#Autowired
TagIncluder tagIncluder;
}
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
#SpringBootTest
#ActiveProfiles("tag_test")
#RunWith(SpringRunner.class)
public class TagIncluderTest {
#Autowired
private TagIncluder sut;
#Test
public void attachIncludedTags_shouldUseTagsInFileIfFileSpecified() {
System.out.println(sut.getTags());
}
}
I have a REST application that reads from a database repository.
I want to add a command line application that reads a CSV and imports the data to the database.
If I add another #SpringBootApplication class that implements CommandLineRunner to the project/jar, Spring starts it at the same time as my main server.
If I add a class that initialises the spring context itself, the jdbc url on the JPARepository uses the defaults instead of those from the spring boot properies
spring.datasource.url=jdbc:h2:file:./test;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE
#ComponentScan(basePackages = "com.test")
public class CsvImport {
#Autowired
private Repository repository;
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CsvImport.class);
context.start();
CsvImport csvImport = context.getBean(CsvImport.class);
File file = new File(args[0]);
if (file.isFile()) {
csvImport.importCsv(file);
}
context.stop();
}
private void importCsv(File file) {
....
....
Is there a better way to do this?
There are several steps required to get this working.
The first thing is that #SpringBootApplication auto-scans any packages below it and will auto-start any CommandLineRunner that it finds so the command line application needs to be in a parallel package.
e.g.
- com
- test
- rest
- model
- repository
- controller
#RestSpringBoot.java
- Commands
#ImportCsv.java
The next thing is that since the command line application is in a parallel package you need to specify the scanning yourself.
There are 3 parts to this:
#ComponentScan
#EntityScan
#EnableJpaRepositories
The first ensures the beans are created, the second ensures that the hibernate entities are created and the third ensures that the JPA classes are generated correctly.
The last part is to disable the start of the spring boot webserver otherwise you will have port conflicts.
This ends up with:
#SpringBootApplication
#ComponentScan(basePackageClasses = {Repository.class})
#EntityScan(basePackageClasses = {MyEntity.class})
#EnableJpaRepositories(basePackageClasses = {Repository.class})
public class CsvImport {
#Autowired
private RiskRepository repository;
public static void main(String[] args) {
SpringApplication app = new SpringApplication(CsvImport.class);
app.setBannerMode(Banner.Mode.OFF);
app.setWebApplicationType(WebApplicationType.NONE);
app.run(args);
}
#Override
public void run(String... args) throws Exception {
File file = new File(args[0]);
if (file.isFile()) {
importCsv(file);
}
}
private void importCsv(File file) {
....
....
After starting my spring boot application I want to start an customer process like creating required folders, files, etc. For that I'm using ApplicationListener<ApplicationReadyEvent>. This works like expected. But I'm building my spring application context with SpringApplicationBuilder. Every child notifies that the application is started correctly. So my customer post-process startes even more than one time.
#SpringBootApplication
#EnableConfigurationProperties(value = {StorageProperties.class})
#EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplicationBuilder parentBuilder
= new SpringApplicationBuilder(Application.class);
parentBuilder.child(Config1.class)
.properties("server.port:1443")
...
.run(args);
parentBuilder.child(Config2.class)
.properties("server.port:2443")
...
.run(args);
}
}
My first idea was, that I can create manuelly a new Bean with #Bean in Config1 for my Event-Listener. But I was not able to overhand the configuration file StorageProperties.class, which is necessary for this class.
Because the Listener has an constructor based dependency injection:
private final Path mPathTo;
public AfterStart(StorageProperties prop) {
this.mPathTo = Paths.get(prob.getPath());
}
How can I be able to start the listener just once per start?
For everyone who is interested in this question. This solution worked for me:
public void onApplicationEvent(ApplicationReadyEvent e) {
if (e.getApplicationContext().getParent == null) {
System.out.println("******************************");
System.out.println("Post-process begins.");
System.out.println("******************************");
}
}
I'm building a spring boot application. I want to run it like this:
java -jar myjar.jar inputFile outputFile
How do I write a #SpringBootTest for this? I imagine that using #SpringBootTest would make Spring fail during startup because some of my code would say, "you need to provide an inputFile and outputFile". Is there a way to pass program arguments when using a #SpringBootTest?
I'm inferring from this answer that I may have to use a SpringApplicationBuilder to do this.
I thought I had the answer but I was wrong. This incorrect information may still be useful to some:
(This information is wrong. I think that some arguments can't be referred to in code as properties, but not all. I still don't know how to get the application arguments in a #SpringBootTest) I was confused because I didn't understand the terminology. The annotation has a parameter for "properties". I thought it was to point it at a property file, but the documentation says:
Properties in form key=value that should be added to the Spring Environment before the test runs.
The other piece of the terminology puzzle is that what I called "program arguments" the Spring docs refer to as "properties".
This is some additional relevant documentation: https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-application-arguments
This is a workaround (not an answer). You can do something like this:
private SpringApplicationBuilder subject;
#Before
public void setUp() throws Exception {
subject = new SpringApplicationBuilder(Application.class);
}
#Test
public void requiresInputAndOutput() throws Exception {
thrown.expect(IllegalStateException.class);
subject.run();
}
#Test
public void happyPathHasNoErrors() throws Exception {
subject.run(EXISTING_INPUT_FILE, "does_not_exist");
}
I don't like this very much. It prevents me from using #Autowired elsewhere in my test.
if your program arguments is
--arg1=val1
before springboot version 2.2.0
you can use
#SpringBootTest({"arg1=val1"})
after springboot 2.2.0
you can use
#SpringBootTest(args={"--arg1=val1"})
I had the same issue. Out of the box, the SpringBootContextLoader (used by default with #SpringBootTest) always runs your app without any arguments. However, you can provide your own bootstrapper, which can then in turn provide your own SpringApplication that gets called to run your test. From there, you can override the run(String... args) method to provide whatever arguments you want.
For example, given a simple application:
#SpringBootApplication
public class Main {
public static void main(final String[] args) {
new SpringApplicationBuilder(Main.class).run(args);
}
#Bean
public ApplicationRunner app() {
return args -> System.out.println(args.getOptionNames());
}
}
You can inject test arguments with:
#ContextConfiguration(classes = Main.class)
#ExtendWith(SpringExtension.class)
#BootstrapWith(RestartAppsTest.Bootstrapper.class)
class RestartAppsTest {
static class Bootstrapper extends SpringBootTestContextBootstrapper {
static class ArgumentSupplyingContextLoader extends SpringBootContextLoader {
#Override
protected SpringApplication getSpringApplication() {
return new SpringApplication() {
#Override
public ConfigurableApplicationContext run(String... args) {
return super.run("--restart");
}
};
}
}
#Override
protected Class<? extends ContextLoader> getDefaultContextLoaderClass(Class<?> testClass) {
return ArgumentSupplyingContextLoader.class;
}
}
#Test
void testRestart() {
//
}
}
It's obviously a bit verbose, but it works. You could clean it up and make a nicer/reusable bootstrapper that looked for your own annotation (or possibly reuse JUnit Jupiter's #Arguments) that declared what arguments to supply (instead of hardcoding them).
You can use #SpringBootTest(classes=Application.class, args ={inputFile, outputFile}) if your app's main method looks a little different like
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}
For me working example is
#SpringBootTest(args ={"--env", "test"})
class QuickFixServerAppTest {
#Test
void loadContextTest() {
}
}
is the same as passing the
--env test
argument when starting Spring
Normally you're writing tests for your services; not the boot strapper. Spring Boot will pass command line parameters to your classes - perhaps using the #Value annotation, which in turn will be parameters to your service. Consider testing your services using the SpringRunner. Here's an example from my code base.
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {
Neo4jConfigTest.class,
FdTemplate.class,
FdServerIo.class,
MapBasedStorageProxy.class})
#ActiveProfiles({"dev", "fd-auth-test", "fd-client"})
public class TestEntityLinks {
#Autowired
private ContentModelService contentModelService;
#Autowired
private BatchService batchService;
#Test
public void doSomething () { ... }
Summary: Adding the #ComponentScan (or #SpringBootApplication) annotation to my application class changes the behaviour of SpringApplicationBuilder.properties() and breaks my integration test.
I am using a cut-down version of the Spring Boot sample:
spring-boot-sample-websocket-jetty
I have removed everything except what is required for the "echo" example (and I'm using Spring Boot 1.3.3).
I am left with the following SampleJettyWebSocketsApplication code:
#Configuration
#EnableAutoConfiguration
//#ComponentScan // --- If I uncomment this the test breaks ---
#EnableWebSocket
public class SampleJettyWebSocketsApplication
implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(), "/echo").withSockJS();
}
#Bean
public EchoService echoService() {
return new DefaultEchoService("Did you say \"%s\"?");
}
#Bean
public WebSocketHandler echoWebSocketHandler() {
return new EchoWebSocketHandler(echoService());
}
public static void main(String[] args) {
SpringApplication.run(SampleJettyWebSocketsApplication.class, args);
}
}
And the following test class (code straight from the Spring Boot samples):
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(SampleJettyWebSocketsApplication.class)
#WebIntegrationTest({"server.port=0"})
#DirtiesContext
public class SampleWebSocketsApplicationTests {
private static Log logger = LogFactory.getLog(SampleWebSocketsApplicationTests.class);
#Value("${local.server.port}")
private int port = 1234;
#Test
public void echoEndpoint() throws Exception {
logger.info("Running the echoEndpoint test. Port: " + port + ". Path: /echo/websocket");
ConfigurableApplicationContext context = new SpringApplicationBuilder(
ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class)
.properties("websocket.uri:ws://localhost:" + this.port
+ "/echo/websocket")
.run("--spring.main.web_environment=false");
long count = context.getBean(ClientConfiguration.class).latch.getCount();
AtomicReference<String> messagePayloadReference = context
.getBean(ClientConfiguration.class).messagePayload;
context.close();
assertThat(count).isEqualTo(0);
assertThat(messagePayloadReference.get())
.isEqualTo("Did you say \"Hello world!\"?");
}
#Configuration
static class ClientConfiguration implements CommandLineRunner {
#Value("${websocket.uri}")
private String webSocketUri;
private final CountDownLatch latch = new CountDownLatch(1);
private final AtomicReference<String> messagePayload = new AtomicReference<String>();
#Override
public void run(String... args) throws Exception {
logger.info("Waiting for response: latch=" + this.latch.getCount());
if (this.latch.await(10, TimeUnit.SECONDS)) {
logger.info("Got response: " + this.messagePayload.get());
}
else {
logger.info("Response not received: latch=" + this.latch.getCount());
}
}
#Bean
public WebSocketConnectionManager wsConnectionManager() {
logger.info("Setting up SimpleClientWebSocketHandler...");
WebSocketConnectionManager manager = new WebSocketConnectionManager(client(),
handler(), this.webSocketUri);
manager.setAutoStartup(true);
return manager;
}
#Bean
public StandardWebSocketClient client() {
return new StandardWebSocketClient();
}
#Bean
public SimpleClientWebSocketHandler handler() {
logger.info("Creating new SimpleClientWebSocketHandler using SimpleGreetingService...");
return new SimpleClientWebSocketHandler(greetingService(), this.latch,
this.messagePayload);
}
#Bean
public GreetingService greetingService() {
return new SimpleGreetingService();
}
}
}
Running the Application and the unit test as above all is fine but if I uncomment the #ComponentScan annotation on the application class the application still runs OK but the test breaks with the error:
Could not resolve placeholder 'websocket.uri' in string value "${websocket.uri}".
I have read at setting-the-run-time-properties-on-springapplicationbuilder that:
The properties you configure on SpringApplicationBuilder are made available in your application's Environment, not as system properties.
And in the #ComponentScan javadoc that:
If specific packages are not defined, scanning will occur from the package of the class that declares this annotation.
But I don't understand why the behaviour changes when the #ComponentScan annotation is added.
How can I set the System Property websocket.uri in the test when the application class is annotated with #ComponentScan (or #SpringBootApplication)?
(I aim to use #SpringBootApplication, which incorporates #ComponentScan, but I can't until I get this working.)
There are several ways to add a system properties.
Solution 1:
Add arguments for Test in format of -Dabc=xyz, that will add property abc to system properties.
Solution 2:
Just like floor 0.
Solution 3:
Just let spring-boot load the properties, such as classpath:bootstrap.yml, and you can specify whatever properties in there.
The annotation #ComponentScan will enable auto scanning based on current package or ComponentScan#basePackages. Which means SampleWebSocketsApplicationTests.ClientConfiguration will be scanned cause they have same base package samples.websocket.jetty.
However, SampleWebSocketsApplicationTests.ClientConfiguration should not be parsed by SpringJUnit4ClassRunner cause we need parse it in SampleWebSocketsApplicationTests#echoEndpoint manually. It's should only be parsed by ApplicationContext created in echoEndpoint().
What's more, #SpringBootApplication equals to use #Configuration and #EnableAutoConfiguration and #ComponentScan together, so comment out #ComponentScan or #SpringBootApplication will have same effect.
My suggestion is move class SampleWebSocketsApplicationTests into package samples.websocket.jettytest(different from samples.websocket.jetty) and enable #ComponentScan or #SpringBootApplication on SampleJettyWebSocketsApplication and try again. It should work.
Adding my thoughts on this (from whatever i could gather from your code):
-Try adding the property websocket.uri in you application properties or if your project contains src/test/resources/test.properties then add it into your test.properties file.#ComponentScan should pick it up.
-Else,you could just say :
public static void main(String[] args) {
System.setProperty("websocket.uri","<your uri>");
SpringApplication.run(SampleJettyWebSocketsApplication.class, args);
}
Hope it helps.