在FeignClient的场景中,每个FeignClient都有独立的子上下文,所以每个子上下文都会有自己的Bean实例,即使配置类有@Configuration注解。 子上下文是什么?
这是一个非常好的问题,触及了 Spring Cloud Feign 的核心机制。让我详细解释 子上下文(FeignContext) 的概念和工作原理。
什么是子上下文?
在 Spring Cloud Feign 中,子上下文是一个独立的、隔离的 ApplicationContext,专门为某个 FeignClient 创建。它有如下特点:
JAVA
ApplicationContext 层次结构:
主 ApplicationContext (Spring Boot 应用)
├── 子上下文 FeignContext-A (为 FeignClient-A 创建)
├── 子上下文 FeignContext-B (为 FeignClient-B 创建)
├── 子上下文 FeignContext-C (为 FeignClient-C 创建)
└── ... 总共 313 个
子上下文的工作原理
1. 创建时机
JAVA
@FeignClient(name = "user-service",
configuration = UserServiceConfig.class)
public interface UserServiceClient {
// FeignClient 接口
}
@FeignClient(name = "order-service",
configuration = OrderServiceConfig.class)
public interface OrderServiceClient {
// 另一个 FeignClient
}
启动时:
- Spring Cloud 为每个
@FeignClient创建一个独立的FeignContext - 每个
FeignContext都是主ApplicationContext的子上下文 - 每个子上下文都有自己的 Bean 定义
2. 代码示例验证
JAVA
@SpringBootTest
class FeignContextTest {
@Autowired
private ApplicationContext parentContext; // 主上下文
@Autowired
private FeignContext feignContext; // FeignContext 工厂
@Test
void testChildContextHierarchy() {
// 获取 user-service 的子上下文
ApplicationContext userContext = feignContext.getInstance(
"user-service", ApplicationContext.class);
// 获取 order-service 的子上下文
ApplicationContext orderContext = feignContext.getInstance(
"order-service", ApplicationContext.class);
// 验证父子关系
System.out.println("userContext 的父上下文: " +
userContext.getParent());
System.out.println("与 parentContext 相同: " +
(userContext.getParent() == parentContext)); // true
// 验证不同的子上下文
System.out.println("userContext == orderContext: " +
(userContext == orderContext)); // false
// 验证不同的 Bean
Object userConfig = userContext.getBean("userServiceConfig");
Object orderConfig = orderContext.getBean("userServiceConfig");
System.out.println("配置类相同: " + (userConfig == orderConfig)); // false
}
}
子上下文的隔离性
示例 1:Bean 隔离
JAVA
@Configuration
public class ClientConfig {
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build();
}
@Bean
public RequestInterceptor interceptor() {
return template -> {
template.header("X-Client-Name", "custom-client");
};
}
}
结果:
主 ApplicationContext
├── FeignContext-user-service
│ ├── OkHttpClient@1234 (user-service 专用)
│ └── RequestInterceptor@5678
│
└── FeignContext-order-service
├── OkHttpClient@9012 (order-service 专用)
└── RequestInterceptor@3456
每个子上下文有独立的、完整的 Bean 集合。
示例 2:验证隔离性
JAVA
@Component
public class BeanCounter {
private static final Map<String, List<OkHttpClient>> clientsByContext =
new ConcurrentHashMap<>();
public void recordClient(String contextName, OkHttpClient client) {
clientsByContext.computeIfAbsent(contextName, k -> new ArrayList<>())
.add(client);
}
}
@Configuration
public class TrackingConfig {
@Autowired
private BeanCounter counter;
@Bean
public OkHttpClient okHttpClient() {
OkHttpClient client = new OkHttpClient();
// 记录是哪个上下文创建的
String contextName = determineContextName();
counter.recordClient(contextName, client);
return client;
}
}
运行后会看到 313 个不同的 OkHttpClient 实例。
子上下文的继承关系
虽然子上下文是独立的,但它们继承父上下文的 Bean:
JAVA
@Configuration
public class ParentConfig {
@Bean
public ConnectionPool sharedConnectionPool() {
// 这个 Bean 在主上下文中
return new ConnectionPool(200, 5, TimeUnit.MINUTES);
}
}
public class ClientConfig {
@Bean
public OkHttpClient okHttpClient(ConnectionPool pool) {
// 这个 pool 是从父上下文继承的
return new OkHttpClient.Builder()
.connectionPool(pool)
.build();
}
}
继承规则:
- 子上下文可以访问父上下文的 Bean
- 子上下文可以定义同名的 Bean,覆盖父上下文的定义
- 父上下文不能访问子上下文的 Bean
查看实际的上下文结构
JAVA
@SpringBootTest
class ContextDebugTest {
@Autowired
private ApplicationContext context;
@Test
void debugContextHierarchy() {
printContextHierarchy(context, "");
}
private void printContextHierarchy(ApplicationContext ctx, String indent) {
System.out.println(indent + "Context: " + ctx.getDisplayName());
System.out.println(indent + " ID: " + ctx.getId());
System.out.println(indent + " Parent: " +
(ctx.getParent() != null ? ctx.getParent().getId() : "null"));
// 统计 Bean
String[] beanNames = ctx.getBeanDefinitionNames();
System.out.println(indent + " Bean 数量: " + beanNames.length);
// 显示 Feign 相关的 Bean
Arrays.stream(beanNames)
.filter(name -> name.contains("Feign") ||
name.contains("Client") ||
name.contains("Config"))
.forEach(name -> System.out.println(indent + " - " + name));
// 递归查看子上下文
if (ctx instanceof HierarchicalBeanFactory) {
HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) ctx;
if (hbf.getParentBeanFactory() instanceof ApplicationContext) {
ApplicationContext parent =
(ApplicationContext) hbf.getParentBeanFactory();
printContextHierarchy(parent, indent + " ");
}
}
}
}
为什么会创建子上下文?
Spring Cloud Feign 创建子上下文的主要原因是:
1. 配置隔离
JAVA
// 不同的微服务可能需要不同的配置
@FeignClient(name = "payment-service",
configuration = PaymentConfig.class) // 支付专用配置
interface PaymentClient {}
@FeignClient(name = "inventory-service",
configuration = InventoryConfig.class) // 库存专用配置
interface InventoryClient {}
2. Bean 覆盖
JAVA
// 主上下文
@Configuration
public class GlobalConfig {
@Bean
public Decoder defaultDecoder() {
return new JacksonDecoder();
}
}
// Client A 需要不同的 Decoder
public class ClientAConfig {
@Bean
public Decoder customDecoder() { // 覆盖父上下文的 Decoder
return new CustomDecoder();
}
}
3. 生命周期管理
JAVA
// 每个 FeignClient 可以有自己的销毁逻辑
public class ClientConfig {
@Bean
@Scope("prototype")
public Client client() {
return new Client();
}
@PreDestroy
public void cleanup() {
// 只清理这个客户端的资源
}
}
验证子上下文的存在
方法 1:通过日志
YAML
# application.yml
logging:
level:
org.springframework.cloud.openfeign: DEBUG
org.springframework.context: DEBUG
启动时日志会显示:
Creating new FeignContext for client: user-service
Creating new FeignContext for client: order-service
方法 2:编程方式
JAVA
@RestController
public class DebugController {
@Autowired
private FeignContext feignContext;
@GetMapping("/debug/feign-contexts")
public Map<String, Object> listFeignContexts() {
Map<String, Object> result = new HashMap<>();
// 获取所有 FeignClient 名称
String[] clientNames = getFeignClientNames();
for (String clientName : clientNames) {
ApplicationContext childContext =
feignContext.getInstance(clientName, ApplicationContext.class);
Map<String, Object> contextInfo = new HashMap<>();
contextInfo.put("parent", childContext.getParent() != null ?
childContext.getParent().getId() : "null");
contextInfo.put("beanCount", childContext.getBeanDefinitionCount());
// 获取特定类型的 Bean
Map<String, OkHttpClient> clients =
childContext.getBeansOfType(OkHttpClient.class);
contextInfo.put("okHttpClientCount", clients.size());
result.put(clientName, contextInfo);
}
return result;
}
}
如何避免 313 个重复的 Bean?
理解了子上下文后,解决方案就清晰了:
方案 1:共享配置到父上下文
JAVA
// 1. 在主上下文中定义共享 Bean
@Configuration
public class SharedConfig {
@Bean
public ConnectionPool sharedConnectionPool() {
return new ConnectionPool(200, 5, TimeUnit.MINUTES);
}
}
// 2. 在 FeignClient 配置中注入
public class ClientConfig {
@Bean
public OkHttpClient okHttpClient(
// 从父上下文注入共享的连接池
@Autowired ConnectionPool sharedConnectionPool) {
return new OkHttpClient.Builder()
.connectionPool(sharedConnectionPool) // 共享
.build();
}
}
方案 2:使用原型 Bean
JAVA
@Configuration
public class ClientConfig {
// 原型 Bean,每次注入时创建新实例
@Bean
@Scope("prototype")
public OkHttpClient okHttpClientPrototype(
@Autowired ConnectionPool sharedPool) {
return new OkHttpClient.Builder()
.connectionPool(sharedPool)
.build();
}
// 单例 Bean,但复用连接池
@Bean
public OkHttpClient okHttpClient(ConnectionPool sharedPool) {
return okHttpClientPrototype(sharedPool);
}
}
子上下文的内存布局
内存布局(简化):
┌─────────────────────────────────────────────────┐
│ 主 ApplicationContext │
│ ┌──────────────────────────────────────────┐ │
│ │ 共享 ConnectionPool@1234 │ │
│ │ 其他共享 Bean... │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │FeignContext││FeignContext││FeignContext│ │
│ │ A ││ B ││ C │ │
│ │ ┌──────┐ ││ ┌──────┐ ││ ┌──────┐ │ │
│ │ │ConfigA│ ││ │ConfigB│ ││ │ConfigC│ │ │
│ │ │@4567 │ ││ │@8910 │ ││ │@1112 │ │ │
│ │ └──────┘ ││ └──────┘ ││ └──────┘ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
每个 FeignContext 有自己的:
1. Config 实例
2. 从 Config 创建的 Bean
3. 但可以访问父上下文的共享 Bean
总结
子上下文是 Spring Cloud Feign 为每个 FeignClient 创建的独立 ApplicationContext:
- 隔离性:每个子上下文是独立的,有自己的 Bean 实例
- 继承性:子上下文可以访问父上下文的 Bean
- 覆盖性:子上下文可以定义同名 Bean 覆盖父上下文
- 独立性:即使配置类有
@Configuration,每个子上下文也会独立实例化 - 问题根源:313 个 FeignClient → 313 个子上下文 → 313 个重复的 Bean 实例
- 解决方案:将共享 Bean 提升到父上下文,子上下文通过注入共享
理解了子上下文,就能明白为什么会出现 313 个重复实例,也知道了如何正确设计配置结构来避免这个问题。
子上下文是Spring 原生支持吗?
是的,子上下文是 Spring 框架的原生特性,称为 ApplicationContext 层次结构(Hierarchical ApplicationContext)。
这是 Spring 的核心设计之一,让我详细解释:
Spring 原生支持的上下文层次结构
1. 基本概念
JAVA
// Spring 原生支持创建父子上下文
public interface ApplicationContext extends ... {
ApplicationContext getParent(); // 获取父上下文
// 层次结构相关方法
boolean containsBeanDefinition(String beanName);
String[] getBeanDefinitionNames();
}
2. 创建父子上下文的原生方式
JAVA
// 手动创建父子上下文层次结构
public class SpringHierarchyDemo {
public static void main(String[] args) {
// 1. 创建父上下文
AnnotationConfigApplicationContext parentContext =
new AnnotationConfigApplicationContext();
parentContext.register(ParentConfig.class);
parentContext.refresh();
// 2. 创建子上下文,并设置父上下文
AnnotationConfigApplicationContext childContext =
new AnnotationConfigApplicationContext();
childContext.setParent(parentContext); // 设置父子关系
childContext.register(ChildConfig.class);
childContext.refresh();
// 3. 验证层次结构
System.out.println("Child parent: " + (childContext.getParent() == parentContext));
// 输出: true
// 4. 验证 Bean 访问
// 子可以访问父的 Bean
ParentBean parentBeanFromChild = childContext.getBean(ParentBean.class);
System.out.println("子上下文能访问父的 Bean: " + (parentBeanFromChild != null));
// 输出: true
// 父不能访问子的 Bean
try {
ChildBean childBeanFromParent = parentContext.getBean(ChildBean.class);
} catch (NoSuchBeanDefinitionException e) {
System.out.println("父上下文不能访问子的 Bean: " + e.getMessage());
}
childContext.close();
parentContext.close();
}
}
@Configuration
class ParentConfig {
@Bean
public ParentBean parentBean() {
return new ParentBean();
}
}
@Configuration
class ChildConfig {
@Bean
public ChildBean childBean() {
return new ChildBean();
}
}
Spring 中上下文层次结构的常见用途
1. Spring MVC 的经典分层
JAVA
// Spring MVC 的传统配置
public class WebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// 1. 创建根上下文(父)
AnnotationConfigWebApplicationContext rootContext =
new AnnotationConfigWebApplicationContext();
rootContext.register(RootConfig.class);
// 2. 创建 Servlet 上下文(子)
AnnotationConfigWebApplicationContext dispatcherContext =
new AnnotationConfigWebApplicationContext();
dispatcherContext.register(WebMvcConfig.class);
dispatcherContext.setParent(rootContext); // 设置父子关系
// 3. 配置 DispatcherServlet
ServletRegistration.Dynamic dispatcher =
servletContext.addServlet("dispatcher",
new DispatcherServlet(dispatcherContext));
}
}
@Configuration
class RootConfig {
// 业务逻辑、数据访问等共享 Bean
@Bean
public DataSource dataSource() { ... }
@Bean
public JpaTransactionManager transactionManager() { ... }
}
@Configuration
@EnableWebMvc
class WebMvcConfig implements WebMvcConfigurer {
// MVC 相关配置
// 可以覆盖父上下文的一些定义
}
2. Spring Boot 的内置层次结构
JAVA
// Spring Boot 也有层次结构
@SpringBootTest
class SpringBootHierarchyTest {
@Test
void testSpringBootHierarchy() {
SpringApplication app = new SpringApplication(MyApp.class);
ConfigurableApplicationContext context = app.run();
// Spring Boot 应用默认的层次结构
System.out.println("应用上下文类型: " + context.getClass());
// 检查是否有父上下文
ApplicationContext parent = context.getParent();
if (parent != null) {
System.out.println("父上下文: " + parent.getClass());
}
context.close();
}
}
Spring Cloud Feign 如何使用这个特性
JAVA
// Spring Cloud Feign 源码简化
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
// FeignContext 继承自 NamedContextFactory
// NamedContextFactory 管理多个子上下文
}
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
// 存储子上下文的映射
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
// 为每个名称创建子上下文
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 设置父上下文
if (this.applicationContext != null) {
context.setParent(this.applicationContext);
}
// 注册配置
context.register(DefaultConfigurations.class);
context.register(getConfigurationsForName(name));
context.refresh();
contexts.put(name, context);
return context;
}
}
实际验证 Spring 原生层次结构
JAVA
@SpringBootTest
class SpringHierarchyProofTest {
@Test
void testSpringNativeHierarchy() {
// 1. 证明 Spring 原生支持层次结构
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
parent.registerBean("parentBean", String.class, () -> "parent-value");
parent.refresh();
AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext();
child.setParent(parent);
child.registerBean("childBean", String.class, () -> "child-value");
child.refresh();
// 2. 验证 Bean 可见性
// 子可以访问父
String parentBeanFromChild = child.getBean("parentBean", String.class);
System.out.println("子访问父: " + parentBeanFromChild); // parent-value
// 父不能访问子
try {
String childBeanFromParent = parent.getBean("childBean", String.class);
} catch (NoSuchBeanDefinitionException e) {
System.out.println("父不能访问子: " + e.getMessage());
}
// 3. 验证 Bean 覆盖
AnnotationConfigApplicationContext child2 = new AnnotationConfigApplicationContext();
child2.setParent(parent);
child2.registerBean("parentBean", String.class, () -> "overridden-value");
child2.refresh();
// 子上下文覆盖父的 Bean
String overridden = child2.getBean("parentBean", String.class);
System.out.println("子覆盖父: " + overridden); // overridden-value
// 父的 Bean 不变
String original = parent.getBean("parentBean", String.class);
System.out.println("父的原始: " + original); // parent-value
child2.close();
child.close();
parent.close();
}
}
Spring 层次结构的源码支持
JAVA
// Spring 框架核心接口
public interface ApplicationContext extends ... {
/**
* 返回父上下文,如果没有则返回 null
*/
@Nullable
ApplicationContext getParent();
/**
* 获取父上下文的 BeanFactory
*/
@Nullable
ConfigurableListableBeanFactory getParentBeanFactory();
}
// 在 AbstractApplicationContext 中的实现
public abstract class AbstractApplicationContext extends ... {
@Nullable
private ApplicationContext parent;
// 在获取 Bean 时,会检查父上下文
@Override
public Object getBean(String name) throws BeansException {
// 先在本上下文中查找
// 如果找不到,并且有父上下文,则在父上下文中查找
return doGetBean(name, null, null, false);
}
}
层次结构的 Bean 查找规则
JAVA
@Component
class BeanLookupDemo {
@Autowired
private ApplicationContext context;
public void demonstrateLookupRules() {
// 创建层次结构
ApplicationContext grandParent = createContext("grandParent");
ApplicationContext parent = createContext("parent");
parent.setParent(grandParent);
ApplicationContext child = createContext("child");
child.setParent(parent);
// 1. 查找顺序:自底向上
// 当在 child 中查找 Bean 时:
// 1) 先在 child 中找
// 2) 然后在 parent 中找
// 3) 最后在 grandParent 中找
// 2. Bean 定义覆盖
// 子上下文可以定义同名的 Bean,覆盖父上下文的定义
// 查找时,优先使用子上下文中的定义
// 3. 原型 Bean 的实例化
// 每个上下文都有自己的原型 Bean 实例
}
private ApplicationContext createContext(String name) {
GenericApplicationContext ctx = new GenericApplicationContext();
ctx.registerBean(name + "Bean", String.class, () -> "value-from-" + name);
ctx.refresh();
return ctx;
}
}
为什么 Spring 要支持层次结构?
1. 模块化和隔离
JAVA
// 不同模块可以有独立的配置
ApplicationContext moduleAContext = new AnnotationConfigApplicationContext();
moduleAContext.register(ModuleAConfig.class);
ApplicationContext moduleBContext = new AnnotationConfigApplicationContext();
moduleBContext.register(ModuleBConfig.class);
// 根上下文共享公共配置
ApplicationContext rootContext = new AnnotationConfigApplicationContext();
rootContext.register(SharedConfig.class);
moduleAContext.setParent(rootContext);
moduleBContext.setParent(rootContext);
2. 配置覆盖
JAVA
// 根上下文:默认配置
@Configuration
class DefaultConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(); // 默认数据源
}
}
// 子上下文:覆盖配置
@Configuration
class OverrideConfig {
@Bean
public DataSource dataSource() {
return new CustomDataSource(); // 自定义数据源
}
}
3. 资源管理
JAVA
// 每个子上下文可以独立关闭
ApplicationContext child1 = createChildContext();
ApplicationContext child2 = createChildContext();
// 独立管理生命周期
child1.close(); // 只关闭 child1,不影响 child2
4. 测试隔离
JAVA
@SpringBootTest
class TestIsolation {
// 每个测试可以有自己的子上下文
// 避免测试之间的干扰
@Test
void test1() {
// 使用独立的上下文配置
}
@Test
void test2() {
// 另一个独立的上下文
}
}
Spring Cloud 对层次结构的扩展
JAVA
// Spring Cloud 扩展了 Spring 的层次结构
public class NamedContextFactory<C extends NamedContextFactory.Specification>
implements ApplicationContextAware, DisposableBean {
// 管理多个命名上下文
private Map<String, AnnotationConfigApplicationContext> contexts = new HashMap<>();
// 为不同的客户端创建隔离的上下文
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
return context.getBean(type);
}
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
}
验证 Feign 使用的确实是 Spring 原生特性
JAVA
@SpringBootTest
class FeignUsesSpringHierarchyTest {
@Autowired
private FeignContext feignContext;
@Test
void verifyFeignUsesSpringHierarchy() {
// 获取主应用上下文
ApplicationContext mainContext = ...;
// 获取 FeignClient 的子上下文
ApplicationContext feignChildContext =
feignContext.getInstance("myFeignClient", ApplicationContext.class);
// 验证确实是 Spring 的 ApplicationContext
System.out.println("子上下文类型: " + feignChildContext.getClass());
// 输出: org.springframework.context.annotation.AnnotationConfigApplicationContext
// 验证父子关系
System.out.println("父上下文: " + feignChildContext.getParent());
System.out.println("父上下文就是主上下文: " +
(feignChildContext.getParent() == mainContext));
// 验证层次结构的方法
System.out.println("是否有父上下文: " +
(feignChildContext.getParent() != null));
// 验证 BeanFactory 层次结构
ConfigurableListableBeanFactory beanFactory =
(ConfigurableListableBeanFactory) feignChildContext.getAutowireCapableBeanFactory();
System.out.println("父 BeanFactory: " + beanFactory.getParentBeanFactory());
}
}
总结
是的,子上下文是 Spring 框架的原生特性:
- 原生支持:Spring 框架的
ApplicationContext从一开始就支持层次结构 - 核心接口:
ApplicationContext.getParent()是框架标准接口 - 广泛使用:Spring MVC、Spring Boot、Spring Cloud 都利用了这个特性
- Feign 实现:Spring Cloud Feign 的
FeignContext是基于NamedContextFactory,而后者使用标准的 Spring 上下文层次结构 - 设计目的:实现配置隔离、Bean 覆盖、模块化和独立生命周期管理
你的 313 个 FeignClient 问题本质是:
- Spring 原生支持上下文层次结构
- Spring Cloud Feign 为每个客户端创建独立的子上下文
- 每个子上下文是标准的 Spring
ApplicationContext - 每个子上下文都会重新实例化配置类中的
@Bean方法 - 要解决内存浪费,需要将共享 Bean 定义在父上下文中