how to manage spring-cloud bootstrap properties in a shared library? - spring

I'm in the process of building a library which provides an opinionated configuration for applications which use our Spring Cloud Config/Eureka setup. The idea is to deliver this configuration as a custom starter with little or no spring cloud-related boilerplate in individual microservice apps.
At this point, the majority of the shared configuration that I want to put in this library consists of stuff in bootstrap.yml. I'd like to provide bootstrap.yml in my custom starter, but applications using the library still need to be able to provide their own bootstrap.yml, even if only so they can set their spring.application.name properly.
Due to the way bootstrap.yml is loaded from the classpath, Spring seems to ignore the one in the shared lib if the application has its own bootstrap.yml. I can't even use an ApplicationContextInitializer to customize the Environment because of the special way the bootstrap context treats ApplicationContextInitializers.
Does anyone have any recommendations for an approach that would work here? I want to provide a drop-in lib that makes our opinionated bootstrap config work without having to duplicate a boilerplate bootstrap.yml in all of our projects.

You can add a PropertySource in a shared library to the bootstrap properties by using the org.springframework.cloud.bootstrap.BootstrapConfiguration key in the META-INF/spring.factories file.
For example, you can create a library containing the following:
src/main/java/com/example/mylib/MyLibConfig.java
package com.example.mylib;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
#Configuration
#PropertySource("classpath:mylib-config.properties")
public class MyLibConfig {
}
src/main/resources/mylib-config.properties
eureka.instance.public=true
# or whatever...
src/main/resources/META-INF/spring.factories
org.springframework.cloud.bootstrap.BootstrapConfiguration=com.example.mylib.MyLibConfig
More details: http://projects.spring.io/spring-cloud/spring-cloud.html#_customizing_the_bootstrap_configuration

I was able to find a solution to this. The goals of this solution are:
Load the values from a yaml file in a shared library.
Allow applications using the library to introduce their own bootstrap.yml that is also loaded into the Environment.
Values in the bootstrap.yml should override values in the shared yaml.
The main challenge is to inject some code at the appropriate point in the application lifecycle. Specifically, we need to do it after the bootstrap.yml PropertySource is added to the environment (so that we can inject our custom PropertySource in the correct order relative to it), but also before the application starts configuring beans (as our config values control behavior).
The solution I found was to use a custom EnvironmentPostProcessor
public class CloudyConfigEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
private YamlPropertySourceLoader loader;
public CloudyConfigEnvironmentPostProcessor() {
loader = new YamlPropertySourceLoader();
}
#Override
public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication application) {
//ensure that the bootstrap file is only loaded in the bootstrap context
if (env.getPropertySources().contains("bootstrap")) {
//Each document in the multi-document yaml must be loaded separately.
//Start by loading the no-profile configs...
loadProfile("cloudy-bootstrap", env, null);
//Then loop through the active profiles and load them.
for (String profile: env.getActiveProfiles()) {
loadProfile("cloudy-bootstrap", env, profile);
}
}
}
private void loadProfile(String prefix, ConfigurableEnvironment env, String profile) {
try {
PropertySource<?> propertySource = loader.load(prefix + (profile != null ? "-" + profile: ""), new ClassPathResource(prefix + ".yml"), profile);
//propertySource will be null if the profile isn't represented in the yml, so skip it if this is the case.
if (propertySource != null) {
//add PropertySource after the "applicationConfigurationProperties" source to allow the default yml to override these.
env.getPropertySources().addAfter("applicationConfigurationProperties", propertySource);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
public int getOrder() {
//must go after ConfigFileApplicationListener
return Ordered.HIGHEST_PRECEDENCE + 11;
}
}
This custom EnvironmentPostProcessor can be injected via META-INF/spring.factories:
#Environment PostProcessors
org.springframework.boot.env.EnvironmentPostProcessor=\
com.mycompany.cloudy.bootstrap.autoconfig.CloudyConfigEnvironmentPostProcessor
A couple things to note:
The YamlPropertySourceLoader loads yaml properties by profile, so if you are using a multi-document yaml file you need to actually load each profile from it separately, including the no-profile configs.
ConfigFileApplicationListener is the EnvironmentPostProcessor responsible for loading bootstrap.yml (or application.yml for the regular context) into the Environment, so in order to position the custom yaml properties correctly relative to the bootstrap.yml properties precedence-wise, you need to order your custom EnvironmentPostProcessor after ConfigFileApplicationListener.
Edit: My initial answer did not work. I'm replacing it with this one, which does.

Related

Enable by default an Actuator Endpoint in Spring Boot

I developed a small library that adds a custom endpoint for the actuator and I like to expose it by default. Spring Boot 2.7.4 only exposes by default health.
At the moment, what I am doing is registering an EnvironmentPostProcessor to add a property to include health,jwks at the last PropertySource in the environment. But it seems a little bit fragile. There are other libraries that have to export other endpoints by default (metrics, prometheus...)
This is what I am doing at the moment:
public class PoCEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String PROPERTY_NAME = "management.endpoints.web.exposure.include";
#Override
public void postProcessEnvironment(
ConfigurableEnvironment environment,
SpringApplication application
) {
var propertySources = environment.getPropertySources();
propertySources.stream()
.filter(it -> it.containsProperty(PROPERTY_NAME))
.findFirst().ifPresentOrElse(source -> {
var property = source.getProperty(PROPERTY_NAME);
var pocSource = new MapPropertySource(PROPERTY_NAME, Map.of(PROPERTY_NAME, property + ",jwks"));
// Add the new property with more priority
propertySources.addBefore(source.getName(), pocSource);
}, () -> {
var pocSource = new MapPropertySource(PROPERTY_NAME, Map.of(PROPERTY_NAME, "health,jwks"));
propertySources.addLast(pocSource);
});
}
}
Is there any way to expose by default that allow me to add several endpoints in different libraries without playing to much with the property sources?
It’s not exactly clear to me if you’re asking how the client apps that use your library would enable specific endpoints, or if you are writing more than one library and want to expose different endpoints. I’ll answer both.
management.endpoints.web.exposure.include=comma-separated-endpoints would enable the listed endpoints without your library having to do anything. Your client apps can set this property in application.yml.
If you want to set this property by default in your library, one of the easiest ways is to put it in a property file, and load it as a #PropertySource on a #Configuration bean. I’m assuming your library is a starter and the #Configuration bean is auto-configured. If you don’t know how to create a starter, refer to this article.

Restricting #PropertySource to specific #Configuration classes in spring

I have the following property files:
application.properties - Base spring config
common.properties - Common config
servicea.properties - Service specific config
password.properties - Password Config
Based on the last three files, I have 3 <Name>Property classes in the following format.
#Configuration
#PropertySource("file:<filepath>")
public class ServiceAProperties {
private final Environment env;
#Autowired
public ServiceAProperties (Environment env) {
this.env = env;
}
public String getTest() {
String test = env.getProperty("application.test"); // Accessible - Not Intended
test = env.getProperty("common.test"); // Accessible - Not Intended
test = env.getProperty("servicea.test"); // Accessible - Intended
test = env.getProperty("password.test"); // Accessible - Not Intended
return env.getProperty("servicea.test");
}
}
For some reason even though I only have the respective Property classes marked with their specific property file paths, they are also picking up paths from other files and adding it to the env.
How can I make sure that I my environment to be generated only from the files I specify?
The spring docs for #PropertySource says:
Annotation providing a convenient and declarative mechanism for adding
a PropertySource to Spring's Environment. To be used in conjunction
with #Configuration classes.
This means that there is only one Spring Environment. When you have multiple classes annotated with this annotation they will always contribute to the same environment because there is only one.
So, to answer your question, in your case the environment will always be filled with data from all classes that have #Configuration and #PropertySource annotations.
In order to fill the environment with data that you specify, you can use profile specific properties. You can separate the data in multiple profiles and choose the profiles that will be activated (and which data will be accessible in the environment).
I am sharing my own solution to this since I was not able to find an acceptable answer.
Using a new ResourcePropertySource("classpath:<location>") allows you to load in multiple individual property files using their respective individual objects.
Once loaded, the configuration can be accessed in the same way as before propertiesObj.getProperty("propKey")

Resolving spring profile before servlet startup for dynamically reading configs from local or spring boot config server

I've integrated spring cloud config server in my non-spring boot app by extending AnnotationConfigWebApplicationContext and overriding createEnvironment().
In my application I set the spring.profiles.default in web.xml. When the value is prod I want to fetch the configs from config server and when the value is local I want to load the configs from local property file.
The problem is when I'm calling acceptsProfiles() on StandardServletEnvironment since the ServletConfigPropertySource is a Stub, its not returning default profile value specified in the web.xml. I suppose later, during refresh the stub will be substituted by the actual class. Calling acceptsProfiles() is for knowing if prod profile is active to connect to the config server or go to the local properpty file.
This is causing problem for me because I want to dynamically create Environment based on the default profile.
Can anyone shed some light on this issue.
Update
Instead of relying on the context parameter for finding out the active profile I decided to pass a system variable to set the active profile and override customizePropertySources of AbstractEnvironment to load proper property sources.
The reason for doing it programmatically is to have control over the ordering of the property sources. Based on Spring doc of PropertySource it is mentioned that
In certain situations, it may not be possible or practical to tightly control property source ordering when using #ProperySource annotations ... it is recommended that the user fall back to using the programmatic PropertySource API. See ConfigurableEnvironment and MutablePropertySources
Please refer to the code below
#Override
protected void customizePropertySources(MutablePropertySources propertySources) {
super.customizePropertySources(propertySources);
loadBootstrapProperties(propertySources);
if (isAppConfigLocal()) {
try {
propertySources.addLast(new ResourcePropertySource("appEnvironmentSpecificProperties", getLocalPropertyName()));
propertySources.addLast(new ResourcePropertySource("appCommonProperties", assembalePropertyName(LOCAL)));
} catch (IOException e) {
logger.error(e.getMessage(), e);
throw new IllegalStateException("Could not locate local PropertySource(s)", e);
}
} else {
PropertySource<?> source = initializeConfigServicePropertyLocator(this);
if (source != null) {
propertySources.addLast(source);
}
}
}

Where does spring boot configure default application.properties

By default Spring Boot will automatically load properties from classpath:/application.properties
I want to know where is this auto configuration source code.
I want to exclude from my app.
IE: #EnableAutoConfiguration(exclude=XXXXAutoconfiguration.class)
The reason is:
Because I cannot override the default application.properties by an external property using #PropertySource
#SpringBootApplication
#ComponentScan(basePackages = {"com.test.green.ws"})
#PropertySource(value = {"classpath:/application.properties", "file:/opt/green-ws/application.properties"})
public class GreenWSApplication {
public static void main(String[] args) {
SpringApplication.run(GreenWSApplication.class, args);
}
}
There are many ways to override property keys without disabling the whole externalized configuration feature; and that's actually the goal.
You can see here the order the properties are considered in. For example, you can add that external properties file in a config folder right next to the packaged JAR, or even configure the file location yourself.
Now if you really want to disable all of that (and the Boot team strongly suggests not to do that), you can register your own EnvironmentPostProcessor (see here) and remove PropertySources from MutablePropertySources, which you can fetch with configurableEnvironment. getPropertySources().
There's no easier way to do that because:
this comes really early in the application init phase, before auto-configurations
this is not something you should do, as it will have many side effects

Setting properties in Adobe CQ5

I'm working on CQ5 based app, which is a whole new area for me as I was mainly working on Spring based web-apps before.
The app is maven project based on Blue-prints archetype(http://www.cqblueprints.com/xwiki/bin/view/Blue+Prints/The+CQ+Project+Maven+Archetype).
Now I have a question, what is a standard approach to add some properties, that would normally go to config.properties (or alike) file in standard web-app. Properties that contain things like hostNames, accountNumbers and alike.
Cheers.
I'm not familiar with blueprints, but as I understand that's just a way to generate your CQ project structure, so I assume it doesn't have any real impact on how you manage configuration parameters.
CQ5 is based on Apache Sling, which uses the OSGi ConfigAdmin service for configurable parameters, and provides a few tools to make this easier.
You can see an example of that in the PathBasedDecorator Sling component, which uses the #Component annotation to declare itself as an OSGi component:
#Component(metatype=true, ...)
and later uses an #Property annotation to declare a multi-value configurable parameter, with default values:
#Property(value={"/content:2", "/sling-test-pbrt:2"}, unbounded=PropertyUnbounded.ARRAY)
private static final String PROP_PATH_MAPPING = "path.mapping";
That value is then read in the component's activate() method:
final Dictionary<?, ?> properties = componentContext.getProperties();
final String[] mappingList = (String[]) properties.get(PROP_PATH_MAPPING);
and the OSGi bundle that contains that component provides a metatype.properties file to define the name and label of the configurable parameter.
That's it - with this, Sling and the OSGi framework generate a basic config UI for the component, that you can access from /system/console/config, and manage your component's activation and reactivation automatically if configuration parameters change.
Those configurations can also come from the JCR repository, thanks to the Sling installer which picks them up there, you can find a number of those in folders named "config" under /libs and /apps in your CQ5 repository.
Another option is to use JCR content directly, depending on how your configurable parameters are used. You could tell your component that its config is under /apps/foo/myparameters in the repository (and make just that value configurable), and add JCR properties and child nodes under that node as needed, that your component can read. The disadvantage is that your #Component won't be restarted automatically when parameters change, as happens when using OSGi configurations directly.
Long explanation...hope this helps ;-)
Thanks a lot to Bertrand, your answer really pointed me in the right direction.
What I did was I created .ConfigService.xml for each of my ran modes, which looks like that:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OsgiConfig"
myconfig.config="{String}My Value"/>
Then in my ConfigService looked like that:
#Component(immediate = true, metatype = true)
#Service(ConfigService.class)
public class ConfigService {
private Dictionary<String, String> properties;
#SuppressWarnings("unchecked")
protected void activate(ComponentContext context) {
properties = context.getProperties();
}
protected void deactivate(ComponentContext context) {
properties = null;
}
public String getProperty(String key) {
return properties.get(key);
}
}
Than I just use ConfigService if I need to get a config property accessing it using #Reference.
I hope that can help someone!
ConfigService example may not be the best approach since the ComponentContext should only be depended upon during component activation and deactivation.

Resources