Spring: acceda a todas las propiedades del entorno como un objeto Mapa o Propiedades

84

Estoy usando anotaciones para configurar mi entorno de primavera de esta manera:

@Configuration
...
@PropertySource("classpath:/config/default.properties")
...
public class GeneralApplicationConfiguration implements WebApplicationInitializer 
{
    @Autowired
    Environment env;
}

Esto lleva a que mis propiedades default.propertiesformen parte del Environment. Quiero usar el @PropertySourcemecanismo aquí, porque ya brinda la posibilidad de sobrecargar propiedades a través de varias capas de respaldo y diferentes ubicaciones dinámicas, según la configuración del entorno (por ejemplo, config_dir location). Simplemente eliminé la alternativa para facilitar el ejemplo.

Sin embargo, mi problema ahora es que quiero configurar, por ejemplo, las propiedades de mi fuente de datos en default.properties. Puede pasar la configuración a la fuente de datos sin saber en detalle qué configuración espera usar la fuente de datos

Properties p = ...
datasource.setProperties(p);

Sin embargo, el problema es que el Environmentobjeto no es ni un Propertiesobjeto Mapni nada comparable. Desde mi punto de vista, es simplemente no es posible acceder a todos los valores del medio ambiente, ya que no hay keySeto iteratormétodo o algo comparable.

Properties p <=== Environment env?

¿Me estoy perdiendo de algo? ¿Es posible acceder a todas las entradas del Environmentobjeto de alguna manera? Si es así, podría asignar las entradas a un objeto Mapu Properties, incluso podría filtrarlas o asignarlas por prefijo: crear subconjuntos como un java estándar Map... Esto es lo que me gustaría hacer. ¿Alguna sugerencia?

RoK
fuente

Respuestas:

72

Necesitas algo como esto, tal vez se pueda mejorar. Este es un primer intento:

...
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
...

@Configuration
...
@org.springframework.context.annotation.PropertySource("classpath:/config/default.properties")
...
public class GeneralApplicationConfiguration implements WebApplicationInitializer 
{
    @Autowired
    Environment env;

    public void someMethod() {
        ...
        Map<String, Object> map = new HashMap();
        for(Iterator it = ((AbstractEnvironment) env).getPropertySources().iterator(); it.hasNext(); ) {
            PropertySource propertySource = (PropertySource) it.next();
            if (propertySource instanceof MapPropertySource) {
                map.putAll(((MapPropertySource) propertySource).getSource());
            }
        }
        ...
    }
...

Básicamente, MapPropertySourcese puede acceder a todo, desde el entorno que es (y hay muchas implementaciones) como una Mapde las propiedades.

Andrei Stefan
fuente
Gracias por compartir este enfoque. Considero que esto es un poco "sucio", pero probablemente sea la única manera de hacerlo. Otro enfoque que me mostró un colega sería poner una propiedad en la configuración usando una clave fija que contiene una lista con todas las claves de propiedad. A continuación, puede leer las propiedades en un objeto Mapa / Propiedades basado en la lista de claves. Eso al menos evitaría los lanzamientos ...
RoK
20
Nota para el arranque de primavera ... que getPropertySources () devuelve PropertySource en el orden de precedencia, por lo que es necesario revertirlo en los casos en que los valores de propiedad se sobrescriben
Rob Bygrave
2
Como @RobBygrave mencionó, el orden podría ser diferente, pero en lugar de revertir el orden (ya que puede implementar el arranque de primavera en el contenedor como guerra o este comportamiento puede cambiar en el futuro), simplemente recopilaría todas las claves y luego las usaría applicationContext.getEnvironment().getProperty(key)para resolverlas
papa
@potato Esa es una buena idea, y lo intenté. El único problema potencial es que se encuentra con problemas de evaluación con marcadores de posición, como en esta pregunta aquí: stackoverflow.com/questions/34584498/…
bischoje
1
¡Gracias! .. Estaba buscando una alternativa de primavera para usar en lugar de org.apache.ibatis.io.Resources.getResourceAsProperties ("Filepath") Esta solución funcionó muy bien para mí.
so-random-dude
67

Esta es una pregunta antigua, pero la respuesta aceptada tiene un grave defecto. Si el Environmentobjeto Spring contiene algún valor primordial (como se describe en Configuración externa ), no hay garantía de que el mapa de valores de propiedad que produce coincida con los devueltos por el Environmentobjeto. Descubrí que simplemente iterar a través de la PropertySources de la Environmentno, de hecho, daba valores primordiales. En su lugar, produjo el valor original, el que debería haberse anulado.

Aquí tienes una mejor solución. Esto usa la EnumerablePropertySources de Environmentpara iterar a través de los nombres de propiedad conocidos, pero luego lee el valor real del entorno Spring real. Esto garantiza que el valor es el que realmente resolvió Spring, incluidos los valores primarios.

Properties props = new Properties();
MutablePropertySources propSrcs = ((AbstractEnvironment) springEnv).getPropertySources();
StreamSupport.stream(propSrcs.spliterator(), false)
        .filter(ps -> ps instanceof EnumerablePropertySource)
        .map(ps -> ((EnumerablePropertySource) ps).getPropertyNames())
        .flatMap(Arrays::<String>stream)
        .forEach(propName -> props.setProperty(propName, springEnv.getProperty(propName)));
pedorro
fuente
1
Vale la pena señalar que a partir de Spring 4.1.2, esta solución (a diferencia de las otras respuestas) no necesita actualizarse para tratar explícitamente con CompositePropertySource, ya que CompositePropertySource extiende EnumerablePropertySource y, por lo tanto, getPropertyNames devolverá el conjunto de todos los nombres de propiedad en el compuesto fuente.
M. Justin
5
También podría recopilar las propiedades utilizando la incorporada en el collectmétodo de la corriente en vez de hacer un forEach: .distinct().collect(Collectors.toMap(Function.identity(), springEnv::getProperty)). Si necesita recopilarlo en Propiedades en lugar de en Mapa, puede usar la versión de cuatro argumentos de collect.
M. Justin
2
¿Qué es springEnv? ¿De dónde viene? ¿Difiere de la envsolución aceptada?
sebnukem
2
@sebnukem Buen punto. springEnves el envobjeto de la pregunta original y la solución aceptada. Supongo que debería haber mantenido el mismo nombre.
pedorro
3
Podrías usar ConfigurableEnvironment y no tener que hacer el yeso.
Abhijit Sarkar
19

Tenía el requisito de recuperar todas las propiedades cuya clave comienza con un prefijo distinto (por ejemplo, todas las propiedades que comienzan con "log4j.appender") y escribí el siguiente código (usando streams y lamdas de Java 8).

public static Map<String,Object> getPropertiesStartingWith( ConfigurableEnvironment aEnv,
                                                            String aKeyPrefix )
{
    Map<String,Object> result = new HashMap<>();

    Map<String,Object> map = getAllProperties( aEnv );

    for (Entry<String, Object> entry : map.entrySet())
    {
        String key = entry.getKey();

        if ( key.startsWith( aKeyPrefix ) )
        {
            result.put( key, entry.getValue() );
        }
    }

    return result;
}

public static Map<String,Object> getAllProperties( ConfigurableEnvironment aEnv )
{
    Map<String,Object> result = new HashMap<>();
    aEnv.getPropertySources().forEach( ps -> addAll( result, getAllProperties( ps ) ) );
    return result;
}

public static Map<String,Object> getAllProperties( PropertySource<?> aPropSource )
{
    Map<String,Object> result = new HashMap<>();

    if ( aPropSource instanceof CompositePropertySource)
    {
        CompositePropertySource cps = (CompositePropertySource) aPropSource;
        cps.getPropertySources().forEach( ps -> addAll( result, getAllProperties( ps ) ) );
        return result;
    }

    if ( aPropSource instanceof EnumerablePropertySource<?> )
    {
        EnumerablePropertySource<?> ps = (EnumerablePropertySource<?>) aPropSource;
        Arrays.asList( ps.getPropertyNames() ).forEach( key -> result.put( key, ps.getProperty( key ) ) );
        return result;
    }

    // note: Most descendants of PropertySource are EnumerablePropertySource. There are some
    // few others like JndiPropertySource or StubPropertySource
    myLog.debug( "Given PropertySource is instanceof " + aPropSource.getClass().getName()
                 + " and cannot be iterated" );

    return result;

}

private static void addAll( Map<String, Object> aBase, Map<String, Object> aToBeAdded )
{
    for (Entry<String, Object> entry : aToBeAdded.entrySet())
    {
        if ( aBase.containsKey( entry.getKey() ) )
        {
            continue;
        }

        aBase.put( entry.getKey(), entry.getValue() );
    }
}

Tenga en cuenta que el punto de partida es el ConfigurableEnvironment que puede devolver los PropertySources incrustados (el ConfigurableEnvironment es un descendiente directo de Environment). Puede cablearlo automáticamente mediante:

@Autowired
private ConfigurableEnvironment  myEnv;

Si no usa tipos muy especiales de fuentes de propiedades (como JndiPropertySource, que generalmente no se usa en la configuración automática de primavera), puede recuperar todas las propiedades que se encuentran en el entorno.

La implementación se basa en el orden de iteración que proporciona el propio resorte y toma la primera propiedad encontrada, todas las propiedades encontradas posteriormente con el mismo nombre se descartan. Esto debería garantizar el mismo comportamiento que si al entorno se le pidiera directamente una propiedad (devolviendo la primera encontrada).

Tenga en cuenta también que las propiedades devueltas aún no se resuelven si contienen alias con el operador $ {...}. Si desea que se resuelva una clave en particular, debe preguntar al entorno directamente nuevamente:

myEnv.getProperty( key );
Heri
fuente
1
¿Por qué no simplemente descubrir todas las claves de esta manera y luego usar environment.getProperty para forzar la resolución de valores adecuada? Quisiera asegurarse de que se respeten las anulaciones del entorno, por ejemplo, application-dev.properties anula un valor predeterminado en application.properties y, como mencionó, eval de marcador de posición.
GameSalutes
Eso es lo que señalé en el último párrafo. El uso de env.getProperty asegura el comportamiento original de Spring.
Heri
¿Cómo prueba esto por unidad? Siempre obtengo un NullPointerExceptionen mis pruebas unitarias cuando intenta obtener la @Autowiredinstancia de ConfigurationEnvironment.
ArtOfWarfare
¿Está seguro de ejecutar la prueba como aplicación de primavera?
Heri
Lo hago así:
Heri
10

La pregunta original insinuaba que sería bueno poder filtrar todas las propiedades según un prefijo. Acabo de confirmar que esto funciona a partir de Spring Boot 2.1.1.RELEASE, para Properties o Map<String,String> . Estoy seguro de que ha funcionado durante un tiempo. Curiosamente, no funciona sin la prefix =calificación, es decir, no sé cómo cargar todo el entorno en un mapa. Como dije, esto podría ser realmente con lo que OP quería comenzar. El prefijo y el siguiente '.' será despojado, que podría ser o no lo que uno quiere:

@ConfigurationProperties(prefix = "abc")
@Bean
public Properties getAsProperties() {
    return new Properties();
}

@Bean
public MyService createService() {
    Properties properties = getAsProperties();
    return new MyService(properties);
}

Posdata: De hecho, es posible, y vergonzosamente fácil, obtener todo el entorno. No sé cómo se me escapó esto:

@ConfigurationProperties
@Bean
public Properties getProperties() {
    return new Properties();
}
AbuNassar
fuente
1
Además, propiedades como abc = x se anidan en {b = {c = x}}
weberjn
Ninguna parte de esto funcionó: getAsProperties()siempre devuelve una Propertiesinstancia vacía , e intentarlo sin un prefijo especificado ni siquiera permite que se compile. Esto es con Spring Boot 2.1.6.RELEASE
ArtOfWarfare
1
No estoy escribiendo Java en el trabajo, pero lo preparé bastante rápido: github.com/AbuCarlo/SpringPropertiesBean . Es posible que no funcione si de alguna manera elude la secuencia de inicio de Spring (es decir, el bean de "propiedades" nunca se completa). Esto es para Java 8, Spring 2.2.6.
AbuNassar
5

Como el boleto de Jira de esta primavera , es un diseño intencional. Pero el siguiente código funciona para mí.

public static Map<String, Object> getAllKnownProperties(Environment env) {
    Map<String, Object> rtn = new HashMap<>();
    if (env instanceof ConfigurableEnvironment) {
        for (PropertySource<?> propertySource : ((ConfigurableEnvironment) env).getPropertySources()) {
            if (propertySource instanceof EnumerablePropertySource) {
                for (String key : ((EnumerablePropertySource) propertySource).getPropertyNames()) {
                    rtn.put(key, propertySource.getProperty(key));
                }
            }
        }
    }
    return rtn;
}
Jasonleakey
fuente
2

Spring no permitirá desvincularse java.util.Propertiesde Spring Environment.

Pero Properties.load()todavía funciona en una aplicación de arranque Spring:

Properties p = new Properties();
try (InputStream is = getClass().getResourceAsStream("/my.properties")) {
    p.load(is);
}
Weberjn
fuente
1

Las otras respuestas han señalado la solución para la mayoría de los casos que involucran PropertySources, pero ninguna ha mencionado que ciertas fuentes de propiedad no se pueden convertir en tipos útiles.

Un ejemplo es la fuente de propiedades para argumentos de línea de comando. La clase que se utiliza es SimpleCommandLinePropertySource. Esta clase privada es devuelta por un método público , por lo que es extremadamente complicado acceder a los datos dentro del objeto. Tuve que usar la reflexión para leer los datos y eventualmente reemplazar la fuente de la propiedad.

Si alguien tiene una solución mejor, realmente me gustaría verla; sin embargo, este es el único truco en el que me he puesto a trabajar.

Chad Van De Hey
fuente
¿Ha encontrado una solución para el problema con la clase privada?
Tobias
1

Trabajando con Spring Boot 2, necesitaba hacer algo similar. La mayoría de las respuestas anteriores funcionan bien, solo tenga en cuenta que en varias fases de los ciclos de vida de la aplicación los resultados serán diferentes.

Por ejemplo, después de una, ApplicationEnvironmentPreparedEventlas propiedades del interior application.propertiesno están presentes. Sin embargo, después de un ApplicationPreparedEventevento lo son.

Miguel
fuente
1

Para Spring Boot, la respuesta aceptada sobrescribirá las propiedades duplicadas con las de menor prioridad . Esta solución recopilará las propiedades en a SortedMapy tomará solo las propiedades duplicadas de mayor prioridad.

final SortedMap<String, String> sortedMap = new TreeMap<>();
for (final PropertySource<?> propertySource : env.getPropertySources()) {
    if (!(propertySource instanceof EnumerablePropertySource))
        continue;
    for (final String name : ((EnumerablePropertySource<?>) propertySource).getPropertyNames())
        sortedMap.computeIfAbsent(name, propertySource::getProperty);
}
Jeff Brower
fuente
env.getPropertySources () da propiedades de menor a mayor prioridad?
Faraz
Es al revés. Están ordenados de alta a baja prioridad.
Samuel Tatipamula
0

Pensé que agregaría una forma más. En mi caso proporciono esto al com.hazelcast.config.XmlConfigBuilderque solo necesita java.util.Propertiesresolver algunas propiedades dentro del archivo de configuración XML de Hazelcast, es decir, solo llama al getProperty(String)método. Entonces, esto me permitió hacer lo que necesitaba:

@RequiredArgsConstructor
public class SpringReadOnlyProperties extends Properties {

  private final org.springframework.core.env.Environment delegate;

  @Override
  public String getProperty(String key) {
    return delegate.getProperty(key);
  }

  @Override
  public String getProperty(String key, String defaultValue) {
    return delegate.getProperty(key, defaultValue);
  }

  @Override
  public synchronized String toString() {
    return getClass().getName() + "{" + delegate + "}";
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;
    SpringReadOnlyProperties that = (SpringReadOnlyProperties) o;
    return delegate.equals(that.delegate);
  }

  @Override
  public int hashCode() {
    return Objects.hash(super.hashCode(), delegate);
  }

  private void throwException() {
    throw new RuntimeException("This method is not supported");
  }

  //all methods below throw the exception

  * override all methods *
}

PD: Terminé no usando esto específicamente para Hazelcast porque solo resuelve propiedades para archivos XML pero no en tiempo de ejecución. Como también uso Spring, decidí usar un archivo org.springframework.cache.interceptor.AbstractCacheResolver#getCacheNames. Esto resuelve propiedades para ambas situaciones, al menos si usa propiedades en nombres de caché.

Sam
fuente