From 1f675c026f08e634e7e821a38d2b7aaf90e9fff4 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 30 Oct 2015 09:44:27 +0000 Subject: [PATCH] Add support for inline JSON in SPRING_APPLICATION_JSON User can supply inline JSON as an env var (SPRING_APPLICATION_JSON) or System property (spring.application.json). Fixes gh-4239 --- .../main/asciidoc/spring-boot-features.adoc | 28 ++++ ...plicationJsonEnvironmentPostProcessor.java | 140 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- ...tionJsonEnvironmentPostProcessorTests.java | 98 ++++++++++++ 4 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/env/SpringApplicationJsonEnvironmentPostProcessor.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/env/SpringApplicationJsonEnvironmentPostProcessorTests.java diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 33ac70dbae..a187ed4a8e 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -291,6 +291,7 @@ Spring Boot uses a very particular `PropertySource` order that is designed to al 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. @@ -333,6 +334,33 @@ default `name`. When running in production, an `application.properties` can be p outside of your jar that overrides `name`; and for one-off testing, you can launch with a specific command line switch (e.g. `java -jar app.jar --name="Spring"`). +[TIP] +==== + +The `SPRING_APPLICATION_JSON` properties can be supplied on the +command line with an environment variable. For example in a +UN{asterisk}X shell: + +---- +$ SPRING_APPLICATION_JSON='{"foo":{"bar":"spam"}}' java -jar myapp.jar +---- + +In this example you will end up with `foo.bar=spam` in the Spring +`Environment`. You can also supply the JSON as +`spring.application.json` in a System variable: + +---- +$ java -Dspring.application.json='{"foo":"bar"}' -jar myapp.jar +---- + +or command line argument: + +---- +$ java -jar myapp.jar --spring.application.json='{"foo":"bar"}' +---- + +or as a JNDI variable `java:comp/env/spring.application.json`. +==== [[boot-features-external-config-random-values]] diff --git a/spring-boot/src/main/java/org/springframework/boot/env/SpringApplicationJsonEnvironmentPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/env/SpringApplicationJsonEnvironmentPostProcessor.java new file mode 100644 index 0000000000..b5417190f3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/env/SpringApplicationJsonEnvironmentPostProcessor.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.env; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.json.JsonParser; +import org.springframework.boot.json.JsonParserFactory; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.context.support.StandardServletEnvironment; + +/** + * An {@link EnvironmentPostProcessor} that parses JSON from + * spring.application.json or equivalently + * {@link SpringApplicationJsonEnvironmentPostProcessor} and adds it as a map property + * source to the {@link Environment}. The new properties are added with higher priority + * than the system properties. + * + * @author Dave Syer + */ +public class SpringApplicationJsonEnvironmentPostProcessor + implements EnvironmentPostProcessor, Ordered { + + private static final Log logger = LogFactory + .getLog(SpringApplicationJsonEnvironmentPostProcessor.class); + + /** + * The default order for the processor. + */ + public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 20; + + private int order = DEFAULT_ORDER; + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, + SpringApplication application) { + String json = environment.resolvePlaceholders( + "${spring.application.json:${SPRING_APPLICATION_JSON:}}"); + if (StringUtils.hasText(json)) { + try { + JsonParser parser = JsonParserFactory.getJsonParser(); + Map map = parser.parseMap(json); + if (!map.isEmpty()) { + MapPropertySource source = new MapPropertySource( + "spring.application.json", flatten(map)); + MutablePropertySources sources = environment.getPropertySources(); + String name = findPropertySource(sources); + if (sources.contains(name)) { + sources.addBefore(name, source); + } + else { + sources.addFirst(source); + } + } + } + catch (Exception e) { + logger.warn("Cannot parse JSON for spring.application.json: " + json, e); + } + } + } + + /** + * Flatten the map keys using period separator. + */ + private Map flatten(Map map) { + Map result = new LinkedHashMap(); + flatten(null, result, map); + return result; + } + + private void flatten(String prefix, Map result, + Map map) { + if (prefix == null) { + prefix = ""; + } + else { + prefix = prefix + "."; + } + for (String key : map.keySet()) { + String name = prefix + key; + Object value = map.get(key); + if (value instanceof Map) { + @SuppressWarnings("unchecked") + Map nested = (Map) value; + flatten(name, result, nested); + } + else { + result.put(name, value); + } + } + } + + private String findPropertySource(MutablePropertySources sources) { + if (ClassUtils.isPresent( + "org.springframework.web.context.support.StandardServletEnvironment", + null) + && sources + .contains(StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME)) { + return StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME; + + } + return StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME; + } + +} diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index cf6e8ae70d..aed00315ca 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -27,4 +27,5 @@ org.springframework.boot.logging.LoggingApplicationListener # Environment Post Processors org.springframework.boot.env.EnvironmentPostProcessor=\ -org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor +org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\ +org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor diff --git a/spring-boot/src/test/java/org/springframework/boot/env/SpringApplicationJsonEnvironmentPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/env/SpringApplicationJsonEnvironmentPostProcessorTests.java new file mode 100644 index 0000000000..9ce7c320cf --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/env/SpringApplicationJsonEnvironmentPostProcessorTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.env; + +import org.junit.Test; + +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +public class SpringApplicationJsonEnvironmentPostProcessorTests { + + private SpringApplicationJsonEnvironmentPostProcessor processor = new SpringApplicationJsonEnvironmentPostProcessor(); + + private ConfigurableEnvironment environment = new StandardEnvironment(); + + @Test + public void error() { + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.application.json=foo:bar"); + this.processor.postProcessEnvironment(this.environment, null); + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + } + + @Test + public void missing() { + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + this.processor.postProcessEnvironment(this.environment, null); + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + } + + @Test + public void empty() { + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.application.json={}"); + this.processor.postProcessEnvironment(this.environment, null); + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + } + + @Test + public void periodSeparated() { + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.application.json={\"foo\":\"bar\"}"); + this.processor.postProcessEnvironment(this.environment, null); + assertEquals("bar", this.environment.resolvePlaceholders("${foo:}")); + } + + @Test + public void envVar() { + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + EnvironmentTestUtils.addEnvironment(this.environment, + "SPRING_APPLICATION_JSON={\"foo\":\"bar\"}"); + this.processor.postProcessEnvironment(this.environment, null); + assertEquals("bar", this.environment.resolvePlaceholders("${foo:}")); + } + + @Test + public void nested() { + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + EnvironmentTestUtils.addEnvironment(this.environment, + "SPRING_APPLICATION_JSON={\"foo\":{\"bar\":\"spam\",\"rab\":\"maps\"}}"); + this.processor.postProcessEnvironment(this.environment, null); + assertEquals("spam", this.environment.resolvePlaceholders("${foo.bar:}")); + assertEquals("maps", this.environment.resolvePlaceholders("${foo.rab:}")); + } + + @Test + public void prefixed() { + assertEquals("", this.environment.resolvePlaceholders("${foo:}")); + EnvironmentTestUtils.addEnvironment(this.environment, + "SPRING_APPLICATION_JSON={\"foo.bar\":\"spam\"}"); + this.processor.postProcessEnvironment(this.environment, null); + assertEquals("spam", this.environment.resolvePlaceholders("${foo.bar:}")); + } + +}