Spring Bean装配深入

田忌赛马

马分为上中下三等,对方出上,忌出下,对方出中,忌出上,对方出下,忌出中,忌胜。

应用中会出现环境依赖的bean,就拿数据源使用场景来说,开发简单功能时可能使用内嵌数据库,
而生产环境一般使用市场份额占据一席之地的数据库。我们不会在发布到测试生产环境时再切换为
特定环境的数据源bean,然后重新编译发布。

@Profile注解

Spring最初提供profile注解来解决装配环境特定的bean。

@Configuration
public class HxDataSourceConfig {
    @Bean
    @Profile("dev")
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScripts(new String[]{"hxschema.sql",
                        "hxdata.sql"})
                .build();
    }

    @Bean
    @Profile("prod")
    public DataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("root");
        datasource.setPassword("h123");
        return datasource;
    }
}

验证

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
//激活dev配置
@ActiveProfiles("dev")
public class HxDataSourceConfigTest {

    @Autowired
    private DataSource dataSource;

    //当前激活dev配置,embeddedDataSource bean被创建
    @Test
    public void testEmbeddedDataSourceBean() {
        assert (dataSource instanceof EmbeddedDatabase);
    }
}

此外,@Profile也可以注解在类级别

@Configuration
@Profile("dev")
public class HxDataSourceConfig {
}

动手实践

如果HxDataSourceConfigTest类上不加@ActiveProfiles注解或者@ActiveProfiles不指定注解值。
会怎样?

上例中的2个bean都不会返回。程序扔出NoSuchBeanDefinitionException

如果HxDataSourceConfigTest类上激活dev,@ActiveProfiles("dev")。但是mysqlDataSource
不加任何注解,那么mysqlDataSource bean还会创建吗?修改配置类为只定义一个mysqlDataSource
bean,且不使用@Profile注解。

没有profile注解的Bean不受当前激活的Profile约束,依然会创建。因此激活的Profile
只会约束那些用profile声明依赖特定环境的bean

@Configuration
public class HxDataSourceConfig {
    @Bean
    public DataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("root");
        datasource.setPassword("h123");
        return datasource;
    }
}

//test
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
@ActiveProfiles("dev")
public class HxDataSourceConfigTest {

    @Autowired
    private DataSource dataSource;

    //测试通过
    @Test
    public void testmysqlDataSource() {
        Assert.notNull(dataSource);
    }
}

基于XML装配的profile配置

<!--方式一,基于环境创建多个定义bean的xml-->
<!--dev.xml-->
<beans profile="dev">
    <bean></bean>
</beans>

<!--prod.xml-->
<beans profile="prod">
    <bean></bean>
</beans>

<!--方式二,所有环境bean都放在一个xml,用beans嵌套-->
<beans>
    <beans profile="dev">
        <bean></bean>
    </beans>

    <beans profile="prod">
        <bean></bean>
    </beans>
</beans>

profile激活

上面在单元测试中,我们已经在做测试时想要激活配置了profile的bean的方案。
在类上使用@ActiveProfiles注解

@ActiveProfiles("dev")
public class HxDataSourceConfigTest{
}

Spring提供spring.profiles.active和spring.profiles.default来激活1个多个profile

若配置了spring.profiles.active,则取其值,否则若配置了spring.profiles.default,
则取之,否则没有任何profile处于激活状态,任何声明了profile的bean将不会被创建。

还可以通过哪些途径设置spring.profiles.active和spring.profiles.default值?

比如开发时在web.xml中设置spring.profiles.default值

<!--web.xml-->
<web-app>
    <!--other omitted for simplification-->
    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </context-param>
    <servlet>
        <servlet-name>delegateServlet</servlet-name>
        <servlet-class>designpattern.delegatepattern.mock.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>spring.profiles.default</param-name>
            <param-value>dev</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
</web-app>

部署时通过环境变量,JVM系统属性,或JNDI条目设置spring.profiles.active值

@Conditional注解

@Profile注解特定于设置环境条件。但bean的创建与否可能还要考量其他的依赖条件。此时
@Profile注解就无能无力,需要更为通用的条件注解解决方案:@Conditional来了

使用方法

首先阅读@Conditional注解的javadoc,这点很重要,通过阅读文档了解到

  1. 在value指定的所有实现了Condition接口的类设定的条件都满足后,@Conditional注解的组件才会注册
  2. condition是一种状态,可以通过编程的方式确定。确定的结果实际上就是boolean类型
  3. @Conditional可以作为类型级别的注解直接用在任意类上,或间接地,和@Component,@Configuration一起
    作为元注解组合自定义注解。
  4. @Conditional注解可作为方法级别的注解用在bean方法上。
  5. 当@Conditional用在@Configuration类上时,类里的bean方法,和类相关的@ComponentScan,
    @Import都要受条件制约。
  6. @Conditional注解的条件是非继承的。超类的condition会被子类忽略。

其次应该带着问题去实践验证及理解这些。

//让我们继续修改HxDataSourceConfig类如下
@Configuration
public class HxDataSourceConfig {
    @Bean
    //这边使用@Conditional注解
    @Conditional(JdbcPropertyExist.class)
    public DataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("root");
        datasource.setPassword("h123");
        return datasource;
    }
}

//设定条件
//application.properties文件存在hxdatasource.jdbcDriver属性则返回true
class JdbcPropertyExist implements Condition {
    //本示例定义的条件并没有依赖ConditionContext及AnnotatedTypeMetadata接口参数
    //但实际使用时,可能需要详细了解这两个接口提供的特性
    @Override
    public boolean matches(ConditionContext context,
                           AnnotatedTypeMetadata metadata) {
        InputStream inputStream = JdbcPropertyExist.class.getClassLoader()
                .getResourceAsStream("application.properties");
        Properties properties = new Properties();
        try {
            properties.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return properties.stringPropertyNames()
                .contains("hxdatasource.jdbcDriver");
    }
}

## application.properties文件
## 文件里的属性决定了mysqlDataSource是否会创建
hxdatasource.jdbcDriver

验证

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
public class HxDataSourceConfigTest {
    @Autowired
    private DataSource dataSource;

    @Test
    public void testmysqlDataSource() {
        Assert.notNull(dataSource);
    }
}

自动装配的二义性

《西游记-真假美猴王》

观音:你们谁是悟空啊

spring中bean自动装配真香,但有多个bean符合条件时spring也会犯难。
这就让我们给它加点料。

@Configuration
public class HxDataSourceConfig {
    @Bean
    public EmbeddedDatabase embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScripts(new String[]{"hxschema.sql",
                        "hxdata.sql"})
                .build();
    }

    @Bean
    public DriverManagerDataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("root");
        datasource.setPassword("h123");
        return datasource;
    }
}

验证

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
public class HxDataSourceConfigTest {
    @Autowired
    private DataSource dataSource;

    @Test
    public void testmysqlDataSource() {
        Assert.notNull(dataSource);
    }
}

Spring开始抱怨:Could not autowire field,NoUniqueBeanDefinitionException

我们声明自动注入的字段为DataSource接口类型,而我们在配置类HxDataSourceConfig中
定义的2各bean的类型都实现了DataSource接口。因此Spring不能在没有帮助的情况下做
任意选择。

可是balabala总归继续,我们可以使用@Primary注解其中一个bean。让它在spring犯难时,
站出来分担。此为花果山选出了美猴王。

@Bean
@Primary
public EmbeddedDatabase embeddedDataSource() {
}
<!--xml中配置primary bean-->
<bean primary="true"></bean>

是日,另一个美猴王也闹着要去取经。

出现多个@Primary时,spring再次陷入了困惑。NoUniqueBeanDefinitionException, more than one 'primary' bean found among candidates more than one 'primary' bean found among candidates
显然@Primary侧重于定性的角度,并没有约束客户端如何使用(比如唯一使用等)

可是balabala总归继续,spring提供了限定符解决方案:@Qualifier。可以理解为前去取经的
要求更严格了,从'美猴王'到'石猴美猴王'。这里面的石猴其实就是一种限定。回到示例

//在自动注入点使用@Qualifier
@Autowired
@Qualifier("mysqlDataSource")
private DataSource dataSource;

注入点的@Qualifier里的value可以匹配2种场景

  1. 使用@Qualifier声明的值为value的bean
  2. 没有使用@Qualifier的id为value的bean

因此上面注入点可以匹配如下情况

//场景一 bean不使用@Qualifier
//方法名和注入点@Qualifier的value相同的bean方法
//或@Component注解的类名和value相同的类(除了首字母大小写)
@Bean
public DriverManagerDataSource mysqlDataSource() {
}

//场景二:bean显式使用@Qualifier声明
@Bean
@Qualifier("mysqlDataSource")
public DriverManagerDataSource mysqlDataSource() {
}

 使用默认的bean Id作为注入点的@Qualifier的值似乎是简单而直接的。
 但是这样就使注入点的代码和被注入的bean方法或类名产生紧耦合,当bean方法或
类名重构时,注入点的代码需要修改,考虑到这点,可以在bean方法或@Component类上
使用@Qulifier自定义一个意义明确的限定符,在注入点再使用这个限定符。
 或者如果使用IDEA开发的话,使用默认的bean Id也没问题,因为在IDEA里重构bean方法时,注入点可以智能感知并重构。

自定义@Qualifier注解

到现在,一切都运作良好,让我们再制造点小麻烦。

 继续之前的美猴王的故事,我们现在假设这么个场景,石猴美猴王也不是唯一的,西部也出现了一个。
 那么我们继续使用@Qualifier来缩写匹配范围。如在bean定义点,注入点都加上@Qualifier("石猴美猴王")和@Qualifier("西部")

问题是Spring的@Qualifier注解不支持在同一条目上使用多次

解决的方法是使用自定义限定符注解,做法很简单,自定义注解内部使用@Qualifier注解即可

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Dev {
}

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Test {
}

@Configuration
public class HxDataSourceConfig {

    @Bean
    @Qualifier("mysql")
    @Dev
    public DriverManagerDataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("dev");
        datasource.setPassword("h123");
        return datasource;
    }

    @Bean
    @Qualifier("mysql")
    @Test
    public DriverManagerDataSource mysqlDataSource2() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("test");
        datasource.setPassword("h123");
        return datasource;
    }
}

验证

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
public class HxDataSourceConfigTest {
    @Autowired
    @Qualifier("mysql")
    @Dev
    private DataSource dataSource;

    @Test
    public void testmysqlDataSource() {
        //这边输出dev下的用户名,因为上面使用了@Dev
        System.out.println(((DriverManagerDataSource)dataSource).getUsername());
        Assert.notNull(dataSource);
    }
}

这边遗留一个问题,很难想象使用@Qulifier不能解决bean二义性问题,而需要借助自定义的多个
限定符注解。

能猜测的一个原因就是,开始没设计好,后续代码也不好随意更改限定符值,
只能新增一个重构为自定义注解的bean

但这种自定义限定符注解优点还是很明显的

  1. 注解名是自解释限定符的,不需要像@Qualifier给它一个值,比如我们上面的@Dev
  2. 这种自定义注解本身可以*组合,实现多种目标。

bean的作用域

作用域类型 描述 使用
单例 整个应用创建一个bean实例 默认
原型 注入时或通过应用上下文获取时都会创建新实例 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
会话 Web应用,每个会话创建一个实例 @Scope(value = WebApplicationContext.SCOPE_SESSION,proxyMode =视情况)
请求 Web应用,每个请求创建一个实例 @Scope(value = WebApplicationContext.SCOPE_REQUEST,proxyMode =视情况)
//定义bean
@Configuration
public class HxDataSourceConfig {

    //单例bean。单例是spring bean的默认作用域
    @Bean
    public DriverManagerDataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("dev");
        datasource.setPassword("h123");
        return datasource;
    }

    //原型bean。使用点将得到一个新的bean实例
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Fruit someKindFruit() {
        Fruit f = new Fruit();
        return f;
    }
}

//验证
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
public class HxDataSourceConfigTest {
    @Autowired
    private DataSource dataSource;

    @Autowired
    private DataSource dataSource2;

    //pass,因为bean mysqlDataSource是单例,dataSource和dataSource2都指向该实例
    @Test
    public void testSingletonBean() {
        Assert.isTrue(dataSource == dataSource2);
    }

    @Autowired
    private Fruit fruit1;

    @Autowired
    private Fruit fruit2;

    //pass fruit1和fruit2指向不同的实例
    @Test
    public void testPrototypeBean() {
        Assert.isTrue(fruit1 != fruit2);
    }
}

会话请求作用域

这边侧重理解下概念

 举个通俗的例子,超市我们都去过,购物车都用过。如果我们把购物车声明为一个特定的bean。
针对该bean,如果超市只有一个购物车,那叫单例
 我们每次去超市,都会取一个空的购物车,那么这叫原型。
 就我们这次去购物的场景,我们认为是一个会话,在这个会话里。我们不管把购物车推到哪个货架,
用的都是这个购物车。
 我们把这次购物里每次对不同的品类进行购买使用的一次性塑料袋视为请求,买水果时放一个塑料袋,
买水产品时放另一个塑料袋。

    //session作用域的bean定义
    @Bean
    @Scope(value = WebApplicationContext.SCOPE_SESSION,
            proxyMode = ScopedProxyMode.INTERFACES)
    public ShorpCart someShopCart() {
        HandPushingShopCart cart = new HandPushingShopCart();
        return cart;
    }

 这边proxyMode解决的问题是:较短生命周期的bean注入到较长生命周期bean时的问题。如应用向单例bean superMarket中注入session bean ShopCart这种场景,在单例bean
初始化时,还没有任何实际意义的ShopCart。因此这边注入到superMarker里的是ShopCart
bean的代理。当superMarket调用ShopCart bean的方法时,将由代理解析将调用委托给
实际session作用域中的ShopCart bean。
  proxyMode这边使用的ScopedProxyMode.INTERFACES,因为HandPushingShopCart实现了
ShopCart接口,可以使用标准的jdk基于接口的代理。如果没有实现接口,可以使用CGLIB代理,只需要改为proxyMode = ScopedProxyMode.TARGET_CLASS

xml中定义session作用域的方式

<bean id="someShopCart" class="com.hxapp.HandPushingShopCart"
          scope="session">
      <aop:scoped-proxy proxy-target-class="false"/>
</bean>

运行时值注入

注入外部值

一般很少在程序中进行硬编码,考虑灵活性,一般在属性文件定义一些属性变量,有点类似我们
配置系统环境变量的做法。

//@PropertySource一般和@Configuration一起使用
//通过使用@PropertySource和Environment,指定属性文件里的属性及值就能被填充到Environment里
@Configuration
@PropertySource(value = "application.properties")
public class SomeConfig {
    @Autowired
    Environment env;

    @Bean
    public Student anyStudent() {
        if (env.containsProperty("student.name")) {
            return new Student(env.getProperty("student.name"));
        }
        return  new Student("");
    }
}

//测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SomeConfig.class)
public class RuntimeValueInjectionTest {

    @Autowired
    Student student;

    @Test
    public void testStudentNameFromProperties() {
        Assert.assertEquals("hyman", student.getName());
    }
}

Spring 4.1后也提供了在测试中使用@TestPropertySource,功能和@PropertySource类似,如下所示

@ContextConfiguration(classes = HxDataSourceConfig.class) 
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource("classpath:application.properties")
public class HxDataSourceConfigTest {

    @Autowired
    Environment env;

    @Test
    public void TestHymanName2() {
        String name = env.getProperty("student.name", String.class);
        assert(name.equals("hyman"));
    }
}

使用属性占位符注入属性值
XML配置文件中以${..}形式使用占位符,且需要声明<context:property-placeholder/>

<!--定义在resources目录下的some-config.xml-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <context:property-placeholder/>
    <bean id="student" class="com.hxapp.Student">
        <constructor-arg name="name" value="${student.name}"/>
    </bean>
</beans>
#定义在resources目录下的application.properties文件
student.name=hyman
//测试
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource("classpath:application.properties")
@ContextConfiguration(locations = "classpath:some-config.xml")
public class RuntimeValueInjectionTest {
    @Autowired
    Student student;

    @Test
    public void testStudentNameFromProperties() {
        Assert.assertEquals("hyman", student.getName());
    }
}

组件扫描和自动装配的方式使用bean的值注入,可以使用@Value

//修改配置类,添加组件扫描组件,以扫描到student bean
@Configuration
@ComponentScan(basePackages = "com.hxapp")
public class SomeConfig {
}

//修改Student类,声明为组件,构造函数参数使用@Value
@Component
public class Student {
    String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    //使用@Value
    public Student(@Value("${student.name}")String name) {
        this.name = name;
    }
}

//测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SomeConfig.class)
@TestPropertySource("classpath:application.properties")
public class RuntimeValueInjectionTest {
    @Autowired
    Student student;

    @Test
    public void testStudentNameFromProperties() {
        Assert.assertEquals("hyman", student.getName());
    }
}

SpEL(Spring Expression language)

形式:#{表达式体}
Spring 3起引入
使用场景:bean装配,spring security安全规则, Thymeleaf模板使用SpEL引用模型数据

特性 示例
对值进行算术、关系和逻辑运算 #{T(java.lang.Math).PI * R ^ 2}、#{1}、#{"123"}、 #{score > 90 ? "good" :"bad"}
引用bean及其属性方法 #{somebean}、 #{somebean.property} 、#{somebean.method()}
调用对象方法或访问属性 #{systemProperties['student.name']}
支持正则表达式 #{26characters.lowercase matches [a-z]}
操作集合 #{shelf.books[0].title}、#{shelf.books.?[title eq 'fire and ice']}

更多详细内容请参考 Spring IN ACTION FOURTH EDITION

上一篇:Spring基于注解的配置1——@Required、@Autowired、@Qualifier示例及与传统注入方法的对比


下一篇:VolgaCTF 2020 Qualifier User Center