服务定位器模式:别让服务查找成为你的噩梦

服务定位器模式,说白了就是给服务做个“黄页”。你想想看,当你的系统里有几十个服务,每个服务都要手动创建或查找,那代码得多乱?我早期做企业级应用时,就吃过这个亏——到处 new 对象,后来要换实现,改得我头皮发麻。

这个模式的核心思想很简单:把服务查找逻辑集中管理。它不像工厂模式那样创建对象,而是帮你找到已经注册好的服务实例。

模式结构:四个角色

服务定位器模式有四个核心参与者,我画了张图帮你理解:

服务定位器模式结构图 客户端 (Client) 服务定位器 缓存 (Cache) 服务接口 具体服务A 具体服务B 具体服务C 请求服务 查/写 创建/获取 缓存已创建的服务 避免重复创建

代码实现: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(),那说明你的架构设计可能有问题。这时候应该考虑引入依赖注入框架。

什么时候用?

说实话,现在新项目我很少直接用服务定位器。但有些场景它确实合适:

  1. 遗留系统改造:老系统里到处都是 new 对象,没法一下子改成 DI。用服务定位器做过渡,逐步替换。
  2. 插件式架构:需要动态加载和卸载服务,服务定位器配合 SPI 机制很好用。
  3. 性能敏感场景:服务创建成本高,且需要频繁获取。缓存机制能省不少时间。

嗯,服务定位器模式就讲到这里。它不算什么高大上的模式,但理解它背后的思想——集中管理、缓存加速——对你设计系统架构会有帮助。记住,没有银弹,选对场景才是关键。


公众号:蓝海资料掘金营,微信deep3321