Spring之高级装配

一、环境与Profile(spring 4.0+)

  配置profile bean,在实际生产中往往需要几套环境,如dev、test和prod等,它们的配置(如数据源)往往不同,这就需要在不同的环境生成不同的bean,spring中用@Profile注解以实现该功能。

  1、@Profile("profile"):指定某个或某些bean属于指定的profile下,当该profile激活(Active)时,属于这个profile下的bean会被创建。@Profile可用在class级别和@Bean标注的方法级别上,在class级别上的profile激活的话,则该类中所有的bean均会被创建,同样在方法级别上只有那一个bean会被创建,相当于进行细粒度化管理。注意哪些不指定profile的bean无论何时都会创建,@Profile可传入多个profile,它可以在不同配置下创建同一个bean,比如@Profile({"dev", "test"})表示在dev或test环境下均创建同一个bean,如启用同一个数据源。

//示例代码
@Configuration
public class Config {
    @Profile("dev")
    public String devStr(){
        return "dev";
    }
    @Profile("prod")
    public String prodStr(){
        return "prod";
    }
}

  2、XML配置profile,在xml中可以在<beans>的profile属性上配置profile bean。示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <beans profile="dev">
        <bean id="xxx-dev" class="com.xxx.xxx">
        </bean>
    </beans>
    <beans profile="prod">
       <bean id="xxx-prod" class="com.xxx.xxx">
       </bean>
    </beans>
</beans>

  3、激活profile:主要依赖spring.profiles.active和spring.profiles.default,前者优先级高,active未设置就启用default,如果两者都没设置就只创建没有在任何profile中的bean。常用激活方式如下:

  1. 作为DispatcherServlet的初始化参数,web.xml节选:
    <!--省略其他声明和配置-->
    <web-app>
        <context-param>
            <param-name>spring.profiles.default</param-name>
            <param-value>dev</param-value>
        </context-param>
        <servlet>
            <init-param>
                <param-name>spring.profile.active</param-name>
                <param-value>prod</param-value>
            </init-param>
        </servlet>
    </web-app>
  2. 作为web应用的上下文参数;(未尝试)
  3. 作为JNDI条目;(未尝试)
  4. 作为环境变量;(未尝试)
  5. 作为JVM的系统属性,添加 -Dspring.profiles.active=test 参数
  6. 在测试类中,使用@ActiveProfile注解设置:
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = Config.class)
    @ActiveProfiles({"prod", "dev"})
    public class ProfileTest {
        //Other code  
    }

    如上面Java代码所示,也可以同时激活多个profile,一般是不同维度的多个profile。

二、条件化的bean

  在Java中经常会遇到if满足某个条件,则创建某个对象,在spring中同样存在此问题,即满足某些条件才创建一些bean。在Spring 4中引入@Conditional注解进行条件化配置bean,条件满足则创建bean,不满足则忽略。

  1、@Conditional(ConditionClass.class):它可以用在配置类上和@Bean注解的方法上,与@Profile类似,只是作用范围不同。它的参数是一个实现了Condition接口的类,这个类只有一个返回值为boolean类型的matches方法,根据该方法的返回值确定是否创建bean,示例如下:

//实现了Condition接口的任意类
public class MagicExistsCondition implements Condition {
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment evn = conditionContext.getEnvironment();
        return evn.containsProperty("magic");
    }
}

//@Conditional注解的使用
@Configuration
@PropertySource("classpath:app.properties")//向环境中增加magic属性
public class MagicConfig {
    @Autowired
    Environment evn;

    @Bean
    @Conditional(MagicExistsCondition.class)
    public String getMagicBean() {
        return evn.getProperty("magic", "can not get property of magic");
    }
}

  可以看到matches方法有两个参数,ConditionContext 和 AnnotatedTypeMetadata,这两个参数为我们做决策提供了大量的信息,具体含义不在此处详解。其实@Profile也是利用@Conditional去决定创建那些bean,其源码如下:

//@Profile的定义
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})//这里利用@Conditional确定是否创建相应的bean
public @interface Profile {
    String[] value();
}

//ProfileCondition类的实现
class ProfileCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles((String[]) value)) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
}
//这个类实际上就是扫描那些带有@Profile的方法和类,然后再遍历@Profile中的参数,
//如果该参数在environment中存在,就返回true创建bean,否则返回false忽略。

 

三、处理自动装配有歧义的bean

  如果我们同时出现同一个类型(一个接口的多种实现,或一个抽象类的多种继承)的多个bean,这时自动装配将无法知道该装配谁合适,因此Spring提供了@Primary和@Qualifier的注解帮我们解决这个问题。

  1、@Primary:这个注解表示多个同类型的bean中,带该注解的bean是最主要的,装配时优先选择,它同样可以用于class和@Bean注解的method上面,只是作用范围不一样。正如你所想的那样,如果出现多个同一类型的bean且都带有@Primary注解(如@Primary加在class上,而class里面能创建多个同一类型的bean),此时spring依然无法做出选择,这是另外一个注解@Qualifier出现了。

  2、@Qualifier("qualifier-id"):这个注解可以用在自动装配(@Autowire)处,也可用在创建bean(@Bean&@Component...)的地方,和@Autowire连用表示选择限定符为qualifier-id的bean进行装配,和声明为bean的注解(@Bean&@Component...)一起使用表示为这个bean设置限定符。注意此处的“限定符”不是bean-id,如果没有显示指定限定符,默认和bean-id一致,在日常使用中经常在@Autowire处的@Qualifier中直接写bean-id就可以,如果连bean-id都没指定的话,bean-id默认就是首字母小写的类名或方法名。

  3、考虑最极端的一种情况,就连一个@Qualifier都无法唯一确定一个bean,这时可能会想到用多个@Qualifier限定,然而Java8之前不允许同一注解在一处出现多次,Java8只允许带有@Repeatable的注解出现多次,而@Qualifier没有@Repeatable修饰。这是另外一个解决办法就是封装@Qualifier进行自定义注解,它可以根据我们具体业务无限叠加规则,示例如下:

//自定义限定注解
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MySql {
}

//使用限定注解
@Configuration
public class Config {
    @Bean(name = "prod-db")
    @Profile("prod")
    @Qualifier("prod")//bean的限定符,在多个同类型的bean存在时Qualifier通过指定限定符区分,默认与bean id一致
    @Mysql//自定义注解限定bean
    public String prodStr(){
        return "prod-mysql";
    }
}

 

  4、理所当然对应的XML配置:最烦XML

<bean id="xxx" class="com.xxx.xxx" primary="true">
    <!--Some properties configuration-->
</bean>
<bean id="xxx" class="com.xxx.xxx">
    <qualifier type="com.xxx.xxx" value="qualifier-id"/>
</bean>

四、Bean的作用域

  Spring默认的bean都是单例模式,无论注入多少次,上下文中始终只存在一个实例对象。当某些bean与状态有关时,单例就不合理了,Java对象会被污染。这时Spring提供了一个作用域概念@Scope设置bean的作用域,具体取下:

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

  前两种很好理解,后两种用于web应用中,相应的还要配置代理模式(proxyMode),后两种不详细说明,后面再记录。在JavaConfig中使用@Scope注解实现:

@Configuration
public class Config {
    @Bean(name = "dev-db")
    @Profile("dev")
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)//单例
    public String devStr(){
        return "dev";
    }
}

  同样XML实现:

<bean id="xxx" class="com.xxx.xxx" scope="prototype">
    <!--Some properties configuration-->
</bean>
<bean id="xxx" class="com.xxx.xxx" scope="session">
    <aop:scoped-proxy/><!--设置代理模式-->
    <!--Some properties configuration-->
</bean>

五、运行时注入

  很多时候,我们需要在配置文件中为某些对象的属性指定值,比如数据源、用户名和密码等。这时候就会用到Spring的运行时注入技术,Spring提供的运行时注入方式包括属性占位符(Property placeholder)和Spring表达式语言(SpEL)。

  1、属性占位符:属性占位符即形如"${}",大括号中填写在配置文件(xxx.properties or anything else)中定义的变量名。通常使用时在@Value中添加占位符,如@Value("${db.url}"),然后在配置文件中定义好相关变量,最后在配置类中用@PropertySource("xxx")注解引入配置文件即可。示例代码如下:

@Configuration
@PropertySource("classpath:app.properties")
public class MagicConfig {
    @Autowired
    Environment evn;//通过environment进行运行时注入

    @Value("${person.name}") 
    String name;

    @Bean
    @Qualifier("jack")
    public String jack(@Value("${person.age}") Integer age) {
        return name + " is " + age + " years old, and his weight is " + evn.getProperty("person.weight", Float.class, 0.0F) + " kg.";
    }
}

  正如你所看到的那样,上面代码还使用了Spring的Environment来获取属性文件中定义的值,这是因为使用@PropertySource时将属性文件加载到了Spring Environment中,因此后来就可以使用Environment bean来获取我们定义的属性值。Spring Environment有着强大的作用,具体用法不在此处详解。使用占位符的XML配置如下:

<context:property-placeholder/><!--生成PropertySourcesPlaceHolderConfigurer bean-->
<bean id="xxx" class="com.xxx.xxx" p:name="${person.name}"/>

  2、Spring表达式语言(SpEL)进行装配,既然它可以作为一门“语言”,强大的功能毋庸置疑。与占位符类似,SpEL以形如"#{}"的形式出现,最简单的SpEL就是"#{1}",它就代表数字1,接下来看一下SpEL的常见用法。

  • #{T(JavaClass).staticMethod}:T()表达式会将()中的内容视为Java中的类,因此可直接调用其static方法,如#{T(System).currentTimeMillis()}。
  • #{3.14}:它表示字面常量,还有字符串类型的#{'abc'},科学记数法#{2.13E4},bool类型#{false}等。
  • #{bean-id}:它可以直接通过bean-id去引用一个bean,注意没有引号,加引号就是字符串常量。
  • #{bean.property}:它可以引用某个bean中的某个属性,如#{jack.age}可以获取java这个bean的name属性的值。
  • #{bean.method()}:它可以调用某个bean上的方法,如#{jack.eat()},返回值可以通过@Value注入到其他变量中。此外还可以向Java中那样级联调用,如#{jack.getName().toUpperCase()}。
  • "?."运算符:"?."运算符可以确保在访问它右边的内容之前,左边返回值不是null,如果是null则右边不会执行,转而整个表达式直接返回null。如#{jack.getName()?.toUpperCase()}可以判定getName返回的是不是null,不是null则继续执行toUpperCase,否则直接返回null。
  • SpEL运算符:其中条件运算可用于三元运算,如#{a==b?c:d};另外还可以检查null值,如果为null就用一个默认值替换,如#{jack.gf ?: 'alice'}。
    运算符类型 运算符
    算术运算 +、-、*、/、%、^
    比较运算 <、>、==、<=、>=、lt、gt、eq、le、ge
    逻辑运算 and、or、not、|
    条件运算 ?:(ternary)、?:(Elvis)
    正则表达式 matches (如:#{1234 matches '[0-9]+'})

 

 

 

 

  • #{bean.list[2].property}:SpEL可以引用数组或列表中的一个元素,下标从0开始,如#{jukebox.songs[2].title}表示jukebox专辑的第3音乐的标题。另外字符串也是一个集合,如#{'abc'[2]}表示第3个字母c。
  • .?[]:SpEL查询运算符,它会对集合进行过滤得到一个子集。如#{jukebox.songs.?[artist == 'Aerosmith']}表示只要jukebox专辑中艺术家为Aerosmith的音乐,[]中是筛选条件。
  • .^[]和.$[]:它们可以查询列表中第一个匹配和最后一个匹配的条目,如#{jukebox.songs.^[artist == 'Aerosmith']}表示查找jukebox专辑中第一个艺术家为Aerosmith的音乐。
  • .![]:它是SpEL中的投影运算,它会从集合的每个成员中选取特定的属性放到一个新的集合中,如#{jukebox.songs.![title]}表示将jukebox中所有音乐的标题提取出来。

SpEL的内容远不止这些,有兴趣可以深入学习,Spring任重而道远,继续努力,本次笔记到此结束!!!

posted @ 2018-03-19 12:15  江影不沉浮  阅读(183)  评论(0)    收藏  举报