【Spring学习笔记】8:高级装配(profile,条件bean,自动装配歧义性,bean的scope,运行时注入)

版权声明:本文为博主原创学习笔记,如需转载请注明来源。 https://blog.csdn.net/SHU15121856/article/details/81812254

今天读了《Spring实战》第三章,总结一下。

环境与profile

软件开发中,应用程序在不同环境之间迁移可能比较麻烦。不同系统中数据库配置、加密算法等可能不同,与外部系统的集成也完全不一样。

如在开发环境中可能会用嵌入式数据库并预先加载数据,在生产环境中可能使用JNDI从容器中获得数据源,在QA环境中可能要使用各种不同的数据源配置。

同一个bean在不同环境中内容可能完全不同。老旧的方式是在单独的配置类或XML中配置每个bean,然后环境迁移时用Maven的profiles确定将哪个配置编译到应用中,这种方式是在构建时决策。

配置profile bean

Spring的bean profile可以在运行时确定使用哪个bean,这样就使同一个部署单元(如war包)能适用于所有环境,不需要重新构建。将这些不同的bean定义到不同的profile中,部署到环境时确保相应的profile处于激活状态即可。

在JavaConfig中配置

注解@Profile可放在配置类上,如:

@Configuration
@Profile("dev")
public class DevelopmentProfileConfig {
    //各种@Bean...
}

表示在dev这个profile激活时该类的所有bean才会创建。常用"dev"表示开发环境,用"prod"表示生产环境。

注解@Profile也可放在@Bean方法上,这样可以在一个配置类中配置多个不同profile的bean。

在XML中配置

通过<beans>元素的profile属性,如profile="dev",来在XML中配置bean的profile。

当该属性配置在根<beans>时,则整个文件中的bean都被配置成了这个profile;另外<beans>可以有子<beans>,通过在子<beans>上配置,可以配置里面的一组bean在这个profile中。

激活profile

判断激活要看两个属性
  • spring.profile.active
  • spring.profile.default

如果有active属性的话,它的值就确定那些profile被激活;如果没有active的话,就由default决定哪些profile被激活。

没有设置profile的bean始终会被创建,设置了profile的bean仅当其profile被激活时才被创建。

设置这两个属性

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

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



书上给出了第一和第二种设置方法,在 web.xml中添加:

<context-param>
    <param-name>spring.profile.default</param-name>
    <param-value>dev</param-value>
</context-param>

web.xml中DispatcherServlet的<servlet>标签内添加:

<init-param>
    <param-name>spring.profile.default</param-name>
    <param-value>dev</param-value>
</init-param>

如果要设置多个profile只要用逗号隔开。



书上给出了最后一种的设置方法,在 集成测试类上添加 @ActiveProfiles注解:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {
    //...
}

集成测试时通常要激活的是开发环境的profile。

条件化的bean

@Conditional注解

仅使用profile对bean分组有时会不够用,当需要更加复杂的条件时,就需要设置条件化的bean,可以在@Bean方法上添加@Conditional注解:

@Bean
@Conditional(UserExistsCondition.class)
public BookService bkSrvc(Book bk) {
    BookService bookService = new BookServiceImp(bk);
    return bookService;
}

该注解的参数类实现了Condition接口:

public class UserExistsCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        //获取环境
        Environment env=conditionContext.getEnvironment();
        //判断"user"属性是否包含在环境中,返回true或false
        return env.containsProperty("user");
    }
}

实现了接口中的matches()方法,该方法返回true时被@Conditional注解的bean就会被创建,否则就会被忽略。

matches()方法的两个参数

matches()方法会传入两个接口的实现类作为参数,里面定义了很多方法用来获取相关信息。

第一个参数ConditionContext中的方法主要用来获取资源、类、环境的相关信息,第二个参数AnnotatedTypeMetadata中的方法用来获取@Bean方法还存在什么注解。



@Profile注解本身就使用了 @Conditional注解,使用 @Conditional注解可以实现各种各样的条件,以实现复杂逻辑下bean的载入和忽略。

处理自动装配的歧义性

自动装配时,接口有多个实现bean时会出现歧义,使得自动装配不能进行。实际上出现歧义的场景很罕见,多数情况下一个接口只有一个实现bean,不会出现歧义。

两种方法,可以将冲突的bean中的某一个设为首选的,或者使用限定符将可选bean的范围缩小到一个。

标注首选bean

可以将@Primary注解加在@Component组件类上、或者加在JavaConfig的@Bean方法上,使之成为首选bean。

在XML中,给<bean>元素添加属性primary="true"即指定其为首选bean。

限定自动装配的bean

使用@Qualifier注解,配合@Autowired或者@Inject就可以限定哪些类可以被装配。以前一直以为@Qualifier注解里面就是指定bean的id,实际上@Qualifier注解里面指定的是限定符,而同时bean本身都会带有限定符,可以用@Qualifier注解显式指明,限定符相同就允许该bean在这里自动装配。当不为bean指明限定符时,默认的限定符与bean的id相同

自定义的限定符注解

限定符的命名,应该使用描述bean的部分特征的短语。一个bean可能有多个特征才能唯一标识,这时候就需要多个限定符来唯一确定它,但是没办法使用多个@Qualifier注解

在Java8之前,就是不允许在一个条目上出现多个相同类型的注解。在Java8中当注解本身带有@Repeatable时可以重复使用在同一条目上,但@Qualifier注解不带有这个注解,所以不论如何目前@Qualifier注解不能重复使用在同一条目上。

为解决这个问题,可以自定义注解,然后给自定义的注解加上@Qualifier注解,这时候限定符就是这个自定义的注解,不但解决了这个问题,还能避免在@Qualifier注解里面直接写限定符字符串的类型不安全。如:

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier//不需要在@Qualifier注解里添加属性了,这个自定义注解本身就会成为一个限定符
public @interface Cold {
}

总结这章的自定义注解

条件化注解

可以创建自定义注解,添加@Conditional注解形成特定条件的注解,如@Profile注解就是基于它实现的。

限定符注解

可以创建自定义注解,添加@Qualifier注解形成自定义的限定符,类型更安全且可以多限定符修饰。

bean的scope

Spring的bean默认是单例的,但bean不一定是重用安全的,bean对象可能会在使用时发生变化被污染,重用可能会出现问题。

Spring的bean有以下几种作用域:

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

设置bean的scope

可以在@Component标注的类或者@Bean标注的bean方法上添加@Scope注解,如:

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Book book() {
    return new Book();
}

在XML中,使用<bean>元素的scope属性如scope="prototype"来设置作用域。

使用会话和请求作用域

在Web应用中常用,如购物车应使用会话作用域,它和特定的用户关联。会话作用域的设置如:

@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.INTERFACES)
public ShopCar shopCar() {
    return new ShopCar();
}

会话作用域的bean,对于某个会话而言是单例的,不同会话之间却相互独立。Spring不会将会话作用域的bean直接注入到使用它的bean中,而是注入一个代理,当使用者调用代理bean的方法时,代理会对其进行懒解析并将调用委托给会话作用域真正的bean

注意上面的proxyMode属性,值为ScopedProxyMode.INTERFACES表示这个代理要实现bean的接口,并将调用委托给bean。当bean只是一个具体类时(没有返回接口),值应该为ScopedProxyMode.TARGET_CLASS,表示使用CGLib生成基于类的代理。

在XML中生成scope代理

基于类的代理

相当于前面的ScopedProxyMode.TARGET_CLASS

<bean id="shopCar" class="org.sb.ShopCar" scope="session">
    <aop:scoped-proxy/>
</bean>
基于接口的代理

相当于前面的ScopedProxyMode.INTERFACES,这里要显式关闭”基于类”:

<bean id="shopCar" class="org.sb.ShopCar" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

运行时注入

不论是用JavaConfig还是用XML,之前学的都是硬编码的注入。很多时候会希望避免硬编码,使之在运行时再确定,Spring提供几种运行时求值的方式:

  • 使用Environment
  • 属性占位符
  • Spring表达式语言(SpEL)

格式分别是:

  • env.getXXX(“key”,…)
  • “${…}”
  • “#{…}”

使用Environment

Environment经常用来处理外部值,整个流程就两部:

  • [1]声明属性源
  • [2]通过Spring的Environment来检索属性

例如:

@Configuration
@PropertySource("classpath:dev/lzh.properties")//声明属性源
public class LzhConfig {

    @Autowired
    Environment env;//自动注入,这个的bean也不用写

    @Bean
    public Student student() {
        //从Environment中检索属性值
        return new Student(env.getProperty("lzh.age", Integer.class), env.getProperty("lzh.name"));
    }

}

点进getProperty可以看见一些常用的方法:

//判断在环境中是否存在某个key
boolean containsProperty(String var1);
//获取字面值,如果找不到相应的key则返回null
String getProperty(String var1);
//获取字面值,如果找不到相应的key则返回var2
String getProperty(String var1, String var2);
//获取字面值,并根据第二个参数(基本类型的包装类class)转换成基本类型值,找不到key返回null
<T> T getProperty(String var1, Class<T> var2);
//获取字面值,并根据第二个参数转换成基本类型值,找不到key返回var3
<T> T getProperty(String var1, Class<T> var2, T var3);

使用属性占位符

为了使用占位符,必须配置一个PropertySourcesPlaceholderConfigurer,它能够基于Environment及其属性源解析占位符。

配置

如果是在JavaConfig中,可以手动添加这个bean:

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

属性源可以在配置类上使用@PropertySource导入。



如果是在XML中,可以直接添加这个bean:

<context:property-placeholder/>

属性源可以直接指出:

<context:property-placeholder location="classpath:dev/lzh.properties"/>
使用

在XML中直接代替使用那些字面量的地方:

<bean class="org.lzh.model.Book" c:_0="${book.id}" c:_1="${book.name}"/>

@Value注解内配合占位符注入字面量,如在构造器参数上添加:

public Book(@Value("${book.id}") String title, @Value("${book.name}") int id) {
    this.title = title;
    this.id = id;
}

使用SpEL

Spring表达式语言——SpEL具有很多强大的功能:

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

书上讲的很详细,这个东西很强大,但是要用好还是蛮复杂的,而且SpEL毕竟只是String类型的值,测试起来很困难。所以尽可能还是让表达式简洁一些,不要用太复杂的SpEL。

这里只记录一下实现和上面的属性占位符例子类似的功能,使用systemProperties对象可以引用系统属性

public Book(@Value("#{systemProperties['book.id']}") String title, @Value("#{systemProperties['book.name']}") int id) {
    this.title = title;
    this.id = id;
}

XML中的使用:

<bean class="org.lzh.model.Book" c:_0="#{systemProperties['book.id']}" c:_1="#{systemProperties['book.name']}"/>

书上说的”系统属性”指的是什么,我暂时还不清楚,好像不是从那个properties文件里读了(那个文件里没有感知到这里的使用,但是却可以感知到占位符)。

SpEL的功能远比属性占位符要强,而且IDEA能够感知SpEL,这样开发起来就舒服很多。关于SpEL的具体使用,有空再单独好好学一下。

猜你喜欢

转载自blog.csdn.net/SHU15121856/article/details/81812254
今日推荐