首页 > 解决方案 > Spring Boot 应用程序需要一个带有 @Primary 注释的 bean 才能启动

问题描述

我在 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 的一个实例:

在此处输入图像描述

标签: springspring-boot

解决方案


@EnableConfigurationProperties与你一起使用时@ConfigurationProperties会得到一个名为 的<prefix>-<fqn>bean kai-com.example.authservice.config.OAuthProperties,. (另请参阅参考指南)。

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

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

注释也会自动应用于您的@EnableConfigurationProperties项目,以便任何现有的带有注释的 bean@ConfigurationProperties都从Environment. MyConfiguration您可以通过确保已经是一个 bean 来实现快捷方式AcmeProperties,如下例所示:(来自参考指南)。

这里的关键是它@EnableConfigurationProperties已经被全局应用并处理任何用@ConfigurationProperties.

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

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


推荐阅读