Spring Boot - inyectar mapa desde application.yml

99

Tengo una aplicación Spring Boot con lo siguiente application.yml, tomado básicamente de aquí :

info:
   build:
      artifact: ${project.artifactId}
      name: ${project.name}
      description: ${project.description}
      version: ${project.version}

Puedo inyectar valores particulares, p. Ej.

@Value("${info.build.artifact}") String value

Sin embargo, me gustaría inyectar todo el mapa, es decir, algo como esto:

@Value("${info}") Map<String, Object> info

¿Es eso (o algo similar) posible? Obviamente, puedo cargar yaml directamente, pero me preguntaba si Spring ya admite algo.

levant pied
fuente

Respuestas:

71

Puede inyectar un mapa usando @ConfigurationProperties:

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
@EnableConfigurationProperties
public class MapBindingSample {

    public static void main(String[] args) throws Exception {
        System.out.println(SpringApplication.run(MapBindingSample.class, args)
                .getBean(Test.class).getInfo());
    }

    @Bean
    @ConfigurationProperties
    public Test test() {
        return new Test();
    }

    public static class Test {

        private Map<String, Object> info = new HashMap<String, Object>();

        public Map<String, Object> getInfo() {
            return this.info;
        }
    }
}

Ejecutar esto con el yaml en la pregunta produce:

{build={artifact=${project.artifactId}, version=${project.version}, name=${project.name}, description=${project.description}}}

Hay varias opciones para establecer un prefijo, controlar cómo se manejan las propiedades que faltan, etc. Consulte el javadoc para obtener más información.

Andy Wilkinson
fuente
Gracias Andy, esto funciona como se esperaba. Es interesante que no funcione sin una clase adicional, es decir, no puede colocar el infomapa dentro MapBindingSamplepor alguna razón (tal vez porque se está utilizando para ejecutar la aplicación en la SpringApplication.runllamada).
levant pied
1
¿Hay alguna forma de inyectar un submapa? Por ejemplo, ¿inyectar en info.buildlugar de infodesde el mapa anterior?
levant pied
1
Si. Establezca el prefijo en @ConfigurationProperties en info y luego actualice Test reemplazando getInfo () con un método llamado getBuild ()
Andy Wilkinson
Bien, gracias Andy, ¡funcionó como un encanto! Una cosa más: al configurar locations(obtener las propiedades de otro ymlarchivo en lugar de las predeterminadas application.yml) @ConfigurationProperties, funcionó, excepto que no resultó en el reemplazo de los marcadores de posición. Por ejemplo, si tuviera un project.version=123conjunto de propiedades del sistema, el ejemplo que dio en la respuesta volvería version=123, mientras que después de establecerlo locationsvolvería project.version=${project.version}. ¿Sabes si hay algún tipo de limitación aquí?
levant pied
Esa es una limitación. Abrí un problema ( github.com/spring-projects/spring-boot/issues/1301 ) para realizar el reemplazo del marcador de posición cuando usa una ubicación personalizada
Andy Wilkinson
108

La siguiente solución es una abreviatura de la solución de @Andy Wilkinson, excepto que no tiene que usar una clase separada o un @Beanmétodo anotado.

application.yml:

input:
  name: raja
  age: 12
  somedata:
    abcd: 1 
    bcbd: 2
    cdbd: 3

SomeComponent.java:

@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "input")
class SomeComponent {

    @Value("${input.name}")
    private String name;

    @Value("${input.age}")
    private Integer age;

    private HashMap<String, Integer> somedata;

    public HashMap<String, Integer> getSomedata() {
        return somedata;
    }

    public void setSomedata(HashMap<String, Integer> somedata) {
        this.somedata = somedata;
    }

}

Podemos club tanto con @Valueanotaciones como @ConfigurationPropertiessin problemas. Pero los captadores y los setters son importantes y @EnableConfigurationPropertieses imprescindible tener la @ConfigurationPropertiescapacidad para funcionar.

Probé esta idea de la maravillosa solución proporcionada por @Szymon Stepniak, pensé que sería útil para alguien.

raksja
fuente
11
¡Gracias! @EnableConfigurationProperties
Usé
Recibo un error de 'constante de carácter no válido' cuando uso esta respuesta. ¿Puede cambiar: @ConfigurationProperties (prefix = 'input') para usar comillas dobles para evitar este error.
Anton Rand
10
Buena respuesta, pero las anotaciones de @Value no son necesarias.
Robin
3
En lugar de escribir el getter & setter ficticio, puede usar las anotaciones de Lombok @Setter (AccessLevel.PUBLIC) y @Getter (AccessLevel.PUBLIC)
RiZKiT
Genial. Tenga en cuenta que la configuración también se puede anidar: Map <String, Map <String, String >>
Máthé Endre-Botond
16

Hoy me encuentro con el mismo problema, pero desafortunadamente la solución de Andy no funcionó para mí. En Spring Boot 1.2.1.RELEASE es aún más fácil, pero debes tener en cuenta algunas cosas.

Aquí está la parte interesante de mi application.yml:

oauth:
  providers:
    google:
     api: org.scribe.builder.api.Google2Api
     key: api_key
     secret: api_secret
     callback: http://callback.your.host/oauth/google

providersmap contiene solo una entrada de mapa, mi objetivo es proporcionar una configuración dinámica para otros proveedores de OAuth. Quiero inyectar este mapa en un servicio que inicializará los servicios según la configuración proporcionada en este archivo yaml. Mi implementación inicial fue:

@Service
@ConfigurationProperties(prefix = 'oauth')
class OAuth2ProvidersService implements InitializingBean {

    private Map<String, Map<String, String>> providers = [:]

    @Override
    void afterPropertiesSet() throws Exception {
       initialize()
    }

    private void initialize() {
       //....
    }
}

Después de iniciar la aplicación, el providersmapa OAuth2ProvidersServiceno se inicializó. Probé la solución sugerida por Andy, pero no funcionó tan bien. Yo uso Groovy en esa aplicación, así que decidí eliminar privatey dejar que Groovy genere getter y setter. Entonces mi código se veía así:

@Service
@ConfigurationProperties(prefix = 'oauth')
class OAuth2ProvidersService implements InitializingBean {

    Map<String, Map<String, String>> providers = [:]

    @Override
    void afterPropertiesSet() throws Exception {
       initialize()
    }

    private void initialize() {
       //....
    }
}

Después de ese pequeño cambio, todo funcionó.

Aunque hay una cosa que vale la pena mencionar. Después de hacerlo funcionar, decidí crear este campo privatey proporcionar al setter un tipo de argumento directo en el método setter. Desafortunadamente, eso no funcionará. Causa org.springframework.beans.NotWritablePropertyExceptioncon mensaje:

Invalid property 'providers[google]' of bean class [com.zinvoice.user.service.OAuth2ProvidersService]: Cannot access indexed value in property referenced in indexed property path 'providers[google]'; nested exception is org.springframework.beans.NotReadablePropertyException: Invalid property 'providers[google]' of bean class [com.zinvoice.user.service.OAuth2ProvidersService]: Bean property 'providers[google]' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?

Téngalo en cuenta si está utilizando Groovy en su aplicación Spring Boot.

Szymon Stepniak
fuente
15

Para recuperar el mapa de la configuración, necesitará la clase de configuración. La anotación @Value no funcionará, desafortunadamente.

Application.yml

entries:
  map:
     key1: value1
     key2: value2

Clase de configuración:

@Configuration
@ConfigurationProperties("entries")
@Getter
@Setter
 public static class MyConfig {
     private Map<String, String> map;
 }
Orbita
fuente
probado la solución anterior funciona contra la versión 2.1.0
Tugrul ASLAN
6

Solución para extraer el mapa usando @Value de la propiedad application.yml codificada como multilínea

application.yml

other-prop: just for demo 

my-map-property-name: "{\
         key1: \"ANY String Value here\", \  
         key2: \"any number of items\" , \ 
         key3: \"Note the Last item does not have comma\" \
         }"

other-prop2: just for demo 2 

Aquí el valor de nuestra propiedad de mapa "my-map-property-name" se almacena en formato JSON dentro de una cadena y hemos obtenido varias líneas usando \ al final de la línea

myJavaClass.java

import org.springframework.beans.factory.annotation.Value;

public class myJavaClass {

@Value("#{${my-map-property-name}}") 
private Map<String,String> myMap;

public void someRandomMethod (){
    if(myMap.containsKey("key1")) {
            //todo...
    } }

}

Mas explicacion

  • \ en yaml se usa para dividir la cadena en varias líneas

  • \ " es el carácter de escape para" (cita) en la cadena yaml

  • {clave: valor} JSON en yaml que será convertido a mapa por @Value

  • # {} es expresión SpEL y se puede usar en @Value para convertir json int Map o Array / list Reference

Probado en un proyecto de arranque de primavera

Milán
fuente
3
foo.bars.one.counter=1
foo.bars.one.active=false
foo.bars[two].id=IdOfBarWithKeyTwo

public class Foo {

  private Map<String, Bar> bars = new HashMap<>();

  public Map<String, Bar> getBars() { .... }
}

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding

emerson moura
fuente
7
¡Bienvenido a Stack Overflow! Si bien este fragmento de código puede resolver la pregunta, incluir una explicación realmente ayuda a mejorar la calidad de su publicación. Recuerde que está respondiendo a la pregunta para los lectores en el futuro, y es posible que esas personas no conozcan los motivos de su sugerencia de código.
Scott Weldon
Sin embargo, el enlace a la wiki es valioso. La explicación está en github.com/spring-projects/spring-boot/wiki/…
dschulten
1

Puede hacerlo aún más simple, si desea evitar estructuras adicionales.

service:
  mappings:
    key1: value1
    key2: value2
@Configuration
@EnableConfigurationProperties
public class ServiceConfigurationProperties {

  @Bean
  @ConfigurationProperties(prefix = "service.mappings")
  public Map<String, String> serviceMappings() {
    return new HashMap<>();
  }

}

Y luego úselo como de costumbre, por ejemplo con un constructor:

public class Foo {

  private final Map<String, String> serviceMappings;

  public Foo(Map<String, String> serviceMappings) {
    this.serviceMappings = serviceMappings;
  }

}
Alexander Korolev
fuente