Spring-IOC

Spring-IOC

基础概念

spring全家桶:spring,springmvc,spring boot,spring cloud

spring:出现在2020年左右,解决企业开发难度——减轻对项目模块之间的管理,类和类之间的管理,帮助开发人员创建对象,管理对象间的关系。spring核心技术iocaop,能实现模块之间、类之间的解耦合。

依赖:类A中使用类B的属性或方法,则称类A依赖类B。

bean的装配

基于XML配置文件:

借助构造器注入初始化bean

  1. 通过<constructor-arg>元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    构造注入:spring 调用类的有参构造方法,在创建对象时给属性赋值
    构造注入使用constructor-arg标签,其标签属性:
    name:表示构造方法的形参名
    index:表示构造方法中的参数位置,参数从左到右是0、1、2的顺序
    value:表示形参为简单类型时的参数的值,注:String类型变量也是简单类型
    ref:表示形参为引用类型时的参数的值,应用bean的id

    <bean id="sgtPeppers" class="cn.hznu.SgtPeppers">
    <constructor-arg name="artist" value="Sgt. Pepper's Lonely Hearts Club Band"/>
    <constructor-arg name="title" value="The Beatles"/>
    </bean>
  2. 通过c-命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
使用前需在xml顶部beans标签中声明命名空间
xmlns:c="http://www.springframework.org/schema/c"

其中各字段含义为:
其中数字代表index,表示构造方法中的参数位置,参数从左到右是0、1、2的顺序,由于xml文件中不允许数字作为属性的第一个字符,'_'仅作为前缀
c:_0="" 代表构造方法第一个参数的为简单类型时赋值
c:_0-ref="" 引用类型时赋值
其中artist代表构造方法形参名
c:artist=""

<bean id="sgtPeppers01" class="cn.hznu.SgtPeppers"
c:_0="The Beatles" c:_1="Sgt. Pepper's Lonely Hearts Club Band">
</bean>

对于集合属性如List'、'Set等其注入方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="sgtPeppers" class="cn.hznu.SgtPeppers">
<constructor-arg name="artist" value="Sgt. Pepper's Lonely Hearts Club Band"/>
<constructor-arg name="title" value="The Beatles"/>
<!-- 集合注入 -->
<constructor-arg name="list">
<list>
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
</list>
</constructor-arg>
</bean>

<constructor-arg>元素比c-命名空间比较:

  • 前者比后者ml文件更加难读懂(书中这么写道,但个人而言觉得前者的可读性较好,但编写更麻烦);
  • 有些事情只有前者能做,但是后者做不了,比如说后者无法完成装配集合功能。

通过设置属性setter

注:一定要有元素对应的setter方法,必须要有对应构造方法,如空构造

基本与构造器注入类似,对应关系为<property>元素、p-命名空间,具体使用参考下述案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
基于<property>元素

<bean id="sgtPeppers" class="cn.hznu.SgtPeppers">
<property name="artist" value="Sgt. Pepper's Lonely Hearts Club Band"/>
<property name="title" value="The Beatles"/>
<property name="list">
<list>
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
</list>
</property>
</bean>

基于p-命名空间,与c-不同是无index下标方法,以属性名作为标识
<bean id="sgtPeppers02" class="cn.hznu.SgtPeppers"
p:title="The Beatles" p:artist="Sgt. Pepper's Lonely Hearts Club Band">
</bean>

对于命名空间无法注入集合的问题,存在下述解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
包含命名空间
xmlns:util="http://www.springframework.org/schema/util"

预定义好集合
<util:list id="list">
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
<value>hey man</value>
</util:list>

再通过ref引用

Spring util-命名空间中的元素l

元素 描述
util-constant 引用某个类型的public static域,并将其暴露为bean
util-list 创建一个java.util.List,类型的bean,其中包含值或引用
util-map 创建一个java.util.Map,类型的bean,其中包含值或引用
util-properties 创建一个java.util.Property,类型的bean,其中包含值或引用
util-prooerty-path 引用bean的属性(或内嵌属性),并将其暴露为bean
util-set 创建一个java.util.Set,类型的bean,其中包含值或引用

缺点:

  1. 在bean的声明中,<bean id="cdPlayer" class="cn.hznu.CDPlayer"/>,由于类型(即Class)由字符串声明,该方式的声明并不能从编译器的类型检查中受益,问题:将Class名重命名后,会发生什么?

处理自动装配的歧义性

问题说明

一个最简单的案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 接口
public interface Dessert {
void eat();
}

// 实现类
public class Cake implements Dessert {
@Override
public void eat() {
System.out.println("eat cake!");
}
}

public class Cookie implements Dessert {
@Override
public void eat() {
System.out.println("eat cookie!");
}
}

public class IceCream implements Dessert {
@Override
public void eat() {
System.out.println("eat icecream!");
}
}

// JavaConfig类
@Configuration
public class DessertConfig {
@bean
public Cake cake() {
return new Cake();
}

@bean
public Cookie cookie() {
return new Cookie();
}

@bean
public IceCream iceCream() {
return new IceCream();
}
}

// 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DessertConfig.class)
public class Test001 {
@Autowired
private Dessert dessert;
}

很明显,在测试类中要通过@Autowired注解自动注入,Spring显然不能明确到低要注入哪一个bean,因此会产生歧义。

以下为几种解决方法。

解决方法

1、创建首选的bean

在创建bean时利用 @Primary 注解标识为首选,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1、JavaCongid中显示声明bean时,加上@Primary注解,标识其为首选
@Primary
@bean
public Cake cake() {
return new Cake();
}

// 2、通过@Component组件扫描
@Primary
@Component
public class Cake implements Dessert {
@Override
public void eat() {
System.out.println("eat cake!");
}
}
1
2
<!-- xml文件声明bean时 -->
<bean id="iceCream" class="org.example.IceCream" primary="true" />

注:但这里并不限制设定首选bean的个数,所以可能还会产生歧义。

2、限制自动装配的bean

通过@Qualifier注解设定注入的bean的限定符,即可对应唯一的bean,从而解决歧义问题,如下:

1
2
3
@Qualifier("iceCream")
@Autowired
private Dessert dessert;

注:限定符与bean的id的区分(二者不是同一概念),在未主动声明bean的限定符时,限定符与bean的id相等,但仅限于字符串的相等

创建自定义的限定符

通过@Qualifier注解在声明bean的时候自定义标识符,同上述 @Primary 的用法,不再赘述。

注:一般自定义限定符时,最佳实践是为bean选择特征性描述性的术语,而不是随意指定名字。

如:

1
2
3
4
5
6
7
8
9
@bean
@Qualifier("cold")
public IceCream iceCream() {
return new IceCream();
}

@Qualifier("cold")
@Autowired
private Dessert dessert;

但这里发现一个很奇怪的问题,当对bean自定义限定符之后,默认限定符依旧可用。

测试后发现,自动注入时首先参照自定义限定符,若给定的限定符不存在bean与之对应,则会向下查找bean id相同的bean然后注入。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Java配置部分
@Configuration
public class DessertConfig {
@bean
public Cake cake() {
return new Cake();
}

@bean
public Cookie cookie() {
return new Cookie();
}

// 自定义限定符
@bean
@Qualifier("cold")
public IceCream iceCream() {
return new IceCream();
}
}

// 测试代码
@Test
public void test01() {
dessert.eat();
}

// 自动注入部分
@Qualifier("cold")
@Autowired
private Dessert dessert; // 输出 eat icecream!

@Qualifier("iceCream")
@Autowired
private Dessert dessert; // 输出 eat icecream!

// 修改Java配置部分如下后
@Configuration
public class DessertConfig {
@bean
public Cake cake() {
return new Cake();
}

@Qualifier("iceCream")
@bean
public Cookie cookie() {
return new Cookie();
}

@bean("hey")
@Qualifier("cold")
public IceCream iceCream() {
return new IceCream();
}
}

// 测试代码不变
@Qualifier("iceCream")
@Autowired
private Dessert dessert; // 输出 eat cookie!

注:在容器中存在多个相同特性的bean的情况下,这种方式也会失效。最终的解决方法是使用自定义的限定注解,具体参照《Spring in Action 4》P82.

bean的作用域

默认情况下,Spring应用上下文中所有的bean都是作为以单例(Singleton)的形式创建的。

Spring定义了多种作用域,具体如下:

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

1、单例(Singleton)

默认状态下就是这个模式,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 实例类
@Component
public class TestClass {
public static int cnt = 0;

public TestClass() {
cnt++;
}
}

// 配置类
@Configuration
@ComponentScan
public class MyConfig {}

// 测试类
public class MyTest {
@Test
public void test01() {
ApplicationContext apl = new AnnotationConfigApplicationContext(MyConfig.class);
for (int i = 0; i < 10; i++) {
TestClass bean = apl.getBean(TestClass.class);
System.out.println(TestClass.cnt);
}
}
}

/* 具体输出如下: 1 1 1 1 1 1 1 1 1 1 */

2、原型(Prototype)

就如图表中所描述,具体测试如下,配置类、测试类相同一下代码不重复贴了:

1
2
3
4
5
6
7
8
9
10
11
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 通过此注解声明为模式为原型,默认为单例模式
public class TestClass {
public static int cnt = 0;

public TestClass() {
cnt++;
}
}

/* 具体输出如下: 1 2 3 4 5 6 7 8 9 10 */
1
2
<!-- xml配置文件中的写法 -->
<bean id="testClass" class="cn.beanscope.prototype.TestClass" scope="prototype" />
注:每创建一次上下文,则会将所有的单例模式的类重新创建bean,对于原型模式下的类只有用到时才会创建。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 其中TestClass是原型模式,而TestClass01则是单例模式,而定义中只包含记录类实例个数的静态变量cnt

// 以下两个注解会在测试类中自动创建Spring上下文
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MyConfig.class)
public class MyTest {
@Test
public void test01() {
System.out.println(TestClass.cnt + " " + TestClass01.cnt);
ApplicationContext apl = new AnnotationConfigApplicationContext(MyConfig.class);
System.out.println(TestClass.cnt + " " + TestClass01.cnt);
apl = new AnnotationConfigApplicationContext(MyConfig.class);
System.out.println(TestClass.cnt + " " + TestClass01.cnt);
}
}

/* 输出结果如下:
0 1
0 2
0
*/

3、使用会话和请求作用域

看到Web部分再填坑。

运行时值注入

1、注入外部的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 配置类
@Configuration
@PropertySource("app.properties") // 外部配置文件
public class ExpressiveConfig {
@Autowired
Environment env;

@Bean
public Pair<String, String> getPair() {
return new Pair<>(env.getProperty("disc.title"), env.getProperty("disc.artist"));
}
}

// 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ExpressiveConfig.class)
public class MyTest {
@Autowired
Pair<String, String> pair;

@Test
public void test01() {
System.out.println(pair);
}
}

2、通过XML配置文件引入Property文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?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: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">

<!-- jdbc配置文件 -->
<context:property-placeholder location="jdbc.properties"/>

<!-- 声明数据源,此处使用ali的Druid连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

<!-- 声明SqlSessionFactoryBean,即在容器中创建一个SqlSessionFactory对象 -->
<bean id="factory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="mybatis.xml"/>
</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" id="configurer">
<property name="sqlSessionFactoryBeanName" value="factory"/>
<property name="basePackage" value="cn.hznu.dao"/>
</bean>
</beans>