无无参构造器时,如何将@ApplicationScoped Bean注入@Dependent作用域?
这是我之前在Stack Overflow上探讨WELD对无参构造器要求问题的后续提问,当时我研究了WELD框架对Bean构造器的约束规则。
我现在遇到的核心问题是:想要把一个**@ApplicationScoped**(属于@Normal作用域)的Keycloak Bean注入到**@Dependent**作用域的Bean中,但这个Keycloak类是第三方库提供的,没有非私有的无参构造器,不符合WELD的代理要求,因此抛出错误:无法注入@Normal作用域Bean,因无无参构造器导致无法代理。
我的代码如下:
@Produces @ApplicationScoped @Named("keycloakAdmin") public Keycloak getKeycloakAdminClient(@Named("keycloakDeployment") final KeycloakDeployment deployment) { String clientId = deployment.getResourceName(); Map<String, Object> clientCredentials = deployment.getResourceCredentials(); // 设置resteasy客户端连接池大小以保证线程安全 ResteasyClient client = new ResteasyClientBuilder() .connectionPoolSize(CONNECTION_POOL_SIZE) .maxPooledPerRoute(CONNECTION_POOL_SIZE) .defaultProxy("localhost",8888) .build(); KeycloakBuilder builder = KeycloakBuilder.builder() .clientId(clientId) .clientSecret((String) clientCredentials.get(CredentialRepresentation.SECRET)) .realm(deployment.getRealm()) .serverUrl(deployment.getAuthServerBaseUrl()) .grantType(OAuth2Constants.CLIENT_CREDENTIALS) .resteasyClient(client); return builder.build(); } // 错误抛出点:无法注入@Normal scoped bean,因为它不可代理(无无参构造器) @Produces @Dependent @Named("keycloakRealm") public RealmRepresentation getKeycloakRealm( @Named("keycloakAdmin") final Keycloak adminClient ){ return adminClient.realm(resolveKeycloakDeployment().getRealm()).toRepresentation(); }
我的需求是:让Keycloak Bean作为应用级单例(线程安全,仅初始化一次),同时能注入到非应用作用域的Bean中。想知道有没有可行的方案,另外@Singleton作用域能不能解决这个问题,以及它和@ApplicationScoped的区别,还有在EAR或多EAR场景下的适用情况。
可行的解决方案:绕开代理要求
方案1:通过Provider注入目标Bean
CDI中,当你注入javax.inject.Provider<Keycloak>而非直接注入Keycloak时,WELD不需要为Keycloak创建代理——因为Provider本身是CDI可代理的类型,你可以通过provider.get()获取实际的单例实例。修改后的代码如下:
@Produces @Dependent @Named("keycloakRealm") public RealmRepresentation getKeycloakRealm( @Named("keycloakAdmin") Provider<Keycloak> adminClientProvider ){ Keycloak adminClient = adminClientProvider.get(); return adminClient.realm(resolveKeycloakDeployment().getRealm()).toRepresentation(); }
这种方式简单直接,不需要修改原有的Keycloak生产者逻辑,完全绕开了第三方类无法代理的问题。
方案2:自定义可代理的包装类
如果你不想依赖Provider,也可以自己编写一个符合CDI代理要求的包装类,将第三方Keycloak实例封装进去:
@ApplicationScoped public class KeycloakAdminWrapper { private final Keycloak keycloak; @Inject public KeycloakAdminWrapper(@Named("keycloakDeployment") KeycloakDeployment deployment) { // 复制原有的Keycloak初始化逻辑 String clientId = deployment.getResourceName(); Map<String, Object> clientCredentials = deployment.getResourceCredentials(); ResteasyClient client = new ResteasyClientBuilder() .connectionPoolSize(CONNECTION_POOL_SIZE) .maxPooledPerRoute(CONNECTION_POOL_SIZE) .defaultProxy("localhost",8888) .build(); this.keycloak = KeycloakBuilder.builder() .clientId(clientId) .clientSecret((String) clientCredentials.get(CredentialRepresentation.SECRET)) .realm(deployment.getRealm()) .serverUrl(deployment.getAuthServerBaseUrl()) .grantType(OAuth2Constants.CLIENT_CREDENTIALS) .resteasyClient(client) .build(); } public Keycloak getKeycloak() { return keycloak; } }
之后在需要注入的地方使用这个包装类:
@Produces @Dependent @Named("keycloakRealm") public RealmRepresentation getKeycloakRealm( KeycloakAdminWrapper adminWrapper ){ Keycloak adminClient = adminWrapper.getKeycloak(); return adminClient.realm(resolveKeycloakDeployment().getRealm()).toRepresentation(); }
这个包装类是你自定义的,拥有符合CDI要求的构造器,WELD可以为它生成代理,完美适配第三方类的限制。
@Singleton vs @ApplicationScoped:区别与适用场景
核心差异对比
作用域范围:
@ApplicationScoped(CDI规范):作用域限定为单个Web应用(WAR),如果是包含多个WAR的EAR,每个WAR会拥有独立的@ApplicationScoped实例。@Singleton(EJB规范):作用域是整个Java EE容器实例,同一个容器内的所有WAR/EAR都会共享同一个@Singleton实例。
并发处理:
@ApplicationScoped:默认无容器级并发控制,需要开发者自行保证Bean的线程安全(比如你设置Resteasy连接池的方式)。@Singleton:默认由容器管理并发,默认情况下同一时间只有一个线程能访问Bean方法;你可以通过@Lock(LockType.READ)或@Lock(LockType.WRITE)自定义并发策略。
代理规则:
@ApplicationScoped属于@Normal作用域,要求Bean可代理(需有非私有无参构造器或实现接口)。@Singleton是EJB作用域,其代理机制独立于CDI,无需目标类拥有无参构造器;如果用@EJB注入而非CDI的@Inject,可以完全绕过CDI的代理限制。
用@Singleton解决问题的实践
如果想用@Singleton解决当前问题,可以将Keycloak的初始化逻辑放到EJB Singleton中:
@Singleton @Named("keycloakAdmin") public class KeycloakAdminProducer { private Keycloak keycloak; @PostConstruct public void init(@Named("keycloakDeployment") KeycloakDeployment deployment) { // Keycloak初始化逻辑 String clientId = deployment.getResourceName(); Map<String, Object> clientCredentials = deployment.getResourceCredentials(); ResteasyClient client = new ResteasyClientBuilder() .connectionPoolSize(CONNECTION_POOL_SIZE) .maxPooledPerRoute(CONNECTION_POOL_SIZE) .defaultProxy("localhost",8888) .build(); this.keycloak = KeycloakBuilder.builder() .clientId(clientId) .clientSecret((String) clientCredentials.get(CredentialRepresentation.SECRET)) .realm(deployment.getRealm()) .serverUrl(deployment.getAuthServerBaseUrl()) .grantType(OAuth2Constants.CLIENT_CREDENTIALS) .resteasyClient(client) .build(); } public Keycloak getKeycloak() { return keycloak; } }
然后在@Dependent的生产者中用@EJB注入:
@Produces @Dependent @Named("keycloakRealm") public RealmRepresentation getKeycloakRealm( @EJB KeycloakAdminProducer adminProducer ){ Keycloak adminClient = adminProducer.getKeycloak(); return adminClient.realm(resolveKeycloakDeployment().getRealm()).toRepresentation(); }
这种方式利用EJB的代理机制,完全避开了CDI对第三方类的代理要求。
EAR/多EAR场景的选择
- 单WAR或需要每个WAR独立实例:选
@ApplicationScoped。 - EAR包含多个WAR且需要共享实例:选
@Singleton,它能保证整个容器内实例唯一。 - 多EAR场景:每个EAR是独立的容器上下文,每个EAR会拥有自己的
@Singleton实例;跨EAR的实例共享需要依赖分布式缓存等额外机制,超出了基础作用域的范畴。
内容的提问来源于stack exchange,提问作者Eric B.

