spring5.0 之@Primary注解的应用

在spring容器中,如果同一个类型有多个实例,但我们需要注入一个的时候,我们必须采取措施,不然spring容器
会报错:....required a single bean, but 2 were found:.........
有时候我们能保证同一个类型在spring容器中只有一个实例,有时候我们保证不了,此时不讨论by name注入。这
个时候@Primary注解就非常重要了。
 
org.springframework.context.annotation
Annotation Type Primary

 
@Target(value={TYPE,METHOD})
 @Retention(value=RUNTIME)
 @Inherited
 @Documented
public @interface Primary
Indicates that a bean should be given preference when multiple candidates are qualified to autowire a single-valued dependency. If exactly one 'primary' bean exists among the candidates, it will be the autowired value.
This annotation is semantically equivalent to the <bean> element's primary attribute in Spring XML.
May be used on any class directly or indirectly annotated with @Component or on methods annotated with @Bean.
Example
 @Component
 public class FooService {

     private FooRepository fooRepository;

     @Autowired
     public FooService(FooRepository fooRepository) {
         this.fooRepository = fooRepository;
     }
 }

 @Component
 public class JdbcFooRepository {

     public JdbcFooService(DataSource dataSource) {
         // ...
     }
 }

 @Primary
 @Component
 public class HibernateFooRepository {

     public HibernateFooService(SessionFactory sessionFactory) {
         // ...
     }
 }
 
Because HibernateFooRepository is marked with @Primary, it will be injected preferentially over the jdbc-based variant assuming both are present as beans within the same Spring application context, which is often the case when component-scanning is applied liberally.
Note that using @Primary at the class level has no effect unless component-scanning is being used. If a @Primary-annotated class is declared via XML, @Primary annotation metadata is ignored, and <bean primary="true|false"/> is respected instead.
Since:
3.0
Author:
Chris Beams
See Also:
Lazy, Bean, ComponentScan, Component

其实@Primary注解的实例优先于其他实例被注入。

下面看一个不得不使用@Primary注解的例子。

在spring security oauth2 资源服务器配置的时候,我们需要实例化一个ResourceServerTokenServices,该类主要从checkTokenEndpointUrl 地址校验用户登录的token。

spring boot 按照默认配置,会自动创建一个RemoteTokenServices,代码在ResourceServerTokenServicesConfiguration中的114行左右:

@Configuration
	@Conditional(RemoteTokenCondition.class)
	protected static class RemoteTokenServicesConfiguration {

		@Configuration
		@Conditional(TokenInfoCondition.class)
		protected static class TokenInfoServicesConfiguration {

			private final ResourceServerProperties resource;

			protected TokenInfoServicesConfiguration(ResourceServerProperties resource) {
				this.resource = resource;
			}

			@Bean
			public RemoteTokenServices remoteTokenServices() {
				RemoteTokenServices services = new RemoteTokenServices();
				services.setCheckTokenEndpointUrl(this.resource.getTokenInfoUri());
				services.setClientId(this.resource.getClientId());
				services.setClientSecret(this.resource.getClientSecret());
				return services;
			}

		}

我想自己创建一个自己的UserInfoTokenServices,如:

package com.sdcuike.spring.security;

import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;

/**
 * Created by beaver on 2017/6/12.
 * <p>
 * 获取用户信息-> oid
 *
 * @see https://stackoverflow.com/questions/35056169/how-to-get-custom-user-info-from-oauth2-authorization-server-user-endpoint/35092561
 */
public class RichUserInfoTokenServices extends UserInfoTokenServices {
    public RichUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
        super(userInfoEndpointUrl, clientId);
    }
    
    public RichUserInfoTokenServices(String userInfoEndpointUrl, String clientId, RichUserPrincipalExtractor richUserPrincipalExtractor) {
        super(userInfoEndpointUrl, clientId);
        setPrincipalExtractor(richUserPrincipalExtractor);
    }
    
}

按照spring boot java config方式创建给bean:

package com.sdcuike.practice.config.security;

import com.sdcuike.spring.security.RichUserDetails;
import com.sdcuike.spring.security.RichUserInfoTokenServices;
import com.sdcuike.spring.security.RichUserPrincipalExtractor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

import java.util.Map;

/**
 * Created by beaver on 2017/6/12.
 * <p>
 * 获取用户信息-> oid
 *
 * @see https://stackoverflow.com/questions/35056169/how-to-get-custom-user-info-from-oauth2-authorization-server-user-endpoint/35092561
 */
@Configuration
public class UserInfoTokenServicesConfig {
    
    @Value("${security.oauth2.resource.clientId}")
    private String clientId;
    
    @Autowired
    private ResourceServerProperties sso;
    
    @Bean
//    @Primary
    public ResourceServerTokenServices richUserInfoTokenServices() {
        return new RichUserInfoTokenServices(sso.getUserInfoUri(), clientId, new RichUserPrincipalExtractor());
    }
    
    
}

如果把@Primary注解注释掉,启动会报错:

***************************
APPLICATION FAILED TO START
***************************

Description:

Method springSecurityFilterChain in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a single bean, but 2 were found:
	- richUserInfoTokenServices: defined by method 'richUserInfoTokenServices' in class path resource [com/sdcuike/practice/config/security/UserInfoTokenServicesConfig.class]
	- remoteTokenServices: defined by method 'remoteTokenServices' in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration$RemoteTokenServicesConfiguration$TokenInfoServicesConfiguration.class]


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

-

但实例化的remoteTokenServices我们无法控制(如果有,请通知俺),我们必须是使用@Primary注解,让我们控制哪个实例优先被注入。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐