Spring Boot - wstrzyknij mapę z pliku application.yml

99

Mam aplikację Spring Boot z następującymi application.ymlelementami - zaczerpniętymi w zasadzie stąd :

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

Potrafię wstrzyknąć określone wartości, np

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

Chciałbym jednak wstrzyknąć całą mapę czyli coś takiego:

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

Czy to (lub coś podobnego) jest możliwe? Oczywiście mogę bezpośrednio załadować yaml, ale zastanawiałem się, czy jest coś już obsługiwanego przez Spring.

levant pied
źródło

Odpowiedzi:

71

Możesz wstrzyknąć mapę za pomocą @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;
        }
    }
}

Uruchomienie tego z yamlem w pytaniu daje:

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

Istnieją różne opcje ustawiania prefiksu, kontrolowania sposobu obsługi brakujących właściwości itp. Więcej informacji można znaleźć w javadoc .

Andy Wilkinson
źródło
Dzięki Andy - to działa zgodnie z oczekiwaniami. Ciekawe, że nie działa bez dodatkowej klasy - tj. Z jakiegoś powodu nie możesz umieścić infomapy w MapBindingSampleśrodku (może dlatego, że jest używana do uruchamiania aplikacji w trakcie SpringApplication.runrozmowy).
levant pied
1
Czy istnieje sposób na wprowadzenie mapy podrzędnej? Np. Wstrzyknąć info.buildzamiast infoz powyższej mapy?
levant pied
1
Tak. Ustaw prefiks na @ConfigurationProperties na info, a następnie zaktualizuj Test zastępując getInfo () metodą o nazwie getBuild ()
Andy Wilkinson
Świetnie, dzięki Andy, działało jak urok! I jeszcze jedno - po ustawieniu locations(aby uzyskać właściwości z innego ymlpliku zamiast domyślnych application.yml) na @ConfigurationProperties, zadziałało, z wyjątkiem tego, że nie spowodowało zastąpienia symboli zastępczych. Np. Gdybyś miał project.version=123ustawioną właściwość systemową , przykład podany w odpowiedzi zwróciłby się version=123, a po ustawieniu locationswróciłby project.version=${project.version}. Czy wiesz, czy są tu jakieś ograniczenia?
levant pied
To jest ograniczenie. Otworzyłem problem ( github.com/spring-projects/spring-boot/issues/1301 ), aby wykonać zamianę symbolu zastępczego, gdy używasz niestandardowej lokalizacji
Andy Wilkinson
109

Poniższe rozwiązanie jest skrótem dla rozwiązania @Andy Wilkinson, z tym wyjątkiem, że nie musi ono używać oddzielnej klasy ani @Beanmetody z adnotacjami.

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

}

Możemy dołączyć zarówno @Valueadnotacje, jak i @ConfigurationPropertiesbez problemów. Ale metody pobierające i ustawiające są ważne i @EnableConfigurationPropertiesmuszą mieć @ConfigurationPropertiesdo działania.

Wypróbowałem ten pomysł z fajnego rozwiązania dostarczonego przez @Szymon Stępniak, pomyślałem, że przyda się komuś.

raksja
źródło
11
dzięki! Użyłem buta sprężynowego 1.3.1, w moim przypadku okazało się, że nie potrzebuje@EnableConfigurationProperties
zhuguowei
Podczas korzystania z tej odpowiedzi pojawia się błąd „Stała nieprawidłowego znaku”. Czy możesz zmienić: @ConfigurationProperties (prefix = 'input'), aby używać podwójnych cudzysłowów, aby zapobiec temu błędowi.
Anton Rand
10
Dobra odpowiedź, ale adnotacje @Value nie są konieczne.
Robin
3
Zamiast pisać fikcyjny getter i setter, możesz użyć adnotacji Lombok @Setter (AccessLevel.PUBLIC) i @Getter (AccessLevel.PUBLIC)
RiZKiT
Genialne. Zauważ, że konfiguracja może być również zagnieżdżona: Map <String, Map <String, String >>
Máthé Endre-Botond
16

Dzisiaj mam ten sam problem, ale niestety rozwiązanie Andy'ego nie zadziałało. W Spring Boot 1.2.1.RELEASE jest jeszcze łatwiej, ale musisz być świadomy kilku rzeczy.

Oto interesująca część z mojego 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 zawiera tylko jeden wpis mapy, moim celem jest zapewnienie dynamicznej konfiguracji dla innych dostawców OAuth. Chcę wstrzyknąć tę mapę do usługi, która będzie inicjować usługi w oparciu o konfigurację podaną w tym pliku yaml. Moja pierwsza realizacja to:

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

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

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

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

Po uruchomieniu aplikacji providersmapa w OAuth2ProvidersServicenie została zainicjowana. Wypróbowałem rozwiązanie zaproponowane przez Andy'ego, ale nie zadziałało. Używam Groovy w tej aplikacji, więc zdecydowałem się usunąć privatei pozwolić Groovy generować getter i setter. Więc mój kod wyglądał tak:

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

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

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

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

Po tej drobnej zmianie wszystko działało.

Chociaż jest jedna rzecz, o której warto wspomnieć. Po tym, jak to działało, zdecydowałem się utworzyć to pole privatei udostępnić setterowi prosty typ argumentu w metodzie ustawiającej. Niestety to nie zadziała. Powoduje org.springframework.beans.NotWritablePropertyExceptionz komunikatem:

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?

Pamiętaj o tym, jeśli używasz Groovy w swojej aplikacji Spring Boot.

Szymon Stępniak
źródło
15

Aby pobrać mapę z konfiguracji, będziesz potrzebować klasy konfiguracyjnej. Adnotacja @Value niestety nie wystarczy.

Application.yml

entries:
  map:
     key1: value1
     key2: value2

Klasa konfiguracji:

@Configuration
@ConfigurationProperties("entries")
@Getter
@Setter
 public static class MyConfig {
     private Map<String, String> map;
 }
Orbite
źródło
przetestowałem powyższe rozwiązanie działa na wersji 2.1.0
Tugrul ASLAN
6

Rozwiązanie do ściągania mapy przy użyciu @Value z właściwości application.yml zakodowanej jako wielowierszowa

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 

Tutaj wartość naszej właściwości mapy „my-map-property-name” jest przechowywana w formacie JSON w ciągu znaków i uzyskaliśmy multilinię przy użyciu \ na końcu wiersza

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

}

Więcej wyjaśnień

  • \ w yaml służy do dzielenia łańcucha na multilinię

  • \ " jest znakiem ucieczki dla" (quote) w łańcuchu yaml

  • {klucz: wartość} JSON w yaml, który zostanie przekonwertowany na Map przez @Value

  • # {} jest to wyrażenie SpEL i może być użyte w @Value do konwersji json int Map lub Array / list Reference

Przetestowano w projekcie butów sprężynowych

Mediolan
źródło
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
źródło
7
Witamy w Stack Overflow! Chociaż ten fragment kodu może rozwiązać problem, dołączenie wyjaśnienia naprawdę pomaga poprawić jakość Twojego posta. Pamiętaj, że odpowiadasz na pytanie do czytelników w przyszłości, a osoby te mogą nie znać powodów, dla których zaproponowałeś kod.
Scott Weldon,
link do wiki jest jednak cenny. Wyjaśnienie znajduje się na github.com/spring-projects/spring-boot/wiki/ ...
dschulten,
1

Możesz to jeszcze uprościć, jeśli chcesz uniknąć dodatkowych struktur.

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

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

}

A potem użyj go jak zwykle, na przykład z konstruktorem:

public class Foo {

  private final Map<String, String> serviceMappings;

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

}
Alexander Korolev
źródło