第三章 高级装配

第三章 高级装配

标签(空格分隔): 未分类


[TOC]

环境与profile

配置profile bean

在开发过程中,不同开发环境可能会出现代码不同的情况,比方说:一般开发和测试会有两个不同的环境和数据库等等。Spring引入了Profile的功能,要使用Profile,你需要将不同的Bean整理到一个或者多个Profile中,将应用部署到每个环境的时候,需确定对应的Profile处于激活状态(active

在Java中,可以使用@Profile注解指定某个bean属于哪个profile

ProfileConfig.class

@Configuration
public class ProfileConfig {

    @Bean
    @Profile("dev") // 指定下面的bean属于dev profile
    public DemoBean devBean(){
        return new DemoBean("这是测试环境");
    }

    @Bean
    @Profile("prod") // 指定下面的bean属于prod profile
    public DemoBean prodBean(){
        return new DemoBean("这是生产环境");
    }

}

MainTest.java


public class MainTest {

    public static void main(String []args){

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        context.getEnvironment().setActiveProfiles("dev"); // 激活某些profile状态
        context.register(ProfileConfig.class);
        context.refresh();

        DemoBean demoBean = context.getBean(DemoBean.class);

        System.out.println(demoBean.getContent());

        context.close();

    }
}

@Profile 注解不仅可以加到@Bean的下方,还可以加到类级别上(@Configuration):

@Configuration
@Profile(value="prod")
public class ProductionProfileConfig{
    // ...
}

表示仅有当@Profile中的环境激活时(也就是例子中的prod,dev...),才可以创建相应的bean

除了在Java文件中加上注解之外,我们还可以通过XML配置文件的方法来配置相应的profile:

spring-config.xml




    
        
        
    

    


当prod的profile被激活时,spring-config.xml才会被用到。

我们还可以在一个XML文件中通过标签嵌套定义多个profile

重复使用 元素来指定多个profile

spring-config.xml




    
        
            ...
        
    

    
        
            ....
        
    


激活profile

激活profile需要依赖两个独立的属性:

spring.profiles.active // 优先级要比default高
spring.profiles.default // 如果active没有设置,才会查找default中的值

系统会优先使用spring.profiles.active中所设置的profile

如果spring.profiles.activespring.profiles.default均没有设置的话,那就没有激活的profile,因此,只会创建那些没有定义在profile中的bean 。

有多种方式来设置这两个属性:

  • 作为DispatcherServlet的初始化参数
  • 作为Web应用的上下文参数
  • 作为JNDI条目
  • 作为环境变量
  • 作为JVM的系统属性
  • 在集成测试上,使用@ActiveProfiles注解设置

在web应用中的web.xml中设置默认的profile
web.xml




    
        index.jsp
    

    
    
        spring.profiles.default
        dev
    

    
        org.springframework.web.context.ContextLoaderListener
    

    
    
        appServlet
        org.springframework.web.servlet.DispatcherServlet
        
            
            spring.profiles.default
            dev
        
        1
    

    
        appServlet
        /
    



使用profile进行测试

Spring提供了 @ActiveProfiles 注解,来指定运行测试时需要激活哪个profile

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-config.xml")
@ActiveProfiles("prod")
public class UserDaoTest {
    // ...
}

条件化的bean

假如我们希望某个bean只有当另一个特定的bean声明了之后才会创建,或者需要某个特定的环境变量才会创建,就需要用到 @Conditional 注解

它可以用在带有 @Bean 注解的方法上,如果给定的条件计算结果为true,就创建这个bean,否则就不创建。

例如,假设有一个MagicBean的类,当设置了magic环境属性的时候,Spring才会实例化这个类:

MagicBeanConfig.class

@Configuration
public class MagicBeanConfig {

    @Bean
    @Conditional(MagicExistsCondition.class)
    public MagicBean magicBean(){
        return new MagicBean();
    }

}

@Conditional 给定了一个class ,它指明了一个条件 —— 在本例中,也就是 MagicExistsCondition 类 。MagicExistsCondition需要实现Condition接口。

@Conditional 将会通过 Condition 接口进行对比:

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

设置给 @Conditional 的类可以是任意实现了Condition 接口的类型 ,实现 Condition接口需要实现matches方法。如果 matches() 方法返回 true , 那么就会创建带有 @Conditional 注解的bean,否则就不会创建这些 bean

MagicExistsCondition.class

public class MagicExistsCondition implements Condition {

    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment environment = conditionContext.getEnvironment();
        // 检查环境中是否存在magic的环境属性
        return environment.containsProperty("magic");
    }
}

MainTest.class —— 测试类

public class MainTest {
    public static void main(String []args){
        System.setProperty("magic","true");
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MagicBeanConfig.class);
        // 打印 ture
        System.out.println(context.containsBean("magicBean"));
        context.close();
    }
}

Condition实现的考量要比本例中的更多,mathes()方法会得到ConditionContextAnnotatedTypeMetadata 对象用来做出决策 。

ConditionContext是一个接口,大致如下所示:

public interface ConditionContext {
    // 检查bean定义
    BeanDefinitionRegistry getRegistry();
    // 检查bean是否存在,甚至探查bean的属性
    ConfigurableListableBeanFactory getBeanFactory();
    // 检查环境变量是否存在以及它的值是什么
    Environment getEnvironment();
    // 返回ResourceLoader所加载的资源
    ResourceLoader getResourceLoader();
    // 加载并检查类是否存在
    ClassLoader getClassLoader();
}

AnnotatedTypeMetadata能让我们检查带有 @Bean 注解的方法上还有什么其他的注解 , AnnotatedTypeMetadata也是一个接口,他如下所示 :

public interface AnnotatedTypeMetadata {
    // 判断方法是否还有其他注解的属性
    boolean isAnnotated(String var1);

    @Nullable
    Map getAnnotationAttributes(String var1);

    @Nullable
    Map getAnnotationAttributes(String var1, boolean var2);

    @Nullable
    MultiValueMap getAllAnnotationAttributes(String var1);

    @Nullable
    MultiValueMap getAllAnnotationAttributes(String var1, boolean var2);
}

返回到这篇笔记刚开始的地方,我们看下 @Profile 是如何实现的:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
    String[] value();
}

@Profile 本身也是用了 @Conditional 注解 ,并引用ProfileCondition 作为 Condition 的实现 。

ProfileCondition 检测某个 bean profile 是否可用

class ProfileCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 获取用于@Profile注解的所有属性
        MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            // 获取value属性
            for (Object value : attrs.get("value")) {
                // 检查value属性中的profile是否处于激活状态
                if (context.getEnvironment().acceptsProfiles((String[]) value)) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
}

自动处理装配的歧义性

仅有一个bean匹配所需的结果时 ,自动装配才是有效的 。如果不仅有一个bean能够匹配结果的话 ,这种歧义性会阻碍 Spring 自动装配属性 、构造器参数或者方法参数 。

在发生歧义时,可以选择bean中的某一个设为首选(Primary)的bean 。或者使用限定符(Quailfier)来帮助Spring将可选的bean的范围缩小到只有一个bean 。

标志首选的bean --- @Primary

甜点的例子

public interface Dessert {
    void play();
}

@Component
public class Cake implements Dessert {
    // ...
}

@Component
public class Cookie implements Dessert {
    // ...
}

@Component
public class IceCream implements Dessert {
    // ...
}

在本例中, Dessert 是一个接口 ,并且有三个类实现了这个接口 。因为这三个类均使用了 @Component 注解 , 在组件扫描时 ,能够发现他们并且将其创建为Spring上下文参数的bean 。

@Component
public class DessertMachine implements Machine {
    private Dessert dessert;
    @Autowired
    public void setDessert(Dessert dessert){
        this.dessert = dessert;
    }
    public void play(){
        dessert.play();
    }
}

当Spring试图装配 setDessert() 中的 Dessert 参数时 ,它并没有唯一、无歧义的可选值 。Spring会抛出相应的异常 。

在Spring中,可以使用 @Primary 来表达最喜欢的方案, @Primary@Component组合用在组件扫描的bean上,例如:

@Primary
@Component
public class IceCream implements Dessert {
    // ...
}

@Primary 也可以与 @Bean组合用在Java 配置的bean 声明中。

如果你使用XML配置bean的话,元素有一个primary属性来指定首选的bean 。


当时,如果你标识了两个或者更多的首选bean ,那么它就无法正常工作了。

限定自动装配的bean

@Qualifier 注解是使用限定符的主要方式 , 可以和 @Autowired 协同使用,在注入的时候指定想要注入哪一个bean 。

@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

@Qualifier注解所设置的参数就是想要注入的bean的ID 。因为所有使用 @Component 注解的声明的类都会创建为bean ,并且bean的ID为首字母变为小写的类名,所以,@Qualifier("iceCream") 指向的是组件扫描时所创建的bean,并且这个bean是 IceCream 的实例 。

这里的问题是如果重构了IceCream类,bean的ID和默认的限定符就会变,这里就无法匹配setDessert()中指定的限定符 。自动装配会失败 。

@Qualifier可以自定义名称,做到与类名(或者说bean的ID)解耦 :

@Component
@Qualifier("cold")
public class IceCream implements Dessert {

    public void play() {
        System.out.println("这是 IceCream");
    }
}

这样在装配bean的时候就需要引用cold限定符了 :

@Autowired
@Qualifier(value = "cold")
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

当程序员自定义@Qualifier值时,最佳实践是为bean选择特征性或者描述性的术语。在IceCream中,我们将Qualifier自定义为cold,假设另外一个Dessert的实现类也是cold的话 :

@Component
@Qualifier(value = "cold")
public class Popsicle implements Dessert {
    public void play() {
        System.out.println("这是 水果冰棒");
    }
}

程序再次出现了歧义性的问题 。

Java8允许出现重复的注解,只要这个注解本身在定义的时候带有 @Repeatable 注解就可以。不过,Spring的@Qualifier注解并没有在定义时添加@Repeatable注解。所以,下面这种使用方法是错误的:

@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert){
  this.dessert = dessert;
}

那么,我们应该如何区分IceCreamPopsicle类呢 ?

创建自定义的限定符注解:

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
    // ...
}

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {
    // ...
}

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Fruit {
    // ...
}

现在,我们可以重新看下 IceCream ,并为其添加以下注解:

@Component
@Cold
@Creamy
public class IceCream implements Dessert {
    // ...
}
// ...
@Component
@Cold
@Fruit
public class Popsicle implements Dessert {
    // ...
}

最终,在注入点,我们使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求 。

@Autowired
@Cold
@Fruit
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

为了创建自定义的条件化注解,我们创建了一个新的注解并在这个注解上添加了@Conditional 。为了创建自定义的限定符注解,我们创建一个新的注解并在这个注解上加上 @Qualifier

自定义@Conditional注解示例

ConditionalOnMyProperties.class

@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnMyPropertiesCondition.class)
public @interface ConditionalOnMyProperties {

    String name();
}

OnMyPropertiesCondition.class

public class OnMyPropertiesCondition implements Condition {

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 获取注解上的name属性
        Object propertiesName = metadata.getAnnotationAttributes(ConditionalOnMyProperties.class.getName()).get("name");
        if (propertiesName != null){
            // 检查环境中是否存在该属性的值
            boolean value = context.getEnvironment().containsProperty(propertiesName.toString());
            if(value){
                return true;
            }
        }
        return false;
    }
}

HelloWorld.class

public class HelloWorld {
    public void print() {
        System.out.println("hello world");
    }
}

ConditionClass.class

@Configuration
@ConditionalOnMyProperties(name = "message")
public class ConditionClass {
    @Bean
    public HelloWorld helloWorld(){
        return new HelloWorld();
    }
}
public static void main(String[] args) {
        System.setProperty("message","something");
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConditionClass.class);
        try {
            context.getBean(HelloWorld.class).print();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

bean的作用域

在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。

Spring定义了多种作用域,可以基于这些作用域创建bean

  • 单例(Singleton):在整个应用中,只创建bean的一个实例。
  • 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。
  • 请求(Request):在Web应用中,为每个请求创建一个bean实例。

单例是默认的作用域,但对于易变(mutable)的类型来说,这种作用域并不合适,如果选择其他的作用域,要使用@Scope注解,他可以和@Component或者@Bean一起使用。

例如:

如果你使用组件扫描来发现bean,那么你可以在bean的类上使用 @Scope 注解,将其声明为原型bean 。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class NotePad{
    // ...
}

如果你想在Java配置(显示配置)中将Notepad声明为原型bean,那么可以组合使用 @Scope@Bean 来指定所需要的作用域。

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad(){
    return new Notepad();
}

你还可以在XML中设置作用域:


使用会话和请求作用域

在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。就购物车bean来说,每一个用户都需要有不同的购物车,所以会话作用域是最为合适的,因为它与给定的用户关联性最大。

要指定会话作用域,我们可以使用@Scope注解:

@Component
@Scope(value= WebApplicationContext.SCOPE_SESSION,
      proxyMode = ScopedProxyMode.INTERFACES)
)
// 当ShoppingCart是一个接口时
public ShoppingCart cart(){
  // ...
}

在这里,我们将value设置为了WebApplicationContext中的SCOPE_SESSION常量,这表明Spring为web应用中的每个会话创建一个实例。

要注意的是,@Scope还有一个proxyMode属性,它被设置为了ScopedProxyMode.INTERFACES。这个属性解决了将会话或者请求作用域的bean注入到单例bean中所遇到的问题 :

假设我们需要将ShoppingCart的bean注入到单例StoreServicebean的setter方法中:

@Component
public class StoreService{

  @Autowired
  public void setShoppingCart(ShoppingCart shoppingCart){
    this.shoppingCart = shoppingCart;
  }

}

因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCartbean注入到setShoppingCart()方法中。但是ShoppingCartbean 是会话作用域的,此时并不存在。直到某个用户进入了系统,创建了会话之后,才会出现ShoppingCart的实例。

另外,系统中将会出现多个ShoppingCart实例,我们并不希望将特定的ShoppingCart注入到StoreService中,我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart恰恰是当前会话所对应的那个。

Spring并不会将实际的ShoppingCartbean注入到StoreService中,Spring会注入一个到ShoppingCartbean的代理:

[图片上传失败...(image-19b5ab-1533698521996)]

这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用购物车方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCartbean 。

如配置所示,proxyMode被设置成为了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。

如果ShoppingCart是一个接口而不是一个类,这是可以的。但是如果ShoppingCart是一个具体的类,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体的类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS

请求作用域和会话作用域一样,也需要以作用域的方式进行注入。

在XML中使用作用域代理




    
        
        
    


中的scope属性能够设置bean的作用域,是和@Scope注解中的proxyMode属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。但是,我们可以将proxy-target-class属性设置为false,进而告诉它生成基于接口的代理。

运行时值注入

在讨论依赖注入的时候,我们通常所讨论的是将一个bean引用注入到另一个bean的属性或者构造器参数中。它通常来讲是将一个对象与另一个对象进行关联。

然而还有另一个方面是将一个值注入到bean的属性或者构造器中。

例如:我们将专辑的名字装配到BlanckDiscbean 的构造器或者title属性中:

BlankDisc.class

public class BlankDisc implements CompactDisc {

    private String title;
    private String artist;

    public BlankDisc(String title, String artist) {
        this.title = title;
        this.artist = artist;
    }

    @Override
    public void play() {
        System.out.print("Playing " + title + " by " + artist);
    }
}

使用XML装配:


        
        

我们还可能使用Java的方式进行装配:

@Bean
public CompactDisc sgtPeppers(){
  return new BlankDisc("Sgt. Pepper's Lonely Hearts Club Band","The Beatles");
}

但是,以上两种方式都是将值以固定编码的形式注入到bean中的。Spring提供了两种方式让这些值在运行时注入:

  • 属性占位符
  • Spring表达式语言

注入外部的值

在Spring中,获取外部值最简单的方式是声明数据源并通过Spring的Enviroment来获取属性值 :

@Configuraion
@PropertyResource(value="classpath:/app.properties")
public class ExpressiveConfig{
    @Autowired
    private Enviroment enviroment;
    @Autowired
    private BlankDisc disc(){
        return new BlankDisc(enviroment.getProperty("disc.title"),
                            enviroment.getProperty("disc.artist")
        );
    }
}

app.properties

disc.title = 寻宝游戏
disc.artist = vae
public interface CompactDisc {
    void play();
}
// ...
public class BlankDisc implements CompactDisc{
    private String title;
    private String artist;

    public BlankDisc(String title , String artist) {
        this.title = title;
        this.artist = artist;
    }

    public void play() {
        System.out.println("title : " + title + " artist : " + artist);
    }
} 

Enviroment类中还有其他一系列的方法,其中包括getProperty()方法的重载以及其他方法 :

public interface PropertyResolver {
    // 检查某个属性是否存在
    boolean containsProperty(String var1);

    String getProperty(String var1);
    // 当指定属性var1不存在时候,返回默认值var2
    String getProperty(String var1, String var2);

    @Nullable
    // 返回非字符串类型T
     T getProperty(String var1, Class var2);
    // 当指定属性var1不存在时候,返回指定类型T var3
     T getProperty(String var1, Class var2, T var3);
    // 当指定属性不存在时候报错
    String getRequiredProperty(String var1) throws IllegalStateException;

     T getRequiredProperty(String var1, Class var2) throws IllegalStateException;

    String resolvePlaceholders(String var1);

    String resolveRequiredPlaceholders(String var1) throws IllegalArgumentException;
}

除了属性相关功能之外,Enviroment对象还提供了一些方法来检查哪些profile处于激活状态 :

public interface Environment extends PropertyResolver {
    // 获取到profile为active的数组
    String[] getActiveProfiles();
    // 获取到profile为default的数组
    String[] getDefaultProfiles();
    // 如果environment支持给定的profile 就返回true
    boolean acceptsProfiles(String... var1);
}

解析属性占位符

使用XML方式



使用Java配置方式

@Configuration
@ComponentScan(basePackages = "com.springdemo")
@PropertySource(value = "classpath:/app.properties")
public class ExpressiveConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
        return new PropertySourcesPlaceholderConfigurer();
    }
}
@Component
public class BlankDisc implements CompactDisc {
    // ...  
    public BlankDisc(
            @Value("${disc.title}") String title ,
            @Value("${disc.artist}") String artist) {
        this.title = title;
        this.artist = artist;
    }
}

总结

在这章中我们主要学习了以下内容 :

  • @Profile注解 : 主要用来区别不同开发或者运行环境问题,例如是mysql还是oracle数据库等等
  • 激活profile的几种方法 : 作为WEB应用上下文、作为JNDI条目、作为环境变量、作为JVM系统属性、在集成测试中使用@ActiveProfiles注解设置
  • @Conditional注解 :@Profile的“升级版”,设置条件化的Bean,@Conditional注解中的类必须要实现Conditional接口中的matches()方法
  • @Primary注解 : 标示首选的bean(不建议使用)
  • @Qualifier注解 : 消除装配时的歧义性 。也可以通过@Qualifier自定义限定符注解 。
  • bean的四种作用域 :单例、原型 、会话 、请求
  • @Scope注解修改bean的作用域 :@Scope(ConfigurationBeanFactory.SCOPE_PROTOTYPE),如果是会话/请求作用域的话,除了设置@Scope(value = WebApplicationContext.SCOPE_SESSION)之外,还需要设置proxyMode=ScopedProxyMode.INTERFACE或者proxyMode=ScopedProxyMode.TARGET_CLASS
第三章 高级装配_第1张图片
第三章 高级装配

你可能感兴趣的:(第三章 高级装配)