栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

spring-boot-maven-plugin 2.0.0.RELEASE之前的版本打jar包classpath顺序错乱导致同包同名类覆盖失效

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

spring-boot-maven-plugin 2.0.0.RELEASE之前的版本打jar包classpath顺序错乱导致同包同名类覆盖失效

起因

Log4j出现了远程执行漏洞, 直接升级log4j版本不实现(启动会报错,新版的包结构可能有改变), 在github发现一个打补丁的方法, 就是用同名类覆盖JndiLookup类使其实例化报错.
在本地启用idea测试的时候非常顺利,包含Jndi地址的日志不会被解析而是直接打印出来. 于是便发包到服务器测试, 结果事与愿违, 漏洞还是能够触发.这确实不应该啊.

分析Jar包以及启动过程 Jar包内容

查看包的内容log4j-patch和log4j-core包都在, 所以可以初步确定是classpath顺序的问题. 查看包文件meta-INf/MANIFEST.MF(相当于jar包描述文件)

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: ***
Start-Class: ***.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 1.4.5.RELEASE
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_131
Main-Class: org.springframework.boot.loader.JarLauncher

可以发现它的启动类是org.springframework.boot.loader.JarLauncher而jar包中确实有这个类的字节码,

Jar包启动分析

通常,spring-boot-maven-plugin打的Jar包都是通过java -jar ***.jar 直接运行的,并没有直接添加classpath参数,所有可以猜测classpath是在运行jar包时指定的, 于是乎接下来就是debug了, 但是没有启动类org.springframework.boot.loader.JarLauncher源码啊, 这怎么好debug(其实没源码应该也能debug,只是比较麻烦), 想了想这是spring-boot-maven-plugin插件生成的jar包, spring-boot仓库应该是有源码的, 于是在github上一番通过包名一番搜索找到类非常相似的包类名(https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader), github上默认是main(master)分支代码都比较新,所以通过tag切换到我所使用的版本(1.4.5.RELEASE),发现结构几乎和jar包相同, 可以肯定这就是源码了.
接着又面向百度编程找如何debug jar包程序

java -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y -jar ***.jar

在Idea中Edit Configurations 添加Remote JVM DEBUG配置,并设置源码路径,debug端口
进行debug

org.springframework.boot.loader.JarLauncher入口

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

调用的是父类org.springframework.boot.loader.Launcher的launch(java.lang.String[])方法

protected void launch(String[] args) throws Exception {
	JarFile.registerUrlProtocolHandler();
	ClassLoader classLoader = createClassLoader(getClassPathArchives());
	launch(args, getMainClass(), classLoader);
}

org.springframework.boot.loader.JarLauncher的父类
org.springframework.boot.loader.ExecutableArchiveLauncher

@Override
protected List getClassPathArchives() throws Exception {
	List archives = new ArrayList(
			this.archive.getNestedArchives(new EntryFilter() {

				@Override
				public boolean matches(Entry entry) {
					return isNestedArchive(entry);
				}

			}));
	postProcessClassPathArchives(archives);
	return archives;
}

getClassPathArchives获取了所有jar依赖包信息.
其中this.archive是org.springframework.boot.loader.archive.JarFileArchive类实例, 参考org.springframework.boot.loader.ExecutableArchiveLauncher的父类org.springframework.boot.loader.Launcher中createArchive方法


package org.springframework.boot.loader;

import java.io.File;
import java.net.URI;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.boot.loader.jar.JarFile;


public abstract class Launcher {

	
	protected void launch(String[] args) throws Exception {
		JarFile.registerUrlProtocolHandler();
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
		launch(args, getMainClass(), classLoader);
	}

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

	
	protected ClassLoader createClassLoader(URL[] urls) throws Exception {
		return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
	}

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

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

	
	protected abstract String getMainClass() throws Exception;

	
	protected abstract List getClassPathArchives() throws Exception;

	protected final Archive createArchive() throws Exception {
		ProtectionDomain protectionDomain = getClass().getProtectionDomain();
		CodeSource codeSource = protectionDomain.getCodeSource();
		URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
		String path = (location == null ? null : location.getSchemeSpecificPart());
		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));
	}

}

createClassLoader方法用到了org.springframework.boot.loader.LaunchedURLClassLoader类,而这个类继承了java.net.URLClassLoader, 而org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)方法修改了当前线程的classLoader, 相当于指定了classpath, 接着便是反射调用我们真正的SpringBoot启动类了

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

通过JDK文档得知java.net.URLClassLoader加载类的顺序和构造参数urls顺序相关

public URLClassLoader(URL[] urls, ClassLoader parent) {
    super(parent);
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    this.acc = AccessController.getContext();
    ucp = new URLClassPath(urls, acc);
}

而这个urls顺序来自org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives方法得到的List的顺序

接下来看看org.springframework.boot.loader.archive.JarFileArchive#getNestedArchives方法

public List getNestedArchives(EntryFilter filter) throws IOException {
	List nestedArchives = new ArrayList();
	for (Entry entry : this) {
		if (filter.matches(entry)) {
			nestedArchives.add(getNestedArchive(entry));
		}
	}
	return Collections.unmodifiableList(nestedArchives);
}

它遍历了自己(它实现了java.lang.Iterable)

@Override
public Iterator iterator() {
	return new EntryIterator(this.jarFile.entries());
}

而这个jarFile就是我们这个jar程序包, 它是org.springframework.boot.loader.jar.JarFile类型, 也是java.util.jar.JarFile的子类

@Override
public Enumeration entries() {
	final Iterator iterator = this.entries.iterator();
	return new Enumeration() {

		@Override
		public boolean hasMoreElements() {
			return iterator.hasNext();
		}

		@Override
		public java.util.jar.JarEntry nextElement() {
			return iterator.next();
		}

	};
}

其中iterator()方法

@Override
public Iterator iterator() {
	return new EntryIterator();
}
private class EntryIterator implements Iterator {

	private int index = 0;

	@Override
	public boolean hasNext() {
		return this.index < JarFileEntries.this.size;
	}

	@Override
	public JarEntry next() {
		if (!hasNext()) {
			throw new NoSuchElementException();
		}
		int entryIndex = JarFileEntries.this.positions[this.index];
		this.index++;
		return getEntry(entryIndex, JarEntry.class, false);
	}

}
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
		RandomAccessData data, JarEntryFilter filter, JarFileType type)
				throws IOException {
	super(rootFile.getFile());
	this.rootFile = rootFile;
	this.pathFromRoot = pathFromRoot;
	CentralDirectoryParser parser = new CentralDirectoryParser();
	
	// 重点
	this.entries = parser.addVisitor(new JarFileEntries(this, filter));
	parser.addVisitor(centralDirectoryVisitor());
	this.data = parser.parse(data, filter == null);
	
	this.type = type;
}

这里面的东西有点乱, 主要看下面这个方法(org.springframework.boot.loader.jar.CentralDirectoryParser#parseEntries)

private void parseEntries(CentralDirectoryEndRecord endRecord,
		RandomAccessData centralDirectoryData) throws IOException {
	byte[] bytes = Bytes.get(centralDirectoryData);
	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 += this.CENTRAL_DIRECTORY_HEADER_base_SIZE
				+ fileHeader.getName().length() + fileHeader.getComment().length()
				+ fileHeader.getExtra().length;
	}
}

总结一下: List是根据jar程序包中依赖包文件地址顺序而来的, 所以要想知道classpath顺序,得知道打包jar程序包是依赖包的写入文件的顺序, 所以问题出在spring-boot-maven-plugin打包项目上



spring-boot-maven-plugin 打包分析

项目用的是spring-boot-maven-plugin的repackage来打成可执行包, 不多说, 还是debug

maven 打包debug (默认是8000端口)

mvnDebug -DskipTests=true package

关键方法
org.springframework.boot.maven.RepackageMojo#repackage

private void repackage() throws MojoExecutionException {
	File source = this.project.getArtifact().getFile();
	File target = getTargetFile();
	Repackager repackager = getRepackager(source);
	// this.project 是maven传递给插件的
	// this.project.getArtifacts() 获取项目的所有依赖
	Set artifacts = filterDependencies(this.project.getArtifacts(),
			getFilters(getAdditionalFilters()));
	Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
			getLog());
	try {
		Launchscript launchscript = getLaunchscript();
		repackager.repackage(target, libraries, launchscript);
	}
	catch (IOException ex) {
		throw new MojoExecutionException(ex.getMessage(), ex);
	}
	updateArtifact(source, target, repackager.getBackupFile());
}

其中 this.project.getArtifacts() 是获取项目的所有依赖, 注意它返回的是linkedHashSet它是有顺序的,而且是按照maven的依赖规则生成的顺序, 但是在我调试的时候filterDependencies方法返回的是HashSet

org.springframework.boot.maven.AbstractDependencyFilterMojo#filterDependencies

protected Set filterDependencies(Set dependencies,
		FilterArtifacts filters) throws MojoExecutionException {
	try {
		return filters.filter(dependencies);
	}
	catch (ArtifactFilterException e) {
		throw new MojoExecutionException(e.getMessage(), e);
	}
}

通过查看代码发现源码中使用的依赖过滤器返回的都是HashSet

  • org.apache.maven.shared.artifact.filter.collection.ScopeFilter
  • org.apache.maven.shared.artifact.filter.collection.ArtifactIdFilter
  • org.springframework.boot.maven.MatchingGroupIdFilter
  • org.springframework.boot.maven.ExcludeFilter
  • org.springframework.boot.maven.IncludeFilter


生成Jar包时的依赖包的写入顺序

org.springframework.boot.loader.tools.Repackager

private void repackage(JarFile sourceJar, File destination, Libraries libraries,
		Launchscript launchscript) throws IOException {
	JarWriter writer = new JarWriter(destination, launchscript);
	try {
		final List unpackLibraries = new ArrayList();
		final List standardLibraries = new ArrayList();
		// 重点关注doWithLibraries
		libraries.doWithLibraries(new LibraryCallback() {

			@Override
			public void library(Library library) throws IOException {
				File file = library.getFile();
				if (isZip(file)) {
					if (library.isUnpackRequired()) {
						unpackLibraries.add(library);
					}
					else {
						standardLibraries.add(library);
					}
				}
			}

		});
		writer.writeManifest(buildManifest(sourceJar));
		Set seen = new HashSet();
		writeNestedLibraries(unpackLibraries, seen, writer);
		if (this.layout instanceof RepackagingLayout) {
			writer.writeEntries(sourceJar,
					new RenamingEntryTransformer(((RepackagingLayout) this.layout)
							.getRepackagedClassesLocation()));
		}
		else {
			writer.writeEntries(sourceJar);
		}
		writeNestedLibraries(standardLibraries, seen, writer);
		if (this.layout.isExecutable()) {
			writer.writeLoaderClasses();
		}
	}
	finally {
		try {
			writer.close();
		}
		catch (Exception ex) {
			// Ignore
		}
	}
}

// doWithLibraries 得到的包按序写入jar文件
private void writeNestedLibraries(List libraries, Set alreadySeen,
		JarWriter writer) throws IOException {
	for (Library library : libraries) {
		String destination = Repackager.this.layout
				.getLibraryDestination(library.getName(), library.getScope());
		if (destination != null) {
			if (!alreadySeen.add(destination + library.getName())) {
				throw new IllegalStateException(
						"Duplicate library " + library.getName());
			}
			writer.writeNestedLibrary(destination, library);
		}
	}
}

org.springframework.boot.maven.ArtifactsLibraries#doWithLibraries是对HashSet进行遍历的,所以写入依赖包的顺序是不确定的, 这就导致使用该插件打的jar包在运行时无法确定依赖的引入顺序,从而导致同包同名类覆盖失效

@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
	// this.artifacts 是 `org.springframework.boot.maven.RepackageMojo#repackage` 传过来的.其类型为HashSet
	Set duplicates = getDuplicates(this.artifacts);
	for (Artifact artifact : this.artifacts) {
		LibraryScope scope = SCOPES.get(artifact.getScope());
		if (scope != null && artifact.getFile() != null) {
			String name = getFileName(artifact);
			if (duplicates.contains(name)) {
				this.log.debug("Duplicate found: " + name);
				name = artifact.getGroupId() + "-" + name;
				this.log.debug("Renamed to: " + name);
			}
			callback.library(new Library(name, artifact.getFile(), scope,
					isUnpackRequired(artifact)));
		}
	}
}
修复

查看github上spring-boot项目源码, 发现它在 2.0.0.RELEASE及之后的版本修复了这个bug,
但是我没在它的commit中找到提及这个bug的相关信息
org.springframework.boot.maven.RepackageMojo

private void repackage() throws MojoExecutionException {
	Artifact source = getSourceArtifact();
	File target = getTargetFile();
	Repackager repackager = getRepackager(source.getFile());
	Libraries libraries = getLibraries(this.requiresUnpack);
	try {
		Launchscript launchscript = getLaunchscript();
		repackager.repackage(target, libraries, launchscript, parseOutputTimestamp());
	}
	catch (IOException ex) {
		throw new MojoExecutionException(ex.getMessage(), ex);
	}
	updateArtifact(source, target, repackager.getBackupFile());
}

org.springframework.boot.maven.AbstractPackagerMojo

protected final Libraries getLibraries(Collection unpacks) throws MojoExecutionException {
	Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
	return new ArtifactsLibraries(artifacts, unpacks, getLog());
}

org.springframework.boot.maven.AbstractDependencyFilterMojo#filterDependencies

protected Set filterDependencies(Set dependencies, FilterArtifacts filters)
		throws MojoExecutionException {
	try {
		// dependencies 本身也是linkedHashSet
		// 过滤这里使用linkedHashSet,这样就和maven原来解析依赖的顺序一致
		Set filtered = new linkedHashSet<>(dependencies);
		filtered.retainAll(filters.filter(dependencies));
		return filtered;
	}
	catch (ArtifactFilterException ex) {
		throw new MojoExecutionException(ex.getMessage(), ex);
	}
}
总结

spring-boot-maven-plugin低于2.0.0.RELEASE时打的包是有一定问题的, 它指定classpath的顺序没有按maven的依赖规则, 它只能确保依赖包是那几个, 不能确定依赖包的引入顺序. 对于项目有同包同名类时(先引入的依赖中的类生效),这是有问题的. 对于spring-boot1.x, 如果使用spring-boot-maven-plugin打包最好还是使用2.0.0.RELEASE及以上版本.

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/679025.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号