美文网首页
Spring实战(三)-高级装配

Spring实战(三)-高级装配

作者: 阳光的技术小栈 | 来源:发表于2018-07-18 10:16 被阅读25次

    本文基于《Spring实战(第4版)》所写。

    环境与profile

    数据源的有三种连接配种,分别是

    // 通过EmbeddedDatabaseBuilder会搭建一个嵌入式的Hypersonic的数据库
      @Bean(destroyMethod = "shutdown")
      @Profile("dev")
      public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:test-data.sql")
            .build();
      }
    
    // 通过JNDI获取DataSource能够让容器决定该如何创建这个DataSource
      @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();
      }
    
    // 还可以配置为Commons DBCP连接池,BasicDataSource可替换为阿里的DruidDataSource连接池
      @Bean(destroyMethod = "close")
      @Profile("qa")
      public DataSource datasource(){
        BasicDataSource datasource = new BasicDataSource();
        datasource.setUrl("jdbc:h2:tcp://dbserver/~/test");
        datasource.setDriverClassName("org.h2.Driver");
        datasource.setUsername("sa");
        datasource.setPassword("password");
        datasource.setInitialSize(20);
        datasource.setMaxActive(30);
    
        return dataSource;
      }
    

    Spring为环境相关的bean所提供的解决方案不是在构建的时候做出决定,而是等待运行时再来确定。Spring引入了bean的profile的功能,在每个数据库连接配置的bean上添加@Profile,指定这个bean属于哪一个profile。
    Spring3.1需要将@Profile指定在配置类上,Spring3.2就可以指定在方法上了。

    我们也可以在XML中通过<bean>元素的profile属性指定。例如:

    <?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>
    

    下一步就是激活某个profile
    Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active属性的话,那么它的值就会用来确定哪个profile是激活的,但如果没有设置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。如果spring.profiles.active和spring.profiles.default均没有设置的话,那就没有激活的profile因此只会创建那些没有定义在profile中的bean。

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

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

    例如,在web应用中,设置spring.profiles.default的web.xml文件会如下所示:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app version="2.5"
             xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
      <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/root-context.xml</param-value>
      </context-param>
      <!--为上下文设置默认的profile-->
      <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
      </context-param>
    
      <listener>
        <listener-class>
          org.springframework.web.context.ContextLoaderListener
        </listener-class>
      </listener>
      
      <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>
          org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
          <!--为Servlet设置默认的profile-->
          <param-name>spring.profiles.default</param-name>
          <param-value>dev</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
    
      <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
      </servlet-mapping>
    </web-app>
    

    条件化的bean

    Spring4实现了条件化配置,需要引入@Conditional(可以用到带有@bean注解的方法上)注解。如果给定条件为true,则创建这个bean,反之,不创建。

    例如:

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Conditional;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class MagicConfig {
    
      @Bean
      @Conditional(MagicExistsCondition.class)  // 条件化创建bean
      public MagicBean magicBean() {
        return new MagicBean();
      }
      
    }
    
    

    @Conditional中给定了一个Class,它指明了条件——本例中是MagicExistsCondition。@Conditional将会通过Condition接口进行条件对比:

    public interface Condition {
         boolean matches(ConditionContext ctxt AnnotatedTypeMetadata metadata);
    }
    

    接下来是MagicExistsCondition的实现类:

    import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.env.Environment;
    import org.springframework.core.type.AnnotatedTypeMetadata;
    
    public class MagicExistsCondition implements Condition {
    
      @Override
      public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
    // 根据环境中是否存在magic属性来决策是否创建MagicBean
        return env.containsProperty("magic");
      }
    }
    

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

    public interface ConditionContext {
        BeanDefinitionRegistry getRegistry();
        ConfigurableListableBeanFactory getBeanFactory();
        Environment getEnvironment();
        ResourceLoader getResourceLoader();
        ClassLoader getClassLoader();
    }
    

    ConditionContext实现的考量因素可能会更多,通过ConditionContext,我们可以做到如下几点:

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

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

    public interface AnnotatedTypeMeta {
        boolean isAnnotated(String annotationType);
        Map<String, Object> getAnnotationAttributes(String annotationType);
        Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString);
        MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType);
        MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString);
    }
    

    借助isAnnotated()方法,能够判断带有@Bean注解的方法是不是还有其他特定的注解。

    处理自动装配的歧义性

    当自动装配bean时,遇到多个实现类的情况下,就出现了歧义,例如:

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

    Dessert是一个接口,并且有三个类实现了这个接口,如下所示:

    @Component
    public class Cake implements Dessert { ... }
    
    @Component
    public class Cookies implements Dessert { ... }
    
    @Component
    public class IceCream implements Dessert { ... }
    

    三个实现均使用了@Component,在组件扫描时,能够创建它们的bean。但Spring试图自动装配setDessert()中的Dessert参数是,它并没有唯一、无歧义的可选值,Spring无法做出选择,则会抛出NoUniqueBeanDefinitionException的异常。

    两种解决办法:

    1. 标示首选的bean
      如下所示:
    @Component
    @Primary
    public class IceCream implements Dessert { ... }
    

    或者,如果通过JavaConfig配置,如下:

    @Bean
    @Primary
    public Dessert iceCream() {
        return new IceCream();
    }
    

    或者,使用XML配置bean的话,如下:

    <bean id="iceCream" 
               class="com.desserteater.IceCream" 
               primary="true" />
    

    需要注意的是:不能标示两个或更多的首选bean,这样会引来新的歧义。

    1. 限定自动装配的bean

    如下所示:

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

    如果不想用默认的bean的名称,也可以创建自定义的限定符

    @Component
    @Qualifier("cold")
    public class IceCream implements Dessert { ... }
    
    @Autowired
    @Qualifier("cold")
    public void setDessert(Dessert dessert) {
        this.dessert = dessert;
    }
    

    或者使用JavaConfig配置

    @Bean
    @Qualifier("cold")
    public Dessert iceCream() {
        return new IceCream();
    }
    

    如果出现多个Qualifier,尝试为bean也标示多个不同的Qualifier来表明要注入的bean。

    @Component
    @Qualifier("cold")
    @Qualifier("creamy")
    public class IceCream implements Dessert { ... }
    
    @Component
    @Qualifier("cold")
    @Qualifier("fruity")
    public class Popsicle implements Dessert { ... }
    
    @Autowired
    @Qualifier("cold")
    @Qualifier("creamy")
    public void setDessert(Dessert dessert) {
        this.dessert = dessert;
    }
    

    但有个问题,Java不允许在同一个条目上重复出现相同类型的注解,编译器会提示错误。

    解决办法是我们可以自定义注解:

    @Targe({ElementType.CONSTRUCTOR, ElementType.FIELD,
                   ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Cold { }
    
    @Targe({ElementType.CONSTRUCTOR, ElementType.FIELD,
                   ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Creamy { }
    
    @Targe({ElementType.CONSTRUCTOR, ElementType.FIELD,
                   ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Fruity { }
    

    重新标注IceCream

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

    注入setDessert() 方法

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

    Bean的作用域

    默认情况下,Spring应用上下文所有bean都是作为以单例的形式创建的。
    Spring定义了多种作用域,可以基于这些作用域创建bean,包括:

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

    例如,如果你使用组件扫描,可以在bean的类上使用@Scope注解,将其声明为原型bean:

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

    或者在javaConfig上声明:

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

    或者在XML上声明:

    <bean id="notepad" 
               class="com.myapp.Notepad"
               scope="prototype" />
    

    在web应用中,如果能够实例化在会话和请求范围内共享bean,那将很有价值。例如:电子商务的购物车,会话作用域最为适合。

    @Component
    @Scope(value=WebApplicationContext.SCOPE_SESSION,
                   proxyMode=ScopedProxyMode.TARGET_CLASS)
    public class ShoppingCart  { ... }
    

    注入一个服务类

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

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

    另外,系统中将会有多个ShoppingCart实例:每个用户一个。我们并不想让Spring注入某个固定的ShoppingCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所用的ShoppingCart实例恰好是当前会话所对应的那一个。

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

    如果ShoppingCart是接口而不是类的话,就用ScopedProxyMode.TARGET_INTERFACES(用JDK的代理)。如果是类而不是接口,就必须使用CGLib来生成基于类的代理,所以要用ScopedProxyMode.TARGET_CLASS。

    请求的作用域原理与会话作用域原理一样。

    作用域代理能够延迟注入请求和会话作用域的bean

    也可用XML配置

    <bean id="cart" 
               class="com.myapp.ShoppingCart" 
               scope="session" >
      <aop:scoped-proxy />
    </bean>
    

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

    <bean id="cart" 
               class="com.myapp.ShoppingCart" 
               scope="session" >
      <aop:scoped-proxy proxy-targe-class="false"/>
    </bean>
    

    运行时植注入-Spring表达式语言

    我们之前在javaConfig配置中,配置了BlankDisc:

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

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

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

    在Spring中,最简单的方式就是声明属性源并通过Spring的Environment来检索属性。

    package com.springinaction;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.core.env.Environment;
    
    @Configuration
    @ComponentScan("com.springinaction")
    @PropertySource("app.properties")
    public class AppConfig {
        
        @Autowired
        Environment environment;
        
        @Bean
        public BlankDisc disc(){
            return new BlankDisc(
                       environment.getProperty("disc.title"),
                       environment.getProperty("disc.artist"));
        }
    
    }
    

    在本例中,@PropertySource引用了类路径中一个名为app.properties的文件,如下所示:

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

    Environment中getProperty有四个重载方法:

         String getProperty(String key);
         String getProperty(String key, String defaultValue);
         T getProperty(String key, Class<T> type);
         T getProperty(String key, Class<T> type, T defaultValue);
    

    第二个方法与第一个的差别就是有了默认值。
    第三、四个方法不会将所有值视为String,可以转换为别的类型,如

    int connectionCount = env.getProperty("db.connection.count", Integer.class, 30);
    

    其他方法还有

         // 如果key没有,则抛出IllegalStateException异常
         String getRequiredProperty(String key); 
        // 检查key的value是否存在
         boolean containsProperty(String key)
        // 将属性解析为类
        Class<T>  getPropertyAsClass(String key, Class<T> type);
        // 返回激活profile名称的数组
        String[] getActiveProfiles();
        // 返回默认profile名称的数组
        String[] getDefaultProfiles()
        // 如果environment支持给定profile的话,就返回true
        boolean acceptsProfiles(String... profiles)
    

    我们还可以用属性占位符来注入,占位符的形式为使用“${ ... }”包装的属性名称。

    <bean id="sgtPeppers" 
               class="soundsystem.BlankDisc"
               c:_title="${disc.title}"
               c:_artist="${disc.artist}" />
    

    如果我们依赖组件扫描和自动装配来创建初始化的话

    public BlankDisc (
              @Value("disc.title") String title,
              @Value("disc.artist") String artist) {
          this.title = title;
          this.artist = artist;
    }
    

    为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。推荐后者。
    如果在javaConfig配置文件中声明:

        @Bean
        public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
            return new PropertySourcesPlaceholderConfigurer();
        }
    

    如果在XML配置文件中声明:

     <context: property-placeholder />
    

    下面我们来看Spring表达式语言进行装配
    SpEl表达式会在运行时计算得到值。
    SpEl拥有很多特性,包括:

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

    常用用法:

    1. SpEL表达式要放到“# { ... }”, 如: #{1}
    2. ‘# {T(System).currentTimeMillis()}’ ,它的最终结果是计算表达式的那一刻当前时间的毫秒数。T () 表达式会将java.lang.System视为Java中对应的类型,因此可以调用其static修饰的currentTimeMillis()方法。
    3. SpEL表达式可以引用其他的bean或其他bean的属性。
      例如,引用sgtPeppers的bean
      ‘# { sgtPeppers }’
      例如,如下的表达式会计算得到ID为sgtPeppers的bean的artist属性:
      ‘# { sgtPeppers.artist }’
    4. 还可以通过systemProperties对象引用系统属性:
      ‘# { systemProperties['disc.title'] }’
    5. 表示字面值:
      ‘# { 3.1415926 } ’
      ‘# { 9.87E4 } ’
      ‘# { 'Hello' } ’
      ‘# { false }’
    6. 引用其他的bean的方法
      ‘# { artistSelector.selectArtist () }’
      为了防止方法值为null,抛出异常,可以使用“?.”
      ‘# { artistSelector.selectArtist ()?.toUpperCase() }’
      不是null,正常返回;如果是null,不执行后面的方法,直接返回null
    7. 如果要在SpEL中访问类作用域的方法和常量的话,要依赖T() 这个关键的运算符。
      ‘# { T(java.lang.Math).PI }’
      ‘# { T(java.lang.Math).random() }’
    8. 还可以将运算符用在表达式上,如:
      ‘# { 2 * T(java.lang.Math).PI * circle.radius }’
      ‘# { disc.title + ' by ' + disc.artist }’
    9. 比较数字相等的写法
      ‘# { counter.total == 100 }’
      ‘# { counter.total eq 100 }’
    10. 三元运算符
      ‘# { scoreboard.score > 1000 ? "Winner!" : "Loser" }’
      ‘# { disc.title ?: 'Rattle and Hum' } ’ // 如果disc.title的值为空,返回'Rattle and Hum'
    11. 支持正则表达式
      ‘# { admin.email matches '[a-zA-Z0-9.%+-]+@[a-zA-Z0-9.]+\.com' }’
    12. 支持与集合和数组相关的表达式
      ‘# { jukebox.songs[4].title }’
      ‘# { jukebox.songs[T(java.lang.Math).random() * jukebox.songs.size()].title }’
      ‘# { 'This is a test' [3] }’ // 引用第4个字符 - “s”
    13. 支持查询运算符
      例如你希望得到jukebox中artist属性为Aerosmith的所有歌曲:
      ‘# { jukebox.songs.?[artist eq 'Aerosmith'] }’
      查找列表中第一个artist属性为Aerosmith的歌曲:
      ‘# { jukebox.songs.^[artist eq 'Aerosmith'] }’
      查找列表中最后一个artist属性为Aerosmith的歌曲:
      ‘# { jukebox.songs.$[artist eq 'Aerosmith'] }’
    14. 支持投影运算符
      假设我们不想要歌曲对象的集合,而是所有歌曲名称的集合。如下表达式会将title属性投影到一个新的String类型的集合中:
      ‘# { jukebox.songs.![title]}’
      获取Aerosmith所有歌曲的title
      ‘# { jukebox.songs.?[artist eq 'Aerosmith'].![title] }’

    相关文章

      网友评论

          本文标题:Spring实战(三)-高级装配

          本文链接:https://www.haomeiwen.com/subject/iavgzxtx.html