Guava:函数式接口与辅助工具

欢迎你来读这篇博客,这篇博客主要是关于Guava 函数式接口与辅助工具
其中包括 Guava 早期函数式接口 Function、Predicate、Supplier、Optional,与 Java 8 函数式接口的对比,为什么 Java 8 之后 Guava 函数式接口使用减少,以及 Guava Stopwatch 和 JDK ServiceLoader SPI 机制的实战应用。

序言

在 Java 8 之前,Java 语言本身没有 Lambda 表达式,也没有 java.util.function 包。

那个时代,如果你想写一些“函数式风格”的代码,比如:

  • 把一个对象转换成另一个对象;
  • 根据条件过滤集合;
  • 延迟提供某个对象;
  • 表示一个可能为空的值;

JDK 标准库本身支持得并不优雅。

Guava 在 Java 8 之前就提供了一套早期函数式工具:

1
2
3
4
com.google.common.base.Function
com.google.common.base.Predicate
com.google.common.base.Supplier
com.google.common.base.Optional

这些接口在当时非常有用。

但 Java 8 之后,JDK 标准库引入了:

1
2
3
4
java.util.function.Function
java.util.function.Predicate
java.util.function.Supplier
java.util.Optional

再加上 Lambda、Stream API 的出现,Guava 早期函数式接口的使用频率明显下降。

所以这一章我们不是简单地“复古式学习 Guava 函数式接口”,而是要理解两个问题:

  1. Guava 为什么要设计这些接口;
  2. Java 8 之后为什么更推荐使用 JDK 原生函数式接口。

这一章还会讲两个辅助工具:

1
2
Guava Stopwatch
JDK ServiceLoader

Stopwatch 主要用于计时和性能统计。

ServiceLoader 是 JDK 原生 SPI 机制,可以实现简单插件扩展。

对应课程结构:

1
2
3
第 3 章:函数式接口与辅助工具
3.1 Guava 函数式接口
3.2 StopWatch 和 JDK ServiceLoader

注意:Guava 中计时工具类的正式类名是:

1
Stopwatch

不是 StopWatch

很多课程或文章会写成 StopWatch,但 Guava API 中是 Stopwatch

正文

chapter 1:为什么 Java 8 之前需要 Guava 函数式接口

在 Java 8 之前,如果要把一个 User 转成 UserVO,常见写法是:

1
2
3
4
5
6
List<UserVO> result = new ArrayList<>();

for (User user : users) {
UserVO vo = new UserVO(user.getId(), user.getName());
result.add(vo);
}

如果要过滤启用用户:

1
2
3
4
5
6
7
List<User> enabledUsers = new ArrayList<>();

for (User user : users) {
if (user.isEnabled()) {
enabledUsers.add(user);
}
}

这些代码没问题,但表达能力比较弱。

它们把“做什么”和“怎么循环”混在一起。

Guava 在 Java 8 之前提供了一些函数式接口,配合集合工具类可以写得更抽象。

例如:

1
2
3
4
Function<User, UserVO>
Predicate<User>
Supplier<Connection>
Optional<User>

这些接口的目的不是让 Java 变成函数式语言,而是让部分常见操作可以被抽象为对象。

chapter 2:Guava Function

Guava 的 Function 在包:

1
com.google.common.base.Function

中。

它的核心方法是:

1
T apply(F input);

含义是:

输入一个 F 类型对象,返回一个 T 类型结果。

例如:

1
2
3
4
5
6
7
8
9
import com.google.common.base.Function;

public class UserNameFunction implements Function<User, String> {

@Override
public String apply(User input) {
return input.getUsername();
}
}

这个函数表示:

1
User -> username

也就是把 User 转成 String

chapter 3:Guava Function 示例

定义用户对象:

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
public class User {

private final Long id;

private final String username;

private final boolean enabled;

public User(Long id, String username, boolean enabled) {
this.id = id;
this.username = username;
this.enabled = enabled;
}

public Long getId() {
return id;
}

public String getUsername() {
return username;
}

public boolean isEnabled() {
return enabled;
}
}

使用 Guava Function:

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
import com.google.common.base.Function;
import com.google.common.collect.Lists;

import java.util.List;

public class GuavaFunctionDemo {

public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "Mario", true),
new User(2L, "Luigi", true),
new User(3L, "Peach", false)
);

Function<User, String> usernameFunction = new Function<User, String>() {
@Override
public String apply(User input) {
return input.getUsername();
}
};

List<String> usernames = Lists.transform(users, usernameFunction);

System.out.println(usernames);
}
}

输出:

1
[Mario, Luigi, Peach]

这里:

1
Lists.transform(users, usernameFunction)

表示把 List<User> 转成 List<String>

在 Java 8 之前,这种写法已经比较优雅了。

chapter 4:Java 8 Function 对比

Java 8 提供了:

1
java.util.function.Function

核心方法也是:

1
R apply(T t);

使用 Java 8 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.List;

public class Java8FunctionDemo {

public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "Mario", true),
new User(2L, "Luigi", true),
new User(3L, "Peach", false)
);

List<String> usernames = users.stream()
.map(User::getUsername)
.toList();

System.out.println(usernames);
}
}

这明显比 Guava 早期匿名内部类简洁。

因为 Java 8 有 Lambda 和方法引用。

1
User::getUsername

本质上就是一个 Function<User, String>

chapter 5:Guava Function 与 Java 8 Function 对比

对比项 Guava Function Java 8 Function
包名 com.google.common.base.Function java.util.function.Function
核心方法 apply apply
出现时间 Java 8 之前常用 Java 8 之后标准
Lambda 支持 可用,但不是首选 原生适配
Stream 支持 不直接适配 原生适配
推荐程度 老项目维护可用 新项目优先推荐

Java 8 之后,更推荐使用:

1
java.util.function.Function

原因很简单:

  • JDK 原生;
  • 与 Stream API 无缝配合;
  • 与 Lambda、方法引用配合自然;
  • 不增加额外依赖;
  • 生态更统一。

chapter 6:Guava Predicate

Guava 的 Predicate 在包:

1
com.google.common.base.Predicate

中。

核心方法是:

1
boolean apply(T input);

它表示一个判断条件。

例如:

1
2
3
4
5
6
7
8
9
import com.google.common.base.Predicate;

public class EnabledUserPredicate implements Predicate<User> {

@Override
public boolean apply(User input) {
return input.isEnabled();
}
}

这个 Predicate 表示:

1
用户是否启用

chapter 7:Guava Predicate 示例

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
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;

import java.util.Collection;
import java.util.List;

public class GuavaPredicateDemo {

public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "Mario", true),
new User(2L, "Luigi", true),
new User(3L, "Peach", false)
);

Predicate<User> enabledPredicate = new Predicate<User>() {
@Override
public boolean apply(User input) {
return input.isEnabled();
}
};

Collection<User> enabledUsers = Collections2.filter(users, enabledPredicate);

System.out.println(enabledUsers.size());
}
}

这里:

1
Collections2.filter(users, enabledPredicate)

表示过滤启用用户。

chapter 8:Java 8 Predicate 对比

Java 8 提供:

1
java.util.function.Predicate

核心方法是:

1
boolean test(T t);

Java 8 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.List;

public class Java8PredicateDemo {

public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "Mario", true),
new User(2L, "Luigi", true),
new User(3L, "Peach", false)
);

List<User> enabledUsers = users.stream()
.filter(User::isEnabled)
.toList();

System.out.println(enabledUsers.size());
}
}

相比 Guava 早期写法,Java 8 更简洁。

chapter 9:Predicate 组合

Java 8 Predicate 支持组合:

1
2
3
4
5
Predicate<User> enabled = User::isEnabled;

Predicate<User> nameStartsWithM = user -> user.getUsername().startsWith("M");

Predicate<User> finalPredicate = enabled.and(nameStartsWithM);

使用:

1
2
3
List<User> result = users.stream()
.filter(finalPredicate)
.toList();

Guava 也有类似工具,例如:

1
2
3
Predicates.and(...)
Predicates.or(...)
Predicates.not(...)

但 Java 8 之后,JDK 原生写法更常见。

chapter 10:Guava Supplier

Guava 的 Supplier 在包:

1
com.google.common.base.Supplier

中。

核心方法是:

1
T get();

它表示:

延迟提供一个对象。

例如:

1
2
3
4
5
6
7
8
9
import com.google.common.base.Supplier;

public class TokenSupplier implements Supplier<String> {

@Override
public String get() {
return "token-" + System.currentTimeMillis();
}
}

Supplier 常见场景:

  • 延迟加载;
  • 懒初始化;
  • 提供默认值;
  • 创建对象;
  • 获取配置;
  • 获取上下文值。

chapter 11:Guava Supplier 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.base.Supplier;

public class GuavaSupplierDemo {

public static void main(String[] args) {
Supplier<String> tokenSupplier = new Supplier<String>() {
@Override
public String get() {
return "token-" + System.currentTimeMillis();
}
};

System.out.println(tokenSupplier.get());
System.out.println(tokenSupplier.get());
}
}

每次调用 get() 都会生成一个新 token。

chapter 12:Java 8 Supplier 对比

Java 8 提供:

1
java.util.function.Supplier

核心方法也是:

1
T get();

Java 8 写法:

1
2
3
4
5
6
7
8
9
10
import java.util.function.Supplier;

public class Java8SupplierDemo {

public static void main(String[] args) {
Supplier<String> tokenSupplier = () -> "token-" + System.currentTimeMillis();

System.out.println(tokenSupplier.get());
}
}

新项目优先使用:

1
java.util.function.Supplier

chapter 13:Supplier 实战:延迟创建默认对象

假设我们要从缓存获取用户,如果缓存没有,就提供默认用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.function.Supplier;

public class DefaultUserSupplierDemo {

public static void main(String[] args) {
Supplier<User> defaultUserSupplier = () -> new User(0L, "Guest", false);

User user = findUserOrDefault(null, defaultUserSupplier);

System.out.println(user.getUsername());
}

private static User findUserOrDefault(User user, Supplier<User> supplier) {
if (user != null) {
return user;
}

return supplier.get();
}
}

Supplier 的价值是:

只有真正需要默认值时,才创建默认对象。

如果默认对象创建成本高,这很有用。

chapter 14:Guava Optional

Guava 在 Java 8 之前提供了:

1
com.google.common.base.Optional

它用于表达:

一个值可能存在,也可能不存在。

常见方法:

1
2
3
4
5
6
7
Optional.absent()
Optional.of(value)
Optional.fromNullable(value)
optional.isPresent()
optional.get()
optional.or(defaultValue)
optional.orNull()

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.google.common.base.Optional;

public class GuavaOptionalDemo {

public static void main(String[] args) {
Optional<String> optional = Optional.fromNullable(findUsername());

if (optional.isPresent()) {
System.out.println(optional.get());
} else {
System.out.println("default");
}

String value = optional.or("Guest");

System.out.println(value);
}

private static String findUsername() {
return null;
}
}

chapter 15:Java 8 Optional

Java 8 提供:

1
java.util.Optional

常见方法:

1
2
3
4
5
6
7
8
9
Optional.empty()
Optional.of(value)
Optional.ofNullable(value)
optional.isPresent()
optional.orElse(defaultValue)
optional.orElseGet(supplier)
optional.map(...)
optional.flatMap(...)
optional.ifPresent(...)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Optional;

public class Java8OptionalDemo {

public static void main(String[] args) {
Optional<String> optional = Optional.ofNullable(findUsername());

String value = optional.orElse("Guest");

System.out.println(value);
}

private static String findUsername() {
return null;
}
}

Java 8 之后,推荐使用 JDK 的 Optional

chapter 16:Guava Optional 与 Java Optional 对比

对比项 Guava Optional Java 8 Optional
包名 com.google.common.base.Optional java.util.Optional
空值方法 absent() empty()
nullable 方法 fromNullable() ofNullable()
默认值 or(value) orElse(value)
返回 null orNull() 不鼓励
Stream 生态 不直接适配 原生适配
推荐程度 老项目可见 新项目优先推荐

Java 8 之后,建议:

1
java.util.Optional

不要在新项目中混用 Guava Optional 和 JDK Optional。

混用会让 API 非常别扭。

chapter 17:Optional 使用建议

1. Optional 适合作为返回值

推荐:

1
2
public Optional<User> findById(Long id) {
}

表示这个用户可能不存在。

2. 不建议作为实体字段

不推荐:

1
private Optional<String> nickname;

实体字段用普通类型即可。

3. 不建议作为方法参数

不推荐:

1
2
public void updateUser(Optional<String> nickname) {
}

参数可选可以通过重载、DTO 字段或明确传 null 约定。

4. 不要直接 get

不推荐:

1
optional.get()

除非你已经明确判断:

1
2
3
if (optional.isPresent()) {
optional.get();
}

更推荐:

1
2
3
optional.orElse(...)
optional.orElseThrow(...)
optional.map(...)

5. Optional 不等于消灭 null

Optional 是表达“可能不存在”的返回语义。

不是让所有地方都包一层 Optional。

chapter 18:为什么 Java 8 之后 Guava 函数式接口使用减少

原因主要有几个。

1. JDK 有标准函数式接口

Java 8 提供:

1
2
3
4
5
6
7
Function
Predicate
Supplier
Consumer
BiFunction
UnaryOperator
BinaryOperator

这些都在:

1
java.util.function

中。

2. Lambda 原生适配 JDK 接口

Java 8 Lambda 与 JDK 函数式接口配合最自然。

例如:

1
users.stream().filter(User::isEnabled)

3. Stream API 成为主流

集合转换、过滤、聚合基本都可以用 Stream。

4. 避免依赖额外库

新项目如果只是为了 Function、Predicate,引入 Guava 没必要。

5. 生态统一

Spring、JDK、第三方库大多使用 java.util.function

所以 Java 8 之后,Guava 函数式接口更多出现在老项目中。

新代码优先用 JDK 原生函数式接口。

chapter 19:Guava 函数式接口迁移建议

如果老项目中有:

1
2
3
4
com.google.common.base.Function
com.google.common.base.Predicate
com.google.common.base.Supplier
com.google.common.base.Optional

可以逐步迁移到:

1
2
3
4
java.util.function.Function
java.util.function.Predicate
java.util.function.Supplier
java.util.Optional

迁移时注意:

1. Predicate 方法名不同

Guava:

1
apply(input)

JDK:

1
test(input)

2. Optional 方法名不同

Guava:

1
2
3
Optional.absent()
Optional.fromNullable(value)
optional.or(value)

JDK:

1
2
3
Optional.empty()
Optional.ofNullable(value)
optional.orElse(value)

3. 不要一次性大规模替换

如果项目很大,建议模块化迁移,避免一次性制造大量风险。

4. 对外 API 要统一

同一个模块对外不要一会儿返回 Guava Optional,一会儿返回 JDK Optional。

chapter 20:Stopwatch 是什么

Stopwatch 是 Guava 提供的计时工具。

包名:

1
com.google.common.base.Stopwatch

它用于测量代码执行耗时。

常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

public class StopwatchDemo {

public static void main(String[] args) throws InterruptedException {
Stopwatch stopwatch = Stopwatch.createStarted();

Thread.sleep(100);

long costMs = stopwatch.elapsed(TimeUnit.MILLISECONDS);

System.out.println("cost = " + costMs + " ms");
}
}

输出类似:

1
cost = 100 ms

Stopwatch 比手写:

1
2
long start = System.currentTimeMillis();
long cost = System.currentTimeMillis() - start;

更清晰。

chapter 21:Stopwatch 基本用法

创建并启动:

1
Stopwatch stopwatch = Stopwatch.createStarted();

创建但不启动:

1
2
3
Stopwatch stopwatch = Stopwatch.createUnstarted();

stopwatch.start();

获取耗时:

1
long costMs = stopwatch.elapsed(TimeUnit.MILLISECONDS);

停止:

1
stopwatch.stop();

重置:

1
stopwatch.reset();

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

public class StopwatchBasicDemo {

public static void main(String[] args) throws InterruptedException {
Stopwatch stopwatch = Stopwatch.createUnstarted();

stopwatch.start();

Thread.sleep(50);

stopwatch.stop();

System.out.println("first cost = " + stopwatch.elapsed(TimeUnit.MILLISECONDS) + " ms");

stopwatch.reset().start();

Thread.sleep(80);

System.out.println("second cost = " + stopwatch.elapsed(TimeUnit.MILLISECONDS) + " ms");
}
}

chapter 22:Stopwatch 与 System.currentTimeMillis 对比

很多人会用:

1
long start = System.currentTimeMillis();

计算耗时。

但更推荐使用:

1
System.nanoTime()

或工具类 Stopwatch

原因是:

  • currentTimeMillis() 表示当前墙上时间;
  • 它可能受到系统时间调整影响;
  • nanoTime() 更适合测量时间间隔;
  • Guava Stopwatch 内部就是基于更适合测量耗时的时间源。

所以:

1
System.currentTimeMillis()

适合记录时间点。

1
Stopwatch

适合计算耗时。

chapter 23:Stopwatch 性能统计场景

Stopwatch 常用于:

  • 接口耗时统计;
  • SQL 查询耗时;
  • 文件导入耗时;
  • 批处理任务耗时;
  • 远程调用耗时;
  • 缓存加载耗时;
  • 算法运行耗时;
  • 调试性能瓶颈。

示例:统计批处理耗时。

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
import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

public class BatchJobDemo {

public void runJob() {
Stopwatch stopwatch = Stopwatch.createStarted();

try {
loadData();
processData();
saveResult();
} finally {
long costMs = stopwatch.elapsed(TimeUnit.MILLISECONDS);

System.out.println("batch job cost = " + costMs + " ms");
}
}

private void loadData() {
System.out.println("加载数据");
}

private void processData() {
System.out.println("处理数据");
}

private void saveResult() {
System.out.println("保存结果");
}
}

chapter 24:Stopwatch 分段计时

有时候我们希望统计每个步骤耗时。

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
import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

public class StepStopwatchDemo {

public void importData() throws InterruptedException {
Stopwatch total = Stopwatch.createStarted();

Stopwatch step = Stopwatch.createStarted();

readFile();
System.out.println("readFile cost = " + step.elapsed(TimeUnit.MILLISECONDS) + " ms");

step.reset().start();

parseData();
System.out.println("parseData cost = " + step.elapsed(TimeUnit.MILLISECONDS) + " ms");

step.reset().start();

saveData();
System.out.println("saveData cost = " + step.elapsed(TimeUnit.MILLISECONDS) + " ms");

System.out.println("total cost = " + total.elapsed(TimeUnit.MILLISECONDS) + " ms");
}

private void readFile() throws InterruptedException {
Thread.sleep(50);
}

private void parseData() throws InterruptedException {
Thread.sleep(80);
}

private void saveData() throws InterruptedException {
Thread.sleep(100);
}
}

分段计时很适合定位性能瓶颈。

chapter 25:Stopwatch 在业务日志中的使用建议

真实项目中,建议统一输出结构化日志。

例如:

1
biz=order,scene=create,step=save_order,cost_ms=35

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

public class OrderCostLogDemo {

public void createOrder() {
Stopwatch stopwatch = Stopwatch.createStarted();

try {
doCreateOrder();
} finally {
long costMs = stopwatch.elapsed(TimeUnit.MILLISECONDS);

System.out.println("biz=order,scene=create,step=total,cost_ms=" + costMs);
}
}

private void doCreateOrder() {
System.out.println("创建订单");
}
}

注意:

Stopwatch 适合轻量计时,但它不是完整监控系统。

线上监控还需要:

  • Micrometer;
  • Prometheus;
  • SkyWalking;
  • OpenTelemetry;
  • APM;
  • 日志链路追踪。

chapter 26:ServiceLoader 是什么

ServiceLoader 是 JDK 原生 SPI 机制。

SPI 是 Service Provider Interface 的缩写。

它解决的问题是:

面向接口编程时,接口由框架定义,实现由外部插件提供,框架在运行时自动发现实现类。

简单说:

ServiceLoader 是 JDK 自带的插件发现机制。

典型使用场景:

  • JDBC Driver 加载;
  • 日志实现发现;
  • 插件系统;
  • 多厂商适配;
  • 规则扩展;
  • 文件解析器扩展;
  • 支付渠道扩展;
  • 消息发送渠道扩展。

chapter 27:ServiceLoader 的基本原理

ServiceLoader 的核心约定是:

在 classpath 下的 META-INF/services/ 目录中,创建一个以接口全限定名命名的文件,文件内容写实现类的全限定名。

例如接口:

1
com.example.plugin.MessagePlugin

那么配置文件路径是:

1
META-INF/services/com.example.plugin.MessagePlugin

文件内容:

1
2
com.example.plugin.EmailMessagePlugin
com.example.plugin.SmsMessagePlugin

运行时:

1
ServiceLoader<MessagePlugin> loader = ServiceLoader.load(MessagePlugin.class);

JDK 会自动读取这些配置文件,并加载实现类。

chapter 28:ServiceLoader 基本示例:定义插件接口

定义一个消息插件接口。

1
2
3
4
5
6
public interface MessagePlugin {

String type();

void send(String receiver, String content);
}

这个接口表示一种消息发送插件。

不同实现可以是:

  • 邮件插件;
  • 短信插件;
  • 企业微信插件;
  • 钉钉插件。

chapter 29:实现邮件插件

1
2
3
4
5
6
7
8
9
10
11
12
public class EmailMessagePlugin implements MessagePlugin {

@Override
public String type() {
return "email";
}

@Override
public void send(String receiver, String content) {
System.out.println("发送邮件,receiver = " + receiver + ",content = " + content);
}
}

chapter 30:实现短信插件

1
2
3
4
5
6
7
8
9
10
11
12
public class SmsMessagePlugin implements MessagePlugin {

@Override
public String type() {
return "sms";
}

@Override
public void send(String receiver, String content) {
System.out.println("发送短信,receiver = " + receiver + ",content = " + content);
}
}

chapter 31:配置 META-INF/services

假设接口全限定名是:

1
com.example.plugin.MessagePlugin

则需要创建文件:

1
src/main/resources/META-INF/services/com.example.plugin.MessagePlugin

文件内容:

1
2
com.example.plugin.EmailMessagePlugin
com.example.plugin.SmsMessagePlugin

注意:

  • 文件名必须是接口全限定名;
  • 文件内容是实现类全限定名;
  • 一个实现类一行;
  • 实现类通常需要无参构造方法;
  • 文件必须在 classpath 下。

chapter 32:使用 ServiceLoader 加载插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.ServiceLoader;

public class ServiceLoaderDemo {

public static void main(String[] args) {
ServiceLoader<MessagePlugin> loader = ServiceLoader.load(MessagePlugin.class);

for (MessagePlugin plugin : loader) {
System.out.println("加载插件:" + plugin.type());

plugin.send("mario@example.com", "Hello SPI");
}
}
}

输出类似:

1
2
3
4
加载插件:email
发送邮件,receiver = mario@example.com,content = Hello SPI
加载插件:sms
发送短信,receiver = mario@example.com,content = Hello SPI

这就是 JDK 原生 SPI。

chapter 33:实战:简单插件系统设计

下面设计一个简单插件系统。

需求:

  1. 系统定义统一插件接口;
  2. 插件实现类由外部提供;
  3. 启动时通过 ServiceLoader 加载;
  4. 根据插件 type 路由;
  5. 调用对应插件执行。

chapter 34:定义插件接口 Plugin

1
2
3
4
5
6
7
8
public interface Plugin {

String type();

String name();

void execute(PluginContext context);
}

插件上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.HashMap;
import java.util.Map;

public class PluginContext {

private final Map<String, Object> attributes = new HashMap<>();

public void put(String key, Object value) {
attributes.put(key, value);
}

@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) attributes.get(key);
}
}

chapter 35:定义插件实现

邮件插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EmailPlugin implements Plugin {

@Override
public String type() {
return "email";
}

@Override
public String name() {
return "邮件插件";
}

@Override
public void execute(PluginContext context) {
String receiver = context.get("receiver");
String content = context.get("content");

System.out.println("执行邮件插件,receiver = " + receiver + ",content = " + content);
}
}

短信插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SmsPlugin implements Plugin {

@Override
public String type() {
return "sms";
}

@Override
public String name() {
return "短信插件";
}

@Override
public void execute(PluginContext context) {
String receiver = context.get("receiver");
String content = context.get("content");

System.out.println("执行短信插件,receiver = " + receiver + ",content = " + content);
}
}

chapter 36:配置插件服务文件

接口全限定名假设为:

1
com.example.spi.Plugin

创建文件:

1
src/main/resources/META-INF/services/com.example.spi.Plugin

内容:

1
2
com.example.spi.EmailPlugin
com.example.spi.SmsPlugin

chapter 37:定义插件管理器 PluginManager

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.util.Map;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

public class PluginManager {

private final Map<String, Plugin> pluginMap;

public PluginManager() {
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);

this.pluginMap = StreamSupport.stream(loader.spliterator(), false)
.collect(Collectors.toUnmodifiableMap(Plugin::type, plugin -> plugin));

System.out.println("插件加载完成,plugins = " + pluginMap.keySet());
}

public Plugin getPlugin(String type) {
Plugin plugin = pluginMap.get(type);

if (plugin == null) {
throw new IllegalArgumentException("Unsupported plugin type: " + type);
}

return plugin;
}

public void execute(String type, PluginContext context) {
Plugin plugin = getPlugin(type);

plugin.execute(context);
}
}

这个管理器负责:

  • 加载插件;
  • 建立 type 到 plugin 的映射;
  • 根据 type 执行插件。

chapter 38:插件系统客户端

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

public static void main(String[] args) {
PluginManager pluginManager = new PluginManager();

PluginContext context = new PluginContext();
context.put("receiver", "mario@example.com");
context.put("content", "Hello Plugin");

pluginManager.execute("email", context);
pluginManager.execute("sms", context);
}
}

输出类似:

1
2
3
插件加载完成,plugins = [email, sms]
执行邮件插件,receiver = mario@example.com,content = Hello Plugin
执行短信插件,receiver = mario@example.com,content = Hello Plugin

这就是一个非常简化的插件系统。

chapter 39:ServiceLoader 的优点

1. JDK 原生支持

不需要引入额外依赖。

2. 面向接口扩展

框架只依赖接口,不依赖具体实现。

3. 插件可独立提供

实现类可以放在独立 jar 包中。

4. 适合扩展点设计

例如:

  • 解析器;
  • 编码器;
  • 支付渠道;
  • 消息渠道;
  • 规则处理器。

5. 使用简单

只需要接口、实现类和 META-INF/services 配置。

chapter 40:ServiceLoader 的缺点

1. 配置容易写错

文件名和实现类全限定名写错,就加载不到。

2. 默认按懒加载方式发现

迭代时才实例化实现类。

3. 不适合复杂依赖注入

ServiceLoader 创建对象不由 Spring 管理。

如果插件需要注入 Spring Bean,就不太方便。

4. 缺少复杂生命周期管理

比如初始化、销毁、健康检查,需要自己设计。

5. 同类型冲突要自己处理

如果多个插件 type 一样,需要自己解决冲突。

chapter 41:ServiceLoader 与 Spring Bean 扩展对比

在 Spring Boot 项目中,经常可以直接用 Bean 自动注入实现扩展。

例如:

1
2
3
4
5
6
public interface MessageSender {

String type();

void send(String receiver, String content);
}

多个实现:

1
2
3
@Component
public class EmailMessageSender implements MessageSender {
}
1
2
3
@Component
public class SmsMessageSender implements MessageSender {
}

然后:

1
2
3
4
5
6
7
8
9
10
@Component
public class MessageSenderRouter {

private final Map<String, MessageSender> senderMap;

public MessageSenderRouter(List<MessageSender> senders) {
this.senderMap = senders.stream()
.collect(Collectors.toMap(MessageSender::type, sender -> sender));
}
}

对比:

对比项 ServiceLoader Spring Bean
来源 JDK 原生 Spring 容器
配置方式 META-INF/services @Component
依赖注入
插件 jar 扩展 方便 需要进入 Spring 扫描
生命周期管理 简单 完整
适合场景 框架 SPI、插件发现 Spring 应用内部策略扩展

如果你在做一个通用框架,ServiceLoader 很适合。

如果你在 Spring Boot 业务系统内部做扩展,Spring Bean 路由通常更方便。

chapter 42:Stopwatch + ServiceLoader 实战:插件耗时统计

我们可以把 StopwatchServiceLoader 结合起来。

在插件执行时统计耗时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

public class TimedPluginManager {

private final PluginManager pluginManager = new PluginManager();

public void execute(String type, PluginContext context) {
Stopwatch stopwatch = Stopwatch.createStarted();

try {
pluginManager.execute(type, context);
} finally {
long costMs = stopwatch.elapsed(TimeUnit.MILLISECONDS);

System.out.println("plugin_type=" + type + ",cost_ms=" + costMs);
}
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
public class TimedPluginDemo {

public static void main(String[] args) {
TimedPluginManager manager = new TimedPluginManager();

PluginContext context = new PluginContext();
context.put("receiver", "mario@example.com");
context.put("content", "Hello Timed Plugin");

manager.execute("email", context);
}
}

这个例子把本章两个辅助工具串起来了:

  • ServiceLoader 负责插件发现;
  • Stopwatch 负责执行耗时统计。

chapter 43:函数式接口与辅助工具使用建议

1. 新项目优先用 JDK 函数式接口

推荐:

1
2
3
4
java.util.function.Function
java.util.function.Predicate
java.util.function.Supplier
java.util.Optional

2. 老项目遇到 Guava 函数式接口要能看懂

尤其是:

1
2
3
4
5
Function.apply()
Predicate.apply()
Supplier.get()
Optional.absent()
Optional.fromNullable()

3. Optional 主要用于返回值

不要滥用到字段和参数上。

4. Stopwatch 适合轻量耗时统计

不要拿它替代完整监控系统。

5. 计时使用 elapsed,不要重复手写时间差

1
stopwatch.elapsed(TimeUnit.MILLISECONDS)

可读性更好。

6. ServiceLoader 适合框架级 SPI

如果是 Spring 内部扩展,优先考虑 Spring Bean 自动注入。

7. 插件系统要处理冲突和异常

包括:

  • 插件 type 重复;
  • 插件加载失败;
  • 插件执行异常;
  • 插件耗时过长;
  • 插件无参构造缺失。

8. 插件接口要稳定

SPI 一旦暴露出去,修改成本很高。

chapter 44:完整案例代码汇总

User

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
public class User {

private final Long id;

private final String username;

private final boolean enabled;

public User(Long id, String username, boolean enabled) {
this.id = id;
this.username = username;
this.enabled = enabled;
}

public Long getId() {
return id;
}

public String getUsername() {
return username;
}

public boolean isEnabled() {
return enabled;
}
}

Java 8 Function、Predicate、Supplier 示例

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
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class Java8FunctionalDemo {

public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "Mario", true),
new User(2L, "Luigi", true),
new User(3L, "Peach", false)
);

List<String> names = users.stream()
.map(User::getUsername)
.toList();

Predicate<User> enabledPredicate = User::isEnabled;

List<User> enabledUsers = users.stream()
.filter(enabledPredicate)
.toList();

Supplier<String> tokenSupplier = () -> "token-" + System.currentTimeMillis();

System.out.println(names);
System.out.println(enabledUsers.size());
System.out.println(tokenSupplier.get());
}
}

Stopwatch 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

public class StopwatchExample {

public static void main(String[] args) throws InterruptedException {
Stopwatch stopwatch = Stopwatch.createStarted();

Thread.sleep(100);

System.out.println("cost = " + stopwatch.elapsed(TimeUnit.MILLISECONDS) + " ms");
}
}

Plugin 接口

1
2
3
4
5
6
7
8
public interface Plugin {

String type();

String name();

void execute(PluginContext context);
}

PluginContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.HashMap;
import java.util.Map;

public class PluginContext {

private final Map<String, Object> attributes = new HashMap<>();

public void put(String key, Object value) {
attributes.put(key, value);
}

@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) attributes.get(key);
}
}

EmailPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EmailPlugin implements Plugin {

@Override
public String type() {
return "email";
}

@Override
public String name() {
return "邮件插件";
}

@Override
public void execute(PluginContext context) {
String receiver = context.get("receiver");
String content = context.get("content");

System.out.println("执行邮件插件,receiver = " + receiver + ",content = " + content);
}
}

SmsPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SmsPlugin implements Plugin {

@Override
public String type() {
return "sms";
}

@Override
public String name() {
return "短信插件";
}

@Override
public void execute(PluginContext context) {
String receiver = context.get("receiver");
String content = context.get("content");

System.out.println("执行短信插件,receiver = " + receiver + ",content = " + content);
}
}

META-INF/services 配置

假设接口全限定名为:

1
com.example.spi.Plugin

文件路径:

1
src/main/resources/META-INF/services/com.example.spi.Plugin

文件内容:

1
2
com.example.spi.EmailPlugin
com.example.spi.SmsPlugin

PluginManager

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.util.Map;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

public class PluginManager {

private final Map<String, Plugin> pluginMap;

public PluginManager() {
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);

this.pluginMap = StreamSupport.stream(loader.spliterator(), false)
.collect(Collectors.toUnmodifiableMap(Plugin::type, plugin -> plugin));

System.out.println("插件加载完成,plugins = " + pluginMap.keySet());
}

public Plugin getPlugin(String type) {
Plugin plugin = pluginMap.get(type);

if (plugin == null) {
throw new IllegalArgumentException("Unsupported plugin type: " + type);
}

return plugin;
}

public void execute(String type, PluginContext context) {
Plugin plugin = getPlugin(type);

plugin.execute(context);
}
}

TimedPluginManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

public class TimedPluginManager {

private final PluginManager pluginManager = new PluginManager();

public void execute(String type, PluginContext context) {
Stopwatch stopwatch = Stopwatch.createStarted();

try {
pluginManager.execute(type, context);
} finally {
long costMs = stopwatch.elapsed(TimeUnit.MILLISECONDS);

System.out.println("plugin_type=" + type + ",cost_ms=" + costMs);
}
}
}

客户端

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

public static void main(String[] args) {
TimedPluginManager manager = new TimedPluginManager();

PluginContext context = new PluginContext();
context.put("receiver", "mario@example.com");
context.put("content", "Hello Plugin");

manager.execute("email", context);
manager.execute("sms", context);
}
}

chapter 45:一句话总结

这一章的核心是:

Guava 早期函数式接口帮助 Java 在没有 Lambda 的时代获得更强的抽象能力,而 Java 8 之后,JDK 原生函数式接口成为更推荐的选择;Stopwatch 适合轻量性能计时,ServiceLoader 适合设计简单 SPI 插件扩展机制。

Function 表示转换。

Predicate 表示判断。

Supplier 表示延迟提供。

Optional 表示可能不存在。

Stopwatch 表示清晰计时。

ServiceLoader 表示原生插件发现。

如果你维护老项目,要能看懂 Guava 函数式接口。

如果你写新项目,优先使用 Java 8 的 java.util.functionjava.util.Optional

如果你写框架或插件扩展,ServiceLoader 是 JDK 原生 SPI 机制中非常值得掌握的一块。

好的代码不是追求所有工具都用最新,而是知道每个工具所处的时代、解决的问题,以及今天是否仍然适合。

参考资料

  • Google Guava API: Function.
  • Google Guava API: Predicate.
  • Google Guava API: Supplier.
  • Google Guava API: Optional.
  • Google Guava API: Stopwatch.
  • Java Documentation: java.util.function.
  • Java Documentation: java.util.Optional.
  • Java Documentation: ServiceLoader.
  • Joshua Bloch. Effective Java.
  • Oracle Java SPI and ServiceLoader Documentation.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Guava:函数式接口与辅助工具
https://allendericdalexander.github.io/2026/04/01/java/utils/guava/03guava-functional-stopwatch-serviceloader-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议