Spring高级装配

文章目录

  • profile
    • @Profile
    • XML profile
    • 激活Profile
    • * Conditional
      • ConditionContext
      • AnnotatedTypeMetadata
  • 处理自动装配歧义性
    • 标示首选Bean(@Primary)
    • 限定自动装配的bean
      • 面向bean ID的限定符
      • 面向特性的的限定符
      • 自定义限定符
  • bean的作用域
    • @Scope
      • 基本使用
      • 使用web相关作用域
      • XML中声明作用域代理
    • 运行时注入
      • 属性占位符(Property placeholder)
        • @PropertySource和Environment
        • 属性占位符
      • Spring表达式语言(SpEL)
        • 基本内容
        • bean装配时使用SpEL
        • 基础表达式
        • SpEL运算符
        • 计算正则表达式
        • 计算集合

profile

为不同环境,选择最为合适的配置,且无需重新构建

@Profile

指定某个bean属于哪个profile
在将应用部署到每个环境时,一起确保对应的profile处于激活(active)状态

  • 类级别 after version 3.1
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig(){
    return new EmbeddedDatabaseBuilder()
    .setType(EmbeddedDatabaseType.H2)
    .addScript("classpath:scema.sql")
    .addScript("classpath:test-data.sql")
    .build();
}

上述代码告诉Spring这个配置类中的bean只有在dev profile激活时才会创建,否则忽略。

  • 方法级别 after version 3.2
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;

import javax.activation.DataSource;


/**
 * @author yanzy
 * @date 2019/2/20 下午5:03
 * @description
 */
@Configuration
public class DataSourceConfig {
    @Bean
    @Profile("dev")
    public DataSource embeddedDataSource() {
        return (DataSource) new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:scema.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();
    }
}

上述例子中,两个环境中对应的注入数据源的bean放在同一个配置类中,在不同的profile被激活时,会创建对应的bean,该配置类中其他为标记@profile的方法,无论在什么环境下都创建。

XML profile

元素中使用profile,告知Spring框架此时激活的环境,需要创建的bean
Spring高级装配_第1张图片
还可以采用元素嵌套的方案,将多环境配置放到同一个XML文件中

Spring高级装配_第2张图片

激活Profile

Spring根据

  • spring.profiles.active
  • spring.profiles.default
    两个独立属性确定哪个profile处于激活状态;
    若上述两个属性都没设置,则未激活任何profile。

设置方法:

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

一般可以将spring.profiles.default设置为开发环境的profile,并使用DispatcherServlet参数的方式进行设置,如在web应用中,web.xml中可以如下设置:


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


    appServlet
    /

上述方法设置spring.profiles.default,所有开发人员都能从版本控制软件获取应用程序源码,并使用开发环境的设置运行代码,无需额外配置
部署到QA、生产或其他环境时,负责部署到人根据情况使用系统属性环境变量JNDI设置spring.profiles.active即可。

可以同时激活多个profile,以,分隔。

集成测试时,@ActiveProfiles可以用来指定运行测试时要激活的profile。

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("dev")
public class DemoApplicationTests {

	@Test
	public void contextLoads() {

	}

}

* Conditional

用在带有@Bean注解的方法上;给定条件计算结果为true,创建bean,否则不创建。

import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

@Configuration
public class TestConfig {
    @Bean 
    @Conditional(TestCondition.class)
    public Object testConfig(){
        return new Object();
    }
}

class TestCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("test");
    }
}

上述代码中,定义了一个仅在环境变量中存在"test"属性时才会创建的bean。我们可以看出,@Conditional中给定了一个Class,指明了条件TestCondition。

指明条件的类可以是任意实现了Condition接口的类,只需实现matches(ConditionContext context, AnnotatedTypeMetadata metadata)方法即可。

ConditionContext

ConditionContext可以做到一下几点:

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

AnnotatedTypeMetadata

AnnotatedTypeMetadata可以检查带有@Bean注解的方法上还有其他什么注解:

  • isAnnotated()判断带有@Bean的方法是否还有其他注解;
  • 其他的getAnnotationAttributes、getAllAnnotationAttributes方法,可获取其他注解的属性;

@Profile从Spring4开始,基于@Conditional和Condition实现。ProfileCondition作为Condition的实现类,用于做出决策。

处理自动装配歧义性

不仅有一个bean匹配自动装配所需结果

eg:
若同一个接口I的三个不同实现类ImplA、ImplB、ImplC都是用@Component注解,注入为Bean,在使用@Autowired自动装配I时,没有唯一、无歧义的可选值,Spring会抛出NoUniqueBeanDefinitionException异常。

标示首选Bean(@Primary)

使用@Primary来表达首选方案,可与@Component共用在组件扫描的bean上,也可以与@Bean共用在配置类中。
XML中可以在属性中,添加primary配置


⚠️注意:若标示了两个以上首选bean,则标示失效。

限定自动装配的bean

Spring限定符@Qualifier能够在所有可选的bean上进行缩小范围的操作。

面向bean ID的限定符

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

@Bean
@Qualify
public class IceCream implement Dessert{
}

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

其中,Qualifier中设置的属性值(限定符)是想要注入的bean的ID。所有使用@Component注解声明的类都会创建为bean且默认ID为首字母小写的类名

若@Qualifier未给定限定符,默认给一个与bean的ID相同的限定符。

使用默认限定符在bean进行重构,类名发生改变后,会导致@Qualifier中设置的限定符失效。此时,可以选择自定义限定符,将指定的限定符与要注入的bean名称解耦。

面向特性的的限定符

@Bean
@Qualifier("cold")
public class IceCream{
}

自定义限定符

面向特性的限定符比默认的基于bean ID的限定符更好一些,但是当有多个bean都具备相同特性时,依然会有歧义性的问题,我们可能会想到添加多个@Qualifier注解去解决这个问题
⚠️

@Bean
@Qualifier("cold")
@Qualifier("cream")
public class IceCream implement Dessert{
}

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

但这种做法存在一个问题:

java不允许在同一个条目上重复出现相同类型的多个注解

所以上述方式会导致java编译器报错。

我们可以使用创建自定义注解的方式解决这个问题。

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

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

@Bean
@Cold
@Creamy
public class IceCream implement Dessert{
}

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

使用这种方法,相比于使用原始@Qualifier并借助String类型来指定限定符,也更为类型安全。

bean的作用域

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

@Scope

基本使用

上述四种作用域都可以使用@Bean或@Component加@Scope的注解组合来进行声明

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

这里使用了ConfigurableBeanFactory类的SCOPE_PROTOTYPE设置了原型作用域,相比于@Scope(“prototype”)来说,此方法更加安全并不易出错。

在java配置类中声明,可选择和@Bean一起使用。

使用XML来配置的话,可如下配置


使用web相关作用域

web相关的作用域配置,可以使用
WebApplicationContext.SCOPE_SESSIONWebApplicationContext.SCOPE_REQUEST
来声明。

此处存在一个问题,当要向一个单例的bean中注入web相关的bean时,单例bean会在应用上下文加载的时候创建,并试图将web bean注入进来。但web bean是会话/请求作用域的,此时并不存在;
另外,系统中会有多个web bean实例,此时并不想让Spring注入某个固定的web bean实例到单例bean中,我们希望所使用的web bean恰好是当前绘画对应的那一个。

要解决上面问题,可以使用proxyMode属性,如下:

@Bean
@Scope(value="WebApplicationContext.SCOPE_SESSION", proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart(){...}

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

此时,Spring不会将实际的ShoppingCart注入到StoreService中,而是会注入一个到ShoppingCart的代理,它会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给回话作用域内真正的ShoppingCart bean。即委托类实现ShoppingCart接口,并将调用委托给实现bean。
上述方法仅对接口有效果,若ShoppingCart是类而非接口时,要用CGLib来生成基于类的代理。此时,使用ScopedProxyMode.TARGET_CLASS,表明要以生成类扩展方式创建代理。

XML中声明作用域代理




">

使用Spring aop命名空间的元素与proxyMode的属性功能相同。但是其默认使用CGLib创建目标类的代理,我们可以将proxy-target-class属性设置为false,要求它生成基于接口的代理:


要使用上述标签,需要在XML配置中声明Spring的aop命名空间:
xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"

运行时注入

属性占位符(Property placeholder)

@PropertySource和Environment

  • @PropertySource和Environment
    处理外部值最简单的方式:
    指定属性源,通过Spring的Environment来检索属性
@Configuration
@PropertySource("classpath:/com/myapp/app.properties")
public class ExpressiveConfig{
    @Autowired
    Environment env;
    
    @Bean
    public BlankDisc disc(){
        return new BlankDisc(
            env.getProperty("disc.title"),
            env.getProperty("disc.artist")
        );
    }
}

上例中的Environment还包含以下方法:

  • String getProperty(String key, String defaultValue)指定属性不存在时的默认值;
  • T getProperty(String key, Class type)转换获取到的属性类型;
  • T getProperty(String key, Class type, T defaultValue)转换获取到的属性类型并指定默认值;
  • String getRequiredProperty(String key)指定必须定义的属性,若无,抛出;IllegalStateException
  • containsProperty(String key)检查属性是否存在;
  • Class getPropertyAsClass(String key, Class clazz)属性解析为类;
  • String[] getActiveProfiles()返回激活profile名称的数组;
  • String[] getDefaultProfiles()返回默认profile的数组;
  • boolean acceptsProfiles(String… profiles)environment支持给定profile,返回true;

属性占位符

形式: ${...}
使用:

  • 依赖组件扫描和自动装配使用的方式@Value("${...}")
  • XML中使用的方式

为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean,后者可以基于Spring Environment及其属性源来解析占位符,所以3.1后推荐使用。

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

使用XML配置,上述bean Spring context命名空间中的context:propertyplaceholder元素会为你生成。

解析外部属性能够将值的处理推迟到运行时。

Spring表达式语言(SpEL)

基本内容

Spring3引入
在其将值装配到bean属性和构造器参数的过程中,所使用的表达式会在运行时计算得到值。

特性:

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

SpEL表达式需要放到#{...}

#{T(System).currentTimeMills()}

上式最终结果是计算表达式的那一刻当前时间的毫秒数。
T()表达式会将java.lang.System视为java中对应的类型。

  • 表达式可以引用其他bean或其他bean的属性
#{sgtPeppers.artist}
  • 表达式可以通过systemProperties对象引用系统属性
#{systemProperties['disc.title']}

bean装配时使用SpEL

  • 通过组件扫描创建bean
    注入属性和构造器参数时,可以使用@Value
public BlanckDisc(
    @Value("#{systemProperties['disc.title']}") String title
){
    this.title = title;
}

XML配置中,可以将SpEL表达式传入或的value属性中,或作为p-或c-命名空间条目的值。


基础表达式

  • 表示字面值
    浮点
    #{3.14159}
    科学计数法
    #{9.87E4}
    String类型
    #{'hello'}
    Boolean类型
    #{true}

  • 引用bean、属性和方法
    通过ID引用其它bean,可以使用SpEL将一个bean装配到另外一个bean的属性中,此时bean ID作为SpEL的表达式,在下例中,就是sgtPeppers。
    #{sgtPeppers}
    引用sgtPeppers的artist属性
    #{sgtPeppers.artist}
    调用方法
    #{sgtPeppers.selectArtist()}
    对于被调用方法的返回值,同样可以调用它的方法
    #{artistSelector.selectArtist().toUpperCase()}
    防止空指针异常,使用类型安全的运算符
    #{artistSelector.selectArtist()?.toUpperCase()}

  • 表达式中使用类型
    依赖T()这个关键运算符来访问类作用域的方法和常量,如,表达Java的Math类
    #{T(java.lang.Math)}
    运算结果会是一个Class对象,其真正的价值在于其可以访问目标类型的静态方法和常量
    #{T(java.lang.Math).PI}
    #{T(java.lang.Math).random()}

SpEL运算符

运算符类型 运算符
算数运算 +、-、*、/、%、^
比较运算 <、>、==、<=、>=、lt、gt、eq、le、ge
逻辑运算 and、 or、 not、 |
条件运算 ?:(ternary)、?:(Elvis)

#{2 * T(java.lang.Math).PI * circle.radius}
String类型+操作
#{disc.title + 'by' + disc.artist}
比较操作符(符号形式和文本形式一样)
#{counter.total == 100}#{counter.total eq 100}
三元运算
#{scoreboard.score > 1000 ? "Winner" : "Loser"}
Elvis为判断结果是否为null的三元计算,命名灵感来自?像猫王的头发???
#{disc.title?:'Rattle and Hum'}

计算正则表达式

通过matches运算符支持表达式中的模式匹配。
左边参数为String类型的文本,右边参数为正则表达式,返回Boolean类型的值。
#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
上式为判断一个字符串是否包含有效的邮件地址。

计算集合

  • 引用列表中的一个元素
    #{jukebox.songs[4].title}
    集合来源于id为jukebox的bean

丰富版
#{jukebox.songs[T(java.lang.Math).random()*jukebox.songs.size()].title}
[]还可以从String中获取一个字符
#{'This is a test'[3]}

  • 查询运算符

.?[]
获取jukebox中artist属性为Aerosmith的所有歌曲
#{jukebox.songs.?[artist eq 'Aerosmith']}

.^[]
在集合中查询第一个匹配项
#{jukebox.songs.^[artiest eq 'Aerosmith']}

.$[]
在集合中查询最后一个匹配项
#{jukebox.songs.$[artiest eq 'Aerosmith']}

.![]
投影运算,从集合的每个成员中选择特定的属性放到另一个集合中
#{jukebox.songs.![title]}

你可能感兴趣的:(框架实践,Spring)