自己动手,春天来了(第2部分)

Evgeny EvgenyBorisov Borisov(NAYA Technologies)和Kirill Tolkkv Tolkachev(Cyan.Finance, Twitter )继续谈论使用Spring Boot解决假想的Braavos铁库的问题。 在第二部分中,我们将重点介绍启动应用程序的配置文件和细微之处。






文章的第一部分可以在这里找到。


因此,直到最近,客户来了,才要求发送乌鸦。 现在情况已经改变。 冬天来了,城墙倒塌了。


首先,发放贷款的原则正在发生变化。 如果以前他们以50%的概率向除Starks以外的所有人进行了捐赠,现在他们仅向偿还债务的人进行了捐赠。 因此,我们正在业务逻辑中更改发放贷款的规则。 但是,仅对于位于冬天已经到来的银行分支机构而言,其余的一切都照旧进行。 我想提醒您,这是一项决定是否授予贷款的服务。 我们将提供仅在冬季工作的另一项服务。


我们遵循我们的业务逻辑:


public class WhiteListBasedProphetService implements ProphetService {  @Override  public boolean willSurvive(String name) {    return false;  } } 

我们已经有了偿还债务者的清单。


 spring: application.name: money-raven jpa.hibernate.ddl-auto: validate ironbank: ---:   -  : -: ,   : true 

还有一类已经与财产相关的阶级-


 public class ProphetProperties { List<String> ; } 

和以前一样,我们只在这里注入它:


 public class WhiteListBasedProphetService implements ProphetService { private final ProphetProperties prophetProperties; @Override public boolean willSurvive(String name) {   return false; } } 

请记住关于构造函数注入(关于魔术注释):


 @Service @RequiredArgsConstructor public class WhiteListBasedProphetService implements ProphetService { private final ProphetProperties prophetProperties; @Override public boolean willSurvive(String name) {   return false; } } 

快完成了


现在,我们必须只向偿还债务的人分发:


 @Service @RequiredArgsConstructor public class WhiteListBasedProphetService implements ProphetService { private final ProphetProperties prophetProperties; @Override public boolean willSurvive(String name) { return prophetProperties.get().contains(name); } } 

但是这里有一个小问题。 现在,我们有两个实现:旧服务和新服务。


 Description Parameter 1 of constructor in com.ironbank.moneyraven.service.TransferMoneyProphecyBackend… - nameBasedProphetService: defined in file [/Users/tolkv/git/conferences/spring-boot-ripper… - WhileListBackendProphetService: defined in file [/Users/tolkv/git/conferences/spring-boot-ripper... 

将这些bean分成不同的配置文件是合乎逻辑的。 资料资料 。 让我们的新服务仅在配置文件中运行:


 @Service @Profile(ProfileConstants.) @RequiredArgsConstructor public class WhiteListBasedProphetService implements ProphetService { private final ProphetProperties prophetProperties; @Override public boolean willSurvive(String name) {   return prophetProperties.get().contains(name); } } 

而且旧服务是在


 @Service @Profile(ProfileConstants.) public class NameBasedProphetService implements ProphetService { @Override public boolean willSurvive(String name) {   return !name.contains("Stark") && ThreadLocalRandom.current().nextBoolean(); } } 

但是冬天来得很慢。 在破墙旁的王国里,已经是冬天了。 但是在南部的某个地方-还没有。 即 位于不同分支和时区的应用程序应该以不同的方式工作。 根据任务的条件,我们不能删除冬天来了的旧实现并使用新类。 我们希望银行员工什么都不做:我们将向他们提供一个可以在夏季模式下运行直到冬季到来的应用程序。 当冬天来临时,他们只是重新开始就这样。 他们将不必更改代码,删除任何类。 因此,我们最初有两个配置文件:部分垃圾箱是在夏天创建的,而部分垃圾箱是在冬天创建的。


但是出现另一个问题:




现在我们没有单个bean,因为我们指定了两个配置文件,并且应用程序在默认配置文件中启动。


因此,我们对客户提出了新要求。


铁法2.禁止个人资料




如果不激活概要文件,我们不想提出上下文,因为冬天已经来临,一切都变得非常糟糕。 根据还是 ,某些事情是否应该发生。 另外,请查看异常,其文本已在上面给出。 他什么也没解释。 未定义配置文件,因此没有ProphetService实现。 同时,没有人说有必要设置配置文件。


因此,我们现在要在启动器中拧入另一块,在构建上下文时,它将检查是否设置了某些配置文件。 如果未设置,我们将不会抛出此类异常(并且不会缺少缺少bin的某些异常)。


我们可以使用我们的应用程序监听器来做到这一点吗? 不行 这有三个原因:


  • 单一责任听众负责使乌鸦飞起来。 侦听器不应检查配置文件是否已激活,因为激活配置文件不仅会影响侦听器本身,还会影响更多。
  • 构建上下文时,会发生不同的事情。 如果不设置配置文件,我们不希望它们开始发生。
  • 解析上下文后,侦听器将在最后工作。 而且没有资料这一事实,我们早就知道了。 为什么要等待这些有条件的五分钟,直到服务几乎上升,然后一切都下降。

另外,由于我们没有配置文件就开始上升,我仍然不知道会出现什么错误(假设我不知道业务逻辑)。 因此,在没有概要文件的情况下,有必要在很早的阶段就取消上下文。 顺便说一句,如果您使用任何Spring Cloud,这对您来说就变得更加重要,因为该应用程序在早期阶段会做很多事情。


为了实现新要求,有ApplicationContextInitializer 。 这是另一个接口,允许我们通过在spring.factories中指定来扩展Spring的某些点。




我们实现了此接口,并且有一个Context Initializer,它具有ConfigurableApplicationContext


 public class ProfileCheckAppInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { } } 

有了它,我们可以获得环境-SpringApplication为我们准备的东西。 我们交给他的所有财产都到了那里。 除其他外,它们还包含配置文件。


如果那里没有个人资料,那么我们应该抛出一个例外,说你不能那样做。


 public class ProfileCheckAppInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { if applicationContext.getEnvironment().getActiveProfiles().length == 0 {     throw new RuntimeException("  !");   } } } 

现在您需要在spring.factories中注册这些东西。


 org.springframework.boot.context.properties.EnableConfigurationProperties=com.ironbank.moneyraven.starter.IronConfiguration org.springframework.context.ApplicationContextInitializer=com.ironbank.moneyraven.starter.ProfileCheckAppInitializer 

从上面可以看出, ApplicationContextInitializer是扩展点。 当上下文刚刚开始构建时, ApplicationContextInitializer可以工作,但是还没有容器。


出现了一个问题:如果我们编写了ApplicationContextInitializer ,为什么作为监听器不以一种可扩展的配置编写它呢? 答案很简单:因为在没有上下文和配置的情况下它应该可以更早地工作。 即 它不能被注入。 因此,我们将其规定为单独的部分。


尝试启动表明一切都已经足够快了,并报告说我们没有配置文件就开始了。 现在,让我们尝试指定一些配置文件,一切正常-乌鸦已发送。


ApplicationContextInitializer在已经创建上下文时执行,但是除了环境之外没有其他内容。




谁创造环境? Carlson- SpringBootApplication 。 他用各种元信息填充了该信息,然后可以将其从上下文中撤出。 大多数东西可以通过@value注入,某些东西可以从环境中获得,因为我们刚刚获得了配置文件。


例如,以下是不同的属性:


  • Spring Boot可以构建;
  • 在启动时通过命令行传输的内容;
  • 系统性
  • 说明为环境变量;
  • 应用属性中规定的内容;
  • 在其他一些属性文件中注册。

所有这些都被收集并设置到环境对象中。 它还包含有关哪些配置文件处于活动状态的信息。 Spring Boot开始构建上下文时,环境对象是唯一存在的对象。


我想自动猜测一下,如果人们忘记了用手问该配置文件的情况(我们会做所有事情,以便那些没有程序员的无奈银行雇员可以启动该应用程序,以便无论什么情况都对他们有用)。 为此,我们将向启动器添加一种可以根据街道温度猜测轮廓( 与否)的事物。 另一个新的魔术界面将为我们所有人提供帮助EnvironmentPostProcessor ,因为我们需要在ApplicationContextInitializer工作之前执行此操作。 在ApplicationContextInitializer之前,只有EnvironmentPostProcessor


我们再次实现一个新的接口。 只有一种方法,以与ConfigurableEnvironment引发SpringApplication方式相同的方式,因为我们还没有ConfigurableContext (它已经存在于SpringInitializer它不在这里;只有环境)。


 public class ResolveProfileEnvironmentPostProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { } } 

在这种环境下,我们可以设置配置文件。 但是首先,您需要检查之前没有人安装过它。 因此,我们getActiveProfiles需要检查getActiveProfiles 。 如果人们知道自己在做什么,并设置了个人资料,那么我们将不会为他们猜测。 但是,如果没有个人资料,我们将尝试通过天气了解。


第二点-我们必须了解我们现在是冬季还是夏季。 我们将返回-300的温度。


 public class ResolveProfileEnvironmentPostProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { if (environment.getActivePrifiles().length == 0 && getTemperature() < -272) {   } } public int getTemperature() { return -300; } } 

在这种情况下,我们有了冬天,我们可以建立一个新的轮廓。 我们记得该配置文件称为


 public class ResolveProfileEnvironmentPostProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {   if (environment.getActivePrifiles().length == 0 && getTemperature() < -272) { environment.setActiveProfiles("");   } else { environment.setActiveProfiles("");   } } public int getTemperature() { return -300; } } 

现在我们需要在spring.factories中指定EnvironmentPostProcessor


 org.springframework.boot.context.properties.EnableConfigurationProperties=com.ironbank.moneyraven.starter.IronConfiguration org.springframework.context.ApplicationContextInitializer=com.ironbank.moneyraven.starter.ProfileCheckAppInitializer org.springframework.boot.env.EnvironmentPostProcessor=com.ironbank.moneyraven.starter.ResolveProfileEnvironmentPostProcessor 

结果,该应用程序启动时没有配置文件,我们说它是正式的,然后检查它是从我们那里启动的。 神奇地,我们意识到我们的概况是 。 而且应用程序没有崩溃,因为接下来要检查是否有配置文件的ApplicationContextInitializer
结果:


 public class ResolveProfileEnvironmentPostProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {   if (getTemperature() < -272) {     environment.setActiveProfiles("");   } else {     environment.setActiveProfiles("");   } } private int getTemperature() {   return -300; } } 

我们讨论了EnvironmentPostProcessor ,它在ApplicationContextInitializer之前运行。 但是谁来运行它?




此异常启动了它,显然是ApplicationListenerEnvironmentPostProcessor的非法儿子,因为它是从ApplicationListenerEnvironmentPostProcessor继承的。 它称为ConfigFileApplicationListener (为什么使用“ ConfigFile”-没人知道)。


他是我们的卡尔森,即 Spring Application提供了一个准备好的环境来监听两个事件: ApplicationPreparedEventApplicationEnvironmentPreparedEvent 。 现在我们将不分析谁引发这些事件。 还有另外一层(至少在Spring开发的这个阶段,我认为已经完全多余了),它引发了一个事件,即环境开始构建(解析Application.yml,属性,环境变量等)。 )
接收到ApplicationEnvironmentPreparedEvent ,侦听器了解您需要配置环境-找到所有的EnvironmentPostProcessor并使它们工作。




之后,他告诉SpringFactoriesLoader将您订购的所有内容(即所有EnvironmentPostProcessor交付给spring.factories。 然后将整个EnvironmentPostProcessor填充到一个列表中。




并且知道他也是(同时)是EnvironmentPostProcessor ,因此他将自己推到了那里,

同时,它们对它们进行排序,处理并调用每个方法的postProcessEnvironment


这样,所有postProcessEnvironment都在SpringApplicationInitializer之前的早期阶段SpringApplicationInitializer 。 在这种情况下,还将启动一个名为ConfigFileApplicationListener的令人费解的EnvironmentPostProcessor


设置环境后,一切将再次返回卡尔森。


如果环境准备就绪,则可以构建上下文。 卡尔森开始使用ApplicationInitializer构建上下文。 在这里,我们有自己的作品,该作品检查上下文中是否存在活动配置文件的环境。 如果不是,那我们就倒下了,因为否则我们以后还会遇到问题。 然后,启动器便开始工作,并且已经具有所有常规配置。


上图反映了Spring也不是很好。 这样的外星人会定期在那儿见面,不遵守单一责任,您需要小心攀爬。


现在,我们想谈谈这种奇怪生物的另一面,它的一侧是侦听器,另一侧是EnvironmentPostProcessor




EnvironmentPostProcessor一样EnvironmentPostProcessor它可以加载application.yml,应用程序属性,各种环境变量,命令参数等。 作为收听者,他可以收听两个事件:


  • ApplicationPreparedEvent
  • ApplicationEnvironmentPreparedEvent

问题是:




所有这些事件都发生在旧的春天。 上面我们讨论的是Spring Boot中的事件(他为生命周期添加的特殊事件)。 还有一大堆。 这些是主要的:


  • ApplicationStartingEvent
  • ApplicationEnvironmentPreparedEvent
  • ApplicationPreparedEvent
  • ContextRefreshedEvent
  • EmbeddedServletContainerInitializedEvent
  • ApplicationReadyEvent
  • ApplicationFailedEvent

这个列表远非所有。 但是重要的是它们中的一些与Spring Boot相关,而与Spring相关(很好的旧ContextRefreshedEvent等)。


需要注意的是,并非所有这些事件都可以在应用程序中接收到(只是凡人-不同的祖母-不能只听Spring Boot引发的复杂事件)。 但是,如果您了解spring.factories的秘密机制,并在spring.factories级别定义了应用程序侦听器,那么从应用程序启动的最早阶段就可以了解这些事件。




因此,您可以在相当早的阶段影响应用程序的启动。 可笑的是,这项工作的一部分是在其他实体中进行的,例如EnvironmentPostProcessorApplicationContextInitializer


您可以对侦听器进行所有操作,但这将带来不便和丑陋。 如果您想侦听Spring引发的所有事件,而不仅仅是ContextRefreshedEventContextStartedEvent ,那么您就不需要像通常那样设置侦听器(如Bean)(否则创建得太晚了)。 还必须通过spring.factories注册它,然后才能更早地创建它。


顺便说一下,当我们查看此列表时,我们还不清楚ContextStartedEventContextStoppedEvent何时触发?




事实证明,这些事件根本不起作用。 为了了解应用程序是否真正启动,我们为应该捕获哪些事件困扰了很长时间。 事实证明,当您从上下文中强制拉出方法时,就会出现我们正在讨论的事件:


  • ctx.start(); -> ContextStartedEvent
  • ctx.stop(); -> ContextStoppedEvent

即 仅当我们运行SpringApplication.run ,获取上下文并从中拉出ctx.start();SpringApplication.run才会发生ctx.start();ctx.stop(); 。 尚不清楚为什么有必要这样做。 但是,他们又给了您一个扩展点。


Spring与此有关吗? 如果是这样,某处应该有一个例外:


  • ctx.stop(); (1)
  • ctx.start(); (2)
  • ctx.close(); (3)
  • ctx.start(); (4)

实际上,它将在最后一行,因为在ctx.close();之后ctx.close(); 上下文无能为力。 但是调用ctx.stop();ctx.start();之前ctx.start(); -您可以(Spring只会忽略这些事件-它们仅适合您)。


写您的听众,听自己,提出您的法律,在ctx.stop();上做什么? ,以及在ctx.start();上做什么? 。


总体而言,交互和应用程序生命周期图如下所示:




这里的颜色显示了生命的不同时期。


  • 蓝色是Spring Boot,应用程序已经启动。 这意味着将处理来自客户端的Tomcat服务请求,肯定会引发整个上下文,所有Bean都在工作,数据库已连接,等等。
  • 绿色-事件ContextRefreshedEvent到达并构建了上下文。 例如,从这一刻起,应用程序侦听器开始工作,您可以通过设置ApplicationListener批注或通过具有监听某些事件的泛型的同名接口来实现。 如果要接收更多事件,则需要在spring.factories中编写相同的ApplicationListener(通常的Spring在这里工作)。 条形图指示“ Spring Ripper”报告的开始位置。
  • 在较早的阶段,SpringApplication可以工作,为我们准备上下文。 这是准备我们常规的Spring开发人员时所做的应用程序的工作。 例如,配置的WebXML。
  • 但是,甚至还有更早的阶段。 它显示了谁,在哪里和为谁工作。
  • 仍然存在一个灰色的阶段,在其中不可能以任何方式楔入。 这是SpringApplication开箱即用的阶段(仅进入代码)。

如果您注意到,在分为两部分的报告中,我们从右到左:从头开始,拧紧了从启动器飞来的配置,然后添加了以下内容,依此类推。 现在,让我们以相反的方向快速讨论整个链条。
您在主SpringApplication.run 。 他找到了不同的听众,并向他们抛出了他开始建立的事件。 之后,侦听器找到EnvironmentPostProcessor ,让他们配置环境。 设置好环境后,我们便开始构建上下文(Carlson进入)。 卡尔森构建上下文,并使所有应用程序初始化程序都可以在此上下文中执行某些操作。 我们有一个扩展点。 此后,已经配置了上下文,然后开始与常规Spring应用程序中发生的事情一样,在构建上下文时BeanFactoryPostProcessorBeanPostProcessor ,bean被配置。 这就是普通Spring所做的。


怎么跑


我们已经完成了编写应用程序的过程的讨论。


但是我们还有另一件事是开发人员不喜欢的。 他们不喜欢最终考虑如何开始应用程序。 管理员将在Tomcat,JBoss或WebLogic中运行它吗? 它只是必须工作。 如果它不起作用,在最坏的情况下,开发人员将不得不再次配置一些内容


那么我们的启动方法是什么?


  • 雄猫战争
  • 主意
  • java -jar/war

Tomcat不是一个大趋势,我们将不对其进行详细讨论。


原则上,想法也不是很有趣。 这比我在下面讲的要棘手。 但是在思想上,原则上应该没有问题。 她知道启动程序将带来什么样的依赖性。
如果执行java -jar ,则主要问题是在启动应用程序之前先构建类路径。


人们在2001年做了什么? 他们写了java -jar应该运行哪个jar,然后写了一个空格, classpath=...并在其中指示了脚本。 在我们的案例中,初学者添加了150 MB的各种依赖项。 所有这些都必须手动完成。 自然,没有人这样做。 我们只写: java -jar ,应该运行哪个jar就是这样。 不知何故,类路径仍在构建中。 我们现在将讨论这个。


让我们从准备jar文件开始,以便甚至在没有Tomcat的情况下也可以启动它。 在创建java -jar之前,您需要构建一个jar。 这个jar很明显应该是不寻常的,类似战争的东西,里面所有东西都在里面,包括嵌入式Tomcat。


 <build> <plugins>    <plugin>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-maven-plugin</artifactId>    </plugin> </plugins> </build> 

当我们下载项目时,已经有人在我们的POM中注册了一个插件。 顺便说一下,您可以在这里抛出配置,但稍后会介绍更多。 jar, Maven Gradle , jar . :




:




war-.


, .


, jar. java -jar , , , , org.springframework.boot . . org.springframework.boot package. META-INF




Spring Boot MANIFEST ( Maven Gradle), main class, jar-.


, jar- : -, main-. java -jar -jar, , main-class-.


, , MANIFEST, main-class , main ( Idea). , . class path? java -jar , main, , — main, . MANIFEST JarLauncher.




即 , , JarLauncher. , main, class path.
, main? property — Start-class .


即 . class path jar. , — org.springframework.boot — class path. org.springframework.boot.loader.JarLauncher main-class. , main-class . class path, BOOT-INF ( lib class , ).


RavenApplication, properties class BOOT-INF , , Tomcat , BOOT-INF/lib/ . JarLauncher classpath, — , start-class . Spring, ContextSpringApplication — flow, .


, start-class-? , . , .


, . property, mainClass , MANIFEST Start-Class , mainClass — JarLauncher.


, mainClass, ? . Spring boot plugin – mainClass:


  • – . — main class;
  • – , mainClass @SpringBootApplication , , , ;
  • — exception , main class, , jar- . 即 , , . , , main class.
  • @SpringBootApplication — .

JarLauncher . Tomcat WarLauncher, war- , jar-.


, java -jar . ? 可以的 .


 <build> <plugins>    <plugin>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-maven-plugin</artifactId>       <configuration>          <executable>true</executable>       </configuration>    </plugin> </plugins> </build> 

<configuration> <executable>true</executable> Gradle , :


 springBoot { executable = true } 

jar executable jar. .


, . Windows , exe-, . Spring Boot, .. jar, . , .
?


(jar — zip-, ):




Spring Boot - .


-, jar-. , , — #!/bin/bash . .


. exit 0 - — zip-.




, zip- — 0xf4ra . , , .




(, ..).


jar :


  • — ;
  • , " bash" ( #!/bin/bash );
  • bash ;
  • exit 0 ;
  • java -jar — jar-, ;
  • java -jar zip- jar-, , , .

结论


, Spring Boot — , , .


-, . , Spring, Spring — Spring Boot. , , — , , , . , , Spring, Spring Boot .


-, @SpringBootApplication , best practice, Spring-.


— , , . property environment variable, var arg , , JSON. @value , . configuration properties , , , . , Spring . , , .


. , . Spring, Spring Boot . - , , , .




分钟的广告。 19-20 Joker 2018, « [Joker Edition]» , «Micronaut vs Spring Boot, ?» 。 , Joker . .

Source: https://habr.com/ru/post/zh-CN425333/


All Articles