[学习笔记] Spring起步-IoC

3 篇文章 0 订阅

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方法一样

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值