• 第03章-高级装配


    1. 环境与profile

    在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。开发阶段中,某些环境相关做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。

    其中一种方式就是在单独的配置类(或XML文件)中配置每个bean,然后在构建阶段(可能会使用Maven的profiles)确定要将哪一个配置编译到可部署的应用中。

    3.1 配置profile bean

    过Spring并不是在构建的时候做出这样的决策,而是等到运行时再来确定。这样的结果就是同一个部署单元(可能会是WAR文件)能够适用于所有的环境,没有必要进行重新构建。

    • Java 配置
      在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。在Spring 3.1中,只能在类级别上使用@Profile注解。不过,从Spring 3.2开始,你也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。这样的话,就能将这两个bean的声明放到同一个配置类之中,如:
    @Configuration
    public class DataSourceConfig {
    
      @Bean(destroyMethod = "shutdown")
      @Profile("dev")
      public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:test-data.sql")
            .build();
      }
    
      @Bean
      @Profile("prod")
      public DataSource jndiDataSource() {
        JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
        jndiObjectFactoryBean.setJndiName("jdbc/myDS");
        jndiObjectFactoryBean.setResourceRef(true);
        jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
        return (DataSource) jndiObjectFactoryBean.getObject();
      }
    
    }
    • XML配置
      在根<beans>元素中嵌套定义<beans>元素,而不是为每个环境都创建一个profile XML文件。这能够将所有的profile bean定义放到同一个XML文件中,如:
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
      xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
      xsi:schemaLocation="
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee.xsd
        http://www.springframework.org/schema/jdbc
        http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
      <beans profile="dev">
        <jdbc:embedded-database id="dataSource" type="H2">
          <jdbc:script location="classpath:schema.sql" />
          <jdbc:script location="classpath:test-data.sql" />
        </jdbc:embedded-database>
      </beans>
    
      <beans profile="prod">
        <jee:jndi-lookup id="dataSource"
          lazy-init="true"
          jndi-name="jdbc/myDatabase"
          resource-ref="true"
          proxy-interface="javax.sql.DataSource" />
      </beans>
    </beans>
    

    3.2 激活profile

    Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.activespring.profiles.default
    如果spring.profiles.activespring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在 profile中的bean。
    有多种方式来设置这两个属性:

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

    在Web应用的web.xml文件中设置默认的profile,使用DispatcherServlet方式激活profile:

    使用profile进行测试
    Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile。

    2. 条件化的bean

    假设你希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。或者我们希望某个bean只有当另外某个特定的bean也声明了之后才会创建。我们还可能要求只有某个特定的环境变量设置之后,才会创建某个bean。
    在Spring 4之前,很难实现这种级别的条件化配置,但是Spring 4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。

    设置给@Conditional的类可以是任意实现了Condition接口的类型。可以看出来,这个接口实现起来很简单直接,只需提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。

    • 实现条件类
    public class MagicExistsCondition implements Condition {
    
      @Override
      public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("magic");
      }
    
    }
    • 定制条件装配JavaConfig
    @Configuration
    @PropertySource("classpath:conf.properties")
    public class MagicConfig {
    
      @Bean
      @Conditional(MagicExistsCondition.class)
      public MagicBean magicBean() {
        return new MagicBean();
      }
    
    }

    其中,MagicBean是一个简单的bean,如下:

    public class MagicBean {
    
    }

    配置文件conf.properties:

    magic=something

    matches()方法很简单但功能强大。它通过给定的ConditionContext对象进而得到Environment对象,并使用这个对象检查环境中是否存在名为magic的环境属性。

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

    AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。像ConditionContext一样,AnnotatedTypeMetadata也是一个接口。

    从Spring 4开始,@Profile注解进行了重构,使其基于@Conditional和Condition实现。

    3. 处理自动装配的歧义性

    发生歧义性的时候,Spring提供了多种可选方案来解决这样的问题。你可以将可选bean中的某一个设为首选(primary)的bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。

    3.1 标示首选的bean

    • 在声明bean的时候声明
      通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。如:

    • 通过Java配置显式地声明

    • 通过XML配置显式地声明

      但是,如果你标示了两个或更多的首选bean,那么它就无法正常工作了。限定符是一种更为强大的机制,下面就将对其进行介绍。

    3.2 限定自动装配的bean

    @Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。
    如,注入特定的bean:

    所有使用@Component注解声明的类都会创建为bean,并且bean的ID为首字母变为小写的类名。基于默认的bean ID作为限定符是非常简单的,但这有可能会引入一些问题。问题在于setDessert()方法上所指定的限定符与要注入的bean的名称是紧耦合的。对类名称的任意改动都会导致限定符失效。

    创建自定义的限定符
    需要做的就是在bean声明上添加@Qualifier注解。例如,它可以与@Component组合使用,如下所示:

    如果多个bean都具备相同特性的话,这种做法也会出现问题。有了两个带有“cold”限定符的甜点。在自动装配Dessert bean的时候,我们再次遇到了歧义性的问题,需要使用更多的限定符来将可选范围限定到只有一个bean。
    Java不允许在同一个条目上重复出现相同类型的多个注解。(Java 8允许出现重复的注解,只要这个注解本身在定义的时候带有@Repeatable注解就可以。不过,Spring的@Qualifier注解并没有在定义时添加@Repeatable注解。)。这时需要使用自定义的限定符注解。

    使用自定义的限定符注解
    自定义的@Cold注解,该注解的定义如下所示:

    同样,你可以创建一个新的@Creamy注解来代替@Qualifier("creamy"):

    现在,我们可以重新看一下IceCream,并为其添加@Cold和@Creamy注解,如下所示:

    4. bean的作用域

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

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

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

    单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。有三种指定Bean作用域的方法:

    • 声明Bean的作用域

    • Java配置指定Bean的作用域

    • XML配置指定Bean的作用域

    不管你使用哪种方式来声明原型作用域,每次注入或从Spring应用上下文中检索该bean的时候,都会创建新的实例。这样所导致的结果就是每次操作都能得到自己的Notepad实例。

    4.1 使用会话和请求作用域

    在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。例如,在典型的电子商务应用中,可能会有一个bean代表用户的购物车。就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。

    要注意的是,@Scope同时还有一个proxyMode属性,它被设置成了ScopedProxyMode.INTERFACES。这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。在描述proxyMode属性之前,我们先来看一下proxyMode所解决问题的场景。
    假设我们要将ShoppingCart bean注入到单例StoreService bean的Setter方法中,如下所示:

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

    Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理,如图3.1所示。这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。

    现在,我们带着对这个作用域的理解,讨论一下proxyMode属性。如配置所示,proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
    如果ShoppingCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果ShoppingCart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。

    4.2 在XML中声明作用域代理

    要设置代理模式,我们需要使用Spring aop命名空间的一个新元素:

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

    5. 运行时值注入

    在前面的介绍中,我们遇到过bean属性或构造器参数硬编码的情况,如:

    • 基于Java的硬编码

    • 基于XML的硬编码

    我们可能会希望避免硬编码值,而是想让这些值在运行时再确定。为了实现这些功能,Spring提供了两种在运行时求值的方式:

    • 属性占位符(Property placeholder)。
    • Spring表达式语言(SpEL)。

    5.1 注入外部的值

    在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。例如,程序清单3.7展现了一个基本的Spring配置类,它使用外部的属性来装配BlankDisc bean。

    • 使用@PropertySource注解和Environment
    @Configuration
    @PropertySource("classpath:/com/soundsystem/app.properties")
    public class EnvironmentConfig {
    
      @Autowired
      Environment env;
    
      @Bean
      public BlankDisc blankDisc() {
        return new BlankDisc(
            env.getProperty("disc.title"),
            env.getProperty("disc.artist"));
      }
    
    }

    其中,app.properties:

    disc.title=Sgt. Peppers Lonely Hearts Club Band
    disc.artist=The Beatles

    深入学习Spring的Environment
    当我们去了解Environment的时候会发现,程序清单3.7所示的getProperty()方法并不是获取属性值的唯一方法,getProperty()方法有四个重载的变种形式:

    如果你希望这个属性必须要定义,那么可以使用getRequiredProperty()方法,如下所示:

    如果想检查一下某个属性是否存在的话,那么可以调用Environment的containsProperty()方法:

    解析属性占位符
    Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用${... }包装的属性名称。作为样例,我们可以在XML中按照如下的方式解析BlankDisc构造器参数:

    如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。在这种情况下,我们可以使用@Value注解,它的使用方式与@Autowired注解非常相似。比如,在BlankDisc类中,构造器可以改成如下所示:

    启用占位符
    为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。从Spring 3.1开始,推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。

    • Java配置启用占位符
      如下的@Bean方法在Java中配置了PropertySourcesPlaceholderConfigurer:

    • XML配置启用占位符

    5.2 使用Spring表达式语言进行装配

    Spring 3引入了Spring表达式语言(Spring Expression Language,SpEL),它能够以一种强大和简洁的方式将值装配到bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值。使用SpEL,你可以实现超乎想象的装配效果,这是使用其他的装配技术难以做到的(甚至是不可能的)。

    SpEL拥有很多特性,包括:

    • 使用bean的ID来引用bean;
    • 调用方法和访问对象的属性;
    • 对值进行算术、关系和逻辑运算;
    • 正则表达式匹配;
    • 集合操作。

    第一件事情就是SpEL表达式要放到#{ ... }之中,这与属性占位符有些类似,属性占位符需要放到${ ... }之中。
    SpEL可进行的操作

    • 表达式使用Java类型

    • 表达式使用bean属性

    • 表达式使用系统属性

    使用SpEL表达式完成占位符的功能

    • 通过组件扫描创建bean

    • 通过XML配置创建bean

    使用SpEL注意
    不要让你的表达式太智能。你的表达式越智能,对它的测试就越重要。SpEL毕竟只是String类型的值,可能测试起来很困难。鉴于这一点,我建议尽可能让表达式保持简洁,这样测试不会是什么大问题。

    Spring 3知识点

    一、注入Bean属性
    1、注入简单值
    元素配置Bean的属性;
    在许多方面都与类似,不过前者通过调用属性的setter方法来注入值,后者构造参数来注入值。
    Instrumentallist.java

    //<start id="instrumentalist_java" />
    package com.springinaction.springidol;
    public class Instrumentalist implements Performer {
      public Instrumentalist() {
      }
      public void perform() throws PerformanceException {
        System.out.print("Playing " + song + " : ");
        instrument.play();
      }
      private String song;
      public void setSong(String song) { //<co id="co_injectSong"/>
        this.song = song;
      }
      public String getSong() {
        return song;
      }
      public String screamSong() {
        return song;
      }
      private Instrument instrument;
      public void setInstrument(Instrument instrument) { //<co id="co_injectInstrument"/>
        this.instrument = instrument;
      }
    }
    //<end id="instrumentalist_java" />

    当需要为song注入值时,可以在spring-idol.xml中如下配置:

    <bean id="kenny"
          class="com.springinaction.springidol.Instrumentalist">
      <property name="song" value="Jingle Bells" />
    </bean>

    该property元素会指示Spring调用setSong()方法将song属性的值设置为Jingle Bells。
    注意,上述value值可以为String/int/float/Double等,Spring将根据Bean属性的类型自动判断value值得正确类型。

    2、引用其他Bean
    当property引用另一个在xml中定义的bean时,需要ref属性,而非value:

    <bean id="kenny2"
          class="com.springinaction.springidol.Instrumentalist">
        <property name="song" value="Jingle Bells" />
        <property name="instrument" ref="saxophone" />
    </bean>
    <bean id="saxophone"
          class="com.springinaction.springidol.Saxophone" />

    3、注入内部Bean
    内部Bean:艺术家Kenny关注乐器的卫生,不想与别人共享萨克斯。
    内部Bean是定义在其他Bean内部的Bean,如:

    即,内部Bean是通过直接声明一个元素作为元素的直接点而定义的;Spring将创建了一个Saxophone,并装配到Kenny的instrument属性中。

    内部Bean并不仅限于setter注入,还可以装配到构造方法的入参中,如:

    注意:内Bean没有ID属性,(有ID属性也是合法的)因为我们永远不会通过名字调用内部Bean。
    即,Bean的最大缺点:不能被复用。

    4、使用Spring的命名空间 p 装配属性
    命名空间p:另一种Bean属性的装配方式,可避免过多的尖括号;该命名空间需要额外引入声明:

    使用p:

    p:song——设置song属性
    p:instrument-ref——设置instrument属性,后缀-ref表示装配一个引用而不是字面值。

    5、装配集合
    装配,除了value的简单属性、ref的引用属性外,还有集合装配——当Bean的属性值是复数时。
    Spring提供了4种类型的集合配置元素:

    注意 的区别。

    OneManBand.java有一个集合类属性:

    //<start id="onemanband_java" />
    package com.springinaction.springidol;
    import java.util.Collection;
    public class OneManBand implements Performer {
      public OneManBand() {
      }
      public void perform() throws PerformanceException {
        for (Instrument instrument : instruments) {
          instrument.play();
        }
      }
      private Collection<Instrument> instruments;
      public void setInstruments(Collection<Instrument> instruments) {
        this.instruments = instruments; //<co id="co_injectInstrumentCollection"/>
      }
    }
    //<end id="onemanband_java" />

    装配List,Set与之类似:

    注意,该bean调用了其他bean(ref属性)。

    OneManBand.java需要知道乐器的类型:

    package com.springinaction.springidol2;
    //<start id="onemanband_java" />
    import java.util.Map;
    import com.springinaction.springidol.Instrument;
    import com.springinaction.springidol.PerformanceException;
    import com.springinaction.springidol.Performer;
    public class OneManBand implements Performer {
      public OneManBand() {
      }
      public void perform() throws PerformanceException {
        for (String key : instruments.keySet()) {
          System.out.print(key + " : ");
          Instrument instrument = instruments.get(key);
          instrument.play();
        }
      }
      private Map<String, Instrument> instruments;
      public void setInstruments(Map<String, Instrument> instruments) {
        this.instruments = instruments; //<co id="co_injectInstrumentMap"/>
      }
    }
    //<end id="onemanband_java" />

    那么,需要定义Map装配:

    的子元素包含key(key-ref)、value(value-ref)属性,其引用类型为:

    注意,当键和值都不是String类型时,将键值对注入Bean属性的唯一方式就是使用了。

    装配props:key和value都只能是String类型:

    注意,props中的子元素是prop。
    以下是propxxxx的作用汇总:

    6、装配空值NULL

    使用了一个元素来为属性设置null值。

    二、使用表达式装配
    1、Spring3引入Spring表达式语言(SpEL)。
    SpELl是一种强大 、简洁的装配Bean的方式,通过运行期执行的表达式将值装配到Bean的属性或构造器参数中。
    2、特性:
    使用Bean的ID来引用Bean;
    调用方法和访问对象的属性;
    对值进行算术、关系和逻辑运算;
    正则表达式匹配;
    集合操作。
    3、SpEL的基本原理
    (1) 字面值:

    <property name="count" value="#{5}" />

    #{}标记会提示Spring这个标记里的内容是SpEL表达式 ,可以和非SpEL表达式的值混用。

    <property name="message" value="The value is #{5}" /> <!--int 字面值-->
    <property name="frequency" value="#{90.8}" /> <!--float字面值-->
    <property name="sci" value="#{1e4}" /> <!--科学计数法-->
    <property name="name" value="#{'Chuck'}" /> <!--String 字面值-->
    <property name="enable" value="#{false}" /> <!--boolean字面值-->

    (2) 引用Bean、Properties和方法
    SpEL表达式可通过ID引用其他Bean。

    <property name="instrument" value="#{saxophone}" /> <!--ID为saxophone的Bean-->
    <property name="instrument" ref="saxophone" /><!--以上和ref的作用是一样的-->

    (3)在SpEL中筛选集合
    例如有以下类,(省略getter/setter)

    在Spring中配置List集合:

    (使用了Spring的util命名空间util:list)
    那么,SpEL可进行操作集合:

    访问集合成员:

    [] 方式:

    <property name="chosenCity" value="#{cities[2]}" /> <!--从集合中挑出第三个city元素,下标从0开始-->

    取值:

    <property name="chosenCity" value="#{cities['Dallas']}" /> <!--取键值为Dallas的entry-->
    查询集合成员

    查询运算发 (.?[ ]

    <property name="biCities" value="#{cities.?[population gt 100000]}" /> <!--从集合中挑出大城市集合-->
    投影集合

    投影运算符(.![ ]
    从集合的每一个成员中选择特定的属性放入一个新的集合中。

    <property name="cityNames" value="#{cities.![name]}" /> <!--cityNames属性被赋予一个String类型的集合,包含各城市的名字,Chicago、Atlanta等 -->
    <property name="cityNames" value="#{cities.![name+ ', '+state]}" /> <!--cityNames属性被赋予一个String类型的集合,包含各“城市,州“,如"Chicago,IL" -->

    混合运算:

    <property name="cityNames" value="#{cities.?[population gt 10000].![name]}" /> <!--cityNames属性被赋予一个String类型的集合,包含各大城市 -->

    注意:不要被SpEL迷惑,非必要情况下不要使用SpEL。

    源码

    Profile:https://github.com/myitroad/spring-in-action-4/tree/master/Chapter_03/profiles
    条件化Bean:https://github.com/myitroad/spring-in-action-4/tree/master/Chapter_03/conditional
    Bean的作用域:https://github.com/myitroad/spring-in-action-4/tree/master/Chapter_03/scopedbeans
    运行时注入:https://github.com/myitroad/spring-in-action-4/tree/master/Chapter_03/externals

  • 相关阅读:
    【Linux】——sleep无法正常休眠
    【网络——Linux】——IPMI详细介绍【转】
    【Linux】——ctags
    10086 8元套餐
    建置 区域划分
    几何
    孙武
    监察委
    python 内置函数
    Oracle cursor and table
  • 原文地址:https://www.cnblogs.com/myitroad/p/9297778.html
Copyright © 2020-2023  润新知