Spring Boot: Change the order of the PropertySource - java

Spring Boot uses a PropertySource order that is designed to allow sensible overriding of values, properties are considered in the following order:
Command line arguments.
Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property)
JNDI attributes from java:comp/env.
Java System properties (System.getProperties()).
OS environment variables.
A RandomValuePropertySource that only has properties in random.*.
Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants)
Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants)
Application properties outside of your packaged jar (application.properties and YAML variants).
Application properties packaged inside your jar (application.properties and YAML variants).
#PropertySource annotations on your #Configuration classes.
Default properties (specified using SpringApplication.setDefaultProperties).
But I don't like this. How can I change it?

I found a way to achieve this. open source!!!!
App.java (main method)
public class App {
public static void main(String[] args) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(AppConfig.class);
SpringApplication app = builder.web(true).listeners(new AppListener()).build(args);
app.run();
}
}
AppListener.java
public class AppListener implements GenericApplicationListener {
public static final String APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME = "applicationConfigurationProperties";
#Override
public boolean supportsEventType(ResolvableType eventType) {
return ApplicationPreparedEvent.class.getTypeName().equals(eventType.getType().getTypeName());
}
#Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}
#Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationPreparedEvent) {
ApplicationPreparedEvent _event = (ApplicationPreparedEvent) event;
ConfigurableEnvironment env = _event.getApplicationContext().getEnvironment();
// change priority order application.properties in PropertySources
PropertySource ps = env.getPropertySources().remove(APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME);
env.getPropertySources().addFirst(ps);
// logging.config is my testing property. VM parameter -Dlogging.config=xxx will be override by application.properties
System.out.println(env.getProperty("logging.config"));
}
}
#Override
public int getOrder() {
return 0;
}
}

Related

Spring MVC: configure properties from before bean creation

I have a scenario where I want to programmatically inject properties into Spring before any beans are created/initialized:
The beans (not modifiable) are configured with ConditionalOnProperty, so properties need to be set before creation.
Properties need to be configured dynamically and programmatically, not via property file (we call an API and use the result to set the property value).
I see ApplicationContext has a way to get the current environment's property sources (via ConfigurableEnvironment), but I am not sure how to inject into the Spring lifecycle to configure the ApplicationContext before beans are initialized.
I'm aware of BeanFactoryPostProcessor as a hook which occurs before bean initialization, but I don't see a way to obtain an instance of ApplicationContext in it.
How can it be accomplished?
Note: the application itself is Spring Web/MVC, not Spring Boot. The third party library internally uses Spring Boot classes (ConditionalOnProperty).
Based on #m-deinum's comment, I was able to get it working with the following:
public class MyPropertyInitializer extends WebMvcConfigurerAdapter implements WebApplicationInitializer, ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
String initializerClasses = servletContext.getInitParameter(ContextLoader.GLOBAL_INITIALIZER_CLASSES_PARAM);
String initClassName = this.getClass().getName();
if (StringUtils.isNotBlank(initializerClasses)) {
initializerClasses += "," + initClassName;
} else {
initializerClasses = initClassName;
}
servletContext.setInitParameter(ContextLoader.GLOBAL_INITIALIZER_CLASSES_PARAM, initializerClasses);
}
#Override
public void initialize(ConfigurableApplicationContext context) {
Properties props = getCustomProperties();
PropertySource<?> propertySource = new PropertiesPropertySource("my-custom-props", props);
context.getEnvironment().getPropertySources().addLast(propertySource);
}
protected Properties getCustomProperties() {
Properties props = new Properties();
// do logic to set desired values
return props;
}
}

#ComponentScan does not detect classes in module path

I'm working on a JavaFX app with Java 11 and Spring. The app module is bundled with a custom JRE using jlink, which only allows named modules to be included in the bundle. Since Spring doesn't provide named modules but relies on automatic modules to achieve Java 9 Module System support, I use moditect to add module descriptors (module-info.java) to the Spring JARs.
Compiling, jlinking and executing the app works without any problems. However, Spring does not detect any of my app's classes annotated with #Component, despite my AppConfig class is annotated with #ComponentScan:
#Configuration
#ComponentScan
public class AppConfig {
}
In Main, I create an AnnotationConfigApplicationContext based on AppConfig and print all registered beans as well as the resources available on class path:
public class Main extends Application {
private ConfigurableApplicationContext applicationContext;
public static void main(String[] args) {
launch(args);
}
#Override
public void init() {
applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
}
#Override
public void start(Stage mainWindow) throws IOException {
System.out.println("Printing beans: " + applicationContext.getBeanDefinitionNames().length);
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
System.out.println(Arrays.toString(resolver.getResources("classpath*:com/myapp/**/*.class")));
}
#Override
public void stop() {
applicationContext.stop();
}
}
If I run the app using IntelliJ, PathMatchingResourcePatternResolver finds all my classes on the class path (I guess because IntelliJ runs the app using the class path, not the module path). Consequently, all components are detected via component scan and the respective beans are created:
Printing beans: 8
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalPersistenceAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
appConfig
com.myapp.services.UserServiceImpl
com.myapp.services.BookingServiceImpl
[file [/Users/user/myapp/target/classes/com/myapp/AppConfig.class], file [/Users/user/myapp/target/classes/com/myapp/Main.class], file [/Users/user/myapp/target/classes/com/myapp/services/UserService.class], file [/Users/user/myapp/target/classes/com/myapp/services/UserServiceImpl.class], file [/Users/user/myapp/target/classes/com/myapp/services/BookingService.class], file [/Users/user/myapp/target/classes/com/myapp/services/BookingServiceImpl.class]]
But if I run the app via the jlink'ed bundle, i.e. on the custom JRE using the module path, Spring is unable to detect any of my classes:
Printing beans: 5
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
appConfig
[]
PathMatchingResourcePatternResolver does not find any classes (because everything is now located on the module path), and not a single bean is instantiated by the component scan.
If I import the component classes manually into AppConfig, the beans are created properly and also injected via #Autowired:
#Configuration
#Import({
com.myapp.service.UserServiceImpl.class,
com.myapp.service.BookingServiceImpl.class
})
public class AppConfig {
}
Why is Spring able to create the beans when using #Import, but does not detect them via #ComponentScan? How can I resolve my components via #ComponentScan?
Another possible solution than the one provided by #IggyBlob in the question's comments is to patch PathMatchingResourcePatternResolver to search resources into the module path at least until a fully module compliant Spring version is released.
One possible implementation:
public class PathMatchingResourcePatternResolverJigsaw extends PathMatchingResourcePatternResolver {
public PathMatchingResourcePatternResolverJigsaw() {
}
public PathMatchingResourcePatternResolverJigsaw(ResourceLoader resourceLoader) {
super(resourceLoader);
}
public PathMatchingResourcePatternResolverJigsaw(ClassLoader classLoader) {
super(classLoader);
}
public List<Resource> getResourcesFromModules(String locationPattern) throws IOException {
String pattern = locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length());
List<Resource> list = new ArrayList<>();
ModuleLayer.boot().configuration().modules().stream()
.map(ResolvedModule::reference)
.forEach(mref -> {
try (ModuleReader reader = mref.open()) {
list.addAll(reader.list()
.filter(p -> getPathMatcher().match(pattern, p))
.map(p -> {
try {
return convertClassLoaderURL(reader.find(p).get().toURL());
} catch (Exception e) {
return null;
}
})
.collect(Collectors.toList()));
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
});
return list;
}
#Override
public Resource[] getResources(String locationPattern) throws IOException {
boolean addModSearch = true;
Resource[] result = super.getResources(locationPattern);
if (addModSearch && locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
List<Resource> list = getResourcesFromModules(locationPattern);
list.addAll(Arrays.asList(result));
result = list.toArray(new Resource[0]);
}
return result;
}
}
And initialize your spring appllication context with:
private ConfigurableApplicationContext context;
#Override
public void init() throws Exception {
ApplicationContextInitializer<GenericApplicationContext> initializer = new ApplicationContextInitializer<GenericApplicationContext>() {
#Override
public void initialize(GenericApplicationContext genericApplicationContext) {
genericApplicationContext.setResourceLoader(new PathMatchingResourcePatternResolverJigsaw());
}
};
this.context = new SpringApplicationBuilder().sources(MyApplication.class)
.initializers(initializer)
.build().run(new String[0]);
}
This is only an ugly workaround, so use it with caution

How do I set the logging properties in a spring java configuration?

I am using a Spring 5 MVC application. I am trying to get a pure java configuration going. I notice that my logging is not working. I have this in my application.properties:
logging.level.org.springframework.web=ERROR
logging.level.org.springframework.security=ERROR
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Now I am not using application.properties of course, so how do I set this in a pure Java way in one of my #Configuration classes?
If you actually want to get a pure java logger configuration, you can setup it like this:
public class LoggingInitializer implements ApplicationContextInitializer {
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
//suppose you use default logback (ch.qos.logback.classic.LoggerContext)
LoggerContext c = (LoggerContext) LoggerFactory.getILoggerFactory();
c.getLogger("ru.varren").setLevel(Level.DEBUG);
c.getLogger("org.springframework.web").setLevel(Level.ERROR);
c.getLogger("org.springframework.security").setLevel(Level.ERROR);
c.getLogger("org.hibernate.SQL").setLevel(Level.DEBUG);
}
}
And then init it in main before the app starts.
public class Main {
public static void main(String[] args){
new SpringApplicationBuilder(Main.class)
.initializers(new LoggingInitializer())
.run(args);
}
}
Also take a look at this answer: https://stackoverflow.com/a/20521500/1032167

Externalize properties and logback Spring

I'm using Spring (without spring-boot). I want to build standalone application that can be run with default configuration (logback.xml and application.properties in resource folder) or with -Dconfig.folder=/path/to/custom/external/directory
(logback.xml and application.properties in /path/to/custom/external/directory). When application will be run with -Dconfig.folder param AppConfig should load both logback and properties from external directory.
Is there anyway to make external folder act like a resource folder?
If not, what is a common solution for this?
My current implementation (using default resource folder only):
App.java
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
SampleAction p = context.getBean(SampleAction.class);
p.performTask();
}
}
AppConfig.java
#ComponentScan
#PropertySource("classpath:application.properties")
class AppConfig {
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
SampleAction.java
#Component
public class SampleAction {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
#Value("${sample.prop}")
private String sampleProp;
public void performTask(){
logger.debug(sampleProp);
}
}
logback.xml and application.properties are not relevant to the problem
Unlike the other answer suggests, if you use file prefix in #PropertySource, you're screwed because it won't be able to load the default application.properties from the jar. What you should do is the following:
#PropertySource("${config.folder:'classpath:'}/application.properties")
public class AppConfig
For logback.xml:
#Value("${config.folder}:")
private String configFolder;
InputStream = Optional.of(new ClassPathResource(configFolder + "/logback.xml"))
.filter(r -> r.exists())
.orElse(new ClassPathResource("classpath:/logback.xml"))
.getInputStream();
In both cases, I gave preference to the command line argument over the default packaged files. Of course, I didn't compile the above, so there may be typos or minor errors, but you get the idea.
Edit:
Since OP claims to not understand where to run the above code -
public class AppConfig {
#PostConstruct
void init() {
// init logback here
}
}
For log4j.xml
-Dlog4j.configuration=C:\neon\log4j.xml as VM argument
In main() method:
String filename = System.getProperty("log4j.configuration");
DOMConfigurator.configure(filename);
For external properties file:
-Dext.prop.dir=C:\neon as VM argument
Change in your AppConfig class will be like
#PropertySource("file:///${ext.prop.dir}/application.properties")
public class AppConfig{
}
Run App class with VM arguments as below and both file will be use from external location
-Dlog4j.configuration=C:\neon\log4j.xml -Dext.prop.dir=C:\neon

How to load an external configuration with Spring Boot?

I'm currently learning how to work with Spring Boot. Until now I never used Frameworks like Spring and used files directly (FileInputStream, etc.)
So here is the case: I have some dynamic configuration values like OAuth tokens. I want to use them inside of my application but I have no clue how to realize this with Spring.
Here is some code to make clear what I'm searching for:
#Config("app.yaml")
public class Test {
#Value("app.token")
private String token;
private IClient client;
public Test(String token) {
this.client = ClientFactory.build(token).login();
}
}
Sure, this example is very plain. Here I want to get the value "token" dynamically from a YAML configuration file. This file must be accessible for the user and not included in the JAR file.
I also found that doc: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html but I have now idea how to apply this to my project.
How can I achive this? Thank you in advance :)
Edit:
Here are some parts of my code:
WatchdogBootstrap.java
package de.onkelmorph.watchdog;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
#SpringBootApplication
#ImportResource("classpath:Beans.xml")
public class WatchdogBootstrap {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(WatchdogBeans.class);
app.setBannerMode(Mode.OFF);
app.setWebEnvironment(false);
app.run(args);
}
}
Beans.xml (Located in default package)
<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:annotation-config></context:annotation-config>
</beans>
Watchdog.java
package de.onkelmorph.watchdog;
// Imports ...
#Component
#PropertySource("file:/watchdog.yml")
public class Watchdog {
// ...
// Configuration
#Value("${watchdog.token}")
private String token;
public Watchdog() {
System.out.println(this.token);
System.exit(0);
}
// ...
}
watchdog.yml (Located in src/main/resources)
watchdog:
token: fghaepoghaporghaerg
First of all your Test class should be annotated with #Component in order for it to be registered as a bean by spring (also make sure all your classes are under your main package - the main package is where a class that is annotated with #SpringBootApplication reside).
Now you should either move all your properties to application.yml (src/main/resources/application.yml), that is picked automatically by spring boot (note that it should be .yml instead of .yaml or register a custom PropertySourcesPlaceholderConfigurer.
Example for PropertySourcesPlaceholderConfigurer:
#Bean
public static PropertySourcesPlaceholderConfigurer PropertySourcesPlaceholderConfigurer() throws IOException {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
MutablePropertySources propertySources = new MutablePropertySources();
Resource resource = new DefaultResourceLoader().getResource("classpath:application.yml");
YamlPropertySourceLoader sourceLoader = new YamlPropertySourceLoader();
PropertySource<?> yamlProperties = sourceLoader.load("yamlProperties", resource, null);
propertySources.addFirst(yamlProperties);
configurer.setPropertySources(propertySources);
return configurer;
}
Now your properties should be loaded to spring's environment and they will be available for injection with #Value to your beans.
You basically got three easy options.
Use application.properties which is Springs internal configuration file.
Load your own configuration file using --spring.config.name as VM parameter.
You can use #PropertySource to load either an internal or external configuration. #PropertySource only works with .properties config files. There is currently an open Jira ticket to implement yaml support. You can follow the progress here: https://jira.spring.io/browse/SPR-13912
Notice, if you are using multiple yaml and/or properties files which contain common keys, the it will always use the definition of the key which was loaded last. This is why the below example uses two different keys. If it used the same key, then it would print out PROPERTIES FILE twice.
Short simple code snippet:
#Component
#PropertySource("file:/path/to/config/app.properties")
class Address{
#Value("${addr.street}")
private String street;
#Value("${addr.city}")
private String city;
}
app.properties
addr.street=Abbey Road
addr.city=London
Extensive Example
DemoApplication.java
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//Call class with properties
context.getBean(WatchdogProperties.class).test();
//Call class with yaml
context.getBean(WatchdogYaml.class).test();
}
//Define configuration file for yaml
#Bean
public static PropertySourcesPlaceholderConfigurer properties() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ClassPathResource("watchdog.yml"));
propertySourcesPlaceholderConfigurer.setProperties(yaml.getObject());
return propertySourcesPlaceholderConfigurer;
}
}
WatchdogProperties.java
#Component
//PropertySource only works for .properties files
#PropertySource("classpath:watchdog.properties")
public class WatchdogProperties{
//Notice the key name is not the same as the yaml key
#Value("${watchdog.prop.token}")
private String token;
public void test(){
System.out.println(token);
}
}
WatchdogYaml.java
#Component
class WatchdogYaml{
//Notice the key name is not the same as the properties key
#Value("${watchdog.token}")
private String token;
public void test(){
System.out.println(token);
}
}
Properties and Yaml files
Both of these files are located in src/main/resources
watchdog.yml:
watchdog:
token: YAML FILE
watchdog.properties:
watchdog.prop.token=PROPERTIES FILE
Output
PROPERTIES FILE
YAML FILE

Categories

Resources