001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.springboot; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.ThrowableUtils.*; 021 022import java.util.*; 023import java.util.concurrent.*; 024import java.util.function.*; 025 026import org.springframework.boot.context.properties.source.*; 027import org.springframework.core.env.*; 028 029/** 030 * Bridges a Spring {@link Environment} into the Juneau {@link org.apache.juneau.commons.settings.Settings Settings} 031 * singleton as a {@link org.apache.juneau.commons.settings.PropertySource PropertySource}. 032 * 033 * <p> 034 * Allows {@link org.apache.juneau.commons.inject.Value @Value("{spring.property.key}")} resolution 035 * against the same property sources that Spring Boot would resolve through 036 * {@code @org.springframework.beans.factory.annotation.Value}: {@code application.yaml}, 037 * {@code application.properties}, command-line args, environment variables, and any custom 038 * {@code org.springframework.core.env.PropertySource} that Spring loads. 039 * 040 * <h5 class='section'>Lifecycle:</h5> 041 * <p> 042 * {@link SpringBeanStore} registers one instance per {@link org.springframework.context.ApplicationContext} in 043 * {@link org.apache.juneau.commons.settings.Settings Settings}, shared by every store on that context. It is removed 044 * when every such store has been {@link SpringBeanStore#clear() cleared}, or when the context closes (for a 045 * {@link org.springframework.context.ConfigurableApplicationContext ConfigurableApplicationContext}). Callers that 046 * want explicit lifecycle control can construct the source directly and add it via {@code Settings.get().addSource(...)}; 047 * such instances are not shared. 048 * 049 * <h5 class='section'>Miss cache:</h5> 050 * <p> 051 * Juneau's builder keys (e.g. {@code MarshallingContext.unsortedProperties}) are not valid Spring Boot property names, 052 * so Spring walks every raw property source for each of them. This source remembers up to 4096 such keys that were 053 * absent. Every lookup checks the Environment's property-source list by identity (an added, removed or replaced 054 * source drops the cache) and re-checks {@link System#getProperty(String)} for a cached key. Valid Boot names and 055 * {@code juneau.profiles.active} are never cached, and hits are never cached. A source whose <em>contents</em> change 056 * in place (a mutated map, {@code MockEnvironment.setProperty}) is not detected for a key already cached as missing; 057 * call {@link #invalidate()} after such a change. Setting {@code juneau.spring.missCache.disable=true} in the Spring 058 * Environment before the first lookup turns the cache off. 059 * 060 * <h5 class='section'>Lazy resolution:</h5> 061 * <p> 062 * The {@code Environment} is resolved lazily on the first {@link #get(String)} call so that 063 * constructors that build the source from an {@code ApplicationContext} do not interact with the 064 * context until a lookup actually happens. This matters for tests that mock the context and assert 065 * "no interactions" until a Spring-side bean is requested. 066 * 067 * <h5 class='section'>Ordering:</h5> 068 * <p> 069 * Because {@code Settings} walks property sources in reverse insertion order, the Spring 070 * {@code Environment} bridge — registered when Spring creates the bean store — shadows any 071 * classpath-default {@code juneau.cfg} loaded earlier through the SPI-discovered 072 * {@code ConfigPropertySourceProvider}. Programmatic {@code addSource()} calls made after the 073 * Spring context is built (e.g. from a user-provided microservice {@code Config}) still win. 074 * 075 * <h5 class='section'>See Also:</h5><ul> 076 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ValueAnnotation">@Value Annotation Basics</a> 077 * <li class='jc'>{@link SpringBeanStore} 078 * </ul> 079 */ 080public class SpringEnvironmentPropertySource implements org.apache.juneau.commons.settings.PropertySource { 081 082 /** Soft cap on cached misses per bridge; same policy as J1037's source cache (copied, not referenced). */ 083 static final int MISS_CACHE_MAX_ENTRIES = 4096; 084 085 /** Opt-out switch, read once at first resolve from the bridged Environment itself (never through Settings). */ 086 private static final String MISS_CACHE_DISABLE_KEY = "juneau.spring.missCache.disable"; 087 088 private final Supplier<Environment> envSupplier; 089 090 // Written inside resolve() before resolved; readers read resolved first, so plain fields are safely published. 091 private volatile boolean resolved; 092 @SuppressWarnings({ 093 "java:S3077" // Publish-once reference: env is assigned before resolved is set (constructor or resolve()) and is never compound-mutated, so volatile visibility is sufficient. 094 }) 095 private volatile Environment env; 096 private boolean cacheEnabled; 097 098 // The miss set for the source list it was built from; replaced, never edited, when the list changes. 099 private volatile MissGeneration generation; 100 101 /** 102 * Constructor that bridges a pre-built {@link Environment}. 103 * 104 * @param env The Spring environment to bridge. Must not be <jk>null</jk>. 105 */ 106 public SpringEnvironmentPropertySource(Environment env) { 107 this.env = Objects.requireNonNull(env, "env"); 108 this.envSupplier = null; 109 } 110 111 /** 112 * Constructor that lazily resolves the {@link Environment} on first lookup. 113 * 114 * <p> 115 * Use this overload when wiring through {@link org.springframework.context.ApplicationContext}: pass 116 * {@code appContext::getEnvironment} so the application context isn't invoked until a lookup 117 * actually occurs. 118 * 119 * @param envSupplier Supplier of the Spring environment. Must not be <jk>null</jk>. May return 120 * <jk>null</jk>, in which case this source returns 121 * {@link org.apache.juneau.commons.settings.PropertyLookupResult#missing()} for every key. 122 */ 123 public SpringEnvironmentPropertySource(Supplier<Environment> envSupplier) { 124 this.envSupplier = Objects.requireNonNull(envSupplier, "envSupplier"); 125 } 126 127 private synchronized void resolve() { 128 if (resolved) // HTT: another thread resolved between the caller's check and lock acquisition; a single-threaded test cannot force it 129 return; 130 // safeOpt swallows exceptions thrown by the supplier, e.g. a mocked ApplicationContext that returns null or 131 // throws during getEnvironment(). 132 var e = nn(envSupplier) ? safeOpt(envSupplier::get).orElse(null) : env; 133 env = e; 134 cacheEnabled = e instanceof ConfigurableEnvironment && ! safeOpt(() -> e.getProperty(MISS_CACHE_DISABLE_KEY)).map(Boolean::parseBoolean).orElse(false); 135 resolved = true; 136 } 137 138 @Override 139 public org.apache.juneau.commons.settings.PropertyLookupResult get(String name) { 140 if (! resolved) 141 resolve(); 142 // Read env only after resolved: reading it first could see null, then resolved == true, and miss wrongly. 143 var e = env; 144 if (e == null) 145 return org.apache.juneau.commons.settings.PropertyLookupResult.missing(); 146 // Profile piggyback: a Juneau config-profile activation lookup resolves to Spring's active profiles, so a 147 // Spring-Boot deployment has a single source of truth for which profiles are active. Only answered when the 148 // caller has NOT explicitly set juneau.profiles.active as a property (that exact key wins if present). 149 if (eq(name, PROFILES_ACTIVE_KEY) && ! e.containsProperty(name)) { 150 var active = e.getActiveProfiles(); 151 if (active.length > 0) // getActiveProfiles() never returns null per the Environment contract. 152 return org.apache.juneau.commons.settings.PropertyLookupResult.present(o(String.join(",", active))); 153 return org.apache.juneau.commons.settings.PropertyLookupResult.missing(); 154 } 155 var g = cacheEnabled && nn(name) ? currentGeneration((ConfigurableEnvironment)e) : null; 156 if (nn(g) && g.misses.contains(name)) { 157 // For an invalid Boot name, the raw systemProperties source (exact key) is the only one whose contents 158 // can change without a source-list change; systemEnvironment wraps System.getenv(), which cannot. 159 if (! g.hasSystemProperties || System.getProperty(name) == null) 160 return org.apache.juneau.commons.settings.PropertyLookupResult.missing(); 161 g.misses.remove(name); 162 } 163 if (! e.containsProperty(name)) { 164 // Valid Boot names are never cached: Boot's relaxed binding can answer them from a changed source. 165 // isValid allocates, so it runs only here, on an uncached miss. 166 if (nn(g) && g.misses.size() < MISS_CACHE_MAX_ENTRIES && ! ConfigurationPropertyName.isValid(name)) 167 g.misses.add(name); 168 return org.apache.juneau.commons.settings.PropertyLookupResult.missing(); 169 } 170 // Spring's getProperty() returns null for a key mapped to an explicit null value, and throws 171 // IllegalArgumentException for an unresolvable nested placeholder. o() maps the null to an empty value. 172 var v = e.getProperty(name); 173 return org.apache.juneau.commons.settings.PropertyLookupResult.present(o(v)); 174 } 175 176 /** 177 * Clears this bridge's miss cache. 178 * 179 * <p> 180 * Needed only after a property source's contents change in place (for example a mutated 181 * {@code MapPropertySource} map, or {@code MockEnvironment.setProperty} in a test) for a key that is not a valid 182 * Spring Boot property name and that was already looked up and missed. Adding, removing or replacing a property 183 * source, and setting a system property, are detected without it. A lookup already in progress on another thread 184 * may still answer from the old cache; lookups that start after this method returns do not. 185 * 186 * @since 10.0.0 187 */ 188 public void invalidate() { 189 generation = null; 190 } 191 192 /** @return The number of cached misses (tests). */ 193 int missCount() { 194 var g = generation; 195 return g == null ? 0 : g.misses.size(); 196 } 197 198 private MissGeneration currentGeneration(ConfigurableEnvironment e) { 199 var live = e.getPropertySources(); 200 var g = generation; 201 if (nn(g) && g.matches(live)) 202 return g; 203 // Racing threads may each build one; the loser's misses go to an unused generation, which only costs a walk. 204 g = new MissGeneration(live); 205 generation = g; 206 return g; 207 } 208 209 /** 210 * Cached misses for one snapshot of the Environment's property-source list. A new generation replaces the old one 211 * when the list changes, so a miss computed against an old list can only land in a generation nobody reads. 212 */ 213 private static final class MissGeneration { 214 final org.springframework.core.env.PropertySource<?>[] sources; 215 final boolean hasSystemProperties; 216 final Set<String> misses = ConcurrentHashMap.newKeySet(); 217 218 MissGeneration(MutablePropertySources live) { 219 sources = live.stream().toArray(org.springframework.core.env.PropertySource<?>[]::new); 220 hasSystemProperties = Arrays.stream(sources).anyMatch(x -> StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME.equals(x.getName())); 221 } 222 223 // Identity, element by element, then count. MutablePropertySources iterates its copy-on-write array without copying. 224 boolean matches(MutablePropertySources live) { 225 var i = 0; 226 for (var s : live) { 227 if (i >= sources.length || s != sources[i]) 228 return false; 229 i++; 230 } 231 return i == sources.length; 232 } 233 } 234 235 /** The Juneau config-profile activation key that piggybacks on Spring's active profiles. */ 236 private static final String PROFILES_ACTIVE_KEY = "juneau.profiles.active"; 237}