1 概述
GoF(Gang of Four),四人组:
设计模式的本质是面向对象设计原则的实际运用
1.1 UML
类的表示方式
在UML类图中,类使用包含类名、属性(field)和方法(method)且带有分割线的矩形来表示
属性/方法名称前的+/-表示这个属性/方法的可见性:
· +:表public
· -:表private
· #:表protected
属性的完整表示方式: 可见性 名称 :类型 [ = 缺省值]
方法的完整表示方式: 可见性 名称(参数列表) [ : 返回值]
类与类之间关系的表示方式
关联关系
关联关系是对象之间的一种引用关系,分为一般关联关系、聚合关系、组合关系
一般关联关系:单向关联、双向关联、自关联
单向关联
双向关联
自关联
聚合关系
强关联关系,是整体和部分之间的关系
聚合关系也是通过成员对象来实现,其中成员对象是整体对象的一部分,但成员对象可以脱离整体对象而独立存在。
组合关系
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系
依赖关系
是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。
继承关系
是对象之间耦合度最大的一种关系,表示一般与特殊的关系
实现关系
是接口与实现类之间的关系,
1.2 软件设计原则
开闭原则
对扩展开放,对修改关闭。例如接口与抽象类
里氏代换原则
任何基类可以出现的地方,子类一定可以出现。
即 子类可以扩展父类的功能,但不能改变父类原有的功能。(子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法)
依赖倒转原则
高层模块不应该依赖底层模块,两者都应该依赖其抽象;
抽象不应该依赖细节,细节应该依赖抽象;
即 要求对抽象进行编程。不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
接口隔离原则
客户端不应该被迫依赖于它不使用的方法;
一个类对另一个类的依赖应该建立在最小的接口上
迪米特法则
最少知识原则
只和你的直接朋友交谈,不跟 “陌生人” 说话。其含义是:如果两个软件实体无须直接通信,则就不应当发生直接的相互调用,可通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可直接访问这些对象的方法。
合成复用原则
尽量先使用组合或聚合等关联关系来实现,其次才考虑使用继承关系来实现
通常类的复用分为继承复用、合成复用
2 创建者模式(5种)
用于描述“怎样创建对象”,主要特点:“将对象的创建与使用分离”
2.1 单例设计模式
涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
该类提供一种访问其唯一的对象的方式,可直接访问,不需要实例化该类的对象
单例模式结构
· 单例类。只能创建一个实例的类
· 访问类。使用单例类
实现
1、饿汉式:类加载就会导致该单实例对象被创建
存在有内存浪费的问题
方式一:静态变量方式
方式二:静态代码块
方式三:枚举方式
枚举类型是线程安全的,且只会装载一次,枚举类型是所有单例实现中唯一不会被破坏的单例实现模式
2、懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
方式一:线程不安全
方式二:线程安全
方式三:双重检查锁
绝大部分的操作都是读操作,读操作是线程安全的,所以没必要让每个线程必须持有锁才能调用该方法,需调整加锁的时机
在多线程情况下,可能出现空指针问题,原因:JVM在实例化对象的时候会进行优化和指令重排序操作。要解决双重检查锁模式带来空指针异常的问题,只需使用volatile关键字
方式四:静态内部类方式
静态内部类单例模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。
静态属性由于被static修饰,保证只被实例化一次,并严格保证实例化顺序
第一次加载 类时不会初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证Singleton类的唯一性
在没有加任何锁的情况下,保证了多线程下的安全,并没有任何性能影响和空间浪费
存在问题
破坏单例模式,方式有序列化、反射
方式一:序列化反序列化
反序列化出的对象是原对象的深拷贝,导致每次反序列化出的对象引用都是不同的
解决方法:在Singleton类中添加 readResolve( ) 方法,在反序列化时被反射调用,如果定义了该方法,就返回该方法的值,如果未定义,则返回新new的对象
方式二:反射
解决方法:
JDK源码 Runtime类
Runtime类采用的是饿汉式单例
2.2 工厂模式
优点:解耦
简单工厂模式(不属于GOF的23种经典设计模式)
更像一种编程习惯
优点:封装了创建对象的过程,可通过参数直接获取对象。把对象的创建和业务逻辑层分开,以此来避免修改客户代码。降低客户代码修改的可能性,更易扩展
缺点:增加新产品时还需修改工厂类代码,违背了 “开闭原则”。
结构
工厂方法模式
遵循开闭原则
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。
优点:用户只需指定具体工厂名称即可得到想要产品,无须知道产品的具体创建过程;在系统增加新的产品时只需添加具体产品类和对应的具体工厂类,无须对原工厂进行修改,满足开闭原则
缺点:每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,增加系统的复杂度
结构
抽象工厂模式
考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构
抽象工厂模式是工厂方法模式的升级版,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品
横轴表示同一类产品;纵轴表示同一品牌的产品(产自同一工厂)
优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象
缺点:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
结构
如果要加同一个产品族,只需再加一个对应的工厂类,不需修改其他类
使用场景
2.2.1 模式扩展
通过工厂模式+配置文件方式解除工厂对象和产品对象的耦合。
在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取
一:定义配置文件
使用properties文件作为配置文件
自定义名称 = 全限定类名
二:定义工厂类
2.2.2 JDK源码解析 Collection.iterator
单列集合获取迭代器方法即使用的是工厂方法模式
Collection接口是抽象工厂类,ArrayList是具体工厂类;Iterator接口是抽象商品类,ArrayList类中的Iter内部类是具体商品类。在具体工厂类中 iterator( ) 方法创建具体的商品类的对象
1 DateForamt类中的getInstance( )方法使用是工厂模式
2 Calendar类中的getInstance( )方法使用是工厂模式
2.3 原型模式
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象
结构
原型模式的克隆分为浅克隆、深克隆
Java中的Object类提供了clone( ) 方法来实现浅克隆。
使用场景
· 对象创建非常复杂,可使用原型模式快捷的创建对象
· 性能和安全要求比较高
2.3.1 扩展 深克隆
浅克隆效果:stu对象和stu1对象是同一个对象,就会产生将stu1对象中name属性值改为“李四”,两个Citation(奖状)对象中显示的都是李四。
对具体原型类(Citation)中的引用类型的属性进行引用的复制,需要进行深克隆。进行深克隆需要使用对象流
其中,Citation类与Student类必须实现Serializable接口
2.4 建造者模式
将一个复杂对象的构建与表示分离,使得同样的构造过程可创建不同的表示
优点:
缺点:
结构
使用场景
· 创建对象较为复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序稳定
· 创建复杂对象的算法独立于该对象的组成部分及它们的装配方式,即产品的构建过程和最终的表示是独立的
2.4.1 模式扩展
当一个类构造器需要传入很多参数时,如果创建该类的实例,代码可读性会非常差且容易引入错误,此时需要建造者模式重构
随即可以使用链式编程来创建对象
2.5 创建者模式对比
2.5.1 工厂方法模式 VS 建造者模式
工厂方法模式注重的是整体对象的创建方式
建造者模式注重的是部件构建的过程,意在通过一步步地精确构造创建出一个复杂的对象
2.5.2 抽象工厂模式 VS 建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只需关心什么产品由什么工厂生产
建造者模式则要求按照指定的蓝图建造产品,主要目的是通过组装零配件而生产一个新产品
3 结构型模式(7种)
用于描述如何将类或对象按某种布局组成更大的结构
3.1 代理模式
由于某些原因需给某对象提供一个代理以控制对该对象的访问。此时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介
Java中的代理按照代理类生成时机不同分为静态代理、动态代理。
静态代理在编译期就生成,而动态代理则是在Java运行时动态生成(JDK代理、CGLib代理)
优点:
· 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
· 代理对象可以扩展目标对象的功能
· 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度
缺点:
· 增加了系统的复杂度
结构
代理模式分三种角色:
使用场景
· 远程(Remote)代理
· 防火墙(Firewall)代理
· 保护(Protect or Access)代理
3.1.1 静态代理
public interface SellTickets {
void sell();
}
public class TrainStation implements SellTickets{
@Override
public void sell() {
System.out.println("火车站卖车牌");
}
}
public class ProxyStation {
private TrainStation trainStation = new TrainStation();
public void sell(){
System.out.println("代理点做操作");
trainStation.sell();
}
}
3.1.2 JDK动态代理
Java中提供了一个动态代理类Proxy,通过提供一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
public interface SellTickets {
void sell();
}
public class TrainStation implements SellTickets {
@Override
public void sell() {
System.out.println("火车站卖车牌");
}
}
//动态代理
public class ProxyFactory {
//声明目标对象
private TrainStation trainStation = new TrainStation();
//获取代理对象
public SellTickets getProxyObject(){
//返回代理对象
/*
ClassLoader loader : 类加载器,用于加载代理类。可通过目标对象获取类加载器
Class<?>[] interfaces : 代理类实现的接口的字节码对象
InvocationHandler h : 代理对象的调用处理程序
*/
SellTickets proxyInstance = (SellTickets)Proxy.newProxyInstance(
trainStation.getClass().getClassLoader(),
trainStation.getClass().getInterfaces(),
new InvocationHandler() {
/*
Object proxy, 代理对象。和proxyInstance对象是同一对象,在invoke方法中基本不用
Method method, 对接口中的方法进行封装的method对象
Object[] args 调用方法的实际参数
返回值 ,方法的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代售点收取费用");
Object invoke = method.invoke(trainStation, args);
return invoke;
}
}
);
return proxyInstance;
}
}
ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类
真正的代理类为:
代理类($ Proxy0)实现了SellTickets。(真实类、代理类实现同样的接口)
代理类($Proxy0)将匿名内部类对象传递给父类
执行流程
1、在测试类中通过代理对象调用sell( )方法
2、根据多态的特性,执行的是代理类( $ Proxy0)中的sell( )方法
3、代理类( $ Proxy0)中的sell( )方法中又调用了InvocationHandler接口的自实现类对象的invoke方法
4、invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell( )方法
3.1.3 CGLib代理
火车站案例如果未定义SellTickets接口,只定义了TrainStation(火车站类)。则JDK代理无法使用,因为JDK动态代理要求必须定义接口,对接口进行代理
3.1.4 三种代理对比
· JDK代理 和 CGLib代理
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用JAVA反射效率要高。CGLib不能对声明为final的类或方法进行代理,因为CGLib原理是动态生成被代理类的子类
· 动态代理 和 静态代理
动态代理和静态代理相比,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。当接口方法数量比较多时,可进行灵活处理,而不需要像静态代理那样每个方法进行中转
如果接口增加一个方法,静态代理除了所有实现类需要实现这个方法外,所有代理类也需要实现该方法。增加了代码维护的复杂度,而动态代理不会出现该问题
3.2 适配器模式
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
结构
应用场景
· 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致
· 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同
3.2.1 类适配器模式
定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
适配器类结构:
类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用·
3.2.2 对象适配器模式
实现方式:对象适配器模式可采用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
满足合成复用原则,当客户类没有接口规范时也可使用
3.2.3 接口适配器模式
当不希望实现一个接口中所有的方法时,可创建一个抽象类Adapter,实现所有方法。随后只需继承该抽象类即可。
3.2.4 JDK源码
从上图可看出:
· InputStreamReader是对同样实现了Reader的StreamDecoder的封装
· StreamDecoder是Sun JDK给出的自身实现。他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换
结论: 从表层看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。从Sun JDK中的实现类关系结构中看,是StreamDecoder的设计实现在实际上采用了适配器模式
3.3 装饰者模式
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式
结构
优点:
· 装饰者可以带来比继承更加灵活性的扩展功能,使用更加方便,可通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者是动态的附加责任
· 装饰类和被装饰类可独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可动态扩展一个实现类的功能
使用场景
3.3.1 JDK源码
BufferedWriter使用装饰者模式对Writer子实现类进行增强,添加缓冲区,提高写数据的效率
3.3.2 代理和装饰者的区别
· 相同点:
· 都实现与目标类相同的业务接口
· 在两个类中都要声明目标对象
· 都可以在不修改目标类的前提下增强目标方法
· 不同点:
· 目的不同;装饰者是为了增强目标对象;静态代理是为了保护和隐藏目标对象
· 获取目标对象构建的地方不同;装饰者是由外界传递进来,可通过构造方法传递;静态代理是在代理类内部创建,以此来隐藏目标对象
3.4 桥接模式
将抽象与实现分离,使它们可独立变化。
它是用组合关系代替继承关系来实现,从而降低了抽象与实现这两个可变维度的耦合度
结构
优点:
· 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统
· 实现细节对客户透明
使用场景
3.5 外观模式
又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。
该模式对外有一个统一接口,外部应用程序不需关系内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高程序的可维护性
外观(Facade)模式是“迪米特法则”的典型应用
优点:
· 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类
· 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易
缺点:
· 不符合开闭原则,修改很麻烦
结构
使用场景
3.5.1 源码解析
3.6 组合模式
又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构
结构
优点:
使用场景
组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。
3.6.1 组合模式分类
根据抽象构件类的定义形式,可分为透明组合模式和安全组合模式两种形式
· 透明组合模式(标准实现方式)
· 安全组合模式
3.7 享元模式
运用共享技术来有效地支持大量细粒度对象的复用。
它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。
结构
两种状态:
角色:
优点:
· 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
· 享元模式中的外部状态相对独立,且不影响内部状态
缺点:
为了使对象可以共享,需将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
使用场景
3.7.1 JDK源码
Integer 默认先创建并缓存 -128~127 之间数的 Integer 对象,当调用 valueof 时如果参数在 -128~127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象
4 行为型模式(11种)
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责
行为型模式分为 类行为模式 和 对象行为模式 ,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性
4.1 模板方法模式(类行为型模式)
定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤
结构
优点:
缺点:
使用场景
4.1.1 JDK源码解析
InputStream类中定义了多个read( )方法,如下:
带三个参数的read方法是模板方法,无参read方法是子类必须重写的方法
4.2 策略模式
定义一系列算法,并将每个算法封装起来,使它们可相互替换,且算法的变化不会影响使用算法的客户。
策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
结构
优点:
缺点:
使用场景
4.2.1 JDK源码解析
Arrays类调用的sort方法其实是Comparator具体子实现类中的sort方法
4.3 命令模式
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,方便将命令对象进行存储、传递、调用、增加与管理。
结构
优点:
缺点:
使用场景
4.3.1 JDK源码解析
Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当调用者,start是其执行方法
会调用一个native方法start0( ),调用系统方法,开启一个线程,而接收者是对程序员开放的,可自己定义接收者
4.4 职责链模式
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
结构
优点:
缺点:
4.4.1 JDK源码解析
在javaWeb中,FilterChain是职责链(过滤器)模式的典型应用
4.5 状态模式
对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
结构
优点:
缺点:
使用场景
4.6 观察者模式
又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。
这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己
结构
优点:
缺点:
使用场景
4.6.1 JDK中提供的实现
通过 Observable 和 Observer 接口定义了观察者模式,只要实现其子类就可编写观察者模式实例
1、Observable类
Observable 类是抽象目标类(被观察者),它有一个Vector集合成员变量,用于保存所有要通知的观察者对象
2、Observer接口
Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update 方法,进行相应工作。
4.7 中介者模式
又名调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可独立地改它们之间的交互
结构
优点:
缺点:
使用场景
4.8 迭代器模式
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
结构
优点:
缺点:
使用场景
4.8.1 JDK源码解析
以ArrayList为例
在 iterator 方法中返回了一个实例化的 Iterator 对象。 Itr 是一个内部类,它实现了 Iterator 接口并重写了其中的抽象方法。
4.9 访问者模式
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作
结构
优点:
缺点:
使用场景
4.9.1 扩展
访问者模式用到了一种双分派的技术
1、分派
变量被声明时的类型叫做变量的静态类型(明显类型);变量所引用的对象的真实类型又叫做变量的实际类型。
比如 Map map = new HashMap( ),map变量的静态类型是Map,实际类型是HashMap。
根据对象的类型而对方法进行的选择,就是分派(Dispatch)
分派(Dispatch)分为两种,即静态分派和动态分派
静态分派 发生在编译时期,分派根据静态类型信息发生。方法重载就是静态分派
动态分派 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派
2、动态分派
3、静态分派
重载方法的分派是根据静态类型进行的,该分派过程在编译时期就完成。
4、双分派
双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。
客户端将 Execute 对象做为参数传递给 Animal 类型的变量调用的方法,完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己 this 作为参数传递进去,完成第二次分派,这里的 Execute 类中有多个重载方法,而传递进行的是 this,就是具体的实际类型的对象。
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的。
4.10 备忘录模式
又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
结构
优点:
缺点:
使用场景
4.11 解释器模式(类行为型模式)
给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和+、-符号组成的合法序列,“1+3-2”就是这种语言的句子。
解释器就是要解析出来语句的含义。
文法(语法)规则
抽象语法树
结构
优点:
缺点:
使用场景
5 自定义Spring框架
5.1 spring核心功能结构
Spring框架总体架构图:
核心容器由 beans、core、context 和 expression 4个模块组成
· spring-beans和spring-core模块是Spring框架的核心模块,包含控制反转(IOC)和依赖注入(DI)。BeanFactory使用控制反转对应用程序的配置和依赖性规范与实际的应用程序代码进行分离。BeanFactory属于延时加载,即在实例化容器对象后并不会自动实例化Bean,只有当Bean被使用时,BeanFactory才会对该Bean进行实例化与依赖关系的装配。
· spring-context模块构架于核心模块之上,扩展了BeanFactory,为它添加了Bean生命周期控制、框架事件体系及资源加载透明化等功能。此外,该模块还提供了许多企业级支持,如邮件访问、远程访问、任务调度等,ApplicationContext 是该模块的核心接口,它的超类是 BeanFactory。与 BeanFactory 不同,ApplicationContext 实例化后会自动对所有的单实例 Bean 进行实例化与依赖关系的装配,使之处于待用状态。
· spring-context-support 模块是对 Spring IoC容器及 IoC 子容器的扩展支持
· spring-context-indexer 模块是对 Spring 的类管理组件和 Classpath 扫描组件
· spring-expression 模块是统一表达式语言(EL)的扩展模块,可查询、管理运行中的对象,同时也可以方便地调用对象方法,以及操作数组、集合等。它的语法类似于传统EL,但提供了额外的功能,最出色的是函数调用和简单字符串的模板函数。EL的特性是基于Spring产品的需求而设计,可方便地同Spring IoC进行交互
5.1.1 Bean概述
Spring 是面向 Bean 的编程(BOP),Bean 在 Spring 中处于核心地位。
Spring IoC容器通过配置文件或注解的方式管理 bean 对象之间的依赖关系。
为什么 Bean 如此重要?
· spring 将 bean 对象交由一个叫 IOC 容器进行管理
· bean 对象之间的依赖关系在配置文件中体现,并由 spring 完成
5.2 Spring IOC分析
5.2.1 BeanFactory解析
Bean的创建是典型的工厂模式,即IOC容器
每个接口都有它的使用场合,主要是为了区分在Spring内部操作过程中对象的传递和转化,对对象的数据访问所做的限制。
这三个接口共同定义了Bean的集合、Bean之间的关系及Bean行为。最基本的IOC容器接口是BeanFactory
BeanFactory有一个很重要的子接口,即ApplicationContext接口,该接口主要来规范容器中的bean对象是非延迟加载,即在创建容器对象的时候就对对象bean进行初始化,并存储到一个容器中。
Spring提供了许多IOC容器实现:
5.2.2 BeanDefinition解析
Bean对象在Spring实现中是以BeanDefinition来描述的
5.2.3 BeanDefinitionReader解析
Bean的解析主要就是对Spring配置文件的解析。这个解析过程主要通过BeanDefinitionReader来完成
BeanDefinitionReader接口定义的功能:
5.2.4 BeanDefinitionRegistry解析
BeanDefinitionReader用来解析bean定义,并封装BeanDefinition对象,而我们定义的配置文件中定义了很多bean标签,解析的BeanDefinition对象存储在注册中心,该注册中心顶层接口就是BeanDefinitionRegistry。
5.2.5 创建容器
5.3 自定义SpringIOC
5.3.1 定义bean相关的pojo类
5.3.1.1 PropertyValue类
用于封装bean的属性,体现到上面的配置文件是封装bean标签的子标签property标签数据
public class PropertyValue {
private String name;
private String ref;
private String value;
public PropertyValue() {
}
public PropertyValue(String name, String ref, String value) {
this.name = name;
this.ref = ref;
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRef() {
return ref;
}
public void setRef(String ref) {
this.ref = ref;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
5.3.1.2 MutablePropertyValues类
一个bean标签可以有多个property子标签,MutablePropertyValues类用于存储并管理多个PropertyValue对象
public class MutablePropertyValues implements Iterable<PropertyValue>{
// 定义list集合对象,用于存储PropertyValue对象
private final List<PropertyValue> propertyValueList;
public MutablePropertyValues() {
this.propertyValueList = new ArrayList<>();
}
public MutablePropertyValues(List<PropertyValue> propertyValueList) {
this.propertyValueList =
propertyValueList == null? new ArrayList<>() : propertyValueList;
}
// 获取迭代器对象
@Override
public Iterator<PropertyValue> iterator() {
return propertyValueList.iterator();
}
// 获取所有的PropertyValue对象,返回数组
public PropertyValue[] getPropertyValues(){
//将集合转成数组返回
return propertyValueList.toArray(new PropertyValue[0]);
}
//根据name属性获取PropertyValue
public PropertyValue getPropertyValue(String PropertyName){
for (PropertyValue propertyValue : propertyValueList) {
if (propertyValue.getName().equals(PropertyName)) return propertyValue;
}
return null;
}
//判断集合是否为空
public boolean isEmpty(){
return propertyValueList.isEmpty();
}
// 添加PropertyValue
public MutablePropertyValues addPropertyValue(PropertyValue pv){
//判断集合中是否与要添加的对象重复,重复则覆盖
synchronized (this){
for (int i = 0; i < propertyValueList.size(); i++) {
if (propertyValueList.get(i).getName().equals(pv)) {
propertyValueList.set(i,pv);
return this; //目的是实现链式编程
}
}
//无重复,则直接添加
propertyValueList.add(pv);
return this;
}
}
//判断是否有指定name值的PropertyValue对象
public boolean contains(String PropertyName){
return getPropertyValue(PropertyName) != null;
}
}
5.3.1.3 BeanDefinition类
BeanDefinition类用来封装bean信息的,主要包含id(bean对象的名称)、class(需要交由spring管理的类的全类名)及子标签property数据
public class BeanDefinition {
private String id;
private String className;
private MutablePropertyValues mutablePropertyValues;
public BeanDefinition() {
mutablePropertyValues = new MutablePropertyValues();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public MutablePropertyValues getMutablePropertyValues() {
return mutablePropertyValues;
}
public void setMutablePropertyValues(MutablePropertyValues mutablePropertyValues) {
this.mutablePropertyValues = mutablePropertyValues;
}
}
5.3.2 定义注册表相关类
5.3.2.1 BeanDefinitionRegistry接口
该接口定义了注册表的相关操作,定义如下功能:
· 注册BeanDefinition对象到注册表中
· 从注册表中删除指定名称的BeanDefinition对象
· 根据名称从注册表中获取BeanDefinition对象
· 判断注册表中是否包含指定名称的BeanDefinition对象
· 获取注册表中BeanDefinition对象的个数
· 获取注册表中所有的BeanDefinition的名称
public interface BeanDefinitionRegistry {
// 注册BeanDefinition对象到注册表中
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition);
//从注册表中删除指定名称的BeanDefinition对象
void removeBeanDefinition(String beanName) throws Exception;
//根据名称从注册表中获取BeanDefinition对象
BeanDefinition getBeanDefinition(String beanName) throws Exception;
boolean containsBeanDefinition(String beanName);
int getBeanDefinitionCount();
String[] getBeanDefinitionNames();
}
5.3.2.2 SimpleBeanDefinitionRegistry类
该类实现了BeanDefinitionRegistry接口,定义了Map集合作为注册表容器
public class SimpleBeanDefinitionRegistry implements BeanDefinitionRegistry{
//定义一个容器,用来存储BeanDefinition对象
private Map<String,BeanDefinition> map = new HashMap<>();
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) {
synchronized (this){
map.put(beanName,beanDefinition);
}
}
@Override
public void removeBeanDefinition(String beanName) throws Exception {
synchronized (this){
map.remove(beanName);
}
}
@Override
public BeanDefinition getBeanDefinition(String beanName) throws Exception {
return map.get(beanName);
}
@Override
public boolean containsBeanDefinition(String beanName) {
return map.containsKey(beanName);
}
@Override
public int getBeanDefinitionCount() {
return map.size();
}
@Override
public String[] getBeanDefinitionNames() {
return map.keySet().toArray(new String[0]);
}
}
5.3.3 定义解析器相关类
5.3.3.1 BeanDefinitionReader接口
BeanDefinitionReader是用来解析配置文件并在注册表中注册bean的信息。定义了两个规范:
· 获取注册表的功能,让外界可通过该对象获取注册表对象
· 加载配置文件,并注册bean数据
5.3.3.2 XmlBeanDefinitionReader类
XmlBeanDefinitionReader类是专门用来解析xml配置文件的。该类实现BeanDefinitionReader接口并实现接口中的两个功能。
public class XmlBeanDefinitionReader implements BeanDefinitionReader {
//声明注册表对象
private BeanDefinitionRegistry registry;
public XmlBeanDefinitionReader() {
registry = new SimpleBeanDefinitionRegistry();
}
@Override
public BeanDefinitionRegistry getRegistry() {
return registry;
}
@Override
public void loadBeanDefinitions(String configLocation) throws Exception {
//使用Dom4j对xml文件进行解析
SAXReader saxReader = new SAXReader();
//获取类路径下的配置文件
InputStream inputStream = XmlBeanDefinitionReader.class.getClassLoader().getResourceAsStream(configLocation);
Document document = saxReader.read(inputStream);
//根据Document对象获取根标签对象(beans)
Element rootElement = document.getRootElement();
//获取根标签下所有的bean标签对象
List<Element> list = rootElement.elements("bean");
for (Element beanelement : list) {
//获取id、class属性 封装到BeanDefinition对象中
String id = beanelement.attributeValue("id");
BeanDefinition beanDefinition = new BeanDefinition(id, beanelement.attributeValue("class"));
//创建MutablePropertyValues对象
MutablePropertyValues mutablePropertyValues = new MutablePropertyValues();
//获取bean标签下所有的property标签对象
List<Element> propertyElements = beanelement.elements("property");
for (Element propertyElement : propertyElements) {
//将property标签中的属性值封装成PropertyValue对象并添加到mutablePropertyValues
mutablePropertyValues.addPropertyValue(
new PropertyValue(
propertyElement.attributeValue("name"),
propertyElement.attributeValue("ref"),
propertyElement.attributeValue("value")
)
);
}
//将MutablePropertyValues对象封装到BeanDefinition中
beanDefinition.setMutablePropertyValues(mutablePropertyValues);
//将BeanDefinition注册到注册表中
registry.registerBeanDefinition(id,beanDefinition);
}
}
}
5.3.4 IOC容器相关类
5.3.4.1 BeanFactory接口
定义IOC容器的统一规范,即获取bean对象
public interface BeanFactory {
//根据bean对象的名称获取bean对象
Object getBean(String name) throws Exception;
//根据bean对象的名称 和 对象的类型 获取bean对象,并进行类型转换
<T> T getBean(String name, Class<? extends T> clazz) throws Exception;
}
5.3.4.2 ApplicationContext接口
该接口的所有子实现类对bean对象的创建都是非延时的,所以在该接口中定义 refresh( ) 方法,主要功能:
· 加载配置文件
· 根据注册表中的BeanDefinition对象封装的数据进行bean对象的创建
public interface ApplicationContext extends BeanFactory {
void refresh() throws Exception;
}
5.3.4.3 AbstractApplicationContext类
· 作为ApplicationContext接口的子类,该类也是非延迟加载,所以需要在该类中定义一个Map集合,作为bean对象存储的容器。
· 声明BeanDefinitionReader类型的变量,用来进行xml配置文件的解析,符合单一职责原则。
BeanDefinitionReader类型的对象创建由子类实现
public abstract class AbstractApplicationContext implements ApplicationContext {
// 声明解析器变量
protected BeanDefinitionReader beanDefinitionReader;
// 定义用于存储bean对象的map容器
protected Map<String,Object> singletonObjects = new HashMap<>();
// 声明配置文件路径的变量
protected String configLocation;
@Override
public void refresh() throws Exception {
//加载BeanDefinition对象
beanDefinitionReader.loadBeanDefinitions(configLocation);
//初始化bean
finishBeanInitialization();
}
//bean初始化
private void finishBeanInitialization() throws Exception{
//获取注册表对象
BeanDefinitionRegistry registry = beanDefinitionReader.getRegistry();
//获取BeanDefinition对象
String[] beanDefinitionNames = registry.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
//进行bean的初始化
getBean(beanDefinitionName); // 因是抽象类不需要重写父接口的方法,直接调用的父接口的方法
}
}
}
5.3.4.4 ClassPathXmlApplicationContext类
该类主要是加载类路径下的配置文件,并进行bean对象的创建,主要完成以下功能:
· 在构造方法中,创建BeanDefinitionReader对象
· 在构造方法中,调用refresh()方法,用于进行配置文件加载、创建bean对象并存储到容器中
· 重写父接口中的getBean()方法,并实现依赖注入操作
public class ClassPathXmlApplicationContext extends AbstractApplicationContext {
public ClassPathXmlApplicationContext(String configLocation) {
this.configLocation = configLocation;
// 构建解析器对象
this.beanDefinitionReader = new XmlBeanDefinitionReader();
try {
this.refresh();
}catch (Exception e){
}
}
// 根据bean对象的名称获取bean对象
@Override
public Object getBean(String name) throws Exception {
//判断对象容器中是否包含指定名称的bean对象,如包含,直接返回,否则要自行创建
Object o = singletonObjects.get(name);
if (o != null) return o;
// 获取BeanDefinition对象
BeanDefinitionRegistry registry = beanDefinitionReader.getRegistry();
BeanDefinition beanDefinition = registry.getBeanDefinition(name);
// 获取bean信息中的className
String className = beanDefinition.getClassName();
// 通过反射创建对象
Class<?> clazz = Class.forName(className);
Object beanobj = clazz.newInstance();
// 进行依赖注入
MutablePropertyValues mutablePropertyValues = beanDefinition.getMutablePropertyValues();
for (PropertyValue propertyValue : mutablePropertyValues) {
//获取Property标签中的name属性
String propertyName = propertyValue.getName();
//获取Property标签中的value属性
String value = propertyValue.getValue();
//获取Property标签中的ref属性
String ref = propertyValue.getRef();
if (ref!=null && !ref.equals("")){
//获取依赖的bean对象
Object bean = getBean(ref);
//拼接方法名
String methodName = StringUtils.getSetterMethodByFieldName(propertyName);
//获取所有方法对象
Method[] methods = clazz.getMethods();
for (Method method : methods) {
//找到目标方法
if (method.getName().equals(methodName)){
//执行setter方法 例如:beanobj为UserService,bean为UserDao,将依赖的bean对象注入到beanobj中的对象
method.invoke(beanobj,bean);
break;
}
}
}
if (value != null && !value.equals("")){
//拼接方法名
String methodName = StringUtils.getSetterMethodByFieldName(propertyName);
//获取Method对象
Method method = clazz.getMethod(methodName, String.class);
method.invoke(beanobj,value);
}
}
//在返回beanobj对象之前,将该对象存储在map容器中
singletonObjects.put(name,beanobj);
return beanobj;
}
@Override
public <T> T getBean(String name, Class<? extends T> clazz) throws Exception {
Object bean = getBean(name);
if (bean == null) return null;
return clazz.cast(bean);
}
}
依赖注入前将bean对象存入容器,就可以解决循环依赖造成的死循环问题
使用到的工具类:
public class StringUtils {
private StringUtils(){}
// userDao -> UserDao
public static String getSetterMethodByFieldName(String fieldName){
String methodName = "set" + fieldName.substring(0,1).toUpperCase() + fieldName.substring(1);
return methodName;
}
}
5.3.5 自定义Spring IOC总结
使用的设计模式
· 工厂模式。工厂模式+配置文件的方式
· 单例模式。Spring IOC管理的bean对象都是单例的,此处的单例不是通过构造器进行单例的控制,而是spring框架对每一个bean只创建了一个对象
· 模板方法模式。AbstractApplicationContext类中的finishBeanInitialization()方法调用了子类的getBean()方法,因getBean方法的实现与环境息息相关。
· 迭代器模式。对于MutablePropertyValues类定义使用到了迭代器模式,因此类存储并管理PropertyValue对象,也属于一个容器,所以给该容器提供一个遍历方式。