简体   繁体   中英

Post-processing YAML properties in Spring based on prefix

I have a Spring boot configuration YAML with something like

spring:
  application:
    name: my-app
a: this is literal
b: <<this is external due to special first and last chars>>

What i'm trying to do is to add some kind of resolver that will detect that the value of b is of the form <<X>> and will trigger retrieving that value from an external rest api to overwrite in memory the value that was in the YAML before it gets passed to the bean that holds the configurations at runtime

I tried and failed using an EnvironmentPostProcessor because I can't get ahold of the actual property values, just the property sources , so I can't post-process the values.

What currently works for me is in the @Configuration bean that holds the fields a and b , implement something in the setters to detect if the value that spring is trying to set starts with << and ends with >> and if so, overwrite what gets loaded into the pojo with the version that i retrieve from the rest api. This is not ideal because I end up with a lot of duplication

What's the right way to implement something like this in Spring 5? I know spring properties support references to other properties using the syntax ${a} so there must be some mechanism that already allows to add custom placeholder resolvers

不知道正确的方法,但从REST调用获取属性的一种方法是实现自己的PropertySource ,它获取(并缓存?)特定命名属性的值。

Here is an hacky solution I came up with using Spring Boot 2.1.5. Probably better to use a custom PropertyResolver

Essentially it goes like:

  1. Grab the PropertySource I care about. For this case it's application.properties . Applications can have N number of sources so if there are other places where << >> can occur, then you'll to check them as well.
  2. Loop through the source's values for << >>
  3. Dynamically replace the value if match.

My properties are:

a=hello from a
b=<<I need special attention>>

My hacked ApplicationListener is:

import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

public class EnvironmentPrepareListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {

    private final RestTemplate restTemplate = new RestTemplate();

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        // Only focused main application.properties (or yml) configuration
        // Loop through sources to figure out name
        final String propertySourceName = "applicationConfig: [classpath:/application.properties]";
        PropertySource<?> propertySource = event.getEnvironment().getPropertySources()
                .get(propertySourceName);

        Map<String, Object> source = ((OriginTrackedMapPropertySource) propertySource).getSource();
        Map<String, Object> myUpdatedProps = new HashMap<>();
        final String url = "https://jsonplaceholder.typicode.com/todos/1";

        for (Map.Entry<String, Object> entry : source.entrySet()) {
            if (isDynamic(entry.getValue())) {
                String updatedValue = restTemplate.getForEntity(url, String.class).getBody();
                myUpdatedProps.put(entry.getKey(), updatedValue);
            }
        }

        if (!myUpdatedProps.isEmpty()) {
            event.getEnvironment().getPropertySources()
                    .addBefore(
                            propertySourceName,
                            new MapPropertySource("myUpdatedProps", myUpdatedProps)
                    );
        }
    }

    private boolean isDynamic(Object value) {
        return StringUtils.startsWith(value.toString(), "<<")
                && StringUtils.endsWith(value.toString(), ">>");
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

Hitting /test yields me:

{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }

I ended up changing things a bit to mark the special properties. Then I created my own PropertySource kind of like @Andreas suggested. It was all inspired by org.springframework.boot.env.RandomValuePropertySource

The trick was changing the special chars << and >> to the syntax used already by spring: ${} , but like the random resolver which uses ${random.int } I did something like ${rest.XXX} . What I didn't know before is that by doing that, Spring will invoke all the property sources a second time with a new property name coming from the placeholder value ( rest.XXX in my previous example). This way in the property source I can handle the value if the name of the property starts with my prefix rest.

Here is a simplified version of my solution

public class MyPropertySource extends PropertySource<RestTemplate> {
  private static final String PREFIX = "rest.";

  public MyPropertySource() {
    super(MyPropertySource.class.getSimpleName());
  }

  @Override
  public Object getProperty(@Nonnull String name) {
    String result = null;
    if (name.startsWith(PREFIX)) {
        result = getValueFromRest(name.substring(PREFIX.length()));
    }

    return result;
  }
}

Finally, to register the property source I used an EnvironmentPostProcessor as described here . I couldn't find a simpler way that doesn't entail maintaining a new file META-INF/spring.factories

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM