前言
公司最近引入分布式事务框架Seata,但在过程中发现Oauth2与Seata存在冲突导致工程无法启动。虽然问题最终被解决但却耗费了较多的时间和精力,故在此记录下问题的完整解决流程及原因,供自身即童鞋们参考。
环境
Spring Boot版本
<!-- 父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
</parent>
Spring Cloud版本
<!-- Spring Cloud系列依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Seata版本
<!-- Spring Cloud工程阿里巴巴Seata分布式事物依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
异常日志
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.clientDetailsService' defined in org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.provider.ClientDetailsService]: Factory method 'clientDetailsService' threw exception; nested exception is java.lang.UnsupportedOperationException: Cannot build client services (maybe use inMemory() or jdbc()).
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:590)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1247)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1096)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:535)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
at io.seata.spring.util.SpringProxyUtils.findTargetClass(SpringProxyUtils.java:51)
at io.seata.spring.annotation.GlobalTransactionScanner.wrapIfNecessary(GlobalTransactionScanner.java:256)
... 31 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.provider.ClientDetailsService]: Factory method 'clientDetailsService' threw exception; nested exception is java.lang.UnsupportedOperationException: Cannot build client services (maybe use inMemory() or jdbc()).
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:582)
... 42 common frames omitted
Caused by: java.lang.UnsupportedOperationException: Cannot build client services (maybe use inMemory() or jdbc()).
at org.springframework.security.oauth2.config.annotation.builders.ClientDetailsServiceBuilder.performBuild(ClientDetailsServiceBuilder.java:82)
at org.springframework.security.oauth2.config.annotation.builders.ClientDetailsServiceBuilder.build(ClientDetailsServiceBuilder.java:75)
at org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration.clientDetailsService(ClientDetailsServiceConfiguration.java:46)
at org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration$$EnhancerBySpringCGLIB$$d99149e2.CGLIB$clientDetailsService$0(<generated>)
at org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration$$EnhancerBySpringCGLIB$$d99149e2$$FastClassBySpringCGLIB$$9a70cf7.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:361)
at org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration$$EnhancerBySpringCGLIB$$d99149e2.clientDetailsService(<generated>)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
... 43 common frames omitted
异常日志很长,这里只截取关键部分。上述片段包含解决问题所需所有内容,我们会逐一讲述,并给出解决方法。
UnsupportedOperationException: Cannot build client services (maybe use inMemory() or jdbc()).
不支持操作异常:无法构建客户端服务集(或许使用内存或JDBC)。异常描述在工程启动时配置客户端步骤发生异常。
BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.provider.ClientDetailsService]: Factory method 'clientDetailsService' threw exception;
Bean实例化异常:实例化ClientDetailsService类Bean失败:工程方法"clientDetailsService"抛出异常。异常描述工程启动时实例化ClientDetailsService类Bean步骤抛出了异常。
at io.seata.spring.util.SpringProxyUtils.findTargetClass(SpringProxyUtils.java:51)
at io.seata.spring.annotation.GlobalTransactionScanner.wrapIfNecessary(GlobalTransactionScanner.java:256)
异常描述在Seata对Bean进行扫描时发生了异常。总结上述三点可知:Seata在扫描到ClientDetailsService类Bean时发生了异常。
ClientDetailsService类Bean有何特殊性?为何在被扫描时会出现异常呢?
@Bean
@Lazy
@Scope(proxyMode = ScopedProxyMode.INTERFACES)
public ClientDetailsService clientDetailsService() throws Exception {
return ((ClientDetailsServiceBuilder)this.configurer.and()).build();
}
上述是Spring Cloud Oauth2框架创建ClientDetailsService类默认Bean的方法,我们定位到该方法的下级,会看到如此逻辑。
public ClientDetailsService build() throws Exception {
Iterator var1 = this.clientBuilders.iterator();
while(var1.hasNext()) {
ClientDetailsServiceBuilder<B>.ClientBuilder clientDetailsBldr = (ClientDetailsServiceBuilder.ClientBuilder)var1.next();
this.addClient(clientDetailsBldr.clientId, clientDetailsBldr.build());
}
return this.performBuild();
}
protected void addClient(String clientId, ClientDetails build) {
}
protected ClientDetailsService performBuild() {
throw new UnsupportedOperationException("Cannot build client services (maybe use inMemory() or jdbc()).");
}
默认方法无法创建ClientDetailsService类Bean。分析代码后可以发现Spring Cloud Oauth2框架创建ClientDetailsService类默认Bean的方法是一个“假方法”,执行后并无法创建ClientDetailsService类Bean而是固定抛出操作不支持异常,并建议我们使用内存或JDBC方式配置客户端。
问题关键在于@Lazy注解。@Lazy注解的作用是当使用到具体的Bean时才对之执行实例化操作,因此方法虽然标注了@Bean注解,但因为在工程启动时未使用到该Bean,故未执行该方法抛出异常。
Seata框架触发了方法执行导致异常。Seata的引入打破了该状态,其下GlobalTransactionScanner在工程启动会扫描到该Bean方法,使其于Spring空间中查找名为clientDetailsService的Bean(Bean方法默认将方法名作为Bean的名称)从而触发方法执行(因为未查找到Bean或查到虚假Bean,代码@Scope注解的作用就是向Spring空间中注入一个虚假Bean),导致异常抛出。
解决方案
创建名为clientDetailsService的Bean来避免方法执行。为了避免的方法执行,我们需要创建名为clientDetailsService的Bean,当GlobalTransactionScanner与Spring空间中找到了该Bean,便不会再执行方法。
/**
* 配置客户端详细服务(方法名必须为clientDetailsService)
*
* @param dataSource 数据源对象
* @return 客户端详细服务对象
*/
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
/**
* 配置客户端
*
* @param clients 客户端集
* @throws Exception 异常对象
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
设置新同名Bean覆盖旧同名Bean。编写上述代码后,启动依然会失败,这是因为我们创建的新同名Bean并未覆盖旧同名Bean保存在Spring空间中。关于Spring同名Bean的覆盖规则是一个相对复杂的逻辑,此处不再赘述,直接给出操作流程。
配置允许覆盖
# 如果配置时未出现提示,可不配置,版本已默认支持覆盖并删除了该配置。
spring:
main:
allow-bean-definition-overriding: true
配置扫描路径顺序
/**
* @Author: 说淑人
* @Date: 2022/1/10 13:55
* @Description: 新生用户工程启动类
* @Description: 包路径"org.springframework.security.oauth2.config.annotation.configuration"不可删除,
* @Description: 并且必须位于包路径"com.lchh.rebirth"之前,否则将造成工程启动异常。
*/
@Slf4j
@SpringBootApplication(scanBasePackages = {"org.springframework.security.oauth2.config.annotation.configuration", "com.lchh.rebirth"})
@EnableAuthorizationServer
@EnableResourceServer
@EnableFeignClients(basePackages = {"com.lchh.rebirth"})
@MapperScan("com.lchh.rebirth.${spring.application.keyword}.mapper")
public class RebirthUserApplication {
public static void main(String[] args) {
SpringApplication.run(RebirthUserApplication.class, args);
log.info("新生用户工程【program-rebirth-user】启动完成。");
}
}
将默认方法所在类ClientDetailsServiceConfiguration的包路径设置在本地工程包路径之前,使之先被Spring扫描并生成默认虚假Bean,随后被后扫描生成的新同名Bean覆盖。
【附】参考博文《springboot bean覆盖注册的问题(allowBeanDefinitionOverriding配置)》