Spring_IoC
一、IoC相关概念简介
1、理解【控制】和【依赖】的概念
IoC即控制反转、依赖注入 。—— OOAD依赖倒置
【控制】和【依赖】都代表对象之间的关联关系,而且他们在Spring中是一对近义词,控制和依赖几乎是并存的。
例如,现有如下需求:汽车的功能是行驶,司机的功能是驾驶汽车,令汽车行驶。
我们可以通过代码描述上述关系:
public class Car {
// 汽车类
private String name; // 品牌
private Double price; // 价格
public void run(){
// 汽车行驶方法
System.out.println("Car : I'm running!");
}
}
public class Driver {
// 司机类
public Car car;
public void drive() {
// 司机驾驶方法
car.run();
}
}
上述案例中,司机可以调用驾驶方法,令其包含的汽车对象执行行驶方法。需要关注的问题是,car对象是如何被实例化的。
在传统的Java编程中,我们习惯于将这个car对象通过new的方式创建出来。那么什么时候去new它呢?答案是不唯一的。例如:
1)声明的同时实例化
public class Driver {
Car car = new Car();
public void drive(){
car.run();
}
}
2)在调用时进行实例化
public class Driver {
Car car;
public void drive(){
car = new Car();
car.run();
}
}
上述两种方式,Car对象被创建的时机不同。但是相同的是,new Car()的过程都发生在Driver类中。这样的形式,我们称之为Driver在自己的【代码内部】,控制了一个Car。
注意,这里的【控制】强调的是控制Car对象的创建,而并非Car对象方法的调用。
而【依赖】是指,此时Driver的正常工作需要依赖于Car。换言之,Driver和Car产生了耦合。
耦合是指对象之间相互依赖的程度。在软件设计和开发中,我们要尽可能地降低类之间的耦合度。一个类如果出现了异常,或发生了改变,那么依赖于它的其他所有类可能都不能正常运行,导致程序大面积的瘫痪,也称为雪崩效应。
2、IoC 控制反转
IoC即Inversion of Control,控制反转。
可以理解为“反转”对象的控制关系,或“改变”对象的控制关系。
传统的Java SE编程中,对象之间的关系是在【对象内部】直接控制,而IoC正是改变了这样的控制关系,改为由【外部容器】来控制。
大致的过程是,我们不再像原来那样将new操作写死在类中,而是将对象之间的关联关系定义在一个xml配置文件中。在程序运行期间,再由容器根据配置文件中定义的规则管理对象的生命周期,并建立对象之间的关联关系。
也可以这样理解,传统的写法是在编译期间就将对象关系规定死,程序运行时一定会按照这样的规则去执行。而Spring IoC的做法是在编译期间将对象全部分离,当进入到运行阶段再将他们动态结合到一起。
IoC体现了好莱坞原则,即“不要打电话过来,我们会打给你”。
3、依赖注入和依赖查找
public class Student{
private int id;
private String name;
private Teacher teacher;
}
Student stu = new Student();
Teacher teacher = new Teacher();
stu.setTeacher(teacher);
理解两个名词:
1)DI(Dependency Injection) 依赖注入 (要求掌握,核心内容)
依赖注入的基本原则是:应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责,“查找资源”的逻辑应该从应用组件的代码中抽取出来,交给IoC容器负责。
2)DL(Dependency Loopup) 依赖查找 (了解即可)
容器创建对象并提供回调接口和上下文环境给组件,需要时通过接口从容器中查找对象(理解即可)
DI和DL的关系:
假设现在需要使用A对象,A对象中需要配置一个B对象。那么我们在容器中配置一个A对象,配置一个B对象,并声明B对象是A对象的一个属性。这个过程叫做依赖注入。
如果你想从容器中取出某个对象对其进行操作,可以调用context组件的相关方法,根据特定的方式获取容器中的Bean。这个过程叫做依赖查找。
DI、DL和IoC的关系:
IoC是一种思想,而DI和DL是实现这种思想具体的方式。
从实际角度出发分析,IoC解决了对象由谁来创建、对象之间存在什么何种关联关系的问题
而DI解决了对象之间的关联关系如何建立的问题
IoC和DI总结:
简而言之,原本我们在代码中会频繁地new对象,现在使用IoC(DI),我们会观察到类似这样的效果:只需要在类中定义需要什么对象作属性,你也可以在方法中让这个对象去调用方法。而这样做并不会抛出空指针异常,因为IoC容器发现你需要用到这个对象时会先帮你进行注入。
二、Core、Beans、Context组件概念
1、组件及原理简介
Spring中包含诸多组件,其中Core、Beans、Context是最核心的三个组件。
它们组合起来,就实现了Spring中最核心的功能——IoC。
IoC是Spring功能的根基。如果没有IoC的存在,就不可能实现AOP、Web等其他上层功能。
通俗地来讲,三个组件的功能及角色如下:
-
Beans:组件(Bean的复数形式)
-
Context:容器(用来包含组件)
-
Core:工具
首先,Bean是Spring中最为核心的概念。Spring框架可以说就是在面向Bean编程。Bean之于Spring相当于Object之于OOP。在Spring中,所有的Java对象都是Bean。
IoC的核心概念是将Bean配置到容器中,而这个容器就是由Context来充当。在编译阶段,我们以xml文件的方式定义容器配置,声明Bean相关内容。运行期间,Spring会解析xml容器配置文件,将其中的内容封装成一个Context对象。我们可以通过这个Context对象获取到容器中的Bean。
Core是处理容器和Bean的一些工具和方法,如果把Core改称为Util就很好理解了。
如果说Context是一个舞台,那么Bean就是这个舞台上的演员。而Core就是演员演出所用的道具。
2.容器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-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<bean></bean>
<bean></bean>
</beans>
三、IoC常用jar包
如果需要在项目中使用基本的IoC功能,通常需要引入如下jar文件(版本信息省略):
spring-beans.jar
spring-context.jar
spring-core.jar
spring-expression.jar
junit.jar
commons-logging.jar
四、Bean对象的定义和获取
1、在容器中声明对象
【类声明】Car.java:
public class Car {
private String name; // 车名
private double price; // 价格
getters/setters...
}
【配置文件】bean.xml:
<bean id="mycar" class="com.briup.day01.Car"></bean>
2、在代码中获取容器中的对象
创建测试类BeanTest.java:
public class SetterTest {
@Test
public void bean_test() {
// 创建容器对象
ApplicationContext context = new ClassPathXmlApplicationContext(
"com/briup/day01/bean.xml");
// 获取Bean
Car car = (Car) context.getBean("mycar");
// 打印输出
System.out.println(car);
}
五、IoC注入方式
什么是注入?
【注入】也称装配,即为对象的属性赋值。
通过注入,可以为容器中的基本属性赋值,例如学生学号、姓名、年龄等,也可以通过注入来创建对象之间的关联关系。例如,在Driver对象中注入一个Car对象,此时由Car对象充当Driver对象的属性。
IoC实现注入有三种常用方式:
-
set注入
-
构造器注入
-
接口注入
比较常用的是set和构造器注入,接口注入用于引入外界系统资源,例如JNDI。
1.set注入
要求:要注入的属性必须包含对应的set方法。
可以注入的类型:①基本数据类型和String类型 ②对象类型 ③集合类型
1)基本数据类型
【配置文件】bean.xml:
<bean id="mycar" class="com.briup.day01.Car">
<property name="name" value="兰博基尼">
<property name="price">
<value>10.5</value>
</property>
</bean>
bean中的属性,id为该对象在容器中起一个名字,class指定对象类型。
除了id之外还可以使用name属性为Bean起名字。
id和name区别在于,id是唯一标识,并且要求比较严格。
容器会检查id属性值是否符合规(名字是否重复、是否以数字开头、是否包含空格等等),但是name不会检查这些内容。
<property>
元素用来为属性赋值,每个<property>
元素为对象的一条属性赋值。
name
属性用来指定属性名,例如name="price"
代表当前<property>
是为了给Car对象的price属性赋值。
value
属性是具体的值,也可以定义为<property>
中的子元素。
2)对象类型
【配置文件】bean.xml:
<bean id="mycar" class="com.briup.day01.Car">
<property name="name" value="兰博基尼">
<property name="price">
<value>10.5</value>
</property>
</bean>
<bean id="mydriver" class="com.briup.day01.Driver">
<property name="name" value="tom"></property>
<property name="age" value="22"></property>
<property name="car" ref="mycar"></property>
</bean>
我们需要给Driver对象的car属性赋值,那么需要在容器中先定义一个Car对象。
然后在property元素中,使用ref属性引用。
3)集合类型
我们同样可以为对象的集合类型属性进行注入,在配置文件中定义集合中的元素。
【配置文件】bean.xml:
<bean id="myColl" class="com.briup.day01.MyCollection">
<property name="list">
<list>
<value>1</value>
<value>2</value>
<value>3</value>
</list>
</property>
<property name="set">
<set>
<value>4</value>
<value>5</value>
<value>6</value>
</set>
</property>
<property name="map">
<map>
<entry key="1001" value="张三"></entry>
<entry key="1002" value="李四"></entry>
<entry key="1003" value="王五"></entry>
<entry key="1004">
<value>赵六</value>
</entry>
</map>
</property>
<property name="property">
<props>
<prop key="driver">oracle.jdbc.driver.OracleDriver</prop>
<prop key="url">jdbc:oracle:thin:@localhost:1521:Xe</prop>
<prop key="user">briup</prop>
<prop key="password">oracle</prop>
</props>
</property>
</bean>
2、构造器注入
方式: 配置<constructor-arg>
元素
在Bean类中不需要为属性定义set方法,但是要有相应的构造器。
构造器注入有两种形式 一个是根据参数类型 一个是根据参数位置的下标
<constructor-arg type="int" value="">
<constructor-arg index="0" value="">
例如:
<bean name="student" class="com.briup.bean.Student">
<constructor-arg type="int" value="25"/>
<constructor-arg type="java.lang.String" value="tom"/>
<constructor-arg type="long" value="100"/>
</bean>
3、自动装配
不需要手动在bean.xml中定义对象和对象的依赖关系。
而是由容器根据某种特定的规则自动帮我们找到合适的对象并进行注入。
例如,在容器中包含A和B两个对象。
而A对象中需要使用到一个B类型的依赖,那么容器就会自动将这个B对象注入到A中。
两种方式来判断是否符合要求:
配置自动装配也有两种方式:
1.在<beans>
中定义属性,相当于全局配置
default-autowire=“xxxxx”
如果在beans中有哪个bean不需要自动装配,那么可以在该bean元素中定义属性:autowire=“no”
2.在<bean>
中定义属性 相当于局部配置
【注意】自动装配仅适用于对象类型
六、容器中配置工厂
Spring IoC容器创建对象的方式主要有两种:
第一种是我们上面用到的声明方式,即:
<bean id="xxx" class="xxxx.xxx.xx"/>
这样的方式,框架底层会直接根据class属性值所定义的全限类名,通过反射机制创建bean对象。
还有另外一种创建对象的方式,即使用工厂模式。
工厂设计模式:专门用一个类来充当“工厂类”,该类的作用是按照固定方式批量生产某种对象。
通常,工厂类有两种形式:静态工厂和实例工厂。
静态工厂是指定类中获取对象的方法为静态方法,可以直接通过类名调用,无需创建工厂类对象。
而实例工厂是指类中获取对象的方法为成员方法,在获取要生产的对象之前,必须先创建一个工厂类对象。
上述两种工厂类都可以配置到IoC容器中,使用获取Bean的方式来获取工厂类生产的对象。
1、配置静态工厂
1)首先,提供一个工厂类。
注意:静态工厂中,必须提供一个static修饰的用来获取对象的方法。
public class StaticFactory {
static String driver = "com.mysql.jdbc.Driver";
static String url = "jdbc:mysql://localhost:3306/briup";
static String user = "root";
static String password = "root";
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() {
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
2)在容器配置文件中配置如下信息
<bean id="factory1" class="com.briup.spring.day02.static_factory.StaticFactory"
factory-method="getConnection"/>
id:获取Bean的标识
class:静态工厂类名
factory-method:用来获取对象的静态方法
2、配置实例工厂
实例工厂需要在容器中定义两个Bean:一个是工厂类对象,一个是工厂类生产的对象。
然后通过属性,指定工厂类和被生产对象的关系。
1)提供如下工厂类
public class InstanceFactory {
private String driver;
private String url;
private String user;
private String password;
public Connection getConnection() {
try {
Class.forName(driver);
Connection conn = DriverManager.getConnection(url,user,password);
return conn;
} catch (Exception e) {
return null;
}
}
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "ConnectionFactory [driver=" + driver + ", url=" + url
+ ", user=" + user + ", password=" + password + "]";
}
}
2)容器配置文件
<!-- 实例工厂 -->
<bean id="factory2" class="com.briup.spring.day02.instance_factory.InstanceFactory">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/briup"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean>
<!-- 工厂生产的对象 -->
<bean id="conn" factory-bean="factory2" factory-method="getConnection">
</bean>
上述两种工厂类都可以配置到IoC容器中,使用获取Bean的方式来获取工厂类生产的对象。
但是,配置过程较为繁琐。实际上,Spring中为我们已经提供了更简单的工厂配置方式,
按照该方式定义工厂,可以简化配置。
3、Spring提供的工厂接口
Spring中提供了一个工厂接口org.springframework.beans.factory.FactoryBean
,我们可以编写工厂类并继承该接口,实现接口中的三个方法。使用这种方式配置工厂类,我们只需要在容器配置文件中通过最简单的定义Bean的方式,定义工厂类对象即可。但是在测试代码中,根据该Bean的名字获取到的则是工厂类生产的对象。
定义如下工厂类,实现FactoryBean接口:
public class SpringFactory implements FactoryBean<Connection> {
private String driver;
private String url;
private String user;
private String password;
@Override
public Connection getObject() throws Exception {
Connection conn = null;
try {
Class.forName(driver);
conn = DriverManager.getConnection(url, user, password);
} catch (Exception e) {
e.printStackTrace();
}
return conn;
}
@Override
public Class getObjectType() {
return Connection.class;
}
@Override
public boolean isSingleton() {
return false;
}
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
接口用来定义程序标准,实现接口的好处是统一了工厂类生产对象所使用的方法。这样就不需要我们额外配置,Spring就知道当我们需要生产一个对象时,应该调用该类中的什么方法了。正所谓“约定优于配置”。
配置文件中,就只需要像往常一样,配置一个bean以及它的id和class即可。根据这个id获取到的,就直接是该工厂调用getObject()
方法返回的对象。
<!-- 实例工厂(实现接口) -->
<bean id="factory3" class="com.briup.spring.day02.spring_factory.SpringFactory">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/briup"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean>
七、在容器配置文件中引用外部资源文件
我们可以像之前在MyBatis的配置文件中那样处理,将数据库四要素单独放到外部的.properties配置文件中,在容器配置文件中引用这个.properties,就能够使用EL表达式的方式定义属性值。
<!-- 读取properties文件的组件 -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="db.properties"/>
</bean>
八、属性编辑器PropertyEditor
该组件的作用是可以将字符串转化为对象,注入到某个Bean中。
例如,有如下Pojo:
public class Person {
private String name; // 姓名
private int age; // 年龄
private String gender; // 性别
private Address address; // 联系地址
}
public class Address {
private String country;
private String city;
private String street;
}
如果按照之前的方式,我们需要在容器中定义两个bean,然后将他们关联起来:
<bean id="addr" class="com.briup.spring.day02.property_editor.Address">
<property name="country" value="中国"/>
<property name="city" value="昆山"/>
<property name="street" value="祖冲之路"/>
</bean>
<bean id="person" class="com.briup.spring.day02.property_editor.Person">
<property name="name" value="张三"/>
<property name="age" value="20"/>
<property name="gender" value="男"/>
<property name="address" ref="addr"/>
</bean>
我们之前通过元素来实现set注入,为对象装配属性值。
对于简单类型的属性值(例如基本数据类型或String字符串),都可以用value值属性直接表示。
但是,如果遇到复杂数据类型(引用类型),则需要另外定义一个bean,再通过ref引用。
但是,通过观察我们会发现,Address中虽然有三个属性,但是都是String类型,比较好描述,也没有形成复杂的嵌套。所以,在这里我们可以简化Address的注入方式,让它通过一个特殊的转换器,也能够使用value属性值直接定义。
这个转换器就是PropertyEditor,属性编辑器。
JDK中提供了java.beans.PropertyEditor接口,用于将xml文件中字符串转换为特定类型的对象。
同时JDK为我们提供一个实现类java.beans.PropertyEditorSupport。
我们需要编写一个自定义属性编辑器,继承该类:
public class MyEditor extends PropertyEditorSupport{
@Override
public void setAsText(String text) throws IllegalArgumentException {
String[] strs = text.split("-");
Address address = new Address();
address.setCountry(strs[0]);
address.setCity(strs[1]);
address.setStreet(strs[2]);
setValue(address);
}
}
Spring在读取xml注入bean对象属性时,如果遇到类型不一致的情况,则会去调用相应的属性编辑器进行转换。
首先,Spring会调用属性编辑器的setAsText(String str)方法,将xml中的字符串传入。
我们自己编写代码,将str转换为对应类型的对象之后,再调用setValue()方法将其设入bean属性中即可。
接下来,我们只需要在容器中定义一个bean即可,就是Person对象。而它所需要的address,我们只需要通过一个value属性声明即可:
<bean id="person" class="com.briup.spring.day02.property_editor.Person">
<property name="name" value="张三"/>
<property name="age" value="20"/>
<property name="gender" value="男"/>
<property name="address" value="中国-昆山-祖冲之路"/>
</bean>
最重要的一点,容器中配置自定义属性编辑器,用来指定需要被自定义编辑的属性字段与编辑器类的关系:
<!-- 该组件用来定义所有类和属性编辑器的对应关系 -->
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="com.briup.spring.day02.Address"
value="com.briup.spring.day02.property_editor.AddressEditor" />
</map>
</property>
</bean>
九、Spring中的注解
1、@Autowired/@Resource实现自动注入
这两个注解用来代替bean容器xml配置文件中的autowire属性,实现引用类型属性的自动装配。
提供pojo如下:
public class Person {
private String name; // 姓名
private int age; // 年龄
private String gender; // 性别
@Autowired // 该注解代表,当从容器中取出一个Person时,会自动按照规则寻找一个bean为address赋值
private Address address; // 联系地址
// getters&setters...
}
public class Address {
private String country;
private String city;
private String street;
// getters&setters...
}
annotation.xml配置文件信息如下:
<!-- 该标签代表开启注解配置方式 -->
<context:annotation-config/>
<bean id="person" class="com.briup.spring.day02.annotation.Person">
<!-- 这里只需要正常为基本属性赋值即可,address属性不用声明 -->
<property name="name" value="张三"/>
<property name="age" value="20"/>
<property name="gender" value="男"/>
</bean>
<bean id="addr" class="com.briup.spring.day02.annotation.Address">
<property name="country" value="中国"></property>
<property name="city" value="昆山"></property>
<property name="street" value="学院路"></property>
</bean>
测试代码:
@Test
public void test1() {
String path = "annotation.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(path);
// 打印输出可以看到,容器中id为address的bean被注入到了id为person的bean中
System.out.println(context.getBean("person"));
}
输出结果为:
>> Console输出:
Person [name=张三, age=20, gender=男, address=Address [country=中国, city=昆山, street=学院路]]
注解使用的注意事项:
1)@Autowired使用后需要在xml文件加入以下配置才能生效:<context:annotation-config/>
2)@Autowired注解可以写在成员变量、setter方法、构造器函数上面
3)@Autowired默认按照byType匹配的方式进行注入,如果没有一个bean的类型是匹配的则会抛异常,如果有多个bean的类型都匹配成功了,那么再按byName方式进行选择。
4)@Autowired如果最终匹配不成功(注意:一定是一个都没有找到的情况)则会抛出异常。但是如果设置为 @Autowired(required=false),则最终匹配不成功没有不会抛出异常。
5)@Autowired可以结合@Qualifier(“beanName”)来使用,则可以达到byName的效果
了解@Resource注解
@Resource和**@Autowired**功能相同,用法相同,
不同点:Autowired默认是先根据Type进行匹配,而Resource先根据Name进行匹配。
注意事项:
1)@Resource使用后需要在xml文件加入以下配置才能生效:<context:annotation-config/>
2)@Resource的作用和@Autowired差不多,只不过 @Resource是默认先用byName,如果找不到合适的就再用byType来注入
3)@Resource有两个属性:name和type
如果使用name属性则表示要byName匹配,使用type属性则表示要byType匹配:
@Resource(“name”) // byName的方式
@Resource(“type”) // byType == @Autowired
2、Component注解
Component注解作用在某个类上,代表将该类的实例纳入到容器中进行管理。
相当于我们在xml中定义了一个。
@Component和上面的@Autowired或@Resource结合使用可以极大程度地减少xml配置文件的内容。
注意事项:
1)@Component注解可以直接定义bean,而无需在xml定义。
但是若两种定义同时存在,xml中的定义会覆盖类中注解的Bean定义。
2)@Component注解直接写在类上面即可。
3)@Component有一个可选的参数,用于指定bean的名称。
4)@Component如果不指定参数,则bean的名称为当前类的类名小写。
5)@Component使用之后需要在xml文件配置一个标签开启扫描注解功能:
<context:component-scan base-package="" />
6)<context:component-scan base-package="com.briup.day02.annotation" />
base-package属性用来定义哪些包下的类需要被扫描。
注意:扫描范围是该包下的所有级别路径(包括子包)。
7)@Component定义的bean默认情况下都是单例模式的,如果要让这个bean变为非单例,可以再结合这个@Scope注解来达到目标@Scope(“prototype”)
8)结合SpringMVC
@Component是Spring中所有bean组件的通用形式,@Repository 、@Service、@Controller则是 @Component的细化,用来表示更具体的组件用途(主要为了提高语义性),分别对应了持久化层、服务层和表现层。但是至少到现在为止这个四种注解的实质区别很小(甚至几乎没有),作用都是把当前类注册为spring容器中的一个bean。
3、观察声明周期注解
Spring中定义了两个注解用来观察bean的声明周期:
@PostConstruct:bean被实例化后调用
@PreDestroy:bean被销毁前调用
IoC容器的作用
1、帮我们管理对象的生命周期(什么时候创建、什么时候销毁)
2、帮我们管理对象之间的关联关系