作者:狮子也疯狂
专栏:《spring开发》
坚持做好每一步,幸运之神自然会驾凌在你的身上
Spring框架是狮子入坑Java的第一个开源框架。当我们接触到它时,总会发现老师或者书本介绍这两个词汇——IOC和AOP,它们分别是控制反转和面向切面,是Spring的思想内核,提供了控制层SpringMVC、数据层SpringData、服务层事务管理等众多技术,并可以整合众多第三方框架。现在我们通过自己手写一个简单的模板来深入地认识一下IOC。
IOC(Inversion of Control) :程序将创建对象的权利交给框架,框架会帮助我们创建对象,分配对象的使用,控制权由程序代码转移到了框架中,控制权发生了反转,这就是Spring的IOC思想。
我们之前在开发过程中,对象实例的创建是由调用者直接管理的,在每个层,需要用到某个对象时,都需要重新实现。我们通过一个简单的例子,来搞清楚这个方法的弊端:
public interface StudentDao { // 根据id查询学生 Student findById(int id); } public class StudentDaoImpl implements StudentDao{ @Override public Student findById(int id) { // 模拟从数据库查找出学生 return new Student(1,"JackieYe","茂名"); } } public class StudentService { public Student findStudentById(int id){ // 此处就是调用者在创建对象 StudentDao studentDao = new StudentDaoImpl(); return studentDao.findById(1); } }
这样的写法,缺点有二:
- 浪费资源:StudentService调用方法时即会创建一个对象,如果不断调用 方法则会创建大量StudentDao对象。
- 代码耦合度高:假设随着开发,我们创建了StudentDao另一个更加完善的实现类StudentDaoImpl2,如果在StudentService中想使StudentDaoImpl2,则必须修改源码。
现在我们来通过一段手写代码模拟一下spring的IOC思想。过程如下:
- 创建一个集合容器。
- 创建对象,然后放到容器中。
- 从容器中获取对象。
在此之前,我们先准备一点数据,我们在上面的例子的基础上,再添加一个StudentDao实例:
public interface StudentDao { // 根据id查询学生 Student findById(int id); } public class StudentDaoImpl implements StudentDao{ @Override public Student findById(int id) { // 模拟从数据库查找出学生 return new Student(1,"JackieYe","茂名"); } } //重新实例化一个,数据一样,再在控制台输出一句话。 public class StudentDaoImpl2 implements StudentDao{ @Override public Student findById(int id) { // 模拟根据id查询学生 System.out.println("新方法!!!"); return new Student(1,"JackieYe","茂名"); } }
创建配置文件bean.properties,该文件中定义管理的对象。
前面我们添加了一个StudentDaoImpl2,如果需要调用它,则只需要更改该配置文件的名字,并不需要更改源代码。
studentDao=com.jackie.dao.StudentDaoImpl
创建容器管理类,该类在类加载时读取配置文件,将配置文件中配置的对象全部创建并放入容器中。代码如下:
public class Container { static Mapmap = new HashMap(); static { // 读取配置文件 InputStream is = Container .class.getClassLoader() .getResourceAsStream("bean.properties"); Properties properties = new Properties(); try { properties.load(is); }catch(IOException e) { e.printStackTrace(); } // 遍历配置文件的所有配置 Enumeration
我们创建了一个名为Container的对象,并且通过反射的方式将其读入了输入流,然后遍历该配置文件的配置,将其放入一个枚举类keys里面,然后遍历该keys,在循环里创建对象实例,放到map集合里。
创建Dao对象的调用者StudentService。
public class StudentService { public Student findStudentById(int id){ // 从容器中获取对象 StudentDao studentDao = (StudentDao)Container.getBean("studentDao"); System.out.println(studentDao.hashCode()); return studentDao.findById(id); } }
测试代码如下:
public class Test { public static void main(String[] args){ StudentService studentService = new StudentService(); System.out.println(studentService.findStudentById(1)); System.out.println(studentService.findStudentById(1)); } }
我们来总结一下控制反转是如何来解决上面我们用原生技术探索出来的两个弊端的:
- 我们会发现,无论调用多少次,这个对象的hashcode都是一样的,节约了资源。(如果是原生方法创建,hashcode肯定会变化)
- 如果我们需要用到StudentDaoImpl2对象,只需要修改bean.properties的内容为
studentDao=com.jackie.dao.StudentDaoImpl2即可,无需修改源代码。
添加如下依赖:
org.springframework spring-context 5.3.13 junit junit 4.12 test
这里我们还是使用第一个例子来说明,方便小伙伴对比。
编写xml配置文件,配置文件中配置需要Spring帮我们创建的对象。
测试从Spring容器中获取对象
public class TestContainer { @Test public void t1(){ // 创建Spring容器 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); // 从容器获取对象 StudentDao studentDao1 = (StudentDao) ac.getBean("studentDao"); StudentDao studentDao2 = (StudentDao) ac.getBean("studentDao"); System.out.println(studentDao1.hashCode()); System.out.println(studentDao2.hashCode()); System.out.println(studentDao1.findById(1)); } }
我们通过控制台第一行和第二行的结果会发现,俩个hashcode值是一样的。说明通过spring容器获取的对象也是和前面自定义容器一样,不会再次实例化。
Spring的接口以及实现类型如上,红圈标注的类是我们常用的五个容器类型。我们来探究一下这个五个类的作用。
BeanFactory:BeanFactory是Spring容器中的顶层接口,它可以对Bean对象进行管理。
ApplicationContext:ApplicationContext是BeanFactory的子接口。它除了继承 BeanFactory的所有功能外,还添加了对国际化、资源访问、事件传播等方面的良好支持。
ApplicationContext有以下三个常用实现类:
- ClassPathXmlApplicationContext:该类可以从项目中读取配置文件
- FileSystemXmlApplicationContext:该类从磁盘中读取配置文件
- AnnotationConfigApplicationContext:使用该类不读取配置文件,而是会读取注解
前面我们已经使用过ClassPathXmlApplicationContext类,他就是专门读取项目的xml文件,而FileSystemXmlApplicationContext类则是需要读取硬盘里面的xml文件,所以它需要输入文件的绝对路径。而AnnotationConfigApplicationContext类的使用则简单多,它里面不用写,直接读取注解的配置。这里是介绍IOC的原理,spring容器的创建则不多作阐述。
Bean对象的生命周期包含创建——使用——销毁,Spring可以配置Bean对象在创建和销毁时自动执行的方法,我们用来判断对象的创建以及销毁时间。
配置生命周期以及销毁周期:
依赖注入(Dependency Injection,简称DI),它是Spring控制反转思想的具体实现。
控制反转将对象的创建交给了Spring,但是对象中可能会依赖其他对象。比如service类中要有dao类的属性,我们称service依赖于dao。之前需要手动注入属性值,代码如下(还是以第一个例子为基础):
public class StudentService { // service依赖dao,手动注入属性值,即手动维护依赖关系 private StudentDao studentDao = new StudentDaoImpl(); public Student findStudentById(int id){ return studentDao.findById(id); } }
此时,当StudentService的想要使用StudentDao的另一个实现类如StudentDaoImpl2时,则需要修改Java源码,造成代码的可维护性降低。
而使用Spring框架后,Spring管理Service对象与Dao对象,此时它能够为Service对象注入依赖的Dao属性值。这就是Spring的依赖注入。简单来说,控制反转是创建对象,依赖注入是为对象的属性赋
值。
spring一共有三种方式注入,分别是Setter注入,构造方法注入和自动注入。
1.被注入类编写属性的setter方法
public class StudentService { private StudentDao studentDao; public void setStudentDao(StudentDao studentDao) { this.studentDao = studentDao; } }
2.配置文件中,给需要注入属性值的 bean 中设置 property
@Test public void t2(){ ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); StudentService studentService = (StudentService)ac.getBean("studentService"); System.out.println(studentService.findStudentById(1)); }
public class StudentService { private StudentDao studentDao; public StudentService(StudentDao studentDao) { this.studentDao = studentDao; } }
同上一个测试方法。
自动注入不需要在 bean标签中添加其他标签注入属性值,而是自动从容器中找到相应的bean对象设置为属性值。
tips:
自动注入有两种配置方式:
- 全局配置:在 中设置 default-autowire 属性可以定义所有bean对象的自动注入策略。
- 局部配置:在 中设置 autowire 属性可以定义当前bean对象的自动注入策略
autowire的取值如下:
- no:不会进行自动注入。 default:全局配置default相当于no,局部配置default表示使用全局配置
- byName:在Spring容器中查找id与属性名相同的bean,并进行注入。需要提供set方法。
- byType:在Spring容器中查找类型与属性类型相同的bean,并进行注入。需要提供set方法。
- constructor:在Spring容器中查找id与属性名相同的bean,并进行注入。需要提供构造方法
本文通过对比原生Java技术加载对象与手写模拟IOC技术来加载对象,突出了spring的IOC功能的强大;另外,还介绍了IOC的生命周期以及IOC的依赖注入方法,以及依赖注入的原理。总的来说,spring很强大,但是配置繁琐,我们已经使用springboot来取代了,但是spring是基础框架,该理解还是得去理解,希望这篇文章可以帮到你。😄