Springboot IDEA启动分析

链路分析

核心加载类PathMatchingResourcePatternResolver

PathMatchingResourcePatternResolver

IDEA启动分析

idea启动后对包进行格式化处理

URL协议处理,获取绝对路径

/**
 * Find all class location resources with the given path via the ClassLoader.
 * Called by {@link #findAllClassPathResources(String)}.
 * @param path the absolute path within the classpath (never a leading slash)
 * @return a mutable Set of matching Resource instances
 * @since 4.1.1
 */
// path = classpath*:com/sec/mdm/manage/
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
   Set<Resource> result = new LinkedHashSet<>(16);
   ClassLoader cl = getClassLoader();
   Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
   while (resourceUrls.hasMoreElements()) {
      URL url = resourceUrls.nextElement();
      result.add(convertClassLoaderURL(url));
   }
   if (!StringUtils.hasLength(path)) {
      // The above result is likely to be incomplete, i.e. only containing file system references.
      // We need to have pointers to each of the jar files on the classpath as well...
      addAllClassLoaderJarRoots(cl, result);
   }
   return result;
}

获取路径下的File[]文件

对包进行格式化处理,最终执行到PathMatchingResourcePatternResolver listDirectory()方法,获取绝对路径下target的文件

protected File[] listDirectory(File dir) {
   File[] files = dir.listFiles();
   if (files == null) {
      if (logger.isInfoEnabled()) {
         logger.info("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
      }
      return new File[0];
   }
   Arrays.sort(files, Comparator.comparing(File::getName));
   return files;
}

然后进行antPathMatcher匹配class文件

private PathMatcher pathMatcher = new AntPathMatcher();
// /Users/xielianjun/005-code/demo/target/classes/com/meituan/sec/mdm/manage/**/*.class
// /Users/xielianjun/005-code/demo/target/classes/com/meituan/sec/mdm/manage/ItsecDynamicDataSourceConfig.class
if (getPathMatcher().match(fullPattern, currPath)) {
   result.add(content);
}

返回Set集合

Set<File> result = new LinkedHashSet<>(8);

包装成FileSystemResource

for (File file : matchingFiles) {
			result.add(new FileSystemResource(file));
}

循环Resource包装成MetadataReader

ClassReader读取字节流resource.getInputStream(),然后字节流处理获取className,组装成元数据信息MetadataReader

jar -jar命令启动

Java资源管理URL

URL url = new URL("https://start.spring.io/");      // https
URL ftpurl = new URL("ftp://ftp.baidu.com");        // ftp
URL jarurl = new URL("jar://jar.baidu.com");        // jar 协议
URL wetchat = new URL("webchat://...");             // 微信协议
URL dubboURL = new URL("dubbo://...");              // dubbo 协议
URL classpathURL = new URL("classpath:/");          /// classpath

jdk源码包包含各种协议处理的handler

  • URLStreamhandler
    • sun.net.www.protocol.file.Handler 文本协议的 Handler
    • sun.net.www.protocol.http.Handler HTTP URl 的handler
    • sun.net.www.protocol.https.Handler https 的
    • sun.net.www.protocol.war.Handler war URl 的handler
    • sun.net.www.protocol.jar.Handler jar URl 的handler
    • sun.net.www.protocol.ftp.Handler HTTP URl 的handler

如何解析各种协议

通过URL中的构造方法调用ge tURLStreamHandler传入协议反射获取该协议处理器

/**
 * Returns the Stream Handler.
 * @param protocol the protocol to use
 */
static URLStreamHandler getURLStreamHandler(String protocol) {
  // 省略。。。。
            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
                }
            }
        }
// 省略。。。。
    return handler;

}

org.springframework.core.io.Resource

/**
 * Interface for a resource descriptor that abstracts from the actual
 * type of underlying resource, such as a file or class path resource.
 *
 * <p>An InputStream can be opened for every resource if it exists in
 * physical form, but a URL or File handle can just be returned for
 * certain resources. The actual behavior is implementation-specific.
 *
 * @author Juergen Hoeller
 * @since 28.12.2003
 * @see #getInputStream()
 * @see #getURL()
 * @see #getURI()
 * @see #getFile()
 * @see WritableResource
 * @see ContextResource
 * @see UrlResource
 * @see FileUrlResource
 * @see FileSystemResource
 * @see ClassPathResource
 * @see ByteArrayResource
 * @see InputStreamResource
 */
public interface Resource extends InputStreamSource {

	boolean exists();
  default boolean isReadable() {
		return exists();
	}
	default boolean isOpen() {
		return false;
	}
	default boolean isFile() {
		return false;
	}
	URL getURL() throws IOException;

	URI getURI() throws IOException;

	File getFile() throws IOException;

	default ReadableByteChannel readableChannel() throws IOException {
		return Channels.newChannel(getInputStream());
	}
	long contentLength() throws IOException;
	long lastModified() throws IOException;
	Resource createRelative(String relativePath) throws IOException;
	@Nullable
	String getFilename();
	String getDescription();

}

Spring的Resource和 URL 非常像,也就是封装的比较好而已。

public static void main(String[] args) throws IOException {
    // Resource
    // FileSystemResource
    // ClassPathResource
    ResourceLoader resourceLoader = new DefaultResourceLoader();
    Resource resource = resourceLoader
            .getResource("classpath:/application.properties");

    InputStream inputStream = resource.getInputStream();
    String content = StreamUtils.copyToString(inputStream, Charset.forName("UTF-8"));
    System.out.println(content);
}

启动类JarLauncher

META-INF主文件MANIFEST.MF启动类定义为JarLauncher

Manifest-Version: 1.0

Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx

Implementation-Title: demo

Implementation-Version: 0.0.1-SNAPSHOT

Spring-Boot-Layers-Index: BOOT-INF/layers.idx

Start-Class: com.meituan.sec.mdm.manage.DemoApplication

Spring-Boot-Classes: BOOT-INF/classes/

Spring-Boot-Lib: BOOT-INF/lib/

Build-Jdk-Spec: 1.8

Spring-Boot-Version: 2.4.3

Created-By: Maven Jar Plugin 3.2.0

Main-Class: org.springframework.boot.loader.JarLauncher
不同环境获取文件路径URL

A.java 和 B.java放在同一包下,再各种情况下,A如何寻找B,默认是在Eclipse编辑环境下

URL url = A.class.getProtectionDomain().getCodeSource()
.getLocation();

System.out.println(url);

结果(未打成JAR时):

file:/C:/Documents and Settings/Administrator.MICROSOF-B715C3/workspace/T/bin/

结果(打成JAR时)

file:/C:/Documents and Settings/Administrator.MICROSOF-B715C3/%e6%a1%8c%e9%9d%a2/1.jar

那么如何加载这个jar中的类呢

JarFile file = new JarFile(url.getFile());

JarLauncher 的执行过程

提示:走读的时候时不时结合概览中的时序图,可能好些。

JarLauncher 的 main 方法:

public static void main(String[] args) {    // 构造JarLauncher,然后调用它的launch方法    new JarLauncher().launch(args);}

JarLauncher 被构造的时候会调用父类 ExecutableArchiveLauncher 的构造方法。

ExecutableArchiveLauncher 的构造方法内部会去构造 Archive,这里构造了 JarFileArchive。构造 JarFileArchive 的过程中还会构造很多东西,比如 JarFile,Entry …

public abstract class ExecutableArchiveLauncher extends Launcher {
	private final Archive archive;
    // 构造器会初始化代表 fat jar 的 Archive
	public ExecutableArchiveLauncher() {
		this.archive = createArchive();
	}
    // 由父类 Launcher 实现
	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);
		}
        // 最终会 new 一个 Arichive,内部生产的 JarFile-->这个逼对FatJar资源加载非常重要
		return (root.isDirectory() ? new ExplodedArchive(root)
				: new JarFileArchive(root));
	}
    
	@Override
	protected List<Archive> getClassPathArchives() throws Exception {
		List<Archive> archives = new ArrayList<Archive>(
        	// 获取内部所有有的 Arichive
            this.archive.getNestedArchives(new EntryFilter() {
                @Override
                public boolean matches(Entry entry) {
                    return isNestedArchive(entry);
                }
            }));
        // 空实现,没用
		postProcessClassPathArchives(archives);
		return archives;
	}

}

JarLauncher 的 launch 方法:

protected void launch(String[] args) {
  try {
// 在系统属性中设置注册了自定义的URL协议处理器:org.springframework.boot.loader.jar.Handler。
// 初始化URL的时候,如果URL中没有指定处理器,会去系统属性中查询
    JarFile.registerUrlProtocolHandler();
// getClassPathArchives方法会去找lib目录下对应的第三方依赖JarFileArchive,同时也会找项目自身的JarFileArchive
// 根据getClassPathArchives得到的JarFileArchive集合去创建类加载器ClassLoader。这里会构造一个LaunchedURLClassLoader类加载器,这个类加载器继承URLClassLoader,并使用这些JarFileArchive集合的URL构造成URLClassPath
// 多说两句句,
// 1.URLClassPath这个属性很重要,自定义ClassLoader,findClass就靠它了!
// 2.可以关注一下构造LaunchedURLClassLoader时,archive.getUrl方法,这里就涉及到自定义URL协议处理器了,JarFile等。毕竟实现jar in jar功能靠他们这些小罗罗。
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
// getMainClass方法会去项目自身的Archive中的Manifest中找出key为Start-Class的类
// 调用重载方法launch
    launch(args, getMainClass(), classLoader);
  }
  catch (Exception ex) {
    ex.printStackTrace();
    System.exit(1);
  }
}

// Archive的getMainClass方法,不过由ExecutableArchiveLauncher实现
// 这里会找出Start-Class标识的com.example.jarlauncher.JarlauncherApplication这个类
public String getMainClass() throws Exception {
	Manifest manifest = 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;
}

// launch重载方法
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
    throws Exception {
    // 设置 LaunchedURLClassLoader 为线程上下文加载器
    Thread.currentThread().setContextClassLoader(classLoader);
    // 创建一个MainMethodRunner 并运行
    createMainMethodRunner(mainClass, args, classLoader).run();
}

MainMethodRunner 的 run 方法:

public void run() throws Exception {
    // 使用线程上下文类加载器加载主类
    Class<?> mainClass = Thread.currentThread().getContextClassLoader()
    .loadClass(this.mainClassName);
    // 反射执行,至此咱们的应用程序就启动起来啦,good,启动流程走读结束,开心!可以跟面试官扯些了
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    mainMethod.invoke(null, new Object[] { this.args });
}

Start-Class 的 main 方法调用之后,内部会构造 Spring 容器,启动内置 Servlet 容器等过程

关于自定义的类加载器

看看传说中的 LaunchedURLClassLoader 有什么神奇的

// 自定义类加载器会执行获取lib路径进行加载,关注后边代码getUrls,进行遍历加载,就是加载此些jar包
protected boolean isSearchCandidate(Entry entry) {
    return entry.getName().startsWith("BOOT-INF/");
}

LaunchedURLClassLoader 重写了 loadClass 方法,走读一下

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    Handler.setUseFastConnectionExceptions(true);
    try {
        try {
        	// 在调用 findClass 之前定义 package,确保嵌套JAR清单与包相关联
            definePackageIfNecessary(name);
        }
        catch (IllegalArgumentException ex) {
            if (getPackage(name) == null) {
                throw new AssertionError("Package " + name + " has already been "
                        + "defined but it could not be found");
            }
        }
        // 调用 父类 loadClass 走正常的加载委派流程
        return super.loadClass(name, resolve);
    }
    finally {
        Handler.setUseFastConnectionExceptions(false);
    }
}

基本就是普通的双亲委派过程。

而且 LaunchedURLClassLoader 使用的 findClass 是从父类 URLClassLoader 继承的。

最终 loadClass 会走到 LaunchedURLClassLoader 的父类 URLClassLoader#findClass

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
// 把类名解析成路径并加上.class后缀                     	
                        String path = name.replace('.', '/').concat(".class");
// 基于之前得到的第三方jar包依赖以及自己的jar包得到URL数组,进行遍历找出对应类名的资源
// 比如path是org/springframework/boot/loader/JarLauncher.class,它在jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/中被找出
// 那么找出的资源对应的URL为jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class                        
                    // 加载fatjar class的关键部分!!!
                    Resource res = ucp.getResource(path, false);
                    if (res != null) { // 找到了资源
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        throw new ClassNotFoundException(name);
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

jar启动调试

<!-- Spring Boot loader		-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-loader</artifactId>
</dependency>

1、Idea配置以 Jar 应用的方式启动

img

2、配置 Jar 路径,然后 Apply

img

3、找到启动类 JarLauncher,打上断点,debug 方式启动

img

参考

https://xie.infoq.cn/article/765f324659d44a5e1eae1ee0c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值