关于express:SpringBoot项目jarwar包启动解析

1次阅读

共计 10633 个字符,预计需要花费 27 分钟才能阅读完成。

一、jar 包和 war 包的区别
1.1 war 包

war 包是 Java Web 应用程序的一种打包形式合乎 Servlet 规范,它是 Web Archive 的缩写,次要用于存储 Web 应用程序相干的文件,包含 Java 类文件、JSP、HTML、CSS、JavaScript、图片等资源文件。
war 包须要部署到 web 服务器中(Tomcat、Apache、IIS)

1.2 jar 包

jar 包是类的归档文件,次要用于存储 Java 类文件和相干资源文件。它通常被用于封装 Java 应用程序或 Java 类库,不便程序的部署和公布
jar 包能够被 JVM 间接加载和运行。

1.3 次要区别:

jar 包次要用于存储 Java 类文件和相干资源文件,而 war 包次要用于存储 Web 应用程序相干的文件。
jar 包能够被 JVM 间接加载和运行,而 war 包须要被 Web 服务器加载和运行。
jar 包通常用于封装 Java 应用程序或 Java 类库,而 war 包用于封装 Java Web 应用程序。

二、SpringBoot 应用 war 包启动
war 包启动:须要先启动内部的 Web 服务器,实现 Servlet3.0 标准中疏导利用启动类,而后将 war 包放入 Web 服务器下,Web 服务器通过回调疏导利用启动类办法启动利用。
2.1 Servlet3.0 标准中疏导利用启动的阐明

在 Servlet 容器(Tomcat、Jetty 等)启动利用时,会扫描利用 jar 包中 ServletContainerInitializer 的实现类。
框架必须在 jar 包的 META-INF/services 的文件夹中提供一个名为 javax.servlet.ServletContainerInitializer 的文件,文件内容要写明 ServletContainerInitializer 的实现类的全限定名。
这个 ServletContainerInitializer 是一个接口,实现它的类必须实现一个办法:onStartUp
能够在这个 ServletContainerInitializer 的实现类上标注 @HandlesTypes 注解,在利用启动的时候自行加载一些附加的类,这些类会以字节码的汇合模式传入 onStartup 办法的第一个参数中。

public interface ServletContainerInitializer {

void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;

}
复制代码
2.2 SpringBootServletInitializer 的作用和原理
Spirng 中 SpringServletContainerInitializer 实现了 Servlet 的标准

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
        throws ServletException {
    // SpringServletContainerInitializer 会加载所有的 WebApplicationInitializer 类型的一般实现类

    List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

    if (webAppInitializerClasses != null) {for (Class<?> waiClass : webAppInitializerClasses) {
            // 如果不是接口,不是抽象类
            if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                    WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                try {
                    // 创立该类的实例
                    initializers.add((WebApplicationInitializer) waiClass.newInstance());
                }
                catch (Throwable ex) {throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                }
            }
        }
    }

    if (initializers.isEmpty()) {servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
        return;
    }

    servletContext.log(initializers.size() + "Spring WebApplicationInitializers detected on classpath");
    AnnotationAwareOrderComparator.sort(initializers);
    // 启动 Web 利用 onStartup 办法
    for (WebApplicationInitializer initializer : initializers) {initializer.onStartup(servletContext);
    }
}

}
复制代码
@HandlesTypes 应用 BCEL 的 ClassParser 在字节码层面读取了 /WEB-INF/classes 和 jar 中 class 文件的超类名和实现的接口名,判断是否与记录的注解类名雷同,若雷同再通过 org.apache.catalina.util.Introspection 类加载为 Class 对象保存起来, 最初传入 onStartup 办法参数中
SpringServletContainerInitializer 类上标注了 @HandlesTypes(WebApplicationInitializer.class),所以会导入 WebApplicationInitializer 实现类
SpringBoot 中 SpringBootServletInitializer 是 WebApplicationInitializer 的抽象类, 实现了 onStartup 办法
@Override
public void onStartup(ServletContext servletContext) throws ServletException {

// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
// 创立 父 IOC 容器
WebApplicationContext rootAppContext = createRootApplicationContext(servletContext);
if (rootAppContext != null) {servletContext.addListener(new ContextLoaderListener(rootAppContext) {
        @Override
        public void contextInitialized(ServletContextEvent event) {// no-op because the application context is already initialized}
    });
}
else {this.logger.debug("No ContextLoaderListener registered, as" + "createRootApplicationContext() did not"
            + "return an application context");
}

}
复制代码
创立父容器
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {

// 应用 Builder 机制,后面也介绍过
SpringApplicationBuilder builder = createSpringApplicationBuilder();
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {this.logger.info("Root context already created (using as parent).");
    servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
    builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
// 设置 Initializer
builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
// 在这里设置了容器启动类:AnnotationConfigServletWebServerApplicationContext
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
//【疏导】多态进入子类(本人定义)的办法中
builder = configure(builder);
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
// builder.build(),创立 SpringApplication
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty()
        && AnnotationUtils.findAnnotation(getClass(), Configuration.class) != null) {application.addPrimarySources(Collections.singleton(getClass()));
}
Assert.state(!application.getAllSources().isEmpty(),
        "No SpringApplication sources have been defined. Either override the"
                + "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}
// 启动 SpringBoot 利用
return run(application);

}
复制代码
所以咱们只须要自定义类继承 SpringBootServletInitializer 并实现 configure 办法通知启动类所在的地位就能够实现 SpringBoot 自启动了
例如:
public class MyInitializer extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {

  //MySpringBootApplication 为 SpingBoot 启动类
  return application.sources(MySpringBootApplication.class);

}
}
复制代码
三、SpringBoot 应用 jar 包启动
依照 java 官网文档规定,java -jar 命令疏导的具体启动类必须配置在 MANIFEST.MF 中的 Main-class 属性中,该值代表应用程序执行入口类也就是蕴含 main 办法的类。
从 MANIFEST.MF 文件内容能够看到,Main-Class 这个属性定义了 org.springframework.boot.loader.JarLauncher,JarLauncher 就是对应 Jar 文件的启动器。而咱们我的项目的启动类 SpringBootDemoApplication 定义在 Start-Class 属性中,
JarLauncher 会将 BOOT-INF/classes 下的类文件和 BOOT-INF/lib 下依赖的 jar 退出到 classpath 下,而后调用 META-INF/MANIFEST.MF 文件 Start-Class 属性实现应用程序的启动。

对于 jar 官网规范阐明请移步

JAR File Specification
JAR (file format)

SpringBoot 的 jar 包,会有 3 个文件夹:

BOOT-INF:寄存本人编写并编译好的 .class 文件和动态资源文件、配置文件等
META-INF:有一个 MANIFEST.MF 的文件
org:spring-boot-loader 的一些 .class 文件

META-INF 上面的 MANIFEST.MF 文件,外面的内容如下:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: my-small-test
Implementation-Version: 1.0-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.small.test.SpringBootDemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.4.0
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
复制代码

在 Start-Class 中注明了 SpringBoot 的主启动类
在 Main-Class 中注明了一个类:JarLauncher

package org.springframework.boot.loader;

import java.io.IOException;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.springframework.boot.loader.archive.Archive;

public class JarLauncher extends ExecutableArchiveLauncher {
private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = “BOOT-INF/classpath.idx”;

static final Archive.EntryFilter NESTED_ARCHIVE_ENTRY_FILTER;

static {

NESTED_ARCHIVE_ENTRY_FILTER = (entry -> entry.isDirectory() ? entry.getName().equals("BOOT-INF/classes/") : entry.getName().startsWith("BOOT-INF/lib/"));

}

public JarLauncher() {}

protected JarLauncher(Archive archive) {

super(archive);

}

protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {

if (archive instanceof org.springframework.boot.loader.archive.ExplodedArchive) {String location = getClassPathIndexFileLocation(archive);
  return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
} 
return super.getClassPathIndex(archive);

}

private String getClassPathIndexFileLocation(Archive archive) throws IOException {

Manifest manifest = archive.getManifest();
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
String location = (attributes != null) ? attributes.getValue("Spring-Boot-Classpath-Index") : null;
return (location != null) ? location : "BOOT-INF/classpath.idx";

}

protected boolean isPostProcessingClassPathArchives() {

return false;

}

protected boolean isSearchCandidate(Archive.Entry entry) {

return entry.getName().startsWith("BOOT-INF/");

}

protected boolean isNestedArchive(Archive.Entry entry) {

return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);

}

public static void main(String[] args) throws Exception {

(new JarLauncher()).launch(args);

}
}
复制代码
父类 Launcher#launch
protected void launch(String[] args) throws Exception {

if (!isExploded())
//3.1 注册 URL 协定并革除利用缓存
JarFile.registerUrlProtocolHandler(); 
//3.2 设置类加载门路
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? "org.springframework.boot.loader.jarmode.JarModeLauncher" : getMainClass();
//3.3 执行 main 办法
launch(args, launchClass, classLoader);

}
复制代码
3.1 registerUrlProtocolHandler:注册 URL 协定并革除利用缓存
先设置以后零碎的一个变量 java.protocol.handler.pkgs,而这个变量的作用,是设置 URLStreamHandler 实现类的包门路。
之后要重置缓存,目标是革除之前启动的残留。
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";

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

// 重置任何缓存的处理程序,以防万一曾经应用了 jar 协定。

// 咱们通过尝试设置 null URLStreamHandlerFactory 来重置处理程序,除了革除处理程序缓存之外,它应该没有任何成果。

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

复制代码
3.2createClassLoader:设置类加载门路
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {

List<URL> urls = new ArrayList<>(50);
while (archives.hasNext())
  urls.add(((Archive)archives.next()).getUrl()); 
return createClassLoader(urls.<URL>toArray(new URL[0]));

}
复制代码
protected ClassLoader createClassLoader(URL[] urls) throws Exception {

return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());

}
复制代码
3.3 执行 main 办法
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {

Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(launchClass, args, classLoader).run();

}

复制代码
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {

return new MainMethodRunner(mainClass, args);

}
复制代码
package org.springframework.boot.loader;

import java.lang.reflect.Method;

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 = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
// 获取主启动类的 main 办法
Method mainMethod = mainClass.getDeclaredMethod("main", new Class[] {String[].class });
mainMethod.setAccessible(true);
// 执行 main 办法
mainMethod.invoke((Object)null, new Object[] { this.args});

}
}
复制代码
所以 SpringBoot 利用在开发期间只须要写 main 办法,疏导启动即可。

正文完
 0