目录
1 Spring启示录
1.1 OCP开闭原则
1.2 依赖倒置原则DIP
1.3 控制反转IoC
2 Spring概述
2.1 Spring简介
2.2 Spring8大模块
2.3 Spring特点
2.4 本次学习使用软件版本
3 Spring入门程序
3.1 Spring下载
3.2 第一个Spring程序
3.3 第一个spring程序的细节
3.4 Spring6启用Log4j2日志框架
4 spring对IoC的实现
4.1 set注入
4.2 构造注入
4.3 set注入专题
4.3.1 注入内部bean和外部bean
4.3.2 注入简单类型
4.3.3 级联属性赋值
4.3.4 数组注入
4.3.5 集合注入
4.3.6 注入null和空字符串
4.3.7 特殊字符的注入
4.4 p命名空间注入
4.5 c命名空间注入
4.6 util命名空间
4.7 基于XML的自动装配
4.7.1 根据名称自动装配
4.7.2 根据类型自动装配
4.8 Spring引入外部属性配置文件
5 Bean的作用域
5.1 singleton
5.2 prototype
5.3 其它scope
6 GoF工厂设计模式
6.1 工厂模式三种形态
6.2 简单工厂模式
6.3 工厂方法模式
6.4 抽象工厂模式(了解)
7 Bean的实例化方式
7.1 通过构造方法实例化
7.2 通过简单工厂模式实例化
7.3 通过factory-bean实例化
7.4 通过FactoryBean接口实例化
7.5 BeanFactory和FactoryBean的区别
7.5.1 BeanFactory
7.5.2 FactoryBean
7.6 注入自定义Date
8 Bean的声明周期
8.1 什么是Bean的生命周期
8.2 为什么要知道Bean的生命周期
8.3 Bean的声明周期之五步
8.4 Bean生命周期之7步
8.5 Bean生命周期之10步
8.6 Bean的作用域不同,管理方式不同
9 Bean的循环依赖问题
9.1 什么是Bean的循环依赖
9.2 singleton下的set注入产生的循环依赖
9.3 prototype下的set注入产生的循环依赖
9.4 singleton下的构造注入产生的循环依赖
10 回顾反射机制
10.1 分析方法四要素
10.2 使用反射机制调用方法
10.3 假设知道属性名
我们之前学过mvc设计模式,这种模式可以有效的降低代码的耦合度,提高扩展力,我们再写一个这样的模式,代码如下:
在创建maven项目前我们可以先设置如下
package com.itzw.spring6.dao.impl;
import com.itzw.spring6.dao.UserDao;
public class UserDaoImplForMySQL implements UserDao {
public void select() {
System.out.println("正在连接数据库。。。");
}
}
package com.itzw.spring6.service.impl;
import com.itzw.spring6.dao.UserDao;
import com.itzw.spring6.dao.impl.UserDaoImplForMySQL;
import com.itzw.spring6.service.UserService;
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImplForMySQL();
public void login() {
userDao.select();
}
}
package com.itzw.spring6.servlet;
import com.itzw.spring6.service.UserService;
import com.itzw.spring6.service.impl.UserServiceImpl;
public class UserServlet {
private UserService userService = new UserServiceImpl();
public void loginRequest(){
userService.login();
}
}
以上大概就是我们之前学的mvc架构模式,分为表示层、业务逻辑层、持久层。而其中UserServlet依赖了具体的UserServiceImpl,UserServiceImpl依赖了具体的UserDaoImplForMySQL。
假如我们不想连接mysql数据库了,我们想连接Oracle数据库,我们就要修改UserServiceImpl中的代码。
什么是OCP?
如上图可以很明显的看出上层是依赖下层的,下面改动上面必然改动,这样同样违背了另一个开发原则:依赖倒置原则
依赖倒置原则(Dependence Inversion Principe),倡导面向接口编程,面向抽象编程,不要面向具体编程,让上层不再依赖下层,下层改动上层不需要改动,这样大大降低耦合度,耦合度低了,扩展力就强了,同时代码复用性也会增强(软件七大开发原则都在为解耦合服务)
那我们可能有疑问,这不就是面向接口编程的吗?确实,是这样的,但是不完全是,我们虽然都是调用接口中的方法,但是我们是通过new 对象,new一个具体的接口实现类,如下:
如下才是完全面向接口,完全符合依赖倒置原则:
但是如果这样编程userDao是null,那么就会出现空指针异常,确实是这样。这也就是我们接下来要解决的问题。
控制反转(Inversion of Control),是面向对象编程的一种设计思想,可以用来降低代码的耦合度,符合依赖倒置原则。控制反转的核心是:将对象的创建权交出去,将对象和对象之间的关系管理权交出去,由第三方容器负责创建与维护。
我们要学的Spring框架实现了控制反转这种思想,Spring框架可以帮我们new 对象,还可以帮我们维护对象与对象之间的关系
控制反转常用的实现方法:依赖注入(Dependency Injection,简称DI)
依赖注入DI,包括两种常见的 方式:
IoC是一种全新的设计模式,但是理论和时间成熟较晚,并没有包含在GoF中(GoF是23中设计模式)
来自百度百科:
注意:Spring5版本之后是8个模块。在Spring5中新增了WebFlux模块。
Spring Core模块:这是Spring框架最基础的部分,它提供了依赖注入特征来实现容器对Bean的管理。
官网地址:Spring | Home
官网地址(中文):Spring 中文网 官网
以上两个地址都可以下载,不过我们可以直接使用maven下载依赖,就像mybatis一样
前期准备:在idea中创建一个maven模块,这个我们早已设置好,直接用即可
第一步:添加spring context依赖
repository.spring.milestone
Spring Milestone Repository
https://repo.spring.io/milestone
org.springframework
spring-context
6.0.0-M2
注意:打包方式为jar
当加入spring context依赖之后,会关联引入其它依赖
第二步:添加junit依赖
junit
junit
4.13.2
test
第三步:定义bean:User
package com.itzw.spring6.bean;
/**
* 封装用户信息
*/
public class User {
}
第四步:编写spring配置文件spring.xml,放在类的根目录下也就是resources目录
我们直接右击resources就可以创建idea提供的文件模板:
在配置文件中进行bean配置:
需要注意的是:这个文件最好放在类路径下,方便移植
bean标签的两个重要属性:id:这是bean的身份证,不能重复,是唯一标识;class:必须填写类的全路径,全限定类名。
第五步:编写测试程序
package com.itzw.spring6.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Spring6Test {
@Test
public void testFirst(){
//第一步:获取spring容器对象
//ApplicationContext是一个接口,接口下有很多实现类,其中有一个叫做:ClassPathXmlApplicationContext
//ClassPathXmlApplicationContext 专门从类路径下加载spring配置文件的一个spring上下文对象
//运行这行代码就相当于启动了spring容器,解析spring.xml文件,并且实例化所有的bean对象,放到spring容器当中
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
//第二步:根据bean的id从spring容器中获取这个对象
Object userBean = applicationContext.getBean("userBean");
System.out.println(userBean);
}
}
测试结果:
(1)bean的id不能重复:
(2)底层是怎样创建对象的:
我们在User类中写上无参构造:
public class User {
public User(){
System.out.println("这是User类的无参构造");
}
}
测试:
如果只有有参构造没有无参呢?
public class User {
/* public User(){
System.out.println("这是User类的无参构造");
}*/
public User(String name){
System.out.println("这是User类的有参构造");
}
}
经过测试:spring是通过调用类的无参构造来创建对象的,所以要想让spring给你创建对象,必须保证无参构造方法是存在的
spring是通过反射机制调用无参构造方法创建对象
(3)创建好的对象存储在map集合中
(4)Spring配置文件的名字可以随意改
(5)spring配置文件可以创建多个
我们再创建一个spring配置文件,配置bean的信息
我们直接在ClassPathXmlApplicationContext构造方法参数上传递路径即可,不需要再new一个ClassPathXmlApplicationContext对象,为什么呢?通过源码查看是可以传多个的
(6)配置文件中的类必须是自定义的吗
不是,可以是jdk自带的类,如下:
Object dateBean = applicationContext.getBean("dateBean");
System.out.println(dateBean);
可以直接输出当前日期
经测试,spring配置文件中的bean可以是任意类,只要它不是抽象的并且有无参构造
(7)执行getBean方法时传入的参数不存在会报异常
(8)getBean方法返回类型问题
默认返回类型是Object,当然我们可以强转,但是有没有别的办法,
User userBean1 = applicationContext.getBean("userBean", User.class);
可以通过第二个参数返回bean的类型
从spring5之后,Spring框架支持集成的日志框架是Log4j2.如何启用日志框架:
第一步:引入log4j2的依赖:
org.apache.logging.log4j
log4j-core
2.19.0
org.apache.logging.log4j
log4j-slf4j2-impl
2.19.0
第二步:在类的根路径下提供log4j2.xml配置文件(文件名固定为:log4j2.xml,文件必须放到类根路径下。)
这样我们的输出信息就有日志信息了:
但是我们自己怎么使用日志信息呢?
@Test
public void testLog(){
//第一步:创建日志记录对象
//获取Spring6Test类的日志记录对象,也就是说只要是Spring6Test类中的代码记录日志的话,就输出日志信息
Logger logger = LoggerFactory.getLogger(Spring6Test.class);
//第二步:记录日志,根据不同级别来输出日志
logger.info("我是一条信息");
logger.debug("我是一个调试信息");
logger.error("我是一条错误信息");
}
前面我们讲过我们可以使用依赖注入实现控制反转。控制反转的思想是将对象的创建权交出去,交给第三方容器。
实现依赖注入主要有两个方式:set注入和构造注入
我们创建一个新的模块,我们先像之前那样创建一个dao文件和一个service文件,不过这次在service目录下我们不new新的dao对象,如下:
package com.itzw.spring6.dao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserDao {
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);
public void insert(){
logger.info("正在插入信息。。");
}
}
package com.itzw.spring6.service;
import com.itzw.spring6.dao.UserDao;
public class UserService {
UserDao userDao;
public void saveUser(){
userDao.insert();
}
}
public class SpringTest {
@Test
public void testInjectBySet(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userServiceBean = applicationContext.getBean("userServiceBean", UserService.class);
userServiceBean.saveUser();
}
}
但是这样测试结果显然是出错的,dao对象是null的
我们使用set注入的方式来实现控制反转:
我们需要提供一个set方法,spring会调用这个set方法给userDao属性赋值,我们直接使用idea工具生成这个set方法即可
我们现在需要service调用set方法,这就要借助spring,我们在bean标签配置property标签,其中name属性就是用来传set方法的,name属性名指定格式为set方法的方法名去掉set,然后剩下的单词首字母小写。这就实现了UserService类调用set方法,但是我们还需要传值,传一个UserDao对象的值,因为上面我们配置类userDao的bean标签,我们直接把它的id值传给property中的ref属性即可这就完成了传值。如下:
配置好后再次测试,测试成功。
我们多建一个dao文件,在service文件中直接用idea自动生成构造方法
package com.itzw.spring6.service;
import com.itzw.spring6.dao.UserDao;
import com.itzw.spring6.dao.VipDao;
public class AccountService {
UserDao userDao;
VipDao vipDao;
public AccountService(UserDao userDao, VipDao vipDao) {
this.userDao = userDao;
this.vipDao = vipDao;
}
public void test(){
userDao.insert();
vipDao.delete();
}
}
在spring配置文件中配置如下,和set方法差不多
测试即可。
以为set注入使用的较多,我们使用set注入来学习下面的内容
我们之前使用的set注入就是注入外部bean,那什么是注入内部bean呢?
在property中嵌套bean标签就是内部bean。这样麻烦了一点,我们一般不用这种方式,我们还是用之前的方式
我们之前注入的数据都不是简单类型,对象属性都是一个对象,我们现在注入属性是一个数据的。
我们写一个类,写上属性,创建set方法和toString方法:
package com.itzw.spring6.beans;
public class User {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
编写spring配置文件:
在这里name属性依然是set方法,但是我们给set方法参数传值就只要传简单的数据就可以了,所以我们使用value来赋值。
测试:结果如下:
那么简单类型包括哪些呢?
public static boolean isSimpleValueType(Class> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
我们查看源码分析:BeanUtils类,得知简单类型有:
简单类型注入的经典应用:
给数据源的属性赋值,比如我们经常使用的数据库的连接:
private String driver;
private String url;
private String username;
private String password;
@Override
public String toString() {
return "MyDataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Test
public void testSimple2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
Object dataSourceBean = applicationContext.getBean("dataSourceBean");
System.out.println(dataSourceBean);
}
你们可以把简单类型都测试一遍,那我不测了,我不打扰我走了哈哈。
也就是我们熟悉的套娃赋值,就是一个类的属性有另一个类。我们先用我们学过的方式赋值:
package com.itzw.spring6.dao;
public class Clazz {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Clazz{" +
"name='" + name + '\'' +
'}';
}
}
package com.itzw.spring6.dao;
public class Student {
private String name;
private int age;
private Clazz clazz;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", clazz=" + clazz +
'}';
}
public void setClazz(Clazz clazz) {
this.clazz = clazz;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
@Test
public void testCascade(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
Student studentBean = applicationContext.getBean("studentBean", Student.class);
System.out.println(studentBean);
}
以上是我们学过的方式,测试结果:
我们使用级联属性赋值:
使用这种方式我们需要给clazz属性构造get方法,这种方式显得很麻烦,还不如之前的方法。
首先简单类型的数组注入:
package com.itzw.spring6.dao;
import java.util.Arrays;
public class Huang {
private String[] hobbies;
public void setHobbies(String[] hobbies) {
this.hobbies = hobbies;
}
@Override
public String toString() {
return "Huang{" +
"hobbies=" + Arrays.toString(hobbies) +
'}';
}
}
抽烟
喝酒
烫头
@Test
public void testArray(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
Huang huang = applicationContext.getBean("huang", Huang.class);
System.out.println(huang);
}
如果数组元素是非简单类型呢?
抽烟
喝酒
烫头
注意的是:list集合有序和重复,set集合无序不重复
package com.itzw.spring6.dao;
import java.util.List;
import java.util.Set;
public class Person {
List names;
Set addrs;
public void setNames(List names) {
this.names = names;
}
public void setAddrs(Set addrs) {
this.addrs = addrs;
}
@Override
public String toString() {
return "Person{" +
"names=" + names +
", addrs=" + addrs +
'}';
}
}
张三
李四
张麻子
黄四郎
张三
徐州市铜山区
徐州市云龙区
徐州市铜山区
徐州市铜山区
@Test
public void testListAndSet(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
Person person = applicationContext.getBean("person", Person.class);
System.out.println(person);
}
map集合,有键值对:
properties集合本质上也是map集合,但是它的注入方式不一样
root
1234
XML中有5个特殊字符,分别是:<、>、'、"、&。
这些字符直接出现在xml当中会报错:
解决方式有两种:
特殊字符对应的转移字符如下:
特殊字符 |
转义字符 |
> |
> |
< |
< |
' |
' |
" |
" |
& |
& |
使用p命名空间注入可以简化配置,使用前提是:
其实p命名空间注入就是代替set注入的
c命名空间注入是用来简化构造注入的,那么使用前提是:
注意:不管是p命名注入还是c命名注入都可以注入非简单类型
使用util命名空间可以让配置复用,使用前提是:在spring配置文件头部添加如下信息:
比如我想给多个java文件都传输jdbc连接的信息,它们的信息都是一样的,这时我们就可以将这段信息使用util命名空间方式
com.mysql.cj.jdbc.Driver
jdbc:mysql://localhost:3306/spring
root
123
Spring还可以完成自动化的注入,自动化注入又被称为自动装配。它可以根据名字进行自动装配,也可以根据类型进行自动装配。
回忆之前的业务逻辑层和持久层之间的连接,其中spring配置信息如下:
我们使用自动装配:
在orderDao的bean中添加autowire,值设为byName表示通过名称进行自动装配
OrderDao类中有一个UserDao属性,set方法为setUserDao,而UserDao的bean的id为userDao,恰好和OrderDao中的set方法对应。满足这些才能自动装配
也就是说需要set方法名和想要注入的类的bean的id值对应上才行
这样连id值都不需要传了,直接就能识别自己想要的类。但是也有缺陷,不能出现多个同一个类的bean,这样它就识别不出哪个是自己需要的了。
值得注意的是:不管是根据name自动装配还是类型自动装配都是基于set注入实现的,也就是都需要有set方法,否则不行。
从这自动装配我们可以看出来,尤其是根据类型自动装配可读性 很差而且有限制,不如我们用原始方法,没有方便多少反而看的 蛋疼。
我们连接数据库的时候需要配置一些信息,我们能像之前学习一样把这些配置信息放在一个文件中然后引入到xml文件中吗?当然可以。
第一步:写一个数据源类 提供相关属性:
package com.itzw.spring6.jdbc;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class MyDataSource implements DataSource {
private String driver;
private String url;
private String username;
private String password;
@Override
public String toString() {
return "MyDataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
return null;
}
//...
}
第二步:在类路径下建立jdbc.properties文件并配置信息:
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=123
第三步:在spring配置文件中引入context命名空间
第四步:在spring配置文件中使用:
使用context标签引入jdbc配置文件
但需要注意的是,${}中的值会默认先去系统找对应的值,比如username会去系统找,极可能输出的结果是系统也就是Windows的usernam。所以我们在配置名称的时候最好前面加上jdbc.
默认情况下,Spring的IoC容器创建的Bean对象是单例的。
@Test
public void testBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-bean.xml");
Customer customer1 = applicationContext.getBean("customer", Customer.class);
System.out.println(customer1);
Customer customer2 = applicationContext.getBean("customer", Customer.class);
System.out.println(customer2);
Customer customer3 = applicationContext.getBean("customer", Customer.class);
System.out.println(customer3);
}
如上我们调用三次getBean,返回的是同一个对象
那么这个对象在什么时候创建的呢,我们写上无参构造,把getBean方法都删除,执行程序,发现无参构造执行了,我们得知默认情况下,Bean对象的创建是在初始化Spring上下文的时候就完成的。
如果想让spring的bean对象以多例的形式存在,可以在bean标签中指定scope属性的值为:prototype,这样spring会在每一次执行getBean的时候都创建bean对象,调用几次就创建几次
我们再执行上段代码:
这时如果不调用getBean方法,那么无参构造就不会只执行
scope属性的值不止两个,它一共包括8个选项:
我们可以自己定义,但是没必要,再见。
设计模式:一种可以被重复利用的解决方案。GoF(Gang of Four),中文名——四人组。
GoF包括了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。
不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。
GoF23种设计模式可分为三大类:
创建型(5个):解决对象创建问题。
结构型(7个):一些类或对象组合在一起的经典结构。
行为型(11个):解决类或对象之间的交互问题。
工厂模式是解决对象创建问题的,所以工厂模式属于创建型设计模式。这里为什么学习工厂模式呢?这是因为Spring框架底层使用了大量的工厂模式
工厂模式通常有三种形态:
简单工厂模式的角色包括三个:
简单工厂模式的代码如下:
抽象产品角色:
package com.itzw.factory;
public abstract class Weapon {
public abstract void attack();
}
具体产品角色:
package com.itzw.factory;
public class Gun extends Weapon{
@Override
public void attack() {
System.out.println("机枪正在发射...");
}
}
package com.itzw.factory;
public class Plane extends Weapon{
@Override
public void attack() {
System.out.println("飞机正在扔小男孩...");
}
}
package com.itzw.factory;
public class Tank extends Weapon{
@Override
public void attack() {
System.out.println("坦克正在开炮...");
}
}
工厂类角色:
package com.itzw.factory;
public class WeaponFactory {
public static Weapon get(String WeaponType){
if ("GUN".equals(WeaponType)){
return new Gun();
}else if ("PLANE".equals(WeaponType)){
return new Plane();
}else if ("TANK".equals(WeaponType)){
return new Tank();
}else {
throw new RuntimeException("不支持该武器");
}
}
}
测试:
package com.itzw.factory;
public class Test {
public static void main(String[] args) {
Weapon tank = WeaponFactory.get("TANK");
tank.attack();
Weapon plane = WeaponFactory.get("PLANE");
plane.attack();
Weapon gun = WeaponFactory.get("GUN");
gun.attack();
}
}
这种模式就是简单工厂模式,它的优点:客户端程序,也就是我们这里的 测试程序不需要关系对象的创建细节,需要哪个对象只需要向工厂索要,初步实现了责任的分离。客户端只负责消费,工厂只负责生产。但 它也有缺点:工厂类中集中了所有产品的创造逻辑,一旦出问题整个系统会瘫痪;还有就是比较明显的,不符合OCP开闭原则,我们想扩展系统时也就是比如需要扩展一个新的武器需要修改工厂类。
工厂方法模式的角色包括:
抽象产品角色和具体产品角色和上面的简单工厂模式一样:
package com.itzw.factory2;
public abstract class Weapon {
public abstract void attack();
}
package com.itzw.factory2;
public class Gun extends Weapon {
@Override
public void attack() {
System.out.println("机枪正在发射...");
}
}
package com.itzw.factory2;
public class Plane extends Weapon {
@Override
public void attack() {
System.out.println("飞机正在扔小男孩...");
}
}
抽象工厂角色:创建一个方法能让它返回一个抽象产品角色
package com.itzw.factory2;
public interface WeaponFactory {
Weapon get();
}
具体工厂角色:
package com.itzw.factory2;
public class GunFactory implements WeaponFactory{
@Override
public Weapon get() {
return new Gun();
}
}
package com.itzw.factory2;
public class PlaneFactory implements WeaponFactory{
@Override
public Weapon get() {
return new Plane();
}
}
测试:
package com.itzw.factory2;
public class Test {
public static void main(String[] args) {
WeaponFactory gun = new GunFactory();
gun.get().attack();
WeaponFactory plane = new PlaneFactory();
plane.get().attack();
}
}
这时我们再想加入新的武器就不需要修改代码了,直接创建一个具体类继承抽象类然后创建具体工厂角色继承抽象工厂即可。
显然这种模式符合OCP开闭原则,但是每次新增一个产品就需要创建两个类,在一定程度上增加了系统的复杂度
抽象工厂模式可以看做上面两种模式的结合。
或者可以说是bean的获取方式
Spring为Bean提供了多种实例化方式,通常包括4种方式。(也就是说在Spring中为Bean对象的创建准备了多种方案,目的是:更加灵活)
之前我们学习的就是通过构造方法实例化:
package com.itzw.constructor;
public class User {
public User(){
System.out.println("User的无参构造执行了");
}
}
spring配置文件:
测试:
@Test
public void testConstructor(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Object user = applicationContext.getBean("user", User.class);
System.out.println(user);
}
bean:
package com.itzw.bean;
public class Vip {
public Vip(){
System.out.println("vip的无参构造执行了");
}
}
编写简单工厂模式中的工厂类:这里的方法是静态方法
package com.itzw.bean;
public class VipFactory {
public static Vip get(){
return new Vip();
}
}
spring.xml:
我们需要在这指定调用哪个类的哪个方法获取Bean。factory-method属性指定的是工厂类当中的静态方法,也就是告诉spring框架调用这个方法可以获取bean
这种方式本质上是:通过工厂方法模式进行实例化。
和上个方法的区别就是spring配置文件的配置不同,把一个bean标签分解成两个,并且具体工厂类的方法是实例方法如下:
package com.itzw.bean;
public class Order {
public Order(){
System.out.println("Order的无参构造执行了");
}
}
注意这里的方法是实例方法
package com.itzw.bean;
public class OrderFactory {
public Order get(){
return new Order();
}
}
spring:factory-bean告诉框架调用哪个对象,factory-method告诉框架调用哪个方法。
以上三种方法,需要我们自定义factory-bean或者factory-method。在我们编写类实现FactoryBean接口后这俩属性就不需要指定了,factory-bean会自动指向实现FactoryBean接口的类,factory-method会自动指向getObject方法
package com.itzw.bean;
public class Animal {
public Animal(){
System.out.println("Animal构造方法执行了");
}
}
具体工厂类实现FactoryBean接口
package com.itzw.bean;
import org.springframework.beans.factory.FactoryBean;
public class AnimalFactory implements FactoryBean {
@Override
public Animal getObject() throws Exception {
return new Animal();
}
@Override
public Class> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return FactoryBean.super.isSingleton();
}
}
Spring IoC容器的顶级对象,BeanFactory被翻译为“Bean工厂”,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象。
BeanFactory是工厂。
FactoryBean:它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean。
在Spring中,Bean可以分为两类:
我们在前面就讲到过Date类型是简单类型可以直接使用value属性赋值,但是对格式要求非常严格,只能是这样类型的:Mon Oct 10 14:30:26 CST 2022,而这种类型不符合我们常见的格式。
package com.itzw.bean;
import java.util.Date;
public class Student {
private Date date;
public void setDate(Date date) {
this.date = date;
}
@Override
public String toString() {
return "Student{" +
"date=" + date +
'}';
}
}
以上就是我们对Date类型当做简单类型的注入。
我们可以把Date当做非简单类型注入:
还是那个Student类,我们创建Date工厂实现FactoryBean,并且定义属性和构造方法接收spring配置文件传来的日期,我们使用SimpleDateFormat定义日期格式接收传来的日期(比如2020-8-8),然后会返回一个日期
package com.itzw.bean;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.factory.FactoryBean;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateFactoryBean implements FactoryBean {
//定义属性接收日期
private String date;
//通过构造方法给日期赋值
public DateFactoryBean(String date) {
this.date = date;
}
@Override
public Date getObject() throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse(this.date);
return date;
}
@Override
public Class> getObjectType() {
return null;
}
}
传给date工厂一个日期,会返回一个Date默认格式的日期,然后我们再通过Student的Bean将这个日期传给Student。
Bean生命周期可以粗略的划分为五大步:
这五步的位置如下:
注意:我们需要自己写初始化和销毁方法,并且最后我们要手动关闭销毁方法。我们还要在spring配置文件中指定初始化方法和销毁方法。
package com.itzw.spring6.lifecycle;
public class User {
private String name;
public User(){
System.out.println("1.实例化bean");
}
public void setName(String name) {
this.name = name;
System.out.println("2.给bean属性赋值");
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
public void initBean(){
System.out.println("3.初始化bean");
}
public void destroyBean(){
System.out.println("4.销毁bean");
}
}
@Test
public void testLifeCycle(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User user = applicationContext.getBean("user", User.class);
//System.out.println(user);
System.out.println("4.使用bean");
//我们需要手动关闭spring容器才能执行销毁方法,我们还需要将applicationContext强转为ClassPathXmlApplicationContext
ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
context.close();
}
在以上的5步的基础上,在第3步初始化Bean前后可以各加一步,可以加入我们想加入的代码,这一共就是七步了,可以加入“Bean后处理器”。
编写一个类实现BeanPostProcessor类,并且重写before和after方法:
package com.itzw.spring6.lifecycle;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class LogBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean后处理器的before方法执行,即将开始初始化");
return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean后处理器的before方法执行,已经完成初始化");
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}
我们需要在spring配置文件中配置bean后处理器,相当于引入这个类。
在上面七步的基础上,在Bean后处理器before执行之前检查bean是否实现Aware的相关接口,并设置相关依赖,在Bean后处理器before执行之后检查bean是否实现了InitialzingBean接口,并调用接口方法,在销毁bean之前检查bean是否实现了DisposableBean接口,并调用接口方法。
以上三个检查接口并执行相关方法或依赖一共是三步,加上前面的七步一共是十步。
那这些接口是什么意思呢?
Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean
当Bean实现了BeanClassLoaderAware,Spring会加载该Bean的类加载器传递给bean
当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给bean
我们实现这些接口感受一下:
package com.itzw.spring6.lifecycle;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean,DisposableBean {
private String name;
public User(){
System.out.println("1.实例化bean");
}
public void setName(String name) {
this.name = name;
System.out.println("2.给bean属性赋值");
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
public void initBean(){
System.out.println("4.初始化bean");
}
public void destroyBean(){
System.out.println("7.销毁bean");
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("这个bean的类加载器是:"+classLoader);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("这个bean的工厂对象是:"+beanFactory);
}
@Override
public void setBeanName(String name) {
System.out.println("这个bean的名字是:"+name);
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean接口在后处理器的before方法执行之后执行了");
}
@Override
public void destroy() throws Exception {
System.out.println("DisposableBean接口在销毁bean前执行了");
}
}
对于生命周期我们掌握七种就可以
Spring根据bean的作用域来选择管理方式
我们把之前User类的spring.xml文件中的片配置scope设置为prototype:
只执行了前八步
A对象有B属性,B对象有A属性。比如Husband对象有Wife属性,Wife对象有Husband属性,如下:
package com.itzw.spring6.bean;
public class Husband {
private String name;
private Wife wife;
}
package com.itzw.spring6.bean;
public class Wife {
private String name;
private Husband husband;
}
package com.itzw.spring6.bean;
public class Husband {
private String name;
private Wife wife;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setWife(Wife wife) {
this.wife = wife;
}
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}
package com.itzw.spring6.bean;
public class Wife {
private String name;
private Husband husband;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setHusband(Husband husband) {
this.husband = husband;
}
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
注意这里的toString方法里面不能直接输出husband和wife对象,因为这样就递归了,我们指定输出对应的get方法即可
@Test
public void testDC(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husband = applicationContext.getBean("husband", Husband.class);
System.out.println(husband);
Wife wife = applicationContext.getBean("wife", Wife.class);
System.out.println(wife);
}
通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
我们简单分析一下原理:因为使用的是singleton是单例模式,对象只会实例化一次,所以在spring容器加载的时候实例化bean,只要进行实例化后就会立刻“曝光”【不等属性值赋值就曝光了】。比如在给Husband对象赋值的时候需要得到WIfe对象才能完成赋值,而这时Wife对象已经实例化已经曝光可以得到。同样的Wife对象在赋值的时候需要Husband对象,此时Husband对象也已经实例化结束已经曝光,所以不会出现问题。
scope改成prototype
这样会出错:
提示我们:请求的 bean 目前正在创建中:是否有无法解析的循环引用?
那为什么会出错呢?因为使用的是prototype。在我们给Husband赋值的时候需要注入WifeBean,这就需要new一个新的Wife对象给Husband,而new出的Wife对象需要Husband对象才行,这时又会new一个新的Husband对象...会成死循环。
但如果有一个Bean的scope是singleton就不会出错了。比如WIfe是singleton
当我们给Husband赋值的时候,需要Wife对象,因为Wife对象是单例的只有一个固定的,会曝光,Husband对象就能得到了。当我们给Wife对象赋值的时候,需要Husband,Husband会new一个新的对象,此时Husband需要Wife对象,因为WIfe对象是单例的,直接就可以得到了。
package com.itzw.spring6.bean2;
public class Husband {
private String name;
private Wife wife;
public String getName() {
return name;
}
public Husband(String name, Wife wife) {
this.name = name;
this.wife = wife;
}
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}
package com.itzw.spring6.bean2;
public class Wife {
private String name;
private Husband husband;
public String getName() {
return name;
}
public Wife(String name, Husband husband) {
this.name = name;
this.husband = husband;
}
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
测试结果是失败的,错误和上面的错一样,为什么呢?
因为使用构造注入,这要求我们在实例化bean的时候就要给属性值赋值,但是经过上面那些分析我们就应该已经明白了,我们是做不到实例化就立刻给属性赋值的,这会造成死循环。
小小总结一下:经过上面测试我们发现只有在使用set方法注入并且scope的值为singleton的时候才能避免循环依赖。
根本原因在于,这种方式可以做到“实例化Bean”和“给Bean属性赋值”这两个动作分开完成。实例化bean的时候调用无参构造来完成,此时可以先不给属性赋值,可以提前将该bean“曝光”给外界。
也就是说,Bean都是单例的,我们可以先把所有的单例bean实例化出来,放到一个集合当中(我们可以称之为缓存)所有的bean都实例化完成之后,以后我们再慢慢的调用set方法给属性赋值,这样就解决了循环依赖的问题。
我们随便创建一个类:
package com.itzw.reflect;
public class Student {
private String name;
private int age;
public void doSome(){
System.out.println("doSome无参方法执行");
}
public String doSome(String name){
System.out.println("doSome返回姓名的方法执行");
return name;
}
}
我们调用这个类的方法:
package com.itzw.reflect;
public class Test {
public static void main(String[] args) {
Student student = new Student();
student.doSome();
String doSome = student.doSome("王德发");
System.out.println(doSome);
}
}
我们发现调用一个类的方法需要四要素:类的对象;方法名;方法参数;返回值
还是那个类,不过我们使用反射机制的方式调用方法
package com.itzw.reflect;
import java.lang.reflect.Method;
public class Test2 {
public static void main(String[] args) throws Exception {
//获取类
Class> clazz = Class.forName("com.itzw.reflect.Student");
//获取方法
Method doSome = clazz.getDeclaredMethod("doSome", String.class);
//获取类的对象
Object obj = clazz.newInstance();
//调用方法
Object retValue = doSome.invoke(obj, "张麻子");
//输出返回值
System.out.println(retValue);
}
}
package com.itzw.reflect;
public class User {
private String name;
private int age;
public User(){}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
假设有如上的一个类,你知道的信息是:
知道以上信息如何给属性赋值呢?
我们这里给age属性赋值:
package com.itzw.reflect;
import java.lang.reflect.Method;
public class Test3 {
public static void main(String[] args) throws Exception{
String className = "com.itzw.reflect.User";
String propertyName = "age";
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
//获取类
Class> clazz = Class.forName(className);
//获取方法
Method setMethod = clazz.getDeclaredMethod(setMethodName,int.class);
//获取对象
Object obj = clazz.newInstance();
//调用方法
setMethod.invoke(obj,12);
//输出类
System.out.println(obj);
}
}
之所以回顾反射机制是为了下面手写spring框架