Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

并发控制

计算机领域中,并发控制(Concurrency Control)是一种机制,它确保并发操作可以产生正确结果。

有两种常用的并发控制机制:

  • 乐观并发控制(Optimistic Concurrency Control, OCC),又称为乐观锁(Optimistic Lock),最早是由孔祥重(H.T.Kung)教授提出的。
  • 悲观并发控制(Pessimistic Concurrency Control, PCC),又称为悲观锁(Pessimistic Lock)。

这两种机制或者锁并不是 MySQL 或者数据库中独有的概念,而是并发编程的基本概念。

乐观并发控制(Optimistic concurrency control)

https://en.wikipedia.org/wiki/Optimistic_concurrency_control

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改(低冲突和低争用),所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,没有才能更新成功;否则更新失败,重新拿数据并重试。

适用场景:

  • 它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其它事务又修改了该数据。如果其它事务有更新的话,正在提交的事务会进行回滚。因此乐观并发控制多数用于数据争用不大、冲突较少的环境中。这种环境中,偶尔回滚事务的成本会低于读取数据时锁定数据的成本,因此可以获得比其它并发控制方法更高的吞吐量。

实现方式:

CAS(Compare And Set)

CAS(Compare And Set):实现思路是在 set 的时候,加上初始状态的 compare 条件判断,只有初始状态不变时,才 set 成功。

为了避免 ABA 问题(例如 CAS 过程中只简单进行“值”的校验,在有些情况下,“值”相同不会引入错误的业务逻辑(例如余额),但有些情况下,“值”虽然相同,却已经不是原来的数据了),CAS 不能只比对“值”,还必须确保数据是原来的数据,才能修改成功。实现方式是采用“数据版本”机制,例如通过版本号(version)、时间戳(update_time),来做乐观锁的判断条件,一个数据一个版本,版本变化,即使值相同,也不应该修改成功。

例如:

悲观并发控制(Pessimistic concurrency control)

顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上互斥锁,直到使用完毕才会解锁,这样别人想拿这个数据就会 block 住直到它拿到锁。

适用场景:

  • 悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,由于会阻塞其它事务导致其一直等待,降低整体吞吐量,这样的开销往往无法承受。而乐观锁机制则避免了长事务中的数据库开销。
  • 面对并发请求,在代码中使用“一锁二判三更新”这套操作,其中第一步加锁是为了确保后两步操作的原子性,实现串行化访问临界资源,即同一时刻只能有一个线程/事务独占性的访问临界资源(同步互斥访问),确保并发情况下临界资源的线程安全。

实现方式:

JVM 同步/锁

仅适用于单机部署环境,不适用于集群部署环境。

Java:

数据库的锁

MySQL InnoDB 存储引擎中,悲观锁的类型还有很多种:

  • Shared and Exclusive Locks(共享锁和排它锁)
  • Intention Locks(意向锁)
  • Record Locks(记录锁)
  • Gap Locks(区间锁)
  • Next-Key Locks
  • Insert Intention Locks(插入意向锁)
  • AUTO-INC Locks(自增锁)
  • Predicate Locks for Spatial Indexes(空间索引谓词锁)

例如,通过 MySQL 加锁读(Locking Reads)机制,在 A 事务中先对资源加排它锁(写锁),阻塞其它事务对同一资源的读写访问,然后在事务内进行代码判断以及资源更新提交,实现串行化访问资源:

1
2
3
4
-- 共享锁(读锁)
SELECT ... LOCK IN SHARE MODE;
-- 排它锁(写锁)
SELECT ... FOR UPDATE;

并发控制总结

分布式锁

Redis:使用命令 SETNX 创建互斥锁(mutex key)。注意点:

  • 防锁死(设置锁的过期时间避免锁死)
  • 锁续命(设置后台线程为锁续命)
  • 持锁人解锁(解锁时只能由集群内同机器、同线程操作)

Zookeeper:使用命令 create -e -s 创建临时+序号(EPHEMERAL_SEQUENTIAL)节点。注意点:

  • 羊群效应

使用分布式锁的好处之一是节约数据库资源。

例子

这里举一个抽奖活动的例子,分别展示乐观锁和悲观锁的两种实现流程:

抽奖活动例子

参考

https://en.wikipedia.org/wiki/Concurrency_control

https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

《支付宝防并发方案之”一锁二判三更新”》

《高性能 MySQL》

查看数据库支持的字符集:SHOW CHARACTER SET

查看数据库支持的排序规则:SHOW COLLATION

字符集(Character Sets)

下表是 MySQL 中八个可能影响到字符集的系统变量,其中有几个如果配置不当可能会乱码问题,需重点关注:

变量 默认值 描述
character_set_client utf8 The character set for statements that arrive from the client.
character_set_connection utf8 The character set used for literals specified without a character set introducer and for number-to-string conversion.
character_set_database latin1 The character set used by the default database.
character_set_filesystem binary The file system character set.
character_set_results utf8 The character set used for returning query results to the client. This includes result data such as column values, result metadata such as column names, and error messages.
character_set_server latin1 The server’s default character set.
character_set_system utf8 The character set used by the server for storing identifiers.
character_sets_dir The directory where character sets are installed.

可以通过下图来了解 MySQL 内部字符集转换过程:

MySQL Character Set

  1. MySQL 收到请求时将请求数据从 character_set_client 转换为 character_set_connection
  2. 进行内部操作前将请求数据从 character_set_connection 转换为内部操作字符集,步骤如下:
    1. 使用每个数据字段的 CHARACTER SET 设定值;
    2. 若上述值不存在,则使用对应数据表的字符集设定值;
    3. 若上述值不存在,则使用对应数据库的字符集设定值;
    4. 若上述值不存在,则使用 character_set_server 设定值。
  3. 最后将操作结果从内部操作字符集转换为 character_set_results

而系统变量 character_set_database 主要用来设置默认创建数据库的编码格式,如果在创建数据库时没有设置编码格式,就按照这个格式设置,如下:

1
CREATE DATABASE `testdb` /*!40100 DEFAULT CHARACTER SET latin1 */

从而影响到建表时默认的字符集:

1
2
3
CREATE TABLE `test` (
`name` varchar(255) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

从而影响到中文字符的插入:

1
2
3
INSERT INTO test values ('你好');

[Err] 1366 - Incorrect string value: '\xE4\xBD\xA0\xE5\xA5\xBD' for column 'name' at row 1

配置说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[mysqld]
# 影响系统变量 character_set_database 和 character_set_server
character-set-server = utf8
collation-server = utf8_unicode_ci
init-connect = 'SET NAMES utf8'

# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB

[client]
default-character-set = utf8

[mysql]
default-character-set = utf8

配置后,需要重启服务:

1
2
net stop mysql
net start mysql

之后,通过命令 SHOW VARIABLES LIKE '%character%' 查看结果:

1
2
3
4
5
6
7
8
9
10
11
12
+--------------------------+----------+
| Variable_name | Value |
+--------------------------+----------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | ... |
+--------------------------+----------+

如果在创建数据库之前,没有在配置文件中配置好默认字符集,可以通过 SET 命令进行修改。

配置好后,建库结果如下:

1
CREATE DATABASE `testdb` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci */

排序规则(Collations)

参考

https://dev.mysql.com/doc/refman/5.7/en/charset.html

https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html

charset

什么是代理?

代理对象 = 增强代码 + 目标对象

proxy

有哪些代理方式?

  • 静态代理
  • 动态代理

什么是动态代理?

动态代理是一种在运行时动态生成代理的机制。这个概念是与静态代理相对的,静态代理需要为每一个目标类都手工编写或用工具生成一个对应的代理类,非常繁琐。

动态代理的实现方式?

动态代理的实现方式有很多种,比如:

  • 利用 JDK 自身提供的动态代理 API(java.lang.reflect

  • 或者利用性能更高的第三方字节码生成框架(例如 ASM、cglib(基于 ASM)、Javassist 等)

最终目标都是生成一个代理类的字节码。

哪些场景用到动态代理?

比如:

包装 RPC 调用、…

面向切面的编程(AOP)

动态代理从代理对象创建到方法执行的整体流程如下:

jdk_proxy_process

下面来看下 JDK 自身提供的动态代理,底层是如何实现的。

例子

来个例子,实现如图效果:

jdk_proxy_process2

首先创建接口 Flyable 和目标类 Bird

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 目标类和代理类共同实现的接口
**/
public interface Flyable {
void fly(String param);
}

/**
* 目标类
**/
@Slf4j
public class Bird implements Flyable {
@Override
public void fly(String param) {
log.info("Target bird fly, param = {}", param);
}
}

然后是关键的一步,实现 InvocationHandler 接口,创建代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 代理类
**/
@Slf4j
@AllArgsConstructor
public class BirdProxy implements InvocationHandler {

// 目标对象
private Flyable target;

// proxy 参数表示动态生成的 Proxy 类 通过反射创建出来的对象
// method 参数表示 proxy 对象本次执行的方法,可以判断该参数动态决定执行对应的业务逻辑
// args 参数表示 proxy 对方本次执行的方法参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在目标对象执行前执行代码
log.info("Before target bird method: {}", method.getName());
// 在目标对象上执行指定方法
Object result = method.invoke(target, args);
// 在目标对象执行后执行代码
log.info("After target bird method: {}", method.getName());
return result;
}

// 创建代理对象
public static Flyable newProxy(Flyable target) {
// 方式一:显式使用反射创建代理对象(先获取 com.sun.proxy.$Proxy0 的 Class 对象)
// Class<?> proxyClass = Proxy.getProxyClass(Flyable.class.getClassLoader(), target.getClass().getInterfaces());
// Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
// return (Flyable) constructor.newInstance(new BirdProxy(target));

// 方式二:隐式使用反射创建代理对象,API 更简单
return (Flyable) Proxy.newProxyInstance(Flyable.class.getClassLoader(), target.getClass().getInterfaces(), new BirdProxy(target));
}

}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
public class FlyTest {

@Test
public void test() {
// 创建目标对象
Bird target = new Bird();
// 创建代理对象
Flyable proxy = BirdProxy.newProxy(target);
// 调用任意方法,将执行代理逻辑
proxy.fly("hello world");
}
}

输出结果如下:

1
2
3
Before target bird method: fly
Target bird fly, param = hello world
After target bird method: fly

源码解析

例子中涉及到两个 API,由 Java 1.3 引入:

java.lang.reflect.InvocationHandler,代理对象内部的成员变量。作为代理对象和目标对象的桥梁,代理对象的每个方法调用,都会调用其 invoke() 方法,委托其去调用目标对象,可在此时机补充增强代码

InvocationHandler

java.lang.reflect.Proxy,用于创建代理类或代理对象,同时还是它们的父类。

Proxy

Proxy 的核心方法 newProxyInstance 用于运行时动态生成代理类并通过反射创建实例,其源码及关键注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class Proxy implements java.io.Serializable {

/** 代理类构造方法的参数类型 */
private static final Class<?>[] constructorParams = { InvocationHandler.class };

public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);

final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

/*
* 查找或生成指定的代理类
*/
Class<?> cl = getProxyClass0(loader, intfs);

/*
* 反射调用代理类的构造方法(入参为指定的 invocation handler)创建实例
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}

// 反射获取构造方法
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 调用构造方法,创建代理对象
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}

/**
* Generate a proxy class. Must call the checkProxyAccess method
* to perform permission checks before calling this.
*/
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
}

获取动态代理生成的 Class 文件

java.lang.reflect.Proxy 底层使用了 sun.misc.ProxyGenerator 工具类生成代理类。通过指定 java 命令参数 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true 可以让工具类将动态生成的字节码写到本地磁盘文件($ProxyN.class)。本例生成的字节码文件反编译后源码如下,重点关注 fly 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.sun.proxy;

import com.github.proxy.Flyable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Flyable {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;

public $Proxy0(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final void fly(String var1) throws {
try {
// 调用 proxy 对象的 fly 方法,则委托 InvocationHandler 对象执行 invoke 方法
super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.github.proxy.Flyable").getMethod("fly", Class.forName("java.lang.String"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

可见,动态生成的 $Proxy0 类同样实现了 Flyable 接口,与目标类 Bird 类形成一个三角结构:

jdk_proxy

fly 方法的实现,仅仅只是调用了 InvocationHandler 对象的 invoke 方法,传入上下文参数。具体的业务逻辑还是在自己的 InvocationHandler 中根据参数判断并自行实现。

参考

Proxy pattern 设计模式

代理设计模式

10分钟看懂动态代理设计模式

动态代理是基于什么原理?

Java 动态代理作用是什么? - 知乎用户的回答

Understanding “proxy” arguments of the invoke method of java.lang.reflect.InvocationHandler

字节码增强技术探索

泛型术语

泛型涉及的术语比较多,其与反射接口的对应关系如下:

术语 中文含义 举例 反射接口 备注
Generic type 泛型 List<E> ParameterizedType
Parameterized type 参数化类型 List<String> ParameterizedType
Raw type 原始类型 List ParameterizedType#getRawType 该方法虽然返回 Type 类型,但实际类型是 Class,可以强转使用:(Class<?>) type
Unbounded wildcard type 无限制通配符类型 List<?> ParameterizedType
Bounded wildcard type 有限制通配符类型(上限) List<? extends Number> ParameterizedType
Bounded wildcard type 有限制通配符类型(下限) List<? super Number> ParameterizedType
wildcard type 通配符类型 ? WildcardType
Formal type parameter 形式类型参数 E TypeVariable
Actual type parameter 实际类型参数 String ParameterizedType#getActualTypeArguments 该方法虽然返回 Type[] 类型,但各元素实际类型是 Class,可以强转使用:(Class<?>) type
Bounded type parameter 有限制类型参数 <E extends Number>
Recursive type bound 递归类型限制 <T extends Comparable<T>>
Generic method 泛型方法 static <E> List<E> asList(E[] a)
Type token 类型令牌 String.class

泛型 API

java.lang.reflect.Type

JDK 1.5 引入了泛型特性,一同引入的还有 Java Type 类型体系。其中 java.lang.reflect.Type 接口作为核心,是 Java 编程语言中所有类型的通用超级接口(common superinterface),这些类型包括:

  • 原始类型(raw types)
  • 参数化类型(parameterized types)
  • 数组类型(array types)
  • 八大原始类型(primitive types)
  • 类型变量(type variables)

调整后新引入的五个接口如下:

1
2
3
4
5
java.lang.reflect.Type
java.lang.reflect.ParameterizedType // 最最常用
java.lang.reflect.TypeVariable
java.lang.reflect.WildcardType
java.lang.reflect.GenericArrayType

Type

它们的核心方法如下:

Type_methods

类、字段、方法、构造方法也相应增加了一组方法,用于获取 Type

  • java.lang.Class

    1
    2
    3
    4
    5
    6
    7
    // 获取普通 Class
    Class<? super T> getSuperclass()
    Class<?>[] getInterfaces()

    // 获取 Type
    Type getGenericSuperclass()
    Type[] getGenericInterfaces()
  • java.lang.reflect.Field

    1
    2
    3
    4
    5
    // 获取普通 Class
    Class<?> getType()

    // 获取 Type
    Type getGenericType()
  • java.lang.reflect.Method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 获取普通 Class
    Class<?> getReturnType()
    Class<?>[] getParameterTypes()
    Class<?>[] getExceptionTypes()

    // 获取 Type
    Type getGenericReturnType()
    Type[] getGenericParameterTypes()
    Type[] getGenericExceptionTypes()
  • java.lang.reflect.Constructor

    1
    2
    3
    4
    5
    6
    7
    // 获取普通 Class
    Class<?>[] getParameterTypes()
    Class<?>[] getExceptionTypes()

    // 获取 Type
    Type[] getGenericParameterTypes()
    Type[] getGenericExceptionTypes()

java.lang.reflect.GenericDeclaration

同时新增的接口还有 java.lang.reflect.GenericDeclaration,用于获取 TypeVariable

GenericDeclaration_method

从源码看,只有三个类实现了该接口(见下图):

  • java.lang.Class
  • java.lang.reflect.Method
  • java.lang.reflect.Constructor

GenericDeclaration

因此只有这三个地方可以声明为泛型,并获取其类型参数(type variables)集合:KV

  • 类型(例如 classinterface

    1
    2
    3
    public class Test<K, V> { ... }

    public interface Test<K, V> { ... }
  • 构造方法(Constructor

    1
    public <K, V> Test(K k, V v) { ... }
  • 方法(Method

    1
    public <K, V> K test(V v) { ... }

类、方法、构造方法也相应增加了一个方法,用于获取 TypeVariable

  • java.lang.Class
  • java.lang.reflect.Method
  • java.lang.reflect.Constructor
1
TypeVariable<?>[] getTypeParameters()

例子

下面是几个获取 Type 的例子,先创建三个类:BaseMapperPersonMapperPerson

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface BaseMapper<T extends Serializable & Comparable<T>, K extends Serializable> {

T getById(K id);

}

public class PersonMapper implements BaseMapper<Person, Long>, Serializable {
@Override
public Person getById(Long id) {
return null;
}

public void log(List<?> list) {

}

public <T extends Number> void test(List<T> list1, List<? extends Comparable<T>> list2, T[] array, T item) {
}
}

public class Person implements Serializable, Comparable<Person> {
private String personName;

@Override
public int compareTo(Person o) {
return this.personName.compareTo(o.getPersonName());
}

例子一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test1() {
Method method = PersonMapper.class.getMethod("test", List.class, List.class, Number[].class, Number.class);

// [0] ParameterizedType
// [1] ParameterizedType
// [2] GenericArrayType
// [3] TypeVariable
Type[] genericParameterTypes = method.getGenericParameterTypes();

// Class
Type genericReturnType = method.getGenericReturnType();

// Class[0]
Type[] genericExceptionTypes = method.getGenericExceptionTypes();
}

三个 Type 变量的内容如下:

generic_type_examples

例子二

接下来看下 ParameterizedType 的使用,可以用于获取泛型的原始类型(Raw type)、实际类型参数(Actual type parameter)列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void test2() {
// com.github.reflection.BaseMapper<com.github.reflection.Person, java.lang.Long>
Type genericInterface = PersonMapper.class.getGenericInterfaces()[0];
assertTrue(genericInterface instanceof ParameterizedType);
ParameterizedType parameterizedType = (ParameterizedType) genericInterface;

// 获取原始类型:BaseMapper.class
assertEquals(BaseMapper.class, parameterizedType.getRawType());

// 获取实际类型参数列表:Person.class、Long.class
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
assertEquals("There are two actual type arguments", 2, actualTypeArguments.length);
// class com.github.reflection.Person
assertClass("One is Person", actualTypeArguments[0], Person.class);
// class java.lang.Long
assertClass("Another is Long", actualTypeArguments[1], Long.class);
}

private void assertClass(Type type, Class expectedClass) {
assertTrue(type instanceof Class);
Class clazz = (Class) type;
assertEquals(expectedClass, clazz);
}

genericInterface 变量的内容如下,接口返回类型虽然为 Type,实际类型为 ParameterizedType,因此可以强转。该泛型变量的原始类型、实际类型参数列表如下,实际都为 Class 类型,因此可以强转 (Class<?>) type

ParameterizedType_example

例子三

WildcardType 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test3() {
// java.util.List<?>
Method method = PersonMapper.class.getMethod("log", List.class);
Type genericParameterType = method.getGenericParameterTypes()[0];
assertTrue("The first parameter type of log method is instance of ParameterizedType",
genericParameterType instanceof ParameterizedType);
ParameterizedType parameterType = (ParameterizedType) genericParameterType;

// WildcardType
Type type = parameterType.getActualTypeArguments()[0];
assertTrue("The actual type argument of ParameterizedType is instance of WildcardType",
type instanceof WildcardType);
WildcardType wildcardType = (WildcardType) type;
assertEquals("No lower bounds exist", 0, wildcardType.getLowerBounds().length);
assertEquals("Only one upper bound exist", 1, wildcardType.getUpperBounds().length);
// 通配符默认上限类型为 Object
assertEquals("The upper bound is Object", Object.class, wildcardType.getUpperBounds()[0]);
}

Type 类型变量 genericParameterType 的内容如下,实际类型是 ParameterizedType,其 ? 通配符是 WildcardType

WildcardType_example

例子四

TypeVariable 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test4() {
Method method = BaseMapper.class.getMethod("getById", Serializable.class);

// TypeVariable
Type genericReturnType = method.getGenericReturnType();
assertTrue("Return type of method is instance of TypeVariable", genericReturnType instanceof TypeVariable);
TypeVariable typeVariable = (TypeVariable) genericReturnType;
assertEquals("First upper bound is Serializable", Serializable.class, typeVariable.getBounds()[0]);
ParameterizedType parameterizedType = (ParameterizedType) typeVariable.getBounds()[1];
assertEquals("Second upper bound is Comparable", Comparable.class, parameterizedType.getRawType());
}

Type 类型变量 genericReturnType 的内容如下,实际类型是 TypeVariable

TypeVariable_example

例子五

定义一个泛型工具类,用于获取 T.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class GenericsUtils {
/**
* 通过反射获得定义 Class 时声明的父类的泛型参数的类型
* @param clazz
* @return 返回第一个类型
*/
public static Class getSuperClassGenricType(Class clazz) {
return getSuperClassGenricType(clazz, 0);
}

/**
* 通过反射获得定义 Class 时声明的父类的泛型参数的类型
* @param clazz
* @param 返回某个下标的类型
*/
public static Class getSuperClassGenricType(Class clazz, int index)
throws IndexOutOfBoundsException {
Type genType = clazz.getGenericSuperclass();
if (!(genType instanceof ParameterizedType)) {
return Object.class;
}
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
if (index >= params.length || index < 0) {
return Object.class;
}
if (!(params[index] instanceof Class)) {
return Object.class;
}
return (Class) params[index];
}
}

参考

API:

https://www.baeldung.com/java-generics-vs-extends-object

Java Generic’s Wildcards

Java Reflection API 介绍

读懂 Java 类型(Type)系统

现代编程语言需要泛型 —— 排序例子

JDK 1.5 引入了注解,其主要用途如下:

  • 生成文档,通过代码里标识的元数据生成 javadoc 文档。
  • 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
  • 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
  • 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。

Annotation

首先,注解也是一种 class,所有注解都默认继承自通用接口:java.lang.annotation.Annotation

annotation

从上图可见,JDK 提供的元注解(meta annotation)如下:

1
2
3
4
5
6
@Native
@Documented
@Inherited
@Repeatable
@Target
@Retention

其中,@Native@Documented@Inherited 是一个“标记注解”,没有成员。

常用元注解的作用如下:

@Inherited

@Inherited 用于定义子类是否可继承父类定义的注解。仅针对 @Target(ElementType.TYPE) 类型的注解有效,并且仅针对 class 的继承,对 interface 的继承无效。注意:

  • 因此 interface 上标注的注解,无法被 JDK Proxy 动态代理生成的类所继承。
  • 也因此 Spring AOP 的切点表达式 @annotation(...) 无法切到基于 JDK Proxy 的动态代理类。

https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Inherited.html

Indicates that an annotation type is automatically inherited. If an Inherited meta-annotation is present on an annotation type declaration, and the user queries the annotation type on a class declaration, and the class declaration has no annotation for this type, then the class’s superclass will automatically be queried for the annotation type. This process will be repeated until an annotation for this type is found, or the top of the class hierarchy (Object) is reached. If no superclass has an annotation for this type, then the query will indicate that the class in question has no such annotation.

Note that this meta-annotation type has no effect if the annotated type is used to annotate anything other than a class. Note also that this meta-annotation only causes annotations to be inherited from superclasses; annotations on implemented interfaces have no effect.

@Target

@Target 用于定义注解能够被标注于源码的哪些位置。可用字段参考 ElementType 枚举。

JDK 8 扩展了注解的上下文,现在注解几乎可以加到任何地方:局部变量、泛型类、⽗类与接⼝的实现,就连⽅法的异常也能添加注解。

JDK 8 引入的这两个枚举如下:

  • ElementType.TYPE_PARAMETER 用于标注泛型的类型参数。
  • ElementType.TYPE_USE 用于标注各种类型。

extended_annotations_support

@Retention

@Retention 用于定义注解的生命周期。可用字段参考下面的 RetentionPolicy 枚举源码,常用的是 RUNTIME 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Annotation retention policy. The constants of this enumerated type
* describe the various policies for retaining annotations. They are used
* in conjunction with the {@link Retention} meta-annotation type to specify
* how long annotations are to be retained.
*
* @author Joshua Bloch
* @since 1.5
*/
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,

/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,

/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}

@Repeatable

JDK 8 引入了 @Repeatable,表示该注解是否可重复标注。配套引入的还有为 AnnotatedElement 接口新增了两个方法 getAnnotationsByTypegetDeclaredAnnotationsByType

AnnotatedElement

读取运行时(@Retention(RetentionPolicy.RUNTIME))的注解,需要用到反射 API。java.lang.reflect.AnnotatedElement 接口提供了一组方法,用于获取注解信息:

AnnotatedElement_methods

1
2
3
4
5
6
7
8
9
10
// 判断某个注解是否存在
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
// 获取注解(包括父类)
<T extends Annotation> T getAnnotation(Class<T>)
<T extends Annotation> T[] getAnnotationsByType(Class<T>)
Annotation[] getAnnotations()
// 获取注解(不包括父类)
<T extends Annotation> T getDeclaredAnnotation(Class<T>)
<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T>)
Annotation[] getDeclaredAnnotations()

由于下列类都实现了该接口,因此都拥有这些方法获取注解信息:

1
2
3
4
5
6
java.lang.Class
java.lang.Package
java.lang.reflect.Field
java.lang.reflect.Method
java.lang.reflect.Constructor
java.lang.reflect.Parameter

main_api

举个例子,看下哪些情况通过反射可以拿到注解(或拿不到):

注解如下:

1
2
3
4
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Check {}

例子一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Check
public class Person {
@Check
public void say() {}

@Check
public void say(String msg) {}
}

@ToString
public class Child extends Person {
public String childName = "Hello";

@Override
public void say() {}
}

public class Test {
@Test
public void test() {
ToString annotation = Child.class.getAnnotation(ToString.class);
assertNull("结果为 null 因为 @ToString 标注为 @Retention(RetentionPolicy.SOURCE)", annotation);

Check annotation1 = Child.class.getAnnotation(Check.class);
assertNotNull("结果不为 null 因为 @Check 标注为 @Retention(RetentionPolicy.RUNTIME)、@Inherited 并且 @Check 被标注在父类上,可以被子类继承", annotation1);

Check annotation2 = Child.class.getDeclaredAnnotation(Check.class);
assertNull("结果为 null 因为 getDeclaredAnnotation 方法无法拿到父类注解", annotation2);

Method sayString = Child.class.getMethod("say", String.class);
Check annotation3 = sayString.getAnnotation(Check.class);
assertNotNull("结果不为 null 因为 say 方法未被子类重写,被完整继承下来", annotation3);

Method say = Child.class.getMethod("say");
Check annotation4 = say.getAnnotation(Check.class);
assertNull("结果为 null 因为 say 方法被子类重写了", annotation4);
}
}

例子二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Check
public interface Sayable {
@Check
void say();
}

public class Student implements Sayable {
@Override
public void say() {}
}

public class Test {
@Test
public void test() {
Check annotation5 = Student.class.getAnnotation(Check.class);
assertNull("结果为 null 因为接口上的注解无法被子类继承", annotation5);

Check annotation6 = Student.class.getMethod("say").getAnnotation(Check.class);
assertNull("结果为 null", annotation6);
}
}

AnnotatedType

JDK 8 引入了 java.lang.reflect.AnnotatedType

AnnotatedType

类、字段、方法、构造方法相应增加了一组方法用于获取 AnnotatedType

  • Class

    1
    2
    AnnotatedType getAnnotatedSuperclass()
    AnnotatedType[] getAnnotatedInterfaces()
  • Field

    1
    AnnotatedType getAnnotatedType()
  • Method

    1
    2
    3
    4
    AnnotatedType getAnnotatedReturnType()
    AnnotatedType[] getAnnotatedParameterTypes()
    AnnotatedType[] getAnnotatedExceptionTypes()
    AnnotatedType getAnnotatedReceiverType()
  • Constructor

    1
    2
    3
    4
    AnnotatedType getAnnotatedReturnType()
    AnnotatedType[] getAnnotatedParameterTypes()
    AnnotatedType[] getAnnotatedExceptionTypes()
    AnnotatedType getAnnotatedReceiverType()

参考

深入理解java注解的实现原理

API:

什么是反射?

反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。

主要类:

1
2
3
4
5
6
7
8
9
java.lang.Class
java.lang.Package
java.lang.reflect.Field
java.lang.reflect.Method
java.lang.reflect.Constructor
// JDK 1.8 新引入,通过该类的 getName 方法能在运行时得到参数的名称(前提是通过 -parameters 指定编译器在编译的时候将参数名编译进去)
java.lang.reflect.Parameter

java.lang.reflect.Array

工具类:

1
java.lang.reflect.Modifier

继承关系:

main_api

Class 类

如何获取一个 classClass 实例?有几种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 方法一:直接通过一个 class 的静态变量 class 获取
Class<String> aClass = String.class;

// 方法二:通过实例变量提供的 getClass() 方法获取
Class<? extends String> aClass2 = "Hello".getClass();

// 方法三:通过 class 的完整类名获取,底层调用的是应用类加载器 Application ClassLoader (sun.misc.Launcher$AppClassLoader)
Class<?> aClass3 = Class.forName("java.lang.String");

// 方法四:通过自定义 ClassLoader 获取
URLClassLoader classLoader = new URLClassLoader(new URL[] {new URL("file:/c:/")});
Class<?> aClass4 = classLoader.loadClass("java.lang.String");

// 获取父类
Class<? super String> superclass = String.class.getSuperclass(); // class java.lang.Object
// 获取外部类
Class<?> enclosingClass = Map.Entry.class.getEnclosingClass(); // interface java.util.Map
// 获取内部类
Class<?>[] classes = HashMap.class.getClasses(); // 2 个内部类
Class<?>[] declaredClasses = HashMap.class.getDeclaredClasses(); // 13 个内部类

由于 Class 实例在 JVM 中是唯一的,因此,上述方法获取的 Class 实例都是同一个实例。可以用 == 比较两个 Class 实例求证:

1
2
3
4
// true
assertTrue(aClass == aClass2);
assertTrue(aClass == aClass3);
assertTrue(aClass2 == aClass3);

拿到 Class 实例后,可以通过下列方法获取字段、方法、构造方法、注解,以进行后续操作。方法名以 getDeclared 开头的表示仅获取当前类的信息(不包括父类):

Member Class API Param type Return type Inherited members Private members
Class getDeclaredClasses() Array N Y
getClasses() Array Y N
Field getDeclaredField() String Single N Y
getField() String Single Y java.lang.NoSuchFieldException
getDeclaredFields() Array N Y
getFields() Array Y N
Method getDeclaredMethod() String, Class<?>... Single N Y
getMethod() String, Class<?>... Single Y java.lang.NoSuchMethodException
getDeclaredMethods() Array N Y
getMethods() Array Y N
Constructor getDeclaredConstructor() Class<?>... Single N/A Y
getConstructor() Class<?>... Single N/A java.lang.NoSuchMethodException
getDeclaredConstructors() Array N/A Y
getConstructors() Array N/A N
Annotation getDeclaredAnnotation() Class<T> Single N N/A
getAnnotation() Class<T> Single Y N/A
getDeclaredAnnotationsByType() Class<T> Array N N/A
getAnnotationsByType() Class<T> Array Y N/A
getDeclaredAnnotations() Array N N/A
getAnnotations() Array Y N/A

字段

一个 Field 对象包含了一个字段的所有信息,例如:

1
2
3
4
5
6
// 字段名称
String getName()
// 字段类型
Class<?> getType()
// 字段的修饰符,返回值是一个int,不同的 bit 表示不同的含义。使用 Modifier 工具类进行解析
int getModifiers()

String 类的 value 字段为例,它的定义是:

1
2
3
public final class String {
private final byte[] value;
}

用反射获取该字段的信息,代码如下:

1
2
3
4
5
6
7
8
9
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false

获取或设置字段值,使用如下方法。注意如果试图获取非 public 字段,将抛出异常 java.lang.IllegalAccessException,解决办法是先设置 setAccessible(true)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取字段值
Object get(Object obj)
boolean getBoolean(Object obj)
byte getByte(Object obj)
char getChar(Object obj)
short getShort(Object obj)
int getInt(Object obj)
long getLong(Object obj)
float getFloat(Object obj)
double getDouble(Object obj)

// 设置字段值
void set(Object obj, Object value)
void setBoolean(Object obj, boolean z)
void setByte(Object obj, byte b)
void setChar(Object obj, char c)
void setShort(Object obj, short s)
void setInt(Object obj, int i)
void setLong(Object obj, long l)
void setFloat(Object obj, float f)
void setDouble(Object obj, double d)

方法

一个 Method 对象包含一个方法的所有信息,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方法名称
String getName()
// 方法参数个数
int getParameterCount()
// 方法参数类型
Class<?>[] getParameterTypes()
// 方法参数注解
Annotation[][] getParameterAnnotations()
// 方法返回值类型
Class<?> getReturnType()
// 方法异常类型
Class<?>[] getExceptionTypes()
// 方法的修饰符,返回值是一个int,不同的 bit 表示不同的含义。使用 Modifier 工具类进行解析
int getModifiers()

方法调用:

1
2
3
4
// 第一个参数为指定的实例对象。
// 如果是静态方法,可传 null。
// 如果是非 public 方法,需先设置 setAccessible(true)
Object invoke(Object obj, Object... args)

构造方法

创建新实例的几种方式:

1
2
3
4
5
6
7
8
9
10
11
// 方式一:通过 new 操作符
Person p = new Person();

// 方式二:通过反射,调用 Class 提供的 newInstance() 方法。局限是只能调用其 public 无参构造方法
Person p = Person.class.newInstance();

// 方式三:通过反射,调用 Constructor 提供的 newInstance() 方法。如果是非 public 方法,需先设置 setAccessible(true)
// 这里通过 private Person(String name) 构造新实例
Constructor<Person> constructor = Person.class.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
Person peter = constructor.newInstance("Peter");

继承关系

通过以下方法获取父类或已实现接口:

1
2
3
4
5
// 获取父类(Object 的父类是 null,其他任何非 interface 的 Class 都必定存在一个父类类型)
Class<? super T> getSuperclass()

// 获取已实现接口(只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型)
Class<?>[] getInterfaces()

isInstance

使用 instanceof 操作符或者 Class#isInstance 方法,可以判断一个实例的继承关系:

1
2
3
4
5
6
7
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

boolean isDouble = Double.class.isInstance(n); // false

cast

如果 instanceoftrue,可以使用以下方法对实例进行强制类型转换:

1
2
Number num1 = (Number) n;
Number num2 = Number.class.cast(n);

isAssignableFrom

如果是两个 Class 实例,要判断向上转型是否成立,可以调用 Class#isAssignableFrom 方法:

1
2
3
4
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer

参考

https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html

https://docs.oracle.com/javase/tutorial/reflect/index.html

JDK1.8 反射包新增了Parameter

https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512

API:

Java SPI

SPI 全称 Service Provider Interface,Java 1.6 引入,是 Java 在语言层面为我们提供了一种方便地创建可扩展应用的途径。SPI 提供了一种 JVM 级别的服务发现机制,我们只需要按照 SPI 的要求,在 jar 包中进行适当的配置,JVM 就会在运行时通过懒加载,帮我们找到所需的服务并加载。如果我们一直不使用某个服务,那么它不会被加载,一定程度上避免了资源的浪费。

整体机制图如下:

Java SPI

使用例子

以 JDBC 为例,标准服务接口com.mysql.jdbc.Driver

MySQL 作为服务提供方,以 mysql-connector-java 5.1.44 为例,按规范要求其 META-INF/services/java.sql.Driver 配置文件中声明了两个实现类,如下:

1
2
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

当类加载器载入 java.sql.DriverManager 类时,会执行其静态代码块,从而执行其中的 SPI 代码加载 JDBC Driver 实现,源码如下,详见:《Java 数据持久化系列(一)JDBC Driver 驱动程序总结》

1
2
3
4
5
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
Driver driver = driversIterator.next();
}

流程如下:

spi_flow_diagram

源码解析

ServiceLoader 的结构如下:

ServiceLoader

其成员变量如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class ServiceLoader<S>
implements Iterable<S>
{

// 类加载器加载配置文件时 所用的固定目录
private static final String PREFIX = "META-INF/services/";

// 代表被加载的类或者接口
// The class or interface representing the service being loaded
private final Class<S> service;

// 用于定位,加载和实例化 providers 的类加载器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// 创建 ServiceLoader 时采用的访问控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// 缓存 providers,按实例化的顺序排列
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 懒查找迭代器
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
}

SPI 的核心在于内部类 LazyIterator,承担了以下职责:

  1. 加载配置文件,解析、验证其内容
  2. 加载类
  3. 反射构造实例

核心源码及注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
    // Private inner class implementing fully-lazy provider lookup
//
private class LazyIterator
implements Iterator<S>
{

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private boolean hasNextService() {
if (nextName != null) {
return true;
}
// 判断是否首次使用
if (configs == null) {
try {
// 本例中值为 META-INF/services/java.sql.Driver
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 使用类加载器从类路径中加载文件:META-INF/services/java.sql.Driver,如果多个 jar 包都存在该文件则结果为多个 URL 实例
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 依次解析 URL,获取 URL 内容的迭代器
pending = parse(service, configs.nextElement());
}
// 依次获取 URL 内容,例如第一条为 com.mysql.jdbc.Driver
nextName = pending.next();
return true;
}

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 使用指定的类加载器查找并加载类:com.mysql.jdbc.Driver
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 通过反射,调用 com.mysql.jdbc.Driver 的 public 无参构造方法创建 Object 实例对象,并强制转换为 interface java.sql.Driver 类型
S p = service.cast(c.newInstance());
// 塞入缓存,key 为 com.mysql.jdbc.Driver 字符串,value 是对应的实例对象
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}

...

}

使用场景

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。这种动态加载机制的使用场景如下:

  • JDBC Driver 驱动程序管理类 java.sql.DriverManager。详见:JDBC Driver 驱动程序总结
  • JSR-303 Bean Validation 的 javax.validation.Validation
  • 日志门面接口实现类加载,SLF4J 加载不同提供商的日志实现类。
  • Spring
    • 对 servlet3.0 规范对 ServletContainerInitializer 的实现
    • 自动类型转换 Type Conversion SPI (Converter SPI、Formatter SPI) 等
    • Spring Factories 机制(SpringFactoriesLoader
  • Dubbo 通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。详见:http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html

对比总结

下面总结下这几个加载类:

  • Java java.util.ServiceLoader
  • Spring org.springframework.core.io.support.SpringFactoriesLoader
  • Dubbo com.alibaba.dubbo.common.extension.ExtensionLoader
Java SPI Spring Factories Dubbo SPI
加载类 ServiceLoader SpringFactoriesLoader ExtensionLoader
加载文件 META-INF/services/接口全限定名 META-INF/spring.factories META-INF/dubbo
文件内容 接口实现类,多值以换行分隔 通过键值对方式(key=value)配置,多值以逗号分隔 通过键值对方式(key=value)配置,支持按需加载接口实现类
接口注解 / / @SPI

参考

https://www.jianshu.com/p/46b42f7f593c

JDK/Dubbo/Spring 三种 SPI 机制,谁更好?

各个类加载器的类路径(Classpath)

Java Launcher(即 java 命令)启动 Java 虚拟机时,各个类加载器按以下顺序类路径加载类:

Class Loader

三个类加载器使用的类路径,从以下系统属性中获取,可以通过 System.getProperty(...) 获取查看:

1
2
3
4
5
6
// Bootstrap ClassLoader
String property1 = System.getProperty("sun.boot.class.path");
// Extension ClassLoader
String property2 = System.getProperty("java.ext.dirs");
// Application ClassLoader
String property3 = System.getProperty("java.class.path");

下面详细介绍各个类加载器:

Bootstrap ClassLoader

负责加载构成 Java 平台的基础类(Bootstrap classes),位于 $JAVA_HOME/jre/lib 目录,包括 rt.jar 和其它几个重要 jar 文件中的类。这些基础类包括 Java 类库(Java Class Library (JCL))的公共类,以及此库可用的私有类。

几乎所有的 Java 类库(JCL) 都存储在一个名为“rt.jar”的 Java archive (jar) 归档文件中,该文件随 JREJDK 发行版一起提供。Java 类库(rt.jar)位于默认的 bootstrap classpath($JAVA_HOME/jre/lib)下,不必出现在为应用程序声明的 classpath 中。JRE 会使用引导类加载器(bootstrap class loader)找到 JCL。

Java 9 的模块系统目前已将这个单块的 rt.jar jar 包拆分并模块化。

Extension ClassLoader

负责加载扩展 Java 平台的扩展类(Extension classes)。位于 $JAVA_HOME/jre/lib/ext 扩展目录的每个 .jar 文件都被假定为扩展文件,并使用 Java Extension Framework 扩展机制加载。

sun.misc.Launcher$ExtClassLoader 执行过程中,URLClassPath 的值如下:

ext_classpath

例如,可以将 MySQL 厂商驱动程序 mysql-connector-java 放到该扩展目录中。

Application ClassLoader

负责加载由开发人员和第三方定义的未利用 Java 扩展机制的用户类(User classes)。

https://en.wikipedia.org/wiki/Classpath

Classpath is a parameter in the Java Virtual Machine or the Java compiler that specifies the location of user-defined classes and packages. The parameter may be set either on the command-line, or through an environment variable.

为了查找 User classes,Java Launcher(即 java 命令)将引用 User Classpath —— 一个包含了用户定义的类文件的目录、jar 包和 zip 包列表。Java Launcher(即 java 命令)将这个 User Classpath 字符串放到 java.class.path 系统属性中。该值的来源及优先级如下:

  • 默认值“ .”,表示当前工作目录下的所有类文件(如果在 jar 包中,则位于其下)。

  • CLASSPATH 环境变量,覆盖默认值。

    1
    2
    3
    4
    5
    # 查看 CLASSPATH 环境变量
    echo $CLASSPATH

    # 设置 CLASSPATH 环境变量
    set CLASSPATH=
  • -cp-classpath 命令行选项,覆盖默认值以及 CLASSPATH 环境变量。

    A semicolon (;) separated list of directories, JAR archives, and ZIP archives to search for class files.

    Specifying classpath overrides any setting of the CLASSPATH environment variable. If the classpath option isn’t used and CLASSPATH isn’t set, then the user class path consists of the current directory (.).

  • -jar 选项指定的 jar 包,它覆盖上述所有值。如果使用此选项,则所有用户类必须来自指定的 jar 包。

    Executes a program encapsulated in a JAR file. The jarfile argument is the name of a JAR file with a manifest that contains a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that serves as your application’s starting point.

    When you use -jar, the specified JAR file is the source of all user classes, and other class path settings are ignored.

    If you’re using JAR files, then see jar.

参考:Setting the Class Path on Windows or Unix

⭐️ 注意点 1:User Classpath 使用字符串格式指定,路径不要含有空格,否则转义为 %20 之后会报错,例如:

1
java.lang.RuntimeException: Cannot resolve classpath entry: java.lang.RuntimeException: Cannot resolve classpath entry: D:\myprogram\mybatis%20tool\mybatis-generator-gui-0.8.4\target\classes\lib\mysql-connector-java-5.1.38.jar

⭐️ 注意点 2:每个路径使用以下方式进行分隔:

  • 在类 Unix 系统中,以冒号(:)分隔
  • 在 Windows 系统中,以分号(;)分隔

⭐️ 注意点 3:类文件具有反映“类的完全限定名称(class’s fully-qualified name)”的子路径名。例如 com.mypackage.MyClass

  • 如果类存储在 /myclasses 目录,则 /myclasses 必须在 User Classpath 中,并且类文件的完整路径必须为 /myclasses/com/mypackage/MyClass.class
  • 如果类存储在 myclasses.jar 中,则 myclasses.jar 必须在 User Classpath 中,并且类文件必须存储 myclasses.jar/com/mypackage/MyClass.class

下面来看几个例子,总结如下:

CLASSPATH 例子

Unpacked Classes

假设我们有一个名为主类:HelloWorld,存储在 D:\myprogram 目录下:

1
2
3
4
5
6
7
8
9
D:\myprogram\
|
---> org\
|
---> mypackage\
|
---> HelloWorld.class
---> SupportClass.class
---> UtilClass.class

查看 Windows 下 CLASSPATH 环境变量:

1
2
$ echo $CLASSPATH
.;E:\Developer\Java\jdk1.8.0_191\lib;

由于 CLASSPATH 环境变量默认包含当前目录(.),这意味着当我们的工作目录为 D:\myprogram\ 时,我们不需要显式指定 CLASSPATH

1
2
3
$ cd /D/myprogram
$ java org.mypackage.HelloWorld
hello world

否则,需要使用 -classpath 参数显式指定如下:

1
2
$ java -classpath D:\myprogram org.mypackage.HelloWorld
hello world

总结,设置 Classpath 的两种方式:

  • 使用 CLASSPATH 环境变量
  • 使用 -cp-classpath 命令行选项

JAR files

  • 单个 jar 包:使用绝对路径指定具体某个 jar 包
  • 多个 jar 包:使用绝对路径加上通配符 *

META-INF/MANIFEST.MF

如果程序已经打成 jar 包,需要使用清单文件指定入口类及 CLASSPATH,并使用 java -jar 命令启动。例如 Tomcat 的 bootstrap.jar 引导包:

1
2
3
......
Main-Class: org.apache.catalina.startup.Bootstrap
Class-Path: commons-daemon.jar

例子

Spring Boot 配置文件

Spring Boot will automatically find and load application.properties and application.yaml files from the following locations when your application starts:

  1. The classpath root
  2. The classpath /config package

The list is ordered by precedence (with values from lower items overriding earlier ones).

参考:External Application Properties

IDEA 如何查找类?

首先,为 IDEA 平台配置上你所拥有的 JDK 及路径:

Platform Settings SDKs

然后,为你的项目指定一个默认 SDK:

Project SDK

搞掂之后,IDEA 会为项目载入指定版本的 SDK,将基础目录 jre/lib/ 的 Bootstrap classes 和扩展目录 jre/lib/ext/ 的 Extension classes 加入 classpath:

External Libraries

参考

https://docs.oracle.com/en/java/javase/11/tools/java.html

https://docs.oracle.com/en/java/javase/11/tools/jar.html

1
2
3
4
5
# Lists the table of contents for the archive.
$ jar tf jar-file

# [Extracting the Contents of a JAR File](https://docs.oracle.com/javase/tutorial/deployment/jar/unpack.html)
$ jar xf jar-file [archived-file(s)]

Setting the Class Path on Windows or Unix

http://cr.openjdk.java.net/~mr/jigsaw/ea/module-summary.html

Wikipedia

类的加载过程

.java 源文件的从编译、加载、到对象创建的过程如下:

classloaoder_process

  • 首先,.java 源文件经过 javac 编译后生成 .class 类文件(内含字节码)。
  • 然后,通过 jar 命令或其它构建工具(如 Maven、Gradle)打包生成可运行的 jar 包。
  • 最终,通过 java -jar 命令运行 jar 包,执行其中清单文件(META-INF/MANIFEST.MF)中通过 Main-Class 指定的入口类的 main 方法以启动程序,并按照其 Class-Path 设置类路径。

从这里开始,就需要使用到类加载器将入口类(Main-Class)加载到 JVM。入口类在使用过程中如果使用到其它类,会根据类路径查找类文件并逐一加载。因此, jar 包中的类、及类路径中指定的类并不是一次性全部加载到 JVM 内存,而是使用到时才动态加载。可以指定启动参数 -verbose:class 输出类加载日志进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Flyable {
void fly(String param);
}

public class Bird implements Flyable {
@Override
public void fly(String param) {}
}

public class Test {
public static void main(String[] args) {
System.out.println("===============");
Bird bird = new Bird();
}
}

类加载日志输出如下:

1
2
3
4
5
6
7
8
9
10
11
[Opened D:\tool\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Object from D:\tool\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.io.Serializable from D:\tool\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Comparable from D:\tool\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from D:\tool\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.String from D:\tool\jdk1.8.0_131\jre\lib\rt.jar]
...
[Loaded Test from file:/D:/workspaces/project-test/target/classes/]
===============
[Loaded com.github.proxy.Flyable from file:/D:/workspaces/project-test/target/classes/]
[Loaded com.github.proxy.Bird from file:/D:/workspaces/project-test/target/classes/]

类的生命周期

类从加载到 JVM 内存到被从内存中释放,经历的生命周期如下:

lifecycle_of_class

  • 加载阶段:包括根据类或接口的二进制名称(binary name)查找其字节码文件(可能是之前由 javac 编译器源代码编译出的字节码文件;或者是通过动态编译,例如 JDK 动态代理使用的 sun.misc.ProxyGenerator 工具类编译出的字节码文件 $Proxy0.class),并构造成一个表示该类或接口的 Class 类对象。加载阶段由类加载器 ClassLoader 及其子类负责实现:findClass 方法负责查找字节码文件,defineClass 方法负责构造成 Class 对象。
  • 验证阶段:确保类或接口的二进制代码在结构上是正确的。类文件校验器(Class File Verifier)会进行以下四类校验:
    • 文件完整性校验(File Integrity Check):第一步也是最简单的一步是检查类文件的结构。 它确保类文件具有适当的签名(前四个字节为魔数 0xCAFEBABE),并且类文件中的每个结构都具有适当的长度。它检查类文件本身不能内容过长或过短,并且常量池仅包含有效条目。当然,类文件的长度可能有所不同,但是每个结构(例如常量池)的长度作为文件规范的一部分都包含其中。
    • 类完整性校验(Class Integrity Check):
      • 该类具有父类(除非该类是 Object)。
      • 该父类不是一个 final 类,并且该子类不会尝试覆盖其父类中的 final 方法。
      • 常量池的条目格式正确,并且所有方法和字段引用均具有合法的名称和签名。
    • 字节码完整性校验(Bytecode Integrity Check):执行字节码校验器(Bytecode Verifier),检查每个字节码以确定代码在运行时的实际行为,包括对方法参数和字节码操作数的数据流分析,堆栈检查和静态类型检查。是整个验证阶段中最复杂的一步。
    • 运行时完整性校验(Runtime Integrity Check)
  • 准备阶段:包括为类或接口创建 static 静态字段(包括类变量和常量),并赋默认值。
  • 解析阶段:包括检查符号引用是否正确、将符号引用替换为直接引用。
  • 初始化阶段:
    • 类的初始化阶段包括执行 static 静态代码块、为 static 静态字段(变量)赋值。
    • 接口的初始化阶段包括为字段(接口字段默认为 public static final 常量)赋值。

各个步骤可以详见官方文档 “Execution” chapter of The Java™ Language Specification

execution

类加载器源码解析

Java 虚拟机中的类加载器(ClassLoader)负责加载来自文件系统、网络或其它来源的类文件。ClassLoader 是一个抽象类,其继承结构如下:

ClassLoader

类加载后,每个 Class 对象都包含一个定义它的类加载器的引用。可以通过以下方式查看:

1
2
3
4
5
6
7
8
9
10
11
public class Test {
@Test
public void test() {
// 结果为 null,因为启动类加载器为 C++ 编写
ClassLoader bootstrapClassLoader = java.lang.String.class.getClassLoader();
// sun.misc.Launcher$ExtClassLoader
ClassLoader extClassLoader = com.sun.crypto.provider.DESKeyFactory.class.getClassLoader();
// sun.misc.Launcher$AppClassLoader
ClassLoader appClassLoader = Test.class.getClassLoader();
}
}

ClassLoader 的核心方法如下:

类加载

loadClass (双亲委派)

loadClass 方法使用二进制名称(binary name)、通过“双亲委派模型(Delegation Model)”自顶向下尝试加载类,如下图所示:

classloader_hierarchy

⭐️ 这种设计的好处体现在:

  • 沙箱安全机制:例如自己写的 java.lang.String 类不会被加载,否则在 defineClass 方法这一步会报错,防止恶意代码污染,核心 API 库被随意篡改。核心 API 库只能由 Bootstrap ClassLoader$JAVA_HOME/jre/lib 目录进行加载。
  • 避免类的重复加载:当父加载器已经加载了该类时,就没有必要再加载一次,保证被加载类的唯一性。

类加载过程如下:

  1. 调用自身的 findLoadedClass(String) 方法以检查类是否已经被加载。
  2. 如未,则递归调用父加载器的 loadClass 方法(如果父加载器为 null,则使用虚拟机内置的 Bootstrap ClassLoader)。
  3. 如果父加载器都加载不到,则调用自身的 findClass(String) 方法查找类。
  4. 如果上述步骤找到了类,并且 resolve 标记为 true,则在目标 Class 对象上调用 resolveClass(Class) 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经被加载
Class<?> c = findLoadedClass(name);
// 未被加载的情况
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果父加载器不为 null,则委托父加载器去加载类
if (parent != null) {
// 调用父加载器的 loadClass 方法,委托其去加载类
c = parent.loadClass(name, false);
}
// 如果父加载器为 null,则委托 Bootstrap ClassLoader 去加载类
else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

// 如果父加载器都加载不到,则调用自身的 findClass 方法查找类
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 如果上述步骤找到了类,并且 resolve 标记为 true,则在目标 Class 对象上调用 resolveClass(Class) 方法,进入“连接(Linking)”阶段(详见官方文档)
if (resolve) {
resolveClass(c);
}
return c;
}
}

加载到的 Class 可以通过反射的方式实例化对象:

1
2
Class<?> clazz = classLoader.loadClass("com.github.parent.HelloWorld");
Object instance = clazz.newInstance();

findClass

实现“加载阶段(Loading)”的查找功能。该方法应当被子类覆盖重写,用于使用指定的二进制名称(binary name)查找类或接口的字节码文件。ClassLoader 的默认实现是抛出一个 ClassNotFoundException

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

ClassLoader

可以通过继承 ClassLoader 类,实现一个自定义的类加载器(参考 NetworkClassLoader 例子)。通过重写 findClass 方法可以从以下途径查找类文件:

  • 从 ZIP 包中读取,最常见,JAR,WAR,EAR 格式的基础。
  • 从网络中获取,典型场景是 Applet。
  • 运行时计算生成,典型情景是 JDK 动态代理技术。
  • 从其它文件中生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Servlet Class 类。

也可以使用自带的 URLClassLoader 从本地路径(file:/)或网络路径(http://)加载类文件,示例如下:

1
2
3
4
5
6
7
8
// 从 E 盘中加载类文件
URLClassLoader classLoader = new URLClassLoader(new URL[] {new URL("file:/e:/")});
// 从 localhost 中加载类文件
// URLClassLoader classLoader = new URLClassLoader(new URL[] {new URL("http://localhost/testfile/")});
Class<?> clazz = classLoader.loadClass("com.github.parent.HelloWorld");
Object instance = clazz.newInstance();
// java.net.URLClassLoader
String name = instance.getClass().getClassLoader().getClass().getName();

下面是子类 URLClassLoader 的默认实现,源码及注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 将二进制名称替换为文件路径(类似全限定名),例如:com.github.HelloWorld > com/github/HelloWorld.class
String path = name.replace('.', '/').concat(".class");
// 从 URLClassPath 对象中查找文件
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// 如果找到文件,则构造为 Class 类实例
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}

可见,如果找不到类(result == null),最底层的 ClassLoader 将抛出 ClassNotFoundException

类可以按需动态加载到内存,这是 Java 的一大特点,也称为运行时绑定,或动态绑定。

defineClass

实现“加载阶段(Loading)”的构造功能。

ClassLoader 提供了四个 defineClass 方法可供自定义类加载器时使用,如下图。其中,第二个方法最常使用:defineClass(String name, byte[] b, int off, int len)

defineClass

其调用的底层源码如下,会调用 preDefineClasspostDefineClass 进行预处理和后置处理:

1
2
3
4
5
6
7
8
9
10
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

如果自定义类加载器打破了双亲委派模型,然后还去加载核心 API 库,例如自己伪造一个 java.lang.String,会报错如下:

1
2
3
4
5
6
7
8
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.github.MyClassLoader.findClass(...)
at com.github.MyClassLoader.loadClass(...)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
...

这是由于 preDefineClass 预处理方法进行了二进制名称的前缀校验,源码如下,关键判断 name.startsWith("java.") 抛出异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* Determine protection domain, and check that:
- not define java.* class,
- signer of this class matches signers for the rest of the classes in
package.
*/
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);

// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
// 关键判断
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}

if (name != null) checkCerts(name, pd.getCodeSource());

return pd;
}

resolveClass

进入类加载的“连接(Linking)”阶段(详见官方文档 “Execution” chapter of The Java™ Language Specification)。

资源加载

使用场景:例如 Spring Factories 机制中 SpringFactoriesLoader 加载类路径下的文件:

1
classLoader.getResources("META-INF/spring.factories")

文件加载后,通过 key-value 的方式读取指定 key,并以反射的方式实例化指定的类型。

getResource

查找指定名称的资源(图像、音频、文本等)。资源的名称是用“/”分隔的路径名,用于标识资源。
该方法首先递归调用父加载器查找资源;如果父加载器为 null,则使用虚拟机内置的启动类加载器(Bootstrap ClassLoader)。如果父加载器查找失败,则调用自身的 findResource(String) 查找资源。整个资源加载过程仍然为“双亲委派模型(Delegation Model)”:

1
2
3
4
5
6
7
8
9
10
11
12
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}

findResource

查找指定名称的资源。类加载器实现应当重写此方法以指定在何处查找资源。默认返回 null

1
2
3
protected URL findResource(String name) {
return null;
}

下面是子类 URLClassLoader 的默认实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
public URL findResource(final String name) {
/*
* The same restriction to finding classes applies to resources
*/
URL url = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
return ucp.findResource(name, true);
}
}, acc);

return url != null ? ucp.checkURL(url) : null;
}

参考

JavaDoc

其它: