[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-32":26,"\u002Farticles\u002Fdesign-pattern-32\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-32\u002Fadjacent":72,"\u002Fcategories":84,"render-design-pattern-32":110},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113935261715927041","design-pattern-32","设计模式修炼之路 ㉜ 服务定位器模式——服务只找一次，之后全靠缓存","查找与缓存一处收口、注册中心只被打扰 N 次、缓存的是服务不是结果、种子从 ㉗ 长成 static 注册台、字符串名字没有编译器保护、与业务代表对账","2113897628365230081",1,1926,7172,18,"2025-06-20T10:10:19","> J2EE 模式第七站。\n\n㉛ 结尾留了个问号：装配代码里，五个过滤器、两个控制器全是 new 出来摆好的。真实应用何止这几个角色——DAO、缓存、支付网关，谁需要谁自己造一份？有没有一个\"服务注册处\"，登记一次、按名取用？\n\n其实这颗种子 ㉗ 就埋下了：ServiceLookup 里那句\"查到就缓存\"，当时还是代表私有的一个字段。本篇让它长大——**把服务的查找与缓存收进一个全局注册台，第一次按名走注册中心，之后缓存直取**——服务定位器模式，J2EE 八式里的服务注册处。\n\n## 一、服务定位器模式是什么\n\nJ2EE 蓝皮书的定义大意：服务定位器（Service Locator）为服务的查找与创建提供全应用统一的入口，封装注册中心的访问细节，并把查到的服务缓存起来——客户端不必知道服务在哪儿、怎么建，按名取用即可。\n\n四个角色：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>角色\u003C\u002Fth>\u003Cth>干什么\u003C\u002Fth>\u003Cth>本文对应\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Service（服务）\u003C\u002Ftd>\u003Ctd>被查找的对象：重服务，建一份全应用共用\u003C\u002Ftd>\u003Ctd>DatabaseService \u002F PaymentService\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Initial Context（初始上下文）\u003C\u002Ftd>\u003Ctd>注册中心的封装：按名查找，又慢又贵\u003C\u002Ftd>\u003Ctd>InitialContext（模拟 JNDI）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Service Locator（服务定位器）\u003C\u002Ftd>\u003Ctd>唯一入口：第一次走注册中心并缓存，之后直取\u003C\u002Ftd>\u003Ctd>ServiceLocator\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Client（客户端）\u003C\u002Ftd>\u003Ctd>要服务只找定位器\u003C\u002Ftd>\u003Ctd>OrderService \u002F ReportService\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n一句话理解分工：**客户端说\"我要 database\"，定位器说\"第一趟我去注册中心拿，之后问我就行\"，注册中心说\"名字到服务，只此一家\"。**\n\n它不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉗ 是直系血亲：㉗ 的 ServiceLookup 是它的种子（单字段、代表私有），㉜ 长大成全局注册台（static Map、全应用一份）。第六节把这爷俩的账摊开算。\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **查找逻辑一处收口**：全应用只有定位器认得注册中心——业务代码里再没有 InitialContext、再没有\"怎么找\"的细节。反面教材里每家自己连注册中心的 N 份拷贝，在这里是一份。\n2. **缓存直取**：注册中心只被打扰 N 次（N = 服务个数），之后全是内存级命中。lookup 一次 800ms 的痛，一次性付清。\n3. **重服务全局一份**：连接池只建一套、支付通道只建一条。反面教材里 database 被查两次就建两套池——浪费的不只是两个 800ms，还有白白占着的 10 个连接。\n4. **按名解耦**：客户端只认名字不认实现——注册中心那边 database 换个实现，客户端一行不用改。\n\n**缺点：**\n\n1. **静态全局，测试难**：单测想换个假 database？得先想办法清掉 static 缓存，或者接受拿真服务跑测试。依赖注入把依赖从构造函数送进来，天生好测——这是后来 DI 容器赢下江湖的核心一手。\n2. **依赖被藏起来了**：构造函数干干净净，打开类体才发现 get(\"database\")、get(\"payment\")——这个类依赖了什么，签名上看不出来，只能全文搜。\n3. **名字是字符串，编译器不管**：get(\"databse\") 拼错了照样编译通过，运行时才炸——细节三展开。\n\n## 三、什么时候用\n\n判断标准：**服务来源在别处（注册中心\u002F远程）、查找贵、服务全局一份**——三条都占，定位器就值回票价。\n\n- **典型场景**：JNDI 时代的 EJB 查找——lookup 要连注册中心走网络，贵得理直气壮\n- **日常场景**：日志门面 LoggerFactory.getLogger——按名取 logger、内部缓存，你天天在用的服务定位器（第五节见）\n- **反面对照**：服务就在本地、直接 new 不贵——满地 ServiceLocator.get 是给简单问题套重壳。更现实的忠告：今天写新项目，同样的事首选依赖注入（Spring 把服务\"送上门\"，而不是\"你去取\"）——定位器是 DI 普及前的主流答案，它的三条缺点，恰好逐条定义了 DI 的优点\n\n## 四、实现方式：注册台怎么搭\n\n场景：一个迷你应用，两个重服务——database（连接池）和 payment（支付网关），住在注册中心后面，查一次 800ms。两个业务组件都要用：下单的要 database + payment，出日报的只要 database。\n\n### 4.1 反面教材：每个组件自己找服务\n\n```java\n\u002F** 反面教材：业务代码里粘满\"怎么找服务\"的细节 *\u002F\npublic class BadOrderService {\n\n    public void placeOrder(String item) {\n        System.out.println(\"   [BadOrderService] 要用数据库了，自己去找：\");\n        DatabaseService database = new InitialContext().lookup(\"database\");\n\n        System.out.println(\"   [BadOrderService] 要用支付了，自己去找：\");\n        PaymentService payment = new InitialContext().lookup(\"payment\");\n\n        System.out.println(\"   [BadOrderService] 业务：下单 \" + item);\n        database.save(\"订单：\" + item);\n        payment.charge(\"订单：\" + item);\n    }\n}\n```\n\nBadReportService 同款，只要 database——又一次 lookup。运行输出：\n\n```\n========== 反面教材：每个组件自己找服务 ==========\n-- OrderService 下单（u001 买一本书） --\n   [BadOrderService] 要用数据库了，自己去找：\n   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\n   [DatabaseService] 初始化连接池（10 个连接）\n   [BadOrderService] 要用支付了，自己去找：\n   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\n   [PaymentService] 建立支付通道（对接银行）\n   [BadOrderService] 业务：下单 u001 买一本书\n   [DatabaseService] 入库：订单：u001 买一本书\n   [PaymentService] 扣款：订单：u001 买一本书\n\n-- ReportService 出日报 --\n   [BadReportService] 要用数据库了，自己去找：\n   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\n   [DatabaseService] 初始化连接池（10 个连接）\n   [BadReportService] 业务：出日报\n   [DatabaseService] 查询：昨天的订单汇总\n   同一个 database 被 lookup 了两次——注册中心被打扰两次，连接池也建了两套\n\n-- 再来一单（u002 买两本书） --\n   [BadOrderService] 要用数据库了，自己去找：\n   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\n   [DatabaseService] 初始化连接池（10 个连接）\n   [BadOrderService] 要用支付了，自己去找：\n   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\n   [PaymentService] 建立支付通道（对接银行）\n   [BadOrderService] 业务：下单 u002 买两本书\n   [DatabaseService] 入库：订单：u002 买两本书\n   [PaymentService] 扣款：订单：u002 买两本书\n   又是两次 lookup——服务明明全局一个，谁用谁新找一遍\n```\n\n病灶：**服务是全局的，找服务的事却是每家自己干**——同一个 database 被 lookup 了两次，连接池建了两套；u002 再下单，又是两次 lookup，前一次的成果没人记得。\"查找贵 + 服务全局一份\"这两个前提一叠加，\"谁用谁自己找\"就是结构性浪费。注意：功能一切正常，订单照入库、日报照出——反面教材从来不是功能错误，是结构错误。\n\n### 4.2 正解：服务定位器\n\n**注册中心原样不动**——定位器不改造世界，只是在它前面搭了个柜台：\n\n```java\npublic class InitialContext {\n\n    @SuppressWarnings(\"unchecked\")\n    public \u003CT> T lookup(String name) {\n        System.out.println(\"   [注册中心] 连接注册中心查找 \" + name + \" ……（耗时 800ms）\");\n        switch (name) {\n            case \"database\":\n                return (T) new DatabaseService();\n            case \"payment\":\n                return (T) new PaymentService();\n            default:\n                throw new IllegalArgumentException(\"没有这个服务：\" + name);\n        }\n    }\n}\n```\n\n**定位器——私有构造 + static 缓存，全应用唯一注册台**：\n\n```java\npublic class ServiceLocator {\n\n    private static final Map\u003CString, Object> cache = new HashMap\u003C>();\n    private static final InitialContext context = new InitialContext();\n\n    private ServiceLocator() {\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static \u003CT> T get(String name) {\n        if (!cache.containsKey(name)) {\n            System.out.println(\"   [服务定位器] 缓存没有 \" + name + \"，去注册中心拿\");\n            cache.put(name, context.lookup(name));\n            System.out.println(\"   [服务定位器] \" + name + \" 已入缓存\");\n        } else {\n            System.out.println(\"   [服务定位器] 缓存命中：\" + name);\n        }\n        return (T) cache.get(name);\n    }\n}\n```\n\n**客户端——要用服务，一行 get**：\n\n```java\npublic class OrderService {\n\n    public void placeOrder(String item) {\n        DatabaseService database = ServiceLocator.get(\"database\");\n        PaymentService payment = ServiceLocator.get(\"payment\");\n\n        System.out.println(\"   [OrderService] 业务：下单 \" + item);\n        database.save(\"订单：\" + item);\n        payment.charge(\"订单：\" + item);\n    }\n}\n```\n\nReportService 同款，只要 database——一样的入口，一样的缓存。运行输出：\n\n```\n========== 正解：服务定位器 ==========\n-- OrderService 下单（u001 买一本书） --\n   [服务定位器] 缓存没有 database，去注册中心拿\n   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\n   [DatabaseService] 初始化连接池（10 个连接）\n   [服务定位器] database 已入缓存\n   [服务定位器] 缓存没有 payment，去注册中心拿\n   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\n   [PaymentService] 建立支付通道（对接银行）\n   [服务定位器] payment 已入缓存\n   [OrderService] 业务：下单 u001 买一本书\n   [DatabaseService] 入库：订单：u001 买一本书\n   [PaymentService] 扣款：订单：u001 买一本书\n\n-- ReportService 出日报 --\n   [服务定位器] 缓存命中：database\n   [ReportService] 业务：出日报\n   [DatabaseService] 查询：昨天的订单汇总\n\n-- 再来一单（u002 买两本书） --\n   [服务定位器] 缓存命中：database\n   [服务定位器] 缓存命中：payment\n   [OrderService] 业务：下单 u002 买两本书\n   [DatabaseService] 入库：订单：u002 买两本书\n   [PaymentService] 扣款：订单：u002 买两本书\n   注册中心全程只被打扰两次（database、payment 各一次），之后全是缓存直取\n```\n\n四处对照读出含金量：第一次下单，database、payment 各走一趟注册中心，**800ms 只付一次**；ReportService 要 database，**缓存命中**——注册中心没被打扰，连接池还是那一套；u002 再下单，**两个都命中**——注册中心全程只被打扰两次，服务有几个就打扰几次，之后永远缓存直取；再对照客户端代码——BadOrderService 四行找服务两行业务，OrderService 两行 get 两行业务，**技术细节全进了定位器，业务方法里只剩业务**。\n\n### 4.3 三处细节\n\n**细节一：缓存的是服务本身，不是调用结果。** 定位器缓存的是 DatabaseService 这个对象的引用——连接池、支付通道这类重服务，建好了全应用反复用；它不缓存 query() 的返回值——那叫业务缓存，是另一层的事。两层缓存搅在一起，是新手最常见的误会。\n\n**细节二：从\"各存各的\"到\"全局注册台\"。** ㉗ 的种子是单字段：`private OrderService orderService`——代表私有，每个代表各缓存各的，别人沾不到光；㉜ 长大成 `static Map`——全应用一份，跨组件共享，ReportService 才能命中 OrderService 存下的 database。缓存挪了个作用域，模式的身份就变了：从代表身上的一个技巧，变成全应用的公共设施。\n\n**细节三：泛型强转是运行时赌博。** `PaymentService payment = ServiceLocator.get(\"database\")`——泛型方法照单全收，编译器一声不吭，运行时 ClassCastException 才炸。字符串名字天生没有编译期约束，拼错名字同罪；DI 容器用\"类型注入\"治好了这一半——按类型给依赖，装错了编译期就报。\n\n## 五、在框架里找它\n\n1. **JNDI 原装**：EJB 时代，InitialContext.lookup(\"java:comp\u002Fenv\u002Fjdbc\u002FOrderDS\") + 一个 ServiceLocator 单例，是蓝皮书标准姿势；Tomcat 里配的 DataSource 至今还挂在 JNDI 树上，Spring 都留了 jndi-lookup 专门去取。\n\n2. **SLF4J 的 LoggerFactory**：getLogger(\"com.foo.OrderService\")——按名取 logger，内部缓存，同名永远同一个实例。你天天在用服务定位器，只是没人叫过它这个名字。\n\n3. **Spring ApplicationContext**：getBean(\"orderService\") 的本质就是一个超级服务定位器——按名取、有缓存（默认单例）。但 Spring 官方把\"满地 getBean\"明确定性为反模式，推崇 @Autowired 注入——同一个容器，取的方式决定它是模式还是反模式。\n\n4. **java.util.ServiceLoader（SPI）**：按接口找实现并实例化——JDBC 驱动的自动加载就靠它。名字从\"字符串\"升级成\"接口\"，细节三的病好了一半，思想同源：组件不用管实现从哪儿来。\n\n5. **Fowler 的判词**：《企业应用架构模式》（PoEAA）给它正名；后来同一人又在《IoC 容器与依赖注入》一文中亲自给它补了一刀——与 DI 相比，定位器把依赖藏进类体、单测难以替换。它输给 DI 的两条，恰好就是本文缺点一、二——一个模式的退位檄文，早就写在它的缺点里。\n\n## 六、对账——㉗ 业务代表 vs ㉜ 服务定位器\n\n㉗ 讲业务代表时，ServiceLookup 只是代表身上的一个零件；㉜ 把这个零件拎出来单独立户。爷俩摊开对账：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>\u003C\u002Fth>\u003Cth>业务代表（㉗）\u003C\u002Fth>\u003Cth>服务定位器（㉜）\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>管什么\u003C\u002Ftd>\u003Ctd>表示层与业务层之间的缓冲带\u003C\u002Ftd>\u003Ctd>服务从哪儿来\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>一句话职责\u003C\u002Ftd>\u003Ctd>重试、异常翻译——把远程说得像本地\u003C\u002Ftd>\u003Ctd>查找、缓存——让注册中心只被打扰一次\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>缓存归谁\u003C\u002Ftd>\u003Ctd>代表私有（每个代表各一份）\u003C\u002Ftd>\u003Ctd>全局 static（全应用一份）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>客户端拿到\u003C\u002Ftd>\u003Ctd>业务话术方法（placeOrder）\u003C\u002Ftd>\u003Ctd>服务对象本身（get(\"database\")）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>典型同框\u003C\u002Ftd>\u003Ctd>EJB 标准答案：Session Facade + Business Delegate + Service Locator 三件套，代表内部用定位器找服务\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n一句话：**㉗ 管\"怎么把远程服务说得像本地话\"，㉜ 管\"服务从哪儿来、只找一次\"**——一个管通话质量，一个管通讯录。顺带一笔：定位器的 static Map 本质是 ② 单例思想——② 管\"这个类全应用只有一个实例\"，㉜ 管\"这些服务全应用只找一次\"；再往前走一步，Spring 容器就是\"全局单例注册表 + 类型注入\"的合体——㉜ 和 ②，在 DI 容器里会师。\n\n## 写在最后\n\n- 服务定位器 = 查找与缓存的统一收口：第一次按名走注册中心，之后缓存直取\n- 疗效：注册中心只被打扰 N 次（N = 服务数），重服务全局一份，客户端一行 get\n- 代价：静态全局测试难、依赖藏进类体、字符串名字编译器不查——三条缺点逐条定义了 DI 的优点\n- 铁律：缓存的是服务引用，不是调用结果——和业务缓存是两层，别搅在一起\n- 种子发芽：㉗ 代表私有的单字段缓存，长成 ㉜ 的 static 注册台；再与 ② 单例会师于 DI 容器\n- 当代转世：JNDI、LoggerFactory.getLogger、getBean、ServiceLoader——按名取用，全球通吃\n\n服务定位器把\"服务从哪儿来\"安排得明明白白——可服务到手之后，数据怎么回去？出日报查回来的汇总要递给调用方，顺着网络递对象，总不能把实体整个扔过去：它身上挂着行为、挂着一整窝依赖对象——㉘ 说过，组合实体住在服务器上。客户端要的只是一份干干净净、拎起来就能走的数据快照——传输对象模式，下一篇见。\n","\u003Cblockquote>\n \u003Cp>J2EE 模式第七站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉛ 结尾留了个问号：装配代码里，五个过滤器、两个控制器全是 new 出来摆好的。真实应用何止这几个角色——DAO、缓存、支付网关，谁需要谁自己造一份？有没有一个\"服务注册处\"，登记一次、按名取用？\u003C\u002Fp>\n\u003Cp>其实这颗种子 ㉗ 就埋下了：ServiceLookup 里那句\"查到就缓存\"，当时还是代表私有的一个字段。本篇让它长大——\u003Cstrong>把服务的查找与缓存收进一个全局注册台，第一次按名走注册中心，之后缓存直取\u003C\u002Fstrong>——服务定位器模式，J2EE 八式里的服务注册处。\u003C\u002Fp>\n\u003Ch2>一、服务定位器模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：服务定位器（Service Locator）为服务的查找与创建提供全应用统一的入口，封装注册中心的访问细节，并把查到的服务缓存起来——客户端不必知道服务在哪儿、怎么建，按名取用即可。\u003C\u002Fp>\n\u003Cp>四个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Service（服务）\u003C\u002Ftd>\n   \u003Ctd>被查找的对象：重服务，建一份全应用共用\u003C\u002Ftd>\n   \u003Ctd>DatabaseService \u002F PaymentService\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Initial Context（初始上下文）\u003C\u002Ftd>\n   \u003Ctd>注册中心的封装：按名查找，又慢又贵\u003C\u002Ftd>\n   \u003Ctd>InitialContext（模拟 JNDI）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Service Locator（服务定位器）\u003C\u002Ftd>\n   \u003Ctd>唯一入口：第一次走注册中心并缓存，之后直取\u003C\u002Ftd>\n   \u003Ctd>ServiceLocator\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Client（客户端）\u003C\u002Ftd>\n   \u003Ctd>要服务只找定位器\u003C\u002Ftd>\n   \u003Ctd>OrderService \u002F ReportService\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>客户端说\"我要 database\"，定位器说\"第一趟我去注册中心拿，之后问我就行\"，注册中心说\"名字到服务，只此一家\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>它不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉗ 是直系血亲：㉗ 的 ServiceLookup 是它的种子（单字段、代表私有），㉜ 长大成全局注册台（static Map、全应用一份）。第六节把这爷俩的账摊开算。\u003C\u002Fp>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>查找逻辑一处收口\u003C\u002Fstrong>：全应用只有定位器认得注册中心——业务代码里再没有 InitialContext、再没有\"怎么找\"的细节。反面教材里每家自己连注册中心的 N 份拷贝，在这里是一份。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>缓存直取\u003C\u002Fstrong>：注册中心只被打扰 N 次（N = 服务个数），之后全是内存级命中。lookup 一次 800ms 的痛，一次性付清。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>重服务全局一份\u003C\u002Fstrong>：连接池只建一套、支付通道只建一条。反面教材里 database 被查两次就建两套池——浪费的不只是两个 800ms，还有白白占着的 10 个连接。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>按名解耦\u003C\u002Fstrong>：客户端只认名字不认实现——注册中心那边 database 换个实现，客户端一行不用改。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>静态全局，测试难\u003C\u002Fstrong>：单测想换个假 database？得先想办法清掉 static 缓存，或者接受拿真服务跑测试。依赖注入把依赖从构造函数送进来，天生好测——这是后来 DI 容器赢下江湖的核心一手。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>依赖被藏起来了\u003C\u002Fstrong>：构造函数干干净净，打开类体才发现 get(\"database\")、get(\"payment\")——这个类依赖了什么，签名上看不出来，只能全文搜。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>名字是字符串，编译器不管\u003C\u002Fstrong>：get(\"databse\") 拼错了照样编译通过，运行时才炸——细节三展开。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>服务来源在别处（注册中心\u002F远程）、查找贵、服务全局一份\u003C\u002Fstrong>——三条都占，定位器就值回票价。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型场景\u003C\u002Fstrong>：JNDI 时代的 EJB 查找——lookup 要连注册中心走网络，贵得理直气壮\u003C\u002Fli>\n \u003Cli>\u003Cstrong>日常场景\u003C\u002Fstrong>：日志门面 LoggerFactory.getLogger——按名取 logger、内部缓存，你天天在用的服务定位器（第五节见）\u003C\u002Fli>\n \u003Cli>\u003Cstrong>反面对照\u003C\u002Fstrong>：服务就在本地、直接 new 不贵——满地 ServiceLocator.get 是给简单问题套重壳。更现实的忠告：今天写新项目，同样的事首选依赖注入（Spring 把服务\"送上门\"，而不是\"你去取\"）——定位器是 DI 普及前的主流答案，它的三条缺点，恰好逐条定义了 DI 的优点\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>四、实现方式：注册台怎么搭\u003C\u002Fh2>\n\u003Cp>场景：一个迷你应用，两个重服务——database（连接池）和 payment（支付网关），住在注册中心后面，查一次 800ms。两个业务组件都要用：下单的要 database + payment，出日报的只要 database。\u003C\u002Fp>\n\u003Ch3>4.1 反面教材：每个组件自己找服务\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F** 反面教材：业务代码里粘满\"怎么找服务\"的细节 *\u002F\npublic class BadOrderService {\n\n    public void placeOrder(String item) {\n        System.out.println(\"   [BadOrderService] 要用数据库了，自己去找：\");\n        DatabaseService database = new InitialContext().lookup(\"database\");\n\n        System.out.println(\"   [BadOrderService] 要用支付了，自己去找：\");\n        PaymentService payment = new InitialContext().lookup(\"payment\");\n\n        System.out.println(\"   [BadOrderService] 业务：下单 \" + item);\n        database.save(\"订单：\" + item);\n        payment.charge(\"订单：\" + item);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>BadReportService 同款，只要 database——又一次 lookup。运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 反面教材：每个组件自己找服务 ==========\n-- OrderService 下单（u001 买一本书） --\n   [BadOrderService] 要用数据库了，自己去找：\n   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\n   [DatabaseService] 初始化连接池（10 个连接）\n   [BadOrderService] 要用支付了，自己去找：\n   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\n   [PaymentService] 建立支付通道（对接银行）\n   [BadOrderService] 业务：下单 u001 买一本书\n   [DatabaseService] 入库：订单：u001 买一本书\n   [PaymentService] 扣款：订单：u001 买一本书\n\n-- ReportService 出日报 --\n   [BadReportService] 要用数据库了，自己去找：\n   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\n   [DatabaseService] 初始化连接池（10 个连接）\n   [BadReportService] 业务：出日报\n   [DatabaseService] 查询：昨天的订单汇总\n   同一个 database 被 lookup 了两次——注册中心被打扰两次，连接池也建了两套\n\n-- 再来一单（u002 买两本书） --\n   [BadOrderService] 要用数据库了，自己去找：\n   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\n   [DatabaseService] 初始化连接池（10 个连接）\n   [BadOrderService] 要用支付了，自己去找：\n   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\n   [PaymentService] 建立支付通道（对接银行）\n   [BadOrderService] 业务：下单 u002 买两本书\n   [DatabaseService] 入库：订单：u002 买两本书\n   [PaymentService] 扣款：订单：u002 买两本书\n   又是两次 lookup——服务明明全局一个，谁用谁新找一遍\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>病灶：\u003Cstrong>服务是全局的，找服务的事却是每家自己干\u003C\u002Fstrong>——同一个 database 被 lookup 了两次，连接池建了两套；u002 再下单，又是两次 lookup，前一次的成果没人记得。\"查找贵 + 服务全局一份\"这两个前提一叠加，\"谁用谁自己找\"就是结构性浪费。注意：功能一切正常，订单照入库、日报照出——反面教材从来不是功能错误，是结构错误。\u003C\u002Fp>\n\u003Ch3>4.2 正解：服务定位器\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>注册中心原样不动\u003C\u002Fstrong>——定位器不改造世界，只是在它前面搭了个柜台：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class InitialContext {\n\n    @SuppressWarnings(\"unchecked\")\n    public &lt;T&gt; T lookup(String name) {\n        System.out.println(\"   [注册中心] 连接注册中心查找 \" + name + \" ……（耗时 800ms）\");\n        switch (name) {\n            case \"database\":\n                return (T) new DatabaseService();\n            case \"payment\":\n                return (T) new PaymentService();\n            default:\n                throw new IllegalArgumentException(\"没有这个服务：\" + name);\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>定位器——私有构造 + static 缓存，全应用唯一注册台\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class ServiceLocator {\n\n    private static final Map&lt;String, Object&gt; cache = new HashMap&lt;&gt;();\n    private static final InitialContext context = new InitialContext();\n\n    private ServiceLocator() {\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static &lt;T&gt; T get(String name) {\n        if (!cache.containsKey(name)) {\n            System.out.println(\"   [服务定位器] 缓存没有 \" + name + \"，去注册中心拿\");\n            cache.put(name, context.lookup(name));\n            System.out.println(\"   [服务定位器] \" + name + \" 已入缓存\");\n        } else {\n            System.out.println(\"   [服务定位器] 缓存命中：\" + name);\n        }\n        return (T) cache.get(name);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>客户端——要用服务，一行 get\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class OrderService {\n\n    public void placeOrder(String item) {\n        DatabaseService database = ServiceLocator.get(\"database\");\n        PaymentService payment = ServiceLocator.get(\"payment\");\n\n        System.out.println(\"   [OrderService] 业务：下单 \" + item);\n        database.save(\"订单：\" + item);\n        payment.charge(\"订单：\" + item);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>ReportService 同款，只要 database——一样的入口，一样的缓存。运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 正解：服务定位器 ==========\n-- OrderService 下单（u001 买一本书） --\n   [服务定位器] 缓存没有 database，去注册中心拿\n   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\n   [DatabaseService] 初始化连接池（10 个连接）\n   [服务定位器] database 已入缓存\n   [服务定位器] 缓存没有 payment，去注册中心拿\n   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\n   [PaymentService] 建立支付通道（对接银行）\n   [服务定位器] payment 已入缓存\n   [OrderService] 业务：下单 u001 买一本书\n   [DatabaseService] 入库：订单：u001 买一本书\n   [PaymentService] 扣款：订单：u001 买一本书\n\n-- ReportService 出日报 --\n   [服务定位器] 缓存命中：database\n   [ReportService] 业务：出日报\n   [DatabaseService] 查询：昨天的订单汇总\n\n-- 再来一单（u002 买两本书） --\n   [服务定位器] 缓存命中：database\n   [服务定位器] 缓存命中：payment\n   [OrderService] 业务：下单 u002 买两本书\n   [DatabaseService] 入库：订单：u002 买两本书\n   [PaymentService] 扣款：订单：u002 买两本书\n   注册中心全程只被打扰两次（database、payment 各一次），之后全是缓存直取\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>四处对照读出含金量：第一次下单，database、payment 各走一趟注册中心，\u003Cstrong>800ms 只付一次\u003C\u002Fstrong>；ReportService 要 database，\u003Cstrong>缓存命中\u003C\u002Fstrong>——注册中心没被打扰，连接池还是那一套；u002 再下单，\u003Cstrong>两个都命中\u003C\u002Fstrong>——注册中心全程只被打扰两次，服务有几个就打扰几次，之后永远缓存直取；再对照客户端代码——BadOrderService 四行找服务两行业务，OrderService 两行 get 两行业务，\u003Cstrong>技术细节全进了定位器，业务方法里只剩业务\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3>4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：缓存的是服务本身，不是调用结果。\u003C\u002Fstrong> 定位器缓存的是 DatabaseService 这个对象的引用——连接池、支付通道这类重服务，建好了全应用反复用；它不缓存 query() 的返回值——那叫业务缓存，是另一层的事。两层缓存搅在一起，是新手最常见的误会。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：从\"各存各的\"到\"全局注册台\"。\u003C\u002Fstrong> ㉗ 的种子是单字段：\u003Ccode>private OrderService orderService\u003C\u002Fcode>——代表私有，每个代表各缓存各的，别人沾不到光；㉜ 长大成 \u003Ccode>static Map\u003C\u002Fcode>——全应用一份，跨组件共享，ReportService 才能命中 OrderService 存下的 database。缓存挪了个作用域，模式的身份就变了：从代表身上的一个技巧，变成全应用的公共设施。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：泛型强转是运行时赌博。\u003C\u002Fstrong> \u003Ccode>PaymentService payment = ServiceLocator.get(\"database\")\u003C\u002Fcode>——泛型方法照单全收，编译器一声不吭，运行时 ClassCastException 才炸。字符串名字天生没有编译期约束，拼错名字同罪；DI 容器用\"类型注入\"治好了这一半——按类型给依赖，装错了编译期就报。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>JNDI 原装\u003C\u002Fstrong>：EJB 时代，InitialContext.lookup(\"java:comp\u002Fenv\u002Fjdbc\u002FOrderDS\") + 一个 ServiceLocator 单例，是蓝皮书标准姿势；Tomcat 里配的 DataSource 至今还挂在 JNDI 树上，Spring 都留了 jndi-lookup 专门去取。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>SLF4J 的 LoggerFactory\u003C\u002Fstrong>：getLogger(\"com.foo.OrderService\")——按名取 logger，内部缓存，同名永远同一个实例。你天天在用服务定位器，只是没人叫过它这个名字。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring ApplicationContext\u003C\u002Fstrong>：getBean(\"orderService\") 的本质就是一个超级服务定位器——按名取、有缓存（默认单例）。但 Spring 官方把\"满地 getBean\"明确定性为反模式，推崇 @Autowired 注入——同一个容器，取的方式决定它是模式还是反模式。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>java.util.ServiceLoader（SPI）\u003C\u002Fstrong>：按接口找实现并实例化——JDBC 驱动的自动加载就靠它。名字从\"字符串\"升级成\"接口\"，细节三的病好了一半，思想同源：组件不用管实现从哪儿来。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Fowler 的判词\u003C\u002Fstrong>：《企业应用架构模式》（PoEAA）给它正名；后来同一人又在《IoC 容器与依赖注入》一文中亲自给它补了一刀——与 DI 相比，定位器把依赖藏进类体、单测难以替换。它输给 DI 的两条，恰好就是本文缺点一、二——一个模式的退位檄文，早就写在它的缺点里。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>六、对账——㉗ 业务代表 vs ㉜ 服务定位器\u003C\u002Fh2>\n\u003Cp>㉗ 讲业务代表时，ServiceLookup 只是代表身上的一个零件；㉜ 把这个零件拎出来单独立户。爷俩摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>业务代表（㉗）\u003C\u002Fth>\n   \u003Cth>服务定位器（㉜）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>管什么\u003C\u002Ftd>\n   \u003Ctd>表示层与业务层之间的缓冲带\u003C\u002Ftd>\n   \u003Ctd>服务从哪儿来\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>一句话职责\u003C\u002Ftd>\n   \u003Ctd>重试、异常翻译——把远程说得像本地\u003C\u002Ftd>\n   \u003Ctd>查找、缓存——让注册中心只被打扰一次\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>缓存归谁\u003C\u002Ftd>\n   \u003Ctd>代表私有（每个代表各一份）\u003C\u002Ftd>\n   \u003Ctd>全局 static（全应用一份）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>客户端拿到\u003C\u002Ftd>\n   \u003Ctd>业务话术方法（placeOrder）\u003C\u002Ftd>\n   \u003Ctd>服务对象本身（get(\"database\")）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>典型同框\u003C\u002Ftd>\n   \u003Ctd>EJB 标准答案：Session Facade + Business Delegate + Service Locator 三件套，代表内部用定位器找服务\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话：\u003Cstrong>㉗ 管\"怎么把远程服务说得像本地话\"，㉜ 管\"服务从哪儿来、只找一次\"\u003C\u002Fstrong>——一个管通话质量，一个管通讯录。顺带一笔：定位器的 static Map 本质是 ② 单例思想——② 管\"这个类全应用只有一个实例\"，㉜ 管\"这些服务全应用只找一次\"；再往前走一步，Spring 容器就是\"全局单例注册表 + 类型注入\"的合体——㉜ 和 ②，在 DI 容器里会师。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>服务定位器 = 查找与缓存的统一收口：第一次按名走注册中心，之后缓存直取\u003C\u002Fli>\n \u003Cli>疗效：注册中心只被打扰 N 次（N = 服务数），重服务全局一份，客户端一行 get\u003C\u002Fli>\n \u003Cli>代价：静态全局测试难、依赖藏进类体、字符串名字编译器不查——三条缺点逐条定义了 DI 的优点\u003C\u002Fli>\n \u003Cli>铁律：缓存的是服务引用，不是调用结果——和业务缓存是两层，别搅在一起\u003C\u002Fli>\n \u003Cli>种子发芽：㉗ 代表私有的单字段缓存，长成 ㉜ 的 static 注册台；再与 ② 单例会师于 DI 容器\u003C\u002Fli>\n \u003Cli>当代转世：JNDI、LoggerFactory.getLogger、getBean、ServiceLoader——按名取用，全球通吃\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>服务定位器把\"服务从哪儿来\"安排得明明白白——可服务到手之后，数据怎么回去？出日报查回来的汇总要递给调用方，顺着网络递对象，总不能把实体整个扔过去：它身上挂着行为、挂着一整窝依赖对象——㉘ 说过，组合实体住在服务器上。客户端要的只是一份干干净净、拎起来就能走的数据快照——传输对象模式，下一篇见。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113934922031828994","design-pattern-27","设计模式修炼之路 ㉗ 业务代表模式——替表示层挡住远程的脏活","替表示层挡住远程脏活、查找+重试+异常翻译一肩挑、重试风暴、外观\u002F代理\u002F业务代表三兄弟对账、Feign 转世",1811,6147,16,"2025-03-25T21:44:13",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113935138768293890","design-pattern-30","设计模式修炼之路 ㉚ 前端控制器模式——所有请求，先过我这道门","横切关注点写一次全站无死角、抄漏一处校验就是一个洞、门神必须薄、路由分散声明集中执行、与 MVC 的 C 对账",2755,5825,15,"2025-05-12T18:40:38",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113935321048551425","design-pattern-33","设计模式修炼之路 ㉝ 传输对象模式——一趟网络，带走整份数据","J2EE 模式第八站，也是 J2EE 八式的收官一站。",2516,8059,21,"2025-07-09T21:16:41",{"code":4,"message":5,"data":73},{"prev":74,"next":83},{"id":75,"slug":76,"title":77,"summary":78,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":79,"likeCount":4,"wordCount":80,"readingMinutes":81,"publishedAt":82},"2113935201573801985","design-pattern-31","设计模式修炼之路 ㉛ 拦截过滤器模式——一个过滤器，只管一件事","一个过滤器一个横切关注点、return 走不出的审计盲区、契约两句话放行就 next、后置代码包住整条链、顺序即范围、与责任链对账",1038,7452,19,"2025-05-27T20:35:21",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},{"code":4,"message":5,"data":85},[86,92,97,103],{"id":87,"name":88,"slug":89,"description":90,"sortOrder":4,"articleCount":91},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":93,"slug":94,"description":95,"sortOrder":4,"articleCount":96},"设计模式","design-pattern","设计模式系列","34",{"id":98,"name":99,"slug":100,"description":101,"sortOrder":33,"articleCount":102},"1946000000000000002","随笔","essay","生活与思考","0",{"id":104,"name":105,"slug":106,"description":107,"sortOrder":108,"articleCount":109},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":111,"toc":112},"\u003Cblockquote>\n \u003Cp>J2EE 模式第七站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉛ 结尾留了个问号：装配代码里，五个过滤器、两个控制器全是 new 出来摆好的。真实应用何止这几个角色——DAO、缓存、支付网关，谁需要谁自己造一份？有没有一个\"服务注册处\"，登记一次、按名取用？\u003C\u002Fp>\n\u003Cp>其实这颗种子 ㉗ 就埋下了：ServiceLookup 里那句\"查到就缓存\"，当时还是代表私有的一个字段。本篇让它长大——\u003Cstrong>把服务的查找与缓存收进一个全局注册台，第一次按名走注册中心，之后缓存直取\u003C\u002Fstrong>——服务定位器模式，J2EE 八式里的服务注册处。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、服务定位器模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：服务定位器（Service Locator）为服务的查找与创建提供全应用统一的入口，封装注册中心的访问细节，并把查到的服务缓存起来——客户端不必知道服务在哪儿、怎么建，按名取用即可。\u003C\u002Fp>\n\u003Cp>四个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Service（服务）\u003C\u002Ftd>\n   \u003Ctd>被查找的对象：重服务，建一份全应用共用\u003C\u002Ftd>\n   \u003Ctd>DatabaseService \u002F PaymentService\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Initial Context（初始上下文）\u003C\u002Ftd>\n   \u003Ctd>注册中心的封装：按名查找，又慢又贵\u003C\u002Ftd>\n   \u003Ctd>InitialContext（模拟 JNDI）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Service Locator（服务定位器）\u003C\u002Ftd>\n   \u003Ctd>唯一入口：第一次走注册中心并缓存，之后直取\u003C\u002Ftd>\n   \u003Ctd>ServiceLocator\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Client（客户端）\u003C\u002Ftd>\n   \u003Ctd>要服务只找定位器\u003C\u002Ftd>\n   \u003Ctd>OrderService \u002F ReportService\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>客户端说\"我要 database\"，定位器说\"第一趟我去注册中心拿，之后问我就行\"，注册中心说\"名字到服务，只此一家\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>它不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉗ 是直系血亲：㉗ 的 ServiceLookup 是它的种子（单字段、代表私有），㉜ 长大成全局注册台（static Map、全应用一份）。第六节把这爷俩的账摊开算。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>查找逻辑一处收口\u003C\u002Fstrong>：全应用只有定位器认得注册中心——业务代码里再没有 InitialContext、再没有\"怎么找\"的细节。反面教材里每家自己连注册中心的 N 份拷贝，在这里是一份。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>缓存直取\u003C\u002Fstrong>：注册中心只被打扰 N 次（N = 服务个数），之后全是内存级命中。lookup 一次 800ms 的痛，一次性付清。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>重服务全局一份\u003C\u002Fstrong>：连接池只建一套、支付通道只建一条。反面教材里 database 被查两次就建两套池——浪费的不只是两个 800ms，还有白白占着的 10 个连接。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>按名解耦\u003C\u002Fstrong>：客户端只认名字不认实现——注册中心那边 database 换个实现，客户端一行不用改。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>静态全局，测试难\u003C\u002Fstrong>：单测想换个假 database？得先想办法清掉 static 缓存，或者接受拿真服务跑测试。依赖注入把依赖从构造函数送进来，天生好测——这是后来 DI 容器赢下江湖的核心一手。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>依赖被藏起来了\u003C\u002Fstrong>：构造函数干干净净，打开类体才发现 get(\"database\")、get(\"payment\")——这个类依赖了什么，签名上看不出来，只能全文搜。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>名字是字符串，编译器不管\u003C\u002Fstrong>：get(\"databse\") 拼错了照样编译通过，运行时才炸——细节三展开。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>服务来源在别处（注册中心\u002F远程）、查找贵、服务全局一份\u003C\u002Fstrong>——三条都占，定位器就值回票价。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型场景\u003C\u002Fstrong>：JNDI 时代的 EJB 查找——lookup 要连注册中心走网络，贵得理直气壮\u003C\u002Fli>\n \u003Cli>\u003Cstrong>日常场景\u003C\u002Fstrong>：日志门面 LoggerFactory.getLogger——按名取 logger、内部缓存，你天天在用的服务定位器（第五节见）\u003C\u002Fli>\n \u003Cli>\u003Cstrong>反面对照\u003C\u002Fstrong>：服务就在本地、直接 new 不贵——满地 ServiceLocator.get 是给简单问题套重壳。更现实的忠告：今天写新项目，同样的事首选依赖注入（Spring 把服务\"送上门\"，而不是\"你去取\"）——定位器是 DI 普及前的主流答案，它的三条缺点，恰好逐条定义了 DI 的优点\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"toc-4\">四、实现方式：注册台怎么搭\u003C\u002Fh2>\n\u003Cp>场景：一个迷你应用，两个重服务——database（连接池）和 payment（支付网关），住在注册中心后面，查一次 800ms。两个业务组件都要用：下单的要 database + payment，出日报的只要 database。\u003C\u002Fp>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：每个组件自己找服务\u003C\u002Fh3>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F** 反面教材：业务代码里粘满\"怎么找服务\"的细节 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadOrderService\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> placeOrder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">item\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [BadOrderService] 要用数据库了，自己去找：\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        DatabaseService database \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InitialContext\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">lookup\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"database\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [BadOrderService] 要用支付了，自己去找：\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        PaymentService payment \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InitialContext\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">lookup\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"payment\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [BadOrderService] 业务：下单 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> item);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        database.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">save\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"订单：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> item);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        payment.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">charge\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"订单：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> item);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>BadReportService 同款，只要 database——又一次 lookup。运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 反面教材：每个组件自己找服务 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- OrderService 下单（u001 买一本书） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [BadOrderService] 要用数据库了，自己去找：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 初始化连接池（10 个连接）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [BadOrderService] 要用支付了，自己去找：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [PaymentService] 建立支付通道（对接银行）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [BadOrderService] 业务：下单 u001 买一本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 入库：订单：u001 买一本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [PaymentService] 扣款：订单：u001 买一本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- ReportService 出日报 --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [BadReportService] 要用数据库了，自己去找：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 初始化连接池（10 个连接）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [BadReportService] 业务：出日报\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 查询：昨天的订单汇总\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   同一个 database 被 lookup 了两次——注册中心被打扰两次，连接池也建了两套\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 再来一单（u002 买两本书） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [BadOrderService] 要用数据库了，自己去找：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 初始化连接池（10 个连接）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [BadOrderService] 要用支付了，自己去找：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [PaymentService] 建立支付通道（对接银行）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [BadOrderService] 业务：下单 u002 买两本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 入库：订单：u002 买两本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [PaymentService] 扣款：订单：u002 买两本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   又是两次 lookup——服务明明全局一个，谁用谁新找一遍\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>病灶：\u003Cstrong>服务是全局的，找服务的事却是每家自己干\u003C\u002Fstrong>——同一个 database 被 lookup 了两次，连接池建了两套；u002 再下单，又是两次 lookup，前一次的成果没人记得。\"查找贵 + 服务全局一份\"这两个前提一叠加，\"谁用谁自己找\"就是结构性浪费。注意：功能一切正常，订单照入库、日报照出——反面教材从来不是功能错误，是结构错误。\u003C\u002Fp>\n\u003Ch3 id=\"toc-6\">4.2 正解：服务定位器\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>注册中心原样不动\u003C\u002Fstrong>——定位器不改造世界，只是在它前面搭了个柜台：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InitialContext\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">SuppressWarnings\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"unchecked\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> &#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">T\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> T \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">lookup\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">name\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [注册中心] 连接注册中心查找 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" ……（耗时 800ms）\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        switch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (name) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"database\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (T) \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> DatabaseService\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"payment\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (T) \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> PaymentService\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            default:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"没有这个服务：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>定位器——私有构造 + static 缓存，全应用唯一注册台\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ServiceLocator\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Object\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> cache \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> HashMap&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InitialContext context \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InitialContext\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ServiceLocator\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">SuppressWarnings\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"unchecked\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> &#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">T\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> T \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">name\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">cache.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">containsKey\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [服务定位器] 缓存没有 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"，去注册中心拿\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cache.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">put\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name, context.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">lookup\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [服务定位器] \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 已入缓存\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        } \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [服务定位器] 缓存命中：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (T) cache.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>客户端——要用服务，一行 get\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> OrderService\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> placeOrder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">item\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        DatabaseService database \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ServiceLocator.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"database\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        PaymentService payment \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ServiceLocator.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"payment\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [OrderService] 业务：下单 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> item);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        database.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">save\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"订单：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> item);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        payment.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">charge\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"订单：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> item);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>ReportService 同款，只要 database——一样的入口，一样的缓存。运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 正解：服务定位器 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- OrderService 下单（u001 买一本书） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务定位器] 缓存没有 database，去注册中心拿\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [注册中心] 连接注册中心查找 database ……（耗时 800ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 初始化连接池（10 个连接）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务定位器] database 已入缓存\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务定位器] 缓存没有 payment，去注册中心拿\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [注册中心] 连接注册中心查找 payment ……（耗时 800ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [PaymentService] 建立支付通道（对接银行）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务定位器] payment 已入缓存\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderService] 业务：下单 u001 买一本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 入库：订单：u001 买一本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [PaymentService] 扣款：订单：u001 买一本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- ReportService 出日报 --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务定位器] 缓存命中：database\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [ReportService] 业务：出日报\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 查询：昨天的订单汇总\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 再来一单（u002 买两本书） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务定位器] 缓存命中：database\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务定位器] 缓存命中：payment\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderService] 业务：下单 u002 买两本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [DatabaseService] 入库：订单：u002 买两本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [PaymentService] 扣款：订单：u002 买两本书\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   注册中心全程只被打扰两次（database、payment 各一次），之后全是缓存直取\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>四处对照读出含金量：第一次下单，database、payment 各走一趟注册中心，\u003Cstrong>800ms 只付一次\u003C\u002Fstrong>；ReportService 要 database，\u003Cstrong>缓存命中\u003C\u002Fstrong>——注册中心没被打扰，连接池还是那一套；u002 再下单，\u003Cstrong>两个都命中\u003C\u002Fstrong>——注册中心全程只被打扰两次，服务有几个就打扰几次，之后永远缓存直取；再对照客户端代码——BadOrderService 四行找服务两行业务，OrderService 两行 get 两行业务，\u003Cstrong>技术细节全进了定位器，业务方法里只剩业务\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：缓存的是服务本身，不是调用结果。\u003C\u002Fstrong> 定位器缓存的是 DatabaseService 这个对象的引用——连接池、支付通道这类重服务，建好了全应用反复用；它不缓存 query() 的返回值——那叫业务缓存，是另一层的事。两层缓存搅在一起，是新手最常见的误会。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：从\"各存各的\"到\"全局注册台\"。\u003C\u002Fstrong> ㉗ 的种子是单字段：\u003Ccode>private OrderService orderService\u003C\u002Fcode>——代表私有，每个代表各缓存各的，别人沾不到光；㉜ 长大成 \u003Ccode>static Map\u003C\u002Fcode>——全应用一份，跨组件共享，ReportService 才能命中 OrderService 存下的 database。缓存挪了个作用域，模式的身份就变了：从代表身上的一个技巧，变成全应用的公共设施。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：泛型强转是运行时赌博。\u003C\u002Fstrong> \u003Ccode>PaymentService payment = ServiceLocator.get(\"database\")\u003C\u002Fcode>——泛型方法照单全收，编译器一声不吭，运行时 ClassCastException 才炸。字符串名字天生没有编译期约束，拼错名字同罪；DI 容器用\"类型注入\"治好了这一半——按类型给依赖，装错了编译期就报。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>JNDI 原装\u003C\u002Fstrong>：EJB 时代，InitialContext.lookup(\"java:comp\u002Fenv\u002Fjdbc\u002FOrderDS\") + 一个 ServiceLocator 单例，是蓝皮书标准姿势；Tomcat 里配的 DataSource 至今还挂在 JNDI 树上，Spring 都留了 jndi-lookup 专门去取。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>SLF4J 的 LoggerFactory\u003C\u002Fstrong>：getLogger(\"com.foo.OrderService\")——按名取 logger，内部缓存，同名永远同一个实例。你天天在用服务定位器，只是没人叫过它这个名字。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring ApplicationContext\u003C\u002Fstrong>：getBean(\"orderService\") 的本质就是一个超级服务定位器——按名取、有缓存（默认单例）。但 Spring 官方把\"满地 getBean\"明确定性为反模式，推崇 @Autowired 注入——同一个容器，取的方式决定它是模式还是反模式。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>java.util.ServiceLoader（SPI）\u003C\u002Fstrong>：按接口找实现并实例化——JDBC 驱动的自动加载就靠它。名字从\"字符串\"升级成\"接口\"，细节三的病好了一半，思想同源：组件不用管实现从哪儿来。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Fowler 的判词\u003C\u002Fstrong>：《企业应用架构模式》（PoEAA）给它正名；后来同一人又在《IoC 容器与依赖注入》一文中亲自给它补了一刀——与 DI 相比，定位器把依赖藏进类体、单测难以替换。它输给 DI 的两条，恰好就是本文缺点一、二——一个模式的退位檄文，早就写在它的缺点里。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-9\">六、对账——㉗ 业务代表 vs ㉜ 服务定位器\u003C\u002Fh2>\n\u003Cp>㉗ 讲业务代表时，ServiceLookup 只是代表身上的一个零件；㉜ 把这个零件拎出来单独立户。爷俩摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>业务代表（㉗）\u003C\u002Fth>\n   \u003Cth>服务定位器（㉜）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>管什么\u003C\u002Ftd>\n   \u003Ctd>表示层与业务层之间的缓冲带\u003C\u002Ftd>\n   \u003Ctd>服务从哪儿来\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>一句话职责\u003C\u002Ftd>\n   \u003Ctd>重试、异常翻译——把远程说得像本地\u003C\u002Ftd>\n   \u003Ctd>查找、缓存——让注册中心只被打扰一次\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>缓存归谁\u003C\u002Ftd>\n   \u003Ctd>代表私有（每个代表各一份）\u003C\u002Ftd>\n   \u003Ctd>全局 static（全应用一份）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>客户端拿到\u003C\u002Ftd>\n   \u003Ctd>业务话术方法（placeOrder）\u003C\u002Ftd>\n   \u003Ctd>服务对象本身（get(\"database\")）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>典型同框\u003C\u002Ftd>\n   \u003Ctd>EJB 标准答案：Session Facade + Business Delegate + Service Locator 三件套，代表内部用定位器找服务\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话：\u003Cstrong>㉗ 管\"怎么把远程服务说得像本地话\"，㉜ 管\"服务从哪儿来、只找一次\"\u003C\u002Fstrong>——一个管通话质量，一个管通讯录。顺带一笔：定位器的 static Map 本质是 ② 单例思想——② 管\"这个类全应用只有一个实例\"，㉜ 管\"这些服务全应用只找一次\"；再往前走一步，Spring 容器就是\"全局单例注册表 + 类型注入\"的合体——㉜ 和 ②，在 DI 容器里会师。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>服务定位器 = 查找与缓存的统一收口：第一次按名走注册中心，之后缓存直取\u003C\u002Fli>\n \u003Cli>疗效：注册中心只被打扰 N 次（N = 服务数），重服务全局一份，客户端一行 get\u003C\u002Fli>\n \u003Cli>代价：静态全局测试难、依赖藏进类体、字符串名字编译器不查——三条缺点逐条定义了 DI 的优点\u003C\u002Fli>\n \u003Cli>铁律：缓存的是服务引用，不是调用结果——和业务缓存是两层，别搅在一起\u003C\u002Fli>\n \u003Cli>种子发芽：㉗ 代表私有的单字段缓存，长成 ㉜ 的 static 注册台；再与 ② 单例会师于 DI 容器\u003C\u002Fli>\n \u003Cli>当代转世：JNDI、LoggerFactory.getLogger、getBean、ServiceLoader——按名取用，全球通吃\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>服务定位器把\"服务从哪儿来\"安排得明明白白——可服务到手之后，数据怎么回去？出日报查回来的汇总要递给调用方，顺着网络递对象，总不能把实体整个扔过去：它身上挂着行为、挂着一整窝依赖对象——㉘ 说过，组合实体住在服务器上。客户端要的只是一份干干净净、拎起来就能走的数据快照——传输对象模式，下一篇见。\u003C\u002Fp>",[113,116,119,122,125,129,132,135,138,141],{"id":114,"text":115,"level":108},"toc-1","一、服务定位器模式是什么",{"id":117,"text":118,"level":108},"toc-2","二、优点与缺点",{"id":120,"text":121,"level":108},"toc-3","三、什么时候用",{"id":123,"text":124,"level":108},"toc-4","四、实现方式：注册台怎么搭",{"id":126,"text":127,"level":128},"toc-5","4.1 反面教材：每个组件自己找服务",3,{"id":130,"text":131,"level":128},"toc-6","4.2 正解：服务定位器",{"id":133,"text":134,"level":128},"toc-7","4.3 三处细节",{"id":136,"text":137,"level":108},"toc-8","五、在框架里找它",{"id":139,"text":140,"level":108},"toc-9","六、对账——㉗ 业务代表 vs ㉜ 服务定位器",{"id":142,"text":143,"level":108},"toc-10","写在最后"]