简体   繁体   English

Spring 引导 2 - 在 bean 初始化之前做一些事情

[英]Spring Boot 2 - Do something before the beans are initialized

Problem Statement问题陈述

I want to load properties from a properties file in a classpath or at an external location before the beans are initialized.我想在初始化 bean 之前从类路径中或外部位置的属性文件中加载属性。 These properties are also a part of Bean initialization.这些属性也是 Bean 初始化的一部分。 I cannot autowire the properties from Spring's standard application.properties or its customization because the same properties file must be accessible by multiple deployables.我无法从 Spring 的标准application.properties或其自定义中自动装配属性,因为同一个属性文件必须可由多个可部署对象访问。

What I Tried我试过的

I'm aware about Spring Application Events ;我知道Spring 应用程序事件 in fact, I'm already hooking ContextRefreshedEvent to perform some tasks after the Spring Context is initialized (Beans are also initialized at this stage).事实上,在 Spring Context 初始化之后,我已经在 hook ContextRefreshedEvent来执行一些任务(在这个阶段也初始化了 Beans)。

For my problem statement, from the description of Spring Docs ApplicationEnvironmentPreparedEvent looked promising, but the hook did not work.对于我的问题陈述,从 Spring Docs 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");
    }

}

Update更新

As suggested by M.Deinum in the comments, I tried adding an application context initializer like below.正如M.Deinum在评论中所建议的那样,我尝试添加一个应用程序上下文初始化程序,如下所示。 It doesn't seem to be working either.它似乎也不起作用。

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

    }

Update #2更新#2

While my problem statement is regarding loading properties, my question/curiosity is really about how to run some code before the classes are initialized as beans and put into Spring IoC container .虽然我的问题陈述是关于加载属性,但我的问题/好奇心实际上是关于如何在类被初始化为 bean 并放入 Spring IoC 容器之前运行一些代码 Now, these beans require some property values during initialization and I can't/don't want to Autowire them because of the following reason:现在,这些 bean 在初始化期间需要一些属性值,我不能/不想自动装配它们,原因如下:

As stated in comments and answers, the same can be done using Spring Boot's externalized configuration and profiles.如评论和答案中所述,同样可以使用 Spring Boot 的外部化配置和配置文件来完成。 However, I need to maintain application properties and domain-related properties separately.但是,我需要分别维护应用程序属性和与域相关的属性。 A base domain properties should have at least 100 properties, and the number grows over time.一个基本域属性应该至少有 100 个属性,并且数量会随着时间的推移而增长。 Both application properties and domain-related properties have a property file for different environments (dev, SIT, UAT, Production).应用程序属性和与域相关的属性都有一个用于不同环境(开发、SIT、UAT、生产)的属性文件。 Property files override one or more of the base properties.属性文件覆盖一个或多个基本属性。 That's 8 property files.那是 8 个属性文件。 Now, the same app needs to be deployed into multiple geographies.现在,需要将同一个应用程序部署到多个地区。 That makes it 8 * n property files where n is the number of geographies.这使它成为8 * n属性文件,其中n是地理区域的数量。 I want all the property files stored in a common module so that they can be accessed by different deployables.我希望所有属性文件都存储在一个公共模块中,以便不同的部署可以访问它们。 Environment and geography would be known in run-time as system properties.环境和地理在运行时将被称为系统属性。

While these might be achieved by using Spring profiles and precedence order, I want to have a programmatic control over it ( I also would maintain my own property repository ).虽然这些可以通过使用 Spring 配置文件和优先顺序来实现,但我希望对其进行编程控制(我还将维护自己的属性存储库)。 Eg.例如。 I would write a convenience utility called MyPropUtil and access them like:我会编写一个名为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;
    }

}

From the docs, it seems like ApplicationEvents and ApplicationInitializers fit my need, but I am not able to get them to work for my problem statement.从文档看来, ApplicationEventsApplicationInitializers似乎符合我的需要,但我无法让它们为我的问题陈述工作。

Bit late to the party but hopefully I can offer a solution to your updated problem statement.聚会迟到了,但希望我能为您更新的问题陈述提供解决方案。

This will focus on problem of how to run some code before the classes are initialized as beans and put into Spring IoC container这将集中讨论如何在类被初始化为 bean 并放入 Spring IoC 容器之前运行一些代码的问题

One issue I notice is that you're defining your application events via the @EventListener annotation.我注意到的一个问题是您正在通过 @EventListener 注释定义应用程序事件。

These are only called once all beans are initiated since these annotations are processed by EventListenerMethodProcessor which is only triggered when the context is ready (see SmartInitializingSingleton#afterSingletonsInstantiated)这些仅在所有 bean 启动后才被调用,因为这些注释由EventListenerMethodProcessor处理,仅在上下文准备好时触发(请参阅 SmartInitializingSingleton#afterSingletonsInstantiated)

As such, some of the events that occur before the context is ready.因此,在上下文准备好之前发生的一些事件。 eg ContextStartedEvent, ApplicationContextInitializedEvent won't make it to your listener.例如 ContextStartedEvent、ApplicationContextInitializedEvent 不会出现在您的监听器中。

Instead, what you can do is extend the interface for these events directly.相反,您可以做的是直接扩展这些事件的接口。

@Slf4j
public class AllEvent implements ApplicationListener<ApplicationEvent> {

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

Note the missing @Component.注意缺少的@Component。 Even bean instantiation can occur after some of these events.甚至 bean 实例化也可能发生其中一些事件之后。 If you use @Component, then you'll get the following logs如果您使用@Component,那么您将获得以下日志

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

Still better and more instant than the annotative listeners but will still not receive the initialization events.仍然比注释性侦听器更好、更即时,但仍不会接收到初始化事件。 For that, what you need to do is follow the instructions found here为此,您需要按照此处的说明进行操作

To summarize,总结一下,

  • Create directory resources/META-INF创建目录资源/META-INF
  • Create file spring.factories创建文件 spring.factories
  • org.springframework.context.ApplicationListener=full.path.to.my.class.AllEvent org.springframework.context.ApplicationListener=full.path.to.my.class.AllEvent

The result:-结果:-

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

In particular, ApplicationContextInitializedEvent should allow you to perform whatever per-instantiation tasks you need.特别是,ApplicationContextInitializedEvent 应该允许您执行所需的任何实例化任务。

I think Spring Cloud Config is a perfect solution for your problem statement.我认为 Spring Cloud Config 是您问题陈述的完美解决方案。 Detailed documentation Here详细文档在这里

Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. Spring Cloud Config 为分布式系统中的外部化配置提供服务器端和客户端支持。

So you can easily manage the configurations outside of the app, as well as all the instances will use same configurations.因此,您可以轻松管理应用程序外部的配置,并且所有实例都将使用相同的配置。

Create a bean that will be a properties repository and inject it in other beans requiring properties.创建一个将作为属性存储库的 bean,并将其注入其他需要属性的 bean。

In your example, instead of having static methods in MyPropUtil , make the class a bean itself with instance methods.在您的示例中,不要在MyPropUtil中使用 static 方法,而是使用实例方法使 class 成为 bean 本身。 Initialize Map<String, Properties> repository in the initialize method annotated with @PostConstruct .在使用@PostConstruct注释的initialize方法中初始化Map<String, Properties> repository

@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) {
    //...
  }
}

and

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

Or you can inject MyPropUtil directly to the SomeClass :或者您可以将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");
  }
  //...
}

As explaned in this post you can add external property files like this;本文所述,您可以像这样添加外部属性文件;

public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
    PropertySourcesPlaceholderConfigurer properties = new PropertySourcesPlaceholderConfigurer();
    properties.setLocation(new FileSystemResource("/Users/home/conf.properties"));
    properties.setIgnoreResourceNotFound(false);
    return properties;
}

If you don't want to use this, just read the property file with jackson and set the properties to System.setProperty("key","value") in the main method before spring starts.如果你不想使用这个,只需在 spring 启动之前在main方法中读取带有 jackson 的属性文件并将属性设置为System.setProperty("key","value")即可。

If you don't want to use this too, take a look at the BeanPostProcessor#postProcessBeforeInitialization method.如果您也不想使用它,请查看BeanPostProcessor#postProcessBeforeInitialization方法。 It runs before bean properties initialized by spring.它在由 spring 初始化的 bean 属性之前运行。

I might be missing what exactly do you mean by "Beans initialization", probably an example of such a bean in a question could be beneficial.我可能会错过“Beans 初始化”的确切含义,可能问题中此类 bean 的示例可能是有益的。

I think you should differentiate between properties reading part and bean initialization.我认为您应该区分属性读取部分和 bean 初始化。 By the time of bean initialization, properties are already read and available.到 bean 初始化时,属性已经被读取并且可用。 Thats a part of spring magic, if you wish.如果您愿意,那是 spring 魔法的一部分。

That's why the following code works for example:这就是为什么以下代码可以工作的原因:

@Component
public class MySampleBean {

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

It doesn't matter from where do these property come (spring boot defines many different ways of these places with precedence between them), it will happen before the initialization of beans happens.这些属性从哪里来并不重要(spring boot 定义了这些地方的许多不同方式,它们之间具有优先级),它会在 bean 的初始化发生之前发生。

Now, lets get back to your original question:现在,让我们回到你原来的问题:

I want to load properties from a properties file in a classpath or at external location (before the beans are initialized - irrelevant).我想从类路径或外部位置的属性文件中加载属性(在初始化 bean 之前 - 不相关)。

In spring / spring-boot there is a concept of profiles that basically allows to create a file application-foo.properties (or yaml) and when you load with --spring.profiles.active=foo it will automatically load properties defined in this application-foo.properties in addition to the regular application.properties在 spring / spring-boot 中有一个配置文件的概念,它基本上允许创建文件application-foo.properties (或 yaml),当您使用--spring.profiles.active=foo加载时,它将自动加载此application-foo.properties中定义的属性- application-foo.properties除了常规的application.properties

So you can place the stuff that you want to "load from classpath" into application-local.properties (the word local is for the sake of example only) and start the application with --spring.profiles.active=local (in the deployment script, docker file or whatever)因此,您可以将要“从类路径加载”的内容放入application-local.properties (本地一词仅用于示例)并使用--spring.profiles.active=local启动应用程序(在部署脚本,docker 文件或其他)

If you want to run the property from external location (outside the classpath) you can use: --spring.config.location=<Full-path-file>如果您想从外部位置(类路径之外)运行该属性,您可以使用: --spring.config.location=<Full-path-file>

Note that even if you put some properties into a regular application.properties and still use --spring.config.location with the same key-value pairs they will take precedence over the properties in the classpath.请注意,即使您将一些属性放入常规application.properties并且仍然使用具有相同键值对的--spring.config.location ,它们也会优先于类路径中的属性。

Alternatively you can use only --sring.profiles.active=local or remote and do not use config locations at all.或者,您可以仅使用--sring.profiles.active=local or remote并且根本不使用配置位置。

You can configure external location directly in the command line:您可以直接在命令行中配置外部位置:

java -jar app.jar --spring.config.location=file:///Users/home/config/external.properties

You can use WebApplicationInitializer to execute code before classes are initialized as beans您可以使用WebApplicationInitializer类被初始化为 bean之前执行代码

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

We create an AnnotationConfigWebApplicationContext and register a web configuration file with register().我们创建一个 AnnotationConfigWebApplicationContext 并使用 register() 注册一个 web 配置文件。

You can check if PropertySource may help you.您可以检查PropertySource是否可以帮助您。

Example:例子:

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

You can use this annotation on every @Configuration or @SpringBootApplication bean您可以在每个@Configuration@SpringBootApplication bean 上使用此注解

It sounds like you want to take some ownership of a part of the bean initialization.听起来您想获得 bean 初始化的一部分的所有权。 Typically people think of Spring completing the bean configuration, but in your case it might be easier to consider Spring as starting it.通常人们认为 Spring完成了 bean 配置,但在您的情况下,将 Spring 视为启动它可能更容易。

So, your bean has some properties you want to configure, and some that you want Spring to configure.所以,你的 bean 有一些想要配置的属性,还有一些你想要Spring配置的属性。 Just annotate the ones you want Spring to configure (with @Autowire or @Inject , or whatever flavour you prefer), and then take over the control from there, using @PostConstruct or InitializingBean .只需注释您希望 Spring 配置的那些(使用@Autowire@Inject或您喜欢的任何风格),然后使用@PostConstructInitializingBean从那里接管控制。

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

Of course your mechanism for property resolution would need to be available statically somehow, but that seems to fit with you MyPropUtil example.当然,您的属性解析机制需要以某种方式静态可用,但这似乎适合您MyPropUtil示例。

Getting far more involved, you start looking at Bean Post Processors directly ( @PostConstruct is a simple variant of sorts).更多参与其中,您开始直接查看 Bean 后处理器( @PostConstruct是一种简单的变体)。

There's a previous question, with a useful answer, here How exactly does the Spring BeanPostProcessor work?以前有一个问题,有一个有用的答案,这里Spring BeanPostProcessor 究竟是如何工作的? , but for simplicity, you'd do something like ,但为简单起见,你会做类似的事情

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

Clearly @PostProcess , or InitializingBean are simpler, but the custom post processor has a big advantage... it can be injected with other Spring managed beans.显然@PostProcessInitializingBean更简单,但自定义后处理器有一个很大的优势......它可以与其他 Spring 托管 bean 一起注入。 That means you can Spring manage your property injection whatever-thing, and still manually manage the actual injection process.这意味着您可以 Spring 管理您的属性注入,并且仍然手动管理实际的注入过程。

Just try to load everything you need in main before只需尝试在 main 之前加载您需要的所有内容

SpringApplication.run() SpringApplication.run()

call称呼

public static void main(String[] args) {
    // before spring initialization
    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    SpringApplication.run(CyberRiskApplication.class, args);
}

You can use ApplicationEnvironmentPreparedEvent but it can't be configured using EventListener annotation.您可以使用 ApplicationEnvironmentPreparedEvent 但不能使用 EventListener 注释进行配置。 Because by this time Bean drfinitions are not loaded.因为此时尚未加载 Bean 定义。 See the below link on how to cofigure this event.请参阅以下链接,了解如何配置此事件。 https://www.thetechnojournals.com/2019/10/spring-boot-application-events.html https://www.thetechnojournals.com/2019/10/spring-boot-application-events.html

I feel like your main issue is that you need to maintain application properties and domain-related properties separately.我觉得您的主要问题是您需要分别维护应用程序属性和与域相关的属性。 From spring's perspective, it doesn't really matter since all properties files are kinda merged together after they have been loaded in memory.从 spring 的角度来看,这并不重要,因为所有属性文件在加载到 memory 之后都会合并在一起。 So for example, you have two files that contain some properties:例如,您有两个包含一些属性的文件:

application.related=property1 # this is in application.properties
domain.related=property2 # this is in domain-specific.properties

After they have been loaded, you will get one big thing that contains all properties, if I am not mistaken, it is a org.springframework.core.env.ConfigurableEnvironment instance.加载它们之后,你会得到一个包含所有属性的大东西,如果我没记错的话,它是一个org.springframework.core.env.ConfigurableEnvironment实例。

Then what you need to do is just inject the property you need using something like @Value .然后你需要做的就是使用@Value类的东西注入你需要的属性。

For the main issue, to separate properties into different files, you just need to specify spring's spring.config.name property (via environment variable, command line or programmatically).对于主要问题,要将属性分隔到不同的文件中,您只需指定 spring 的spring.config.name属性(通过环境变量、命令行或以编程方式)。 Following the above example, it should be spring.config.name=application,domain-specific .按照上面的例子,它应该是spring.config.name=application,domain-specific

Furthermore, if you really want to have programmatic control , you can add a custom EnvironmentPostProcessor which exposes the ConfigurableEnvironment instance.此外,如果你真的想要编程控制,你可以添加一个自定义EnvironmentPostProcessor来公开ConfigurableEnvironment实例。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM