通过ConfigurableEnvironment激活Spring Profile失败的解决方法
问题
在Tomcat上部署的Spring Web应用中,接口ClienteFarmacia有两个实现类,通过@Profile注解区分实例化逻辑。通过环境变量export spring_profiles_active=farmaciaDb可正常激活Profile,但在@Component的@PostConstruct方法中通过ConfigurableEnvironment设置激活Profile时,虽该逻辑先于目标Bean执行,仍出现NoSuchBeanDefinitionException,找不到对应类型的Bean。
相关代码
接口与实现类
public interface ClienteFarmacia {} @Component @Profile("farmaciaDb") public class ClienteFarmaciaDbImpl implements ClienteFarmacia {} @Component @Profile("farmaciaWs") public class ClienteFarmaciaWsImpl implements ClienteFarmacia {}
配置类
@Component public class Configuracion { @Autowired private ConfigurableEnvironment env; @PostConstruct void init() { // [... 加载数据] switch (farmaciaClientType) { case "BD": log.debug("Asignado activeProfile farmaciaDb"); this.env.setActiveProfiles("farmaciaDb"); break; case "WS": log.debug("Asignado activeProfile farmaciaWs"); this.env.setActiveProfiles("farmaciaWs"); break; default: log.error("farmacia.tipoConsulta es " + tipoConsultaFarmacia + ", valores válidos BD o WS"); throw new RuntimeException("Error cargando configuración"); } } }
错误日志
[2025-01-10T14:59:52.145+01:00] [main] DEBUG
e.s.d.e.e.p.config.Configuracion.init(123) - Asignado activeProfile farmaciaDb
[2025-01-10T14:59:52.154+01:00] [main] WARN
o.s.w.c.s.AnnotationConfigWebApplicationContext.refresh(599) - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'autenticacionController': Unsatisfied dependency expressed through field 'idp'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'idp': Unsatisfied dependency expressed through field 'clienteFarmacia'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'es.ssib.dtic.ereceta.ehdsi.portal.farmacia.ClienteFarmacia' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
[2025-01-10T14:59:52.156+01:00] [main] INFO
o.s.o.j.LocalEntityManagerFactoryBean.destroy(651) - Closing JPA EntityManagerFactory for persistence unit 'PortalEhdsiDB'
[2025-01-10T14:59:52.160+01:00] [main] ERROR
o.s.web.servlet.DispatcherServlet.initServletBean(534) - Context initialization failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'autenticacionController': Unsatisfied dependency expressed through field 'idp'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'idp': Unsatisfied dependency expressed through field 'clienteFarmacia'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'es.ssib.dtic.ereceta.ehdsi.portal.farmacia.ClienteFarmacia' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:713)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693)
解决方案
核心原因
Spring在上下文刷新阶段前期就完成了Profile解析和Bean候选筛选,@PostConstruct是Bean实例化后执行的方法,此时Profile筛选逻辑已结束,修改ConfigurableEnvironment的activeProfiles不会回溯重新筛选Bean,导致依赖ClienteFarmacia的Bean找不到对应实现类。
可行方案
方案1:用ApplicationContextInitializer提前设置Profile
实现ApplicationContextInitializer<ConfigurableApplicationContext>接口,在上下文刷新前设置activeProfiles,这是Spring官方推荐的上下文初始化前修改环境参数的方式。
public class ProfileInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { ConfigurableEnvironment env = applicationContext.getEnvironment(); String farmaciaClientType = env.getProperty("farmacia.tipoConsulta"); if ("BD".equals(farmaciaClientType)) { env.setActiveProfiles("farmaciaDb"); } else if ("WS".equals(farmaciaClientType)) { env.setActiveProfiles("farmaciaWs"); } else { throw new RuntimeException("farmacia.tipoConsulta值无效,仅支持BD或WS"); } } }
在web.xml中配置初始化器:
<context-param> <param-name>contextInitializerClasses</param-name> <param-value>com.yourpackage.ProfileInitializer</param-value> </context-param>
方案2:配置文件动态指定Profile
若配置文件(如application.properties)已有farmacia.tipoConsulta,可通过SpEL表达式直接设置spring.profiles.active:
application.properties
farmacia.tipoConsulta=BD spring.profiles.active=${farmacia.tipoConsulta == 'BD' ? 'farmaciaDb' : 'farmaciaWs'}
无需额外代码,直接通过配置驱动完成Profile动态激活。
方案3:用@Conditional替代@Profile
自定义Condition实现类,根据配置参数决定是否实例化对应实现类,绕过Profile激活时机限制:
public class FarmaciaDbCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return "BD".equals(context.getEnvironment().getProperty("farmacia.tipoConsulta")); } } public class FarmaciaWsCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return "WS".equals(context.getEnvironment().getProperty("farmacia.tipoConsulta")); } }
修改实现类注解:
@Component @Conditional(FarmaciaDbCondition.class) public class ClienteFarmaciaDbImpl implements ClienteFarmacia {} @Component @Conditional(FarmaciaWsCondition.class) public class ClienteFarmaciaWsImpl implements ClienteFarmacia {}
在Bean扫描阶段直接根据配置决定是否注册Bean,逻辑更直观。
内容的提问来源于stack exchange,提问作者SJuan76

