Spring Cloud【2】Oauth2与Seata冲突

前言

    公司最近引入分布式事务框架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配置)》

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

说淑人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值