詳解spring boot應(yīng)用啟動(dòng)原理分析
前言
本文分析的是spring boot 1.3. 的工作原理。spring boot 1.4. 之后打包結(jié)構(gòu)發(fā)現(xiàn)了變化,增加了BOOT-INF目錄,但是基本原理還是不變的。
關(guān)于spring boot 1.4.* 里ClassLoader的變化,可以參考:http://www.dbjr.com.cn/article/141479.htm
spring boot quick start
在spring boot里,很吸引人的一個(gè)特性是可以直接把應(yīng)用打包成為一個(gè)jar/war,然后這個(gè)jar/war是可以直接啟動(dòng)的,不需要另外配置一個(gè)Web Server。
如果之前沒(méi)有使用過(guò)spring boot可以通過(guò)下面的demo來(lái)感受下。
下面以這個(gè)工程為例,演示如何啟動(dòng)Spring boot項(xiàng)目:
git clone git@github.com:hengyunabc/spring-boot-demo.git mvn spring-boot-demo java -jar target/demo-0.0.1-SNAPSHOT.jar
如果使用的IDE是spring sts或者idea,可以通過(guò)向?qū)?lái)創(chuàng)建spring boot項(xiàng)目。
對(duì)spring boot的兩個(gè)疑問(wèn)
剛開(kāi)始接觸spring boot時(shí),通常會(huì)有這些疑問(wèn)
- spring boot如何啟動(dòng)的?
- spring boot embed tomcat是如何工作的? 靜態(tài)文件,jsp,網(wǎng)頁(yè)模板這些是如何加載到的?
下面來(lái)分析spring boot是如何做到的。
打包為單個(gè)jar時(shí),spring boot的啟動(dòng)方式
maven打包之后,會(huì)生成兩個(gè)jar文件:
demo-0.0.1-SNAPSHOT.jar demo-0.0.1-SNAPSHOT.jar.original
其中demo-0.0.1-SNAPSHOT.jar.original是默認(rèn)的maven-jar-plugin生成的包。
demo-0.0.1-SNAPSHOT.jar是spring boot maven插件生成的jar包,里面包含了應(yīng)用的依賴(lài),以及spring boot相關(guān)的類(lèi)。下面稱(chēng)之為fat jar。
先來(lái)查看spring boot打好的包的目錄結(jié)構(gòu)(不重要的省略掉):
├── 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 ├── ExecutableArchiveLauncher.class ├── JarLauncher.class ├── JavaAgentDetector.class ├── LaunchedURLClassLoader.class ├── Launcher.class ├── MainMethodRunner.class ├── ...
依次來(lái)看下這些內(nèi)容。
MANIFEST.MF
Manifest-Version: 1.0 Start-Class: com.example.SpringBootDemoApplication Implementation-Vendor-Id: com.example Spring-Boot-Version: 1.3.0.RELEASE Created-By: Apache Maven 3.3.3 Build-Jdk: 1.8.0_60 Implementation-Vendor: Pivotal Software, Inc. Main-Class: org.springframework.boot.loader.JarLauncher
可以看到有Main-Class是org.springframework.boot.loader.JarLauncher ,這個(gè)是jar啟動(dòng)的Main函數(shù)。
還有一個(gè)Start-Class是com.example.SpringBootDemoApplication,這個(gè)是我們應(yīng)用自己的Main函數(shù)。
@SpringBootApplication public class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoApplication.class, args); } }
com/example 目錄
這下面放的是應(yīng)用的.class文件。
lib目錄
這里存放的是應(yīng)用的Maven依賴(lài)的jar包文件。
比如spring-beans,spring-mvc等jar。
org/springframework/boot/loader 目錄
這下面存放的是Spring boot loader的.class文件。
Archive的概念
- archive即歸檔文件,這個(gè)概念在linux下比較常見(jiàn)
- 通常就是一個(gè)tar/zip格式的壓縮包
- jar是zip格式
在spring boot里,抽象出了Archive的概念。
一個(gè)archive可以是一個(gè)jar(JarFileArchive),也可以是一個(gè)文件目錄(ExplodedArchive)??梢岳斫鉃镾pring boot抽象出來(lái)的統(tǒng)一訪問(wèn)資源的層。
上面的demo-0.0.1-SNAPSHOT.jar 是一個(gè)Archive,然后demo-0.0.1-SNAPSHOT.jar里的/lib目錄下面的每一個(gè)Jar包,也是一個(gè)Archive。
public abstract class Archive { public abstract URL getUrl(); public String getMainClass(); public abstract Collection<Entry> getEntries(); public abstract List<Archive> getNestedArchives(EntryFilter filter);
可以看到Archive有一個(gè)自己的URL,比如:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/
還有一個(gè)getNestedArchives函數(shù),這個(gè)實(shí)際返回的是demo-0.0.1-SNAPSHOT.jar/lib下面的jar的Archive列表。它們的URL是:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/aopalliance-1.0.jar jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar
JarLauncher
從MANIFEST.MF可以看到Main函數(shù)是JarLauncher,下面來(lái)分析它的工作流程。
JarLauncher類(lèi)的繼承結(jié)構(gòu)是:
class JarLauncher extends ExecutableArchiveLauncher class ExecutableArchiveLauncher extends Launcher
以demo-0.0.1-SNAPSHOT.jar創(chuàng)建一個(gè)Archive:
JarLauncher先找到自己所在的jar,即demo-0.0.1-SNAPSHOT.jar的路徑,然后創(chuàng)建了一個(gè)Archive。
下面的代碼展示了如何從一個(gè)類(lèi)找到它的加載的位置的技巧:
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)); }
獲取lib/下面的jar,并創(chuàng)建一個(gè)LaunchedURLClassLoader
JarLauncher創(chuàng)建好Archive之后,通過(guò)getNestedArchives函數(shù)來(lái)獲取到demo-0.0.1-SNAPSHOT.jar/lib下面的所有jar文件,并創(chuàng)建為L(zhǎng)ist。
注意上面提到,Archive都是有自己的URL的。
獲取到這些Archive的URL之后,也就獲得了一個(gè)URL[]數(shù)組,用這個(gè)來(lái)構(gòu)造一個(gè)自定義的ClassLoader:LaunchedURLClassLoader。
創(chuàng)建好ClassLoader之后,再?gòu)腗ANIFEST.MF里讀取到Start-Class,即com.example.SpringBootDemoApplication,然后創(chuàng)建一個(gè)新的線程來(lái)啟動(dòng)應(yīng)用的Main函數(shù)。
/** * 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
LaunchedURLClassLoader和普通的URLClassLoader的不同之處是,它提供了從Archive里加載.class的能力。
結(jié)合Archive提供的getEntries函數(shù),就可以獲取到Archive里的Resource。當(dāng)然里面的細(xì)節(jié)還是很多的,下面再描述。
spring boot應(yīng)用啟動(dòng)流程總結(jié)
看到這里,可以總結(jié)下Spring Boot應(yīng)用的啟動(dòng)流程:
- spring boot應(yīng)用打包之后,生成一個(gè)fat jar,里面包含了應(yīng)用依賴(lài)的jar包,還有Spring boot loader相關(guān)的類(lèi)
- Fat jar的啟動(dòng)Main函數(shù)是JarLauncher,它負(fù)責(zé)創(chuàng)建一個(gè)LaunchedURLClassLoader來(lái)加載/lib下面的jar,并以一個(gè)新線程啟動(dòng)應(yīng)用的Main函數(shù)。
spring boot loader里的細(xì)節(jié)
代碼地址:https://github.com/spring-projects/spring-boot/tree/master/spring-boot-tools/spring-boot-loader
JarFile URL的擴(kuò)展
Spring boot能做到以一個(gè)fat jar來(lái)啟動(dòng),最重要的一點(diǎn)是它實(shí)現(xiàn)了jar in jar的加載方式。
JDK原始的JarFile URL的定義可以參考這里:
http://docs.oracle.com/javase/7/docs/api/java/net/JarURLConnection.html
原始的JarFile URL是這樣子的:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/
jar包里的資源的URL:
可以看到對(duì)于Jar里的資源,定義以'!/‘來(lái)分隔。原始的JarFile URL只支持一個(gè)'!/‘。
Spring boot擴(kuò)展了這個(gè)協(xié)議,讓它支持多個(gè)'!/‘,就可以表示jar in jar,jar in directory的資源了。
比如下面的URL表示demo-0.0.1-SNAPSHOT.jar這個(gè)jar里lib目錄下面的spring-beans-4.2.3.RELEASE.jar里面的MANIFEST.MF:
自定義URLStreamHandler,擴(kuò)展JarFile和JarURLConnection
在構(gòu)造一個(gè)URL時(shí),可以傳遞一個(gè)Handler,而JDK自帶有默認(rèn)的Handler類(lèi),應(yīng)用可以自己注冊(cè)Handler來(lái)處理自定義的URL。
public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException
Spring boot通過(guò)注冊(cè)了一個(gè)自定義的Handler類(lèi)來(lái)處理多重jar in jar的邏輯。
這個(gè)Handler內(nèi)部會(huì)用SoftReference來(lái)緩存所有打開(kāi)過(guò)的JarFile。
在處理像下面這樣的URL時(shí),會(huì)循環(huán)處理'!/‘分隔符,從最上層出發(fā),先構(gòu)造出demo-0.0.1-SNAPSHOT.jar這個(gè)JarFile,再構(gòu)造出spring-beans-4.2.3.RELEASE.jar這個(gè)JarFile,然后再構(gòu)造出指向MANIFEST.MF的JarURLConnection。
//org.springframework.boot.loader.jar.Handler public 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); }
ClassLoader如何讀取到Resource
對(duì)于一個(gè)ClassLoader,它需要哪些能力?
- 查找資源
- 讀取資源
對(duì)應(yīng)的API是:
public URL findResource(String name) public InputStream getResourceAsStream(String name)
上面提到,Spring boot構(gòu)造LaunchedURLClassLoader時(shí),傳遞了一個(gè)URL[]數(shù)組。數(shù)組里是lib目錄下面的jar的URL。
對(duì)于一個(gè)URL,JDK或者ClassLoader如何知道怎么讀取到里面的內(nèi)容的?
實(shí)際上流程是這樣子的:
- LaunchedURLClassLoader.loadClass
- URL.getContent()
- URL.openConnection()
- Handler.openConnection(URL)
最終調(diào)用的是JarURLConnection的getInputStream()函數(shù)。
//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(); }
從一個(gè)URL,到最終讀取到URL里的內(nèi)容,整個(gè)過(guò)程是比較復(fù)雜的,總結(jié)下:
- spring boot注冊(cè)了一個(gè)Handler來(lái)處理”jar:”這種協(xié)議的URL
- spring boot擴(kuò)展了JarFile和JarURLConnection,內(nèi)部處理jar in jar的情況
- 在處理多重jar in jar的URL時(shí),spring boot會(huì)循環(huán)處理,并緩存已經(jīng)加載到的JarFile
- 對(duì)于多重jar in jar,實(shí)際上是解壓到了臨時(shí)目錄來(lái)處理,可以參考JarFileArchive里的代碼
- 在獲取URL的InputStream時(shí),最終獲取到的是JarFile里的JarEntryData
這里面的細(xì)節(jié)很多,只列出比較重要的一些點(diǎn)。
然后,URLClassLoader是如何getResource的呢?
URLClassLoader在構(gòu)造時(shí),有URL[]數(shù)組參數(shù),它內(nèi)部會(huì)用這個(gè)數(shù)組來(lái)構(gòu)造一個(gè)URLClassPath:
URLClassPath ucp = new URLClassPath(urls);
在 URLClassPath 內(nèi)部會(huì)為這些URLS 都構(gòu)造一個(gè)Loader,然后在getResource時(shí),會(huì)從這些Loader里一個(gè)個(gè)去嘗試獲取。
如果獲取成功的話,就像下面那樣包裝為一個(gè)Resource。
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(); } }; }
從代碼里可以看到,實(shí)際上是調(diào)用了url.openConnection()。這樣完整的鏈條就可以連接起來(lái)了。
注意,URLClassPath這個(gè)類(lèi)的代碼在JDK里沒(méi)有自帶,在這里看到 http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/sun/misc/URLClassPath.java#506
在IDE/開(kāi)放目錄啟動(dòng)Spring boot應(yīng)用
在上面只提到在一個(gè)fat jar里啟動(dòng)Spring boot應(yīng)用的過(guò)程,下面分析IDE里Spring boot是如何啟動(dòng)的。
在IDE里,直接運(yùn)行的Main函數(shù)是應(yīng)用自己的Main函數(shù):
@SpringBootApplication public class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoApplication.class, args); } }
其實(shí)在IDE里啟動(dòng)Spring boot應(yīng)用是最簡(jiǎn)單的一種情況,因?yàn)橐蕾?lài)的Jar都讓IDE放到classpath里了,所以Spring boot直接啟動(dòng)就完事了。
還有一種情況是在一個(gè)開(kāi)放目錄下啟動(dòng)Spring boot啟動(dòng)。所謂的開(kāi)放目錄就是把fat jar解壓,然后直接啟動(dòng)應(yīng)用。
java org.springframework.boot.loader.JarLauncher
這時(shí),Spring boot會(huì)判斷當(dāng)前是否在一個(gè)目錄里,如果是的,則構(gòu)造一個(gè)ExplodedArchive(前面在jar里時(shí)是JarFileArchive),后面的啟動(dòng)流程類(lèi)似fat jar的。
Embead Tomcat的啟動(dòng)流程
判斷是否在web環(huán)境
spring boot在啟動(dòng)時(shí),先通過(guò)一個(gè)簡(jiǎn)單的查找Servlet類(lèi)的方式來(lái)判斷是不是在web環(huán)境:
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; }
如果是的話,則會(huì)創(chuàng)建AnnotationConfigEmbeddedWebApplicationContext,否則Spring context就是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); }
獲取EmbeddedServletContainerFactory的實(shí)現(xiàn)類(lèi)
spring boot通過(guò)獲取EmbeddedServletContainerFactory來(lái)啟動(dòng)對(duì)應(yīng)的web服務(wù)器。
常用的兩個(gè)實(shí)現(xiàn)類(lèi)是TomcatEmbeddedServletContainerFactory和JettyEmbeddedServletContainerFactory。
啟動(dòng)Tomcat的代碼:
//TomcatEmbeddedServletContainerFactory @Override public EmbeddedServletContainer getEmbeddedServletContainer( ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null ? this.baseDirectory : 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); }
會(huì)為tomcat創(chuàng)建一個(gè)臨時(shí)文件目錄,如:
/tmp/tomcat.2233614112516545210.8080,做為tomcat的basedir。里面會(huì)放tomcat的臨時(shí)文件,比如work目錄。
還會(huì)初始化Tomcat的一些Servlet,比如比較重要的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"); }
spring boot的web應(yīng)用如何訪問(wèn)Resource
當(dāng)spring boot應(yīng)用被打包為一個(gè)fat jar時(shí),是如何訪問(wèn)到web resource的?
實(shí)際上是通過(guò)Archive提供的URL,然后通過(guò)Classloader提供的訪問(wèn)classpath resource的能力來(lái)實(shí)現(xiàn)的。
index.html
比如需要配置一個(gè)index.html,這個(gè)可以直接放在代碼里的src/main/resources/static目錄下。
對(duì)于index.html歡迎頁(yè),spring boot在初始化時(shí),就會(huì)創(chuàng)建一個(gè)ViewController來(lái)處理:
//ResourceProperties public 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
像頁(yè)面模板文件可以放在src/main/resources/template目錄下。但這個(gè)實(shí)際上是模板的實(shí)現(xiàn)類(lèi)自己處理的。比如ThymeleafProperties類(lèi)里的:
public static final String DEFAULT_PREFIX = "classpath:/templates/";
jsp
jsp頁(yè)面和template類(lèi)似。實(shí)際上是通過(guò)spring mvc內(nèi)置的JstlView來(lái)處理的。
可以通過(guò)配置spring.view.prefix來(lái)設(shè)定jsp頁(yè)面的目錄:
spring.view.prefix: /WEB-INF/jsp/
spring boot里統(tǒng)一的錯(cuò)誤頁(yè)面的處理
對(duì)于錯(cuò)誤頁(yè)面,Spring boot也是通過(guò)創(chuàng)建一個(gè)BasicErrorController來(lái)統(tǒng)一處理的。
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController
對(duì)應(yīng)的View是一個(gè)簡(jiǎn)單的HTML提醒:
@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的這個(gè)做法很好,避免了傳統(tǒng)的web應(yīng)用來(lái)出錯(cuò)時(shí),默認(rèn)拋出異常,容易泄密。
spring boot應(yīng)用的maven打包過(guò)程
先通過(guò)maven-shade-plugin生成一個(gè)包含依賴(lài)的jar,再通過(guò)spring-boot-maven-plugin插件把spring boot loader相關(guān)的類(lèi),還有MANIFEST.MF打包到j(luò)ar里。
spring boot里有顏色日志的實(shí)現(xiàn)
當(dāng)在shell里啟動(dòng)spring boot應(yīng)用時(shí),會(huì)發(fā)現(xiàn)它的logger輸出是有顏色的,這個(gè)特性很有意思。
可以通過(guò)這個(gè)設(shè)置來(lái)關(guān)閉:
spring.output.ansi.enabled=false
原理是通過(guò)AnsiOutputApplicationListener ,這個(gè)來(lái)獲取這個(gè)配置,然后設(shè)置logback在輸出時(shí),加了一個(gè) ColorConverter,通過(guò)org.springframework.boot.ansi.AnsiOutput ,對(duì)一些字段進(jìn)行了渲染。
一些代碼小技巧
實(shí)現(xiàn)ClassLoader時(shí),支持JDK7并行加載
可以參考LaunchedURLClassLoader里的LockProvider
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; } }
檢測(cè)jar包是否通過(guò)agent加載的
InputArgumentsJavaAgentDetector,原理是檢測(cè)jar的URL是否有”-javaagent:”的前綴。
private static final String JAVA_AGENT_PREFIX = "-javaagent:";
獲取進(jìn)程的PID
ApplicationPid,可以獲取PID。
private String getPid() { try { String jvmName = ManagementFactory.getRuntimeMXBean().getName(); return jvmName.split("@")[0]; } catch (Throwable ex) { return null; } }
包裝Logger類(lèi)
spring boot里自己包裝了一套logger,支持java, log4j, log4j2, logback,以后有需要自己包裝logger時(shí),可以參考這個(gè)。
在org.springframework.boot.logging包下面。
獲取原始啟動(dòng)的main函數(shù)
通過(guò)堆棧里獲取的方式,判斷main函數(shù),找到原始啟動(dòng)的main函數(shù)。
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; }
spirng boot的一些缺點(diǎn):
當(dāng)spring boot應(yīng)用以一個(gè)fat jar方式運(yùn)行時(shí),會(huì)遇到一些問(wèn)題。以下是個(gè)人看法:
- 日志不知道放哪,默認(rèn)是輸出到stdout的
- 數(shù)據(jù)目錄不知道放哪, jenkinns的做法是放到 ${user.home}/.jenkins 下面
- 相對(duì)目錄API不能使用,servletContext.getRealPath(“/“) 返回的是NULL
- spring boot應(yīng)用喜歡把配置都寫(xiě)到代碼里,有時(shí)會(huì)帶來(lái)混亂。一些簡(jiǎn)單可以用xml來(lái)表達(dá)的配置可能會(huì)變得難讀,而且凌亂。
總結(jié)
spring boot通過(guò)擴(kuò)展了jar協(xié)議,抽象出Archive概念,和配套的JarFile,JarUrlConnection,LaunchedURLClassLoader,從而實(shí)現(xiàn)了上層應(yīng)用無(wú)感知的all in one的開(kāi)發(fā)體驗(yàn)。盡管Executable war并不是spring提出的概念,但spring boot讓它發(fā)揚(yáng)光大。
spring boot是一個(gè)驚人的項(xiàng)目,可以說(shuō)是spring的第二春,spring-cloud-config, spring-session, metrics, remote shell等都是深?lèi)?ài)開(kāi)發(fā)者喜愛(ài)的項(xiàng)目、特性。幾乎可以肯定設(shè)計(jì)者是有豐富的一線開(kāi)發(fā)經(jīng)驗(yàn),深知開(kāi)發(fā)人員的痛點(diǎn)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
mybatis如何封裝List<String>類(lèi)型屬性
這篇文章主要介紹了mybatis如何封裝List<String>類(lèi)型屬性問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12MyBatis實(shí)現(xiàn)高級(jí)映射的示例代碼
高級(jí)映射主要還是映射,只是映射中的數(shù)據(jù)關(guān)系復(fù)雜了,其中就包括一對(duì)一、一對(duì)多、多對(duì)多的關(guān)系,本文主要介紹了MyBatis實(shí)現(xiàn)高級(jí)映射的示例代碼,感興趣的可以了解一下2024-06-06springmvc HttpServletRequest 如何獲取c:forEach的值
這篇文章主要介紹了springmvc HttpServletRequest 如何獲取c:forEach的值方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08springboot 如何使用jackson來(lái)處理實(shí)體類(lèi)
這篇文章主要介紹了springboot使用jackson來(lái)處理實(shí)體類(lèi)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10關(guān)于jackson序列化和feign返回值的問(wèn)題
這篇文章主要介紹了關(guān)于jackson序列化和feign返回值的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03SpringCloud使用Zookeeper作為注冊(cè)中心
這篇文章主要介紹了SpringCloud如何使用Zookeeper作為注冊(cè)中心,幫助大家更好的理解和學(xué)習(xí)使用Zookeeper,感興趣的朋友可以了解下2021-04-04