《设计模式》之一文带你理解单例、JDK动态代理、CGLIB动态代理、静态代理

个人认为我在动态代理方面的分析算是比较深入了,下次更新再修改一下,争取做到最好,后续还有建造者模式、模板方法、适配器、外观、责任链、策略和原型模式的深入!各位读者如果觉得还不错的可以持续关注哦。谢谢各位!!!

我的github,到时上传例子代码
https://github.com/tihomcode


《设计模式》之一文带你理解建造者模式、模板方法、适配器模式、外观模式
《设计模式》之一文带你理解策略模式、原型模式(深浅拷贝)、观察者模式、装饰模式


设计模式

设计模式的六大原则

开闭原则(Open Close Principle)

开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

能够提供扩展性

里氏代换原则(Liskov Substitution Principle)

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科

重写、继承

依赖倒转原则(Dependence Inversion Principle)

这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。

面向接口编程

接口隔离原则(Interface Segregation Principle)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。

迪米特法则(最少知道原则)(Demeter Principle)

为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。

减少模块之间的相互依赖关系

合成复用原则(Composite Reuse Principle)

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

单例

什么是单例

保证在一个JVM中只能存在一个实例,保证对象的唯一性。

必须自行创建这个实例

必须自行向整个程序提供这个实例

应用场景

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗?

  2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

  3. 网站的计数器,一般也是采用单例模式实现,否则难以同步。

  4. 应用程序的日志应用,一般使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

  5. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

  6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。

  7. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

  8. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

  9. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

单例的优缺点

优点:节约内存、重复利用、方便管理

缺点:线程安全问题

单例创建方式

饿汉式

类初始化时,会立即加载该对象,线程天生安全,调用效率高。

public class HungryType {

    private static final HungryType hungryType = new HungryType();

    private HungryType() {

    }

    public HungryType getInstance() {
        return hungryType;
    }
}
public class HungryTest {
    public static void main(String[] args) {
        HungryType hungryType1 = HungryType.getInstance();
        HungryType hungryType2 = HungryType.getInstance();
        System.out.println(hungryType1==hungryType2);  //true
    }
}

因为使用了static final保证了唯一性,不可修改,且已经实例化过了,所以调用的时候只能拿到唯一的一个hungryType。

懒汉式

类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能,天生线程不安全,需要解决线程安全问题,所以效率比较低。

public class LazyType {

    private static LazyType lazyType;

    private LazyType() {

    }

    //线程安全问题,如果多个线程访问lazyType的时候,可能会创建多个对象,那么就与单例原则违背了
    public static synchronized LazyType getInstance() {
        if(lazyType==null) {
            lazyType = new LazyType();
        }
        return lazyType;
    }
}
public class LazyTest {
    public static void main(String[] args) {
        LazyType lazyType1 = LazyType.getInstance();
        LazyType lazyType2 = LazyType.getInstance();
        System.out.println(lazyType1==lazyType2);  //true
    }
}

为了解决线程安全问题,在方法上加上synchronized关键字,但是这样就使效率下降了。

静态内部类

结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

public class StaticInnerClassType {
    private StaticInnerClassType() {
        System.out.println("初始化。。。");
    }

    public static class SingletonClassInstance {
        private static final StaticInnerClassType staticInnerClassType = new StaticInnerClassType();
    }

    //方法没有同步
    public static StaticInnerClassType getInstance() {
        System.out.println("getInstance()");
        return SingletonClassInstance.staticInnerClassType;
    }
}
public class StaticInnerClassTest {
    public static void main(String[] args) {
        StaticInnerClassType staticInnerClassType1 = StaticInnerClassType.getInstance();
        StaticInnerClassType staticInnerClassType2 = StaticInnerClassType.getInstance();
        System.out.println(staticInnerClassType1==staticInnerClassType2);
    }
}

缺点:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久区的对象

枚举单例

使用枚举实现单例模式,优点是实现简单、调用效率高,枚举本身就是单例,由JVM从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点是没有延迟加载。

public class EnumType {
    private EnumType() {

    }

    public static EnumType getInstance() {
        return SingletonEnum.INSTANCE.getInstance();
    }

    static enum SingletonEnum{
        INSTANCE;
        private EnumType enumType;

        private SingletonEnum() {
            enumType = new EnumType();
        }

        public EnumType getInstance() {
            return this.enumType;
        }
    }
}
public class EnumTest {
    public static void main(String[] args) {
        EnumType enumType1 = EnumType.getInstance();
        EnumType enumType2 = EnumType.getInstance();
        System.out.println(enumType1==enumType2);  //true
    }
}

双重检测锁式

因为JVM本质重排序的原因,可能会初始化多次,不推荐使用

public class DoubleDetectionLockType {
    //保证可见性,volatile本身可以禁止重排序,但是有博客说加上后也没用
    private static volatile DoubleDetectionLockType doubleDetectionLockType;

    private DoubleDetectionLockType() {

    }

    public static DoubleDetectionLockType getInstance() {
        if(doubleDetectionLockType==null) {
            synchronized (DoubleDetectionLockType.class) {
                if(doubleDetectionLockType==null) {
                    //当在new的时候,会进行很多步骤,比如复制内存、进行赋值,底层可能会进行重排序
                    //在多线程的情况下,可能本来已经被赋了值,在优化后可能会重复创建
                    doubleDetectionLockType = new DoubleDetectionLockType();
                }
            }
        }
        return doubleDetectionLockType;
    }
}
public class DoubleDetectionLockTest {
    public static void main(String[] args) {
        DoubleDetectionLockType doubleDetectionLockType1 = DoubleDetectionLockType.getInstance();
        DoubleDetectionLockType doubleDetectionLockType2 = DoubleDetectionLockType.getInstance();
        System.out.println(doubleDetectionLockType1==doubleDetectionLockType2); //true
    }
}

单例防止反射漏洞攻击

在构造函数中,只能允许初始化一次即可解决

public class PreventAttackSingleton {

    private static boolean flag = false;

    private PreventAttackSingleton() {
        if (flag==false) {
            flag = !flag;
        } else {
            throw new RuntimeException("单例模式被侵犯!");
        }
    }
    .....
}

单例创建方式的选择

如果不需要延迟加载单例,可以使用枚举或者饿汉式,相对来说枚举好于饿汉式。

如果需要延迟加载,可以使用静态内部类或者懒汉式,相对来说静态内部类好于懒汉式。

初始化时最好使用的饿汉式,比如读取配置文件、Spring初始化。

工厂模式

工厂模式实现了创建者和调用者分离,工厂模式分为简单工厂、工厂方法、抽象工厂模式

工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。利用工厂模式可以降低程序的耦合性,为后期的维护修改提供了很大的便利。将选择实现类、创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。

简单工厂模式

优点:简单工厂模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。明确区分了各自的职责和权力,有利于整个软件体系结构的优化。

缺点:很明显工厂类集中了所有实例的创建逻辑,容易违反GRASPR的高内聚的责任分配原则

在这里插入图片描述
在这里插入图片描述

工厂方法模式

工厂方法模式Factory Method,又称多态性工厂模式。在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。该核心类成为一个抽象工厂角色,仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。

在这里插入图片描述
在这里插入图片描述

抽象工厂模式

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

小结和区别

简单工厂 : 用来生产同一等级结构中的任意产品。(不支持拓展增加产品),就是只有一个汽车工厂,根据名称生产不同的车

工厂方法 :用来生产同一等级结构中的固定产品。(支持拓展增加产品),就是分多个汽车工厂,有奥迪工厂、奔驰工厂、宝马工厂,想生产什么车就到这些不同品牌的工厂去创建

抽象工厂 :用来生产不同产品族的全部产品。(不支持拓展增加产品;支持增加产品族),就是有很多汽车工厂,但是每个汽车工厂下又有不同发动机、座椅,所以在宝马工厂里就实现发动机A和座椅B、在奔驰工厂里就实现发动机B和座椅A

个人理解:
抽象工厂模式和工厂模式的区别是在于不同的维度。抽象工厂是多维的,举个例子,汽车分为小轿车、越野车、火车这几种类型,不同的类型下又有不同的规格,比如以小轿车来说:2…0排量的小驾车和2.4排量的小车,这是不同的规格。所以在设计抽象工厂接口时,抽象工厂所抽象是轿车的类型,抽象工厂的实现是提供生产多种轿车类型的实例。这个实例再去提供生产不同规格轿车的实例。而工厂模式是单维对的,只提供一种类型多种规格的轿车。

代理模式

通过代理控制对象的访问,可以详细访问某个对象的方法,在这个方法调用处理之前或调用后处理。既(AOP的微实现),AOP核心技术面向切面编程。

在没有代理的时候我们想去实现在方法前后加上一些处理时我们最直观的想法就是使用继承聚合。继承实现要扩展方法的类,复写它的方法;聚合是重新定义一个类,实现接口并且引入要扩展方法的类,再对方法内部进行修改,这里可能讲的比较抽象。

静态代理

静态代理是需要生成代理对象的

//接口
public interface IUserDao {

    public void add();

}

//实现类
public class UserDaoImpl implements IUserDao {

    public void add() {
        System.out.println("add方法。。。");
    }
}

//代理类
public class UserDaoProxy implements IUserDao {

    private IUserDao iUserDao;

    public UserDaoProxy(IUserDao iUserDao) {
        this.iUserDao = iUserDao;
    }

    public void add() {
        System.out.println("开启事务");
        iUserDao.add();
        System.out.println("关闭事务");
    }
}

//main方法
public class ClientDemo {
    public static void main(String[] args) {
        //未代理
        IUserDao userDao = new UserDaoImpl();
        userDao.add();
        //静态代理
        UserDaoProxy userDaoProxy = new UserDaoProxy(userDao);
        userDaoProxy.add();
    }
}

一个接口对应一个代理类,那么业务量大的时候,可能要写几百个代理类,而且每增加一个方法就要修改代码,使得代码不易扩展且很冗余,一般不使用静态代理

动态代理

那么动态代理通俗点说就是用一个类来动态的实现扩展,不需要重复修改或创建代理类。

可能我们会先想到反射机制,但是反射只能获取实例对象的属性和方法,并对其进行调用和赋值,并不能实现在方法前后进行扩展。

所以如果我们可以动态生成Proxy类,并且动态编译。然后,再通过反射创建对象并加载到内存中,不就实现了对任意对象进行代理了吗

在这里插入图片描述

JDK动态代理

原理:是根据类加载器和接口创建代理类(此代理类是接口的实现类,所以必须使用接口,面向接口生成代理,位于java.lang.reflect包下)

实现方式:

  1. 通过实现InvocationHandler接口创建自己的调用处理器 IvocationHandler handler = new InvocationHandlerImpl(…);

  2. 通过为Proxy类指定ClassLoader对象和一组interface创建动态代理类Class clazz = Proxy.getProxyClass(classLoader,new Class[]{…});

  3. 通过反射机制获取动态代理类的构造函数,其参数类型是调用处理器接口类型Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});

  4. 通过构造函数创建代理类实例,此时需将调用处理器对象作为参数被传入Interface Proxy = (Interface)constructor.newInstance(new Object[] (handler));

/**
 * 每次生成动态代理对象时,实现InvocationHandler接口的调用处理器对象
 * @author TiHom
 * create at 2018/11/10 0010.
 */
public class InvocationHandlerImpl implements InvocationHandler {

    private Object target;  //目标代理对象

    public InvocationHandlerImpl(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //这里是调用要代理的对象
        System.out.println("动态代理-开启事务");
        Object invoke = method.invoke(target,args);
        System.out.println("动态代理-提交事务");
        return invoke;
    }

    public static void main(String[] args) {
        IUserDao iUserDao = new UserDaoImpl();
        //通过实现InvocationHandler接口创建自己的调用处理器 IvocationHandler handler = new InvocationHandlerImpl(…);
        InvocationHandlerImpl invocationHandler = new InvocationHandlerImpl(iUserDao);
        //通过为Proxy类指定ClassLoader对象和一组interface创建动态代理类Class clazz = Proxy.getProxyClass(classLoader,new Class[]{…}); 
        ClassLoader classLoader = iUserDao.getClass().getClassLoader();
        Class<?>[] interfaces = iUserDao.getClass().getInterfaces();
        // 主要装载器、一组接口及调用处理动态代理实例
        IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
        userDao.add();
    }
}

invoke三个参数的介绍

proxy:指代我们所代理的那个真实对象
method:指代的是我们所要调用真实对象的某个方法的Method对象
args:指代的是调用真实对象某个方法时接受的参数

newProxyInstance三个参数的介绍

classLoader:一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载

interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了

invocationHandler:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上

newProxyInstance就是JVM运行时动态生成一个代理对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行时动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy为中,最后一个数字表示对象的标号。(如$Proxy0)

增加InvocationHandler接口是实现任意对象的关键,且可以根据自己的需求对代理类进行自定义的处理,不过这里设计的巧妙之处在于,InvocationHandler是一个接口,真正的实现由用户指定。另外,在每一个方法执行的时候,invoke方法都会被调用 ,这个时候如果你需要对某个方法进行自定义逻辑处理,可以根据method的特征信息进行判断分别处理。

在这里插入图片描述

CGLIB动态代理

使用cglib[Code Generation Library]实现动态代理,并不要求委托类必须实现接口,底层采用asm字节码生成框架生成代理类的字节码

public class CglibProxy implements MethodInterceptor {

    //目标代理对象
    private Object targetObject;

    private Object getInstance(Object target) {
        this.targetObject = target;
        //操作字节码 生成虚拟子类,因为要扩展,所以虚拟子类复写父类的方法
        Enhancer enhancer = new Enhancer();
        //设置超类
        enhancer.setSuperclass(target.getClass());
        //回调给当前的cglib代理对象
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("开启事务");
        Object invoke = methodProxy.invoke(targetObject,objects);
        System.out.println("提交事务");
        return invoke;
    }

    public static void main(String[] args) {
        CglibProxy cglibProxy = new CglibProxy();
        UserDaoImpl userDaoImpl = (UserDaoImpl) cglibProxy.getInstance(new UserDaoImpl());
        userDaoImpl.add();
    }
}

CGLIB动态代理与JDK动态区别

Java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

在Spring中:

1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP

2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP

3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

JDK动态代理只能对实现了接口的类生成代理,而不能针对类 。
CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 。
因为是继承,所以该类或方法最好不要声明成final ,final可以阻止继承和多态。

参考文章:

http://www.cnblogs.com/xiaoluo501395377/p/3383130.html

https://juejin.im/post/5a99048a6fb9a028d5668e62

©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页