文章目录
链路分析
核心加载类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
文本协议的 Handlersun.net.www.protocol.http.Handler
HTTP URl 的handlersun.net.www.protocol.https.Handler
https 的sun.net.www.protocol.war.Handler
war URl 的handlersun.net.www.protocol.jar.Handler
jar URl 的handlersun.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 应用的方式启动
2、配置 Jar 路径,然后 Apply
3、找到启动类 JarLauncher,打上断点,debug 方式启动
参考
https://xie.infoq.cn/article/765f324659d44a5e1eae1ee0c