Java应用启动mainclass与Spring boot启动的原理

前言

最近把旧项目的框架更换为Spring boot,打包方式更为jar启动,其实以前也不是war部署,而是通过main方式启动的,这里看看原理。

1. main方式启动的原理

java 命令启动可以-jar也可以直接带main class,那么直接启动带main方法的类即可启动应用

            <!-- Assembly plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <id>bin</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <descriptors>
                                <descriptor>src/main/assembly/assembly.xml</descriptor>
                            </descriptors>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

maven有插件可以打包,比如上面的插件,通过一个配置文件如下:

直接配置好class jar与lib复制的对应关系,打成一个zip或者其他,最好封装好start脚本

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1 http://maven.apache.org/xsd/assembly-1.1.1.xsd">
  <id>bin</id>
  <includeBaseDirectory>false</includeBaseDirectory>
  <formats>
    <format>dir</format>
    <format>zip</format>
  </formats>

  <files>
    <file>
      <source>../xxx.jar</source>
      <destName>../lib/arthas-spy.jar</destName>
    </file>
    <file>
      <source>../xxx/xxx.class</source>
      <destName>xxx.class</destName>
    </file>
  </files>
  <fileSets>
    <fileSet>
      <directory>../xxx</directory>
    </fileSet>
    <fileSet>
      <directory>../lib</directory>
    </fileSet>
  </fileSets>
</assembly>

打包完成是一个main类一个lib目录,然后java -cp ./lib xxx.class

通过java指令可以看出 cp即classpath:表示这个jar或者zip,目录是classpath的意思

等同于Xbootclasspath,不过Xbootclasspath更细分:/a表示在classpath末尾追加,兼容性好,建议使用;/p表示在classpath前,可能会冲突。

2. java -jar

官方已经定义了java的程序启动class或者-jar,那么-jar是怎么启动呢

其实是META-INF.MANIFEST.MF这个文件定义的

Main-Class

以Spring boot为例,看看Spring boot打包的插件,定义了mainClass,如下

    <build>
        <finalName>boot-demo</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.1.RELEASE</version>
                <configuration>
                    <mainClass>com.feng.boot.BootMain</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

那么打成的jar呢,居然替换了,不过也是,毕竟没有脚本来-cp lib,classes这些目录,而是一个jar包括所有的启动相关的类 配置等,Spring boot相当于使用自定义代码把这些代码的启动参数模拟出来了。

3. Spring boot启动原理

3.1 加载classes与lib

既然有Main-Class,那么java就会去执行org.springframework.boot.loader.JarLauncher

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

因为new会先new parent的构造,会初始化一些路径,可以看出先找到当前类的URI,进而读取文件,这个很重要,关乎于其他classes lib 等的加载。

    public ExecutableArchiveLauncher() {
		try {
			this.archive = createArchive();
			this.classPathIndex = getClassPathIndex(this.archive);
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
	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);
		}
        //这里非常关键,其实获取jar里面的文件就是通过这里实现的
		return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
	}

jar包:JarFileArchive    这个是主要途径

解压缩的目录:ExplodedArchive

使用JDK的ZIpFile读取,JDK1.7开始的,jar即zip文件

Opens a new <code>ZipFile</code> to read from the specified <code>File</code> object in the specified mode.


    /**
     * Opens a new <code>ZipFile</code> to read from the specified
     * <code>File</code> object in the specified mode.  The mode argument
     * must be either <tt>OPEN_READ</tt> or <tt>OPEN_READ | OPEN_DELETE</tt>.
     *
     * <p>First, if there is a security manager, its <code>checkRead</code>
     * method is called with the <code>name</code> argument as its argument to
     * ensure the read is allowed.
     *
     * @param file the ZIP file to be opened for reading
     * @param mode the mode in which the file is to be opened
     * @param charset
     *        the {@linkplain java.nio.charset.Charset charset} to
     *        be used to decode the ZIP entry name and comment that are not
     *        encoded by using UTF-8 encoding (indicated by entry's general
     *        purpose flag).
     *
     * @throws ZipException if a ZIP format error has occurred
     * @throws IOException if an I/O error has occurred
     *
     * @throws SecurityException
     *         if a security manager exists and its <code>checkRead</code>
     *         method doesn't allow read access to the file,or its
     *         <code>checkDelete</code> method doesn't allow deleting the
     *         file when the <tt>OPEN_DELETE</tt> flag is set
     *
     * @throws IllegalArgumentException if the <tt>mode</tt> argument is invalid
     *
     * @see SecurityManager#checkRead(java.lang.String)
     *
     * @since 1.7
     */
    public ZipFile(File file, int mode, Charset charset) throws IOException
    {
        if (((mode & OPEN_READ) == 0) ||
            ((mode & ~(OPEN_READ | OPEN_DELETE)) != 0)) {
            throw new IllegalArgumentException("Illegal mode: 0x"+
                                               Integer.toHexString(mode));
        }
        String name = file.getPath();
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkRead(name);
            if ((mode & OPEN_DELETE) != 0) {
                sm.checkDelete(name);
            }
        }
        if (charset == null)
            throw new NullPointerException("charset is null");
        this.zc = ZipCoder.get(charset);
        long t0 = System.nanoTime();
        jzfile = open(name, mode, file.lastModified(), usemmap);
        sun.misc.PerfCounter.getZipFileOpenTime().addElapsedTimeFrom(t0);
        sun.misc.PerfCounter.getZipFileCount().increment();
        this.name = name;
        this.total = getTotal(jzfile);
        this.locsig = startsWithLOC(jzfile);
    }

可以看见文件描述符 

 

org.springframework.boot.loader.jar.JarFile在初始化的时候,就把文件的offset定义好了,Spring boot取文件就是通过offset取文件的,速度极快

	private JarFile(JarFile parent, RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data,
			JarEntryFilter filter, JarFileType type, Supplier<Manifest> manifestSupplier) throws IOException {
		super(rootFile.getFile());
		super.close();
		this.parent = parent;
		this.rootFile = rootFile;
		this.pathFromRoot = pathFromRoot;
		CentralDirectoryParser parser = new CentralDirectoryParser();
		this.entries = parser.addVisitor(new JarFileEntries(this, filter));
		this.type = type;
		parser.addVisitor(centralDirectoryVisitor());
		try {
            //最关键的是这里
			this.data = parser.parse(data, filter == null);
		}
		catch (RuntimeException ex) {
			close();
			throw ex;
		}
		this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> {
			try (InputStream inputStream = getInputStream(MANIFEST_NAME)) {
				if (inputStream == null) {
					return null;
				}
				return new Manifest(inputStream);
			}
			catch (IOException ex) {
				throw new RuntimeException(ex);
			}
		};
	}

 跟踪parse

	RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes) throws IOException {
		CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
		if (skipPrefixBytes) {
			data = getArchiveData(endRecord, data);
		}
		RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data);
		visitStart(endRecord, centralDirectoryData);
		parseEntries(endRecord, centralDirectoryData);
		visitEnd();
		return data;
	}

 获取文件总的offset length,下一步拆分文件的offset position

visitStart,初始化centralDirectory hashCode、Offsets  positions 

	public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
		int maxSize = endRecord.getNumberOfRecords();
		this.centralDirectoryData = centralDirectoryData;
		this.hashCodes = new int[maxSize];
		this.centralDirectoryOffsets = new int[maxSize];
		this.positions = new int[maxSize];
	}

通过文件头信息分割,可见Spring boot的插件开发者对zip文件的读写原理研究得非常厉害。

	private void parseEntries(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData)
			throws IOException {
		byte[] bytes = centralDirectoryData.read(0, centralDirectoryData.getSize());
		CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
		int dataOffset = 0;
		for (int i = 0; i < endRecord.getNumberOfRecords(); i++) {
			fileHeader.load(bytes, dataOffset, null, 0, null);
			visitFileHeader(dataOffset, fileHeader);
			dataOffset += CENTRAL_DIRECTORY_HEADER_BASE_SIZE + fileHeader.getName().length()
					+ fileHeader.getComment().length() + fileHeader.getExtra().length;
		}
	}
/**
 * A ZIP File "Central directory file header record" (CDFH).
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @author Dmytro Nosan
 * @see <a href="https://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
 */

final class CentralDirectoryFileHeader implements FileHeader {

解析这段代码非常复杂,笔者看不懂

    static long littleEndianValue(byte[] bytes, int offset, int length) {
		long value = 0;
		for (int i = length - 1; i >= 0; i--) {
			value = ((value << 8) | (bytes[offset + i] & 0xFF));
		}
		return value;
	}

    void load(byte[] data, int dataOffset, RandomAccessData variableData, int variableOffset, JarEntryFilter filter)
			throws IOException {
		// Load fixed part
		this.header = data;
		this.headerOffset = dataOffset;
		long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2);
		long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2);
		long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2);
		this.localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4);
		// Load variable part
		dataOffset += 46;
		if (variableData != null) {
			data = variableData.read(variableOffset + 46, nameLength + extraLength + commentLength);
			dataOffset = 0;
		}
		this.name = new AsciiBytes(data, dataOffset, (int) nameLength);
		if (filter != null) {
			this.name = filter.apply(this.name);
		}
		this.extra = NO_EXTRA;
		this.comment = NO_COMMENT;
		if (extraLength > 0) {
			this.extra = new byte[(int) extraLength];
			System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0, this.extra.length);
		}
		if (commentLength > 0) {
			this.comment = new AsciiBytes(data, (int) (dataOffset + nameLength + extraLength), (int) commentLength);
		}
	}

关键是visitFileHeader,精准定位文件的hashCode、offset与position

	private void add(AsciiBytes name, int dataOffset) {
		this.hashCodes[this.size] = name.hashCode();
		this.centralDirectoryOffsets[this.size] = dataOffset;
		this.positions[this.size] = this.size;
		this.size++;
	}

然后处理end

	public void visitEnd() {
		sort(0, this.size - 1);
		int[] positions = this.positions;
		this.positions = new int[positions.length];
		for (int i = 0; i < this.size; i++) {
			this.positions[positions[i]] = i;
		}
	}

 读取文件到这里结束,这里就把jar(zip文件)里的文件hashcode、offset与position详细的获取完成,方便后续处理。

那么再看看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);
	}

关键是,这里获取到各个需要加载的目录了

getClassPathArchivesIterator()
	protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
        //函数式编程
		Archive.EntryFilter searchFilter = this::isSearchCandidate;
        //2个过滤器
		Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
				(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
        //处理classpath的其他jar或者类 配置
		if (isPostProcessingClassPathArchives()) {
			archives = applyClassPathArchivePostProcessing(archives);
		}
		return archives;
	}

这里isSearchCandidate,是一个搜索过滤器,条件是:BOOT-INF/开头

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

关键代码,其中一个是前面的,筛选哪些是要加载的。

this.archive.getNestedArchives(searchFilter,
                (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));

isNestedArchive && !isEntryIndexed

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

    protected boolean isNestedArchive(Archive.Entry entry) {
		return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
	}

判断是否目录,前缀限制

	private boolean isEntryIndexed(Archive.Entry entry) {
		if (this.classPathIndex != null) {
			return this.classPathIndex.containsEntry(entry.getName());
		}
		return false;
	}

classpath索引,比如类或者jar必须包括xxx,默认情况是没有限制的。

进一步跟踪

    public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException {
		return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter);
	}
    private class NestedArchiveIterator extends AbstractIterator<Archive> {

		NestedArchiveIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
			super(iterator, searchFilter, includeFilter);
		}

关键迭代器,自定义的

	private abstract static class AbstractIterator<T> implements Iterator<T> {

		private final Iterator<JarEntry> iterator;

		private final EntryFilter searchFilter;

		private final EntryFilter includeFilter;

		private Entry current;

		AbstractIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
			this.iterator = iterator;
			this.searchFilter = searchFilter;
			this.includeFilter = includeFilter;
			this.current = poll();
		}

		@Override
		public boolean hasNext() {
			return this.current != null;
		}

		@Override
		public T next() {
            //这个很有意思,直接把当前的'处理'返回
			T result = adapt(this.current);
            //然后读取一个
			this.current = poll();
			return result;
		}

		private Entry poll() {
			while (this.iterator.hasNext()) {
                //用其他迭代器,然后过滤
				JarFileEntry candidate = new JarFileEntry(this.iterator.next());
                //条件处理,就是上面说的filter
				if ((this.searchFilter == null || this.searchFilter.matches(candidate))
						&& (this.includeFilter == null || this.includeFilter.matches(candidate))) {
					return candidate;
				}
			}
			return null;
		}

		protected abstract T adapt(Entry entry);

	}

看boot的jar,确实是这样的,当读取完红框的内容就迭代结束 

3.2 创建classloader载入

这样就可以把所有Spring boot需要的文件读取到,加载进classloader

	protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
		List<URL> urls = new ArrayList<>(guessClassPathSize());
		while (archives.hasNext()) {
			urls.add(archives.next().getUrl());
		}
		if (this.classPathIndex != null) {
			urls.addAll(this.classPathIndex.getUrls());
		}
		return createClassLoader(urls.toArray(new URL[0]));
	}
public class LaunchedURLClassLoader extends URLClassLoader
	protected ClassLoader createClassLoader(URL[] urls) throws Exception {
		return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
	}

通过META-INF.MANIFEST.MF获取到,Spring包装的main的类

Start-Class: com.feng.boot.BootMain
	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;
	}

然后,jarmode,就是运行Spring的定义jar

		private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
        String jarMode = System.getProperty("jarmode");
		String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
		launch(args, launchClass, classLoader);

jarmode,很熟悉的代码。Spring boot的自动装配JarMode

    public static void main(String[] args) {
		String mode = System.getProperty("jarmode");
		List<JarMode> candidates = SpringFactoriesLoader.loadFactories(JarMode.class,
				ClassUtils.getDefaultClassLoader());
		for (JarMode candidate : candidates) {
			if (candidate.accepts(mode)) {
				candidate.run(mode, args);
				return;
			}
		}
		System.err.println("Unsupported jarmode '" + mode + "'");
		if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) {
			System.exit(1);
		}
	}

3.3 launch

回到launch,设置当前线程classloader为LaunchedURLClassLoader,毕竟很多中间件会从线程取classloader

	protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
		Thread.currentThread().setContextClassLoader(classLoader);
		createMainMethodRunner(launchClass, args, classLoader).run();
	}

最后一步,反射main类的main方法。

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

至此Spring boot启动的过程就完结了。

 

总结

其实java启动程序,官方定义了2种方式main class或者-jar,Spring boot使用-jar模式,自定义了类、配置、jar的路径,并通过loader的jar加载,然后反射我们写的main类的main方法。其实我们也可以自己写个脚本,然后把其他文件打成jar放在lib,配置放在config,然后通过main类指定classpath启动,原理一样。不过Spring boot对文件的解析确实厉害。

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值