Preface
This article analyzes the working principle of spring boot 1.3. spring boot 1.4. After that, the packaging structure was found to change and the BOOT-INF directory was added, but the basic principles remain unchanged.
For changes in ClassLoader in spring boot 1.4.*, please refer to: http://www.VeVB.COM/article/141479.htm
spring boot quick start
In spring boot, a very attractive feature is that the application can be packaged directly into a jar/war, and then this jar/war can be started directly without configuring another Web Server.
If you have not used spring boot before, you can experience it through the demo below.
Here is a project as an example to demonstrate how to start the Spring boot project:
git clone [email protected]:hengyunabc/spring-boot-demo.gitmvn spring-boot-demojava -jar target/demo-0.0.1-SNAPSHOT.jar
If the IDE used is spring sts or idea, you can create a spring boot project through the wizard.
You can also refer to the official tutorial:
http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#getting-started-first-application
Two questions about spring boot
When you first start to contact spring boot, you usually have these questions
Let’s analyze how spring boot is done.
How spring boot is started when packaged as a single jar
After maven is packaged, two jar files will be generated:
demo-0.0.1-SNAPSHOT.jardemo-0.0.1-SNAPSHOT.jar.original
where demo-0.0.1-SNAPSHOT.jar.original is the default maven-jar-plugin generated package.
demo-0.0.1-SNAPSHOT.jar is a jar package generated by the spring boot maven plug-in, which contains application dependencies and spring boot-related classes. Hereinafter, it is called fat jar.
First, let’s check the directory structure of the packages prepared by spring boot (omit it if it’s not important):
├── META-INF│ ├── MANIFEST.MF├── application.properties├── com│ └── example│ └── SpringBootDemoApplication.class├── lib│ ├── aopalliance-1.0.jar│ ├── spring-beans-4.2.3.RELEASE.jar│ ├── ...└── org └── springframework └── boot └── loader ├── ExecutiveArchiveLauncher.class ├── JarLauncher.class ├── JavaAgentDetector.class ├── LaunchedURLClassLoader.class ├── Launcher.class ├── MainMethodRunner.class ├── ...
Let’s take a look at these contents in turn.
MANIFEST.MF
Manifest-Version: 1.0Start-Class: com.example.SpringBootDemoApplicationImplementation-Vendor-Id: com.exampleSpring-Boot-Version: 1.3.0.RELEASECreated-By: Apache Maven 3.3.3Build-Jdk: 1.8.0_60Implementation-Vendor: Pivotal Software, Inc.Main-Class: org.springframework.boot.loader.JarLauncher
You can see that Main-Class is org.springframework.boot.loader.JarLauncher, which is the main function started by jar.
There is also a Start-Class which is com.example.SpringBootDemoApplication, which is the Main function we apply.
@SpringBootApplicationpublic class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoApplication.class, args); }} com/example directory
Here is the application's .class file.
lib directory
Here is the jar package file that the application depends on.
For example, spring-beans, spring-mvc and other jars.
org/springframework/boot/loader directory
Here is the .class file of Spring boot loader.
Archive concept
In spring boot, the concept of Archive is abstracted.
An archive can be a jar (JarFileArchive) or a file directory (ExplodedArchive). It can be understood as the layer of unified access resources abstracted by Spring boot.
The above demo-0.0.1-SNAPSHOT.jar is an Archive, and then each Jar package under the /lib directory in demo-0.0.1-SNAPSHOT.jar is also an Archive.
public abstract class Archive { public abstract URL getUrl(); public String getMainClass(); public abstract Collection<Entry> getEntries(); public abstract List<Archive> getNestedArchives(EntryFilter filter);You can see that Archive has its own URL, such as:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/
There is also a getNestedArchives function, which actually returns the Archive list of jars under demo-0.0.1-SNAPSHOT.jar/lib. Their URLs are:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/aopalliance-1.0.jarjar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar
JarLauncher
From MANIFEST.MF, we can see that the Main function is JarLauncher. Let’s analyze its workflow below.
The inheritance structure of the JarLauncher class is:
class JarLauncher extends ExecutiveArchiveLauncherclass ExecutiveArchiveLauncher extends Launcher
Create an Archive with demo-0.0.1-SNAPSHOT.jar:
JarLauncher first finds the path of the jar where he is located, that is, demo-0.0.1-SNAPSHOT.jar, and then creates an Archive.
The following code shows the trick of how to find the loading location from a class:
protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource == null ? null : codeSource.getLocation().toURI()); String path = (location == null ? null : location.getSchemeSpecificPart()); if (path == null) { throw new IllegalStateException("Unable to determine code source archive"); } File root = new File(path); if (!root.exists()) { throw new IllegalStateException( "Unable to determine code source archive from " + root); } return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));} Get the jar below lib/ and create a LaunchedURLClassLoader
After JarLauncher creates Archive, he uses the getNestedArchives function to obtain all jar files below demo-0.0.1-SNAPSHOT.jar/lib and creates them as List.
Note that as mentioned above, Archive has its own URL.
After obtaining these Archive URLs, you will get a URL[] array, and use this to construct a custom ClassLoader: LaunchedURLClassLoader.
After creating the ClassLoader, read Start-Class from MANIFEST.MF, that is, com.example.SpringBootDemoApplication, and then create a new thread to start the application's Main function.
/** * Launch the application given the archive file and a fully configured classloader. */protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { Runnable runner = createMainMethodRunner(mainClass, args, classLoader); Thread runnerThread = new Thread(runner); runnerThread.setContextClassLoader(classLoader); runnerThread.setName(Thread.currentThread().getName()); runnerThread.start();}/** * Create the {@code MainMethodRunner} used to launch the application. */protected Runnable createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) throws Exception { Class<?> runnerClass = classLoader.loadClass(RUNNER_CLASS); Constructor<?> constructor = runnerClass.getConstructor(String.class, String[].class); return (Runnable) constructor.newInstance(mainClass, args);} LaunchedURLClassLoader
The difference between LaunchedURLClassLoader and ordinary URLClassLoader is that it provides the ability to load .class from Archive.
Combining the getEntries function provided by Archive, you can get the Resource in Archive. Of course there are still many details inside, so I will describe it below.
Summary of spring boot application startup process
After seeing this, you can summarize the startup process of Spring Boot application:
Details in spring boot loader
Code address: https://github.com/spring-projects/spring-boot/tree/master/spring-boot-tools/spring-boot-loader
Extensions of JarFile URLs
Spring boot can be started with a fat jar. The most important thing is that it implements the loading method of jar in jar.
The definition of the original JarFile URL of JDK can be found here:
http://docs.oracle.com/javase/7/docs/api/java/net/JarURLConnection.html
The original JarFile URL looks like this:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/
URL of resource in jar package:
Copy the code as follows: jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/com/example/SpringBootDemoApplication.class
You can see that for the resources in Jar, the definition is separated by '!/'. The original JarFile URL only supports one '!/'.
Spring boot extends this protocol to support multiple '!/', which can represent jar in jar, jar in directory resources.
For example, the following URL represents the spring-beans-4.2.3.RELEASE.jar in the lib directory of demo-0.0.1-SNAPSHOT.jar:
Copy the code as follows: jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar!/META-INF/MANIFEST.MF
Custom URLStreamHandler, extend JarFile and JarURLConnection
When constructing a URL, you can pass a Handler, and the JDK comes with a default Handler class. The application can register the Handler itself to handle custom URLs.
public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException
refer to:
https://docs.oracle.com/javase/8/docs/api/java/net/URL.html#URL-java.lang.String-java.lang.String-int-java.lang.String-
Spring boot handles the logic of multiple jars in jars by registering a custom Handler class.
This Handler will use SoftReference to cache all open JarFiles.
When processing URLs like the following, the '!/' separator is processed cycled. Starting from the top layer, first construct the JarFile demo-0.0.1-SNAPSHOT.jar, then construct the JarFile spring-beans-4.2.3.RELEASE.jar, and then construct the JarURLConnection pointing to MANIFEST.MF.
Copy the code as follows: jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar!/META-INF/MANIFEST.MF
//org.springframework.boot.loader.jar.Handlerpublic class Handler extends URLStreamHandler { private static final String SEPARATOR = "!/"; private static SoftReference<Map<File, JarFile>> rootFileCache; @Override protected URLConnection openConnection(URL url) throws IOException { if (this.jarFile != null) { return new JarURLConnection(url, this.jarFile); } try { return new JarURLConnection(url, getRootJarFileFromUrl(url)); } catch (Exception ex) { return openFallbackConnection(url, ex); } } public JarFile getRootJarFileFromUrl(URL url) throws IOException { String spec = url.getFile(); int separatorIndex = spec.indexOf(SEPARATOR); if (separatorIndex == -1) { throw new MalformedURLException("Jar URL does not contain !/ separator"); } String name = spec.substring(0, separatorIndex); return getRootJarFile(name); } How ClassLoader reads to Resource
What abilities does it require for a ClassLoader?
The corresponding API is:
public URL findResource(String name)public InputStream getResourceAsStream(String name)
As mentioned above, when Spring boot constructs LaunchedURLClassLoader, it passes a URL[] array. The array is the URL of the jar under the lib directory.
How do JDK or ClassLoader know how to read the content in it for a URL?
In fact, the process looks like this:
The final call is the getInputStream() function of JarURLConnection.
//org.springframework.boot.loader.jar.JarURLConnection @Override public InputStream getInputStream() throws IOException { connect(); if (this.jarEntryName.isEmpty()) { throw new IOException("no entry name specified"); } return this.jarEntryData.getInputStream(); }From a URL to finally reading the content in the URL, the whole process is quite complicated. Let's summarize:
There are many details here, only some important points are listed.
Then, how does URLClassLoader getResource?
When constructing the URLClassLoader, it has URL[] array parameters, and it will use this array to construct a URLClassPath:
URLClassPath ucp = new URLClassPath(urls);
In the URLClassPath, a loader will be constructed for all URLs, and then when gettingResource, it will try to get them from these Loaders one by one.
If the acquisition is successful, it is packaged as a Resource as below.
Resource getResource(final String name, boolean check) { final URL url; try { url = new URL(base, ParseUtil.encodePath(name, false)); } catch (MalformedURLException e) { throw new IllegalArgumentException("name"); } final URLConnection uc; try { if (check) { URLClassPath.check(url); } uc = url.openConnection(); InputStream in = uc.getInputStream(); if (uc instanceof JarURLConnection) { /* Need to remember the jar file so it can be closed * in a hurry. */ JarURLConnection juc = (JarURLConnection)uc; jarfile = JarLoader.checkJar(juc.getJarFile()); } } catch (Exception e) { return null; } return new Resource() { public String getName() { return name; } public URL getURL() { return url; } public URL getCodeSourceURL() { return base; } public InputStream getInputStream() throws IOException { return uc.getInputStream(); } public int getContentLength() throws IOException { return uc.getContentLength(); } };}As you can see from the code, url.openConnection() is actually called. This way the complete chain can be connected.
Note that the code of the URLClassPath class does not come with its own in the JDK. Here you can see http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/sun/misc/URLClassPath.java#506
Start Spring boot application in IDE/Open Directory
The above is only mentioned the process of starting Spring boot application in a fat jar. The following is an analysis of how Spring boot is started in the IDE.
In the IDE, the main function that is directly run is to apply its own Main function:
@SpringBootApplicationpublic class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoApplication.class, args); }}In fact, starting Spring boot application in the IDE is the easiest case, because the dependency Jars are put into the classpath, so Spring boot is just started.
Another situation is to start Spring boot in an open directory. The so-called open directory is to decompress the fat jar and then directly start the application.
java org.springframework.boot.loader.JarLauncher
At this time, Spring boot will determine whether it is currently in a directory. If so, construct an ExplodedArchive (the previous one is JarFileArchive), and the subsequent startup process is similar to fat jar.
Embead Tomcat startup process
Determine whether it is in the web environment
When spring boot is started, first determine whether it is in the web environment by simply searching the Servlet class:
private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" };private boolean deduceWebEnvironment() { for (String className : WEB_ENVIRONMENT_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return false; } } return true;}If so, AnnotationConfigEmbeddedWebApplicationContext will be created, otherwise the Spring context is AnnotationConfigApplicationContext:
//org.springframework.boot.SpringApplication protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { contextClass = Class.forName(this.webEnvironment ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS); } catch (ClassNotFoundException ex) { throw new IllegalStateException( "Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass", ex); } } return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass); } Get the implementation class of EmbeddedServletContainerFactory
spring boot starts the corresponding web server by obtaining EmbeddedServletContainerFactory.
The two commonly used implementation classes are TomcatEmbeddedServletContainerFactory and JettyEmbeddedServletContainerFactory.
Code to start Tomcat:
//TomcatEmbeddedServletContainerFactory@Overridepublic EmbeddedServletContainer getEmbeddedServletContainer( ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null ? this.baseDir: createTempDir("tomcat")); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); tomcat.getEngine().setBackgroundProcessorDelay(-1); for (Connector additionalConnector: this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } prepareContext(tomcat.getHost(), initializers); return getTomcatEmbeddedServletContainer(tomcat);} A temporary file directory will be created for tomcat, such as:
/tmp/tomcat.2233614112516545210.8080, as the basisr of tomcat. Temporary tomcat files, such as work directory, will be placed inside.
Some Tomcat servlets will also be initialized, such as the more important default/jsp servlet:
private void addDefaultServlet(Context context) { Wrapper defaultServlet = context.createWrapper(); defaultServlet.setName("default"); defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); defaultServlet.addInitParameter("debug", "0"); defaultServlet.addInitParameter("listings", "false"); defaultServlet.setLoadOnStartup(1); // Otherwise the default location of a Spring DispatcherServlet cannot be set defaultServlet.setOverridable(true); context.addChild(defaultServlet); context.addServletMapping("/", "default");}private void addJspServlet(Context context) { Wrapper jspServlet = context.createWrapper(); jspServlet.setName("jsp"); jspServlet.setServletClass(getJspServletClassName()); jspServlet.addInitParameter("fork", "false"); jspServlet.setLoadOnStartup(3); context.addChild(jspServlet); context.addServletMapping("*.jsp", "jsp"); context.addServletMapping("*.jspx", "jsp");} How to access Resource with spring boot web application
How do I access the web resource when the spring boot application is packaged as a fat jar?
It is actually implemented through the URL provided by Archive and then through the ability to access the classpath resource provided by Classloader.
index.html
For example, you need to configure an index.html, which can be placed directly in the src/main/resources/static directory in the code.
For the index.html welcome page, when spring boot is initialized, a ViewController will be created to handle:
//ResourcePropertiespublic class ResourceProperties implements ResourceLoaderAware { private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" }; private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; //WebMvcAutoConfigurationAdapter @Override public void addViewControllers(ViewControllerRegistry registry) { Resource page = this.resourceProperties.getWelcomePage(); if (page != null) { logger.info("Adding welcome page: " + page); registry.addViewController("/").setViewName("forward:index.html"); } } template
For example, the page template file can be placed in the src/main/resources/template directory. But this is actually handled by the template implementation class itself. For example, in the ThymeleafProperties class:
public static final String DEFAULT_PREFIX = "classpath:/templates/";
jsp
The jsp page is similar to template. It is actually handled through the JstlView built in spring mvc.
You can configure spring.view.prefix to set the directory of the jsp page:
spring.view.prefix: /WEB-INF/jsp/
Handling of unified error pages in spring boot
For error pages, Spring boot is also handled uniformly by creating a BasicErrorController.
@Controller@RequestMapping("${server.error.path:${error.path:/error}}")public class BasicErrorController extends AbstractErrorControllerThe corresponding View is a simple HTML reminder:
@Configuration@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)@Conditional(ErrorTemplateMissingCondition.class)protected static class WhitelabelErrorViewConfiguration { private final SpelView defaultErrorView = new SpelView( "<html><body><h1>Whitelabel Error Page</h1>" + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>" + "<div id='created'>${timestamp}</div>" + "<div>There was an unexpected error (type=${error}, status=${status}).</div>" + "<div>${message}</div></body></html>"); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; }Spring boot is a good practice, which avoids the default exception thrown when traditional web applications make errors, making it easy to leak secrets.
Maven packaging process of spring boot application
First, generate a jar containing dependencies through maven-shade-plugin, and then package the classes related to spring boot loader and MANIFEST.MF into the jar through spring-boot-maven-plugin plugin.
Implementation of color logs in spring boot
When starting the spring boot application in the shell, you will find that its logger output is colored, which is very interesting.
This setting can be turned off:
spring.output.ansi.enabled=false
The principle is to obtain this configuration through AnsiOutputApplicationListener, and then set logback to output, add a ColorConverter, and render some fields through org.springframework.boot.ansi.AnsiOutput.
Some code tips
When implementing ClassLoader, support JDK7 parallel loading
You can refer to the LockProvider in LaunchedURLClassLoader
public class LaunchedURLClassLoader extends URLClassLoader { private static LockProvider LOCK_PROVIDER = setupLockProvider(); private static LockProvider setupLockProvider() { try { ClassLoader.registerAsParallelCapable(); return new Java7LockProvider(); } catch (NoSuchMethodError ex) { return new LockProvider(); } } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (LaunchedURLClassLoader.LOCK_PROVIDER.getLock(this, name)) { Class<?> loadedClass = findLoadedClass(name); if (loadedClass == null) { Handler.setUseFastConnectionExceptions(true); try { loadedClass = doLoadClass(name); } finally { Handler.setUseFastConnectionExceptions(false); } } if (resolve) { resolveClass(loadedClass); } return loadedClass; } } Check whether the jar package is loaded through the agent
InputArgumentsJavaAgentDetector, the principle is to detect whether the URL of the jar has the prefix of "-javaagent:".
private static final String JAVA_AGENT_PREFIX = "-javaagent:";
Get the PID of the process
ApplicationPid, you can get the PID.
private String getPid() { try { String jvmName = ManagementFactory.getRuntimeMXBean().getName(); return jvmName.split("@")[0]; } catch (Throwable ex) { return null; }} Packaging Logger class
Spring boot has a set of loggers that support java, log4j, log4j2, logback. You can refer to this when you need to package your loggers yourself in the future.
Under the org.springframework.boot.logging package.
Get the original started main function
Through the method obtained in the stack, judge the main function and find the original started main function.
private Class<?> deduceMainApplicationClass() { try { StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { if ("main".equals(stackTraceElement.getMethodName())) { return Class.forName(stackTraceElement.getClassName()); } } } catch (ClassNotFoundException ex) { // Swallow and continue } return null;} Some disadvantages of spirng boot:
When the spring boot application runs in a fat jar, some problems will be encountered. Here are my personal opinions:
Summarize
Spring boot extends the jar protocol, abstracts the Archive concept, and the supporting JarFile, JarUrlConnection, and LaunchedURLClassLoader, thus realizing the development experience of all in one without perception of the upper layer application. Although Executable war is not a concept proposed by spring, spring boot allows it to be carried forward.
spring boot is an amazing project, which can be said to be the second spring of spring. spring-cloud-config, spring-session, metrics, remote shell, etc. are all projects and features that developers love. It is almost certain that the designer has rich experience in front-line development and is well aware of the pain points of developers.
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.