常用的设计模式

本文深入浅出地介绍了单例模式、适配器模式、工厂模式和代理模式等经典设计模式的概念、实现方式及其应用场景,帮助读者更好地理解和运用这些模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.单例模式

概念:
  java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
  单例模式有以下特点:
  1、单例类只能有一个实例。
  2、单例类必须自己创建自己的唯一实例。
  3、单例类必须给所有其他对象提供这一实例。
  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。


一、懒汉式单例

//懒汉式单例类.在第一次调用的时候实例化自己

  1. public class Singleton {

  2. private Singleton() {}

  3. private static Singleton single=null;

  4. //静态工厂方法

  5. public static Singleton getInstance() {

  6. if (single == null) {

  7. single = new Singleton();

  8. }

  9. return single;

  10. }

  11. }

 

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)

但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,如果你第一次接触单例模式,对线程安全不是很了解,可以先跳过下面这三小条,去看饿汉式单例,等看完后面再回头考虑线程安全的问题:

 

1、在getInstance方法上加同步

  1. public static synchronized Singleton getInstance() {

  2. if (single == null) {

  3. single = new Singleton();

  4. }

  5. return single;

  6. }

 

 

2、双重检查锁定

  1. public static Singleton getInstance() {

  2. if (singleton == null) {

  3. synchronized (Singleton.class) {

  4. if (singleton == null) {

  5. singleton = new Singleton();

  6. }

  7. }

  8. }

  9. return singleton;

  10. }

 

3、静态内部类

  1. public class Singleton {

  2. private static class LazyHolder {

  3. private static final Singleton INSTANCE = new Singleton();

  4. }

  5. private Singleton (){}

  6. public static final Singleton getInstance() {

  7. return LazyHolder.INSTANCE;

  8. }

  9. }

这种比上面1、2都好一些,既实现了线程安全,又避免了同步带来的性能影响。
 

二、饿汉式单例

  1. //饿汉式单例类.在类初始化时,已经自行实例化

  2. public class Singleton1 {

  3. private Singleton1() {}

  4. private static final Singleton1 single = new Singleton1();

  5. //静态工厂方法

  6. public static Singleton1 getInstance() {

  7. return single;

  8. }

  9. }

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

 

三、登记式单例(可忽略)

 
  1. //类似Spring里面的方法,将类名注册,下次从里面直接获取。

  2. public class Singleton3 {

  3. private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();

  4. static{

  5. Singleton3 single = new Singleton3();

  6. map.put(single.getClass().getName(), single);

  7. }

  8. //保护的默认构造子

  9. protected Singleton3(){}

  10. //静态工厂方法,返还此类惟一的实例

  11. public static Singleton3 getInstance(String name) {

  12. if(name == null) {

  13. name = Singleton3.class.getName();

  14. System.out.println("name == null"+"--->name="+name);

  15. }

  16. if(map.get(name) == null) {

  17. try {

  18. map.put(name, (Singleton3) Class.forName(name).newInstance());

  19. } catch (InstantiationException e) {

  20. e.printStackTrace();

  21. } catch (IllegalAccessException e) {

  22. e.printStackTrace();

  23. } catch (ClassNotFoundException e) {

  24. e.printStackTrace();

  25. }

  26. }

  27. return map.get(name);

  28. }

  29. //一个示意性的商业方法

  30. public String about() {

  31. return "Hello, I am RegSingleton.";

  32. }

  33. public static void main(String[] args) {

  34. Singleton3 single3 = Singleton3.getInstance(null);

  35. System.out.println(single3.about());

  36. }

  37. }

 登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。 

这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。

 

饿汉式和懒汉式区别

从名字上来说,饿汉和懒汉,

饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,

而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

另外从以下两点再区分以下这两种方式:

 

1、线程安全:

饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。


 

2、资源加载和性能:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

至于1、2、3这三种实现又有些区别,

第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,

第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗

第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

 

什么是线程安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。

 

结论:由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。

对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,还有懒汉式为了实现线程安全的3种方式的细微差别。

 

二.适配器模式

适配器就是一种适配中间件,它存在于不匹配的二者之间,用于连接二者,将不匹配变得匹配,简单点理解就是平常所见的转接头,转换器之类的存在。

  适配器模式有两种:类适配器、对象适配器、接口适配器

  前二者在实现上有些许区别,作用一样,第三个接口适配器差别较大。

1、类适配器模式:

  原理:通过继承来实现适配器功能。

  当我们要访问的接口A中没有我们想要的方法 ,却在另一个接口B中发现了合适的方法,我们又不能改变访问接口A,在这种情况下,我们可以定义一个适配器p来进行中转,这个适配器p要实现我们访问的接口A,这样我们就能继续访问当前接口A中的方法(虽然它目前不是我们的菜),然后再继承接口B的实现类BB,这样我们可以在适配器P中访问接口B的方法了,这时我们在适配器P中的接口A方法中直接引用BB中的合适方法,这样就完成了一个简单的类适配器。

  详见下方实例:我们以ps2与usb的转接为例

ps2接口:Ps2

1 public interface Ps2 {
2     void isPs2();
3 }

USB接口:Usb

1 public interface Usb {
2     void isUsb();
3 }

USB接口实现类:Usber

复制代码

1 public class Usber implements Usb {
2 
3     @Override
4     public void isUsb() {
5         System.out.println("USB口");
6     }
7 
8 }

复制代码

适配器:Adapter

复制代码

1 public class Adapter extends Usber implements Ps2 {
2 
3     @Override
4     public void isPs2() {
5         isUsb();
6     }
7 
8 }

复制代码

测试方法:Clienter

复制代码

1 public class Clienter {
2 
3     public static void main(String[] args) {
4         Ps2 p = new Adapter();
5         p.isPs2();
6     }
7 
8 }

复制代码

显示结果:

USB口

实例讲解:

  我手中有个ps2插头的设备,但是主机上只有usb插头的插口,怎么办呢?弄个转换器,将ps2插头转换成为USB插头就可以使用了。

  接口Ps2:描述ps2接口格式

  接口Usb:描述USB接口格式

  类Usber:是接口Usb的实现类,是具体的USB接口格式

  Adapter:用于将ps2接口格式转换成为USB接口格式

2、对象适配器模式

  原理:通过组合来实现适配器功能。

  当我们要访问的接口A中没有我们想要的方法 ,却在另一个接口B中发现了合适的方法,我们又不能改变访问接口A,在这种情况下,我们可以定义一个适配器p来进行中转,这个适配器p要实现我们访问的接口A,这样我们就能继续访问当前接口A中的方法(虽然它目前不是我们的菜),然后在适配器P中定义私有变量C(对象)(B接口指向变量名),再定义一个带参数的构造器用来为对象C赋值,再在A接口的方法实现中使用对象C调用其来源于B接口的方法。

  详见下方实例:我们仍然以ps2与usb的转接为例

ps2接口:Ps2

1 public interface Ps2 {
2     void isPs2();
3 }

USB接口:Usb

1 public interface Usb {
2     void isUsb();
3 }

USB接口实现类:Usber

复制代码

1 public class Usber implements Usb {
2 
3     @Override
4     public void isUsb() {
5         System.out.println("USB口");
6     }
7 
8 }

复制代码

适配器:Adapter

复制代码

 1 public class Adapter implements Ps2 {
 2     
 3     private Usb usb;
 4     public Adapter(Usb usb){
 5         this.usb = usb;
 6     }
 7     @Override
 8     public void isPs2() {
 9         usb.isUsb();
10     }
11 
12 }

复制代码

测试类:Clienter

复制代码

1 public class Clienter {
2 
3     public static void main(String[] args) {
4         Ps2 p = new Adapter(new Usber());
5         p.isPs2();
6     }
7 
8 }

复制代码

结果显示:

USB口

实例讲解:

  我手中有个ps2插头的设备,但是主机上只有usb插头的插口,怎么办呢?弄个转换器,将ps2插头转换成为USB插头就可以使用了。

  接口Ps2:描述ps2接口格式

  接口Usb:描述USB接口格式

  类Usber:是接口Usb的实现类,是具体的USB接口格式

  Adapter:用于将ps2接口格式转换成为USB接口格式

3、接口适配器模式

  原理:通过抽象类来实现适配,这种适配稍别于上面所述的适配。

  当存在这样一个接口,其中定义了N多的方法,而我们现在却只想使用其中的一个到几个方法,如果我们直接实现接口,那么我们要对所有的方法进行实现,哪怕我们仅仅是对不需要的方法进行置空(只写一对大括号,不做具体方法实现)也会导致这个类变得臃肿,调用也不方便,这时我们可以使用一个抽象类作为中间件,即适配器,用这个抽象类实现接口,而在抽象类中所有的方法都进行置空,那么我们在创建抽象类的继承类,而且重写我们需要使用的那几个方法即可。

目标接口:A

复制代码

1 public interface A {
2     void a();
3     void b();
4     void c();
5     void d();
6     void e();
7     void f();
8 }

复制代码

适配器:Adapter

复制代码

1 public abstract class Adapter implements A {
2     public void a(){}
3     public void b(){}
4     public void c(){}
5     public void d(){}
6     public void e(){}
7     public void f(){}
8 }

复制代码

实现类:Ashili

复制代码

1 public class Ashili extends Adapter {
2     public void a(){
3         System.out.println("实现A方法被调用");
4     }
5     public void d(){
6         System.out.println("实现d方法被调用");
7     }
8 }

复制代码

测试类:Clienter

复制代码

1 public class Clienter {
2 
3     public static void main(String[] args) {
4         A a = new Ashili();
5         a.a();
6         a.d();
7     }
8 
9 }

复制代码

4、适配器模式应用场景

类适配器与对象适配器的使用场景一致,仅仅是实现手段稍有区别,二者主要用于如下场景:

  (1)想要使用一个已经存在的类,但是它却不符合现有的接口规范,导致无法直接去访问,这时创建一个适配器就能间接去访问这个类中的方法。

  (2)我们有一个类,想将其设计为可重用的类(可被多处访问),我们可以创建适配器来将这个类来适配其他没有提供合适接口的类。

  以上两个场景其实就是从两个角度来描述一类问题,那就是要访问的方法不在合适的接口里,一个从接口出发(被访问),一个从访问出发(主动访问)。

接口适配器使用场景:

  (1)想要使用接口中的某个或某些方法,但是接口中有太多方法,我们要使用时必须实现接口并实现其中的所有方法,可以使用抽象类来实现接口,并不对方法进行实现(仅置空),然后我们再继承这个抽象类来通过重写想用的方法的方式来实现。这个抽象类就是适配器。

 

三.工厂模式

 

1. 简单工厂模式

1.1 描述

 

简单工厂模式是由一个工厂对象根据收到的消息决定要创建哪一个类的对象实例。

1.2 使用场景

 

工厂类负责创建的对象比较少,客户只需要传入工厂类参数,对于如何创建对象(逻辑)不关心。简单工厂模式很容易违反高内聚低耦合的原则,因此一般只在很简单的情况下使用。

1.3 优点

最大的优点在于工厂类中包含了必要的逻辑,根据客户需要的逻辑动态实例化相关的类。

1.4 例子

  1. public interface Shape {

  2.  
  3. public void draw();

  4.  
  5. }

  6. public class Circle implements Shape {

  7.  
  8. @Override

  9. public void draw() {

  10. System.out.println("Inside Circle::draw() method.");

  11. }

  12.  
  13. }

  14. public class Rectangle implements Shape {

  15.  
  16. @Override

  17. public void draw() {

  18. System.out.println("Inside Rectangle::draw() method.");

  19. }

  20.  
  21. }

  22. public class Square implements Shape {

  23.  
  24. @Override

  25. public void draw() {

  26. System.out.println("Inside Square::draw() method.");

  27. }

  28.  
  29. }

  30. public class ShapeFactory {

  31.  
  32. public Shape getShape(String type) {

  33. if ("circle".equals(type)) {

  34. return new Circle();

  35. } else if ("rectangle".equals(type)) {

  36. return new Rectangle();

  37. } else if ("square".equals(type)) {

  38. return new Square();

  39. } else {

  40. return null;

  41. }

  42. }

  43.  
  44. }

 
  1. public class FactoryPatternDemo {

  2.  
  3. public static void main(String[] args) {

  4. ShapeFactory shapeFactory = new ShapeFactory();

  5. shapeFactory.getShape("circle").draw();

  6. shapeFactory.getShape("rectangle").draw();

  7. shapeFactory.getShape("square").draw();

  8. }

  9.  
  10. }

 

2. 工厂方法模式

2.1 描述

定义一个创建对象的工厂接口,让子类决定实例化哪一个类,将实际创建工作推迟到子类当中。

2.2 使用场景

 1、日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 2、数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 3、设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。

 

2.3 优点

 

创建对象的接口,让子类决定具体实例化的对象,把简单的内部逻辑判断移到了客户端。工厂方法模式克服了简单工厂所违背的开闭原则的缺点,又保持了封装对象创建过程的优点。扩展性高,想要增加一个产品,只要扩展一个工厂类就可以。

 

2.4 例子

 
  1. public interface ShapeFactory {

  2.  
  3. public Shape getShape();

  4.  
  5. }

 
  1. public class CircleFactory implements ShapeFactory {

  2.  
  3. @Override

  4. public Shape getShape() {

  5. return new Circle();

  6. }

  7.  
  8. }

 
  1. public class RectangleFactory implements ShapeFactory {

  2.  
  3. @Override

  4. public Shape getShape() {

  5. return new Rectangle();

  6. }

  7.  
  8. }

 
  1. public class SquareFactory implements ShapeFactory {

  2.  
  3. @Override

  4. public Shape getShape() {

  5. return new Square();

  6. }

  7.  
  8. }

 
  1. public class FactoryMethodDemo {

  2.  
  3. public static void main(String[] args) {

  4. ShapeFactory circleFactory = new CircleFactory();

  5. circleFactory.getShape().draw();

  6. ShapeFactory rectangleFactory = new RectangleFactory();

  7. rectangleFactory.getShape().draw();

  8. ShapeFactory squareFactory = new SquareFactory();

  9. squareFactory.getShape().draw();

  10. }

  11.  
  12. }

 

3. 抽象工厂

 

3.1 描述

抽象工厂是围绕一个超级工厂创建其他工厂,该超级工厂又称为其他工厂的工厂。提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们具体的类。

3.2 使用场景

系统的产品多于一个产品族,而系统只消费某一族的产品。

3.3 优点

当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

3.4 例子

 
  1. public interface Button {

  2.  
  3. public void processEvent();

  4.  
  5. }

 
  1. public interface Text {

  2.  
  3. public void getWholeText();

  4.  
  5. }

 
  1. public class LinuxButton implements Button {

  2.  
  3. @Override

  4. public void processEvent() {

  5. System.out.println("Inside LinuxButton::processEvent() method.");

  6. }

  7.  
  8. }

 
  1. public class WindowsButton implements Button {

  2.  
  3. @Override

  4. public void processEvent() {

  5. System.out.println("Inside WindowsButton::processEvent() method.");

  6. }

  7.  
  8. }

 
  1. public class LinuxText implements Text {

  2.  
  3. @Override

  4. public void getWholeText() {

  5. System.out.println("Inside LinuxText::getWholeText() method.");

  6. }

  7.  
  8. }

 
  1. public class WindowsText implements Text {

  2.  
  3. @Override

  4. public void getWholeText() {

  5. System.out.println("Inside WindowsText::getWholeText() method.");

  6. }

  7.  
  8. }

 
  1. public interface AbstractFactory {

  2.  
  3. public Button createButton();

  4.  
  5. public Text createText();

  6.  
  7. }

 
  1. public class LinuxFactory implements AbstractFactory {

  2.  
  3. @Override

  4. public Button createButton() {

  5. return new LinuxButton();

  6. }

  7.  
  8. @Override

  9. public Text createText() {

  10. return new LinuxText();

  11. }

  12.  
  13. }

 
  1. public class WindowsFactory implements AbstractFactory {

  2.  
  3. @Override

  4. public Button createButton() {

  5. return new WindowsButton();

  6. }

  7.  
  8. @Override

  9. public Text createText() {

  10. return new WindowsText();

  11. }

  12.  
  13. }

 
  1. public class AbstractFactoryDemo {

  2.  
  3. public static void main(String[] args) {

  4. AbstractFactory linuxFactory = new LinuxFactory();

  5. linuxFactory.createButton().processEvent();

  6. linuxFactory.createText().getWholeText();

  7. AbstractFactory windowsFactory = new WindowsFactory();

  8. windowsFactory.createButton().processEvent();

  9. windowsFactory.createText().getWholeText();

  10. }

  11.  
  12. }

 

4. 总结

无论是简单工厂模式,工厂方法模式,还是抽象工厂模式,他们都属于工厂模式,在形式和特点上也是极为相似的,他们的最终目的都是为了解耦。在使用时,我们不必去在意这个模式到底工厂方法模式还是抽象工厂模式,因为他们之间的演变常常是令人琢磨不透的。经常你会发现,明明使用的工厂方法模式,当新需求来临,稍加修改,加入了一个新方法后,由于类中的产品构成了不同等级结构中的产品族,它就变成抽象工厂模式了;而对于抽象工厂模式,当减少一个方法使的提供的产品不再构成产品族之后,它就演变成了工厂方法模式。
       所以,在使用工厂模式时,只需要关心降低耦合度的目的是否达到了。

 

四.代理模式

1、中介隔离作用

    代理模式,第一次接触它,是在学习.Net的时候看的一本书,叫做《大话设计模式》,至今已快三年了。相信看过这本书的同学们,都记得书中的代理模式,就是为他人做嫁衣裳的故事。好,我们回归代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用, 其特征是代理类与委托类有同样的接口,。代理模式是常用的java设计模式。

表现形式如下:

            

       以上一张图就是当初对代理模式的认识。

 

2、开闭原则,增加功能

     现在有了进一步的认识。代理类不仅仅是一个隔离客户端和委托类的中介。我们还可以借助代理来在增加一些功能,而不需要修改原有代码,严重的复合开闭原则哦。

    代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。

     就是这样,真正的业务功能还是有委托类来实现,但是在实现业务类之前的一些公共服务。例如在项目开发中我们没有加入缓冲,日志这些功能,后期想加入,我们就可以使用代理来实现,而没有必要打开已经封装好的委托类。

3、代理的分类

    根据以上对代理的理解,对于代理的具体实现,我们有不同的方式,如果按照代理的创建时期,代理类可以分为两种。:静态代理、动态代理。
    静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。 
    动态代理:在程序运行时,运用反射机制动态创建而程。

    1、静态代理

    那我们先来看一下静态代理。只需要三步即可实现。首先,我们需要定义业务接口,业务接口实现类,然后定义代理类,且实现业务接口;最后写一个Client来调用。

   第一:需要定义业务接口,业务接口实现类

 
  1. <span style="font-family:KaiTi_GB2312;font-size:18px;"> /**

  2. * 定义一个业务接口

  3. * @author Cassie

  4. */

  5. public interface Account {

  6. // 查询

  7. public void queryAccount ();

  8.  
  9. // 修改

  10. public void updateAccount ();

  11. }

  12.  
  13. <pre name="code" class="java"> /**

  14. * 接口实现类(包含业务逻辑)

  15. * 即:委托类

  16. * @author Cassie

  17. */

  18. public class AccountImpl implements Account{

  19.  
  20. @Override

  21. public void queryAccount() {

  22. System.out.println("查询方法...");

  23. }

  24.  
  25. @Override

  26. public void updateAccount() {

  27. System.out.println("修改方法...");

  28. }

  29.  
  30. } </span>



  

   第二:定义代理类,实现业务接口  

 
  1. <span style="font-family:KaiTi_GB2312;font-size:18px;">/**

  2. * 代理类(增强AccountImpl的功能)

  3. * @author Cassie

  4. */

  5. public class AccountProxy implements Account{

  6. private AccountImpl accountImpl;

  7.  
  8. /**

  9. * 重写默认构造函数

  10. * @param accountImpl :真正要执行业务的对象

  11. */

  12. public AccountProxy(AccountImpl accountImpl) {

  13. this.accountImpl =accountImpl;

  14. }

  15.  
  16. @Override

  17. public void queryAccount() {

  18. System.out.println("业务处理之前...");

  19. // 调用委托类的方法,这是具体的业务方法

  20. account>Impl.queryCount();

  21. System.out.println("业务处理之后...");

  22. }

  23.  
  24. @Override

  25. public void updateAccount() {

  26. System.out.println("业务处理之前...");

  27. // 调用委托类的方法;

  28. accountImpl.updateAccount();

  29. System.out.println("业务处理之后...");

  30. }

  31. } </span>


    第三:写客户端,我这里写的测试类。   

 
  1. <span style="font-family:KaiTi_GB2312;font-size:18px;">/**

  2. * 测试Account类

  3. * @author Cassie

  4. */

  5. public class TestAccount {

  6. public static void main(String[] args) {

  7. AccountImpl accountImpl = new AccountImpl();

  8. //在这里传入要调用的业务对象

  9. AccountProxy accountProxy = new AccountProxy(accountImpl);

  10. //开始调用业务对象的方法,这两个方法都被增强了。

  11. accountProxy.updateAcc>ount();

  12. accountProxy.queryAccount();

  13. }

  14. } </span>

   看到的执行效果:

<span style="font-family:KaiTi_GB2312;font-size:18px;">业务处理之前...</span>
<span style="font-family:KaiTi_GB2312;font-size:18px;">修改方法...</span>
<span style="font-family:KaiTi_GB2312;font-size:18px;">业务处理之后...</span>
 
  1. <span style="font-family:KaiTi_GB2312;font-size:18px;">业务处理之前...

  2. 查询方法...</span>

<span style="font-family:KaiTi_GB2312;font-size:18px;">业务处理之后...</span>

    好了,至此我们的静态代理实现了,但是问题也跟着来了,观察代码可以发现每一个代理类只能为一个接口服务,一个AccountProxy 类实现了一个Account接口,那么我要是有多个接口,是不是要写多个Proxy类与之对应。这样一来程序开发中必然会产生过多的代理,而且,所有的代理操作除了调用的方法不一样之外,其他的操作都一样,则此时肯定是重复代码。解决这一问题最好的做法是可以通过一个代理类完成全部的代理功能,那就引入了我们的动态代理了。

 

让我们就接着上篇博客的静态代理来开始今天的动态代理。

  动态代理

             静态代理需要在运行之前就写好代理类,这样就造成了代码的大量重复,所以我们通过动态代理在运行时期动态生成业务类的代理类,那么动态代理类是如何实现的呢?

        动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。java.lang.reflect 包中的Proxy类和InvocationHandler 接口提供了生成动态代理类的能力。 原来是利用反射的机制来实现的,今天我们不讨论反射,我们看JDK的动态代理的实现。

      JDK动态代理中包含一个类和一个接口: InvocationHandler接口,和我们定义的一个实现类“Proxy“,这是一个万能的代理类,我们就是通过这个代理类来动态代理的。
      InvocationHandler接口: 
      public interface InvocationHandler { 
      public Object invoke(Object proxy,Method method,Object[] args) throws Throwable; 
      } 
      参数说明: 
           Object proxy:指被代理的对象。 
           Method method:要调用的方法 
          Object[] args:方法调用时所需要的参数 

      可以将InvocationHandler接口的子类想象成一个代理的最终操作类,替换掉ProxySubject。

 

      Proxy类: 
      Proxy类是专门完成代理的操作类,可以通过此类为一个或多个接口动态地生成实现类,此类提供了如下的操作方法: 
     public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 
                               throws IllegalArgumentException 
      参数说明: 
           ClassLoader loader:类加载器 
          Class<?>[] interfaces:得到全部的接口 
          InvocationHandler h:得到InvocationHandler接口的子类实例 

 

     Ps:类加载器 
      在Proxy类中的newProxyInstance()方法中需要一个ClassLoader类的实例,ClassLoader实际上对应的是类加载器,在Java中主要有一下三种类加载器; 
      Booststrap ClassLoader:此加载器采用C++编写,一般开发中是看不到的; 
      Extendsion ClassLoader:用来进行扩展类的加载,一般对应的是jre\lib\ext目录中的类; 
      AppClassLoader:(默认)加载classpath指定的类,是最常使用的是一种加载器。

  3、使用JDK的动态代理

      还是使用上篇博客的代码,我们也是简单的说三步来实现:业务接口,业务实现类的创建;代理类的创建,业务的调用。

   我们看代码实现:  

 
  1. <span style="font-family:SimSun;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"> /**

  2. * 定义一个业务接口

  3. * @author Cassie

  4. */

  5. public interface Account {

  6. // 查询

  7. public void queryAccount ();

  8.  
  9. // 修改

  10. public void updateAccount ();

  11. }

  12.  
  13. /**

  14. * 接口实现类(包含业务逻辑)

  15. * 即:委托类

  16. * @author Cassie

  17. */

  18. public class AccountImpl implements Account{

  19.  
  20. @Override

  21. public void queryAccount() {

  22. System.out.println("查询方法...");

  23. }

  24.  
  25. @Override

  26. public void updateAccount() {

  27. System.out.println("修改方法...");

  28. }

  29.  
  30. }</span></span>

    关键的动态代理是如下几行代码:该处的代理类Proxy 不去实现具体的某个业务接口,而是实现了JDK提供的InvocationHander类。在Proxy中我们不需要知道具体的业务类,即委托类,在运行之前,讲委托类和代理类进行解耦,在运行期才发生的联系,是通过这句话实现的:private object target。然后在调用的时候通过getInstance 在确定谁是委托对象。

  1. <span style="font-family:SimSun;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">/**

  2. * JDK动态代理代理类

  3. *

  4. * @author Cassie

  5. *

  6. */

  7. public class Proxy implements InvocationHandler {

  8. private Object target;

  9. /**

  10. * 绑定委托对象并返回一个代理类

  11. * @param target

  12. * @return

  13. */

  14. public Object GetInstance(Object target) {

  15. this.target = target;

  16. //取得代理对象

  17. return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);

  18. }

  19.  
  20. @Override

  21. /**

  22. * 调用方法

  23. */

  24. public Object invoke(Object proxy, Method method, Object[] args)

  25. throws Throwable {

  26. Object result=null;

  27. System.out.println("before");

  28. //执行方法

  29. result=method.invoke(target, args);

  30. System.out.println("after");

  31. return result;

  32. }

  33.  
  34. } </span></span>

再看我们的客户端调用:这时候才传入真正的委托类。

 
  1. <span style="font-family:SimSun;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class TestProxy {

  2.  
  3. public static void main(String[] args) {

  4. Proxy proxy = new Proxy();

  5. // 在这里进行真正的对象传入

  6. Account account= (Account )proxy.getInstance(new AccountImpl());

  7. proxy.queryAccount();

  8. }

  9.  
  10. }</span></span>


以上过程就是JDK动态代理的实现,我们发现JDK动态代理帮我们讲代理类和委托类的绑定关系延迟了,什么时候用,什么时候调,这样我们的业务类不仅得到了增强,还简化了代码。

    当然,JDK的动态代理也有缺陷,不知道你发现了没有,这里的每个委托类都必须是要有接口的,也就是说JDK的动态代理依靠接口实现,要是我一个没有接口的类想被代理怎么办?

    如果有些类并没有实现接口,则不能使用JDK代理,这就要使用cglib动态代理了。请看下篇博客。

 

接着上篇博客的代理模式,我们继续,上篇博客介绍了JDK的动态代理,但是JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,这样就存在一定的局限性。对于这种情况,我们采用CGLIB来实现。

 3、CGLIB动态代理

     cglib是针对类来实现代理的,其实现原理:CGLIB的底层采用ASM字节码生成框架,使用字节码技术生成代理,比使用反射生成代理的效果要高,是对指定的目标类生成一个子类,并覆盖其中方法实现增强。但是也有一点点不足,因为采用的是继承,所以不能对final修饰的类进行代理。 

    还是使用以前的代码,依然还是简单的三步来实现。第一:建立一个普普通通的业务类;第二:写CGLIB代理类;第三:写测试代码或者客户端调用。这里的不同是第一步中,我们不需要在建接口了,只是一个普普通通的java类。

看代码:

 

 
  1. <span style="font-family:KaiTi_GB2312;font-size:18px;"> /**

  2. * 业务类(包含业务逻辑)

  3. * 即:委托类

  4. * @author Cassie

  5. */

  6. public class Account{

  7.  
  8. @Override

  9. public void queryAccount() {

  10. System.out.println("查询方法...");

  11. }

  12.  
  13. @Override

  14. public void updateAccount() {

  15. System.out.println("修改方法...");

  16. }

  17.  
  18. }</span>

     然后,我们来写CGLIB代理类:

 

 
  1. <span style="font-family:KaiTi_GB2312;font-size:18px;">import java.lang.reflect.Method;

  2. import net.sf.cglib.proxy.Enhancer;

  3. import net.sf.cglib.proxy.MethodInterceptor;

  4. import net.sf.cglib.proxy.MethodProxy;

  5.  
  6. /**

  7. * 使用cglib动态代理

  8. * @author Cassie

  9. * 实现MethodInterceptor 接口

  10. */

  11. public class CglibProxy implements MethodInterceptor {

  12.  
  13. //委托对象,运行时定类型

  14. private Object target;

  15.  
  16. /**

  17. * 创建代理对象

  18. *

  19. * @param target

  20. * @return

  21. */

  22. public Object getInstance(Object target) {

  23. this.target = target;

  24. Enhancer enhancer = new Enhancer();

  25. enhancer.setSuperclass(this.target.getClass());

  26. // 回调方法

  27. enhancer.setCallback(this);

  28. // 创建代理对象

  29. return enhancer.create();

  30. }

  31.  
  32. @Override

  33. // 回调方法

  34. public Object intercept(Object obj, Method method, Object[] args,

  35. MethodProxy proxy) throws Throwable {

  36. System.out.println("before");

  37. Object result = proxy.invokeSuper(obj, args);

  38. System.out.println("after");

  39. return result

  40. }

  41.  
  42. } </span>

    最后写测试类调用

 

 
  1. <span style="font-family:KaiTi_GB2312;font-size:18px;">public class TestCglib {

  2.  
  3. public static void main(String[] args) {

  4. //实例化代理

  5. CglibProxy cglib=new CglibProxy();

  6. //通过代理拿到对象

  7. Account account = (Account)cglib.getInstance(new Account());

  8. account.query();

  9. }

  10. }</span>


          通过以上代码,我们发现proxy.invokeSuper(obj,arg)是执行的关键。

         使用CGLIB,需要实现 CGLib 给我们提供的 MethodInterceptor 实现类,并填充 intercept() 方法。方法中最后一个 MethodProxy 类型的参数 proxy,值得注意!CGLib 给我们提供的是方法级别的代理,也可以理解为对方法的拦截。我们直接调用 proxy 的 invokeSuper() 方法,将被代理的对象 obj 以及方法参数 args 传入其中即可。

         至此,CGLIB代理也实现了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值