【问题标题】:Spring Boot app requires a bean annotated with @Primary to startSpring Boot 应用程序需要一个带有 @Primary 注释的 bean 才能启动
【发布时间】:2019-04-09 09:36:53
【问题描述】:

我在 Spring Boot 应用启动时看到以下消息:

> *************************** APPLICATION FAILED TO START
> ***************************
> 
> Description:
> 
> Field oauthProps in com.example.authservice.AuthorizationServerConfig
> required a single bean, but 2 were found:
>   - OAuthProperties: defined in file [/Users/simeonleyzerzon/abc/spring-security/spring-security-5-oauth-client/auth-service/target/classes/com/example/authservice/config/OAuthProperties.class]
>   - kai-com.example.authservice.config.OAuthProperties: defined in null
> 
> 
> Action:
> 
> Consider marking one of the beans as @Primary, updating the consumer
> to accept multiple beans, or using @Qualifier to identify the bean
> that should be consumed

我想知道 是什么导致了该 bean 的重复,以及如何在无需使用 @Primary 注释的情况下将其删除?不确定上面的 kai-com 包(?)来自哪里。

这是有问题的 bean:

package com.example.authservice.config;

    //@Primary
    @Component
    @ConfigurationProperties(prefix="kai")
    @Setter @Getter
    public class OAuthProperties {


        private String[] redirectUris;


        private String clientId;


        private String clientSecret;

        private final Token token = new Token();


        @Setter @Getter
        public static class Token{

            private String value;

            private String type="";

        }

    }

以及应用程序/配置等:

package com.example.authservice;

import ...
@SpringBootApplication
public class AuthServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class, args);
    }
}

@Controller
class MainController {

    @GetMapping("/")
    String index() {
        return "index";
    }
}

@RestController
class ProfileRestController {

    @GetMapping("/resources/userinfo")
    Map<String, String> profile(Principal principal) {
        return Collections.singletonMap("name", principal.getName());
    }
}

@Configuration
@EnableResourceServer
class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/resources/**")
                .authorizeRequests()
                .mvcMatchers("/resources/userinfo").access("#oauth2.hasScope('profile')");
    }
}

@Configuration
@EnableAuthorizationServer
@EnableConfigurationProperties(OAuthProperties.class)
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {



    @Autowired private OAuthProperties oauthProps;

    private final AuthenticationManager authenticationManager;

    AuthorizationServerConfig(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients
            .inMemory()

                .withClient(oauthProps.getClientId())
                .secret(oauthProps.getClientSecret())
                .authorizedGrantTypes("authorization_code")
                .scopes("profile")
                .redirectUris(oauthProps.getRedirectUris());


    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.authenticationManager(this.authenticationManager);

        if (oauthProps.getToken().getType().equals("jwt")) {
            endpoints.tokenStore(this.tokenStore()).accessTokenConverter(jwtAccessTokenConverter());
        }else {
            endpoints.tokenEnhancer(eapiTokenEnhancer());
        }
    }

    TokenEnhancer eapiTokenEnhancer() {

        return new TokenEnhancer() {

            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

                DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
                result.setValue(oauthProps.getToken().getValue());
                return result;
            }
        };

    }

    @Bean
    JwtAccessTokenConverter jwtAccessTokenConverter() {
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource(".keystore-oauth2-demo"), //keystore
                "admin1234".toCharArray());                                                                 //storepass
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(factory.getKeyPair("oauth2-demo-key"));                          //alias
        return jwtAccessTokenConverter;
    }

    @Bean
    TokenStore tokenStore() {
        return new JwtTokenStore(this.jwtAccessTokenConverter());
    }
}

@Service
class SimpleUserDetailsService implements UserDetailsService {

    private final Map<String, UserDetails> users = new ConcurrentHashMap<>();

    SimpleUserDetailsService() {
        Arrays.asList("josh", "rob", "joe")
                .forEach(username -> this.users.putIfAbsent(
                        username, new User(username, "pw", true, true, true, true, AuthorityUtils.createAuthorityList("USER","ACTUATOR"))));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return this.users.get(username);
    }
}

@Configuration
@EnableWebSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .formLogin();

    }
}

Eclipse 似乎也只知道 bean 的单个实例:

【问题讨论】:

标签: spring spring-boot


【解决方案1】:

当使用@EnableConfigurationProperties 和@ConfigurationProperties 时,您将得到一个名为&lt;prefix&gt;-&lt;fqn&gt; 的bean,即kai-com.example.authservice.config.OAuthProperties。 (另见the reference guide)。

当@ConfigurationProperties bean 以这种方式注册时,bean 有一个常规名称:&lt;prefix&gt;-&lt;fqn&gt;,其中&lt;prefix&gt; 是@ConfigurationProperties 注释中指定的环境键前缀,&lt;fqn&gt; 是完全限定名称豆的。如果注解不提供任何前缀,则仅使用 bean 的完全限定名称。 上面示例中的 bean 名称是 acme-com.example.AcmeProperties。 (来自参考指南)。

@Component 将导致使用小写字符的类名的常规名称再次注册 bean。您的属性的另一个实例。

@EnableConfigurationProperties 注释也会自动应用到您的项目中,因此任何现有的带有@ConfigurationProperties 注释的 bean 都是从 Environment 配置的。您可以通过确保 AcmeProperties 已经是一个 bean 来缩短 MyConfiguration,如下例所示:(来自参考指南)。

这里的关键是@EnableConfigurationProperties 已经被全局应用并处理任何带有@ConfigurationProperties 注释的bean。

所以基本上你现在混合使用@ConfigurationProperties 和 Spring Boot 2 的两种方式可以防止这种误用。这样您可以编写更好的代码(并稍微减少内存占用和性能)。

因此,要么删除@Component,要么删除@EnableConfigurationProperties,任何一种方式都可以。

【讨论】:

  • 在对文档进行了大约 6 次密集的重新阅读并参考代码和您的解释之后,我只能说前者的措辞和给定的示例远非显而易见如果不是(无意)晦涩难懂。你上面的措辞用更少的词更清楚。我强烈建议重写文档的相关部分并突出显示这些副作用,以指导开发人员完成手头的任务。
【解决方案2】:

以下更改(删除 @EnableConfigurationProperties)似乎有助于减轻对 @Primary 注释的需求:

@Configuration
@EnableAuthorizationServer
//@EnableConfigurationProperties(OAuthProperties.class)
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired private OAuthProperties oauthProps;

也许有人可以通过看起来会导致与@Autowired 冲突的注释来描述二级 bean 创建的内部 Spring 机制(及其命名空间/包分配),或者指向此行为的适当文档。

【讨论】:

  • 因为@EnableConfigurationProperties 并且您的班级也是@Component。您应该使用其中一种,但不能同时使用。
猜你喜欢
  • 2021-07-02
  • 2023-02-01
  • 2021-09-22
  • 1970-01-01
  • 2018-08-12
  • 1970-01-01
  • 1970-01-01
  • 2016-08-13
  • 1970-01-01
相关资源
最近更新 更多