封面来源:碧蓝航线 远汇点作战 活动CG

本文涉及的代码:springboot-study/LambdaSafeTest.java

0. 背景

前段时间看到同事在代码中使用到了 LambdaSafe,网上搜了一圈也没看到有文章将这东西讲明白的。

LambdaSafe 是 SpringBoot 提供的一个工具类,根据其类注释可知:使用 LambdaSafe 可以以一种 安全的 方式来执行 Lambda,主要用于规避由于泛型擦除可能导致的 ClassCastException

LambdaSafe 提供了两个可被使用的静态方法,这些方法都有很多个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static <C, A> Callback<C, A> callback(Class<C> callbackType, 
C callbackInstance,
A argument,
Object... additionalArguments) {
// --snip--
}

public static <C, A> Callbacks<C, A> callbacks(Class<C> callbackType,
Collection<? extends C> callbackInstances,
A argument,
Object... additionalArguments) {
// --snip--
}

其中 callbacks() 方法是 callback() 方法的复数版本,是 Spring 中常用设计模式 —— 组合模式的体现,因此重点放在 callback() 方法上。

callback() 方法接收的参数并没有比 callbacks() 方法少,无论是参数名称,还是方法注释,都能看到一个词 —— callback

callback 被翻译为 回调,尽管这个翻译被无数人诟病,但翻译的艺术显然不是本文重点。理解回调不仅可以提升对 Java 中 Lambda 表达式的理解,对这两个方法的使用方式也会迎刃而解。

1. Java 中的回调

1.1 回调函数

有两个方法 sum()subtract(),提供了对给定整型参数进行加法、减法的运算:

1
2
3
4
5
6
7
private int sum(int a, int b) {
return a + b;
}

private int subtract(int a, int b) {
return a - b;
}

在计算出结果后,要求输出计算结果,因此又有:

1
2
3
private void print(int result) {
System.out.println("计算结果是: " + result);
}

可以像这样调用方法:

1
2
3
4
5
@Test
public void testCallbackFunction() {
print(sum(1, 2));
print(subtract(2, 1));
}

运行后,控制台打印出:

计算结果是: 3
计算结果是: 1

现在需要乘法运算的需求,实现乘法运算过程中发现,这些运算都是接收两个整型参数,然后返回运算结果,根据面向接口编程的思想,可以将运算这个操作进行抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Calculator {
double calculator(int a, int b);
}

static class Multiply implements Calculator {
@Override
public double calculator(int a, int b) {
return a * b;
}
}

private void print(Calculator calculator, int a, int b) {
System.out.println("计算结果是: " + calculator.calculator(a, b));
}

最终可以这样调用:

1
2
3
4
5
6
@Test
public void testCallbackFunction() {
// --snip--

print(new Multiply(), 2, 3);
}

运行后,控制台打印出:

计算结果是: 6.0

如果需求又发生了变化,现在需要计算两个数的平均值,又新增 Calculator 接口的实现类?

这并不是最优解,两个整型数据的运算居然需要一个类?如果后续又新增了需求呢?

为了应对多变的需求,防止出现类爆炸,可以使用 匿名内部类 来解决:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testCallbackFunction() {
// --snip--

print(new Calculator() {
@Override
public double calculator(int a, int b) {
return (a + b) / 2.0;
}
}, 2, 2);
}

在匿名内部类中,重写了父类的方法,使其能够满足多变的需求,但在 使用时并不关心实现类,而是关心实现的方法, 也就是上述代码中的 new Calculator() {} 并没有实际意义,真正有用的是 (a + b) / 2.0

在 Java8 之前,上述代码是不能简化的,Java8 横空出世后,可以使用 Lambda 表达式来实现:

1
2
3
4
5
6
7
@Test
public void testCallbackFunction() {
// --snip--

Calculator calculator = (a, b) -> (double) a / b;
print(calculator, 6, 2);
}

(a, b) -> (double) a / b 这样的写法可以理解为对方法的实现,换而言之,可以把它理解为一个方法,现在可以 将一个方法作为参数传入到另一个方法中,方法成为一等公民!

在 JavaScript 中,函数作为参数是再常见不过了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function sum(a, b) {
return a + b;
}

function subtract(a, b) {
return a - b;
}

function print_with_fun(fun, a, b) {
console.log("计算结果是: " + fun(a, b));
}

print_with_fun(sum, 1, 2);
print_with_fun(subtract, 2, 1);

Java8 引入 Lambda 表达式后,使用 Java 的方式也能编写出类似代码(尽管仍然不够简洁,但相比匿名内部类还是好许多),这对 Java 来说是跨时代的。

像这种 将 A 函数作为 B 函数的参数,并在 B 函数内部调用 A 函数,此时的 A 函数就被称为回调函数,而执行回调函数时就是回调。

在 Java 中,Lambda 表达式就是回调函数的一种体现。

1.2 延迟执行

既然说到 Lambda 表达式了,就补充下 Lambda 表达式的一个好处:延迟执行

方法 printLog() 接收一个字符串类型的参数,随机打印出接收的字符串:

1
2
3
4
5
6
7
private void printLog(String log) {
int i = new Random().nextInt(10);
// 随机数大于 5 才打印
if (i > 5) {
System.out.println(log);
}
}

接收的字符串参数可能是其他方法的返回值,更有可能是多个方法返回值的拼接:

1
2
3
4
5
6
7
private String aInfo() {
return "A";
}

private String bInfo() {
return "B";
}
1
2
3
4
5
@Test
public void testDelayExecute() {
// 对信息进行拼接作为日志信息
printLog(aInfo() + "-" + bInfo());
}

如果随机数大于 5,那么控制台会打印出:

A-B

这样的调用存在一个问题: 无论随机数是多少,都会执行字符串的拼接。

可以对 printLog() 方法进行修改,使其接收一个 Supplier<String> 类型的参数,整体逻辑不变:

1
2
3
4
5
6
private void printLog(Supplier<String> supplier) {
int i = new Random().nextInt(10);
if (i > 5) {
System.out.println(supplier.get());
}
}

对其的调用修改为:

1
2
3
4
5
6
7
@Test
public void testDelayExecute() {
// --snip--

// 无论是否需要打印,都会进行字符串的拼接,这又何尝不是一种性能损耗?
printLog(() -> aInfo() + "<->" + bInfo());
}

那怎么验证在随机数不大于 5 时没有进行字符串的拼接呢?

可以对 printLog() 再改造下:

1
2
3
4
5
6
7
8
9
private int printLog(Supplier<String> supplier, Set<String> pool) {
int i = new Random().nextInt(10);
if (i > 5) {
String log = supplier.get();
pool.add(log);
System.out.println(log);
}
return i;
}

当随机数大于 5 时,获取到要打印的字符串,并将它添加到 Set 集合中。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testDelayExecute() {
// --snip--

Set<String> pool = new HashSet<>();
int random = printLog(() -> bInfo() + "<==>" + aInfo(), pool);
if (random > 5) {
assertThat(pool).isNotEmpty();
} else {
assertThat(pool).isEmpty();
}
}

运行测试方法,测试通过。

也就说, 只有在有需要时才会执行字符串的拼接,使拼接字符串的操作延迟了。

2. LambdaSafe

2.1 简单使用

回归正题,前文说到,Java 中 Lambda 表达式就是回调函数的一种体现,现在再来看 LambdaSafe 提供的 callback() 方法需要的参数:

1
2
3
4
5
6
public static <C, A> Callback<C, A> callback(Class<C> callbackType, 
C callbackInstance,
A argument,
Object... additionalArguments) {
// --snip--
}

那么可以这样理解:

  • callbackType:使用 Lambda 表达式时对应的函数式接口的 Class 对象(Java 是一门极致的面向对象的语言,尽管能够模仿出将函数作为参数传入另一个函数,但依旧离不开类);
  • callbackInstance:一个 Lambda 表达式(需要执行的回调函数);
  • argument:执行 Lambda 表达式需要的参数(执行回调函数需要的参数);
  • additionalArguments:执行 Lambda 表达式需要的附加参数(回调函数的参数可以是多个)。

因此可以这样使用 LambdaSafe

1
2
3
4
5
6
7
8
9
@Test
public void testSimplyUseLambdaSafe() {
BiFunction<Integer, Integer, Integer> sum = Integer::sum;
@SuppressWarnings("unchecked")
Integer result = (Integer) LambdaSafe.callback(BiFunction.class, sum, 1, 2)
.invokeAnd(biFunction -> biFunction.apply(1, 2))
.get(0);
assertThat(result).isEqualTo(3);
}

LambdaSafe.callback() 方法返回了 Callback 对象,Callback 类中有两个可被调用的方法:

  • invoke():接收 Consumer 类型的参数,表示执行回调函数(即给定的 Lambda 表达式);
  • invokeAnd():接收 Function 类型的参数,返回 InvocationResult 对象(与 Optional 的设计类似,只不过允许 null 值作为合法值),表示执行回调函数并将执行结果使用 InvocationResult 包装。

两数之和显然是有返回值的,因此调用 invokeAnd() 方法,之后再调用 get() 方法,如果执行结果为 null,则返回传入 get() 方法的参数。

2.2 Callback#invoke()

Callback 中的 invoke() 方法源码如下:

1
2
3
4
5
6
public void invoke(Consumer<C> invoker) {
invoke(this.callbackInstance, () -> {
invoker.accept(this.callbackInstance);
return null;
});
}

接收 Consumer 类型的参数,其泛型类型与 LambdaSafe#callback() 方法的第二个参数一致,即与传入的 Lambda 表达式(回调函数)对应的函数式接口类型一致。

比如这样调用:

1
2
3
4
5
6
Consumer<String> consumer = System.out::println;
LambdaSafe.callback(
Consumer.class,
consumer,
"hello lambda safe"
);

那么后续调用 invoke() 方法时,其 Consumer 类型的参数的泛型为 Consumer<String>,完整参数类型为 Consumer<Consumer<String>>

执行回调函数的过程类似:

1
2
3
4
5
6
7
@Test
public void testNestedConsumer() {
Consumer<String> consumer = System.out::println;
Consumer<Consumer<String>> nestedConsumer = c -> c.accept("hello lambda safe");

nestedConsumer.accept(consumer);
}

如果难以理解,上述 Java 代码等价于如下 JavaScript 代码:

1
2
3
4
5
6
7
8
9
function consumer(str) {
console.log(str);
}

function nestedConsumer(fun) {
fun("hello lambda safe");
}

nestedConsumer(consumer);

2.3 LambdaSafeCallback#invoke

Callback#invoke() 内部又调用了个 invoke() 方法,这个方法是 LambdaSafeCallback 类中的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected final <R> InvocationResult<R> invoke(C callbackInstance, Supplier<R> supplier) {
if (this.filter.match(this.callbackType, callbackInstance, this.argument, this.additionalArguments)) {
try {
return InvocationResult.of(supplier.get());
}
catch (ClassCastException ex) {
if (!isLambdaGenericProblem(ex)) {
throw ex;
}
logNonMatchingType(callbackInstance, ex);
}
}
return InvocationResult.noResult();
}

接收两个参数:

  • callbackInstance:回调函数实例,用于后续进行过滤;
  • supplierSupplier 类型参数,封装回调函数执行过程,与给定的类型过滤器匹配时,执行其 get() 方法,获取回调函数的执行结果并封装到 InvocationResult 对象中。

LambdaSafeCallback#invoke()LambdaSafe 的核心,理解该方法,就能明白 Safe 的含义。

类型过滤

方法第一行是一个 if 判断,将调用 LambdaSafe.callback() 传入的四个参数都传入 filtermatch() 方法中。

filter 的类型是 Filter<C, A>,也是一个函数式接口,此处使用其默认实现 GenericTypeFilter

尽管 LambdaSafeCallback 中存在 withFilter() 方法,传入一个过滤器对象,后续使用此过滤器对象的 match() 方法完成类型过滤,但 withFilter() 并不是 public,用户无法显式调用,比较鸡肋,只能 使用其默认实现 GenericTypeFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
private static class GenericTypeFilter<C, A> implements Filter<C, A> {

@Override
public boolean match(Class<C> callbackType, C callbackInstance, A argument, Object[] additionalArguments) {
ResolvableType type = ResolvableType.forClass(callbackType, callbackInstance.getClass());
if (type.getGenerics().length == 1 && type.resolveGeneric() != null) {
return type.resolveGeneric().isInstance(argument);
}

return true;
}

}

使用到了 ResolvableType#forClass() 方法,ResolvableType 是 Spring 对 Java 中的 Type 类型的封装,看一个测试方法,理解 GenericTypeFilter 的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract static class MyList implements List<String> {

}

@Test
public void testSimplyUseResolvableType() {
ResolvableType type = ResolvableType.forClass(List.class, ArrayList.class);
assertThat(type.getGenerics().length).isEqualTo(1);
assertThat(type.resolveGeneric()).isNull();

type = ResolvableType.forClass(List.class, MyList.class);
assertThat(type.getGenerics().length).isEqualTo(1);
Class<?> clazz = type.resolveGeneric();
assertThat(clazz).isNotNull().isAssignableFrom(String.class);
}

对于 GenericTypeFilter#match() 来说:

  • 当回调函数只有 单个 泛型(具体的类型,不是泛型参数),且主参数 argument 是它的实例时才匹配,否则不匹配;
  • 当回调函数有多个泛型,或者只有单个泛型的情况下,那个泛型还是泛型参数时,match() 方法也返回 true
1
2
3
4
5
6
7
interface FirstConsumer extends Consumer<String> {

}

interface SecondConsumer extends Consumer<Integer> {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
@SuppressWarnings("unchecked")
public void testMatch() {
FirstConsumer consumer = System.out::println;
LambdaSafe.callback(
Consumer.class,
consumer,
2
).invoke(i -> i.accept("hello lambda safe"));

LambdaSafe.callback(
Consumer.class,
consumer,
2
).invoke(i -> i.accept(2));
}

回调函数只有单个泛型,并且也不是泛型参数,回调函数需要参数类型是 String,但主参数类型是 Integermatch() 方法返回 false,最终不会执行回调函数,避免 ClassCastException 的发生。

如果主参数类型也修改为 String,则会执行回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@SuppressWarnings("unchecked")
public void testMatch() {
// --snip--

SecondConsumer consumer2nd = integer -> System.out.println(1 + integer);
LambdaSafe.callback(
Consumer.class,
consumer2nd,
2
).invoke(i -> i.accept(2));
}

运行后,控制台打印出:

3

主参数 argument 不会影响传入给回调函数的参数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@SuppressWarnings("unchecked")
public void testMatch() {
// --snip--

// argument 与回调函数真正执行时使用的参数无关,仅用于过滤和是否抛出 ClassCastException 的判断
LambdaSafe.callback(
Consumer.class,
consumer2nd,
10000000
).invoke(i -> i.accept(2));
}

运行后,控制台 依旧 打印出:

3

异常处理

由于泛型擦除,执行回调函数时传入的参数类型变为了 Object,可以传递任意类型的数据,如果此时传入的参数类型与回调函数真实需要的参数类型不一致,则会抛出 ClassCastException

最终 是否抛出 ClassCastException 异常由 LambdaSafeCallback#isLambdaGenericProblem() 方法决定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected final <R> InvocationResult<R> invoke(C callbackInstance, Supplier<R> supplier) {
if (this.filter.match(this.callbackType, callbackInstance, this.argument, this.additionalArguments)) {
try {
return InvocationResult.of(supplier.get());
}
catch (ClassCastException ex) {
// 是否抛出异常的判断
if (!isLambdaGenericProblem(ex)) {
throw ex;
}
logNonMatchingType(callbackInstance, ex);
}
}
return InvocationResult.noResult();
}

见名之意,如果不是回调函数泛型的问题,证明是用户的使用方式不对,则抛出异常,反之不抛出异常,仅仅打印日志信息。

LambdaSafeCallback#isLambdaGenericProblem() 方法的实现如下:

1
2
3
4
5
6
7
8
private boolean isLambdaGenericProblem(ClassCastException ex) {
return (ex.getMessage() == null || startsWithArgumentClassName(ex.getMessage()));
}

private boolean startsWithArgumentClassName(String message) {
Predicate<Object> startsWith = (argument) -> startsWithArgumentClassName(message, argument);
return startsWith.test(this.argument) || Stream.of(this.additionalArguments).anyMatch(startsWith);
}

Spring 通过异常的 message 判断是否是回调函数的泛型导致的类型转换失败,如果 message 信息中带有回调函数的泛型信息,证明是由于泛型导致的类型转换失败,不抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
@SuppressWarnings("unchecked")
public void testClassCastException() {
// 需要使用的回调函数实例
FirstConsumer consumer = System.out::println;
// 回调函数需要的参数
String str = "hello lambda safe";
assertThatThrownBy(() -> {
LambdaSafe.callback(
Consumer.class,
consumer,
str
).invoke(i -> i.accept(2));
}).isInstanceOf(ClassCastException.class);
}

回调函数只有单个泛型,并且不是泛型参数,主参数是该类型的实例,过滤器的 match() 方法返回 true,准备执行回调函数。

回调函数需要的参数类型是 String,但用户在调用时传入的类型是 Integer,进行类型转换时抛出异常,异常的 message 信息不以主参数的类型开头,判定为用户调用方式错误,最终抛出 ClassCastException 异常。

回调函数的泛型问题导致类型转换失败

那有没有什么情况是由于回调函数的泛型问题导致的类型转换失败呢?

先回到过滤器的 match() 方法,只有该方法返回 true 时才会执行回调函数:

  1. 回调函数只有单个泛型,并且不是泛型参数,主参数是该类型的实例
  2. 回调函数有多个泛型,或者只有单个泛型,但是是泛型参数

在第一种情况下,要想类型转换失败,那么传递给回调函数的参数类型与主参数的类型不一致,那么执行 isLambdaGenericProblem() 方法时一定返回 false,最终一定会抛出异常。

在第二种情况下,主参数类型与执行回调函数时传入的参数类型一致,但这类型不是回调函数需要的,认为是回调函数的泛型产生的类型转换失败,最终不会抛出异常,只打印日志。

1
2
interface FirstFunction extends Function<Integer, Integer> {
}
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 testNoException() {
// 回调函数有多个泛型
FirstFunction function = i -> i + 1;
String strParam = "lambda";
assertThatNoException().isThrownBy(() -> {
LambdaSafe.callback(
Function.class,
function,
strParam
).invokeAnd(i -> i.apply(strParam));
});

// 回调函数是单个泛型,但是是泛型参数
Consumer<String> consumer = System.out::println;
int intParam = 1;
assertThatNoException().isThrownBy(() -> {
LambdaSafe.callback(
Consumer.class,
consumer,
intParam
).invoke(i -> i.accept(intParam));
});
}

要让 LambdaSafe 执行过程中不抛出异常, 一定要使调用回调函数时传入的参数类型与调用 callback() 方法时传入的主参数、附加参数类型一一匹配。

2.4 LambdaSafe#callbacks()

LambdaSafeSafe 体现在两点:

  1. 使用 Filter,过滤不符合条件的回调函数,但只能针对单个泛型的回调函数,且这个泛型不是泛型参数;
  2. 调用回调函数传入的类型与调用 callback() 方法时传入的主参数、附加参数类型一一匹配时,就算这参数类型不是回调函数真正需要的也不会抛出异常。

回调函数有两个泛型时,Filter 失效,只要保证第二点就能达到 Safe

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
interface FirstFunction extends Function<Integer, Integer> {
}

interface SecondFunction extends Function<String, String> {
}

@Test
@SuppressWarnings("unchecked")
public void testHasTwoGenericParam() {
// 1 -> 2
FirstFunction fun1st = i -> i + 1;
// 1 -> 12
SecondFunction fun2nd = i -> i + 2;
List<Function<?, ?>> functions = Arrays.asList(fun1st, fun2nd);

int param = 1;
// 不抛异常,但是有日志输出,能体现 safe
assertThatNoException().isThrownBy(() -> {
LambdaSafe.callbacks(
Function.class,
functions,
param
).invoke(i -> System.out.println(i.apply(param)));
});

assertThatThrownBy(() -> {
LambdaSafe.callbacks(
Function.class,
functions,
param
).invoke(i -> System.out.println(i.apply(String.valueOf(param))));
}).isInstanceOf(ClassCastException.class);
}

运行后,测试通过,但控制台打印出 ClassCastException 的异常信息。

如果回调函数只有一个泛型,同时满足上述两点,不匹配的回调函数会直接被 Filter 过滤,程序甚至不会走到执行回调函数那一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
@SuppressWarnings("unchecked")
public void testCallBacks() {
FirstConsumer consumer1st = System.out::println;
SecondConsumer consumer2nd = (i) -> System.out.println(i + 1);
List<Consumer<?>> consumers = Arrays.asList(consumer1st, consumer2nd);

int hundred = 100;
assertThatCode(() -> {
LambdaSafe.callbacks(
Consumer.class,
consumers,
hundred
).invoke(i -> i.accept(hundred));
}).doesNotThrowAnyException();
}

运行后,测试通过,控制台不含任何异常信息,只打印出:

101

2.5 实战示例

实际开发过程中使用的接口往往都不是函数式接口,这些接口中往往会有多个抽象方法:

1
2
3
4
5
6
7
8
9
enum Type {
ONE, TWO
}

interface MyConsumer<T> {
Type getType();

void accept(T t);
}

程序员提供 MyConsumer 的两种实现:

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
static class FirstMyConsumer implements MyConsumer<String> {

@Override
public Type getType() {
return Type.ONE;
}

@Override
public void accept(String s) {
System.out.println("->" + s + "<-");
}
}

static class SecondMyConsumer implements MyConsumer<Integer> {

@Override
public Type getType() {
return Type.TWO;
}

@Override
public void accept(Integer integer) {
System.out.println(integer);
}
}

此时依旧能使用 LambdaSafe 实现回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
@SuppressWarnings("unchecked")
public void testNotOnlyLambda() {
List<MyConsumer<?>> consumers = Arrays.asList(new FirstMyConsumer(), new SecondMyConsumer());
String param = "callback safe";
assertThatNoException().isThrownBy(() -> {
LambdaSafe.callbacks(
MyConsumer.class,
consumers,
param
).invoke(i -> i.accept(param));
});
// lambda safe? callback safe!
}

LambdaSafe 似乎并不局限于 Lambda 表达式和函数式接口,LambdaSafe 适用于:

  • 某个泛型接口有多个实现类
  • 被调用的目标方法的主参数类型恰好是泛型接口的泛型(参数可以是多个)
  • 想要安全地调用目标方法

尤其是存在多个实现类时,使用 LambdaSafe#callbacks() 进行 安全地 回调不外乎是一种好方法。