Java开发的23种设计模式详解(结构型模式)
- 1.适配器模式
- 1.1 模式的结构与分类
- 1.2 实例
- 1.2.1 类适配器模式
- 1.2.2 对象适配器模式
- 1.2.3 接口适配器模式
- 1.3 优缺点
- 1.4 使用场景
- 2.桥接模式
- 2.1 定义
- 2.2 目的
- 2.3 UML结构图
- 2.4 例子
- 2.5 优缺点
- 2.5.1 优点:
- 2.5.2 缺点
- 2.6 使用场景
- 3.组合模式
- 3.1 定义
- 3.2 组成
- 3.3 例子
- 3.4 适用场景
- 3.5 优缺点
- 4.装饰模式
- 4.1 定义
- 4.2 组成
- 4.3 例子
- 4.4 适用场景
- 4.5 优缺点
- 5.外观模式
- 5.1 定义
- 5.2 组成
- 5.3 例子
- 5.4 适用场景
- 5.5 优缺点
- 6.亨元模式
- 6.1 定义
- 6.2 结构
- 6.3 例子
- 6.4 适用场景
- 6.5 优缺点
- 7.代理模式
- 7.1 组成及分类
- 7.2 实例
- 7.2.1 静态代理(需要接口)
- 7.2.2 JDK动态代理
- 7.2.3 cglib动态代理
- 7.2.4 比较
- 7.3 优缺点
- 7.4 使用场景
观前提示:
本文所使用Eclipse版本为Photon Release (4.8.0),Idea版本为ultimate 2019.1,JDK版本为1.8.0_141。
1.适配器模式
适配器模式将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。做法是将类自己的接口包裹在一个已存在的类中。
1.1 模式的结构与分类
模式的结构
适配器模式(Adapter)包含以下主要角色。
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
分类
- 类适配器模式
- 对象适配器模式
- 接口适配器模式
1.2 实例
1.2.1 类适配器模式
通过继承来实现适配器功能。
当我们要的目标接口Target中没有我们想要的方法 ,却在另一个适配者类Adaptee中发现了合适的方法,我们又不能改变目标接口Target,在这种情况下,我们可以定义一个适配器Adapter来进行中转,这个适配器Adapter要实现我们目标接口Target,然后再继承适配者类Adaptee,这样我们可以在适配器Adapter中访问适配者类Adaptee的方法了,这时我们在适配器Adapter中的目标接口Target方法中直接引用Adaptee中的合适方法,这样就完成了一个简单的类适配器。
简单来说,接口A没有我们想要的方法,类B中有,我们定义C实现接口A,继承类B,在C中的接口A方法引用类B的方法,实现类适配器。
这里我们以充电器为例,充电器为Adapter,220V交流电为Adaptee,5V直流电为Target。
Adaptee220.java
package adapter;
public class Adaptee220 {
public void input220() {
System.out.println("220V交流电");
}
}
Target5.java
package adapter;
public interface Target5 {
void output5();
}
Adapter.java
package adapter;
public class Adapter extends Adaptee220 implements Target5 {
@Override
public void output5() {
input220();
System.out.println("转化为5V直流电");
}
}
测试类Test.java
package adapter;
public class Test {
public static void main(String[] args) {
Target5 target5 = new Adapter();
target5.output5();
}
}
测试结果
1.2.2 对象适配器模式
适配器容纳一个它包裹的类的实例,通过组合来实现适配器功能。
当我们要的目标接口Target中没有我们想要的方法 ,却在另一个适配者类Adaptee中发现了合适的方法,我们又不能改变目标接口Target,在这种情况下,我们可以定义一个适配器Adapter来进行中转,这个适配器Adapter要实现我们目标接口Target,然后在适配器Adapter中定义私有变量C(对象)(适配者类Adaptee),再定义一个带参数的构造器用来为对象C赋值,再在目标接口Target的方法实现中使用对象C调用其来源于适配者类Adaptee的方法。
简单来说,接口A没有我们想要的方法,类B中有,我们定义C实现接口A,然后再C中定义私有变量D(类B),再定义一个有参构造器赋值D,再在接口A的方法中使用D调用类B的方法。
我们同样使用充电器为例。
Adaptee220.java
package adapter;
public class Adaptee220 {
public void input220() {
System.out.println("220V交流电");
}
}
Target5.java
package adapter;
public interface Target5 {
void output5();
}
Adapter.java
package adapter;
public class Adapter extends Adaptee220 implements Target5 {
private Adaptee220 adaptee220;
public Adapter(Adaptee220 adaptee220) {
this.adaptee220 = adaptee220;
}
@Override
public void output5() {
adaptee220.input220();
System.out.println("转化为5V直流电");
}
}
测试类Test.java
package adapter;
public class Test {
public static void main(String[] args) {
Target5 target5 = new Adapter(new Adaptee220());
target5.output5();
}
}
测试结果
1.2.3 接口适配器模式
通过抽象类来实现适配。也可以称为缺省适配器模式。
当存在这样一个接口,其中定义了N多的方法,而我们现在却只想使用其中的一个到几个方法,如果我们直接实现接口,那么我们要对所有的方法进行实现,哪怕我们仅仅是对不需要的方法进行置空(只写一对大括号,不做具体方法实现)也会导致这个类变得臃肿,调用也不方便,这时我们可以使用一个抽象类作为中间件,即适配器,用这个抽象类实现接口,而在抽象类中所有的方法都进行置空,那么我们在创建抽象类的继承类,而且重写我们需要使用的那几个方法即可。
我们还是以充电器为例,这里我们的充电器不仅仅可以将220V交流电转化为5V直流电,还可以转为12V,18V,110V等。
Adaptee220.java
package adapter;
public class Adaptee220 {
public void input220() {
System.out.println("220V交流电");
}
}
Target.java
package adapter;
public interface Target {
void output5();
void output12();
void output18();
void output110();
}
Adapter.java
package adapter;
public class Adapter extends Adaptee220 implements Target {
@Override
public void output5() {
}
@Override
public void output12() {
}
@Override
public void output18() {
}
@Override
public void output110() {
}
}
Adapter5.java
package adapter;
public class Adapter5 extends Adapter {
@Override
public void output5() {
input220();
System.out.println("转化为5V直流电");
}
}
测试类Test.java
package adapter;
public class Test {
public static void main(String[] args) {
Target target5 = new Adapter5();
target5.output5();
}
}
测试结果
1.3 优缺点
优点:
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
缺点:
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
- 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
1.4 使用场景
- 系统需要使用现有的类,而此类的接口不符合系统的需要。
- 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
- 通过接口转换,将一个类插入另一个类系中。
2.桥接模式
2.1 定义
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式。
2.2 目的
桥梁模式的用意是将 抽象化 (Abstraction)与 实现化 (Implementation) 脱耦 ,使得二者可以独立地变化。
- 抽象化
存在于多个实体中的共同的概念性联系,就是抽象化。作为一个过程,抽象化就是忽略一些信息,从而把不同的实体当做同样的实体对待。 - 实现化
抽象化给出的具体实现,就是实现化。 - 脱耦
所谓耦合,就是两个实体的行为的某种强关联。而将它们的强关联去掉,就是耦合的解脱,或称脱耦。在这里,脱耦是指将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联。
2.3 UML结构图
- 抽象化(Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用。
- 修正抽象化(Refined Abstraction)角色:扩展抽象化角色,改变和修正父类对抽象化的定义。
- 实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
- 具体实现化(Concrete Implementor)角色:这个角色给出实现化角色接口的具体实现。
2.4 例子
我们以不同颜色不同品牌的手机为例
创建抽象化角色Phone.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public abstract class Phone {
Color color;
public void setColor(Color color){
this.color = color;
}
public abstract void salePhone();
}
小米手机Xiaomi.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public class Xiaomi extends Phone {
public void salePhone() {
color.phoneBrand("小米手机");
}
}
华为手机Huawei.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public class Huawei extends Phone {
public void salePhone() {
color.phoneBrand("华为手机");
}
}
Vivo手机Vivo.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public class Vivo extends Phone {
public void salePhone() {
color.phoneBrand("Vivi手机");
}
}
实现化角色Color.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public interface Color {
void phoneBrand(String phoneBrand);
}
白色White.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public class White implements Color {
public void phoneBrand(String phoneBrand) {
System.out.println("白色的" + phoneBrand);
}
}
黑色Black.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public class Black implements Color {
public void phoneBrand(String phoneBrand) {
System.out.println("黑色的" + phoneBrand);
}
}
蓝色Blue.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public class Blue implements Color {
public void phoneBrand(String phoneBrand) {
System.out.println("蓝色的" + phoneBrand);
}
}
客户端Client.java
package testDesignPatterns.testStructuredMode.testBridgeMode;
public class Client {
public static void main(String[] args) {
//黑色
Color color = new Black();
//小米
Phone xiaomi = new Xiaomi();
xiaomi.setColor(color);
xiaomi.salePhone();
//华为
Phone huawei = new Huawei();
huawei.setColor(color);
huawei.salePhone();
}
}
运行结果
2.5 优缺点
2.5.1 优点:
- 分离抽象接口及其实现部分。提高了比继承更好的解决方案。
- 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明,可以对用户隐藏实现细节。
2.5.2 缺点
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
2.6 使用场景
- 如果一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
3.组合模式
3.1 定义
组合模式,将对象组合成树形结构以表示"部分-整体"的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。
3.2 组成
- Component 是组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
- Leaf 在组合中表示叶子结点对象,叶子结点没有子结点。
- Composite 定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等。
3.3 例子
我们以文件目录结构为例,顶级文件夹为java,顶级文件夹下有三个子级文件夹testAop,testApplicationContext,testDesignPatterns,子级文件夹testDesignPatterns下又有testCreatedMode和testStructuredMode
目录结构如下
Directory.java
package testDesignPatterns.testStructuredMode.testCompositePattern;
import java.util.ArrayList;
import java.util.List;
public class Directory {
private String name;
private int depth;
private List<Directory> childrenDirectory;
public Directory(String name, int depth) {
this.name = name;
this.depth = depth;
childrenDirectory = new ArrayList<Directory>();
}
public void add(Directory d) {
childrenDirectory.add(d);
}
public void remove(Directory d) {
childrenDirectory.remove(d);
}
public List<Directory> getChildrenDirectory(){
return childrenDirectory;
}
@Override
public String toString() {
return "Directory{" +"name='" + name + '\'' +
", depth=" + depth +
'}';
}
}
Client.java
package testDesignPatterns.testStructuredMode.testCompositePattern;
public class Client {
public static void main(String[] args) {
Directory d1 = new Directory("一级目录java",1);
Directory d2 = new Directory("二级目录testAop",2);
Directory d3 = new Directory("二级目录testApplicationContext",2);
Directory d4 = new Directory("二级目录testDesignPatterns",2);
Directory d5 = new Directory("三级目录testCreatedMode",3);
Directory d6 = new Directory("三级目录testStructuredMode",3);
d1.add(d2);
d1.add(d3);
d1.add(d4);
d4.add(d5);
d4.add(d6);
//打印目录结构
System.out.println(d1.toString());
for (Directory firstChildren: d1.getChildrenDirectory()) {
System.out.println(firstChildren.toString());
for (Directory secondChildren: firstChildren.getChildrenDirectory()) {
System.out.println(secondChildren.toString());
}
}
}
}
运行结果如下
3.4 适用场景
- 表示对象的部分-整体层次结构。
- 忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象。
3.5 优缺点
优点
- 高层模块调用简单。
- 节点自由增加。
缺点
- 使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
4.装饰模式
4.1 定义
装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
4.2 组成
- 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色:定义一个将要接收附加责任的类。
- 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并实现一个与抽象构件接口一致的接口。
- 具体装饰(Concrete Decorator)角色:负责给构件对象添加上附加的责任。
4.3 例子
我们以鸟为例
目录结构如下
抽象构件(Component)角色 Bird.java
package testDesignPatterns.testStructuredMode.testDecorator;
/**
* 抽象构件(Component)角色
*/
public interface Bird {
void description();
}
具体构件(Concrete Component)角色 Sparrow.java
package testDesignPatterns.testStructuredMode.testDecorator;
/**
* 具体构件(Concrete Component)角色
*/
public class Sparrow implements Bird {
@Override
public void description() {
System.out.println("这是一只麻雀");
}
}
装饰(Decorator)角色 BirdDecorator.java
package testDesignPatterns.testStructuredMode.testDecorator;
/**
* 装饰(Decorator)角色
*/
public class BirdDecorator implements Bird {
private Bird bird;
public BirdDecorator(Bird bird) {
this.bird = bird;
}
@Override
public void description() {
bird.description();
}
}
具体装饰(Concrete Decorator)角色 LittleSparrow.java
package testDesignPatterns.testStructuredMode.testDecorator;
/**
* 具体装饰(Concrete Decorator)角色
*/
public class LittleSparrow extends BirdDecorator {
public LittleSparrow(Bird bird) {
super(bird);
}
@Override
public void description() {
super.description();
System.out.println("小巧的麻雀");
}
}
测试类DecorationTest.java
package testDesignPatterns.testStructuredMode.testDecorator;
public class DecorationTest {
public static void main(String[] args) {
Bird sparrow = new Sparrow();
sparrow.description();
System.out.println("--------------装饰麻雀--------------");
Bird littleSparrow = new LittleSparrow(new Sparrow());
littleSparrow.description();
}
}
运行结果
4.4 适用场景
- 需要扩展一个类的功能,或给一个类添加附加职责。
- 需要动态的给一个对象添加功能,这些功能可以再动态的撤销。
- 需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变的不现实。
- 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
4.5 优缺点
优点
- Decorator模式与继承关系的目的都是要扩展对象的功能,但是Decorator可以提供比继承更多的灵活性。
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
缺点
- 这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。
- 装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂。
- 装饰模式是针对抽象组件(Component)类型编程。但是,如果你要针对具体组件编程时,就应该重新思考你的应用架构,以及装饰者是否合适。当然也可以改变Component接口,增加新的公开的行为,实现“半透明”的装饰者模式。在实际项目中要做出最佳选择。
5.外观模式
5.1 定义
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。
5.2 组成
- Facade,这个外观类为子系统提供一个共同的对外接口。
- Clients,客户对象通过一个外观接口读写子系统中各接口的数据资源。
5.3 例子
我们以建房为例,有接口House,实现接口的Bungalow,Villa,Building,提供一个外观类HouseFacade,客户端访问外观类建房子。
目录结构如下
接口 House.java
package testDesignPatterns.testStructuredMode.testFacade;
public interface House {
void bulid();
}
实现类 Bungalow.java
package testDesignPatterns.testStructuredMode.testFacade;
public class Bungalow implements House {
@Override
public void bulid() {
System.out.println("建造平房");
}
}
实现类 Villa.java
package testDesignPatterns.testStructuredMode.testFacade;
public class Villa implements House {
@Override
public void bulid() {
System.out.println("建造别墅");
}
}
实现类 Building.java
package testDesignPatterns.testStructuredMode.testFacade;
public class Building implements House {
@Override
public void bulid() {
System.out.println("建造楼房");
}
}
外观类 HouseFacade.java
package testDesignPatterns.testStructuredMode.testFacade;
public class HouseFacade {
private House bungalow;
private House building;
private House villa;
public HouseFacade() {
this.bungalow = new Bungalow();
this.building = new Building();
this.villa = new Villa();
}
public void buildBungalow(){
bungalow.bulid();
}
public void buildBuilding(){
building.bulid();
}
public void buildVilla(){
villa.bulid();
}
}
测试类 Test.java
package testDesignPatterns.testStructuredMode.testFacade;
public class Test {
public static void main(String[] args) {
HouseFacade houseFacade = new HouseFacade();
houseFacade.buildBuilding();
houseFacade.buildBungalow();
houseFacade.buildVilla();
}
}
运行结果如下
5.4 适用场景
- 设计初期阶段,应该有意识的将不同层分离,层与层之间建立外观模式。
- 开发阶段,子系统越来越复杂,增加外观模式提供一个简单的调用接口。
- 维护一个大型遗留系统的时候,可能这个系统已经非常难以维护和扩展,但又包含非常重要的功能,为其开发一个外观类,以便新系统与其交互。
5.5 优缺点
优点
- 实现了子系统与客户端之间的松耦合关系。
- 客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。
缺点
- 不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
6.亨元模式
6.1 定义
6.2 结构
- 抽象享元角色:为具体享元角色规定了必须实现的方法,而外蕴状态就是以参数的形式通过此方法传入。在Java中可以由抽象类、接口来担当。
- 具体享元角色:实现抽象角色规定的方法。如果存在内蕴状态,就负责为内蕴状态提供存储空间。
- 享元工厂角色:负责创建和管理享元角色。要想达到共享的目的,这个角色的实现是关键!
- 客户端角色:维护对所有享元对象的引用,而且还需要存储对应的外蕴状态。
补充:内蕴状态和外蕴状态
- 内蕴状态存储在享元内部,不会随环境的改变而有所不同,是可以共享的。
- 外蕴状态是不可以共享的,它随环境的改变而改变的,因此外蕴状态是由客户端来保持(因为环境的变化是由客户端引起的)。
6.3 例子
我们以建楼房为例,我们要建造同样高度和材质的楼房,但是建造地址不同。
目录结构如下
抽象享元角色 House.java
package testDesignPatterns.testStructuredMode.testFlyweight;
public interface House {
void bulid();
}
具体享元角色 Building.java
package testDesignPatterns.testStructuredMode.testFlyweight;
public class Building implements House {
private String material;
private String address;
private int Height;
public Building(String material) {
this.material = material;
}
public void setAddress(String address) {
this.address = address;
}
public void setHeight(int height) {
Height = height;
}
@Override
public void bulid() {
System.out.println("建造楼房,材质为:" + material + ",高度为:" + height + ",地址为:" + address);
}
}
享元工厂角色 HouseFactory.java
package testDesignPatterns.testStructuredMode.testFlyweight;
import java.util.HashMap;
import java.util.Map;
public class HouseFactory {
private static final Map<String, House> buildingMap = new HashMap<>();
public static Building getBuilding(String material) {
Building building = (Building) buildingMap.get(material);
if(building == null) {
building = new Building(material);
buildingMap.put(material, building);
System.out.println("新建楼房,材料为: " + material);
}
return building;
}
}
测试类 Test.java
package testDesignPatterns.testStructuredMode.testFlyweight;
import java.util.Random;
public class Test {
private static final String[] materials = {"砖头", "水泥", "黄土", "冰块"};
private static final String[] addresses = {"市中心", "郊区", "开发区"};
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Building building = HouseFactory.getBuilding(getRandomMaterial());
building.setHeight(10);
building.setAddress(getRandomAddress());
building.bulid();
}
}
private static String getRandomMaterial() {
int index = new Random().nextInt(materials.length);
return materials[index];
}
private static String getRandomAddress() {
int index = new Random().nextInt(addresses.length);
return addresses[index];
}
}
运行结果如下
6.4 适用场景
- 如果一个应用程序使用了大量的对象,而这些对象造成了很大的存储开销的时候就可以考虑是否可以使用享元模式。
6.5 优缺点
优点
- 大大减少对象的创建,降低系统的内存,使效率提高。
缺点
- 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
7.代理模式
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
7.1 组成及分类
组成
- 抽象角色:通过接口或抽象类声明真实角色实现的业务方法。
- 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
- 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。
分类
- 静态代理
- 动态代理
- cglib代理
7.2 实例
7.2.1 静态代理(需要接口)
静态代理需要真实角色和代理角色实现相同的接口。
我们以明星和经纪人为例,明星为真实角色RealRole,经纪人为代理角色ProxyRole,他们共同实现抽象角色Role。
Role.java
package proxy;
public interface Role {
void performance();
}
RealRole.java
package proxy;
public class RealRole implements Role {
@Override
public void performance() {
System.out.println("明星演出");
}
}
ProxyRole.java
package proxy;
public class ProxyRole implements Role {
private Role role;
public ProxyRole(Role role) {
this.role = role;
}
@Override
public void performance() {
System.out.println("经纪人代理");
role.performance();
}
}
测试类Test.java
package proxy;
public class Test {
public static void main(String[] args) {
Role role = new RealRole();
Role proxyRole = new ProxyRole(role);
proxyRole.performance();
}
}
测试结果
优点
- 可以做到在不修改目标对象的功能前提下,对目标功能扩展。
缺点
- 因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护。
7.2.2 JDK动态代理
实现阶段不用关系代理是哪个,而在运行阶段指定具体哪个代理。
代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型),动态代理也叫做:JDK代理,接口代理。
代理类必须实现InvocationHandler接口,并且,JDK动态代理只能代理实现了接口的类,没有实现接口的类是不能实现JDK动态代理。
JDK中生成代理对象所在包:java.lang.reflect.Proxy
我们还是以明星和经纪人为例
Role.java
package proxy;
public interface Role {
void performance(String address);
void sing();
}
RealRole.java
package proxy;
public class RealRole implements Role {
@Override
public void performance(String address) {
System.out.println("明星在" + address +"演出");
}
@Override
public void sing() {
System.out.println("明星演唱");
}
}
ProxyRole.java
package proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class ProxyRole implements InvocationHandler {
private Object obj;
public ProxyRole(Object obj) {
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("动态代理-预处理操作");
Object result = method.invoke(obj, args);
System.out.println("动态代理-调用后操作");
return result;
}
}
测试类Test.java
package proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Test {
public static void main(String[] args) {
Role role = new RealRole();
InvocationHandler handler = new ProxyRole(role);
//获取真实角色的ClassLoader
ClassLoader classLoader = role.getClass().getClassLoader();
//动态产生一个代理者
Class<?>[] classes = new Class[] {Role.class};
Role proxy = (Role) Proxy.newProxyInstance(classLoader, classes, handler);
proxy.performance("上海");
proxy.sing();
}
}
测试结果
7.2.3 cglib动态代理
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要cglib了。cglib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与cglib动态代理均是实现Spring AOP的基础。 cglib不是java自带的API,我们要使用cglib代理必须引入 cglib的jar包。
我们还是以明星和经纪人为例
RealRole.java
package proxy;
public class RealRole {
public void performance(String address) {
System.out.println("明星在" + address +"演出");
}
public void sing() {
System.out.println("明星演唱");
}
}
ProxyRole.java
package proxy;
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
public class ProxyRole implements MethodInterceptor {
private Object obj;
public Object getInstance(Object obj) {
this.obj = obj;
//创建加强器,用来创建动态代理类
Enhancer enhancer = new Enhancer();
//为加强器指定要代理的业务类(即:为下面生成的代理类指定父类)
enhancer.setSuperclass(this.obj.getClass());
//设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦
enhancer.setCallback(this);
// 创建动态代理类对象并返回
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method arg1, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("cglib动态代理-预处理操作");
Object result = proxy.invokeSuper(obj, args);
System.out.println("cglib动态代理-调用后操作");
return result;
}
}
测试类Test.java
package proxy;
public class Test {
public static void main(String[] args) {
RealRole role = new RealRole();
ProxyRole cglib = new ProxyRole();
RealRole proxy = (RealRole) cglib.getInstance(role);
proxy.performance("上海");
proxy.sing();
}
}
测试结果
7.2.4 比较
- 静态代理是通过在代码中显式定义一个业务实现类一个代理,在代理类中对同名的业务方法进行包装,用户通过代理类调用被包装过的业务方法。
- JDK动态代理是通过接口中的方法名,在动态生成的代理类中调用业务实现类的同名方法。
- cglib动态代理是通过继承业务类,生成的动态代理类是业务类的子类,通过重写业务方法进行代理。
7.3 优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用。
- 代理对象可以扩展目标对象的功能。
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度。
缺点:
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢。
- 增加了系统的复杂度。
7.4 使用场景
- 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
- 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
- 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。