Spring实战(三)-高级装配

2018-07-18  本文已影响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。

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

例如,在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,我们可以做到如下几点:

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,包括:

例如,如果你使用组件扫描,可以在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提供了两种在运行时求值的方式:

在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拥有很多特性,包括:

常用用法:

  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] }’
上一篇下一篇

猜你喜欢

热点阅读