Spring Boot 2 - сделать что-то до инициализации bean-компонентов - PullRequest
6 голосов
/ 06 ноября 2019

Постановка проблемы

Я хочу загрузить свойства из файла свойств в classpath или во внешнем расположении до инициализации bean-компонентов. Потому что эти свойства также будут частью инициализации Бина. Я не могу автоматически связать эти свойства со стандартным application.properties Spring или его настройкой, потому что один и тот же файл свойств должен быть доступен нескольким depoyables.

Что я пробовал

I 'm осведомлен о событиях весенних приложений ;На самом деле, я уже подключаю ContextRefreshedEvent для выполнения некоторых задач после инициализации контекста Spring (компоненты Bean также инициализируются на этом этапе).

Для моего постановки задачи из описания SpringДокументы ApplicationEnvironmentPreparedEvent выглядели многообещающе, но я не могу заставить их работать.


@SpringBootApplication
public class App {

    public static void main(String[] args) throws IOException {
        SpringApplication.run(App.class, args);
    }


    @EventListener
    public void onStartUp(ContextRefreshedEvent event) {
        System.out.println("ContextRefreshedEvent");    // WORKS
    }

    @EventListener
    public void onShutDown(ContextClosedEvent event) {
        System.out.println("ContextClosedEvent");   // WORKS
    }

    @EventListener
    public void onEvent6(ApplicationStartedEvent event) {
        System.out.println("ApplicationStartedEvent");  // WORKS BUT AFTER ContextRefreshedEvent
    }


    @EventListener
    public void onEvent3(ApplicationReadyEvent event) {
        System.out.println("ApplicationReadyEvent");    // WORKS WORKS BUT AFTER ContextRefreshedEvent
    }


    public void onEvent1(ApplicationEnvironmentPreparedEvent event) {
        System.out.println("ApplicationEnvironmentPreparedEvent");  // DOESN'T WORK
    }


    @EventListener
    public void onEvent2(ApplicationContextInitializedEvent event) {
        System.out.println("ApplicationContextInitializedEvent");   // DOESN'T WORK
    }


    @EventListener
    public void onEvent4(ApplicationContextInitializedEvent event) {
        System.out.println("ApplicationContextInitializedEvent");
    }

    @EventListener
    public void onEvent5(ContextStartedEvent event) {
        System.out.println("ContextStartedEvent");
    }

}

Обновление

Как подсказано M.Deinum в комментариях, япопытался добавить инициализатор контекста приложения, как показано ниже. Похоже, он тоже не работает.

    public static void main(String[] args) {
        new SpringApplicationBuilder()
                .sources(App.class)
                .initializers(applicationContext -> {
                    System.out.println("INSIDE CUSTOM APPLICATION INITIALIZER");
                })
                .run(args);

    }

Обновление # 2

Хотя мое постановление проблемы касается загрузки свойств, мой вопрос / любопытство действительно о как запуститьнекоторый код перед тем, как классы инициализируются как бины и помещаются в контейнер Spring IoC . Теперь этим компонентам требуются значения некоторых свойств во время инициализации, и я не могу / не хочу автоматически связывать их по следующей причине.

Хотя, как указано в комментариях и ответах, то же самое можно сделать с помощьюВнешняя конфигурация и профили Spring Boot. Однако мне нужно поддерживать свойства приложения и свойства, связанные с доменом, отдельно. Ожидается, что свойства базового домена будут иметь не менее 100 свойств, которые будут расти со временем. У каждого из них есть файл свойств, каждый для своей среды (dev, SIT, UAT, Production), который будет переопределять одно или несколько базовых свойств. Это 8 файлов свойств. Теперь одно и то же приложение необходимо развернуть в нескольких регионах. Это делает его 8 * n файлами свойств, где n - количество географических регионов. Я хочу, чтобы все они хранились в общем модуле, чтобы к нему могли обращаться различные развертываемые объекты. Окружение и география будут известны во время выполнения как системные свойства.

Хотя это может быть достигнуто с помощью профилей Spring и порядка приоритетов, я хочу иметь программный контроль над ним ( Я бы также поддержалмой собственный репозиторий ). Например. Я написал бы удобную утилиту под названием MyPropUtil и получил бы к ним доступ, например:

public class MyPropUtil {
     private static Map<String, Properties> repository;

     public static initialize(..) {
         ....
     }

     public static String getDomainProperty(String key) {
        return repository.get("domain").getProperty(key);
     }

     public static String getAppProperty(String key) {
         return repository.get("app").getProperty(key);
     }

     public static String getAndAddBasePathToAppPropertyValue(String key) {
        ...
     }

}

@Configuration
public class MyComponent {

    @Bean
    public SomeClass getSomeClassBean() {
        SomeClass obj = new SomeClass();
        obj.someProp1(MyPropUtil.getDomainProperty('domainkey1'));
        obj.someProp2(MyPropUtil.getAppProperty('appkey1'));
        // For some properties
         obj.someProp2(MyPropUtil.getAndAddBasePathToAppPropertyValue('some.relative.path.value'));
        ....
        return obj;
    }

}

Из документов кажется, что ApplicationEvents и ApplicationInitializers соответствуют моим потребностям, но я не могузаставить их работать для моей постановки проблемы.

Ответы [ 11 ]

2 голосов
/ 14 ноября 2019

Я думаю, что Spring Cloud Config - это идеальное решение для вашей задачи. Подробная документация Здесь

Spring Cloud Config обеспечивает поддержку на стороне сервера и на стороне клиента для внешней конфигурации в распределенной системе.

Таким образом, вы можетелегко управлять конфигурациями вне приложения, так как все экземпляры будут использовать одинаковые конфигурации.

1 голос
/ 12 ноября 2019

Создайте bean-компонент, который будет репозиторием свойств, и добавьте его в другие bean-компоненты, требующие свойств.

В вашем примере вместо статических методов в MyPropUtil сделайте сам класс bean-компонентом с методами экземпляра. ,Инициализируйте Map<String, Properties> repository в методе initialize с комментариями @PostConstruct.

@Component
public class MyPropUtil {

  private static final String DOMAIN_KEY = "domain";
  private static final String APP_KEY = "app";

  private Map<String, Properties> repository;

  @PostConstruct
  public void init() {
    Properties domainProps = new Properties();
    //domainProps.load();
    repository.put(DOMAIN_KEY, domainProps);

    Properties appProps = new Properties();
    //appProps.load();
    repository.put(APP_KEY, appProps);
  }

  public String getDomainProperty(String key) {
    return repository.get(DOMAIN_KEY).getProperty(key);
  }

  public String getAppProperty(String key) {
    return repository.get(APP_KEY).getProperty(key);
  }

  public String getAndAddBasePathToAppPropertyValue(String key) {
    //...
  }
}

и

@Configuration
public class MyComponent {

  @Autowired
  private MyPropUtil myPropUtil;

  @Bean
  public SomeClass getSomeClassBean() {
    SomeClass obj = new SomeClass();
    obj.someProp1(myPropUtil.getDomainProperty("domainkey1"));
    obj.someProp2(myPropUtil.getAppProperty("appkey1"));
    // For some properties
    obj.someProp2(myPropUtil.getAndAddBasePathToAppPropertyValue("some.relative.path.value"));
      //...
      return obj;
  }
}

Или вы можете ввести MyPropUtil непосредственно в SomeClass:

@Component
public class SomeClass {

  private final String someProp1;
  private final String someProp2;

  @Autowired
  public SomeClass(MyPropUtil myPropUtil) {
    this.someProp1 = myPropUtil.getDomainProperty("domainkey1");
    this.someProp2 = myPropUtil.getAppProperty("appkey1");
  }
  //...
}
0 голосов
/ 15 ноября 2019

Немного опоздал на вечеринку, но, надеюсь, я могу предложить решение вашей обновленной формулировки проблемы.

Это будет сосредоточено на проблеме , как запустить некоторый код, прежде чем классы будут инициализированы как bean-компоненты и помещеныв контейнер Spring IoC

Одна проблема, которую я замечаю, заключается в том, что вы определяете события своего приложения с помощью аннотации @EventListener.

Они вызываются только после того, как все bean-компоненты инициированы, так как эти аннотацииобрабатываются EventListenerMethodProcessor , который запускается только тогда, когда контекст готов (см. SmartInitializingSingleton # afterSingletonsInstantiated)

Таким образом, некоторые события, которые происходят до того, как контекст готов. Например, ContextStartedEvent, ApplicationContextInitializedEvent не сможет сделать это для вашего слушателя.

Вместо этого вы можете напрямую расширить интерфейс для этих событий.

@Slf4j
public class AllEvent implements ApplicationListener<ApplicationEvent> {

    @Override
    public void onApplicationEvent(final ApplicationEvent event) {
        log.info("I am a {}", event.getClass().getSimpleName());
    }

Обратите внимание на отсутствующий @Component. Даже создание бина может произойти после некоторых из этих событий. Если вы используете @Component, вы получите следующие журналы

I am a DataSourceSchemaCreatedEvent
I am a ContextRefreshedEvent
I am a ServletWebServerInitializedEvent
I am a ApplicationStartedEvent
I am a ApplicationReadyEvent

Все еще лучше и быстрее, чем аннотативные слушатели, но все равно не получат события инициализации. Для этого вам нужно выполнить следующие инструкции: здесь

Подводя итог,

  • Создать каталог ресурсов / META-INF
  • Создать файл spring.factories
  • org.springframework.context.ApplicationListener = full.path.to.my.class.AllEvent

Результат: -

I am a ApplicationContextInitializedEvent
I am a ApplicationPreparedEvent
I am a DataSourceSchemaCreatedEvent
I am a ContextRefreshedEvent
I am a ServletWebServerInitializedEvent
I am a ApplicationStartedEvent
I am a ApplicationReadyEvent

В частности, ApplicationContextInitializedEvent должно позволять вам выполнять любые задачи для каждого экземпляра.

0 голосов
/ 14 ноября 2019

Вы можете использовать ApplicationEnvironmentPreparedEvent, но его нельзя настроить с помощью аннотации EventListener. Потому что к этому времени бина drfinitions не загружены. Смотрите ниже ссылку о том, как настроить это событие. https://www.thetechnojournals.com/2019/10/spring-boot-application-events.html

0 голосов
/ 13 ноября 2019

Просто попробуйте загрузить все необходимое в main перед

SpringApplication.run ()

call

public static void main(String[] args) {
    // before spring initialization
    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    SpringApplication.run(CyberRiskApplication.class, args);
}
0 голосов
/ 12 ноября 2019

Звучит так, будто вы хотите стать владельцем части инициализации bean-компонента. Обычно люди думают, что Spring завершает настройку bean-компонента, но в вашем случае было бы проще рассматривать Spring как , начиная it.

Итак, у вашего bean-компонента есть некоторые свойства вы хотите настроить, а некоторые из них вы хотите Spring настроить. Просто аннотируйте те из них, которые вы хотите сконфигурировать в Spring (с помощью @Autowire или @Inject, или с любым другим вкусом, который вы предпочитаете), а затем перенесите управление оттуда, используя @PostConstruct или InitializingBean.

class MyMultiStageBoosterRocket {

  private Foo foo;
  private Bar bar;
  private Cat cat;

  @Autowire
  public MyMultiStageBoosterRocket(Foo foo, Bar bar) {
    this.foo = foo;
    this.bar = bar'
  }

  // called *after* Spring has done its injection, but *before* the bean
  // is registered in the context
  @PostConstruct
  public void postConstruct() {
    // your magic property injection from whatever source you happen to want
    ServiceLoader<CatProvider> loader = ServiceLoader.load(CatProvider.class);
    // etc...
  }
}

Конечно, ваш механизм разрешения свойств должен был бы быть как-то статически доступным, но это, кажется, подходит вам * пример 1018 *.

Если вы на далеко более вовлечены, вы начинаете искатьнепосредственно в Bean Post Processors (@PostConstruct - простой вариант сортировки).

Здесь есть предыдущий вопрос с полезным ответом, здесь Как именно Spring BeanPostProcessor работает? , нодля простоты вы бы сделали что-то вроде

public class CustomBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {

        // fixme: detect if this bean needs fancy initialization

        return bean;
    }
}

Очевидно, что @PostProcess или InitializingBean проще, но пользовательский постпроцессор имеет большое преимущество ... его можно внедрить с помощью других управляемых Springфасоль. Это означает, что вы можете управлять Spring-инжинирингом любых свойств Spring и в то же время вручную управлять процессом фактического внедрения.

0 голосов
/ 11 ноября 2019

Вы можете проверить, может ли PropertySource помочь вам.

Пример:

@PropertySource({"classpath:persistence/persistence.properties"})

Вы можете использовать эту аннотацию на каждом @Configuration или @SpringBootApplication бобе

0 голосов
/ 11 ноября 2019

Вы можете использовать WebApplicationInitializer для выполнения кода перед инициализацией классов в виде бинов

public class MyWebInitializer implements WebApplicationInitializer {    
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
       var ctx = new AnnotationConfigWebApplicationContext();
       ctx.register(WebConfig.class);
       ctx.setServletContext(servletContext);

Мы создаем AnnotationConfigWebApplicationContext и регистрируем файл веб-конфигурациис регистром ().

0 голосов
/ 09 ноября 2019

Вы можете настроить внешнее местоположение непосредственно в командной строке:

java -jar app.jar --spring.config.location=file:///Users/home/config/external.properties
0 голосов
/ 09 ноября 2019

Мне может не хватать того, что именно вы подразумеваете под "инициализацией Beans", возможно, пример такого компонента в вопросе может быть полезным.

Я думаю, вы должны различать часть чтения свойств и инициализацию компонента,К моменту инициализации компонента свойства уже прочитаны и доступны. Это часть весенней магии, если хотите.

Вот почему, например, работает следующий код:

@Component
public class MySampleBean {

    public MySampleBean(@Value("${some.prop}" String someProp) {...}
}

Не имеет значения, откуда взялись эти свойства (весенняя загрузка определяет много разных путей этих мест с приоритетом между ними), это произойдет до того, как произойдет инициализация bean-компонентов.

Теперь вернемся к исходному вопросу:

Я хочу загрузить свойства из файла свойств в пути к классам или во внешнем расположении (до инициализации bean-компонентов - не имеет значения).

В spring / spring-boot существует концепция профилей, которая в основном позволяет создавать файл application-foo.properties (или yaml), и при загрузке с --spring.profiles.active=foo он автоматически загружает свойства, определенныев этом application-foo.properties в дополнение к обычному application.properties

Таким образом, вы можете поместить материал, который вы хотите "загрузить из classpath", в application-local.properties (слово local только для примера)и запустите приложение с --spring.profiles.active=local (в сценарии развертывания, в файле Docker и т. д.)

Если вы хотите запустить свойство из внешнего расположения (вне пути к классам), вы можете использовать: --spring.config.location=<Full-path-file>

Обратите внимание, что даже если вы поместите некоторые свойства в обычный application.properties и по-прежнему используете --spring.config.location с теми же парами ключ-значение, они будут иметь приоритет над свойствами в classpath.

В качестве альтернативы вы можете использовать только --sring.profiles.active=local or remote и вообще не использовать местоположения конфигурации.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...