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

Posted

技术标签:

【中文标题】Spring Boot 2 - 在初始化 bean 之前做一些事情【英文标题】:Spring Boot 2 - Do something before the beans are initialized 【发布时间】:2020-03-02 15:52:31 【问题描述】:

问题陈述

我想在初始化 bean 之前从类路径中或外部位置的属性文件加载属性。这些属性也是 Bean 初始化的一部分。我无法从 Spring 的标准 application.properties 或其自定义中自动装配属性,因为同一个属性文件必须可由多个可部署对象访问。

我尝试了什么

我知道Spring Application Events;其实我已经上钩了 ContextRefreshedEvent 在 Spring Context 初始化后执行一些任务(在这个阶段也初始化了 Beans)。

对于我的问题陈述,来自 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");
    



更新

按照 cmets 中 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

虽然我的问题陈述是关于加载属性,但我的问题/好奇实际上是关于如何在将类初始化为 bean 并放入 Spring IoC 容器之前运行一些代码。现在,这些 bean 在初始化期间需要一些属性值,我不能/不想自动装配它们,原因如下:

如 cmets 和 answers 中所述,同样可以使用 Spring Boot 的外部化配置和配置文件来完成。但是,我需要分别维护应用程序属性和与域相关的属性。一个基本域属性应该至少有 100 个属性,并且数量会随着时间的推移而增长。应用程序属性和与域相关的属性都有一个用于不同环境(开发、SIT、UAT、生产)的属性文件。属性文件覆盖一个或多个基本属性。那是 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;
    


从文档来看,ApplicationEventsApplicationInitializers 似乎符合我的需要,但我无法让它们为我的问题陈述工作。

【问题讨论】:

为什么不添加spring.config.additional-locations 工作?或者只是通过spring.config.location 提供所有配置文件?我不明白你为什么需要自己加载它们?这些位置可以是您想要加载的外部位置(只需使用 file: 前缀从文件系统加载它们)。 你试过@PreConstruct吗? @M.Deinum 这有点复杂。只是谈论可能的属性文件的数量 - 产品应该是多个地理区域,每个地理区域都有自己的开发、UAT 和生产版本的属性,我需要能够设置优先级;除此之外,我还有一个包装器(PropertyUtility)来获取和处理基于不同上下文的属性值。 (我可以再次使用 @Configuration 类执行此操作,但属性太多。) @medTech 这行不通,因为我需要将多个属性文件加载到属性存储库,而不仅仅是几个 bean。 这就是配置文件的用途(每个环境的不同配置),优先级是您指定它们的顺序。无论使用框架而不是围绕它工作。您想使用 Spring 工具但不使用框架。如果您真的想做一些您认为不可能的复杂事情,请使用 ApplicationContextInitializer 实现来执行此操作并注册其他 PropertySources(不要与 @PropertySource 混淆!)。 【参考方案1】:

我觉得您的主要问题是您需要分别维护应用程序属性和域相关属性。 从 spring 的角度来看,这并不重要,因为所有属性文件在加载到内存后都会合并在一起。 例如,您有两个包含一些属性的文件:

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

加载它们之后,你会得到一个包含所有属性的大东西,如果我没记错的话,它是一个org.springframework.core.env.ConfigurableEnvironment 实例。

那么你需要做的就是使用@Value之类的东西注入你需要的属性。

对于主要问题,要将属性分隔到不同的文件中,您只需指定 spring 的 spring.config.name 属性(通过环境变量、命令行或以编程方式)。按照上面的例子,应该是spring.config.name=application,domain-specific

此外,如果您真的想要拥有编程控制,您可以添加一个自定义的EnvironmentPostProcessor,它会公开ConfigurableEnvironment 实例。

【讨论】:

回复很晚 - 但EnvironmentPostProcessor 为我完成了这项工作。【参考方案2】:

聚会迟到了,但希望我能为您更新的问题陈述提供解决方案。

这将集中在如何在类被初始化为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。甚至 bean 实例化也可能发生在其中一些事件之后。如果你使用@Component,那么你会得到以下日志

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

仍然比注释性侦听器更好、更即时,但仍不会接收到初始化事件。为此,您需要按照here中的说明进行操作

总结一下,

创建目录资源/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 应该允许您执行所需的任何实例化任务。

【讨论】:

【参考方案3】:

您可以使用 ApplicationEnvironmentPreparedEvent 但不能使用 EventListener 注解进行配置。因为此时尚未加载 Bean 定义。请参阅以下链接,了解如何配置此事件。 https://www.thetechnojournals.com/2019/10/spring-boot-application-events.html

【讨论】:

【参考方案4】:

我认为 Spring Cloud Config 是您问题陈述的完美解决方案。详细文档Here

Spring Cloud Config 为分布式系统中的外部化配置提供服务器端和客户端支持。

因此您可以轻松管理应用程序外部的配置,并且所有实例都将使用相同的配置。

【讨论】:

【参考方案5】:

试着在 main 之前加载你需要的一切

SpringApplication.run()

打电话

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

【讨论】:

【参考方案6】:

听起来您想获得 bean 初始化的一部分的所有权。通常人们认为 Spring 完成 bean 配置,但在您的情况下,将 Spring 视为 启动 它可能更容易。

所以,您的 bean 有一些 想要配置的属性,以及一些您想要 Spring 配置的属性。只需注释您希望 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...
  

当然,您的属性解析机制需要以某种方式静态可用,但这似乎适合您 MyPropUtil 示例。

更多地参与其中,您开始直接查看 Bean 后处理器(@PostConstruct 是一种简单的变体)。

有一个以前的问题,有一个有用的答案,这里是How exactly does the Spring BeanPostProcessor work?,但为简单起见,你会做类似的事情

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;
    

显然@PostProcessInitializingBean 更简单,但自定义后处理器有一个很大的优势......它可以与其他Spring 托管bean 一起注入。这意味着你可以通过 Spring 管理你的属性注入,并且仍然手动管理实际的注入过程。

【讨论】:

【参考方案7】:

创建一个将作为属性存储库的 bean,并将其注入其他需要属性的 bean。

在您的示例中,不是在 MyPropUtil 中使用静态方法,而是使用实例方法使类成为 bean 本身。在用@PostConstruct注解的initialize方法中初始化Map&lt;String, Properties&gt; 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) 
    //...
  

@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");
  
  //...

【讨论】:

目前几乎每个地方(超过 1000 个类)都会读取属性。您认为将 PropertyUtil 注入到每个此类和场景中是一种干净的方式(或一个好主意)吗? 是的,这是一种干净的方式。考虑一个 User 实体 (@Entity) 和一个 DAO UserRepository。你会在所有需要这个 DAO 功能的类中注入它吗?是的,注入此 DAO 的类数量无关紧要。 考虑到可以从 1000 多个类中读取属性,因此更有理由使用这种方法并避免使用静态方法来获取这些属性。想象一下,如果有一天您需要更改 MyPropUtil 的工作方式,并且需要用它的一些变体/子类来替换它。 Spring 通过依赖注入提供了许多选项来做到这一点,这与手动将方法从调用 MyPropUtil 更改为 MyPropUtil2.. 相比。当然,这取决于您的业务领域,例如MyPropUtil 未来发生变化的可能性有多大。所以,静态也可以。【参考方案8】:

您可以查看PropertySource 是否对您有帮助。

例子:

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

您可以在每个 @Configuration@SpringBootApplication bean 上使用此注解

【讨论】:

【参考方案9】:

你可以使用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);

我们创建一个 AnnotationConfigWebApplicationContext 并使用 register() 注册一个 web 配置文件。

【讨论】:

【参考方案10】:

您可以直接在命令行中配置外部位置:

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

【讨论】:

【参考方案11】:

我可能错过了“Beans 初始化”的确切含义,可能问题中此类 bean 的示例可能是有益的。

我认为您应该区分属性读取部分和 bean 初始化。 到 bean 初始化时,属性已经被读取并可用。如果你愿意,那是春天魔法的一部分。

这就是为什么下面的代码可以工作的原因:

@Component
public class MySampleBean 

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

这些属性从哪里来并不重要(spring boot defines这些地方有很多不同的方式,它们之间有优先级),它会在bean的初始化发生之前发生。

现在,让我们回到你原来的问题:

我想从类路径或外部位置的属性文件中加载属性(在初始化 bean 之前 - 无关紧要)。

在 spring/spring-boot 中有一个配置文件的概念,它基本上允许创建一个文件 application-foo.properties(或 yaml),当您使用 --spring.profiles.active=foo 加载时,它会自动加载在此 application-foo.properties 中定义的属性到普通的application.properties

所以你可以把你想“从类路径加载”的东西放到application-local.properties(本地这个词只是为了举例)并用--spring.profiles.active=local启动应用程序(在部署脚本中,docker文件或其他)

如果您想从外部位置(类路径之外)运行该属性,您可以使用:--spring.config.location=&lt;Full-path-file&gt;

请注意,即使您将一些属性放入常规 application.properties 并仍然使用具有相同键值对的 --spring.config.location,它们仍将优先于类路径中的属性。

或者,您可以只使用--sring.profiles.active=local or remote,并且根本不使用配置位置。

【讨论】:

【参考方案12】:

如this post 中所述,您可以像这样添加外部属性文件;

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

如果不想用这个,只要在spring开始前用jackson读取属性文件,在main方法中设置属性为System.setProperty("key","value")即可。

如果您也不想使用它,请查看BeanPostProcessor#postProcessBeforeInitialization 方法。它在spring初始化的bean属性之前运行。

【讨论】:

这可能会导致问题,因为它注册了一个额外的PropertySourcesPlaceholderConfigurer,而这些属性在Environment 中不可用。 Spring 会在缺少 PropertySourcesPlaceholderConfigurer 类型的任何 bean 时创建自己的 bean。没有额外的PropertySourcesPlaceholderConfigurer。可以看PropertyPlaceholderAutoConfiguration类代码。 在 1.5 中添加,在早期版本中,这是导致问题的原因。但是您不需要额外的PropertySourcesPlaceholderConfigurer,只需添加一个更容易且不易出错的@PropertySource,并且可以通过Environment 解决属性,如果您决定按照建议的方式加载属性,则无法解决。

以上是关于Spring Boot 2 - 在初始化 bean 之前做一些事情的主要内容,如果未能解决你的问题,请参考以下文章

spring boot 读取 application.properties 初始化bean

在 Spring MVC 中初始化 OAuth WebClient Bean

Spring Boot - 在初始化 DataSource bean 之前运行自定义代码

Spring Boot核心注解

(转)Spring Boot启动过程 和 Bean初始化过程中的拓展接口详解

Spring Boot如何让自己的bean优先加载