前言
Spring
的核心是一个IOC
(控制反转)容器,IOC
的含义与DI
(依赖注入)等同,但是DI
显然更好理解,也就是说当我一个类依赖另一个类的实例时,我们不需要主动的去创建这个实例,而是请求IOC
容器帮我们把这个特定实例注入进来。为什么我们需要一个IOC
容器呢?实际上是为了降低代码的耦合性,也就是解耦。
IOC是如何帮助我们降低耦合性的?
从实际出发,我们设计一个场景,假设一个People
类,其实例表示一个人。同时设计一个Fruit
接口,定义了一个与其关联的行为eat()
,表示吃这个动作。只要实现了这个接口的水果,就可以被食用。
- Fruit接口
public interface Fruit {
public void eat();
}
- Apple类
public class Apple implements Fruit {
@Override
public void eat() {
System.out.println("苹果,直接吃。");
}
}
- Orange类
public class Orange implements Fruit{
@Override
public void eat() {
System.out.println("橘子,剥皮吃。");
}
}
因为People
既可以吃apple
也可以吃 orange
,所以我们可以给People
类设计两个方法eatOrange
和eatApple
,分别表示吃苹果和吃橘子这两个行为。
- People类
public class People {
private String name;
private int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
public void eatApple(){
Apple apple = new Apple();
apple.eat();
}
public void eatOrange(){
Orange orange = new Orange();
orange.eat();
}
}
直到目前为止都很正常,但是现在假设我们又多了一种水果Banana
, 那我们只要给People
类添加一个eatBanana
方法就可以了,似乎也不麻烦。但注意到我们这里只有People
这一种动物类别,假设我们还有Dog
、Cat
、Monkey
等100种不同动物,他们都能够吃Banana
这种水果,那我们就要手动的给100个类都添加上eatBanana
方法,是不是很麻烦。
为了解决这个问题,我们应当想到的是 People
这个类应当设计一个eatFruit
方法,其可以根据水果的种类,从一个地方拿到对应的水果,而不是自己去new
一个出来。工厂模式
似乎可以很好的解决这个问题,让我们建立一个FruitFactory
。
- FruitFactory类
public class FruitFactory {
public static Fruit getFruit(String name) {
if ("apple".compareTo(name)==0) {
return new Apple();
} else {
return new Orange();
}
}
}
- 新的People类
public class People {
private String name;
private int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
public void eatFruit(String name) {
Fruit fruit = FruitFactory.getFruit(name);
fruit.eat();
}
}
现在如果我们想新增加一个水果种类,只要在FruitFactory
中的一处修改就好了。组件之间的关系图如下。
可以看到很关键的一点是原来的People
类从一开始的主动new
一个水果对象实例,变成了根据水果名从FruitFactory
获取对应的水果实例,从创建变成了获取,完成了控制反转。我们知道SpringIOC容器
是整个Spring
的核心所在,我们可以简单的把它也想像成一个工厂,只不过IOC
容器不仅能够创建Fruit
类,还能够创建各种各样其他类型的对象,此外,除了创建对象实例本身,它还负责了这些实例的生命周期管理等。
使用IOC
容器
虽然现在Spring
框架更多的使用注解这种形式来完成依赖注入的工作,但其本质上和基于XML
描述文件的原理是不变的。XML
文件就像一张图纸,你在这张图纸里给你的XML
里描述你要创建的Bean
的各个属性,以及一个唯一的id
,然后交给Spring
,Spring
会使用这张图纸按照你的要求实例化各个对象,并且存放在IOC
容器里等你来获取并使用它们,我们以之前的People
类为例来说明这件事。
- People类
public class People {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void eatFruit(String name) {
Fruit fruit = FruitFactory.getFruit(name);
fruit.eat();
}
}
绘制图纸(编写XML
)文件
- applicationContext.xml
<?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">
<bean id="people" class="entity.People">
<property name="name" value="Tom"></property>
<property name="age" value="26"></property>
</bean>
</beans>
中间<bean>
开头</bean>
结尾的部分就是我们队一个bean
的详细描述,翻译过来就是给我创建一个bean
,它是entity.People
类的一个实例,唯一标识符为id='people'
,并且它的 name
属性的值是Tom
,age
属性的值为26
。
由
SpringIOC
容器管理的对象实例称为Spring bean
,这里简称bean
然后我们把这个xml文件
交由Spring
框架,按照这个xml
的文件生成一个对应的IOC
容器实例,然后我们就可以按照之前定义的id
,来从IOC
里直接取用这个对象实例了。这里要注意因为IOC
容器是可以管理各种类型的bean
的,所以这里返回的是一个Object
类型,需要我们进行强制类型转换。
public static void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
People people = (People) applicationContext.getBean("people");
System.out.println(people.getName() + " " + people.getAge());
}
- 输出结果
Tom 26
Process finished with exit code 0
Spring IOC
容器是如何根据XML
文件中的描述创建对应的对象实例呢?我们在XML
文件里指定了需要创建实例的全类名
,那么可以采用反射机制创建类的实例,注意到我们为People
类的每一个属性都设置了set
方法,那么利用反射机制的时候,只要 <property name="name" value="Tom"></property>
中name
的首字母大写,再在首部拼接上set
字段就可以推测出set
方法全名,然后调用这个方法设置对应的value
就可以了(这里属于Setter
注入,还有Constructor
注入,即构造器注入)。我们简单模拟一下这个实例化过程。
public static void testReflect() {
Object object = null;
try {
object = (People) Class.forName("entity.People").newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
Method m1, m2;
try {
m1 = Class.forName("entity.People").getMethod("setName", String.class);
m1.invoke(object,"Tom");
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
m2 = Class.forName("entity.People").getMethod("setAge", int.class);
m2.invoke(object, 26);
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println(((People) object).getName() + " " + ((People) object).getAge());
}
整个执行流程如下
依赖注入
注入方式
我们在编写XML
文件的时候,给每个Bean
预先设置好了值,将这些值赋给对应的属性时,有两种最基本的方式可以选择,一是Setter
注入,另一个是Constructor
注入。
Setter
注入
Setter
注入就是利用一个类的set
方法来对某个属性进行赋值,比如我们之前演示的 People
类,就利用了Setter
注入的方式。
<bean id="people" class="entity.People">
<property name="name" value="Tom"></property>
<property name="age" value="26"></property>
</bean>
再对name
和age
属性进入赋值的时候,会分别调用以下两个方法
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
让我们给这两个方法各添加一行打印信息,来验证是否真的被调用了。
public void setName(String name) {
System.out.println("调用了setName方法");
this.name = name;
}
public void setAge(int age) {
System.out.println("调用了setAge方法");
this.age = age;
}
运行结果如下,确实调用了这2个set
方法
调用了setName方法
调用了setAge方法
Tom 26
Process finished with exit code 0
当我们进行Setter
注入的时候,一定要保证这个类存在一个无参构造函数,因为Spring
会首先利用无参构造函数创建对象实例,再利用set
方法进行属性赋值,注意到如果我们不手动添加任何构造函数,系统会默认给我们添加一个无参构造函数,但是如果你手动添加了任何有参构造函数,系统则不会添加无参构造函数,这时候再调用setter
注入就会报错,例如如下代码。
public class People {
private String name;
private int age;
// 覆盖掉默认的无参构造函数
public People(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("调用了setName方法");
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
System.out.println("调用了setAge方法");
this.age = age;
}
public void eatFruit(String name) {
Fruit fruit = FruitFactory.getFruit(name);
fruit.eat();
}
}
则会给我们抛出一个异常,通知我们Failed to instantiate [entity.People]: No default constructor found
,说明找不到默认的无参构造函数。
Constructor
注入
除了使用set
方法,我们也可以通过Constructor
来对一个Bean
的属性进行赋值,例如
<bean id="people" class="entity.People">
<constructor-arg value="jack"></constructor-arg>
<constructor-arg value="26"></constructor-arg>
</bean>
指明了constructor
的参数,这里就会调用如下的构造器进行注入
public People(String name, int age) {
this.name = name;
this.age = age;
}
执行结果如下
jack 26
Process finished with exit code 0
这里需要注意,我们并没有指定每种参数的类型,这里完全是按照默认的顺序(即xml
里第一个constructor-arg
对应构造器中传入的第一个参数来赋值),让我们颠倒下xml
里两个参数的顺序,看看会怎么样。
<bean id="people" class="entity.People">
<constructor-arg value="26"></constructor-arg>
<constructor-arg value="jack"></constructor-arg>
</bean>
这时候就会告诉我们发生了错误Could not convert argument value of type [java.lang.String] to required type [int]
,不能把这个字符串类型的"jack"
转换成int
型,为了解决这个问题,我们可以明确的告诉Spring
每个value
对应的属性名具体是什么
<bean id="people" class="entity.People">
<constructor-arg name="age" value="26"></constructor-arg>
<constructor-arg name="name" value="jack"></constructor-arg>
</bean>
在这里由于2个参数类型不同,也可以通过指明类型来解决
<bean id="people" class="entity.People">
<constructor-arg value="26" type="int"></constructor-arg>
<constructor-arg value="jack" type="java.lang.String"></constructor-arg>
</bean>
此外,还可以通过标注对应的位置,index
从0
开始
<bean id="people" class="entity.People">
<constructor-arg value="26" index="1"></constructor-arg>
<constructor-arg value="jack" index="0"></constructor-arg>
</bean>
以上三种方法最推荐第一种,更加清晰。
ref
、null
与集合类型的注入
-
null
值
当我们需要对一个值赋予空值null
的时候,可以采用标签<null/>
<bean id="people" class="entity.People">
<constructor-arg value="26" index="1"></constructor-arg>
<constructor-arg index="0">
<null/>
</constructor-arg>
</bean>
-
ref
引用类型
有时候一个bean
的内部可能会引用另外一个bean
,这时候就需要ref
引用类型,ref
的值是被引用的bean
的id
。
我们先设计一个Car
类,brand
表示它的品牌。
public class Car {
private String brand;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
}
再在People
类里添加一个Car
的属性,表示这个人拥有的汽车。并修改一下构造方法。
public class People {
private String name;
private String address;
private Car car;
private int age;
public People(){}
// 覆盖掉默认的无参构造函数
public People(String name, int age, Car car) {
this.name = name;
this.age = age;
this.car = car;
}
}
修改xml
文件
<bean id="car" class="entity.Car">
<property name="brand" value="Benz"></property>
</bean>
<bean id="people" class="entity.People">
<constructor-arg value="26" index="1"></constructor-arg>
<constructor-arg index="0">
<null/>
</constructor-arg>
<constructor-arg ref="car" index="2"></constructor-arg>
</bean>
注意people
的第三个构造参数,不是value
而是ref
,表示注入的是对id=car
这个bean
的引用。
执行测试方法
public static void testRef(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
People people = (People) applicationContext.getBean("people");
System.out.println(people.getName() + " " + people.getAge()+ " " + people.getCar().getBrand());
}
输出结果:
null 26 Benz
Process finished with exit code 0
- 集合类型的注入
java
里诸如list
、set
、map
、array
等都属于集合类型,Spring
也为它们提供了对应的注入方式,分别对应于xml
中的<list>
、<set>
、<map>
、<array>
标签,我们先创建一个CollectionDemo
类用于演示。
public class CollectionDemo {
private List<String> listDemo;
private Set<String> setDemo;
private Map<String, String> mapDemo;
private String[] arrDemo;
public List<String> getListDemo() {
return listDemo;
}
public void setListDemo(List<String> listDemo) {
this.listDemo = listDemo;
}
public Set<String> getSetDemo() {
return setDemo;
}
public void setSetDemo(Set<String> setDemo) {
this.setDemo = setDemo;
}
public Map<String, String> getMapDemo() {
return mapDemo;
}
public void setMapDemo(Map<String, String> mapDemo) {
this.mapDemo = mapDemo;
}
public String[] getArrDemo() {
return arrDemo;
}
public void setArrDemo(String[] arrDemo) {
this.arrDemo = arrDemo;
}
}
其对应的xml
配置。
<bean id="collectionDemo" class="entity.CollectionDemo">
<property name="arrDemo">
<array>
<value>arr-apple</value>
<value>arr-orange</value>
<value>arr-banana</value>
</array>
</property>
<property name="listDemo">
<list>
<value>list-apple</value>
<value>list-orange</value>
<value>list-banana</value>
</list>
</property>
<property name="setDemo">
<set>
<value>set-apple</value>
<value>set-orange</value>
<value>set-banana</value>
</set>
</property>
<property name="mapDemo">
<map>
<entry>
<key>
<value>key-apple</value>
</key>
<value>val-apple</value>
</entry>
<entry>
<key>
<value>key-orange</value>
</key>
<value>val-orange</value>
</entry>
<entry>
<key>
<value>key-banana</value>
</key>
<value>val-banana</value>
</entry>
</map>
</property>
</bean>
测试类
public static void testCollection(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
CollectionDemo collectionDemo = (CollectionDemo) applicationContext.getBean("collectionDemo");
System.out.println("arr中的元素");
for (String e : collectionDemo.getArrDemo()) {
System.out.print(e + " ");
}
System.out.println();
System.out.println("list中的元素");
for (String e : collectionDemo.getListDemo()) {
System.out.print(e + " ");
}
System.out.println();
System.out.println("set中的元素");
for (String e : collectionDemo.getSetDemo()) {
System.out.print(e + " ");
}
System.out.println();
System.out.println("map中的元素");
for (String e : collectionDemo.getMapDemo().keySet()) {
System.out.println(e + " " + collectionDemo.getMapDemo().get(e));
}
System.out.println();
}
输出结果
arr中的元素
arr-apple arr-orange arr-banana
list中的元素
list-apple list-orange list-banana
set中的元素
set-apple set-orange set-banana
map中的元素
key-apple val-apple
key-orange val-orange
key-banana val-banana
Process finished with exit code 0
Autowire
自动装配
Spring
还提供了自动装配的功能,就是说如果一个Bean
中的一个属性没有手动的对其进行赋值,开启了自动装配选项后,就会在当前的容器里找到一个id
值与这个属性名相同的Bean
,用这个Bean
对这个属性赋值。注意Autowire
只适用于ref
类型。
用之前的People
类举例
<bean id="people" class="entity.People" autowire="byName">
<property name="name" value="Tom"></property>
<property name="age" value="26"></property>
<!--<constructor-arg ref="car" index="2"></constructor-arg>-->
</bean>
我们在这个bean
上指定了autowire="byName"
,告诉Spring
,我这里有一个属性car
没有赋值,你去找找当前有没有一个id=car
的bean
,如果正好有,就把它赋值给我这个ref
类型的名叫car
的属性,没有就算了。这里的byName
其实是根据id
来搜寻。
利用注解的方式完成依赖注入
利用注解的方式完成依赖注入可以节省编写xml
的大量时间。利用注解的方式完成依赖注入有两步
- 在想要被注入到IOC容器里的类上添加相应的注解。
- 配置注解扫描器,指明要扫描注解的目录。
现在假设我们想利用注解的方式,将我们之前的People
类注入到IOC
容器中去,首先在People
类上添加注解如下
@Component("people")
public class People {
@Value("Ben")
private String name;
@Value("China")
private String address;
@Autowired
private Car car;
@Value("34")
private int age;
}
第一行的@Component("people")
告诉Spring
框架,这是一个我想注入到IOC
容器中的Bean
,它的id
是people
,以后可以通过这个id
找到它。每个属性上的@Value("xxxx")
,表示这是一个把这个值赋给注解下面的属性。
@Autowired
注解下的是一个Car
类型的引用。这里采用了之前说的自动装配
的方式,去IOC
容器里找有没有id=car
的bean
,有的话就赋值给People
的car
属性。实际上使用上述的注解就相当写了如下的xml
。
<bean id="people" class="entity.People" autowire="byName">
<property name="name" value="Bem"></property>
<property name="age" value="34"></property>
<property name="address" value="china"></property>
</bean>
再让我们配置扫描器,告诉Spring
去哪里扫描以获取对应的Bean
<context:component-scan base-package="entity"></context:component-scan>
因为People
类在entity
目录下,所以这里base-package="entity"
。
最后,测试一下是否注入成功
public static void testAnnotationDI(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
People people = (People) applicationContext.getBean("people");
System.out.println(people.getName() + " " + people.getAge() + " " + people.getAddress() + " " + people.getCar().getBrand());
}
执行结果,发现注入成功。
Ben 34 China Benz
Process finished with exit code 0
网友评论