当一个值有多种获取方式时,调用方不应该到处重复“先看请求头,再看路径”的分支。Micronaut 可以把每一种取值规则做成独立组件,通过有序依赖注入把它们汇总,再用一个主组件向外提供统一接口。
本文译自 Sergio del Amo 的 Micronaut Patterns – Composite(Java / Gradle),按原文顺序展示接口、两种策略、控制器、组合组件及测试。代码遵循 Apache License 2.0,原文与媒体遵循 CC BY 4.0。中文翻译、版本说明和安全点评为编辑改动。
版本核对:源页面仍写 JDK 21 或更高,但 2026 年 10 月 5 日下载的官方源码 ZIP 在 gradle.properties 中使用 Micronaut 5.2.0,build.gradle 的 Java 编译目标和运行镜像均为 25。使用这份源码应准备匹配的 JDK 25,不要仅凭页面前提使用 JDK 21。以下测试只是原文测试代码的说明;没有运行 Gradle、编译应用或启动 HTTP 服务。

把统一契约定义成有序接口
示例构造一个返回“偏爱颜色”的 API。接口继承 Ordered,并只声明一个业务抽象方法;@FunctionalInterface 让编译器检查它确实满足函数式接口的约束。Optional 表达“这条策略没有找到结果”,使组合组件可以继续尝试后续策略。
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.order.Ordered;
import io.micronaut.http.HttpRequest;
import java.util.Optional;
@FunctionalInterface
public interface ColorFetcher extends Ordered {
@NonNull
Optional<String> favouriteColor(@NonNull HttpRequest<?> request);
}
继承 Ordered 的意义是让框架能够注入按顺序排列的同类型组件集合。它不会自动把多个候选者合成一个值;如何遍历、何时停止,仍由后面要写的组合组件决定。
策略一:读取 HTTP 请求头
第一种实现读取名为 color 的请求头。@Singleton 把它注册为单例组件,getOrder() 返回 10。
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpRequest;
import jakarta.inject.Singleton;
import java.util.Optional;
@Singleton
public class HttpHeaderColorFetcher implements ColorFetcher {
@Override
@NonNull
public Optional<String> favouriteColor(@NonNull HttpRequest<?> request) {
return request.getHeaders().get("color", String.class);
}
@Override
public int getOrder() {
return 10;
}
}
在 Ordered 中,数值越小,优先级越高。因此 10 会排在下一种策略的 20 前面。该实现并未验证请求头是不是合法颜色,也没有规范化大小写;它展示的是组合机制,不是完整的输入验证方案。
策略二:从请求路径中查找颜色
原文的第二种实现遍历一组预定义颜色,检查请求路径是否包含相应的小写字符串,并返回第一个匹配值。下面保留原始算法和完整颜色列表,只压缩数组排版:
package example.micronaut;
import io.micronaut.http.HttpRequest;
import jakarta.inject.Singleton;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Stream;
@Singleton
public class PathColorFetcher implements ColorFetcher {
private static final String[] COLORS = {
"Red", "Blue", "Green", "Orange", "White", "Black", "Yellow",
"Purple", "Silver", "Brown", "Gray", "Pink", "Olive", "Maroon",
"Violet", "Charcoal", "Magenta", "Bronze", "Cream", "Gold",
"Tan", "Teal", "Mustard", "Navy Blue", "Coral", "Burgundy",
"Lavender", "Mauve", "Peach", "Rust", "Indigo", "Ruby",
"Clay", "Cyan", "Azure", "Beige", "Turquoise", "Amber", "Mint"
};
@Override
public Optional<String> favouriteColor(HttpRequest<?> request) {
return Stream.of(COLORS)
.filter(c -> request.getPath().contains(c.toLowerCase(Locale.ROOT)))
.map(String::toLowerCase)
.findFirst();
}
@Override
public int getOrder() {
return 20;
}
}
例如 /color/mint 可以命中 Mint。不过,匹配顺序来自颜色数组,而不是路径中出现的先后次序;请求路径本身没有统一转换成小写,因此大小写行为也不能凭想象推断。
静态审查发现:contains 只是子串匹配,/color/redwood 也会命中 Red。此外,过滤时使用 Locale.ROOT,输出时却使用默认 Locale,规范化策略不一致。这些不适合作为生产接口的严格业务识别,更不能用来判断身份、角色或资源访问权限。实际项目应使用明确的路由参数、允许列表和一致的规范化规则。
如果仅修正输出的 Locale,原文的 .map(String::toLowerCase) 可改为 .map(c -> c.toLowerCase(Locale.ROOT))。这是编辑建议,不能解决子串误匹配问题;本稿没有把这一处改动说成已经完成了生产级校验。
直接注入单个接口时,会出现多个候选者
控制器只依赖 ColorFetcher,并提供 /color 和 /color/mint 两条 GET 路由:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import java.util.Optional;
@Controller("/color")
public class ColorController {
private final ColorFetcher colorFetcher;
public ColorController(ColorFetcher colorFetcher) {
this.colorFetcher = colorFetcher;
}
@Produces(MediaType.TEXT_PLAIN)
@Get("/mint")
Optional<String> mint(@NonNull HttpRequest<?> request) {
return colorFetcher.favouriteColor(request);
}
@Produces(MediaType.TEXT_PLAIN)
@Get
Optional<String> index(@NonNull HttpRequest<?> request) {
return colorFetcher.favouriteColor(request);
}
}
@Controller("/color") 指定基础路径,@Get("/mint") 为其附加子路径。HTTP 请求直接绑定到方法参数。由于这里返回的是字符串,原文显式设置 text/plain,而不是沿用默认的 JSON 响应类型。
此时如果上下文里只有请求头和路径两个实现,控制器要求注入一个 ColorFetcher 就会遇到歧义。原文展示的异常包含 Multiple possible bean candidates found,列出 PathColorFetcher 与 HttpHeaderColorFetcher。排序只解决集合中的顺序,并不能替单值注入消除歧义。
用 @Primary 选择组合组件
第三个实现负责组合前两者。它同样实现 ColorFetcher,但构造函数接收 List<ColorFetcher>,而且用 @Primary 指明自己是单值注入时的首选候选者。
package example.micronaut;
import io.micronaut.context.annotation.Primary;
import io.micronaut.http.HttpRequest;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
@Primary
@Singleton
public class CompositeColorFetcher implements ColorFetcher {
private final List<ColorFetcher> colorFetcherList;
public CompositeColorFetcher(List<ColorFetcher> colorFetcherList) {
this.colorFetcherList = colorFetcherList;
}
@Override
public Optional<String> favouriteColor(HttpRequest<?> request) {
return colorFetcherList.stream()
.map(colorFetcher -> colorFetcher.favouriteColor(request))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
}
}
按照原文说明,列表包含其他 ColorFetcher 组件,并排除正在构造的 CompositeColorFetcher 自身,避免它把调用递归回自己。由于接口实现了 Ordered,注入的列表按优先级排序。流处理按这个顺序寻找第一个存在的值,findFirst() 找到后就可以短路。
因此,同时带有请求头 color: yellow 和路径 /color/mint 的请求会返回 yellow;请求头缺失时,才轮到路径策略返回 mint。控制器不需要知道这些顺序细节。
分别验证组件选择与 HTTP 行为
原文首先用不启动嵌入式服务器的上下文测试,检查单值注入选中了组合组件:
package example.micronaut;
import io.micronaut.context.BeanContext;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
@MicronautTest(startApplication = false)
class CompositeColorFetcherTest {
@Inject
BeanContext beanContext;
@Test
void compositeColorFetcherIsThePrimaryBeanOfTypeColorFetcher() {
assertTrue(beanContext.getBean(ColorFetcher.class)
instanceof CompositeColorFetcher);
}
}
第二个测试启动框架上下文与嵌入式服务器,用指向该服务器的 HTTP 客户端检查四种行为:
package example.micronaut;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest
class ColorControllerTest {
@Inject
@Client("/")
HttpClient httpClient;
@Test
void testCompositePattern() {
BlockingHttpClient client = httpClient.toBlocking();
assertEquals("yellow",
client.retrieve(HttpRequest.GET("/color").header("color", "yellow")));
assertThrows(HttpClientResponseException.class,
() -> client.retrieve(HttpRequest.GET("/color")));
assertEquals("yellow",
client.retrieve(HttpRequest.GET("/color/mint").header("color", "yellow")));
assertEquals("mint", client.retrieve(HttpRequest.GET("/color/mint")));
}
}
| 请求 | 原文断言 | 验证的设计点 |
|---|---|---|
| /color + color: yellow | yellow | 请求头策略提供结果 |
| /color,无请求头 | 客户端抛出响应异常 | 所有策略缺省时的 HTTP 行为 |
| /color/mint + color: yellow | yellow | 请求头优先于路径 |
| /color/mint,无请求头 | mint | 第一策略缺省后继续尝试路径 |
测试代码写出了期望行为,但没有执行它们。原测试只断言缺省请求抛出 HttpClientResponseException,并未在该断言中验证具体状态码,因此不能把它描述成已经验证了某个响应码。若项目需要固定的错误协议,应增加对应断言。
扩展时保留边界
新增策略时可以继续实现同一个接口,选择明确的顺序值,再让组合组件沿用原有调用流程。每个策略最好只负责一种来源,返回缺省而不是随意吞掉程序错误。是否允许高优先级请求头覆盖路径值,也应由业务和信任边界决定;来自客户端的请求头并不天然可信。
原文提到框架中的 CompositeDiscoveryClient、CompositeHttpLocaleResolver 与 CompositeMessageSource 也采用组合思想,可结合主组件与次组件文档继续阅读。
静态检查没有在本例发现硬编码密钥、SQL 拼接或动态代码执行;这并不说明代码不存在漏洞。下载源码中的构建插件与依赖解析会访问仓库,运行前仍应按项目要求固定版本、审查构建逻辑。本文只读取了源码包,没有执行其中脚本。
原作者 Sergio del Amo;发布方 Micronaut。代码 Apache-2.0,正文和原媒体 CC BY 4.0。本文包含中文翻译、数组排版调整及已标明的编辑补充;配图为编辑原创技术示意图。文末保留 Apache-2.0 许可证文本。
原始候选歧义异常
下列为源文提供的异常文本,供理解尚未加入组合组件时的构造注入路径,并非执行报告。
Message: Multiple possible bean candidates found:
[example.micronaut.PathColorFetcher,
example.micronaut.HttpHeaderColorFetcher]
Path Taken: new ColorController(ColorFetcher colorFetcher)
--> new ColorController([ColorFetcher colorFetcher])
io.micronaut.context.exceptions.DependencyInjectionException:
Failed to inject value for parameter [colorFetcher]
of class: example.micronaut.ColorController
代码版权与完整许可
官方源码包相关Java文件保留的版权头如下。源包目录没有提供额外NOTICE文件,未编造NOTICE。示例代码数组仅调整排版,中文说明与明确标注的修订建议为未完纪于2026-10-05增加。
/* * Copyright 2017-2026 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
原文正文和媒体采用 Creative Commons Attribution 4.0 International(CC BY 4.0),由 Sergio del Amo / Micronaut 提供;本文为中文翻译与已注明的编辑改动,不暗示原作者背书,材料不提供保证。再使用须适当署名、保留来源与许可链接并说明改动,不增加限制。许可说明与完整法律条款。












暂无评论内容