Spring起步-IoC
刚开始学spring时最困惑我的一点是: spring到底是个啥?
查资料说spring就是一个容器, 那容器又是啥?
容器都是用来装东西的, spring容器就是用来装JavaBean的
之前开发, 需要一个对象时我们是直接new出来, 现在有了spring容器, 需要对象时从容器中取出来即可(调用其getBean()
方法)
控制反转
之前开发
之前开发中, 创建对象时直接new一个对象即可, 创建对象的功能和责任在开发者自己手里
需要被创建的对象:
package _01_start;
public class First_spring {
private String username;
public void setUsername(String username) {
this.username = username;
}
public void printName() {
System.out.println("username: " + username);
}
}
测试代码:
调用者手动创建对象, 和创建对象依赖的对象, 并组装依赖关系
package _01_start;
import org.junit.Test;
public class First_spring_test {
@Test
public void demo01() throws Exception {
First_spring fs = new First_spring();
fs.setUsername("Fighter");
fs.printName();//输出"Fighter"
}
}
spring开发
以上代码创建对象时是直接new出来, 而在使用spring之后, 将由spring创建对象实例, 之后需要实例对象时, 从spring工厂(容器)中获得, 需要将类的全限定名称配置到xml文件中.
将创建对象的功能和责任交由Spring容器, 就叫控制反转(IoC)
需要拷贝的jar包:
spring-beans
spring-core
- 报错后再添加
com.springsource.org.apache.commons.logging
拷贝jar包并buildpath后需要以某种方式告诉spring让其管理我们的First_spring
类, 所以需要添加配置文件
在src同级目录下新建resources目录(与src目录下的文件一同编译), 新建applicationContext.xml文件
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">
<!-- 配置service
<bean>配置需要创建的对象
id用于从容器中获得实例
class:需要创建实例的全限定名称
-->
<bean id="firstSpringId" class="_01_before.First_spring">
<!-- 对应First-spring类中的setUserame方法 -->
<property name="username" value="Fighter" />
</bean>
</beans>
- bean元素的属性:
- id: 用于从容器中获取对象实例
- class: 类的全限定名称
- property子元素: 用于给对象设置属性, 相当于执行setter方法
- name: 属性名
- value: 属性值
测试代码:
package _01_start;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
public class First_spring_test {
//之前开发
@Test
public void demo01() throws Exception {
First_spring fs = null;
//new出一个对象
fs = new First_spring();
//设置属性
fs.setUsername("Fighter");
fs.printName();//输出"Fighter"
}
//spring开发
@Test
public void demo02() throws Exception {
First_spring fs = null;
//从spring容器中获取指定名称的对象
//1. 从classpath路径寻找配置文件, 创建资源对象
Resource resource = new ClassPathResource("applicationContext.xml");
//2. 根据资源对象, 创建Spring IoC容器
BeanFactory factory = new XmlBeanFactory(resource);
//3. 从容器中获取对象
fs = (First_spring) factory.getBean("firstSpringId");
fs.printName();
}
}
解析
- 什么是BeanFactroy:
BeanFactory是Spring最古老的一个接口, 表示Spring IoC容器, 即生产bean的工厂, 负责配置, 创建和管理bean
-
被IoC容器管理的对象称为bean
-
Spring IoC容器如何知道哪些是它需要管理的对象?
使用配置文件, Spring IoC容器通过读取配置文件中的配置元数据, 通过元数据对应用中的各个对象进行实例化及装配
Spring IoC管理bean的原理:
1. 通过Resourc对象加载配置文件
2. 解析配置文件, 得到指定名称的bean
3. 解析bean元素, id属性作为bean的名字, class属性用于反射得到bean实例(因此bean必须存在一个无参构造器)
4. 调用getBean方法时, 从容器中返回对象实例
模拟Spring创建对象:
//模仿spring原理
@Test
public void springMock() throws Exception {
First_spring fs = null;
Class cls = Class.forName("_01_before.First_spring");
Object obj = cls.newInstance();
BeanInfo beanInfo = Introspector.getBeanInfo(cls);
PropertyDescriptor[] pdsDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pdsDescriptors) {
String propertyName = pd.getName();
if("username".equals(propertyName)){
pd.getWriteMethod().invoke(obj, "Fighter");
}
}
fs = (First_spring) obj;
fs.printName();
}
结论: 其实就是把代码从java文件转移到了XML文件中
getBean方法的三种签名(getBean方法重载)
- 根据bean对象在容器中的名称来获取:
Object getBean(String beanName)
之前的案例就是使用的这个方法
@Test
public void demo02() throws Exception {
First_spring fs = null;
Resource resource = new ClassPathResource("applicationContext.xml");
BeanFactory factory = new XmlBeanFactory(resource);
//根据名称获取bean
fs = (First_spring) factory.getBean("firstSpringId");
fs.printName();
}
因此, 如果applicationContext.xml文件中多个bean元素的id属性相同就会报错
- 通过指定的类型寻找bean对象:
<T> T getBean(Class<T> requiredType)
@Test
public void demo02() throws Exception {
First_spring fs = null;
Resource resource = new ClassPathResource("applicationContext.xml");
BeanFactory factory = new XmlBeanFactory(resource);
//根据名称获取bean
//fs = (First_spring) factory.getBean("firstSpringId");
//根据类型获取bean
fs = factory.getBean(First_spring.class);
fs.printName();
}
因为是根据类型获取bean, 所以不需要强转(泛型)
但是如果xml文件中同一个类, 配置有多个bean元素, 即使id不同依然会报错
- 按照名字和类型获取(前面两种方式结合), 推荐使用这种方式
@Test
public void demo02() throws Exception {
First_spring fs = null;
Resource resource = new ClassPathResource("applicationContext.xml");
BeanFactory factory = new XmlBeanFactory(resource);
//根据名称获取bean
//fs = (First_spring) factory.getBean("firstSpringId");
//根据类型获取bean
//fs = factory.getBean(First_spring.class);
//通过名字和类型获取
fs = factory.getBean("firstSpringId", First_spring.class);
fs.printName();
}
spring单元测试
为什么使用单元测试
以前我们使用的是junit单元测试, 运行没有问题, 但存在一些缺点, 先来看一下之前的测试代码
@Test
public void demo02() throws Exception {
First_spring fs = null;
Resource resource = new ClassPathResource("applicationContext.xml");
BeanFactory factory = new XmlBeanFactory(resource);
fs = factory.getBean("firstSpringId", First_spring.class);
fs.printName();
}
可以看到, 以上测试代码是测试代码在管理IoC容器, 即每做一次新的测试都会创建新的IoC容器, 这对性能开销很大
层次关系:
测试代码:
- Spring IoC容器
- bean1
- bean2
解决: 不应该是测试代码管理spring容器, 而应该是spring容器管理测试代码
层次关系
Spring IoC容器:
- bean1
- bean2
- 测试代码
spring单元测试
依赖的jar包:
spring-test
spring-context
spring-aop
spring-expression
需要被测试的代码:
package _02_springTest;
public class SomeBean {
public void doWork() {
System.out.println("SomeBean.doWork()");
}
}
同级目录下新建被测试类的配置文件:
<?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="someBean" class="_02_springTest.SomeBean" />
</beans>
同级目录下新建测试类:
package _02_springTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
//springTest测试案例
//运行spring的junit4
@RunWith(SpringJUnit4ClassRunner.class)
//上下文配置对象, 用于寻找配置文件
@ContextConfiguration("classpath:_02_springTest/spring-test.xml")
public class springTestTest {
//表示自动按照类型去spring容器中找到bean对象, 并注入给该字段
@Autowired
private SomeBean someBean;
@Test
public void testIoC() throws Exception {
someBean.doWork();
}
}
- 以前单元测试是创建spring容器对象, 再从容器对象中获取bean对象, 再使用bean
@RunWith
注解: 表示要运行spring的junit4驱动@ContextConfiguration
注解: 用于寻找配置文件(找文件最好加上classpath, 表示从项目根路径寻找), 找到后才能加载配置文件, 类比上一个案例测试时加载配置文件@AutoWired
注解: 表示自动按照类型去spring容器中找到bean对象, 并设置给该字段. 类比上面getBean方法三种签名的第二种(通过指定的类型寻找bean对象:<T> T getBean(Class<T> requiredType)
), 好比spring容器中有多个bean元素, AutoWired会根据对象类型自动找到我们需要的bean
ApplicationContext接口
之前开发使用的是BeanFactory接口, 该接口是Spring最底层的一个接口, 开发一般不会使用, 而是使用ApplicationContext接口, 该接口是BeanFactory的子接口. 下面两个案例展示了二者区别
BeanFactory
需要被创建的bean:
package _03_container;
public class SomeBean {
public SomeBean() {
System.out.println("Constructor");
}
}
同级目录下的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="someBean" class="_03_container.SomeBean" />
</beans>
测试代码:
package _03_container;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
//springTest测试案例
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:_02_springTest/spring-test.xml")
public class App {
//使用BeanFactory
@Test
public void testBeanFactrory() throws Exception {
Resource resource = new ClassPathResource("_03_container/container.xml");
BeanFactory factory = new XmlBeanFactory(resource);
SomeBean someBean = factory.getBean("someBean", SomeBean.class);
System.out.println(someBean);
}
}
注: 编写时可以采用倒推的形式, 先写BeanFactroy factroy = null; SomeBean someBean = (SomeBean) factory.getBean("someBean", SomeBean.class);
, 先从工厂(factory)中获取bean, 再创建factory
ApplicationContet
ApplicationContext是BeanFactory的子接口, 所以一样拥有getBean方法. 而且BeanFactory是先通过路径加载配置文件, 再创建工厂对象, 而ApplicationContext是直接根据路径创建工厂
测试代码:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:_02_springTest/spring-test.xml")
public class App {
//使用BeanFactory
@Test
public void testBeanFactrory() throws Exception {
Resource resource = new ClassPathResource("_03_container/container.xml");
BeanFactory factory = new XmlBeanFactory(resource);
SomeBean someBean = factory.getBean("someBean", SomeBean.class);
System.out.println(someBean);
}
//使用ApplicationContext
@Test
public void testApplicationContext() throws Exception {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("_03_container/container.xml");
SomeBean someBean = applicationContext.getBean("someBean", SomeBean.class);
System.out.println(someBean);
}
}
分析
以上代码不存在任何问题, 但有些地方值得我们深入研究一下
为了分析bean对象是什么时候创建, 我们在getBean方法前输出一条分割线
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:_03_springTest/spring-test.xml")
public class App {
//使用BeanFactory
@Test
public void testBeanFactrory() throws Exception {
Resource resource = new ClassPathResource("_03_container/container.xml");
BeanFactory factory = new XmlBeanFactory(resource);
System.out.println("----------------------------");
SomeBean someBean = factory.getBean("someBean", SomeBean.class);
System.out.println(someBean);
}
//使用ApplicationContext
@Test
public void testApplicationContext() throws Exception {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("_03_container/container.xml");
System.out.println("----------------------------");
SomeBean someBean = applicationContext.getBean("someBean", SomeBean.class);
System.out.println(someBean);
}
}
输出结果:
-
testBeanFactory:
---------------------------- Constructor _03_container.SomeBean@1e67a849
-
testApplicationContext:
Constructor ---------------------------- _03_container.SomeBean@ca263c2
分析:
我们发现分割线输出的位置不一样, BeanFactory是在创建bean之前输出(即在调用getBean时才创建bean对象), 而ApplicationContext是在创建bean之后输出(即在创建工厂时就创建好了bean对象)
结论:
- BeanFactory有延迟初始化的特点, 在创建spring容器的时候不会立刻创建容器管理的bean对象, 而是要等到从容器中获取对象的时候才创建对象(如果将getBean方法注释起来会发现构造器不会执行, 即不会创建对象), 类似懒加载.
- ApplicationContext在创建容器的时候就会把容器中的bean初始化, 而不会等到调用getBean时才创建对象, 类似饿加载
bean实例化方式
1. 构造器实例化(重要)
这种实例化方式要求bean有无参构造器, 是最标准, 使用最多的实例化方法, 之前案例中的实例化方法就属于构造器实例化
这里再演示一次
需要被创建的bean:
package _04_createBean._01_constructor;
public class Cat1 {
public Cat1() {
System.out.println("Cat1.Cat1()");
}
}
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="cat1" class="_04_createBean._01_constructor.Cat1" />
</beans>
测试代码:
package _04_createBean;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import _04_createBean._01_constructor.Cat1;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:_04_createBean/AppTest.xml")
public class App {
@Autowired
private Cat1 c1;
@Test
public void test() throws Exception {
System.out.println(c1);
}
}
可以看到构造器被执行(输出"Cat1.Cat1()"), 并输出了对象的哈希值
注: 必须保证该类有一个无参构造器(底层采用反射创建对象)
2. 静态工厂方法实例化
创建工厂类, 在工厂类中定义静态方法, 方法返回我们需要创建的对象
需要创建的bean:
package _04_createBean._02_static_factory;
public class Cat2 {
}
同级目录下新建工厂类, 工厂类中有一个返回Cat2的静态方法:
package _04_createBean._02_static_factory;
//Cat2的工厂
public class Cat2Factroy {
public static Cat2 createInstance(){
return new Cat2();
}
}
测试代码:
@Test
public void testStaticFactory() throws Exception {
//之前开发
Cat2 c2_01 = new Cat2();
//工厂开发
Cat2 c2_02 = Cat2Factroy.createInstance();
}
如果需要创建Cat2对象, 则需要知道工厂类名以及静态方法. 那如果我们需要spring帮我们创建Cat2对象, 则需要告诉spring工厂类的类名以及工厂类中的静态方法
配置文件:
<?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="cat1" class="_04_createBean._01_constructor.Cat1" />
<bean id="cat2" class="_04_createBean._02_static_factory.Cat2Factroy" factory-method="createInstance" />
</beans>
- class属性: 工厂类的全限定类名
- factory-method属性: 静态方法的方法名
测试代码:
public class App {
@Autowired
private Cat2 c2_03;
@Test
public void testStaticFactory() throws Exception {
//之前开发
Cat2 c2_01 = new Cat2();
//工厂开发
Cat2 c2_02 = Cat2Factroy.createInstance();
//spring静态工厂
System.out.println(c2_03);
}
}
这就是静态工厂方法实例化
3. 实例工厂方法实例化
既然方法2是静态方法, 那去掉static修饰就变成了实例工厂方法
需要被创建的bean:
package _04_createBean._03_instance_factory;
public class Cat3{
}
工厂类, 注意没有static修饰:
package _04_createBean._03_instance_factory;
//Cat3的工厂
public class Cat3Factroy {
public Cat3 createInstance(){
return new Cat3();
}
}
测试代码:
@Test
public void testInstanceFactory() throws Exception {
Cat3 c_01 = new Cat3Factroy().createInstance();
}
因为没有static修饰所以不能直接类名.方法名()
调用, 而是需要先创建工厂对象, 再创建bean
而如果让spring帮我们管理, 就需要在spring先创建工厂对象, 再创建bean, 所以一样需要告诉spring工厂类和方法名
配置文件:
<bean id="cat3factory" class="_04_createBean._03_instance_factory.Cat3Factroy" />
<bean id="cat3" factory-bean="cat3factory" factory-method="createInstance"/>
4. 实现FactoryBean接口实例化(重要)
根据方法3思考: 如果我们所有获取bean的方法都同名(比如, 都叫getObject
), 那是不是可以省略factory-method
属性?
创建Cat4Factory工厂类, 实现FactoryBean接口:
public class Cat4Factory implements FactoryBean<Cat4>{
public Cat4 getObject() throws Exception {
return new Cat4();
}
public Class<?> getObjectType() {
return Cat4.class;
}
}
这时, xml文件中可以不写factory-method属性:
<bean id="cat4" class="_04_createBean._04_factory_bean.Cat4Factory"/>
注意: 这里看似和方法1(通过构造器)很像, 但方法1是通过类的全限定名称直接获取bean, 而这个方法是通过工厂类的全限定名称获得工厂, 再调用工厂的getObject方法返回我们需要的bean
这种方法是实例工厂的变种, 要求工厂类实现FactoryBean
接口
Bean作用域
在spring容器中是指其创建的Bean对象相对于其它bean对象的请求可见范围, 语法格式是<bean id="" class="" scpoe="" />
, 常用的可选值是singleton
(单例, 默认值)和prototype
(多例)
singelton演示
bean代码:
package _05_scope;
public class Bean_Singleton {
public Bean_Singleton(){
System.out.println("Bean_Singleton Constructor");
}
}
配置文件:
<?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="bean_Singleton" class="_05_scope.Bean_Singleton" scope="singleton" />
</beans>
测试代码:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:_05_scope/AppTest.xml")
public class App {
@Autowired
private Bean_Singleton bean_Singleton1;
@Autowired
private Bean_Singleton bean_Singleton2;
@Test
public void testSingleton() throws Exception {
System.out.println(bean_Singleton1);
System.out.println(bean_Singleton2);
}
}
运行结果: 构造器执行一次, 且两次输出的对象哈希值是相同的
prototype演示
bean:
package _05_scope;
public class Bean_Prototype {
public Bean_Prototype(){
System.out.println("Bean_Prototype");
}
}
配置文件:
<bean id="bean_Prototype" class="_05_scope.Bean_Prototype" scope="prototype" />
测试代码:
@Autowired
private Bean_Prototype bean_Prototype1;
@Autowired
private Bean_Prototype bean_Prototype2;
@Test
public void testPrototype() throws Exception {
System.out.println(bean_Prototype1);
System.out.println(bean_Prototype2);
}
运行结果: 构造器执行两次, 且两次输出的对象哈希值是不同的
Bean初始化和销毁
类比DataSource, SqlSessionFactroy, 最终都需要关闭资源, 下面案例手动模拟对象初始化和扫尾操作
bean:
package _06_lifecycle;
public class MyDataSource {
public MyDataSource(){
System.out.println("MyDataSource Constructor");
}
public void open(){
System.out.println("MyDataSource.open()");
}
public void doWork(){
System.out.println("MyDataSource.doWork()");
}
public void close(){
System.out.println("MyDataSource.close()");
}
}
测试代码, 在创建对象后执行初始化操作, 销毁之前执行扫尾操作:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:_05_scope/AppTest.xml")
public class App {
@Test
public void testOldWay() throws Exception {
//创建对象
MyDataSource myds = new MyDataSource();
//对对象做初始化操作
myds.open();
myds.doWork();
//在销毁之前做扫尾操作
myds.close();
}
}
如果我们要spring框架帮我们管理bean, 除了需要像往常一样配置id属性和class属性, 还需要告诉spring初始化方法和销毁方法, 在spring中分别对应init-method
属性和destroy-method
属性
配置文件:
<bean id="bean_Singleton" class="_06_lifecycle.MyDataSource" init-method="open" destroy-method="close" />
测试代码:
@Autowired
private MyDataSource ds;
@Test
public void testSpringWay() throws Exception {
ds.doWork();
}
运行结果和我们手动执行open, close方法一样