设计模式

月伴飞鱼 2024-11-16 19:55:27
基础知识
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!

基本概念

封装

通过对类的封装,仅仅暴露少许必要的方法给调用方使用,调用方不需要了解太多背后的业务细节,大大降低了用错的概率,提高了代码的易用性。

抽象

通过抽象化只暴露出方法定义,而隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。

作用:通过抽象化手段,没有暴露出来实现细节,所以当后来改变具体的实现细节时,对于调用方来说不需要或仅有少量修改,提高了代码的可扩展性和可维护性。

继承

表示类之间is-a的关系,比如猫是一种哺乳动物。

作用:最大的一个好处就是提高了代码的复用性。

多态

一个父类有多个不同的子类,而且多个子类都可以替换父类去使用。

作用:提高代码的可扩展性和复用性。

面向对象和面向过程

面向对象编程:以类为组织代码的基本单元,面向过程编程:以过程(或方法)作为组织代码的基本单元。

面向过程编程:数据和方法相分离。不支持丰富的面向对象编程特性,比如继承、多态、封装。

设计原则

单一职责原则(Single Responsibility Principle)

一个类应该只负责一项职责。

  • 如类A负责两个不同职责:职责1,职责2。

当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。

接口隔离原则(Interface Segregation Principle)

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。

  • 大接口要分割成小接口,接口专用。
  • 客户端使用专用接口。

依赖倒转原则(Dependency Inversion Principle)

高层模块不应该依赖低层模块,二者都应该依赖其抽象(接口或者抽象类)。

  • 抽象不应该依赖细节,细节应该依赖抽象。

依赖倒转(倒置)的中心思想是面向接口编程。

里氏替换原则(Liskov Substitution Principle)

所有引用基类(父类)的地方必须能透明地使用其子类的对象。

  • 把父类设计为抽象类或者接口。
  • 子类必须实现父类的所有方法。

开闭原则(Open Closed Principle)

对扩展开放,对修改关闭。

  • 使用接口和抽象类,实现不修改原代码,又可以拓展新方法。

迪米特法制(Demeter Principle)

一个对象应该对其他对象保持最少的了解。

一个实体应当尽量少的与其他实体之间发生相互作用。

  • 依赖者只依赖该依赖的对象,被依赖者只暴露该暴露的对象。

合成复用原则(Composite Reuse principle)

尽量使用合成/聚合的方式,而不是使用继承。

UML

关联关系

泛化:

  • 继承关系,符号:空心三角箭头的实线,箭头指向父类。

实现:

  • 类与接口的关系,表达类实现了接口的特征行为,符号:带三角箭头的虚线,箭头指向接口。

组合:

  • 整体与部分的关系,但部分不能离开整体而单独存在,符号:带实心菱形的实线,菱形指向整体。

聚合:

  • 整体和部分关系,且部分可以离开整体而单独存在,符号:空心菱形的实心线,菱形指向整体。

关联:

  • 类和类的关系,表达一个类知道另一个类的属性和方法,符号:带普通箭头(或实心三角形箭头)的实心线。

依赖:

  • 使用的关系,即一个类的实现需要另一个类的协助,所以要尽量不使用双向的互相依赖,符号:带箭头的虚线,指向被依赖的类。

img

类图表示方法

从上到下分为3部分:类名、属性、方法。

符号含义:+ public、- private、#protected

img

工厂模式

简单工厂模式

定义一个工厂类,使用static方法创建对象,根据不同参数返回不同实例,每增加一个对象需要修改工厂类,违背了开闭原则

  • 不推荐使用

只适用于工厂类负责创建的对象较少的场景,且客户端只需要传入工厂类的参数,对于如何创建对象不需要关心。

一旦有了新的实现类,就需要修改工厂实现,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

UML类图:

img

使用手机生产来讲解该模式:

public interface Phone {
    void make();
}

制造小米手机(Product1):

public class MiPhone implements Phone {
    @Override
    public void make() {
        System.out.println("make xiaomi phone!");
    }
}

手机代工厂(Factory):

public class PhoneFactory {
    public Phone makePhone(String phoneType) {
        if(phoneType.equalsIgnoreCase("MiPhone")){
            return new MiPhone();
        }
        return null;
    }
}

工厂方法模式

定义一个用于创建对象的接口,让子类决定将哪一个类实例化,此模式使一个类的实例化延迟到其子类

  • 但是工厂方法会导致类的个数过多,增加复杂度,增加系统的抽象性和理解难度。

它可以创建一个工厂接口和多个工厂实现类,这样如果增加新的功能,只需要添加新的工厂类就可以,不需要修改之前的代码。

另外,工厂方法模式还可以和模板方法模式结合一起,将他们共同的基础逻辑抽取到父类中,其它的交给子类去实现。

UML类图:

img

代码体现

某平台先拥有Java和Python学习视频,将来需要拓展业务开放FE(前端)视频学习。

image-20231007150732447

public abstract class Video {
    public abstract void produce();
}
public class JavaVideo extends Video {
    @Override
    public void produce() {
        System.out.println("录制Java课程视频");
    }
}
public class PythonVideo extends Video {
    @Override
    public void produce() {
        System.out.println("录制Python课程视频");
    }
}
public abstract class VideoFactory {
    public abstract Video getVideo();
}
public class JavaVideoFactory extends VideoFactory {
    public Video getVideo() {
        return new JavaVideo();
    }
}
public class PythonVideoFactory extends VideoFactory {
    public Video getVideo() {
        return new PythonVideo();
    }
}
public class Test {
    public static void main(String[] args) {
        VideoFactory videoFactory = new JavaVideoFactory();
        Video video = videoFactory.getVideo();
        video.produce();
        System.out.println(video);
    }
}

当拓展业务时,可以仿照JavaPython,可以直接添加FEVideoFEVideoFactory,可以体现出开放封闭原则。

抽象工厂模式

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类,用于创建一个产品族的产品。

工厂方法模式和抽象工厂模式,它们之间最大的区别在于:

  • 工厂方法模式只有一个抽象产品类,具体工厂类只能创建一个具体产品类的实例。
  • 抽象工厂模式有多个抽象产品类,具体工厂类可以创建多个具体产品类的实例。

UML类图:

img

定义PC产品的接口(AbstractPC):

public interface PC {
    void make();
}

定义小米电脑产品(MIPC):

public class MiPC implements PC {
    @Override
    public void make() {
        System.out.println("make xiaomi PC!");
    }
}

定义苹果电脑产品(MAC):

public class MAC implements PC {
    @Override
    public void make() {
        System.out.println("make MAC!");
    }
}

增加PC产品制造接口:

public interface AbstractFactory {
    Phone makePhone();
    PC makePC();
}

增加小米PC的制造(ConcreteFactory1):

public class XiaoMiFactory implements AbstractFactory{
    @Override
    public Phone makePhone() {
        return new MiPhone();
    }
    @Override
    public PC makePC() {
        return new MiPC();
    }
}

建造者模式

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

构造器模式使用多个简单的对象一步一步构建成一个复杂的对象。

它提供了一种创建对象的最佳方式。

UML类图:

img

桥接模式

桥接模式就是把抽象和实现分离出来,然后中间通过组合来搭建他们之间的桥梁。

UML类图:

img

代码实现

中国有很多银行,有中国农业银行和中国工商银行,关于账号,有定期账号和活期账号,一个就是银行一个账号。

public interface Account {
    /** 打开我们的账号,打开账号,就要返回账号 */
    Account openAccount();
    
    /** 打开我们的账号,查看为什么账户类型,是定期类型还是活期类型 */
    void showAccountType();

}
/** 定期的账号 */
public class DepositAccount implements Account {
    @Override
    public Account openAccount() {
        System.out.println("定期账号");
        return new DepositAccount();
    }

    @Override
    public void showAccountType() {
        System.out.println("这是一个定期账号");
    }
}
/** 活期账号 */
public class SavingAccount implements Account {
    @Override
    public Account openAccount() {
        System.out.println("打开活期账号");
        return new SavingAccount();
    }

    @Override
    public void showAccountType() {
        System.out.println("这是一个活期账号");
    }
}
public abstract class Bank {
    /** 只有子类能拿到这个Account的这个接口 */
    protected Account account;

    /** 组合的时候,可以通过构造器的方式来进行注入也可以通过set方法的方式来进行注入 */
    public Bank(Account account) {
        this.account = account;
    }

    /** 这里声明成和接口里面的方法名一致,只是方便理解,Bank里面的方法要委托给Account接口里面的方法 */
    abstract Account openAccount();

}
public class ABCBank extends Bank {
    /**
     * 组合的时候,可以通过构造器的方式来进行注入也可以通过set方法的方式来进行注入
     *
     * @param account
     */
    public ABCBank(Account account) {
        super(account);
    }

    @Override
    Account openAccount() {
        System.out.println("打开中国农业银行账号");
        return account;
    }
}
public class ICBCBank extends Bank {
    /**
     * 组合的时候,可以通过构造器的方式来进行注入也可以通过set方法的方式来进行注入
     *
     * @param account
     */
    public ICBCBank(Account account) {
        super(account);
    }

    @Override
    Account openAccount() {
        System.out.println("打开中国工商银行账号");
        return account;
    }
}
public class Test {
    public static void main(String[]args){
        Bank icbcBank = new ICBCBank(new DepositAccount());
        Account icbcAccount = icbcBank.openAccount();
        icbcAccount.showAccountType();

        Bank icbcBank2 = new ICBCBank(new SavingAccount());
        Account icbcAccount2 = icbcBank2.openAccount();
        icbcAccount2.showAccountType();

        Bank abcBank = new ABCBank(new SavingAccount());
        Account abcAccount = abcBank.openAccount();
        abcAccount.showAccountType();
    }
}

单例模式

单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象。

饿汉模式

//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
public class SingletonObject {
    // 利用静态变量来存储唯一实例
    private static final SingletonObject instance = new SingletonObject();
 
    // 私有化构造函数
    private SingletonObject(){
        // 里面可能有很多操作
    }
 
    // 提供公开获取实例接口
    public static SingletonObject getInstance(){
        return instance;
    }
}

饿汉模式优缺点:

缺点:

  • 不能实现懒加载,造成空间浪费。
  • 如果一个类比较大,在初始化的时就加载了这个类,但是长时间没有使用这个类,这就导致了内存空间的浪费。

懒汉模式

在程序初始化时不会创建实例,只有在使用实例的时候才会创建实例。

  • 所以懒汉模式解决了饿汉模式带来的空间浪费问题。
public class SingletonObject {
    // 定义静态变量时,未初始化实例
    private static SingletonObject instance;
 
    // 私有化构造函数
    private SingletonObject(){
 
    }
 
    public static SingletonObject getInstance(){
        // 使用时,先判断实例是否为空,如果实例为空,则实例化对象
        // 这段代码在多线程的情况下是不安全的
        if (instance == null)
            instance = new SingletonObject();
        return instance;
    }
}
public class SingletonObject {
    private static SingletonObject instance;
 
    private SingletonObject(){
 
    }
 
    public synchronized static SingletonObject getInstance(){
        /**
         * 添加class类锁,影响了性能,加锁之后将代码进行了串行化
         * 我们的代码块绝大部分是读操作,在读操作的情况下,代码线程是安全的
         */
        if (instance == null)
            instance = new SingletonObject();
        return instance;
    }
}

懒汉模式的优缺点:

优点:

  • 实现了懒加载,节约了内存空间。

缺点:

  • 在不加锁的情况下,线程不安全,可能出现多份实例。
  • 在加锁的情况下,会是程序串行化,使系统有严重的性能问题。

双重检查锁模式

public class SingletonObject {
    private static SingletonObject instance;
 
    private SingletonObject(){
 
    }
 
    public static SingletonObject getInstance(){
 
        // 第一次判断,如果这里为空,不进入抢锁阶段,直接返回实例
        if (instance == null)
            synchronized (SingletonObject.class){
                // 抢到锁之后再次判断是否为空
                if (instance == null){
                    instance = new SingletonObject();
                }
            }
 
        return instance;
    }
}

在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

    private SingletonObject(){
     1   int x = 10;
     2   int y = 30;
     3  Object o = new Object();
                
    }

我们编写的顺序是1、2、3,JVM 会对它进行指令重排序。

  • 所以执行顺序可能是3、1、2,也可能是2、3、1,不管是那种执行顺序,JVM 最后都会保证所以实例都完成实例化。

如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。

双重检测锁出现空指针问题的原因就是出现在这里。

  • 当某个线程获取锁进行实例化时,其他线程就直接获取实例使用。
  • 由于JVM指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。

要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字。

    // 添加volatile关键字
    private static volatile SingletonObject instance;
 
    private SingletonObject(){
 
    }
 
    public static SingletonObject getInstance(){
 
        if (instance == null)
            synchronized (SingletonObject.class){
                if (instance == null){
                    instance = new SingletonObject();
                }
            }
 
        return instance;
    }
}

静态内部类单例模式

静态内部类单例模式实例由内部类创建。

由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。

静态属性由static修饰,保证只被实例化一次,并且严格保证实例化顺序。

public class SingletonObject {
 
 
    private SingletonObject(){
 
    }
    // 单例持有者
    private static class InstanceHolder{
        private  final static SingletonObject instance = new SingletonObject();
 
    }
    
    // 
    public static SingletonObject getInstance(){
        // 调用内部类属性
        return InstanceHolder.instance;
    }
}

枚举类单例模式

枚举类型是线程安全的,并且只会装载一次。

设计者充分的利用了枚举的这个特性来实现单例模式。

  • 枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
public class SingletonObject {
 
 
    private SingletonObject(){
 
    }
 
    /**
     * 枚举类型是线程安全的,并且只会装载一次
     */
    private enum Singleton{
        INSTANCE;
 
        private final SingletonObject instance;
 
        Singleton(){
            instance = new SingletonObject();
        }
 
        private SingletonObject getInstance(){
            return instance;
        }
    }
 
    public static SingletonObject getInstance(){
 
        return Singleton.INSTANCE.getInstance();
    }
}

破坏单例模式的方法及解决办法

除枚举方式外,其他方法都会通过反射的方式破坏单例。

反射是通过调用构造方法生成新的对象。

  • 所以如果想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例。
Class objClass = SingletonObject.class;
//获取类的构造器
Constructor constructor = objClass.getDeclaredConstructor();
//把构造器私有权限放开
constructor.setAccessible(true);
//正常的获取实例方式
SingletonObject staticInnerClass = SingletonObject.getInstance();
//反射创建实例
SingletonObject newStaticInnerClass = (SingletonObject) constructor.newInstance();
private SingletonObject(){
    if (instance !=null){
        throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
    }
}

如果单例类实现了序列化接口Serializable ,就可以通过反序列化破坏单例。

所以可以不实现序列化接口。

  • 如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。
  public Object readResolve() throws ObjectStreamException {
        return instance;
    }

策略模式

定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。

UML类图:

img

使用举例

以去公园为例,可以有很多路线方式去公园。

//抽象策略(Strategy)类
public interface Strategy {
 
	void goToPark(); //去公园
}
//具体策略(Concrete Strategy)类
public class BySubwayStrategy implements Strategy{
 
	@Override
	public void goToPark() {
		System.out.println("乘地铁去公园");
	}
 
}
 
//具体策略(Concrete Strategy)类
public class ByBusStrategy implements Strategy{
 
	@Override
	public void goToPark() {
		System.out.println("乘公交去公园");
	}
 
}
 
//具体策略(Concrete Strategy)类
public class ByTaxiStrategy implements Strategy{
 
	@Override
	public void goToPark() {
		System.out.println("打车去公园");
	}
 
}
//负责和具体的策略类交互,使得算法和客户端分离
public class Context {
	private Strategy strategy; //当前采用的具体对象,即具体的算法对象
	
	//支持使用构造注入
	public Context(Strategy strategy){
		this.strategy = strategy;
	}
	
	//也可以通过set注入
	public void setStrategy(Strategy strategy){
		this.strategy = strategy;
	}
	
	public void goToPark(){
		this.strategy.goToPark();
	}
	
}
public class Client {
 
	public static void main(String[] args) {
		Strategy bySubway = new BySubwayStrategy();
		//乘地铁
		Context ctx = new Context(bySubway);
		ctx.goToPark();
		//乘公交
		Strategy byBus = new ByBusStrategy();
		ctx.setStrategy(byBus);
		ctx.goToPark();
		//打车
		Strategy byTaxi = new ByTaxiStrategy();
		ctx.setStrategy(byTaxi);
		ctx.goToPark();
	}
}

模板方法模式

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。

  • Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

在模板模式中,一个抽象类公开定义了执行它的方法的方式/模板。

  • 它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

简单来说,有多个子类共有的方法,且逻辑相同,可以考虑作为模板方法。

模板模式的关键是:

  • 子类可以置换掉父类的可变部分,但是子类却不可以改变模板方法所代表的顶级逻辑。

UML类图:

img

抽象模板角色类:

public abstract class AbstractTemplate {
    /**
     * 模板方法
     */
    public void templateMethod(){
        //调用基本方法
        abstractMethod();
        hookMethod();
        concreteMethod();
    }
    /**
     * 基本方法的声明(由子类实现)
     */
    protected abstract void abstractMethod();
    /**
     * 基本方法(空方法)
     */
    protected void hookMethod(){}
    /**
     * 基本方法(已经实现)
     */
    private final void concreteMethod(){
        //业务相关的代码
    }
}

具体模板角色类:

public class ConcreteTemplate extends AbstractTemplate{
    //基本方法的实现
    @Override
    public void abstractMethod() {
        //业务相关的代码
    }
    //重写父类的方法
    @Override
    public void hookMethod() {
        //业务相关的代码
    }
}

访问者模式

表示一个作用于某对象结构中的各元素的操作。

它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

UML类图:

img

适配器模式

适配器模式它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

适配器模式有两种实现方式:类适配器和对象适配器。

  • 类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

UML类图:

img

代码实现

ITarget 表示要转化成的接口定义。

Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口。

// 类适配器: 基于继承
public interface ITarget {
  void f1();
  void f2();
  void fc();
}
public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}
public class Adaptor extends Adaptee implements ITarget {
  public void f1() {
    super.fa();
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
    

// 对象适配器:基于组合
public interface ITarget {
  void f1();
  void f2();
  void fc();
}
public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}
public class Adaptor implements ITarget {
  private Adaptee adaptee;
  
  public Adaptor(Adaptee adaptee) {
    this.adaptee = adaptee;
  }
  
  public void f1() {
    adaptee.fa(); //委托给Adaptee
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  public void fc() {
    adaptee.fc();
  }
}

如何选择

如果 Adaptee 接口并不多,那两种实现方式都可以。

如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那推荐使用类适配器。

  • 因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。

如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那推荐使用对象适配器。

  • 因为组合结构相对于继承更加灵活。

应用场景

封装有缺陷的接口设计:

假设依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到自身代码的可测试性。

为了隔离设计上的缺陷,希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。

统一多个类的接口设计:

假设系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。

但是,每个系统提供的过滤接口都是不同的,这就意味着没法复用一套逻辑来调用各个系统。

这个时候,就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样可以复用调用敏感词过滤的代码。

代理模式

代理模式提供了对目标对象额外的访问方式,即通过代理对象访问目标对象。

  • 这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

UML类图:

img

静态代理

在编译时就已经实现,编译完成后代理类是一个实际的class文件。

使用方式

创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。

之后再创建一个代理类,同时使其也实现这个接口,在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。

public interface UserDao {    
  void save();     
}
public class UserDaoImpl implements UserDao {
    @Override
    public void save() {
        System.out.println("正在保存用户...");
    }
}
public class TransactionHandler implements UserDao {
    //目标代理对象
    private UserDao target;
    //构造代理对象时传入目标对象
    public TransactionHandler(UserDao target) {
        this.target = target;
    }
    @Override
    public void save() {
        //调用目标方法前的处理
        System.out.println("开启事务控制...");
        //调用目标对象的方法
        target.save();
        //调用目标方法后的处理
        System.out.println("关闭事务控制...");
    }
}
public class Main {
    public static void main(String[] args) {
        //新建目标对象
        UserDaoImpl target = new UserDaoImpl();
        //创建代理对象, 并使用接口对其进行引用
        UserDao userDao = new TransactionHandler(target);
        //针对接口进行调用
        userDao.save();
    }
}

使用JDK静态代理很容易就完成了对一个类的代理操作。

但是JDK静态代理的缺点也暴露了出来:

  • 由于代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。

动态代理

JDK动态代理

使用JDK动态代理的五大步骤:

  • 通过实现InvocationHandler接口来自定义自己的InvocationHandler
  • 通过Proxy.getProxyClass获得动态代理类
  • 通过反射机制获得代理类的构造方法,方法签名为getConstructor(InvocationHandler.class)
  • 通过构造函数获得代理对象并将自定义的InvocationHandler实例对象传为参数传入
  • 通过代理对象调用目标方法
public interface IHello {
    void sayHello();
}
 public class HelloImpl implements IHello {
    @Override
    public void sayHello() {
        System.out.println("Hello world!");
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
 
public class MyInvocationHandler implements InvocationHandler {
 
    /** 目标对象 */
    private Object target;
 
    public MyInvocationHandler(Object target){
        this.target = target;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("------插入前置通知代码-------------");
        // 执行相应的目标方法
        Object rs = method.invoke(target,args);
        System.out.println("------插入后置处理代码-------------");
        return rs;
    }
}
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
 
public class MyProxyTest {
    public static void main(String[] args)
            throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
        // =========================第一种==========================
        // 1、生成$Proxy0的class文件
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 2、获取动态代理类
        Class proxyClazz = Proxy.getProxyClass(IHello.class.getClassLoader(),IHello.class);
        // 3、获得代理类的构造函数,并传入参数类型InvocationHandler.class
        Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
        // 4、通过构造函数来创建动态代理对象,将自定义的InvocationHandler实例传入
        IHello iHello1 = (IHello) constructor.newInstance(new MyInvocationHandler(new HelloImpl()));
        // 5、通过代理对象调用目标方法
        iHello1.sayHello();
 
        // ==========================第二种=============================
        /**
         * Proxy类中还有个将2~4步骤封装好的简便方法来创建动态代理对象,
         *其方法签名为:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h)
         */
        IHello  iHello2 = (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(), // 加载接口的类加载器
                new Class[]{IHello.class}, // 一组接口
                new MyInvocationHandler(new HelloImpl())); // 自定义的InvocationHandler
        iHello2.sayHello();
    }
}

JDK静态代理与JDK动态代理不同之处:

在静态代理中我们需要对哪个接口和哪个被代理类创建代理类,所以在编译前就需要代理类实现与被代理类相同的接口,并且直接在实现的方法中调用被代理类相应的方法。

但是动态代理不知道要针对哪个接口、哪个被代理类创建代理类,因为它是在运行时被创建的。

CGLIB

CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。

CGLIB代理实现如下:

  • 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
  • 然后在需要使用的时候,通过CGLIB动态代理获取代理对象。
 public class HelloService {
 
    public HelloService() {
        System.out.println("HelloService构造");
    }
 
    /**
     * 该方法不能被子类覆盖,Cglib是无法代理final修饰的方法的
     */
    final public String sayOthers(String name) {
        System.out.println("HelloService:sayOthers>>"+name);
        return null;
    }
 
    public void sayHello() {
        System.out.println("HelloService:sayHello");
    }
}
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
import java.lang.reflect.Method;
 
/**
 * 自定义MethodInterceptor
 */
public class MyMethodInterceptor implements MethodInterceptor{
 
    /**
     * sub:cglib生成的代理对象
     * method:被代理对象方法
     * objects:方法入参
     * methodProxy: 代理方法
     */
    @Override
    public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("======插入前置通知======");
        Object object = methodProxy.invokeSuper(sub, objects);
        System.out.println("======插入后者通知======");
        return object;
    }
}
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
 
public class Client {
    public static void main(String[] args) {
        // 代理类class文件存入本地磁盘方便我们反编译查看源码
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
        // 通过CGLIB动态代理获取代理对象的过程
        Enhancer enhancer = new Enhancer();
        // 设置enhancer对象的父类
        enhancer.setSuperclass(HelloService.class);
        // 设置enhancer的回调对象
        enhancer.setCallback(new MyMethodInterceptor());
        // 创建代理对象
        HelloService proxy= (HelloService)enhancer.create();
        // 通过代理对象调用目标方法
        proxy.sayHello();
    }
}

JDK代理要求被代理的类必须实现接口,有很强的局限性。

CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。

CGLIB在进行代理的时候都进行了哪些工作

生成的代理类继承被代理类:

  • 如果委托类被final修饰,那么它不可被继承,即不可被代理
  • 同样,如果委托类中存在final修饰的方法,那么该方法也不可被代理

代理类会为委托方法生成两个方法:

  • 一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法
  • 另一个是代理类独有的方法

当执行代理对象的方法时,会首先判断一下是否存在实现了MethodInterceptor接口的CGLIB$CALLBACK_0,如果存在,则将调用MethodInterceptor中的intercept方法。

  • intercept方法中,除了会调用委托方法,还会进行一些增强操作。

在Spring AOP中,典型的应用场景就是在某些敏感方法执行前后进行操作日志记录。

在CGLIB中,方法的调用并不是通过反射来完成的,而是直接对方法进行调用:

通过FastClass机制对Class对象进行特别的处理。

比如将会用数组保存method的引用,每次调用方法的时候都是通过一个index下标来保持对方法的引用。

Fastclass机制

CGLIB采用了FastClass的机制来实现对被拦截方法的调用。

FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。

三种代理方式之间对比

代理方式 实现 优点 缺点 特点
JDK静态代理 代理类与委托类实现同一接口,并且在代理类中需要硬编码接口 实现简单,容易理解 代理类需要硬编码接口,在实际应用中可能会导致重复编码,浪费存储空间并且效率很低 好像没啥特点
JDK动态代理 代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler并重写invoke方法来进行动态代理的,在invoke方法中将对方法进行增强处理 不需要硬编码接口,代码复用率高 只能够代理实现了接口的委托类 底层使用反射机制进行方法的调用
CGLIB动态代理 代理类将委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法;另一个是代理类独有的方法。在代理方法中,它会判断是否存在实现了MethodInterceptor接口的对象,若存在则将调用intercept方法对委托方法进行代理 可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口 不能对final类以及final方法进行代理 底层将方法全部存入一个数组中,通过数组索引直接进行方法调用

CGlib比JDK快?

使用CGLiB实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK6之前比使用Java反射效率要高。

  • 唯一需要注意的是,CGLib不能对声明为Final的方法进行代理, 因为CGLib原理是动态生成被代理类的子类。

在JDK6、JDK7、JDK8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率。

只有当进行大量调用的时候,JDK6和JDK7比CGLIB代理效率低一点,但是到JDK8的时候,JDK代理效率高于CGLIB代理。

  • 总之,每一次JDK版本升级,JDK代理效率都得到提升。

Spring如何选择用JDK还是CGLIB?

当Bean实现接口时,Spring就会用JDK的动态代理。

当Bean没有实现接口时,Spring使用CGlib实现。

可以强制使用CGlib。

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。

UML类图:

img

组合模式

将对象组合成树形结构以表示部分-整体的层次结构,它使得客户对单个对象和复合对象的使用具有一致性。

UML类图:

img

装饰器模式

动态地给一个对象添加一些额外的职责,就扩展功能而言,它比生成子类方式更为灵活。

UML类图:

img

外观模式

为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式Fcade不一定是接口,也可以是类,例如只有FacadeImpl这个类。

  • Interface1、Interface2、Interface3都是子系统接口。

  • 使用Facade即可实现对3个子系统的调用。

UML类图:

img

享元模式

运用共享技术来有效地支持大量细粒度对象的复用。

它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

UML类图:

img

责任链模式

解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。

将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。

UML类图:

img

抽象接口RequestHandler:

public interface RequestHandler {

    void doHandler(String req);
}

抽象类BaseRequestHandler

public abstract class BaseRequestHandler implements RequestHandler {

    protected RequestHandler next;

    public void next(RequestHandler next) {
        this.next = next;
    }
}

具体处理类AHandler:

public class AHandler extends BaseRequestHandler {

    @Override
    public void doHandler(String req) {
        // 处理自己的业务逻辑
        System.out.println("A中处理自己的逻辑");
        // 传递给下个类(若链路中还有下个处理类)
        if (next != null) {
            next.doHandler(req);
        }
    }
}

使用类Client

public class Client {
    public static void main(String[] args) {
        BaseRequestHandler a = new AHandler();
        BaseRequestHandler b = new BHandler();
        BaseRequestHandler c = new CHandler();
        a.next(b);
        b.next(c);
        a.doHandler("链路待处理的数据");
    }
}

命令模式

将一个请求封装为一个命令对象,使发出请求、执行请求解耦。

UML类图:

img

解释器模式

给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。

UML类图:

img

迭代器模式

提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。

UML类图:

img

中介者模式

用一个中介对象来封装一系列的对象交互。

中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

UML类图:

img

备忘录模式

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

  • 这样以后就可将该对象恢复到保存的状态。

UML类图:

img

观察者模式

定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。

UML类图:

img

状态模式

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它所属的类。

UML类图:

img

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!