概述
最近在项目中遇到个问题,一次升级依赖之后,发现线上某台机器日志无输出;这种问题通常都是由于log jar冲突导致,查看依赖果然发现项目中同时存在log4j 1.x和log4j2.x jar,问题本身并不复杂,让我好奇的是为什么部分机器都工作正常,部分不正常呢?
jar加载顺序分析
从上面的问题推测,应该是不同机器加载的jar包顺序存在不一致,那么jar包的加载顺序遵循什么样的规则呢?
按照项目的启动方式不一样,在自己接触的项目中,通常有三种不同的启动方式:
- spring boot项目
- tomcat容器项目
- appassembler-maven-plugin项目
spring boot项目
spring boot项目通常都是用spring-boot-maven-plugin插件打包成flat jar,flat jar的内部结构如下:
屏幕快照 2020-06-08 下午11.05.15.png
其中META-INF目录下的MANIFEST.MF文件内容如下:
Manifest-Version: 1.0
Built-By: developer
Start-Class: com.example.storage.ExampleApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.1.5.RELEASE
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_121
Main-Class: org.springframework.boot.loader.JarLauncher
其中的Main-Class大家应该很熟悉,执行java -jar
命令默认就是执行Main-Class的main方法:
public class JarLauncher
extends Launcher
{
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
private final Archive archive;
public JarLauncher() {}
private final Archive archive;
public static void main(String[] args) throws Exception {
(new JarLauncher()).launch(args); }
}
public JarLauncher() {
try {
this.archive = createArchive();
}
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);
}
return root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root);
}
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
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;
}
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
}
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<Archive>(this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
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;
}
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<Archive>(this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<URL>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader((URL[])urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception { return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); }
}
为了方便阅读,上面的代码我做了部分处理;从上面可以看到,spring boot loader使用的类加载器是LaunchedURLClassLoader,该类加载器继承自URLClassLoader,那么该类加载器会从哪里加载类呢?
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<URL>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader((URL[])urls.toArray(new URL[0]));
}
public List<Archive> getNestedArchives(Archive.EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<Archive>();
for (Archive.Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
protected Archive getNestedArchive(Archive.Entry entry) throws IOException {
File file = ((FileEntry)entry).getFile();
return file.isDirectory() ? new ExplodedArchive(file) : new JarFileArchive(file);
}
可以看到类加载器是从BOOT-INF/classes/
和BOOT-INF/lib/
加载类的,那么加载的顺序是如何确定的呢?
private Iterator<File> listFiles(File file) {
File[] files = file.listFiles();
if (files == null) {
return Collections.emptyList().iterator();
}
Arrays.sort(files, this.entryComparator);
return Arrays.asList(files).iterator();
}
private static class EntryComparator
extends Object
implements Comparator<File>
{
private EntryComparator() {}
public int compare(File o1, File o2) { return o1.getAbsolutePath().compareTo(o2.getAbsolutePath()); }
}
}
可以看到是基于文件的全路径来排序的,另外可以判定BOOT-INF/classes/
下面的类的加载顺序是优先于BOOT-INF/lib/
的;
tomcat容器项目
查看tomcat源码可以看到,tomcat默认的类加载器为ParallelWebappClassLoader:
public class WebappLoader extends LifecycleMBeanBase
implements Loader, PropertyChangeListener {
private static final Log log = LogFactory.getLog(WebappLoader.class);
// ----------------------------------------------------------- Constructors
/**
* Construct a new WebappLoader with no defined parent class loader
* (so that the actual parent will be the system class loader).
*/
public WebappLoader() {
this(null);
}
/**
* Construct a new WebappLoader with the specified class loader
* to be defined as the parent of the ClassLoader we ultimately create.
*
* @param parent The parent class loader
*/
public WebappLoader(ClassLoader parent) {
super();
this.parentClassLoader = parent;
}
// ----------------------------------------------------- Instance Variables
/**
* The class loader being managed by this Loader component.
*/
private WebappClassLoaderBase classLoader = null;
/**
* The Context with which this Loader has been associated.
*/
private Context context = null;
/**
* The "follow standard delegation model" flag that will be used to
* configure our ClassLoader.
*/
private boolean delegate = false;
/**
* The Java class name of the ClassLoader implementation to be used.
* This class should extend WebappClassLoaderBase, otherwise, a different
* loader implementation must be used.
*/
private String loaderClass = ParallelWebappClassLoader.class.getName();
``
查看ParallelWebappClassLoader的loadClass方法,可以发现其最终调用的方法为:
```java
StandardRoot.java
private final List<List<WebResourceSet>> allResources =
new ArrayList<>();
{
allResources.add(preResources);
allResources.add(mainResources);
allResources.add(classResources);
allResources.add(jarResources);
allResources.add(postResources);
}
@Override
public WebResource getClassLoaderResource(String path) {
return getResource("/WEB-INF/classes" + path, true, true);
}
protected final WebResource getResourceInternal(String path,
boolean useClassLoaderResources) {
WebResource result = null;
WebResource virtual = null;
WebResource mainEmpty = null;
for (List<WebResourceSet> list : allResources) {
for (WebResourceSet webResourceSet : list) {
if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() ||
useClassLoaderResources && !webResourceSet.getStaticOnly()) {
result = webResourceSet.getResource(path);
if (result.exists()) {
return result;
}
if (virtual == null) {
if (result.isVirtual()) {
virtual = result;
} else if (main.equals(webResourceSet)) {
mainEmpty = result;
}
}
}
}
}
// Use the first virtual result if no real result was found
if (virtual != null) {
return virtual;
}
// Default is empty resource in main resources
return mainEmpty;
}
protected void startInternal() throws LifecycleException {
mainResources.clear();
main = createMainResourceSet();
mainResources.add(main);
for (List<WebResourceSet> list : allResources) {
// Skip class resources since they are started below
if (list != classResources) {
for (WebResourceSet webResourceSet : list) {
webResourceSet.start();
}
}
}
// This has to be called after the other resources have been started
// else it won't find all the matching resources
processWebInfLib();
// Need to start the newly found resources
for (WebResourceSet classResource : classResources) {
classResource.start();
}
cache.enforceObjectMaxSizeLimit();
setState(LifecycleState.STARTING);
}
从上面的代码可以看到WEB-INF/classes下面类的加载顺序是优先于WEB-INF/lib的
protected void processWebInfLib() throws LifecycleException {
WebResource[] possibleJars = listResources("/WEB-INF/lib", false);
for (WebResource possibleJar : possibleJars) {
if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
createWebResourceSet(ResourceSetType.CLASSES_JAR,
"/WEB-INF/classes", possibleJar.getURL(), "/");
}
}
}
这边列出lib目录下的jar文件,使用的是File.list方法,该方法在jdk8中的实现如下:
JNIEXPORT jobjectArray JNICALL
Java_java_io_UnixFileSystem_list(JNIEnv *env, jobject this,
jobject file)
{
DIR *dir = NULL;
struct dirent64 *ptr;
struct dirent64 *result;
int len, maxlen;
jobjectArray rv, old;
WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
dir = opendir(path);
} END_PLATFORM_STRING(env, path);
if (dir == NULL) return NULL;
ptr = malloc(sizeof(struct dirent64) + (PATH_MAX + 1));
if (ptr == NULL) {
JNU_ThrowOutOfMemoryError(env, "heap allocation failed");
closedir(dir);
return NULL;
}
/* Allocate an initial String array */
len = 0;
maxlen = 16;
rv = (*env)->NewObjectArray(env, maxlen, JNU_ClassString(env), NULL);
if (rv == NULL) goto error;
/* Scan the directory */
while ((readdir64_r(dir, ptr, &result) == 0) && (result != NULL)) {
jstring name;
if (!strcmp(ptr->d_name, ".") || !strcmp(ptr->d_name, ".."))
continue;
if (len == maxlen) {
old = rv;
rv = (*env)->NewObjectArray(env, maxlen <<= 1,
JNU_ClassString(env), NULL);
if (rv == NULL) goto error;
if (JNU_CopyObjectArray(env, rv, old, len) < 0) goto error;
(*env)->DeleteLocalRef(env, old);
}
#ifdef MACOSX
name = newStringPlatform(env, ptr->d_name);
#else
name = JNU_NewStringPlatform(env, ptr->d_name);
#endif
if (name == NULL) goto error;
(*env)->SetObjectArrayElement(env, rv, len++, name);
(*env)->DeleteLocalRef(env, name);
}
closedir(dir);
free(ptr);
/* Copy the final results into an appropriately-sized array */
old = rv;
rv = (*env)->NewObjectArray(env, len, JNU_ClassString(env), NULL);
if (rv == NULL) {
return NULL;
}
if (JNU_CopyObjectArray(env, rv, old, len) < 0) {
return NULL;
}
return rv;
error:
closedir(dir);
free(ptr);
return NULL;
}
使用的是readdir64_r系统调用,查询了资料,该函数的实现和文件系统有关,例如Ext4,文件顺序与目录文件的大小是否超过一个磁盘块和文件系统计算的Hash值有关;
网友评论