天天看点

创建型设计模式之原型模式

作者:Java机械师

概述

设计模式是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。

大部分设计模式要解决的都是代码的可扩展性问题。

对于灵活多变的业务,需要用到设计模式,提升扩展性和可维护性,让代码能适应更多的变化;

设计模式的核心就是,封装变化,隔离可变性

设计模式解决的问题:

  • 创建型设计模式主要解决“对象的创建”问题,创建和使用代码解耦;
  • 结构型设计模式主要解决“类或对象的组合或组装”问题,将不同功能代码解耦;
  • 行为型设计模式主要解决的就是“类或对象之间的交互”问题。将不同的行为代码解耦,具体到观察者模式,它是将观察者和被观察者代码解耦。

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

  • 单例模式用来创建全局唯一的对象。
  • 工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
  • 建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
  • 原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。

设计模式关注重点: 了解它们都能解决哪些问题,掌握典型的应用场景,并且懂得不过度应用。

经典的设计模式有 23 种。随着编程语言的演进,一些设计模式(比如 Singleton)也随之过时,甚至成了反模式,一些则被内置在编程语言中(比如 Iterator),另外还有一些新的模式诞生(比如 Monostate)。

常用的有:单例模式、工厂模式(工厂方法和抽象工厂)、建造者模式。

不常用的有:原型模式。

原型模式:

系统变的复杂,系统的层次划分越来越细,边界也越来越明确。 然后每一层之间一般都有自己要处理的领域对象,统称为pojo一般在model或者domain包下(类的后缀不能为pojo)。

常见的一些模型类型:

  • PO、DO:持久层对象,一般和数据库直接打交道。
  • DTO:数据传输对象,系统之间的交互,再服务层提供服务的时候输出到其它系统。
  • VO:视图对象,用于前端模型展示。 当然有时候前端也可以看做另外一个系统,使用DTO模型;
  • BO:业务逻辑对象,比较少用...

这些模型对象常常需要相互拷贝传递数据;

创建型设计模式之原型模式

定义:

利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象

第一种实现:浅拷贝

原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。

如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。大数据量的拷贝操作非常耗时,这种情况下比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用深拷贝。

第二种实现:深拷贝

第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。根据这个思路对之前的代码进行重构。重构之后的代码如下所示:

第二种方法:先将对象序列化,然后再反序列化成新的对象。

上面两种实现方法,不管采用哪种,深拷贝都要比浅拷贝耗时、耗内存空间。针对我们这个应用场景,有没有更快、更省内存的实现方式呢?

可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,我们再使用深度拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象。毕竟需要更新的数据是很少的。这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的中数据都是老版本的数据。

java复制代码public class Demo {
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;

  public void refresh() {
    // Shallow copy
    HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();

    // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
        maxNewUpdatedTime = searchWord.getLastUpdateTime();
      }
      if (newKeywords.containsKey(searchWord.getKeyword())) {
        newKeywords.remove(searchWord.getKeyword());
      }
      newKeywords.put(searchWord.getKeyword(), searchWord);
    }

    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }

  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}
           

应用场景:

两个差别不大的对象,可以通过拷贝来创建新的对象,以达到节省创建时间的目的。

模型之间的转换:

在项目中经常需要使用到Java的对象拷贝和属性复制,如DTO、VO和数据库Entity之间的转换,经常需要用到拷贝;

建议不要用的方式

  • 手写get\set; 虽然性能高,但是费劲并且眼花缭乱,一不小心就写错了,难以维护,复用度不高
  • BeanUtils,apacha和spring包下都有对应的类,但是底层用到的都是反射,性能比较差,大流量的情况下一般不用
  • 直接fastjson,gc会很频繁,而且性能比较差
  • 序列化和反序列化进行深拷贝涉及到I/O操作,即从内存到磁盘或网络的数据传输,效率较低,不建议使用
创建型设计模式之原型模式

常用的方式

  • cglib的beanCopier,开销在创建BeanCopier,一般在创建类的时候提前创建好一个,在代码运行的时候直接进行copy,性能接近原生。
  • mapstruct 性能和原生代码一样,支持复杂的转化场景,实现原理同lombok编译的时候生成对应的代码。MapStruct底层是通过调用(settter/getter)来实现的,而不是反射来快速执行。动态代理实现,BeanCopier是Cglib包中的一个类,用于对象的复制。目标对象必须先实例化 而且对象必须要有setter方法。由于BeanCopier是动态代理实现所以性能上比前两个要好的多。

以上从技术分类的角度来看:

  • 反射:fastjson,beanutil 都不建议用
  • get\set: beancoper通过字节码进行getset,mapstruct编译的时候生成getset。 性能相对较好。

反射性能差的原因:

java反射之所以慢,根本原因是编译器没法对反射相关的代码做优化。

  1. 相比非反射的代码,性能开销大。反射尤其不能用在执行频率高的代码段。
  2. 反射功能在运行时,会涉及要求更多的权限。对于受限的安全上下文中,使用反射时需要谨慎。

我们都知道 Java 代码是需要编译才能在虚拟机里运行的,但其实 Java 的编译期是一段不确定的操作过程。因为它可能是一个前端编译器(如 Javac)把 *.java 文件编译成 *.class 文件的过程;也可能是程序运行期的即时编译器(JIT 编译器,Just In Time Compiler)把字节码文件编译成机器码的过程;还可能是静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成本地机器码的过程。

其中即时编译器(JIT)在运行期的优化过程对于程序运行来说更重要,Java虚拟机在编译阶段的代码优化就在这里进行,由于反射涉及动态解析的类型,因此无法执行某些Java虚拟机优化。因此,反射操作的性能要比非反射操作慢,因此应该避免在对性能敏感的应用程序中频繁使用Java反射来创建对象。

使用方式

如果说对象比较简单的时候,使用BeanCopier就可以了,因为spring的aop依赖cglib,默认情况下就已经引入了对应的包了,不需要额外的依赖直接就可以用。

如果很复杂的模型之间的转换,并且对性能有更极致的要求,考虑使用下MapStruct。

BeanCopier

常用的bean拷贝工具类当中,主要有Apache提供的beanUtils、Spring提供的beanUtils、Cglib提供的beanCopier,性能上分析如下表所示

创建型设计模式之原型模式

上表当中可以发现三者性能:cglib > spring > hutool

本次所讲的内容是关于BeanCopier类的使用,当我们需要拷贝大量的数据,使用这个是最快的,而对于拷贝少量对象时,和其它的拷贝工具类速度也差不多,现在CGLIB也并入Spring,所以在Spring项目中可以直接使用它,不需要添加其他maven

优点:

  • 拷贝速度快

缺点:

  • 无法实现null字段跳过
  • 每次使用都需要调用create函数,如果上述实验把create放到循环里,结果会变成5s
  • 无法实现复杂类型的拷贝(如List)

在使用他的时候,我们需要先创建一个BeanCopier对象,源代码如下

java复制代码public static BeanCopier create(Class source, Class target, boolean useConverter) {
    Generator gen = new Generator();
    gen.setSource(source);
    gen.setTarget(target);
    gen.setUseConverter(useConverter);
    return gen.create();
}
           

create函数参数解析:

  • 第一个参数source:需要拷贝的对象
  • 第二个参数target:拷贝后的目标对象类型
  • 第三个参数useConverter:用户控制转换器,是否使用自定义的转换器

useConverter控制权限转换:

这个是用户控制转换器,如果设置为false,它会对拷贝的对象和被拷贝的对象的类型进行判断,如果类型不同就不会拷贝,如果要使他会拷贝,就需要设置为true,自己拿到控制权对其进行处理,一般情况下使用的都是false

最简单的使用方式

BeanCopier beanCopier = BeanCopier.create(UserDO.class, UserDTO.class, false);

bean.copy即可;

java复制代码private static void simpleBeanCopy() {
    BeanCopier beanCopier = BeanCopier.create(UserDO.class, UserDTO.class, false);
    UserDO userDO = new UserDO();
    userDO.setId(1L);
    userDO.setName("aihe");
    userDO.setGmtCreate(new Date());
    userDO.setGender(0);
    userDO.setPassword("xxxxxx");
    UserDTO userDTO = new UserDTO();
    beanCopier.copy(userDO, userDTO,null);
    Assert.assertEquals("名称未成功拷贝",userDTO.getName(),"aihe");
    Assert.assertEquals("Id未成功拷贝", 1L, (long)userDTO.getId());
    Assert.assertEquals("性别未成功拷贝", Integer.valueOf(0),userDTO.getGender());
  }
           

创建可复用的BeanCopier工具类

由于每次使用工具类进行拷贝都需要调用create()函数创建一个BeanCopier对象,可以将创建过的BeanCopier实例可以放到缓存中,下次相同的转换可以直接获取,提升性能 【注】在这里,以源类名 + “_” + 目标类名作为key,对应创建好的BeanCopier作为value

java复制代码package com.vinjcent.api.utils;

import org.springframework.cglib.beans.BeanCopier;
import org.springframework.cglib.core.Converter;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * @author vinjcent
 * @description cglib包中的bean拷贝(性能优于Spring当中的BeanUtils)
 * @since 2023/3/28 14:22
 */
public class BeanCopierUtils {


    /**
     * 创建一个map来存储BeanCopier缓存
     */
    private static final Map<String, BeanCopier> BEAN_COPIER_MAP = new ConcurrentHashMap<>();

    /**
     * 深拷贝,我们可以直接传实例化的拷贝对象和被实例化的拷贝对象进行深拷贝
     *
     * @param source 源对象
     * @param target 目标类
     */
    private static void copy(Object source, Object target) {
        // 获取当前两者转换map对应的key
        String key = getKey(source, target);
        BeanCopier beanCopier;
        // 判断键是否存在,不存在就将BeanCopier插入到map里,存在就直接获取
        if (!BEAN_COPIER_MAP.containsKey(key)) {
            // 参数1: 源对象类   参数2: 目标对象类   参数3: 是否使用自定义转换器
            beanCopier = BeanCopier.create(source.getClass(), target.getClass(), false);
            BEAN_COPIER_MAP.put(key, beanCopier);
        } else {
            beanCopier = BEAN_COPIER_MAP.get(key);
        }
        // 自定义转换器可在copy函数当中的第三个参数设置
        beanCopier.copy(source, target, null);
    }

    /**
     * 深拷贝
     *
     * @param source 源对象
     * @param target 目标类
     * @param <T>    目标类型
     * @return 单个目标类
     */
    public static <T> T copy(Object source, Class<T> target) {
        // 如果源对象为空,结束
        if (source == null) {
            return null;
        }
        // 用来判断目标类型空指针异常
        Objects.requireNonNull(target);
        T result = null;
        try {
            result = target.newInstance();
            copy(source, result);
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * List深拷贝
     *
     * @param sources 源集合
     * @param target  目标类
     * @param <S>     源类型
     * @param <T>     目标类型
     * @return 目标类集合
     */
    public static <S, T> List<T> copyList(List<S> sources, Class<T> target) {
        // 用来判断目标类型空指针异常
        Objects.requireNonNull(target);
        return sources.stream().map(src -> copy(src, target)).collect(Collectors.toList());

    }

    /**
     * 自定义类型转换器
     *
     * @param source    源对象
     * @param target    目标类
     * @param converter 转换器
     */
    private static void copy(Object source, Object target, Converter converter) {
        if (!Objects.isNull(converter)) {
            BeanCopier beanCopier = BeanCopier.create(source.getClass(), target.getClass(), true);
            beanCopier.copy(source, target, converter);
        } else {
            copy(source, target);
        }
    }

    /**
     * 自定义类型转换器
     *
     * @param source    源对象
     * @param target    目标类
     * @param converter 转换器
     * @param <T>       目标类型
     * @return 单个目标类
     */
    public static <T> T copy(Object source, Class<T> target, Converter converter) {
        // 如果源对象为空,结束
        if (source == null) {
            return null;
        }
        // 用来判断目标类型空指针异常
        Objects.requireNonNull(target);
        T result = null;
        try {
            result = target.newInstance();
            copy(source, result, converter);
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 获取Map中的Key
     *
     * @param source 源对象
     * @param target 目标类
     * @return 源对象与目标类名字的拼接
     */
    private static String getKey(Object source, Object target) {
        return source.getClass().getName() + "_" + target.getClass().getName();
    }

}
           

MapStruct

案例集:github.com/mapstruct/m…

引入mapstruct

xml复制代码<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${org.mapstruct.version}</version>
            <!-- IntelliJ does not pick up the processor if it is not in the dependencies.
             There is already an open issue for IntelliJ see https://youtrack.jetbrains.com/issue/IDEA-150621
            -->
            <scope>provided</scope>
        </dependency>
    </dependencies>
           

MapStruct由于是在编译时生成相应的拷贝方法,因此性能很好,理论上拷贝速度是最快的。这里注意运行前需先进行 mvn compile 。

简单Demo

定义Mapper mapstruct默认是浅拷贝,如果需要深拷贝,需要在mapper上加注解 @Mapper(mappingControl = DeepClone.class)

java复制代码@Mapper
// @Mapper(mappingControl = DeepClone.class)
public interface UserDTOMapper {

    UserDTOMapper MAPPER = Mappers.getMapper( UserDTOMapper.class );

    //@Mapping( source = "test", target = "testing" )
    //@Mapping( source = "test1", target = "testing2" )
    UserDTO toTarget( UserDO s );
}
           

但是以上的 DeepClone.class 会导致同名字段在不同类型之间的自动转换失效,如果age从int转换为Long,会编译不通过,提示 Consider to declare/implement a mapping method: "Long map(int value)". 可自定义注解如下:

java复制代码@Retention(RetentionPolicy.CLASS)
@MappingControl( MappingControl.Use.MAPPING_METHOD )
@MappingControl( MappingControl.Use.BUILT_IN_CONVERSION )
public @interface CustomDeepClone {
}
           

在mapper上加注解 @Mapper(mappingControl = CustomDeepClone.class) ,即可实现深拷贝并保证同名字段在不同类型之间的自动转换生效。

使用:

java复制代码public static void main( String[] args ) {
        //simpleDemo();
        UserDO userDO = new UserDO();
        userDO.setId(1L);
        userDO.setName("aihe");
        userDO.setGmtCreate(new Date());
        userDO.setGender(0);
        userDO.setPassword("xxxxxx");
        UserDTO userDTO = UserDTOMapper.MAPPER.toTarget(userDO);
        Assert.assertEquals("名称未成功拷贝",userDTO.getName(),"aihe");
        Assert.assertEquals("Id未成功拷贝", 1L, (long)userDTO.getId());
        Assert.assertEquals("性别未成功拷贝", Integer.valueOf(0),userDTO.getGender());
    }
           

常见用法

  • 属性类型相同,名称不同的时候,使用@Mapping注解指定source和target字段名称对应关系, 如果有多个这种属性,那就指定多个@Mapping注解。
  • 忽略某个字段,在@Mapping的时候,加上ignore = true
  • 转化日期格式,字符串到数字的格式,可以使用dateFormat,numberFormat
  • 如果有自定义转换的需求,写一个简单的Java类即可,然后在方法上打上Mapstruct的注解@Named,在在@Mapper(uses = 自定义的类),然后@Mapping中用上qualifiedByName。
java复制代码@Mapping(target = "userNick1", source = "userNick")
@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd")
@Mapping(target = "age", source = "age", numberFormat = "#0.00")
@Mapping(target = "id", ignore = true)
@Mapping(target = "userVerified", defaultValue = "defaultValue-2")
UserDTO toTarget( UserDO s );
           

性能测试

java复制代码public class BenchDemoTest{
   //转化对象
  private UserDO userDO;
   //转化次数
  private final static int count = 1000000;
  
  @Before
  public void before() {
    userDO = new UserDO();
    userDO.setId(1L);
    userDO.setName("aihe");
    userDO.setGmtCreate(new Date());
    userDO.setGender(0);
    userDO.setPassword("xxxxxx");
  }

  @Test
  public void mapstruct() {
    long startTime = System.currentTimeMillis();
    for (int i = 1; i <=count; i++) {
      UserDTO userDTO = UserDTOMapper.MAPPER.toTarget(userDO);
    }
    System.out.println("mapstruct time" + (System.currentTimeMillis() - startTime));
  }

  @Test
  public void beanCopier() {
    long startTime = System.currentTimeMillis();
    for (int i = 1; i <= count; i++) {
      UserDTO targetBean = new UserDTO();
      BeanCopyUtils.copy(userDO, targetBean);
    }
    System.out.println("beanCopier time" + (System.currentTimeMillis() - startTime));
  }

  @Test
  public void springBeanUtils(){
    long startTime = System.currentTimeMillis();
    for (int i = 1; i <=count; i++) {
      UserDTO userDTO = new UserDTO();
      org.springframework.beans.BeanUtils.copyProperties(userDO, userDTO);
    }
    System.out.println("springBeanUtils time" + (System.currentTimeMillis() - startTime));
  }

  @Test
  public void fastjson() {
    long startTime = System.currentTimeMillis();
    for (int i = 1; i <= count; i++) {
      JSON.parseObject(JSON.toJSONString(userDO), UserDTO.class);
    }
    System.out.println("fastjson time" + (System.currentTimeMillis() - startTime));
  }

}
           
创建型设计模式之原型模式
  • 可以看出BeanCopier和MapStruct是远远超过其他转换方式的...
  • BeanCopier虽然快,但是比mapstruct还是有20倍的性能差距...

小结

  • 软件系统一般都会进行分层,领域模型也会随之进行分层,即每层都有自己关注的模型对象; 分层的主要原因是便于维护。
  • 模型之间的对象经常要互相转换,常用的转换实现有反射和get/set,反射的性能很差不建议使用
  • 然后写了基于get/set实现的beancopier和mapstruct使用方式,简单测试了下性能,mapstrcut优于其它各种对象转换方式。并且MapStrcut支持功能更加复杂的对象转换。 性能又好,功能又强大,所以可以考虑优先使用.

继续阅读