Java源码(一)ThreadLocal、SpringBoot Jar 启动原理

思维导图



一、ThreadLocal

1.场景

项目采用SSM+Shiro登录认证,改造需求如下:

后台管理员登录需要限制,同一个用户的不同IP需要通过过自定义验证后才能登录。

2.问题

  • 在完成需求后发现有管理员用户(这里就用A)通过验证登录了,那么后面登录的管理员用户(这里就用B、C等)可能会直接跳过验证就直接登录了。这肯定不符合要求!

3.问题解决

3.1 Debug

  • 因为是在原来shiro框架自定义过滤器AuthenticationFilter基础上添加了用户ID+IP验证,过滤器中涉及到生成token的createToken方法和认证成功后登录方法executeLogin,所以在该类设置了一个全局私有变量isWebLogin 来判断是否通过自定义验证。
  • 而isWebLogin所有线程(即所有用户)都可以访问并修改,所以导致上述问题。

3.2 解决

  • 那如何让isWebLogin变量的每个线程都拥有自己的专属线程本地变量,且每个线程的访问及修改都互不影响呢?
  • ThreadLocal变量不就正好可以解决这个问题吗?于是isWebLogin使用ThreadLocal定义就解决问题了,修改后的如下图(其中只保留部分使用到的关键代码):

在这里插入图片描述


为什么ThreadLocal能够解决这个问题?且看下面ThreadLocal及其源码分析:

4.ThreadLocal

关于ThreadLocal的基础介绍,见我另一篇文章:
Java并发编程(一)常见知识点 — 21 ThreadLocal

5.ThreadLocal源码分析

5.1 get()源码


 public T get() {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null) {
         ThreadLocalMap.Entry e = map.getEntry(this);
         if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;
             return result;
         }
     }
     return setInitialValue();
 }

逻辑如下:

  • 获取当前线程内部的ThreadLocalMap
  • map存在则获取当前ThreadLocal对应的value值
  • map不存在或者找不到value值,则调用setInitialValue,进行初始化

5.2 setInitialValue()源码

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

逻辑如下:

  • 调用initialValue方法,获取初始化值【调用者通过覆盖该方法,设置自己的初始化值】
  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

5.3 set(T value)源码

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

逻辑如下:

  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

5.4 remove()源码

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

逻辑如下:

  • 获取当前线程内部的ThreadLocalMap,存在则从map中删除这个ThreadLocal对象。

6.ThreadLocal使用注意

6.1 内存泄露

  • 因为ThreadLocal 使用的是ThreadLocalMap
  • 而ThreadLocalMap中使用的 key 为ThreadLocal 的弱引用,而value是强引用。
  • ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
  • ThreadLocalMap中就会出现 key 为null的 Entry。value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。

6.2 如何避免

threadLocalMap在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录,但是还是有可能有编码导致内存泄露,所以我们还需要从以下方面去避免:

  • 使用完 ThreadLocal方法后最好手动调用remove()方法

  • 将ThreadLocal变量定义为private static




二、SpringBoot Jar 启动原理

本文目的: jar 包是如何运行,并启动 Spring Boot 项目的呢?

思维导图


在这里插入图片描述


1.概述

SpringBoot jar包结构如图:


![在这里插入图片描述](https://img-blog.csdnimg.cn/1a0afd8290a8489e8530a44209587872.png)
  1. BOOT-INF/lib 目录:我们 Spring Boot 项目中引入的依赖的 jar 包们。
  2. BOOT-INF/classes 目录:我们在 Spring Boot 项目中 Java 类所编译的 .class、配置文件等等。
  3. META-INF 目录:通过 MANIFEST.MF 文件提供 jar 包的元数据,声明了 jar 的启动类。
  4. org 目录:为 Spring Boot 提供的 spring-boot-loader 项目,它是 java -jar 启动 Spring Boot 项目的核心。


2.MANIFEST.MF

Properties 配置文件,每一行都是一个配置项目,如下图:


在这里插入图片描述


### 2.1 Main-Class 配置项
  • Java 规定的 jar 包的启动类,这里设置为 spring-boot-loader 项目的 JarLauncher 类,进行 Spring Boot 应用的启动。

2.2 Start-Class 配置项

  • Spring Boot 规定的主启动类,这里设置为我们定义的 Application 类。

3.JarLauncher

代码如下图:


在这里插入图片描述


  1. 通过 #main(String[] args) 方法,创建 JarLauncher 对象,并调用其 #launch(String[] args) 方法进行启动
  2. 整体的启动逻辑,其实是由父类 Launcher 所提供,如下图所示:

在这里插入图片描述


  1. 父类 Launcher 的 #launch(String[] args) 方法,代码如下:
// Launcher.java

protected void launch(String[] args) throws Exception {
	// 3.1  注册 URL 协议的处理器
	JarFile.registerUrlProtocolHandler();
	// 3.2  创建类加载器
	ClassLoader classLoader = createClassLoader(getClassPathArchives());
	// 3.3  执行启动类的 main 方法
	launch(args, getMainClass(), classLoader);
}
  • 3.1 调用 JarFile 的 #registerUrlProtocolHandler() 方法,注册 Spring Boot 自定义的 URLStreamHandler 实现类,加载读取 jar 包。

  • 3.2 调用自身的 #createClassLoader(List archives) 方法,创建自定义的 ClassLoader 实现类,加载 jar 包中的类。

  • 3.3 执行我们声明的 Spring Boot 启动类,启动Spring Boot 应用。


3.1 registerUrlProtocolHandler

代码如下:

// JarFile.java

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";

private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";

/**
 * Register a {@literal 'java.protocol.handler.pkgs'} property so that a
 * {@link URLStreamHandler} will be located to deal with jar URLs.
 */
public static void registerUrlProtocolHandler() {
    // 获得 URLStreamHandler 的路径
	String handlers = System.getProperty(PROTOCOL_HANDLER, "");
	// 将 Spring Boot 自定义的 HANDLERS_PACKAGE(org.springframework.boot.loader) 补充上去
	System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
			: handlers + "|" + HANDLERS_PACKAGE));
	// 重置已缓存的 URLStreamHandler 处理器们
	resetCachedUrlHandlers();
}

/**
 * Reset any cached handlers just in case a jar protocol has already been used.
 * We reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
 * should have no effect other than clearing the handlers cache.
 *
 * 重置 URL 中的 URLStreamHandler 的缓存,防止 `jar://` 协议对应的 URLStreamHandler 已经创建
 * 我们通过设置 URLStreamHandlerFactory 为 null 的方式,清空 URL 中的该缓存。
 */
private static void resetCachedUrlHandlers() {
	try {
		URL.setURLStreamHandlerFactory(null);
	} catch (Error ex) {
		// Ignore
	}
}
  • 通过将 org.springframework.boot.loader 包设置到 “java.protocol.handler.pkgs” 环境变量,从而使用到自定义的 URLStreamHandler 实现类 Handler,处理 jar: 协议的 URL。

3.2 createClassLoader

3.2.1 getClassPathArchives

代码如下:

// ExecutableArchiveLauncher.java

private final Archive archive;

@Override
protected List<Archive> getClassPathArchives() throws Exception {
	// <1> 获得所有 Archive
	List<Archive> archives = new ArrayList<>(
			this.archive.getNestedArchives(this::isNestedArchive));
	// <2> 后续处理
	postProcessClassPathArchives(archives);
	return archives;
}

protected abstract boolean isNestedArchive(Archive.Entry entry);

protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
  1. <1> 处,this::isNestedArchive 代码段,创建了 EntryFilter 匿名实现类,用于过滤 jar 包不需要的目录。
// Archive.java

/**
 * Represents a single entry in the archive.
 */
interface Entry {

	/**
	 * Returns {@code true} if the entry represents a directory.
	 * @return if the entry is a directory
	 */
	boolean isDirectory();

	/**
	 * Returns the name of the entry.
	 * @return the name of the entry
	 */
	String getName();

}

/**
 * Strategy interface to filter {@link Entry Entries}.
 */
interface EntryFilter {

	/**
	 * Apply the jar entry filter.
	 * @param entry the entry to filter
	 * @return {@code true} if the filter matches
	 */
	boolean matches(Entry entry);

}

  1. 上面的isNestedArchive(Archive.Entry entry) 方法,它是由 JarLauncher 所实现,代码如下:
// JarLauncher.java

static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

static final String BOOT_INF_LIB = "BOOT-INF/lib/";

@Override
protected boolean isNestedArchive(Archive.Entry entry) {
    // 如果是目录的情况,只要 BOOT-INF/classes/ 目录
	if (entry.isDirectory()) {
		return entry.getName().equals(BOOT_INF_CLASSES);
	}
	// 如果是文件的情况,只要 BOOT-INF/lib/ 目录下的 `jar` 包
	return entry.getName().startsWith(BOOT_INF_LIB);
}
  • 目的就是过滤获得,BOOT-INF/classes/ 目录下的类,以及 BOOT-INF/lib/ 的内嵌 jar 包。
  1. <1> 处,this.archive.getNestedArchives 代码段,调用 Archive 的 #getNestedArchives(EntryFilter filter) 方法,获得 archive 内嵌的 Archive 集合。代码如下:
// Archive.java

/**
 * Returns nested {@link Archive}s for entries that match the specified filter.
 * @param filter the filter used to limit entries
 * @return nested archives
 * @throws IOException if nested archives cannot be read
 */
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
  • BOOT-INF/classes/ 目录被归类为一个 Archive 对象,而 BOOT-INF/lib/ 目录下的每个内嵌 jar 包都对应一个 Archive 对象。

在这里插入图片描述


3.2.2 createClassLoader

  1. createClassLoader(List archives) 方法,它是由 ExecutableArchiveLauncher 所实现,代码如下:
// ExecutableArchiveLauncher.java

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
	// 获得所有 Archive 的 URL 地址
    List<URL> urls = new ArrayList<>(archives.size());
	for (Archive archive : archives) {
		urls.add(archive.getUrl());
	}
	// 创建加载这些 URL 的 ClassLoader
	return createClassLoader(urls.toArray(new URL[0]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
	return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
  • 基于获得的 Archive 数组,创建自定义 ClassLoader 实现类 LaunchedURLClassLoader,通过它来加载 BOOT-INF/classes 目录下的类,以及 BOOT-INF/lib 目录下的 jar 包中的类

  1. LaunchedURLClassLoader

通过 LaunchedURLClassLoader 加载 jar 包中内嵌的类

  • LaunchedURLClassLoader 是 spring-boot-loader 项目自定义的类加载器,实现对 jar 包中 META-INF/classes 目录下的类和 META-INF/lib 内嵌的 jar 包中的类的加载。

  • 它的创建代码如下:

// LaunchedURLClassLoader.java

public class LaunchedURLClassLoader extends URLClassLoader {

	public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
		super(urls, parent);
	}
	
}
  • 它的实现代码如下:

在这里插入图片描述

  • 在<1> 处,在通过父类的 #getPackage(String name) 方法获取不到指定类所在的包时,会通过遍历 urls 数组,从 jar 包中加载类所在的包。当找到包时,会调用 #definePackage(String name, Manifest man, URL url) 方法,设置包所在的 Archive 对应的 url。

  • 在<2> 处,调用父类的 #loadClass(String name, boolean resolve) 方法,加载对应的类。


3.3 launch

3.3.1 getMainClass

代码如下:

// ExecutableArchiveLauncher.java

@Override
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;
}
  • 从 jar 包的 MANIFEST.MF 文件的 Start-Class 配置项,,获得我们设置的 Spring Boot 的主启动类。

3.3.2 createMainMethodRunner

  1. launch() 方法负责最终的 Spring Boot 应用真正的启动。
    它是由 Launcher 所实现,代码如下:
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
		throws Exception {
    // <1> 设置 LaunchedURLClassLoader 作为类加载器
	Thread.currentThread().setContextClassLoader(classLoader);
	// <2> 创建 MainMethodRunner 对象,并执行 run 方法,启动 Spring Boot 应用
	createMainMethodRunner(mainClass, args, classLoader).run();
}
  • <1> 处:设置「3.2.2 createClassLoader」创建的 LaunchedURLClassLoader 作为类加载器,从而保证能够从 jar 加载到相应的类。

  • <2> 处,调用 #createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) 方法,创建 MainMethodRunner 对象,并执行其 #run() 方法来启动 Spring Boot 应用。

  1. MainMethodRunner 类,负责 Spring Boot 应用的启动。代码如下:
public class MainMethodRunner {

	private final String mainClassName;

	private final String[] args;

	/**
	 * Create a new {@link MainMethodRunner} instance.
	 * @param mainClass the main class
	 * @param args incoming arguments
	 */
	public MainMethodRunner(String mainClass, String[] args) {
		this.mainClassName = mainClass;
		this.args = (args != null) ? args.clone() : null;
	}

	public void run() throws Exception {
	    // <1> 加载 Spring Boot
		Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
		// <2> 反射调用 main 方法
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		mainMethod.invoke(null, new Object[] { this.args });
	}

}
  • <1> 处:通过 LaunchedURLClassLoader 类加载器,加载到我们设置的 Spring Boot 的主启动类。
  • <2> 处:通过反射调用主启动类的 #main(String[] args) 方法,启动 Spring Boot 应用。这里也告诉了我们答案,为什么我们通过编写一个带有 #main(String[] args) 方法的类,就能够启动 Spring Boot 应用。



总结图


在这里插入图片描述






下一篇跳转—Java源码(一)



本篇文章主要参考链接如下:

参考链接-芋道源码


持续更新中…

随心所往,看见未来。Follow your heart,see light!

欢迎点赞、关注、留言,一起学习、交流!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/7360.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android build.gradle配置详解

Android Studio是采用gradle来构建项目的&#xff0c;gradle是基于groovy语言的&#xff0c;如果只是用它构建普通Android项目的话&#xff0c;是可以不去学groovy的。当我们创建一个Android项目时会包含两个Android build.gradle配置详解文件&#xff0c;如下图&#xff1a; 一…

区块链3链(TRC ERC BSC)授权持币生息源码

分享一款3链&#xff08;TRC ERC BSC&#xff09;授权持币生息源码、来自群友投稿的资源、据说是运营级的。简单的看了下没有问题什么大问题、有能力的可以拿来二开其他的模板。 搭建非常简单&#xff0c;教程就不写了、环境NGINX1.2PHP7.2MYSQL5.6TP默认伪静态 此类源码需要…

【Python】数学 - 用 Python 自动化求解函数 f(x) 的值

目录 1、缘起 2、求以下函数的值 3、代码清单 3.1、求解 f(0)、f(1)、 f(​编辑)、f(​编辑) 3.2、求解 g(0)、g(1)、g(​编辑)、g(​编辑) 3.3、求解 h(0)、h(1)、h(​编辑)、h(​编辑) 4、总结 1、缘起 Python 是一种强大的编程语言&#xff0c;它具有广泛的应用领域。…

Python模拟星空

文章目录前言Turtle基础1.1 Turtle画板1.2 Turtle画笔1.3 Turtle画图1.4 Turtle填色1.5 Turtle写字模拟星空模拟星球浪漫星空尾声前言 Python模拟星空&#xff0c;你值得拥有&#xff01;uu们一周不见啦&#xff0c;本周博主参考网上大佬们的星空&#xff0c;给大家带来了属于…

C语言操作符优先级

在平时写代码时&#xff0c;经常会用到操作符&#xff0c;但是如果不了解这些操作符的优先级&#xff0c;可能会让程序的执行效果和我们预期的不一样。 例如&#xff1a; int a 2;int b 3;int c 4;//int ret a b * c;//我们想要执行的顺序是ab的值再乘c//如果了解操作符优…

chat GPT人工智能写论文-怎么用chatGpt写论文

用chatGPT写文章会重复吗 使用 ChatGPT 写文章可能会出现重复的情况。因为 ChatGPT 是基于机器学习的自然语言处理技术&#xff0c;它并不具备人类的创造性思维&#xff0c;其生成的文本内容是基于已有语言数据的统计模型而产生的。 当输入信息重复、语言结构复杂或指定主题较…

【测试】《软件测试》阅读总结

第一章 软件测试的流程是什么&#xff1f; 需求分析--------测试计划----------测试开发--------测试执行-------测试报告 如何描述一个BUG 版本&#xff0c;测试环境、测试步骤和测试数据、实际结果、预期结果、附件&#xff08;截图、错误日志&#xff09; 软件测试过程包括…

HashMap,HashTable和ConcurrentHashMap之间有什么区别?

前言 在之前HashMap的学习中,我们可以知道HashMap是线程不安全的数据结构,它存储的一般是数据的键值对(Key-Value模型),其中Key允许为null,它底层是数组链表的实现,当单个链表的数据元素过多时,会转变为红黑树,在多线程环境下,对某个HashMap对象进行操作,是无法保证线程安全的,…

代理服务器与CDN的概念

代理服务器 特点&#xff1a;本身不产生内容&#xff0c;处于中间位置转发上下游的请求和响应 面向下游的客户端&#xff1a;它是服务器面向上游的服务器&#xff1a;它是客户端 正向代理&#xff1a;代理的对象是客户端 隐藏客户端身份绕过防火墙&#xff08;突破访问限制&am…

今天面了一个来京东要求月薪25K,明显感觉他背了很多面试题...

最近有朋友去京东面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…

LeetCode-146. LRU 缓存

目录LRU理论题目思路代码实现一代码实现二题目来源 146. LRU 缓存 LRU理论 LRU 是 Least Recently Used 的缩写&#xff0c;这种算法认为最近使用的数据是热门数据&#xff0c;下一次很大概率将会再次被使用。而最近很少被使用的数据&#xff0c;很大概率下一次不再用到。当缓…

把ChatGPT接入我的个人网站

效果图 详细内容和使用说明可以查看我的个人网站文章 把ChatGPT接入我的个人网站 献给有外网服务器的小伙伴 如果你本人已经有一台外网的服务器&#xff0c;并且页拥有一个OpenAI API Key&#xff0c;那么下面就可以参照我的教程来搭建一个自己的ChatGPT。 需要的环境 Cento…

大数据分析工具Power BI(三):导入数据操作介绍

导入数据操作介绍 进入PowBI,弹出的如下页面也可以直接关闭,在Power BI中想要导入数据需要通过Power Query 编辑器,Power Query 主要用来清洗和整理数据。

Go分布式爬虫笔记(十七) 4月Day1

文章目录17 协程线程与协程对比调度方式调度策略栈大小上下文切换速度GMP调度循环调度算法如果本地运行队列已经满了&#xff0c;无法处理全局运行队列中的协程怎么办&#xff1f;查找协程的先后顺序主动调度被动调度抢占调度执行时间过长的抢占调度陷入到系统调用中的抢占调度…

leetcode:颠倒二进制位(详解)

前言&#xff1a;内容包括&#xff1a;题目&#xff0c;代码实现&#xff0c;大致思路及图示 题目&#xff1a; 颠倒给定的 32 位无符号整数的二进制位。 提示&#xff1a; 请注意&#xff0c;在某些语言&#xff08;如 Java&#xff09;中&#xff0c;没有无符号整数类型。…

ThreeJS-聚光灯物体投影(二十)

聚光灯&#xff08;灯泡&#xff09; 关键代码&#xff1a; //直线光&#xff08;由光源发出的灯光&#xff09; // const directionalLight new THREE.DirectionalLight(0xFFFFFF, 0.7); // directionalLight.position.set(10, 10, 10); …

【蓝桥杯冲刺】蓝桥杯12届省赛C++b组真题-编程题

目录 试题F&#xff1a;时间显示 解题思路 代码 试题G&#xff1a;砝码称重 解题思路 代码 试题H&#xff1a;杨辉三角 解题思路 代码 试题I&#xff1a;双向排序 解题思路 试题J&#xff1a;括号序列 解题思路 试题F&#xff1a;时间显示 【问题描述】 小蓝要和…

Linux总结(二)

基础IO 1.什么叫文件? 我们需要在操作系统的角度理解文件。 文件 = 文件内容 + 属性(所以即使是空文件,也会占空间,因为我们是需要保存文件属性的,属性也是数据,所以占空间) C/C++程序默认会打开三个文件流,叫做标准输入(stdin),标准输出(stdout),标准错误(std…

【新2023Q2押题JAVA】华为OD机试 - 服务依赖

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧本篇题解:服务依赖 题目 在某系统中有…

时间序列的迁移学习

目录 时间序列及其研究状况&#xff1a; 时间序列中存在迁移学习问题吗&#xff1f; 已有的时间序列建模方法的大致思路 迁移学习如何应用于时间序列建模&#xff1f; 本内容摘录于王晋东老师的《迁移学习导论》 时间序列及其研究状况&#xff1a; 所谓时间序列&#…