立足于核心技术的平台研发

spring boot学习笔记——spring boot可执行jar的理解

spring boot应用通过mvn clean package等打包生成可执行jar在target路径下, 该路径下还包括与jar同名的.original文件是应用本地资源 (classes编译后的资源文件等),不含第三方依赖。

jar文件又称Fat Jar,采用zip格式压缩。

Fat jar的目录结构:

BOOT-INF/classes目录存放应用编译后的class文件;

BOOT-INF/lib目录存放应用依赖的JAR包;

META-INF/目录存放应用相关的元信息,如MANIFEST.MF文件;

org/目录存放Spring Boot相关的class文件。

该jar可以用java -jar执行,属于标准的jar,遵循规范,检查主类,查看META-INF/MANIFEST.MF:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: zhilecloud
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.zhiletu.zhilecloud.ZhilecloudApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.2.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

可以看出主类是springboot的JarLauncher,而应用引导类定义在Start-Class属性中。

如果打包格式是WAR包的话,可以看到主类是WarLauncher。

这说明在springboot中不同格式的打包方式,对应不同的启动主类。

那么可执行jar启动的原理是怎样的呢?

首先看JarLauncher的源码:

public class JarLauncher extends ExecutableArchiveLauncher {

   private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";

   static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
      if (entry.isDirectory()) {
         return entry.getName().equals("BOOT-INF/classes/");
      }
      return entry.getName().startsWith("BOOT-INF/lib/");
   };

   public JarLauncher() {
   }

   protected JarLauncher(Archive archive) {
      super(archive);
   }


   @Override
   protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
      // Only needed for exploded archives, regular ones already have a defined order
      if (archive instanceof 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(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
      return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
   }

   @Override
   protected boolean isPostProcessingClassPathArchives() {
      return false;
   }

   @Override
   protected boolean isSearchCandidate(Archive.Entry entry) {
      return entry.getName().startsWith("BOOT-INF/");
   }

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

}

可以看出BOOT-INF/classes/和BOOT-INF/lib/路径用于判定文件是否在两个路径下,并且分别对应了普通文件和jar文件。

Archive.Entry可以表示文件或目录,Archive接口存在两种实现:JarFileArchive和ExplodedArchive。

public class JarFileArchive implements Archive {
…此处省略…
   /**
    * {@link Archive.Entry} implementation backed by a {@link JarEntry}.
    */
   private static class JarFileEntry implements Entry {

      private final JarEntry jarEntry;

      JarFileEntry(JarEntry jarEntry) {
         this.jarEntry = jarEntry;
      }

      JarEntry getJarEntry() {
         return this.jarEntry;
      }

      @Override
      public boolean isDirectory() {
         return this.jarEntry.isDirectory();
      }

      @Override
      public String getName() {
         return this.jarEntry.getName();
      }

   }

}
public class ExplodedArchive implements Archive {
…此处省略…
/**
 * {@link Entry} backed by a File.
 */
private static class FileEntry implements Entry {

   private final String name;

   private final File file;

   private final URL url;

   FileEntry(String name, File file, URL url) {
      this.name = name;
      this.file = file;
      this.url = url;
   }

   File getFile() {
      return this.file;
   }

   @Override
   public boolean isDirectory() {
      return this.file.isDirectory();
   }

   @Override
   public String getName() {
      return this.name;
   }

   URL getUrl() {
      return this.url;
   }

}
}

由上面的实现,可知JarLauncher支持Jar和文件系统两种方式启动。

JarLauncher作为引导类,当执行java -jar时,/META-INF/资源清单的Main-Class属性将调用其main(String[])方法,
实际上调用的是

JarLauncher#launch(args)方法,该方法是继承自基类org.springframework.boot.loader.Launcher,
他们的继承关系:

org.springframework.boot.loader.Launcher

org.springframework.boot.loader.ExecutableArchiveLauncher

org.springframework.boot.loader.Launcher

下面分析Launcher#launch方法:

/**
 * 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 {
   if (!isExploded()) {
      JarFile.registerUrlProtocolHandler();
   }
   ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
   String jarMode = System.getProperty("jarmode");
   String launchClass = (jarMode != null && !jarMode.isEmpty()) ?
 JAR_MODE_LAUNCHER : getMainClass();
   launch(args, launchClass, classLoader);
}

由上面的launch方法可以看出,springboot应用的启动主要经过了三步(寻找主类属于常规操作):

第一步:JarFile.registerUrlProtocolHandler();
第二步:ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());

第三步:launch(args, launchClass, classLoader);

首先分析第一步JarFile.registerUrlProtocolHandler();,其具体实现如下:

public class JarFile extends AbstractJarFile implements Iterable<java.util.jar.JarEntry> {
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";

private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
/**
 * Register a {@literal 'java.protocol.handler.pkgs'} property so that a
 * {@link URLStreamHandler} will be located to deal with jar URLs.
 */
public static void registerUrlProtocolHandler() {
   String handlers = System.getProperty(PROTOCOL_HANDLER, "");
   System.setProperty(PROTOCOL_HANDLER,
         ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
   resetCachedUrlHandlers();
}

/**
 * Reset any cached handlers just in case a jar protocol has already been used. We
 * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
 * should have no effect other than clearing the handlers cache.
 */
private static void resetCachedUrlHandlers() {
   try {
      URL.setURLStreamHandle  rFactory(null);
   }
   catch (Error ex) {
      // Ignore
   }
}

}

上面这个方法做的事情是给java内建系统参数java.protocol.handler.pkgs追加值,
也就是把springboot的org.springframework.boot.loader包给追加到该参数上了,

这么做的目的是利用java.net.URLStreamHandler的扩展机制,
通过这种扩展机制实现的效果是springboot的handler覆盖了java内建的handler实现类。

具体来讲,覆盖的实现首先得益于java内建的可扩展机制,
也就是java通过统一实现和规定处理类的所在包路径和类名称规范管理了针对多种协议
(file、http、jar、ftp、https)的处理类。

如果要覆盖内建的处理类,需要修改内建参数java.protocol.handler.pkgs指定的处理类包路径。
如果没有重新指定处理类包路径到该参数,java就会通过

固定的get方法取回默认值所代表的处理类包路径,并把该默认包路径下的相关协议的handler作为处理类来调起执行。

这里的get方法是指getURLStreamHandler(String protocol),关于这个框架还有几点要明确:

1.内建协议处理程序包路径参数:java.protocol.handler.pkgs,可以通过System.getProperties()获取,
其设置的值是处理类包路径,可以设置多个,以|分隔,java。

2.内建处理类默认包路径:sun.net.www.protocol,无论是否设置了java.protocol.handler.pkgs
对应的新的处理类包路径,该默认值都会追加到上面的内建参数上。

3.URL关联协议(Protocol):JDK默认支持的协议,文件(file)、HTTP、HTTPS、JAR等。

4.协议处理类:JDK内建了对以上协议的事项,这些实现类存放在默认包sun.net.www.protocol下,
并且类名必须为Handler,类全名遵循固定模式sun.net.www.protocol.${protocol}.Handler,
其中${protocol}表示协议名,java NET底层对任何资源的处理遵循URL方式,URL的协议有确定的封装和解析逻辑,
为了支持这种特性,所有协议处理类必须继承URLStreamHandler类。

比如对于JAR文件,java内建的协议处理类是:sun.net.www.protocol.jar.Handler。
下面展示一下getURLStreamHandler的源码,然后研究一下ClassLoader的创建,也就是第二步:
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());。

* @author  James Gosling
 * @since JDK1.0
 */
public final class URL implements java.io.Serializable {
static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol";
static final long serialVersionUID = -7627629688361524110L;

/**
 * The property which specifies the package prefix list to be scanned
 * for protocol handlers.  The value of this property (if any) should
 * be a vertical bar delimited list of package names to search through
 * for a protocol handler to load.  The policy of this class is that
 * all protocol handlers will be in a class called <protocolname>.Handler,
 * and each package in the list is examined in turn for a matching
 * handler.  If none are found (or the property is not specified), the
 * default package prefix, sun.net.www.protocol, is used.  The search
 * proceeds from the first package in the list to the last and stops
 * when a match is found.
 */
private static final String protocolPathProp = "java.protocol.handler.pkgs";

/**
 * The protocol to use (ftp, http, nntp, ... etc.) .
 * @serial
 */
private String protocol;

/**
 * Returns the Stream Handler.
 * @param protocol the protocol to use
 */
static URLStreamHandler getURLStreamHandler(String protocol) {

    URLStreamHandler handler = handlers.get(protocol);
    if (handler == null) {

        boolean checkedWithFactory = false;

        // Use the factory (if any)
        if (factory != null) {
            handler = factory.createURLStreamHandler(protocol);
            checkedWithFactory = true;
        }

        // Try java protocol handler
        if (handler == null) {
            String packagePrefixList = null;

            packagePrefixList
                = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                    protocolPathProp,""));
            if (packagePrefixList != "") {
                packagePrefixList += "|";
            }

            // REMIND: decide whether to allow the "null" class prefix
            // or not.
            packagePrefixList += "sun.net.www.protocol";

            StringTokenizer packagePrefixIter =
                new StringTokenizer(packagePrefixList, "|");

            while (handler == null &&
                   packagePrefixIter.hasMoreTokens()) {

                String packagePrefix =
                  packagePrefixIter.nextToken().trim();
                try {
                    String clsName = packagePrefix + "." + protocol +
                      ".Handler";
                    Class<?> cls = null;
                    try {
                        cls = Class.forName(clsName);
                    } catch (ClassNotFoundException e) {
                        ClassLoader cl = ClassLoader.getSystemClassLoader();
                        if (cl != null) {
                            cls = cl.loadClass(clsName);
                        }
                    }
                    if (cls != null) {
                        handler  =
                          (URLStreamHandler)cls.newInstance();
                    }
                } catch (Exception e) {
                    // any number of exceptions can get thrown here
                }
            }
        }

        synchronized (streamHandlerLock) {

            URLStreamHandler handler2 = null;

            // Check again with hashtable just in case another
            // thread created a handler since we last checked
            handler2 = handlers.get(protocol);

            if (handler2 != null) {
                return handler2;
            }

            // Check with factory if another thread set a
            // factory since our last check
            if (!checkedWithFactory && factory != null) {
                handler2 = factory.createURLStreamHandler(protocol);
            }

            if (handler2 != null) {
                // The handler from the factory must be given more
                // importance. Discard the default handler that
                // this thread created.
                handler = handler2;
            }

            // Insert this handler into the hashtable
            if (handler != null) {
                handlers.put(protocol, handler);
            }

        }
    }

    return handler;

}

}

OK,书归正传,分析ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
这句代码,这句createClassLoader方法以getClassPathArchivesIterator方法的返回值为入参,
分析一下其源码:

public abstract class Launcher {
/**
 * Returns the archives that will be used to construct the class path.
 * @return the class path archives
 * @throws Exception if the class path archives cannot be obtained
 * @since 2.3.0
 */
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
   return getClassPathArchives().iterator();
}
}
该方法均为抽象方法,具体实现由子类ExecutableArchiveLauncher提供:
public abstract class ExecutableArchiveLauncher extends Launcher {
@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
   Archive.EntryFilter searchFilter = this::isSearchCandidate;
   Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
         (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
   if (isPostProcessingClassPathArchives()) {
      archives = applyClassPathArchivePostProcessing(archives);
   }
   return archives;
}
}
由于篇幅原因,对此方法不做详细探究。本方法主要是确定要启动的springboot应用
是否在BOOT-INF/classes/下或者BOOT-INF/lib/下,
然后根据关于archive的相关实现创建classloader实例,
有了这个classloader,就可以进入第三步的执行:
public abstract class Launcher {
/**
 * Launch the application given the archive file and a fully configured classloader.
 * @param args the incoming arguments
 * @param launchClass the launch class to run
 * @param classLoader the classloader
 * @throws Exception if the launch fails
 */
protected void launch(String[] args, String launchClass, ClassLoader classLoader)
 throws Exception {
   Thread.currentThread().setContextClassLoader(classLoader);
   createMainMethodRunner(launchClass, args, classLoader).run();
}

/**
 * Create the {@code MainMethodRunner} used to launch the application.
 * @param mainClass the main class
 * @param args the incoming arguments
 * @param classLoader the classloader
 * @return the main method runner
 */
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;

   /**
    * 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 = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
      Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
      mainMethod.setAccessible(true);
      mainMethod.invoke(null, new Object[] { this.args });
   }
}
最终执行的是上面这个类的run方法,意思是找到主类的main方法执行,主类是哪个类呢?
在Launcher类中传递了mainClass,并且有对应的抽象获取方法:
/**
 * Returns the main class that should be launched.
 * @return the name of the main class
 * @throws Exception if the main class cannot be obtained
 */
protected abstract String getMainClass() throws Exception;

该抽象方法的实现:

public abstract class ExecutableArchiveLauncher extends Launcher {

   private static final String START_CLASS_ATTRIBUTE = "Start-Class";

@Override
protected String getMainClass() throws Exception {
   Manifest manifest = this.archive.getManifest();
   String mainClass = null;
   if (manifest != null) {
      mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
   }
   if (mainClass == null) {
      throw new IllegalStateException("No 'Start-Class' manifest entry specified in "
 + this);
   }
   return mainClass;
}
}
至此,可以明确主类的名称是从/META-INF/MANIFEST.MF资源中的Start-Class属性取得的, 这就是spring boot应用中声明的启动类。并且子类并没有覆写该方法, 说明jar和war打包方式均使用该属性读取spring boot的启动类。
JarLauncher实际上就是同进程内调用Start-Class类的main方法,并且在启动前准备好Class Path。
而WarLauncher大同小异,不同之处在于:classpath为WEB-INF/classes/和WEB-INF/lib/,
并且新增了WEB-INF/lib-provided作为spring boot WarLauncher启动的依赖,这样传统servlet容器
将不把新增的路径作为classpath执行,实现了兼容。

上一篇
下一篇

相关文章

在线留言

*

*

在线客服
在线客服 X

售前: 点击这里给我发消息
售后: 点击这里给我发消息

电话:18660802028