服务定位器模式:别让服务查找成为你的噩梦
服务定位器模式,说白了就是给服务做个“黄页”。你想想看,当你的系统里有几十个服务,每个服务都要手动创建或查找,那代码得多乱?我早期做企业级应用时,就吃过这个亏——到处 new 对象,后来要换实现,改得我头皮发麻。
这个模式的核心思想很简单:把服务查找逻辑集中管理。它不像工厂模式那样创建对象,而是帮你找到已经注册好的服务实例。
模式结构:四个角色
服务定位器模式有四个核心参与者,我画了张图帮你理解:
代码实现:Java 版
先看服务接口和具体服务。这部分很简单,就是标准的接口与实现:
// 服务接口
public interface Service {
String getName();
void execute();
}
// 具体服务A
public class ServiceA implements Service {
@Override
public String getName() { return "ServiceA"; }
@Override
public void execute() {
System.out.println("执行服务A的业务逻辑");
}
}
// 具体服务B
public class ServiceB implements Service {
@Override
public String getName() { return "ServiceB"; }
@Override
public void execute() {
System.out.println("执行服务B的业务逻辑");
}
}
接下来是缓存。我个人习惯用 ConcurrentHashMap,因为服务定位器往往是全局单例,多线程场景下要保证安全:
import java.util.concurrent.ConcurrentHashMap;
public class ServiceCache {
private ConcurrentHashMap<String, Service> cache =
new ConcurrentHashMap<>();
public Service getService(String serviceName) {
return cache.get(serviceName);
}
public void addService(String serviceName, Service service) {
cache.put(serviceName, service);
System.out.println("缓存服务: " + serviceName);
}
}
核心的服务定位器来了。它负责查找或创建服务,并利用缓存避免重复创建:
public class ServiceLocator {
private static ServiceCache cache = new ServiceCache();
public static Service getService(String serviceName) {
// 先从缓存查
Service service = cache.getService(serviceName);
if (service != null) {
System.out.println("从缓存返回服务: " + serviceName);
return service;
}
// 缓存没有,创建新的
// 这里可以用 JNDI 查找,也可以直接 new
service = lookupService(serviceName);
cache.addService(serviceName, service);
return service;
}
private static Service lookupService(String serviceName) {
// 模拟 JNDI 查找或直接创建
switch (serviceName) {
case "ServiceA":
return new ServiceA();
case "ServiceB":
return new ServiceB();
default:
throw new IllegalArgumentException("未知服务: " + serviceName);
}
}
}
客户端使用
public class Client {
public static void main(String[] args) {
Service serviceA = ServiceLocator.getService("ServiceA");
serviceA.execute();
Service serviceB = ServiceLocator.getService("ServiceB");
serviceB.execute();
// 第二次获取,走缓存
Service serviceAagain = ServiceLocator.getService("ServiceA");
serviceAagain.execute();
}
}
运行结果:
缓存服务: ServiceA
执行服务A的业务逻辑
缓存服务: ServiceB
执行服务B的业务逻辑
从缓存返回服务: ServiceA
执行服务A的业务逻辑
核心要点:服务定位器模式的关键在于缓存。第一次查找时创建并缓存,后续直接返回缓存实例。这能显著提升性能,尤其是服务创建成本较高时。
JNDI 与依赖注入的对比
说到服务定位器,就绕不开 JNDI 和依赖注入。我在项目中三种方式都用过,说说我的感受。
| 对比维度 | 服务定位器 | JNDI | 依赖注入 |
|---|---|---|---|
| 查找方式 | 通过名称查找 | 通过 JNDI 名称查找 | 容器自动注入 |
| 耦合度 | 中等(依赖定位器) | 高(依赖 JNDI API) | 低(只依赖接口) |
| 测试难度 | 中等(可 mock 定位器) | 高(需要 JNDI 环境) | 低(直接注入 mock) |
| 性能 | 高(有缓存) | 中(每次查找) | 高(单例模式) |
| 适用场景 | 遗留系统、EJB | Java EE 传统应用 | 现代 Spring 应用 |
我的建议:如果你在用 Spring 这类现代框架,优先用依赖注入。服务定位器更适合那些无法使用 DI 的遗留系统,或者你需要动态切换服务实现的场景。
避坑指南
我曾经在一个项目中过度使用服务定位器,结果踩了几个坑:
- 全局状态问题:服务定位器本质上是全局单例,缓存的服务实例也是全局共享的。如果服务有状态,多线程环境下容易出问题。我建议服务本身设计成无状态的。
- 测试困难:虽然比 JNDI 好一些,但服务定位器仍然是全局的。测试时如果不清理缓存,可能会影响其他测试用例。我后来用 ThreadLocal 做了隔离。
- 隐藏依赖:客户端代码里看不到依赖关系,只能通过运行时才能知道依赖了哪些服务。这给代码审查带来麻烦。
特别注意:服务定位器模式容易演变成反模式。如果你发现代码里到处都是 ServiceLocator.getService(),那说明你的架构设计可能有问题。这时候应该考虑引入依赖注入框架。
什么时候用?
说实话,现在新项目我很少直接用服务定位器。但有些场景它确实合适:
- 遗留系统改造:老系统里到处都是 new 对象,没法一下子改成 DI。用服务定位器做过渡,逐步替换。
- 插件式架构:需要动态加载和卸载服务,服务定位器配合 SPI 机制很好用。
- 性能敏感场景:服务创建成本高,且需要频繁获取。缓存机制能省不少时间。
嗯,服务定位器模式就讲到这里。它不算什么高大上的模式,但理解它背后的思想——集中管理、缓存加速——对你设计系统架构会有帮助。记住,没有银弹,选对场景才是关键。
公众号:蓝海资料掘金营,微信deep3321