📜 ⬆️ ⬇️

Boot yourself, Spring is coming (Part 2)

Evgeny EvgenyBorisov Borisov (NAYA Technologies) and Kirill tolkkv Tolkachev (Cyan.Finance, Twitter ) continue to talk about applying Spring Boot to solving problems of an imaginary Iron Bank of Braavos. In the second part, we will focus on the profiles and intricacies of launching the application.






The first part of the article can be found here .


So, until recently, the customer came and simply demanded to send a crow. Now the situation has changed. Winter came, the wall fell.


First, the principle of issuing loans is changing. If earlier, with a probability of 50%, they were issued to everyone except Starks, now they are issued only to those who return debts. Therefore, we are changing the rules for issuing loans in our business logic. But only for the branches of the bank, which are located where the winter has already arrived, in all the others everything remains as before. I remind you that this is a service that decides whether to issue a loan or not. We just make another service that will work only in winter.


Go to our business logic:


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

We already have a list of those who return debts.


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

And there is a class that is already associated with property - .


 public class ProphetProperties { List<String> ; } 

As in previous times, we simply inject it here:


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

We remember about constructor injection (about magical annotations):


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

Almost done.


Now we have to issue only those who return the debts:


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

But here we have a little problem. Now we have two implementations: the old and the new services.


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

It is logical to separate these bins in different profiles. Profile and profile . Let our new service be launched only in the profile:


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

And the old service - in . :


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

But winter is coming slowly. In the kingdom, located next to the broken wall, it is already winter. But somewhere in the south - not yet. Those. Applications that are located in different branches and time zones should work differently. Under the terms of our task, we cannot erase the old implementation where winter has come, and use the new class. We want bank employees to do nothing at all: we will install an application for them that will work in summer mode until the arrival of winter. And when winter comes, they just restart it and that's it. They will not have to change the code, erase any classes. Therefore, we initially have two profiles: some of the bins are created when summer is, and some of the bins are created when winter is.


But there is another problem:




Now we do not have a single bean, because we specified two profiles, and the application starts in the default profile.


So we have a new requirement from the customer.


Iron law 2. No profile can not




We do not want to raise the context, if the profile is not activated, because winter has already arrived, everything has become very bad. There are certain things that must occur or not, depending on whether or . Also, look at the exception, the text of which is shown above. He does not explain anything. The profile is not set, so there is no implementation of ProphetService . At the same time, no one said that it is necessary to set the profile.


Therefore, we want to now tighten the additional thing in our starter, which, when building the context, will check that some profile has been set. If it is not specified, we will not rise and throw such an exception (and not any exception about the lack of a bin).


Can we do this with the help of our application listener? Not. And there are three reasons for this:



In addition, I still do not know what bugs will appear due to the fact that we began to rise without a profile (suppose I do not know the business logic). Therefore, in the absence of a profile, it is necessary to bring the context at a very early stage. By the way, if you use any Spring Cloud, it becomes even more relevant for you, because the application at an early stage does quite a lot of things.


To implement the new requirement, there is an ApplicationContextInitializer . This is another interface that allows us to extend some Spring point by specifying it in spring.factories.




We implement this interface, and we have a Context Initializer, in which there is a ConfigurableApplicationContext :


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

With it, we can get the environment - the thing that SpringApplication has prepared for us. All property that we gave to him got there. Among other things, it contains profiles.


If there are no profiles there, then we should throw an exception that it is impossible to work like this.


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

Now you need to register it in spring.factories.


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

From the above, it can be guessed that ApplicationContextInitializer is a kind of extension point. ApplicationContextInitializer works when the context is just being built, there are no beans yet.


The question arises: if we wrote ApplicationContextInitializer , why not listen as a listener in a configuration that stretches anyway? The answer is simple: because it should work much earlier, when there is no context and no configurations. Those. it can not be injected yet. Therefore, we prescribe it as a separate thing.


Attempt to launch showed: everything fell quickly enough and reported that we run without a profile. And now we will try to specify some profile, and everything works - the crow is sent.


ApplicationContextInitializer - works when the context has already been created, but there is nothing else in it except the environment.




Who creates the environment? Carlson - SpringBootApplication . He fills it with different meta-information, which then from the context can be pulled. Most things can be injected through @value , something can be obtained from the environment, as we have just received profiles.


For example, different properties come here:



All this is going to and looking into the environment object. There also gets information about which profiles are active. The environment object is the only thing that exists at the time when the Spring Boot starts building a context.


I would like to guess automatically what the profile will be if people forgot to set it up with their hands (we do everything so that bank employees, who are helpless enough without programmers, can run the application - so that everything will work for them, no matter what). To do this, we will add a piece to our starter that will guess the profile - or not - depending on the temperature outside. And with this, another new magic interface, EnvironmentPostProcessor , will help us all, because we need to do this before the ApplicationContextInitializer works. And before ApplicationContextInitializer there is only EnvironmentPostProcessor .


We are again implementing a new interface. There’s just one method that, just like the ConfigurableEnvironment Environment pushes into SpringApplication , because we don’t have the ConfigurableContext yet (we already have it in SpringInitializer , but we don’t have it here; there is only environment).


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

In this environment, we can install a profile. But first you need to check that no one has installed it before. Therefore, in any case, we need the getActiveProfiles check. If people know what they are doing and they set up a profile, then we will not try to guess for them. But if there is no profile, then we will try to understand the weather.


And second, we must understand whether the weather is winter or summer. We will return the temperature -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; } } 

Under this condition, we have winter, and we can set a new profile. We remember that the profile is called :


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

Now you need to specify EnvironmentPostProcessor in spring.factories.


 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 

As a result, the application runs without a profile, we say that this is a production, and we check in which profile it started with us. Magically, we realized that we have a . And the application did not fall, because the ApplicationContextInitializer , which checks if there is a profile, comes next.
Total:


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

We talked about EnvironmentPostProcessor , which works before the ApplicationContextInitializer . But who runs it?




It is started by such a weirdo who, apparently, is an illegitimate son of ApplicationListener and EnvironmentPostProcessor , because it is inherited from both ApplicationListener and EnvironmentPostProcessor . It is called ConfigFileApplicationListener (nobody knows why the "ConfigFile").


He is our Carlson, i.e. Spring Application, provides a prepared environment for listening to two event: ApplicationPreparedEvent and ApplicationEnvironmentPreparedEvent . We will not disassemble now who throws these events. There is another layer (in my opinion already completely superfluous, at least at this stage in the development of Spring), which throws an event that the environment is now being built (parted by Application.yml, properties, environment variables, etc. ).
Having received ApplicationEnvironmentPreparedEvent , the listener understands that it is necessary to configure the environment - to find all the EnvironmentPostProcessor and let them work.




After that, he tells SpringFactoriesLoader to deliver everything that you have prescribed, namely all EnvironmentPostProcessor , in spring.factories. Then he stuffs all of EnvironmentPostProcessor into one List.




and understands that he is also an EnvironmentPostProcessor (part-time), so he shoves himself there,

at the same time sorts them, goes along with them and calls each method postProcessEnvironment .


Thus, all postProcessEnvironment are started at an early stage even before the SpringApplicationInitializer . At the same time, an incomprehensible EnvironmentPostProcessor called ConfigFileApplicationListener also starts.


When the environment is tuned, everything returns to Carlson again.


If the environment is ready, you can build a context. And Carlson starts building context with the ApplicationInitializer . Here we have our own piece, which checks that there is an environment in the context in which there are active profiles. If not, we fall, because otherwise we will still have problems later. Starters continue to work, with all the usual configurations already.


The picture above reflects that Spring is not doing well either. There are such aliens from time to time, single responsibility is not respected and it is necessary to climb there carefully.


Now we want to talk a little about the other side of this strange creature, which is on the one hand a listener, and on the other hand is an EnvironmentPostProcessor .




As an EnvironmentPostProcessor it can load application.yml, application properties, any environment variable, command arguments, etc. And as a listener, he can listen to two events:



The question arises:




All these events were in the old Spring. And the ones we talked about above are the Spring Boot event (special events that he added for his life cycle). And their whole pack. These are the main ones:



This list is far from all. But it is important that some of them belong to Spring Boot, and some - to Spring (good old ContextRefreshedEvent , etc.).


The caveat is that not all of these events can be obtained in the application (mere mortals - different grandmothers - can’t just listen to complex events that Spring Boot throws). But if you know about the secret mechanisms of spring.factories and define your Application Listener at the level of spring.factories, then these event-s of the earliest stage of the application start get to you.




As a result, you can influence the start of your application at a rather early stage. The joke, however, is that part of this work has been moved to other entities, such as EnvironmentPostProcessor and ApplicationContextInitializer .


It was possible to do everything on the listener, but it would be inconvenient and ugly. If you want to listen to all the events that Spring throws, not just ContextRefreshedEvent and ContextStartedEvent , then you don’t need to prescribe the listener as a bin in the usual way (otherwise it is created too late). It should also be registered through spring.factories, then it will be created much earlier.


By the way, when we looked at this list, it was not clear to us when ContextStartedEvent and ContextStoppedEvent work at all?




It turned out that these events never work at all. We have long puzzled over what kind of events we need to catch in order to understand that the application has really started. And it turned out that the events we are talking about now appear when you forcibly pull methods from the context:



Those. SpringApplication.run will come only if we run SpringApplication.run , get the context, drag it with ctx.start(); or ctx.stop(); . It is not very clear why this is necessary. But you, again, were given an extension point.


Does Spring have anything to do with this? If so, there should be an exception somewhere:



In fact, it will be on the last line, because after ctx.close(); with the context nothing can be done. But call ctx.stop(); before ctx.start(); - it is possible (Spring simply ignores these event-s - they are just for you).


Write your listeners, listen for yourself, think up your laws, what to do at ctx.stop(); , and what to do on ctx.start(); .


Total scheme of interaction and application life cycle looks like this:




The colors here show different periods of life.



If you noticed, in the process of a two-part report we went from right to left: we started from the very end, screwed up the configuration that came from the starter, then added the following, etc. Now let's quickly talk through this whole chain in the opposite direction.
You write in your main SpringApplication.run . He finds different listeners, throws an event to them that he has started to build. After this, listeners find EnvironmentPostProcessor , let them set up the environment. Once the environment is set up, we begin to build the context (Carlson enters). Carlson builds the context and allows all the Application Initializer to do something with this context. We have an extension point. After that, the context is already set up and the same thing starts happening next as in the usual Spring application, when the context is built - BeanFactoryPostProcessor , BeanPostProcessor , the bins are set up. This deals with the usual Spring.


How to run


We have finished discussing the process of writing an application.


But we had one more thing developers don't like. They do not like to think, how in the end their application will start. Will the admin run it in Tomcat, JBoss or WebLogic? It just has to work. If it doesn't work, in the worst case, the developer has to set something up again.


So, what are our ways to start?



Tomcat is not a massive trend, we will not talk about it in detail.


Idea is also, in principle, not very interesting. There is just a little smarter than I will tell below. But in Idea, in principle, there should be no problems. She sees what dependences the starter will bring.
If we do java -jar , the main problem is to build the classpath before we run the application.


What did people do in 2001? They wrote java -jar , which jar should be run, then a space, classpath=... and there they specified scripts. In our case, there are 150 MB of different dependencies that have added starters. And all this would have to be done manually. Naturally, no one does. We just write: java -jar , which jar should be run and that's it. Somehow the classpath is still built. About this we will now talk.


Let's start with the preparation phase of the jar file so that it can be started without Tomcat at all. Before you make java -jar , you need to build a jar. This jar should obviously be unusual, some kind of analog of war, where everything will be inside, including the embedded Tomcat.


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

When we downloaded the project, we have already registered a plugin in POM. Here, by the way, you can throw in configurations, but more on that later. As a result, in addition to the usual jar, which builds Maven or Gradle of your application, another unusual jar is built. On the one hand, it looks fine:




But if you look from the side:




This is basically an analogue of 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.




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


Those. . 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:



JarLauncher . Tomcat WarLauncher, war- , jar-.


, java -jar . ? Can. .


 <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 :



findings


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




Minute advertising. 19-20 Joker 2018, « [Joker Edition]» , «Micronaut vs Spring Boot, ?» . , Joker . .

')

Source: https://habr.com/ru/post/425333/


All Articles