preface
This is the last core article in the Spring series.
After reading the source code of SpringBoot, I was impressed by the designers of Spring. There are not many lines of code in the Spring series that we cannot understand, but it is difficult to understand the design idea. It takes time to read Spring, SpringMVC and SpringBoot. It took me 17 days to get a general understanding at home, including the time to write articles and verify the code of every knowledge point. If these are excluded, I think I can get familiar with them in about 10 days.
The Spring core alone has over 3000 Java files, the Web module has 1368, and SpringBoot is relatively small at 1,880. Seeing these numbers gives us a sense of where to start.
I also recommend an old book, 2006, called Advanced Programming for the Spring Framework, written by one of the original Spring people. Although he is old, he rarely talks about code, but instead talks about Spring design ideas.
The body of the
We have already talked about Spring and SpringMVC in the previous chapters. This chapter talks about the last chapter, SpringBoot. Many people don’t know the difference. Its core is often said IOC container and AOP, container storage is instantiated after the object, AOP depends on two technologies to achieve, CGLIB and JDK Proxy, if the use of Spring alone development program, we can do desktop programs, can also do command line programs, but if you want to do back-end interface, This will require us to implement servlets ourselves, and we must be very familiar with Spring.
But then SpringMVC came along, and that led to the end of servlets. There was a more convenient way to write it, which was to tag the @Controller annotation on a class with @getMapping to implement an interface. SpringMVC also provided parameter converters. To make it easier to retrieve parameters from requests, the heart of SpringMVC is the DispatcherServlet, which you learned about in the previous chapter.
Later SpringBoot was born, SpringBoot does not have any new technology, it is more assembly ability, it is based on SpringMVC, Servlet container is hidden, because when building SpringMVC, also need to configure such as Tomcat, this is also a troublesome step. SpringBoot simply encapsulates the process of configuring it, so a novice can implement a back-end API in a matter of minutes.
But it’s not just about hiding the Servlet container.
So let’s have an in-depth analysis of SpringBoot, mainly by the following points:
- Principle of Automatic configuration
- Embedded Tomcat principles
- What does @enableWebMVC do
- WebMvcConfigurationSupport and WebMvcConfigurer difference
- Jar Startup Principle
- War Startup Principle
Principle of Automatic configuration
Automatic configuration is not a big deal. It’s a solution to a problem that Spring can’t scan. As we’ve seen in previous chapters, objects can enter containers only if they are tagged with the @Component annotation and can be scanned by Spring, or if they are registered manually.
Then think of a problem like this: your Spring application has been developed, packaged into a JAR, and deployed online, but one day you need to add functions, and this function needs to be in a separate JAR, and your main project depends on this JAR, but the trouble is that you need to modify the scan path in the main project. Or add @ComponentScan so that the objects in the JAR can be managed in the main project.
One day the main project will rely on jars developed by other teams with different package names, so add @ComponentScan to the main project.
Is that a bit of a hassle?
So somebody said, can we have a convention that external jars follow, that the main project loads according to?
The convention here is that a file contains the class names you want to be managed by Spring, and when the jar is scanned for the file in the main project, the class names from the file contents are registered with the container.
Then this file is what’s known as spring.factories, and you can start loading by convention with @enableAutoConfiguration.
@EnableAutoConfiguration has probably heard of it, it’s labeled @SpringBootApplication,
The @enableAutoConfiguration annotation also has an @import annotation. The @import annotation is used to Import some beans into the container. The annotation itself is not useful.
For a SpringBoot below this kind of method to start, we need to understand the working principle of the annotation above the first key information under the ConfigurationClassPostProcessor.
@SpringBootApplication
public class SampleWebUiApplication {
public static void main(String[] args) throws IOException { SpringApplication.run(SampleWebUiApplication.class, args); }}Copy the code
ConfigurationClassPostProcessor classes in the Spring to refresh (), detailed information can see it in the Spring of the second chapter.
ConfigurationClassPostProcessor to SampleWebUiApplication parsing, we put the label in SampleWebUiApplication annotation on the class together, First of all, this class has a @ ComponentScan annotation, then when ConfigurationClassPostProcessor execution can extract the information on the @ ComponentScan, scanned for bean, Spring, If the basePackages on @ComponentScan are empty, the default basePackages will be the package he’s in, which is why it’s common to put the “startup class” in the outermost layer so that all of his child packages can be scanned.
The next step is to determine if there is an @import annotation, which there is, which is the one below.
@Import(AutoConfigurationImportSelector.class)
Copy the code
If there is a @ Import, instantiation of his value, the value here is AutoConfigurationImportSelector, and there are three kinds of situation, which is a situation in which the value inheritance ImportSelector, Spring then decides that this class is used to import new beans into the container and calls its selectImports.
This class is the key class that we automatically configure.
He will get all the jar in the path of the meta-inf/spring. Factories all org. Springframework. Boot. Autoconfigure. EnableAutoConfiguration values, If you open any spring.Factories file, you’ll see more or less the following configuration.
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration,\
org.springframework.boot.devtools.autoconfigure.DevToolsR2dbcAutoConfiguration,\
org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration,\
org.springframework.boot.devtools.autoconfigure.RemoteDevToolsAutoConfiguration
Copy the code
It is loaded via the SpringFactoriesLoader, which is a Spring class that you rarely see used in Spring.
This is the convention we mentioned above, so that any JAR that follows this convention can register objects with Spring. Here is a brief look at the source code, if you want to explore in detail, the focus object is still the SpringFactoriesLoader.
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if(! isEnabled(annotationMetadata)) {return NO_IMPORTS;
}
/ * * * to get all the jar is located in the path to the meta-inf/spring. Factories all ` org. Springframework. Boot. Autoconfigure. EnableAutoConfiguration ` value * /
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
/** * returns */ when converted to an array
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
Copy the code
This is auto-configuration. It’s very simple, but the benefits are many.
Embedded Tomcat principles
If Tomcat does not support embedding, SpringBoot may not be able to do it, or it may find another way.
Tomcat itself have a Tomcat class, yes call Tomcat, the full path is org. The apache. Catalina. Startup. Tomcat, we’d like to start a Tomcat, direct new Tomcat (), after calling start ().
And it provides the basics of adding servlets and configuring connectors.
Let’s look at an example.
public class Main {
public static void main(String[] args) {
try {
Tomcat tomcat =new Tomcat();
tomcat.getConnector();
tomcat.getHost();
Context context = tomcat.addContext("/".null);
tomcat.addServlet("/"."index".new HttpServlet(){
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("hello"); }}); context.addServletMappingDecoded("/"."index");
tomcat.init();
tomcat.start();
}catch (Exception e){}
}
}
Copy the code
After startup, visit http://localhost:8080/ and you can see the response Hello.
The next step is to analyze where SpringBoot created Tomcat and where the DispatcherServlet was added.
The first thing you need to know is the extension functions provided by Spring itself, because SpringBoot is built on top of Spring.
The need to focus on AbstractApplicationContext refresh () method, Spring’s start-up process, so this method one calls the template method onRefresh (), in his comment on also has said, left to do extension, This method is called when all beans have been collected but not instantiated.
When we develop Web application, the realization of his classes are AnnotationConfigServletWebServerApplicationContext, and create a Tomcat is rewritten in his onRefresh (), as follows.
@Override
protected void onRefresh(a) {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex); }}Copy the code
private void createWebServer(a) {
WebServer webServer = this.webServer;
/ * * * ServletContext is Tomcat starts by ServletContainerInitializer callback * /
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
/** * service creates factory, */
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
/** * Tomcat will be called back to {@link ServletWebServerApplicationContext#selfInitialize(ServletContext)}
*/
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown".new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop".new WebServerStartStopLifecycle(this.this.webServer));
}
else if(servletContext ! =null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
Copy the code
There are two key points here. One is the Web service creation factory, ServletWebServerFactory. Because there are many Web server types, besides Tomcat, Jetty and Undertow, the server created by each factory is wrapped in a WebServer object and returned. WebServer provides basic methods for starting and stopping services, and instantiating the corresponding ServletWebServerFactory is not a direct new, it goes through the Spring creation process, because it is designed to be an extension, Have a WebServerFactoryCustomizerBeanPostProcessor post processor is responsible for calling all WebServerFactoryCustomizer customize () method, used to customize the service configuration, You can modify the listening port and other information here.
Then there will be a direction of Spring to add WebServerFactoryCustomizerBeanPostProcessor, is actually in the Spring. The factories, Responsible for the import of class is ServletWebServerFactoryAutoConfiguration.
Second, ServletContainerInitializer, this is defined in the Servlet specification, by each Servlet container to call, used to do some initialization, Usually put his implementation class in the jar in the meta-inf/services/javax.mail servlet. ServletContainerInitializer, Tomcat will scan, if any is invoked, As well as through the Context addServletContainerInitializer, this Context said a Web application instance of Tomcat.
The ServletContext is basically passed in for the callback.
SpringBoot uses the second method. After Tomcat is successfully created, the TomcatStarter object is generated and sent to Tomcat. After Tomcat is successfully started, the onStartup object will be called back.
But difficult in TomcatStarter onStartup would call all his save ServletContextInitializer object, take note, this is two different objects.
ServletContainerInitializer is in the Servlet specification. ServletContextInitializer SpringBoot is in.Copy the code
One of his saved instances is passed by method reference.
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer(a) {
return this::selfInitialize;
}
Copy the code
So you end up calling back here, and notice that this is being called by Tomcat.
private void selfInitialize(ServletContext servletContext) throws ServletException {
/** * the Tomcat OnStart phase will call back to this */
/** * Set ServletContext below */
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
/ * * * from the container for all currently ServletContextInitializer, and call the * * is the most critical DispatcherServletRegistrationBean * /
for(ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); }}Copy the code
Here’s where it gets interesting. TomcatStarter has three instances by default that do different things. Note that none of these instances are in the Spring container. The three one is above the selfInitialize method, this method is specific to do is get all ServletContextInitializer instance, in the Spring container and, in turn, calls the onStartup () method, And one of the examples are DispatcherServletRegistrationBean, see the name will know that this object must have a little things.
What did we enter DispatcherServletRegistrationBean onStartup as, here not tracking source, he did it is to add DispatcherServlet ServletContext context.
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
String name = getServletName();
return servletContext.addServlet(name, this.servlet);
}
Copy the code
But add the Servlet, also didn’t give him a mapping, then take the configure () is doing this, we almost have not used ServletRegistration. Dynamic, this also is in the Servlet specification, not SpringBoot, his role is to add mapping, Let the specified URL reach this Servlet, get it by adding Servlet above, and the url path of DispatcherServlet is /.
We said just now, he will get all ServletContextInitializer instance, in the Spring container and, in turn, calls, then we can achieve a ServletContextInitializer, @ Component tag, You can also add servlets.
But SpringBoot provides a ServletRegistrationBean, which is dedicated to adding servlets to the Web container, so we can do this with it.
@Configuration
public class Test {
static class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("hello"); }}@Bean
public ServletRegistrationBean<MyServlet> servletRegistrationBean(a) {
return new ServletRegistrationBean<MyServlet>(new MyServlet(), "/test"); }}Copy the code
Here will be over, DispatcherServlet has been added to the container, that there are two small problems, where the DispatcherServlet generation, where DispatcherServletRegistrationBean generated, According to SpringBoot naming rules, must be DispatcherServletAutoConfiguration and all AutoConfiguration at the end of the class in the spring. Almost every factories.
What does @enableWebMVC do
Never use @ EnableWebMvc SpringBoot, at least, I found that because of the @ EnableWebMvc role is to guide such as DelegatingWebMvcConfiguration container, This class has already been imported by WebMvcAutoConfiguration.
Specific DelegatingWebMvcConfiguration done, we said below.
WebMvcConfigurationSupport and WebMvcConfigurer difference
We first said WebMvcConfigurationSupport, DelegatingWebMvcConfiguration is his inheritance.
HandlerMapping, ViewResolver and other components that are required by DispatcherServlet are imported to the container. If not, it will be generated by default. It will not be added to the Spring container once created by default.
We usually inherited WebMvcConfigurationSupport add resources or parameters of converter, will feel very magical, SpringBoot call themselves as addResourceHandlers method, These methods are called when creating components needed in the DispatcherServlet, such as the following, which returns objects that handle static resources.
@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
this.servletContext, contentNegotiationManager, pathConfig.getUrlPathHelper());
// Call this method to add a custom resource pathaddResourceHandlers(registry); .return handlerMapping;
}
Copy the code
Is EnableWebMvcConfiguration SpringBoot import, they are three relationship.
EnableWebMvcConfiguration DelegatingWebMvcConfiguration "WebMvcConfigurationSupportCopy the code
EnableWebMvcConfiguration will also import some beans, WebMvcAutoConfiguration EnableWebMvcConfiguration is located in the source code, belongs to the inner class, But on WebMvcAutoConfiguration SpringBoot through the following annotations said, if there has been a WebMvcConfigurationSupport instance, then I won’t to import the container, Namely WebMvcConfigurationSupport in SpringBoot will only keep one.
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
Copy the code
But is not to say that you have inherited WebMvcConfigurationSupport several classes, only one will eventually Spring management, but the Spring in the class, will parse his parent, and the parent class that can be parsed once, he marked @ inside the Bean method is invoked only once.
That is if A, B two classes extend WebMvcConfigurationSupport, all your rewriting method, the only one of A, B will be invoked.
So what if you want to several kind of configuration, that requires WebMvcConfigurer, if you look at DelegatingWebMvcConfiguration source, he rewrite method is very simple, There will be an internal List
to hold all WebMvcConfigurer implementation classes, and overriding methods will iterate over this collection, calling the same methods.
So where did this set come from?
Can look at the following method in DelegatingWebMvcConfiguration, marked with @autowired, Spring will automatically call this method, and put in the container all realize WebMvcConfigurer pass here after the class instantiation of the interface.
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if(! CollectionUtils.isEmpty(configurers)) {this.configurers.addWebMvcConfigurers(configurers); }}Copy the code
The WebMvcConfigurer interface can be implemented in multiple classes, and SpringBoot will also add some configuration through this interface by default, such as the following resources can be accessed directly through the URL.
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/"
Copy the code
But there is a problem, if I inherited WebMvcConfigurationSupport, they could not instantiate DelegatingWebMvcConfiguration, all achieved WebMvcConfigurer interface is enforced, isn’t it?
It is.
But there is a problem, as long as the inherited WebMvcConfigurationSupport, so the above SpringBoot configuration default resource access path for you, will also fail, because SpringBoot automatic configuration, Found that if have WebMvcConfigurationSupport, then their will not be added.
Add the default resource access path to the WebMvcConfigurer interface.
Yes, but this interface implementation class is also an inner class under WebMvcAutoConfiguration.
Don’t inherit WebMvcConfigurationSupport solution is, we should implement WebMvcConfigurer interface, this is the best way.
Jar Startup Principle
There are two online deployment methods for SpringBoot, JAR and WAR.
To run a JAR, you first need to configure main-class in meta-INF/manifest.mf, and open any SpringBoot project. You will find the Main – Class are org. Springframework.. The boot loader. JarLauncher.
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.example.springdemo.SpringDemoApplicationKt
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.56.
Main-Class: org.springframework.boot.loader.JarLauncher
Copy the code
So let’s follow through to the main method of this class.
This Class is the “main Class” that we developed when we called it.
So why not just make main-class our Main Class?
In fact, it is possible, but it is much more troublesome, mainly because jar files cannot contain other third-party JARS. Many articles say that jar is forbidden to nest jar, but I specifically went to the official website to find the jar format instructions, but I did not find the word about banning this kind of. More about third-party lib configuration needs to be configured under class-path, which is relative to the main JAR Path search.
If a third-party JAR is to be added to a Java project, it will be unpacked and put into the jar along with the compiled directory of the main project. It will not put the whole JAR into the main JAR file, so that the class will not be found.
If SpringBoot uses this approach to unpack all of Spring’s dependencies and other dependencies into the main JAR, it can lead to chaos.
In the end, SpringBoot’s approach was forced.
His implementation idea is that since the JVM cannot load jars within jars, SpringBoot implements a ClassLoader itself to load, then assigns a value to the current thread’s ClassLoader, and finally specifies a ClassLoader when our main class is loaded by reflection.
public void run(a) throws Exception { Class<? > mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
mainMethod.invoke(null.new Object[] { this.args });
}
Copy the code
This is why the LaunchedURLClassLoader that outputs a class after it is packaged is LaunchedURLClassLoader.
The LaunchedURLClassLoader is responsible for loading the boots-INF classes and JARS.
War Startup Principle
War is loaded by Tomcat, start the principle is simple, depend on ServletContainerInitializer to complete, the above said, this is the Tomcat back and forth, but also to have an annotation and say @ HandlesTypes, This annotation is used to mark on the implementation Class of ServletContainerInitializer, parameters of a Class type, said tell Tomcat, for correction, I all inherited or equal Class and the Class to give me.
ServletContainerInitializer implementation class, need in the meta-inf/services/javax.mail servlet. ServletContainerInitializer, but SpringBoot didn’t realize this convention, What implements the convention is the Spring-Web module, which is defined below.
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {}
Copy the code
As you can see, he asked the Tomcat implementation class to give him back all WebApplicationInitializer, then carefully you may find that if it is to be deployed in Tomcat, must inherit SpringBootServletInitializer, Rewrite the configure method and configure our main class, and the class just WebApplicationInitializer interface is realized.
But do is instantiate SpringServletContainerInitializer passed all WebApplicationInitializer implementation class, call them the onStartup ().
. Then it came to the above said SpringBootServletInitializer onStartup, will eventually according to the main class we configuration, also called SpringApplication. Start the run.
protected WebApplicationContext run(SpringApplication application) {
return (WebApplicationContext) application.run();
}
Copy the code