该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读

Spring Boot 版本:2.2.x

最好对 Spring 源码有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章导读》 系列文章

如果该篇内容对您有帮助,麻烦点击一下“推荐”,也可以关注博主,感激不尽~

概述

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以很方便的将我们的 Spring Boot 项目打成 jar 包或者 war 包。

考虑到部署的便利性,我们绝大多数(99.99%)的场景下,都会选择打成 jar 包,这样一来,我们就无需将项目部署于 Tomcat、Jetty 等 Servlet 容器中。

那么,通过 Spring Boot 插件生成的 jar 包是如何运行,并启动 Spring Boot 应用的呢?这个就是本文的目的,我们一起来弄懂 Spring Boot jar 包的运行原理

这里,我通过 Spring Boot Maven Plugin 生成了一个 jar 包,其里面的结构如下所示:

  1. BOOT-INF 目录,里面保存了我们自己 Spring Boot 项目编译后的所有文件,其中 classes 目录下面就是编译后的 .class 文件,包括项目中的配置文件等,lib 目录下就是我们引入的第三方依赖
  2. META-INF 目录,通过 MANIFEST.MF 文件提供 jar 包的元数据,声明 jar 的启动类等信息。每个 Java jar 包应该是都有这个文件的,参考 Oracle 官方对于 jar 的说明,里面有一个 Main-Class 配置用于指定启动类
  3. org.springframework.boot.loader 目录,也就是 Spring Boot 的 spring-boot-loader 工具模块,它就是 java -jar xxx.jar 启动 Spring Boot 项目的秘密所在,上面的 Main-Class 指定的就是该工具模块中的一个类

MANIFEST.MF

META-INF/MANIFEST.MF 文件如下:

Manifest-Version: 1.0
Implementation-Title: spring-boot-study
Implementation-Version: 1.0.0-SNAPSHOT
Built-By: jingping
Implementation-Vendor-Id: org.springframework.boot.demo
Spring-Boot-Version: 2.0.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher # spring-boot-loader 中的启动类
Start-Class: org.springframework.boot.demo.Application # 你的 Spring Boot 项目中的启动类
Spring-Boot-Classes: BOOT-INF/classes/ # 你的 Spring Boot 项目编译后的 .class 文件所在目录
Spring-Boot-Lib: BOOT-INF/lib/ # 你的 Spring Boot 项目所引入的第三方依赖所在目录
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_251
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/info-dependencies/dwzq-info/info-stock-project/sp-provider

参考 Oracle 官方对该的说明:

  • Main-Class:Java 规定的 jar 包的启动类,这里设置为 spring-boot-loader 项目的 JarLauncher 类,进行 Spring Boot 应用的启动
  • Start-Class:Spring Boot 规定的启动类,这里通过 Spring Boot Maven Plugin 插件打包时,会设置为我们定义的 Application 启动类

为什么不直接将我们的 Application 启动类设置为 Main-Class 启动呢?

因为通过 Spring Boot Maven Plugin 插件打包后的 jar 包,我们的 .class 文件在 BOOT-INF/classes/ 目录下,在 Java 默认的 jar 包加载规则下找不到我们的 Application 启动类,也就需要通过 JarLauncher 启动加载。

当然,还有一个原因,Java 规定可执行器的 jar 包禁止嵌套其它 jar 包,在 BOOT-INF/lib 目录下有我们 Spring Boot 应用依赖的所有第三方 jar 包,因此spring-boot-loader 项目自定义实现了 ClassLoader 实现类 LaunchedURLClassLoader,支持加载 BOOT-INF/classes 目录下的 .class 文件,以及 BOOT-INF/lib 目录下的 jar 包。

接下来,我们一起来看看 Spring Boot 的 JarLauncher 这个类

1. JarLauncher

类图:

上面的 WarLauncher 是针对 war 包的启动类,和 JarLauncher 差不多,感兴趣的可以看一看,这里我们直接来看到 JarLauncher 这个类

public class JarLauncher extends ExecutableArchiveLauncher {

	static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

	static final String BOOT_INF_LIB = "BOOT-INF/lib/";

	public JarLauncher() {
} protected JarLauncher(Archive archive) {
super(archive);
} @Override
protected boolean isNestedArchive(Archive.Entry entry) {
// 只接受 `BOOT-INF/classes/` 目录
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// 只接受 `BOOT-INF/lib/` 目录下的 jar 包
return entry.getName().startsWith(BOOT_INF_LIB);
} /**
* 这里是 java -jar 启动 SpringBoot 打包后的 jar 包的入口
* 可查看 jar 包中的 META-INF/MANIFEST.MF 文件(该文件用于对 Java 应用进行配置)
* 参考 Oracle 官方对于 jar 的说明(https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html)
* 该文件其中会有一个配置项:Main-Class: org.springframework.boot.loader.JarLauncher
* 这个配置表示会调用 JarLauncher#main(String[]) 方法,也就当前方法
*/
public static void main(String[] args) throws Exception {
// <1> 创建当前类的实例对象,会创建一个 Archive 对象(当前应用),可用于解析 jar 包(当前应用)中所有的信息
// <2> 调用其 launch(String[]) 方法
new JarLauncher().launch(args);
}
}

可以看到它有个 main(String[]) 方法,前面说到的 META-INF/MANIFEST.MF 文件中的 Main-Class 配置就是指向了这个类,也就会调用这里的 main 方法,会做下面两件事:

  1. 创建一个 JarLauncher 实例对象,在 ExecutableArchiveLauncher 父类中会做以下事情:

    public abstract class ExecutableArchiveLauncher extends Launcher {
    
    	private final Archive archive;
    
    	public ExecutableArchiveLauncher() {
    try {
    // 为当前应用创建一个 Archive 对象,可用于解析 jar 包(当前应用)中所有的信息
    this.archive = createArchive();
    }
    catch (Exception ex) {
    throw new IllegalStateException(ex);
    }
    } protected final Archive createArchive() throws Exception {
    // 获取 jar 包(当前应用)所在的绝对路径
    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");
    }
    // 当前 jar 包
    File root = new File(path);
    if (!root.exists()) {
    throw new IllegalStateException("Unable to determine code source archive from " + root);
    }
    // 为当前 jar 包创建一个 JarFileArchive(根条目),需要通过它解析出 jar 包中的所有信息
    // 如果是文件夹的话则创建 ExplodedArchive(根条目)
    return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
    }
    }

    会为当前应用创建一个 Archive 对象,可用于解析 jar 包(当前应用)中所有的信息,可以把它理解为一个“根”对象,可以通过它获取我们所需要的类信息

  2. 调用 JarLauncher#launch(String[]) 方法,也就是调用父类 Launcher 的这个方法

2. Launcher

org.springframework.boot.loader.Launcher,Spring Boot 应用的启动器

2. launch 方法

public abstract class Launcher {

	/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
// <1> 注册 URL(jar)协议的处理器
JarFile.registerUrlProtocolHandler();
// <2> 先从 `archive`(当前 jar 包应用)解析出所有的 JarFileArchive
// <3> 创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 中所有的类
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// <4> 获取当前应用的启动类(你自己写的那个 main 方法)
// <5> 执行你的那个 main 方法
launch(args, getMainClass(), classLoader);
}
}

会做以下几件事:

  1. 调用 JarFile#registerUrlProtocolHandler() 方法,注册 URL(jar)协议的处理器,主要是使用自定义的 URLStreamHandler 处理器处理 jar 包
  2. 调用 getClassPathArchives() 方法,先从 archive(当前 jar 包应用)解析出所有的 JarFileArchive,这个 archive 就是在上面创建 JarLauncher 实例对象过程中创建的
  3. 调用 createClassLoader(List<Archive>) 方法,创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 包中所有的类,包括依赖的第三方包
  4. 调用 getMainClass() 方法,获取当前应用的启动类(你自己写的那个 main 方法所在的 Class 类对象)
  5. 调用 launch(...) 方法,执行你的项目中那个启动类的 main 方法(反射)

你可以理解为会创建一个自定义的 ClassLoader 类加载器,主要可加载 BOOT-INF/classes 目录下的类,以及 BOOT-INF/lib 目录下的 jar 包中的类,然后调用你 Spring Boot 应用的启动类的 main 方法

接下来我们逐步分析上面的每个步骤

2.1 registerUrlProtocolHandler 方法

备注:注册 URL(jar)协议的处理器

这个方法在 org.springframework.boot.loader.jar.JarFile 中,这个类是 java.util.jar.JarFile 的子类,对它进行扩展,提供更多的功能,便于操作 jar

public static void registerUrlProtocolHandler() {
// <1> 获取系统变量中的 `java.protocol.handler.pkgs` 配置的 URLStreamHandler 路径
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
// <2> 将 Spring Boot 自定义的 URL 协议处理器路径(`org.springframework.boot.loader`)添加至系统变量中
// JVM 启动时会获取 `java.protocol.handler.pkgs` 属性,多个用 `|` 分隔,以他们作为包名前缀,然后使用 `包名前缀.协议名.Handler` 作为该协议的实现
// 那么这里就会将 `org.springframework.boot.loader.jar.Handler` 作为 jar 包协议的实现
System.setProperty(PROTOCOL_HANDLER,
("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
// <3> 重置已缓存的 URLStreamHandler 处理器们,避免重复创建
resetCachedUrlHandlers();
}

方法的处理过程如下:

  1. 获取系统变量中的 java.protocol.handler.pkgs 配置的 URLStreamHandler 路径

  2. 将 Spring Boot 自定义的 URL 协议处理器路径(org.springframework.boot.loader)添加至系统变量中

    JVM 启动时会获取 java.protocol.handler.pkgs 属性,多个用 | 分隔,以他们作为包名前缀,然后使用 包名前缀.协议名.Handler 作为该协议的实现

    那么这里就会将 org.springframework.boot.loader.jar.Handler 作为 jar 包协议的实现,用于处理 jar 包

  3. 重置已缓存的 URLStreamHandler 处理器们,避免重复创建

    private static void resetCachedUrlHandlers() {    try {        URL.setURLStreamHandlerFactory(null);    } catch (Error ex) {        // Ignore    }}

2.2 getClassPathArchives 方法

备注:从 archive(当前 jar 包应用)解析出所有的 JarFileArchive

该方法在 org.springframework.boot.loader.ExecutableArchiveLauncher 子类中实现,如下:

@Overrideprotected List<Archive> getClassPathArchives() throws Exception {    // <1> 创建一个 Archive.EntryFilter 类,用于判断 Archive.Entry 是否匹配,过滤 jar 包(当前应用)以外的东西    // <2> 从 `archive`(当前 jar 包)解析出所有 Archive 条目信息    List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));    postProcessClassPathArchives(archives);    // <3> 返回找到的所有 JarFileArchive    // `BOOT-INF/classes/` 目录对应一个 JarFileArchive(因为就是当前应用中的内容)    // `BOOT-INF/lib/` 目录下的每个 jar 包对应一个 JarFileArchive    return archives;}

过程如下:

  1. 创建一个 Archive.EntryFilter 实现类,用于判断 Archive.Entry 是否匹配,过滤掉 jar 包(当前应用)以外的东西

    public class JarLauncher extends ExecutableArchiveLauncher {	static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";	static final String BOOT_INF_LIB = "BOOT-INF/lib/";	@Override	protected boolean isNestedArchive(Archive.Entry entry) {		// 只接受 `BOOT-INF/classes/` 目录		if (entry.isDirectory()) {			return entry.getName().equals(BOOT_INF_CLASSES);		}		// 只接受 `BOOT-INF/lib/` 目录下的 jar 包		return entry.getName().startsWith(BOOT_INF_LIB);	}}
  2. archive(当前 jar 包)解析出所有 Archive 条目信息,这个 archive 在上面 1. JarLauncher 讲到过,创建 JarLauncher 实例化对象的时候会初始化 archive,是一个 JarFileArchive 对象,也就是我们打包后的 jar 包,那么接下来需要从中解析出所有的 Archive 对象

    // JarFileArchive.java@Overridepublic List<Archive> getNestedArchives(EntryFilter filter) throws IOException {    List<Archive> nestedArchives = new ArrayList<>();    // 遍历 jar 包(当前应用)中所有的 Entry    for (Entry entry : this) {        // 进行过滤,`BOOT-INF/classes/` 目录或者 `BOOT-INF/lib/` 目录下的 jar 包        if (filter.matches(entry)) {            // 将 Entry 转换成 JarFileArchive            nestedArchives.add(getNestedArchive(entry));        }    }    // 返回 jar 包(当前应用)找到的所有 JarFileArchive    // `BOOT-INF/classes/` 目录对应一个 JarFileArchive(因为就是当前应用中的内容)    // `BOOT-INF/lib/` 目录下的每个 jar 包对应一个 JarFileArchive    return Collections.unmodifiableList(nestedArchives);}

    返回 jar 包(当前应用)找到的所有 JarFileArchive:

    • BOOT-INF/classes/ 目录对应一个 JarFileArchive(因为就是当前 Spring Boot 应用编译后的内容)
    • BOOT-INF/lib/ 目录下的每个 jar 包对应一个 JarFileArchive
  3. 返回从 jar 包中找到的所有 JarFileArchive

这一步骤就是从 jar 包中解析出我们需要的东西来,如上描述,每个 JarFileArchive 会对应一个 JarFile 对象

2.3 createClassLoader 方法

备注:创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 中所有的类

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {    // <1> 获取所有 JarFileArchive 对应的 URL    List<URL> urls = new ArrayList<>(archives.size());    for (Archive archive : archives) {        urls.add(archive.getUrl());    }    // <2> 创建 Spring Boot 自定义的 ClassLoader 类加载器,并设置父类加载器为当前线程的类加载器    // 通过它解析这些 URL,也就是加载 `BOOT-INF/classes/` 目录下的类和 `BOOT-INF/lib/` 目录下的所有 jar 包    return createClassLoader(urls.toArray(new URL[0]));}protected ClassLoader createClassLoader(URL[] urls) throws Exception {    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());}

该过程如下:

  1. 获取所有 JarFileArchive 对应的 URL
  2. 创建 Spring Boot 自定义的 ClassLoader 类加载器,并设置父类加载器为当前线程的类加载器

可以看到 LaunchedURLClassLoader 为自定义类加载器,这样就能从我们 jar 包中的 BOOT-INF/classes/ 目录下和 BOOT-INF/lib/ 目录下的所有三方依赖包中加载出 Class 类对象

2.4 getMainClass 方法

备注:获取当前应用的启动类(你自己写的那个 main 方法)

// ExecutableArchiveLauncher.java@Overrideprotected String getMainClass() throws Exception {    // 获取 jar 包(当前应用)的 Manifest 对象,也就是 META-INF/MANIFEST.MF 文件中的属性    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;}

过程如下:

  1. 获取 jar 包(当前应用)的 Manifest 对象,也就是 META-INF/MANIFEST.MF 文件中的属性
  2. 获取启动类(当前应用自己的启动类),也就是 Start-Class 配置,并返回

可以看到,这一步就是找到你 Spring Boot 应用的启动类,前面 ClassLoader 类加载器都准备好了,那么现在不就可以直接调用这个类的 main 方法来启动应用了

2.5 launch 方法

备注:执行你的 Spring Boot 应用的启动类的 main 方法

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {    // 设置当前线程的 ClassLoader 为刚创建的类加载器    Thread.currentThread().setContextClassLoader(classLoader);    // 创建一个 MainMethodRunner 对象(main 方法执行器)    // 执行你的 main 方法(反射)    createMainMethodRunner(mainClass, args, classLoader).run();}protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {    return new MainMethodRunner(mainClass, args);}

整个过程很简单,先设置当前线程的 ClassLoader 为刚创建的类加载器,然后创建一个 MainMethodRunner 对象(main 方法执行器),执行你的 main 方法(反射),启动 Spring Boot 应用

public class MainMethodRunner {	private final String mainClassName;	private final String[] args;	public MainMethodRunner(String mainClass, String[] args) {		this.mainClassName = mainClass;		this.args = (args != null) ? args.clone() : null;	}	public void run() throws Exception {		// 根据名称加载 main 方法所在类的 Class 对象		Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);		// 获取 main 方法		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);		// 执行这个 main 方法(反射)		mainMethod.invoke(null, new Object[] { this.args });	}}

这里就是通过反射调用你的 Spring Boot 应用的启动类的 main 方法

LaunchedURLClassLoader

org.springframework.boot.loader.LaunchedURLClassLoaderspring-boot-loader 中自定义的类加载器,实现对 jar 包中 BOOT-INF/classes 目录下的BOOT-INF/lib 下第三方 jar 包中的加载

public class LaunchedURLClassLoader extends URLClassLoader {	static {		ClassLoader.registerAsParallelCapable();	}	/**	 * Create a new {@link LaunchedURLClassLoader} instance.	 * @param urls the URLs from which to load classes and resources	 * @param parent the parent class loader for delegation	 */	public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {		super(urls, parent);	}	/**	 * 重写类加载器中加载 Class 类对象方法	 */	@Override	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {		Handler.setUseFastConnectionExceptions(true);		try {			try {				// 判断这个类是否有对应的 Package 包				// 没有的话会从所有 URL(包括内部引入的所有 jar 包)中找到对应的 Package 包并进行设置				definePackageIfNecessary(name);			}			catch (IllegalArgumentException ex) {				// Tolerate race condition due to being parallel capable				if (getPackage(name) == null) {					// This should never happen as the IllegalArgumentException indicates					// that the package has already been defined and, therefore,					// getPackage(name) should not return null.					throw new AssertionError("Package " + name + " has already been defined but it could not be found");				}			}			// 加载对应的 Class 类对象			return super.loadClass(name, resolve);		}		finally {			Handler.setUseFastConnectionExceptions(false);		}	}	/**	 * Define a package before a {@code findClass} call is made. This is necessary to	 * ensure that the appropriate manifest for nested JARs is associated with the	 * package.	 * @param className the class name being found	 */	private void definePackageIfNecessary(String className) {		int lastDot = className.lastIndexOf('.');		if (lastDot >= 0) {			// 获取包名			String packageName = className.substring(0, lastDot);			// 没找到对应的 Package 包则进行解析			if (getPackage(packageName) == null) {				try {					// 遍历所有的 URL,从所有的 jar 包中找到这个类对应的 Package 包并进行设置					definePackage(className, packageName);				}				catch (IllegalArgumentException ex) {					// Tolerate race condition due to being parallel capable					if (getPackage(packageName) == null) {						// This should never happen as the IllegalArgumentException						// indicates that the package has already been defined and,						// therefore, getPackage(name) should not have returned 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((PrivilegedExceptionAction<Object>) () -> {				// 把类路径解析成类名并加上 .class 后缀				String packageEntryName = packageName.replace('.', '/') + "/";				String classEntryName = className.replace('.', '/') + ".class";				// 遍历所有的 URL(包括应用内部引入的所有 jar 包)				for (URL url : getURLs()) {					try {						URLConnection connection = url.openConnection();						if (connection instanceof JarURLConnection) {							JarFile jarFile = ((JarURLConnection) connection).getJarFile();							// 如果这个 jar 中存在这个类名,且有对应的 Manifest							if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null									&& jarFile.getManifest() != null) {								// 定义这个类对应的 Package 包								definePackage(packageName, jarFile.getManifest(), url);								return null;							}						}					}					catch (IOException ex) {						// Ignore					}				}				return null;			}, AccessController.getContext());		}		catch (java.security.PrivilegedActionException ex) {			// Ignore		}	}}

上面的代码就不一一讲述了,LaunchedURLClassLoader 重写了 ClassLoader 的 loadClass(String, boolean) 加载 Class 类对象方法,在加载对应的 Class 类对象之前新增了一部分逻辑,会尝试从 jar 包中定义 Package 包对象,这样就能加载到对应的 Class 类对象。

总结

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以很方便的将我们的 Spring Boot 项目打成 jar 包,jar 包中主要分为三个模块:

  • BOOT-INF 目录,里面保存了我们自己 Spring Boot 项目编译后的所有文件,其中 classes 目录下面就是编译后的 .class 文件,包括项目中的配置文件等,lib 目录下就是我们引入的第三方依赖
  • META-INF 目录,通过 MANIFEST.MF 文件提供 jar 包的元数据,声明 jar 的启动类等信息。每个 Java jar 包应该是都有这个文件的,参考 Oracle 官方对于 jar 的说明,里面有一个 Main-Class 配置用于指定启动类
  • org.springframework.boot.loader 目录,也就是 Spring Boot 的 spring-boot-loader 子模块,它就是 java -jar xxx.jar 启动 Spring Boot 项目的秘密所在,上面的 Main-Class 指定的就是里面的一个类

通过 java -jar 启动应用时,根据 Main-Class 配置会调用 org.springframework.boot.loader.JarLaunchermain(String[]) 方法;其中会先创建一个自定义的 ClassLoader 类加载器,可从BOOT-INF目录下加载出我们 Spring Boot 应用的 Class 类对象,包括依赖的第三方 jar 包中的 Class 类对象;然后根据 Start-Class 配置调用我们 Spring Boot 应用启动类的 main(String[]) 方法(反射),这样也就启动了应用,至于我们的 main(String[]) 方法中做了哪些事情,也就是后续所讲的内容。

精尽Spring Boot源码分析 - Jar 包的启动实现的更多相关文章

  1. 精尽Spring Boot源码分析 - 文章导读

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  2. 精尽Spring Boot源码分析 - SpringApplication 启动类的启动过程

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  3. 精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  4. 精尽Spring Boot源码分析 - 支持外部 Tomcat 容器的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  5. 精尽Spring Boot源码分析 - 剖析 @SpringBootApplication 注解

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  6. 精尽Spring Boot源码分析 - 配置加载

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  7. 精尽Spring Boot源码分析 - 日志系统

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  8. 精尽Spring Boot源码分析 - Condition 接口的扩展

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  9. 精尽Spring Boot源码分析 - @ConfigurationProperties 注解的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

随机推荐

  1. OO第三单元作业(JML)总结

    OO第三单元作业(JML)总结 目录 OO第三单元作业(JML)总结 JML语言知识梳理 使用jml的目的 jml注释结构 jml表达式 方法规格 类型规格 SMT Solver 部署JMLUnitN ...

  2. 向Vertex Shader传递vertex attribute

    在VBO.VAO和EBO那一节,介绍了如何向Vertex Shader传递vertex attribute的基本方法.现在我准备把这个话题再次扩展开. 传递整型数据 之前我们的顶点属性数据都是floa ...

  3. 4.启动虚拟机 设置CentOS7

    启动虚拟机 CentOS设置 1.点击箭头方向即可启动我们的VMware 2.设置语言 在第一步设置完成后,我们一直等待,即可来到语言设置界面 此处我们设置[中文] 3.设置安装信息 将下面带有[感叹 ...

  4. java中基本数据类型、包装类及字符串之间的相互转换

    基本数据类型:不支持面向对象的编程机制(没有属性和方法),即不支持面向对象,之所以提供8中基本数据类型,是为了方便常规数据的处理. 包装类:通过包装类可以将基本数据类型的值包装为引用数据类型的对象,使 ...

  5. ARM64平台编译stream、netperf出错解决办法 解决办法:指定编译平台为alpha [root@localhost netperf-2.6.0]# ./configure –build=alpha

    ARM64平台编译stream.netperf出错解决办法 http://ilinuxkernel.com/?p=1738 stream编译出错信息: [root@localhost stream]# ...

  6. linux进阶之nmtui和nmcli配置网络

    CentOS7配置网络推荐使用NetworkManager服务(不推荐network服务). 图形化方式:nmtui或Applications->System Tools->Setting ...

  7. 大数据 什么是 ETL

    ETL 概念 ETL 这个术语来源于数据仓库,ETL 指的是将业务系统的数据经过抽取.清洗转换之后加载到数据仓库的过程.ETL 的目的是将企业中的分散.零乱.标准不统一的数据整合到一起,为企业的决策提 ...

  8. IDEA workspace.xml 在 git 中无法忽略 ignore 问题

    问题描述 关于 .idea 的文件夹中的 workspace.xml 设置 ignore 之后每次 commit 依旧提示需要提交改变,这就会导致, 每次merge就会导致提示"本地文件改变 ...

  9. CentOS 下解决ssh登录 locale 警告

    最近登录一台CentOS 6机器,发现每次登录都提示如下警告: -bash: warning: setlocale: LC_CTYPE: cannot change locale (en_US.UTF ...

  10. 孔乙己,一名ERP顾问

    欢迎关注微信公众号:sap_gui (ERP咨询顾问之家) 公司的会议室的格局,是和别处不同的:都是中间一个大的会议圆桌,桌子上面放着各台电脑,可以随时打开ERP系统.做ERP顾问的人,傍午傍晚下了班 ...