1.Spring Boot之独立的Spring应用

307 阅读4分钟

独立的Spring应用

之前的Spring web项目,都是Spring被动监听Tomcat(ContextLoaderListener监听Servlet容器的启动),然后启动上下文。而Spring Boot搭建的应用却是自己当家做主,将Tomcat作为内嵌容器使用。

我们运行Spring Boot应用时,只需要通过如下两种方式:

  • 开发环境

    mvn spring-boot:run

  • 线上环境

    mvn package 获取可执行jar/war包

    java -jar ....war/jar 执行jar

从如上的描述,我们可以明显得知,Spring Boot应用是独立的Spring应用,Spring Boot应用尽在一个war/jar包中,一切都由Spring当家做主,而之前的Spring程序都是被打成一个war包放入Servlet容器中运行。

JarLauncher

我们将mvn package后的jar包解压,可得

这目录本身像极了一个web程序

我们主要看一下META-INF下的MANIFEST.MF文件

Manifest-Version: 1.0
Implementation-Title: first-app-by-gui
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: thinkinginspringboot.firstappbygui.FirstAppByGuiApplicati
 on
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.7.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
  • Start-Class对应的是Spring Boot程序的项目引导类

  • Main-Class 是可执行jar的启动器

    可执行jar 也就是执行了mvn package得到的jar包

org.springframework.boot.loader.JarLauncher需要我们仔细研究一下

/**
 * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
 * included inside a {@code /BOOT-INF/lib} directory and that application classes are
 * included inside a {@code /BOOT-INF/classes} directory.
 *
 * @author Phillip Webb
 * @author Andy Wilkinso
 * @since 1.0.0n
 */
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) {
		if (entry.isDirectory()) {
			return entry.getName().equals(BOOT_INF_CLASSES);
		}
        // entry.getName()获取的是jar文件夹中的相对路径
		return entry.getName().startsWith(BOOT_INF_LIB);
	}

	public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
	}

}

Archive.Entry可以被认为是JAR文件中的资源

其实重点在于JarLauncher的main方法,它是什么时候被调用的。

在我们调用java -jar executable***.jar 命令时,/META-INF 中的资源文件的MAIN-CLASS(例如:

Main-Class: org.springframework.boot.loader.JarLauncher)对应的类将调用自身的main(String[] args);方法。

public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
}

实际上JarLauncher是调用了父类的父类的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 {
		JarFile.registerUrlProtocolHandler(); // URL协议的扩展
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
		launch(args, getMainClass(), classLoader);
	}

JarFile.registerUrlProtocolHandler();

对于URL协议的扩展,不太了解,但是我们可以看下JarFile.registerUrlProtocolHandler()方法

	public static void registerUrlProtocolHandler() {
		String handlers = System.getProperty(PROTOCOL_HANDLER, "");
		System.setProperty(PROTOCOL_HANDLER,
				("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
		resetCachedUrlHandlers();
	}

搜寻下resetCachedUrlHandlers();方法:

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

继续往下:

public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) {
    // private static Object streamHandlerLock = new Object();
        synchronized (streamHandlerLock) {
            // static URLStreamHandlerFactory factory;
            if (factory != null) {
                throw new Error("factory already defined");
            }
            // SecurityManager是一个允许应用程序实现安全策略的类。
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkSetFactory();
            }
            handlers.clear();
            factory = fac;
        }
    }

这是synchronized来实现了线程安全。

Launcher.createClassLoader(getClassPathArchives());

创建一个类加载器

// Launcher
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
		List<URL> urls = new ArrayList<>(archives.size());
		for (Archive archive : archives) {
			urls.add(archive.getUrl());
		}
		return createClassLoader(urls.toArray(new URL[0]));
	}

ExecutableArchiveLauncher.getClassPathArchiives()

	// ExecutableArchiveLauncher
	@Override
	protected List<Archive> getClassPathArchives() throws Exception {
		List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
		postProcessClassPathArchives(archives);
		return archives;
	}

在这里首先看下org.springframework.boot.loader.ExecutableArchiveLauncher类中的this.archive

	private final Archive archive;

	public ExecutableArchiveLauncher() {
		try {
			this.archive = createArchive();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}

archive引用的对象是通过Launcher#createArchive()方法来创建的。

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));
	}

Launcher.launch(args, getMainClass(), classLoader);

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
        // 为当前线程设置一个ClassLoader
		Thread.currentThread().setContextClassLoader(classLoader);
		createMainMethodRunner(mainClass, args, classLoader).run();
	}

在给定Archive File 和完全配置的ClassLoader情况下启动应用程序。

该方法的实际执行者为createMainMethodRunner(mainClass, args, classLoader).run();

protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
		return new MainMethodRunner(mainClass, args);
	}

再去看MainMethodRunner这个类

public class MainMethodRunner {
	private final String mainClassName;
	private final String[] args;
	/**
	 * Create a new {@link MainMethodRunner} instance.
	 * @param mainClass the main class
	 * @param args incoming arguments
	 */
	public MainMethodRunner(String mainClass, String[] args) {
		this.mainClassName = mainClass;
		this.args = (args != null) ? args.clone() : null;
	}

	public void run() throws Exception {
		Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		mainMethod.invoke(null, new Object[] { this.args });
	}
}

总结:

其实到这里就是很简单,JarLauncher就是对我们的jar包进行解析,先创建一个ClassLoader,然后MainMethodRunner这个工具类,调用其run()方法,使用创建好的ClassLoader类加载器加载我们Spring Boot程序中的引导类,然后获取其main方法,调用main()方法使之运行。这就是为什么一个Spring Boot程序为什么可以只写一个默认的main()方法就可以运行了.

@SpringBootApplication
public class FirstAppByGuiApplication {

    public static void main(String[] args) {
        SpringApplication.run(FirstAppByGuiApplication.class, args);
    }

}

至于Spring Boot引导启动类的启动流程,在此处先不做深入了解。

关于ClassLoader的创建以及其他原理,会在后续学习中将其理解。

ExecutableArchiveLauncher#getMainClass()

这里我们还需要啰嗦的是ExecutableArchiveLauncher#getMainClass()方法,因为该方法的返回值会被作为MainMethodRunner的构造函数的入参初始化一个MainMethodRunner对象。

这是LaunchergetMainClass()方法,是一个空方法。

protected abstract String getMainClass() throws Exception;

我们看下子类ExecutableArchiveLauncher中的实现

@Override
	protected 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;
	}
Manifest-Version: 1.0
Implementation-Title: first-app-by-gui
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: thinkinginspringboot.firstappbygui.FirstAppByGuiApplicati
 on
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.7.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

其实本质很简单,就是到META-INF下的资源文件中获取Start-Class: thinkinginspringboot.firstappbygui.FirstAppByGuiApplicati on对应的启动类,也就是FirstAppByGuiApplication

总结

​ 通过以上讨论,不难发现,Spring Boot为了逃离Tomcat等Servlet容器,直接将应用通过spring-boot-maven-plugin插件打包成一个可执行war/jar包,然后java -jar执行的时候,是调用的JarLauncher/WarLauncher来引导启动的。