思维导图
一、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)
- BOOT-INF/lib 目录:我们 Spring Boot 项目中引入的依赖的 jar 包们。
- BOOT-INF/classes 目录:我们在 Spring Boot 项目中 Java 类所编译的 .class、配置文件等等。
- META-INF 目录:通过 MANIFEST.MF 文件提供 jar 包的元数据,声明了 jar 的启动类。
- 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
代码如下图:
- 通过 #main(String[] args) 方法,创建 JarLauncher 对象,并调用其 #launch(String[] args) 方法进行启动
- 整体的启动逻辑,其实是由父类 Launcher 所提供,如下图所示:
- 父类 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> 处,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);
}
- 上面的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> 处,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
- 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 包中的类
- 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
-
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 应用。
- 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源码(二)Spring Application Context
本篇文章主要参考链接如下:
持续更新中…
随心所往,看见未来。Follow your heart,see light!
欢迎点赞、关注、留言,一起学习、交流!