IOC和DI解决什么问题?
OCP 开闭原则
OCP 开闭原则是软件七大开发原则当中最基本的一个原则
OCP 开闭原则:
- 在软件开发过程中应当对扩展开放,对修改关闭。
- 在软件开发过程中,通过添加额外的类对原先程序已有的功能进行扩展是没问题的,但是如果因为要进行功能扩展而修改之前运行正常的程序,这是不被允许的。
- 因为一旦修改之前运行正常的程序,那么项目整体就要进行重新测试,这会导致额外的开销和资源消耗,并且测试过程相当麻烦。
OCP 开闭原则是最核心的、最基本的,其他的六个开发原则都是为这个原则服务的。
是否违反 OCP 开闭原则的判断:
- 只要你在扩展系统功能的时候,没有修改以前写好的代码,那么你就是符合 OCP 原则的。
- 反之,如果在扩展系统功能的时候,你修改了之前的代码,那么这个设计是失败的,违背了OCP原则。
DIP依赖倒置原则
依赖倒置原则,就是不再显示的建立类之间的关联关系,而是面向接口编程,面向抽象编程,不要面向具体编程,从而降低程序的耦合度,提高程序的扩展力,增强代码复用性
依赖倒置原则的目的:降低程序的耦合度,提高程序的扩展力,增强代码复用性
是否符合依赖倒置原则:
- 上 不依赖 下,就是符合,即类的实例对象之间没有明确固定在代码中的依赖关系
- 上 依赖 下,就是违背。只要“下”一改动,“上”就受到牵连。
符合依赖倒置原则的编程思想:控制反转
Spring
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
- Spring实现了控制反转思想,Spring框架可以帮你维护对象和对象之间的关系
- Spring实现了面向切面编程
- 由于我们可以将创建对象交给Spring负责,所以Spring也是一个存放对象的容器
IoC控制反转
IoC是面向对象编程中的一种设计思想,可以用来降低代码之间的耦合度,当程序的设计既违背OCP又违背DIP,可以采用IoC这种编程思想来解决这个问题,符合DIP。
控制反转的核心思想:
- 将对象的创建权交出去
- 将对象和对象之间关系的管理权交出去
- 对象的创建和对象之间关系的维护都由第三方容器来负责
控制反转中的反转是两件事:
- 第一件事:不在程序中采用硬编码的方式来new对象了,将new对象的权利交出去
- 第二件事:不在程序中采用硬编码的方式来维护对象的关系了,对象之间关系的维护权交出去了
控制反转思想的实现:依赖注入
DI依赖注入
控制反转的实现方式有多种,其中比较重要的叫做:依赖注入
依赖注入,用于实现对象之间关系的建立
控制反转是思想。依赖注入是这种思想的具体实现。
依赖注入DI,又包括常见的两种方式:
- 第一种:set注入(执行set方法给属性赋值)
- 第二种:构造方法注入(执行构造方法给属性赋值)
依赖注入 中 “依赖”是什么意思? “注入”是什么意思?
- 依赖:A对象和B对象的关系。
- 注入:是一种手段,通过这种手段,可以让A对象和B对象产生关系。
依赖注入:对象A和对象B之间的关系,靠注入的手段来维护。
名词总结
OCP:开闭原则(开发原则)
DIP:依赖倒置原则(开发原则)
IoC:控制反转(一种思想,一种新型的设计模式)
DI:依赖注入(控制反转思想的具体实现方式)
Spring入门程序
定义普通 Java 类
在开发中,普通Java对象,称为 pojo(Plain Old Java Object),或者称为 java bean(咖啡豆),也叫 domain(领域模型)
public class User {
private String name;
private Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
编写 Spring 配置文件
Spring 配置文件放在类根路径下,即 resource 目录下
pring的配置文件的命名任意
编写测试类
public class Test {
@org.junit.Test
public void tset1() {
// 第一步:获取Spring容器对象。
// ApplicationContext 翻译为:应用上下文。其实就是Spring容器。
// ApplicationContext 是一个接口。
// ApplicationContext 接口下有很多实现类。其中有一个实现类叫做:ClassPathXmlApplicationContext
// ClassPathXmlApplicationContext 专门用于从类路径当中加载spring配置文件的一个Spring上下文对象。
// 这行代码只要执行,就相当于启动了Spring容器,同时会解析spring.xml(Spring配置文件名)文件,
// 并且实例化在Spring配置文件中配置的所有的bean对象,创建完成对象后会将其放到spring容器当中。
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
// 第二步:根据bean的id从Spring容器中获取这个对象。(id Bean的唯一标识)
Object userBean = applicationContext.getBean("user");
System.out.println(userBean);
}
}
程序分析
- 在spring的配置文件中,每个bean的id是不能重名,因为id是每个bean的唯一表示,用于通过容器对象获取相应的bean
报错
- spring是通过反射机制调用类的无参数构造方法来创建对象的,所以要想让spring给你创建对象,必须保证无参数构造方法是存在的
public class User {
private String name;
private Integer age;
public User() {
System.out.println("User 无参构造方法执行...");
}
...
}
- spring把创建好的对象存储到一个Map集合当中
bean对象最终存储在spring容器中,在spring源码底层就是一个map集合,存储bean的map在DefaultListableBeanFactory类中
Spring容器加载到Bean类时 , 会把这个类的描述信息, 以包名加类名的方式存到beanDefinitionMap 中,
Map
public class DefaultListableBeanFactory
extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
...
private final Map beanDefinitionMap;
...
}
- spring的配置文件可以有多个,在ClassPathXmlApplicationContext构造方法的参数上传递文件路径即可。且配置文件的文件名、文件路径都是任意的
//ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
//ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml", "beans.xml");
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml", "beans.xml", "xml/beans.xml");
// 源码:
public ClassPathXmlApplicationContext(String... configLocations) throws BeansException {
this(configLocations, true, (ApplicationContext)null);
}
- 在spring配置文件中配置的bean可以是任意类,只要这个类不是抽象的,并且提供了无参数构造方法。
- getBean()方法返回的类型是Object,如果访问子类的特有属性和方法时,还需要向下转型,有其它办法可以解决这个问题吗?
//Object nowTime = applicationContext.getBean("nowTime");
//Date nowTime = (Date) applicationContext.getBean("nowTime");
// 不想强制类型转换,可以使用以下代码(通过第二个参数来指定返回的bean的类型。)
Date nowTime = applicationContext.getBean("nowTime", Date.class);
- ApplicationContext的超级父接口BeanFactory。
BeanFactory是Spring容器的超级接口。
ApplicationContext是BeanFactory的子接口。
//ApplicationContext接口的超级父接口是:BeanFactory
//BeanFactory翻译为Bean工厂,就是能够生产Bean对象的一个工厂对象
//BeanFactory是IoC容器的顶级接口。
//Spring的IoC容器底层实际上使用了:工厂模式。
//Spring底层的IoC是怎么实现的?XML解析 + 工厂模式 + 反射机制
//ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
- Spring底层的IoC容器是通过:XML解析+工厂模式+反射机制实现的。
- spring不是在调用getBean()方法的时候创建对象,执行new ClassPathXmlApplicationContext("spring.xml");加载配置文件的时候,就会实例化对象。
Spring对IoC的实现
IoC
控制反转是一种思想。
控制反转是为了降低程序耦合度,提高程序扩展力,达到OCP原则,达到DIP原则。
控制反转,反转的是什么?
- 将对象的创建权利交出去,交给第三方容器负责。
- 将对象和对象之间关系的维护权交出去,交给第三方容器负责。
控制反转思想的实现:DI(Dependency Injection,依赖注入)
DI
依赖注入实现了控制反转的思想。
Spring通过依赖注入的方式来完成Bean管理的
Bean管理:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。
依赖注入:
- 依赖指的是对象和对象之间的关联关系。
- 注入指的是一种数据传递行为,通过注入行为来让对象和对象产生关系。
依赖注入常见的实现方式包括两种:
- 第一种:set注入
- 第二种:构造注入
set 注入
- set注入基于set方法实现,底层会通过反射机制调用属性对应的set方法然后给属性赋值。
- 这种方式要求属性必须对外提供set方法。
set 注入的使用
public class UserDao {
public void insert(){
log.info("数据库保存用户信息。");
}
}
public class VipDao {
public void insert(){
log.info("数据库保存vip用户信息。");
}
}
public class UserService {
private UserDao userDao;
private VipDao vipDao;
public void setAbc(VipDao vipDao){
this.vipDao = vipDao;
}
// set注入,必须提供一个set方法。
// Spring容器会调用这个set方法,来给userDao属性赋值!!!
// 我自己写一个set方法,不使用IDEA工具生成的。不符合javabean规范。
// 至少这个方法是以set单词开始的。前三个字母不能随便写,必须是“set"
/*public void setMySQLUserDao(UserDao xyz){
this.userDao = xyz;
}*/
// 这个set方法是IDEA工具生成的,符合javabean规范。
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void saveUser(){
// 保存用户信息到数据库
userDao.insert();
vipDao.insert();
}
}
set 注入分析
set 注入的实现原理:
通过property标签获取到属性名:userDao
通过属性名推断出与该属性对应的set方法的方法名:setUserDao
通过反射机制调用setUserDao()方法给属性赋值
一般情况下,set方法的命名符合JavaBean规范,property标签的name属性的属性值就是属性名。
property标签的ref属性的属性值是要注入的bean对象的id,通过ref属性来完成bean的装配,ref作为set方法的参数
总结:set注入的核心实现原理:通过反射机制调用set方法来给属性赋值,让两个对象之间产生关系。
构造注入
通过调用构造方法来给属性赋值。
与set注入相比,构造注入是在创建对象的同时进行注入,进行属性的赋值,而set注入是在对象创建之后。
public class CustomerService {
private UserDao userDao;
private VipDao vipDao;
public CustomerService(UserDao userDao, VipDao vipDao) {
this.userDao = userDao;
this.vipDao = vipDao;
}
public void saveUser(){
userDao.insert();
vipDao.insert();
}
}
通过构造方法进行注入有三种形式:
1.可以通过下标
2.可以通过参数名
3.也可以不指定下标和参数名,可以类型自动推断。
注入
注入外部Bean
外部Bean:要进行注入的bean定义到需要被注入的Bean的外面
对于外部Bean,在property标签中使用ref属性进行注入,或者使用ref标签进行注入,其中通过ref属性进行注入是常用。
注入内部Bean
内部Bean:在bean标签中嵌套bean标签,即需要进行注入的Bean通过Bean标签声明在需要被注入的Bean中,然后直接注入到需要被注入的Bean中。
注入简单类型
给简单类型赋值,即给属性注入简单类型,使用value标签,而不是ref。
public class User {
private String username; // String是简单类型
private String password;
private int age; // int是简单类型
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAge(int age) {
this.age = age;
}
}
Spring 中的简单类型
通过BeanUtils类的源码分析得知,简单类型包括:
- 基本数据类型
- 基本数据类型对应的包装类
- String或其他的CharSequence子类
- Number子类
- Date子类,java.util.Date是简单类型
- Enum子类
- URI
- URL
- Temporal子类,Temporal是Java8提供的时间和时区类型
- Locale,Locale是语言类,也是简单类型。
- Class
- 另外还包括以上简单值类型对应的数组类型。
级联属性赋值
// 班级类
public class Clazz {
// 班级名称
private String name;
public void setName(String name) {
this.name = name;
}
}
public class Student {
private String name;
// 学生属于哪个班级
private Clazz clazz;
public void setClazz(Clazz clazz) {
this.clazz = clazz;
}
// 使用级联属性赋值,这个需要这个get方法。
// 因为级联给学生所在的班级属性赋值会调用学生的getClazz()方法
public Clazz getClazz() {
return clazz;
}
public void setName(String name) {
this.name = name;
}
}
原先的注入方法
级联注入:
注意:级联属性赋值,对于对其他对象引用的成员变量,一定要提供get方法,因为进行级联属性赋值时,需要先调用get方法获取其他对象引用的成员变量,然后再调用该成员变量属性的set方法进行属性赋值
注入数组
数组中的元素是简单类型
public class QianDaYe {
private String[] aiHaos;
public void setAiHaos(String[] aiHaos) {
this.aiHaos = aiHaos;
}
}
抽烟
喝酒
烫头
数组中的元素是非简单类型
public class Woman {
private String name;
public void setName(String name) {
this.name = name;
}
}
public class QianDaYe {
private String[] aiHaos;
// 多个女性朋友
private Woman[] womens;
public void setWomens(Woman[] womens) {
this.womens = womens;
}
public void setAiHaos(String[] aiHaos) {
this.aiHaos = aiHaos;
}
}
抽烟
喝酒
烫头
注入数组小结
如果数组中是简单类型,使用value标签。
如果数组中是非简单类型,使用ref标签。
注入List集合与Set集合
List集合:有序可重复
注入List集合的时候使用标签,如果List集合中是简单类型使用value标签,反之使用ref标签。
Set集合:无序不可重复
注入set集合的时候使用
public class Person {
// 注入List集合
private List names;
// 注入Set集合
private Set addrs;
public void setNames(List names) {
this.names = names;
}
public void setAddrs(Set addrs) {
this.addrs = addrs;
}
}
张三
李四
王五
张三
张三
张三
张三
北京大兴区
北京大兴区
北京海淀区
北京海淀区
北京大兴区
注入Map集合与注入Properties
Map集合:
使用
java.util.Properties继承java.util.Hashtable,所以Properties也是一个Map集合。
Properties使用
在Properties中,key和value的类型都是String类型
public class Person {
// 注入Map集合
// 多个电话
private Map phones;
// 注入属性类对象
// Properties本质上也是一个Map集合。
// Properties的父类Hashtable,Hashtable实现了Map接口。
// 虽然这个也是一个Map集合,但是和Map的注入方式有点像,但是不同。
// Properties的key和value只能是String类型。
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}
public void setPhones(Map phones) {
this.phones = phones;
}
}
com.mysql.cj.jdbc.Driver
jdbc:mysql://localhost:3306/spring6
root
123456
注入null和空字符串
注入空字符串使用:
注入null使用:
public class Cat {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
}
注入的值中含有特殊符号
XML中有5个特殊字符,分别是:<、>、'、"、&
以上5个特殊符号在XML中会被特殊对待,会被当做XML语法的一部分进行解析,如果这些特殊符号直接出现在注入的字符串当中,会报错。
解决方案包括两种:
- 第一种:特殊符号使用转义字符代替。
5个特殊字符对应的转义字符分别是: - 第二种:将含有特殊符号的字符串放到: 当中。放在CDATA区中的数据不会被XML文件解析器解析。使用CDATA时,不能使用value属性,只能使用value标签。
public class MathBean {
private String result;
public void setResult(String result) {
this.result = result;
}
}
命名空间注入
p命名空间注入
目的:简化配置。p命名空间实际上是对set注入的简化。
使用p命名空间注入的前提条件包括两个:
- 第一:在XML头部信息中添加p命名空间的配置信息:xmlns:p="http://www.springframework.org/schema/p"
- 第二:p命名空间注入是基于setter方法的,所以需要对应的属性提供setter方法。
public class Dog {
// 简单类型
private String name;
private int age;
// 非简单类型
private Date birth;
// p命名空间注入底层还是set注入,只不过p命名空间注入可以让spring配置变的更加简单。
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setBirth(Date birth) {
this.birth = birth;
}
}
c命名空间注入
c命名空间是简化构造方法注入的。
使用c命名空间的两个前提条件:
- 第一:需要在xml配置文件头部添加c命名空间的配置信息:xmlns:c="http://www.springframework.org/schema/c"
- 第二:需要提供构造方法。
注意:不管是p命名空间还是c命名空间,注入的时候都可以注入简单类型以及非简单类型。
public class People {
private String name;
private int age;
private boolean sex;
// c命名空间是简化构造注入的。
// c命名空间注入办法是基于构造方法的。
public People(String name, int age, boolean sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
}
util命名空间
使用util命名空间可以让配置复用。
使用util命名空间的前提是:在spring配置文件头部添加配置信息。
public class MyDataSource1 {
// Properties属性类对象,这是一个Map集合,
// key和value都是String类型。
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}
}
package com.powernode.spring6.beans;
import java.util.Properties;
public class MyDataSource2 {
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}
}
com.mysql.cj.jdbc.Driver
jdbc:mysql://localhost:3306/spring6
root
123
基于 XML 的自动装配
Spring还可以完成自动化的注入,自动化注入又被称为自动装配。
它可以根据名字进行自动装配,也可以根据类型进行自动装配。
根据名称自动装配
如果根据名称装配(byName),底层会调用set方法进行注入。
例如:setAge() 对应的名字是age,setPassword()对应的名字是password,setEmail()对应的名字是email。
public class OrderDao {
public void insert(){
logger.info("订单正在生成....");
}
}
public class OrderService {
private OrderDao orderDao;
// 通过set方法给属性赋值。
public void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao;
}
// 生成订单的业务方法
public void generate(){
orderDao.insert();
}
}
OrderService 类中有一个OrderDao属性,而OrderDao属性的名字是orderDao,对应的set方法是setOrderDao(),正好和OrderDao Bean的id是一样的,可以根据名称自动装配。
根据类型自动装配
无论是byName还是byType,在装配的时候都是基于set方法的,所以set方法是必须要提供的,提供构造方法是不行的
当byType进行自动装配的时候,配置文件中某种类型的Bean必须是唯一的,不能出现多个。如果存在多个类型一样的Bean,则Spring会不知道使用那个Bean进行注入。
public class CustomerService {
private UserDao userDao;
private VipDao vipDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setVipDao(VipDao vipDao) {
this.vipDao = vipDao;
}
public void save(){
userDao.insert();
vipDao.insert();
}
}
Spring引入外部属性配置文件
我们都知道编写数据源的时候是需要连接数据库的信息的,例如:driver url username password等信息。这些信息可以单独写到一个属性配置文件properties中,这样用户修改起来会更加的方便
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring6
jdbc.username=root
jdbc.password=123
Bean的作用域
singleton 与 prototype
singleton单例
Spring默认情况下Bean是单例的
singleton在Spring上下文初始化的时候实例化。
每一次调用getBean()方法的时候,都返回那个单例的对象。
prototype多例
prototype在spring上下文初始化的时候,并不会初始化这些prototype的bean。
每一次调用getBean()方法的时候,实例化该bean对象。
prototype翻译为:原型。
Bean 的作用域类型
scope属性的值一共包括8个选项:
- singleton:默认的,单例。
- prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
- request:一个请求对应一个Bean。仅限于在WEB应用中使用。
- session:一个会话对应一个Bean。仅限于在WEB应用中使用。
- global session:portlet应用中专用的。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
- application:一个应用对应一个Bean。仅限于在WEB应用中使用。
- websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。
- 自定义scope:很少使用。