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;
从文档来看,ApplicationEvents
和 ApplicationInitializers
似乎符合我的需要,但我无法让它们为我的问题陈述工作。
【问题讨论】:
为什么不添加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
,或任何您喜欢的风格),然后使用 @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...
当然,您的属性解析机制需要以某种方式静态可用,但这似乎适合您 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;
显然@PostProcess
或InitializingBean
更简单,但自定义后处理器有一个很大的优势......它可以与其他Spring 托管bean 一起注入。这意味着你可以通过 Spring 管理你的属性注入,并且仍然手动管理实际的注入过程。
【讨论】:
【参考方案7】:创建一个将作为属性存储库的 bean,并将其注入其他需要属性的 bean。
在您的示例中,不是在 MyPropUtil
中使用静态方法,而是使用实例方法使类成为 bean 本身。在用@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)
//...
和
@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=<Full-path-file>
请注意,即使您将一些属性放入常规 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 之前运行自定义代码