欢迎你来读这篇博客,这篇博客主要是关于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 函数式接口”,而是要理解两个问题:
- Guava 为什么要设计这些接口;
- 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 中计时工具类的正式类名是:
不是 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
|
中。
它的核心方法是:
含义是:
输入一个 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(); } }
|
这个函数表示:
也就是把 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
| Lists.transform(users, usernameFunction)
|
表示把 List<User> 转成 List<String>。
在 Java 8 之前,这种写法已经比较优雅了。
chapter 4:Java 8 Function 对比
Java 8 提供了:
1
| java.util.function.Function
|
核心方法也是:
使用 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 和方法引用。
本质上就是一个 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 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 表示:
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
|
核心方法是:
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 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
|
核心方法也是:
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 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 之后,建议:
不要在新项目中混用 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 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
|
这些都在:
中。
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:
JDK:
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"); } }
|
输出类似:
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 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();
|
计算耗时。
但更推荐使用:
或工具类 Stopwatch。
原因是:
currentTimeMillis() 表示当前墙上时间;
- 它可能受到系统时间调整影响;
nanoTime() 更适合测量时间间隔;
- Guava
Stopwatch 内部就是基于更适合测量耗时的时间源。
所以:
1
| System.currentTimeMillis()
|
适合记录时间点。
适合计算耗时。
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); } }
|
假设接口全限定名是:
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:实战:简单插件系统设计
下面设计一个简单插件系统。
需求:
- 系统定义统一插件接口;
- 插件实现类由外部提供;
- 启动时通过 ServiceLoader 加载;
- 根据插件 type 路由;
- 调用对应插件执行。
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
| 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 实战:插件耗时统计
我们可以把 Stopwatch 和 ServiceLoader 结合起来。
在插件执行时统计耗时。
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); } }
|
假设接口全限定名为:
文件路径:
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.function 和 java.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.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。