Spring Boot - injetar mapa de application.yml

99

Eu tenho um aplicativo Spring Boot com o seguinte application.yml- tirado basicamente daqui :

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

Posso injetar valores específicos, por exemplo

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

Gostaria, no entanto, de injetar todo o mapa, ou seja, algo assim:

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

Isso (ou algo semelhante) é possível? Obviamente, posso carregar o yaml diretamente, mas gostaria de saber se há algo já suportado pelo Spring.

Levant pied
fonte

Respostas:

71

Você pode ter um mapa injetado 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;
        }
    }
}

Executar isso com o yaml na questão produz:

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

Existem várias opções para definir um prefixo, controlar como as propriedades ausentes são tratadas, etc. Consulte o javadoc para obter mais informações.

Andy Wilkinson
fonte
Obrigado Andy - isso funciona conforme o esperado. Interessante que não funciona sem uma classe extra - ou seja, você não pode colocar o infomapa dentro MapBindingSamplepor algum motivo (talvez porque ele está sendo usado para executar o aplicativo na SpringApplication.runchamada).
levant pied
1
Existe uma maneira de injetar um submapa? Por exemplo, injetar em info.buildvez do infomapa acima?
levant pied
1
Sim. Defina o prefixo em @ConfigurationProperties para info e, em seguida, atualize Test substituindo getInfo () por um método chamado getBuild ()
Andy Wilkinson
Legal, obrigado Andy, funcionou como um encanto! Mais uma coisa - ao definir locations(para obter as propriedades de outro ymlarquivo em vez do padrão application.yml) @ConfigurationProperties, funcionou, exceto que não resultou na substituição dos marcadores. Por exemplo, se você tivesse uma propriedade do sistema project.version=123definida, o exemplo que você deu na resposta retornaria version=123, enquanto após a configuração locationsretornaria project.version=${project.version}. Você sabe se há algum tipo de limitação aqui?
levant pied
Isso é uma limitação. Abri um problema ( github.com/spring-projects/spring-boot/issues/1301 ) para realizar a substituição do marcador quando você usa um local personalizado
Andy Wilkinson
108

A solução abaixo é um atalho para a solução de @Andy Wilkinson, exceto que ela não precisa usar uma classe separada ou em um @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 marcar @Valueanotações e @ConfigurationPropertiessem problemas. Mas getters e setters são importantes e @EnableConfigurationPropertiesdevem @ConfigurationPropertiesfuncionar.

Experimentei essa ideia com uma solução bacana fornecida por @Szymon Stepniak, achei que seria útil para alguém.

raksja
fonte
11
obrigado! Eu usei a bota de mola 1.3.1, no meu caso achei não precisa@EnableConfigurationProperties
zhuguowei
Recebo um erro de 'constante de caractere inválido' ao usar esta resposta. Você pode alterar: @ConfigurationProperties (prefix = 'input') para usar aspas duplas para evitar esse erro.
Anton Rand
10
Boa resposta, mas as anotações @Value não são necessárias.
Robin de
3
Em vez de escrever o getter e setter fictício, você pode usar as anotações do Lombok @Setter (AccessLevel.PUBLIC) e @Getter (AccessLevel.PUBLIC)
RiZKiT
Genioso. Observe que a configuração também pode ser aninhada: Map <String, Map <String, String >>
Máthé Endre-Botond
16

Tive o mesmo problema hoje, mas infelizmente a solução de Andy não funcionou para mim. No Spring Boot 1.2.1.RELEASE é ainda mais fácil, mas você deve estar ciente de algumas coisas.

Aqui está a parte interessante do meu 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 contém apenas uma entrada de mapa, meu objetivo é fornecer configuração dinâmica para outros provedores OAuth. Quero injetar esse mapa em um serviço que inicializará serviços com base na configuração fornecida neste arquivo yaml. Minha implementação inicial foi:

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

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

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

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

Depois de iniciar o aplicativo, o providersmap in OAuth2ProvidersServicenão foi inicializado. Tentei a solução sugerida por Andy, mas não funcionou tão bem. Eu uso o Groovy nesse aplicativo, então decidi remover privatee permitir que o Groovy gere getter e setter. Então, meu código era assim:

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

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

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

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

Depois dessa pequena mudança, tudo funcionou.

Embora haja uma coisa que vale a pena mencionar. Depois de fazê-lo funcionar, decidi criar este campo privatee fornecer ao setter o tipo de argumento direto no método setter. Infelizmente não vai funcionar assim. Isso causa org.springframework.beans.NotWritablePropertyExceptioncom a mensagem:

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?

Lembre-se disso se estiver usando o Groovy em seu aplicativo Spring Boot.

Szymon Stepniak
fonte
15

Para recuperar o mapa da configuração, você precisará da classe de configuração. A anotação @Value não resolverá o problema, infelizmente.

Application.yml

entries:
  map:
     key1: value1
     key2: value2

Classe de configuração:

@Configuration
@ConfigurationProperties("entries")
@Getter
@Setter
 public static class MyConfig {
     private Map<String, String> map;
 }
Orbite
fonte
testado, a solução acima funciona contra a versão 2.1.0
Tugrul ASLAN
6

Solução para puxar o mapa usando @Value da propriedade application.yml codificada como multilinha

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 

Aqui, o valor da propriedade do mapa "my-map-property-name" é armazenado no formato JSON dentro de uma string e nós alcançamos várias linhas usando \ no final da linha

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...
    } }

}

Mais explicação

  • \ no yaml é usado para quebrar a string em várias linhas

  • \ " é um caractere de escape para" (aspas) na string yaml

  • JSON {key: value} em yaml que será convertido em Map por @Value

  • # {} é a expressão SpEL e pode ser usado em @Value para converter json int Map ou Array / list Reference

Testado em um projeto de bota de mola

Milão
fonte
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
fonte
7
Bem-vindo ao Stack Overflow! Embora este snippet de código possa resolver a questão, incluir uma explicação realmente ajuda a melhorar a qualidade de sua postagem. Lembre-se de que você está respondendo à pergunta para leitores no futuro e essas pessoas podem não saber os motivos de sua sugestão de código.
Scott Weldon
o link para o wiki é valioso. A explicação está em github.com/spring-projects/spring-boot/wiki/…
dschulten
1

Você pode torná-lo ainda mais simples, se quiser evitar estruturas extras.

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

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

}

Em seguida, use-o normalmente, por exemplo, com um construtor:

public class Foo {

  private final Map<String, String> serviceMappings;

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

}
Alexander Korolev
fonte