Spring Boot 容器启动原理揭秘
不得不讲SpringBoot 使用起来太方便了,它的外表轻巧简单,在企业级的应用系统中非常流行,已经成为java开发者必备技能。而它采用的one-jar的方案已经深入人心,其实one-jar技术早在2004年就已经被提出,除此之外spring boot的强大的自动配置类也是非常的受用,总之用过的童靴都会很感觉一个字“爽”。但是 SpringBoot它内部实现却非常的复杂,它常常把爱研究源码的读者绕的晕头转向。
目录
1. spring-boot-maven-plugin是个什么鬼
2. maven-shade-plugin和spring-boot-maven-plugin有何区别
3. JarLauncher
4. Archive
5. LaunchedURLClassLoader
6. WarLauncher
7. SpringBootApplication
1. spring-boot-maven-plugin是个什么鬼
如果你不知道从哪里开始,就按作者的思路向下看吧!SpringBoot在于打包时它使用了one-jar (也有很多人叫 FatJar)技术,它就是将所有的依赖 jar 包一起放进了最终的 jar 包中的 BOOT-INF/lib 目录中,当前应用系统的 class 被统一放到了 BOOT-INF/classes 目录中。
简单的讲:它可以实现将所有的依赖 jar 包及class 文件塞进统一的 jar 包中。如果你还不了解,但有一个onejar-maven-plugin你可能听说过,是不是有一种旧瓶换新药的感觉。
这里提个问题:META-INF目录和org/springframework/boot/loader目录有什么特别之处呢?
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
springboot-exaple-1.0-SNAPSHOT.jar
├── BOOT-INF
│ ├── classes
│ │ └── com
│ └── lib
│ ├── classmate-1.3.4.jar
│ ├── jackson-datatype-jdk8-2.9.6.jar
│ ├── jackson-datatype-jsr310-2.9.6.jar
│ ├── snakeyaml-1.19.jar
│ ├── spring-aop-5.0.9.RELEASE.jar
│ ├── spring-beans-5.0.9.RELEASE.jar
│ ├── spring-boot-2.0.5.RELEASE.jar
│ ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-json-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-logging-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-web-2.0.5.RELEASE.jar
│ ├── spring-context-5.0.9.RELEASE.jar
│ ├── spring-core-5.0.9.RELEASE.jar
│ ├── spring-expression-5.0.9.RELEASE.jar
│ ├── spring-jcl-5.0.9.RELEASE.jar
│ ├── spring-web-5.0.9.RELEASE.jar
│ ├── spring-webmvc-5.0.9.RELEASE.jar
│ ├── tomcat-embed-core-8.5.34.jar
│ ├── tomcat-embed-el-8.5.34.jar
│ ├── tomcat-embed-websocket-8.5.34.jar
│ └── validation-api-2.0.1.Final.jar
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── org.springframework
└── org
└── springframework
└── boot
└── loader
└── Launcher.class
└── JarLauncher.class
2. maven-shade-plugin和spring-boot-maven-plugin有何区别
META-INF 目录一定不陌生吧!基础知识提问:MANIFEST 文件有何用?
借助MANIFEST 文件可以直接使用 java -jar springboot-exaple-1.0-SNAPSHOT.jar 运行,而不用 java -classpath jar1:jar2:jar3... mainClassName 这么复杂的语法格式运行。
如果你对maven-shade-plugin有一定的了解(不了解的可以去看dubbo中的运用),二者本身没有可比性,这里讲的区别主要指的是:两者的 MANIFEST 文件的差异性。
// Generated by Maven Shade Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/gs-spring-boot
Main-Class: com.kxtx.Application// Generated by SpringBootLoader Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Spring-Boot-Version: 2.0.5.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.kxtx.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/gs-spring-boot
but,spring-boot并没有指定classpath啊,它是怎么做的的呢?
对,这里要说道说道了,首先运行环境一般有两种:
- IDE中运行,你完全感知不到打包逻辑的,它会自动帮你下载Jar包、指定classpath(细心的会发现启动控制台有答案)
- 直接执行java -jar的话,这就是springboot自己的JarLauncher奥秘,它进行解包、用依赖包配置ClassLoader、用反射调用实际main函数
具体是如何做的的呢?
SpringBoot 将 jar 包中的 Main-Class 进行了替换,换成了 JarLauncher,还增加了一个 Start-Class 参数,这个参数对应的类才是真正的业务 main 方法入口。其实jar包在启动时实际上启动的是springboot自己的JarLauncher,通过这个JarLauncher去加载 lib 下的依赖,然后去启动 Start-Class 配置对应下的类。
Start-Class是怎么找到呢?
spring-boot-maven-plugin执行时会去找有@SpringBootApplication注解的类,如果找不到,那么就检测所有的类中有Main函数的,如果找到且只找到一个就皆大欢喜,否则就报错给你看。
public abstract class MainClassFinder {private String getMainClassName() {Set<MainClass> matchingMainClasses = new LinkedHashSet<MainClass>();if (this.annotationName != null) {for (MainClass mainClass : this.mainClasses) {if (mainClass.getAnnotationNames().contains(this.annotationName)) {matchingMainClasses.add(mainClass);}}}if (matchingMainClasses.isEmpty()) {matchingMainClasses.addAll(this.mainClasses);}if (matchingMainClasses.size() > 1) {throw new IllegalStateException("Unable to find a single main class from the following candidates "+ matchingMainClasses);}return (matchingMainClasses.isEmpty() ? null: matchingMainClasses.iterator().next().getName());}}
}
public class Repackager {private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";private static final String START_CLASS_ATTRIBUTE = "Start-Class";private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes";private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";protected String findMainMethod(JarFile source) throws IOException {return MainClassFinder.findSingleMainClass(source,this.layout.getClassesLocation(), SPRING_BOOT_APPLICATION_CLASS_NAME);}
}
3. JarLauncher
Launcher
for JAR based archives. This launcher assumes that dependency jars are included inside a/BOOT-INF/lib
directory and that application classes are included inside a/BOOT-INF/classes
directory.
Main-Class的启动类是JarLaucher(源于org/springframework/boot/loader目录) ,它创建了一个特殊的 ClassLoader,然后由这个 ClassLoader 来加载 MainClass 并运行。
这里提个问题:Archive(红色的部分)又是个什么鬼?
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
//源于org.springframework.boot.loader;
public class JarLauncher extends ExecutableArchiveLauncher {protected JarLauncher(Archive archive) {super(archive);}public static void main(String[] args) throws Exception {new JarLauncher().launch(args);}
}
public abstract class Launcher {protected void launch(String[] args) throws Exception {JarFile.registerUrlProtocolHandler();// 生成自定义ClassLoaderClassLoader classLoader = this.createClassLoader(this.getClassPathArchives());// 启动应用this.launch(args, this.getMainClass(), classLoader);}protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {List<URL> urls = new ArrayList(archives.size());Iterator var3 = archives.iterator();while(var3.hasNext()) {Archive archive = (Archive)var3.next();urls.add(archive.getUrl());}return this.createClassLoader((URL[])urls.toArray(new URL[urls.size()]));}protected ClassLoader createClassLoader(URL[] urls) throws Exception {return new LaunchedURLClassLoader(urls, this.getClass().getClassLoader());}protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {//设置为当前线程上下文类加载器Thread.currentThread().setContextClassLoader(classLoader);this.createMainMethodRunner(mainClass, args, classLoader).run();}protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {return new MainMethodRunner(mainClass, args);}
}
public class MainMethodRunner {private final String mainClassName;private final String[] args;public MainMethodRunner(String mainClass, String[] args) {this.mainClassName = mainClass;this.args = args != null ? (String[])args.clone() : null;}public void run() throws Exception {Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);// 调用业务系统类(startClass)的main方法mainMethod.invoke((Object)null, this.args);}
}
4. Archive
Archive是spring boot中特有的对象,可以理解为:
- 归档文件,通常为tar/zip等格式压缩包,jar为zip格式归档文件
- 一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层
public interface Archive extends Iterable<Archive.Entry> {// 获取该归档的urlURL getUrl() throws MalformedURLException;// 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MFManifest getManifest() throws IOException;// 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jarList<Archive> getNestedArchives(EntryFilter filter) throws IOException;interface Entry {boolean isDirectory();String getName();}
}
public class JarFileArchive implements Archive {@Overridepublic URL getUrl() throws MalformedURLException {if (this.url != null) {return this.url;}return this.jarFile.getUrl();}@Overridepublic Manifest getManifest() throws IOException {return this.jarFile.getManifest();}@Overridepublic List<Archive> getNestedArchives(EntryFilter filter) throws IOException {List<Archive> nestedArchives = new ArrayList<>();for (Entry entry : this) {if (filter.matches(entry)) {nestedArchives.add(getNestedArchive(entry));}}return Collections.unmodifiableList(nestedArchives);}
}
public class ExplodedArchive implements Archive {}
springboot-exaple-1.0-SNAPSHOT.jar 既为一个JarFileArchive,springboot-exaple-1.0-SNAPSHOT.jar!/BOOT-INF/lib下的每一个jar包也是一个JarFileArchive。将springboot-exaple-1.0-SNAPSHOT.jar解压到目录springboot-exaple-1.0-SNAPSHOT后,则该目录就是一个ExplodedArchive。
创建archive在ExecutableArchiveLauncher ,它是JarLauncher 和WarLauncher的父类。
public abstract class ExecutableArchiveLauncher extends Launcher {// 在自己所在的jar,并创建Archivepublic ExecutableArchiveLauncher() {try {this.archive = createArchive();}catch (Exception ex) {throw new IllegalStateException(ex);}}@Overrideprotected String getMainClass() throws Exception {Manifest manifest = this.archive.getManifest();String mainClass = null;if (manifest != null) {mainClass = manifest.getMainAttributes().getValue("Start-Class");}if (mainClass == null) {throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);}return mainClass;}// 获取/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录对应的archive@Overrideprotected List<Archive> getClassPathArchives() throws Exception {List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));postProcessClassPathArchives(archives);return archives;}
}
public abstract class Launcher {protected final Archive createArchive() throws Exception {ProtectionDomain protectionDomain = getClass().getProtectionDomain();CodeSource codeSource = protectionDomain.getCodeSource();URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;String path = (location != null) ? location.getSchemeSpecificPart() : null;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));}
}
5. LaunchedURLClassLoader
新的问题来了,当 JVM 遇到一个不认识的类,BOOT-INF/lib 目录里又有那么多 jar 包,它是如何知道去哪个 jar 包里加载呢?
java中定义了URL的概念,对应的URLConnection,可以灵活地获取多种URL协议(http、 file、 ftp、 jar )下的资源,具体的可以看我之前分享的URL拓展协议。
每个jar都会对应一个url,如:jar:file:/data/springboot-exaple-1.0-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/
jar中的资源对应的url,并以'!/'分割,如:jar:file:/data/springboot-exaple-1.0-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class
关于jar中的资源对应的url,原始的 java.util.jar.JarFile只支持一个'!/',而SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源。自定义URL的类格式为[pkgs].[protocol].Handler,具体实现参考JarFile.registerUrlProtocolHandler()。
spring如何读取SpringProxy.class呢?
会循环处理'!/'分隔符,从最上层出发,先构造springboot-exaple-1.0-SNAPSHOT.jar的JarFile,再构造spring-aop-5.0.4.RELEASE.jar的JarFile,最后构造指向SpringProxy.class的JarURLConnection,通过JarURLConnection的getInputStream方法获取SpringProxy.class内容。
ClassLoader 会在本地缓存包名和 jar包路径的映射关系。
public class LaunchedURLClassLoader extends URLClassLoader {//入口protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {Handler.setUseFastConnectionExceptions(true);Class var3;try {try {this.definePackageIfNecessary(name);} catch (IllegalArgumentException var7) {if (this.getPackage(name) == null) {throw new AssertionError("Package " + name + " has already been defined but it could not be found");}}var3 = super.loadClass(name, resolve);} finally {Handler.setUseFastConnectionExceptions(false);}return var3;}private void definePackageIfNecessary(String className) {int lastDot = className.lastIndexOf(46);if (lastDot >= 0) {String packageName = className.substring(0, lastDot);if (this.getPackage(packageName) == null) {try {this.definePackage(className, packageName);} catch (IllegalArgumentException var5) {if (this.getPackage(packageName) == null) {throw new AssertionError("Package " + packageName + " has already been defined but it could not be found");}}}}}private void definePackage(String className, String packageName) {try {AccessController.doPrivileged(() -> {String packageEntryName = packageName.replace('.', '/') + "/";String classEntryName = className.replace('.', '/') + ".class";URL[] var5 = this.getURLs();int var6 = var5.length;for(int var7 = 0; var7 < var6; ++var7) {URL url = var5[var7];try {URLConnection connection = url.openConnection();if (connection instanceof JarURLConnection) {JarFile jarFile = ((JarURLConnection)connection).getJarFile();if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null && jarFile.getManifest() != null) {this.definePackage(packageName, jarFile.getManifest(), url);return null;}}} catch (IOException var11) {}}return null;}, AccessController.getContext());} catch (PrivilegedActionException var4) {}}public void clearCache() {URL[] var1 = this.getURLs();int var2 = var1.length;for(int var3 = 0; var3 < var2; ++var3) {URL url = var1[var3];try {URLConnection connection = url.openConnection();if (connection instanceof JarURLConnection) {this.clearCache(connection);}} catch (IOException var6) {}}}
}
// java.util.jar.JarFile的拓展体,可以访问内部任何目录项或jar文件
public class JarFile extends java.util.jar.JarFile {private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");// 注册一个'java.protocol.handler.pkgs'属性以便URLStreamHandler可以处理jar urlspublic static void registerUrlProtocolHandler() {String handlers = System.getProperty(PROTOCOL_HANDLER, "");System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE: handlers + "|" + HANDLERS_PACKAGE));resetCachedUrlHandlers();}
}
//jar-url处理器
public class Handler extends URLStreamHandler {private static final String JAR_PROTOCOL = "jar:";private static final String FILE_PROTOCOL = "file:";private static final String SEPARATOR = "!/";//在处理如下URL时,会循环处理'!/'分隔符private static final String CURRENT_DIR = "/./";private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile("/./");private static final String PARENT_DIR = "/../";private static final String[] FALLBACK_HANDLERS = new String[]{"sun.net.www.protocol.jar.Handler"};private static final Method OPEN_CONNECTION_METHOD;private static SoftReference<Map<File, JarFile>> rootFileCache;private final JarFile jarFile;private URLStreamHandler fallbackHandler;
}
//jar-url 读取文件
final class JarURLConnection extends java.net.JarURLConnection {private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal();private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException("Jar file or entry not found");private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION;private static final String SEPARATOR = "!/";private static final URL EMPTY_JAR_URL;private static final JarURLConnection.JarEntryName EMPTY_JAR_ENTRY_NAME;private static final String READ_ACTION = "read";
}
这里思考一个问题:war是一个怎样的处理流程呢?
6. WarLauncher
首先将内嵌容器相关依赖设为provided,再重写SpringBootServletInitializer的configure方法。
@SpringBootApplication
public class WebApp extends SpringBootServletInitializer {public static void main(String[] args) {SpringApplication.run(WebApp.class, args);}@Overrideprotected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {return builder.sources(WebApp.class);}
}
它构建处理的结构大概是这样的:
springboot-exaple-1.0-SNAPSHOT.war
├── META-INF
│ └── MANIFEST.MF
├── WEB-INF
│ ├── classes
│ │ └── 应用程序
│ └── lib
│ └── 第三方依赖jar
│ └── lib-provided
│ └── 与内嵌容器相关的第三方依赖jar
└── org
└── springframework
└── boot
└── loader
└── springboot启动程序
MANIFEST.MF内容为:
Manifest-Version: 1.0
Start-Class: com.kxtx.Application
Main-Class: org.springframework.boot.loader.WarLauncher
WarLauncher实现,其实与JarLauncher并无太大差别。差别仅在于:JarLauncher在构建LauncherURLClassLoader时,会搜索BOOT-INF/classes目录及BOOT-INF/lib目录下jar,WarLauncher在构建LauncherURLClassLoader时,则会搜索WEB-INFO/classes目录及WEB-INFO/lib和WEB-INFO/lib-provided两个目录下的jar。
因此构建出的war便支持两种启动方式:
- 直接运行./springboot-exaple-1.0-SNAPSHOT.war start
- 部署到Tomcat容器中
spring boot提供的除了JarLauncher、WarLauncher 之外,还提供更为轻量的PropretiesLauncher。
7. SpringBootApplication
我们知道SpringBoot 深度依赖注解来完成配置的自动装配工作,它发明了几十个注解,你需要仔细阅读文档才能知道它是用来干嘛的。@SpringBootApplication
是一个复合注解,包括@ComponentScan
,和@SpringBootConfiguration
,@EnableAutoConfiguration。
@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。可以通过basePackages等属性来细粒度的定制自动扫描的范围,因为默认不指定basePackages,这也就是为什么SpringBoot的启动类最好是放在root package下的原因。
@SpringBootApplication(scanBasePackages = {"com.example"})
public class WebApp {public static void main(String[] args) {SpringApplication.run(WebApp.class, args);}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration // springboot的大神器之一,其借助@import的帮助
@ComponentScan(excludeFilters = { // 扫描路径设置@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
// 继承了Configuration,表示当前是注解类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
总结,EnableAutoConfiguration的强大神奇之处,三言两语无法道尽,在下一篇中工作原理介绍。
Spring Boot 容器启动原理揭秘相关推荐
- Spring Boot(18)---启动原理解析
Spring Boot(18)---启动原理解析 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会 ...
- spring boot应用启动原理分析
spring boot quick start 在spring boot 里,很吸引人的一个特性是可以直接把应用打包成为一个jar/war,然后这个jar/war是可以直接启动的,不需要另外配置一个W ...
- 硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
" 摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/jar/ 「芋道源码」欢迎转载,保留摘要,谢谢! 1. 概述 2. MANIFEST.MF 3. J ...
- Spring Boot 项目启动原理彻底解剖分析
文章目录 一.场景介绍 二.项目搭建 三.解体 JAR 包 四.原理分析 一.场景介绍 spring-boot 项目搭建以后启动方式一般有两种: 源码方式启动 @SpringBootApplicati ...
- Spring Boot学习总结(26)—— Spring Boot 容器启动详解
一.容器启动 spring boot 一般是指定容器启动 main 方法,然后以命令行方式启动Jar包,如: @SpringBootApplication public class Applicati ...
- 2. Spring Boot项目启动原理初探
SpringBoot从宏观上说,就是对spring容器进行了一层包装.它内部的入口是利用 SpringApplication类的static的 run 方法进行启动的,调用的图: 上图中的这些方法都位 ...
- 学习第三篇:【SpringBoot-Labs】芋道 Spring Boot 自动配置原理
本周(8.21-8.27)将学习芋道 Spring Boot的以下文章: 8.21: 快速入门 8.22:Spring Boot 自动配置原理 .Jar 启动原理 8.23:调试环境. 热部署入门.消 ...
- Spring Boot自动装配原理详解
目录 1.环境和依赖 1.1.spring boot版本 1.2.依赖管理 2.自动装配 2.1.流程概述 2.2.三大步前的准备工作 2.2.1.注解入口 2.2.2.获取所有配置类 2.3.获取过 ...
- Spring Boot自动配置原理、实战
Spring Boot自动配置原理 Spring Boot的自动配置注解是@EnableAutoConfiguration, 从上面的@Import的类可以找到下面自动加载自动配置的映射. org.s ...
最新文章
- python 参数类型的多态_【Python】面向对象:类与对象\封装\继承\多态
- Android获取屏幕尺寸大小
- php怎么实现md5加密,php如何进行md5加密
- 对齐方式有那些_Excel基础:开始菜单之对齐方式,那些被遗忘的实用功能
- Javascript 中 Array.push 要比 Array.concat 快 945 倍
- java获取正在执行的timer_Java线程与并行编程(一)
- qwtqplot用法
- 很棒的远程执行工具psexec的用法
- 对于打LOG的方法 可以这样搞
- ubuntu 查找opencv安装路径_Ubuntu系统---配置OpenCV
- unity 脚本把变量放一起,在界面上显示,同时鼠标靠近时有注释出现,变量是滑动条有区间
- C#实现对Access数据库的通用操作
- linux如何共享网络打印机,Ubunt如何安装网络打印机的详细图文步骤
- 四金及个人所得税的计算方法
- Jmeter性能测试云平台搭建
- 丁神去谷歌-北邮OJ416
- 数字图像处理第八章----图像压缩
- libjpeg的简单使用
- 《编译原理》-用例题理解-自底向上的语法分析,FIRSTVT,LASTVT集
- 推荐计算机视觉机器视觉行业研究发展规划前景投资市场行情分析报告(附件中为网盘链接,报告持续更新)
热门文章
- 个人主页制作更新——添加导航栏
- 腾讯云弹性伸缩工程优化揭秘
- Winform界面实现控件中英文语言切换
- 开挂的00后!17岁「天才少女」被8所世界名校录取,最终选择MIT计算机系
- Warning: can't write resource [META-INF/MANIFEST.MF] (Duplicate zip entry [yyy.jar:META-INF/MANIFEST
- 爬取淘宝商家货物简单销售数据,双十一马上就到了,秒杀准备了吗
- 三防加固平板电脑能在恶劣的工业环境中依然正常运作
- 知识蒸馏论文翻译(3)—— Ensembled CTR Prediction via Knowledge Distillation
- safari调试iPhone app web页面
- 大度是人生不可缺少的高贵品质