[Java/Spring] 深入理解 : SpringBoot PropertyMapper

千千寰宇 / 2024-10-12 / 原文

1 概述: SpringBoot PropertyMapper

简介:PropertyMapper ∈ 对象拷贝与转换工具

  • PropertyMapper是Spring提供的一个工具类,主要用于对对象的重新赋值,拷贝、转换等操作。
  • 位于: org.springframework.boot.context.properties.PropertyMapper

辨析: Spring BeanUtils 与 SpringBoot PropertyMapper

  • 共同点:
  • 对象及属性拷贝工具:BeanUtils 和 PropertyMapper 都是 Spring 框架中用于处理 Java Bean 之间对象属性拷贝的工具。
  • 不同点:
  • 通用 vs 定制 :BeanUtils提供了拷贝属性的通用方法,而PropertyMapper提供了一种更灵活、可扩展的方式来定制属性的映射逻辑。
  • 对于BeanUtils.copyProperties来说,你必须保证属性名类型是相同的,因为它是根据get和set方法来赋值的。
  • 浅拷贝 vs 深拷贝:
  • BeanUtils :浅拷贝

org.springframework.beans.BeanUtils 工具类中的 copyProperties() 无法实现深拷贝,只能实现浅拷贝
详情参见:

  • PropertyMapper : 支持深拷贝,完全取决于应用程序的开发者用户的诉求
  • 模块/包
  • BeanUtils : spring-beans 模块
  • org.springframework.beans.BeanUtils
  • PropertyMapper : spring-boot 模块
  • org.springframework.boot.context.properties.PropertyMapper

2 应用场景

场景 :2个异构数据对象的拷贝与转换

  • 在实际工作中,经常会遇到将数据库的实体类 Entity 转成 DTO 类的操作。通常的方法:
  • 手工方法:我们有可以将属性一个个get出来,再set进去。

但经常涉及到判空、数据类型的转换等简单的逻辑处理,容易留下一大堆 IF ELSE 的臃肿代码。

  • 第三方工具:用BeanUtils工具类 将对应类型的属性一个个copy进去。
  • 现在还可以尝试使用 SpringBoot 的 PropertyMapper 来做数据对象的转换

案例1:SpringBoot 的 RabbitTemplateConfiguration

  • SpringBoot 官方模块 spring-boot-starter-amqpRabbitTemplate 的配置实现
  • org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.RabbitTemplateConfiguration
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean(RabbitOperations.class)
public RabbitTemplate rabbitTemplate(RabbitProperties properties,
                                     ObjectProvider<MessageConverter> messageConverter,
                                     ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers,
                                     ConnectionFactory connectionFactory) {
    PropertyMapper map = PropertyMapper.get();
    RabbitTemplate template = new RabbitTemplate(connectionFactory);
    messageConverter.ifUnique(template::setMessageConverter);
    template.setMandatory(determineMandatoryFlag(properties));
    RabbitProperties.Template templateProperties = properties.getTemplate();
    if (templateProperties.getRetry().isEnabled()) {
        template.setRetryTemplate(
            new RetryTemplateFactory(retryTemplateCustomizers.orderedStream().collect(Collectors.toList()))
            .createRetryTemplate(templateProperties.getRetry(),
                                 RabbitRetryTemplateCustomizer.Target.SENDER));
    }
    map.from(templateProperties::getReceiveTimeout).whenNonNull().as(Duration::toMillis)
        .to(template::setReceiveTimeout);
    map.from(templateProperties::getReplyTimeout).whenNonNull().as(Duration::toMillis)
        .to(template::setReplyTimeout);
    map.from(templateProperties::getExchange).to(template::setExchange);
    map.from(templateProperties::getRoutingKey).to(template::setRoutingKey);
    map.from(templateProperties::getDefaultReceiveQueue).whenNonNull().to(template::setDefaultReceiveQueue);
    return template;
}

案例2:基于 Http11NioProtocol、WebServerFactoryCustomizer 、自定义配置实体,实现 自定义 SpringBoot 的 Tomcat Server 配置

TomcatEmbedServerProperties : 应用程序的自定义配置实体

//import org.springframework.boot.context.properties.ConfigurationProperties;
//import org.springframework.context.annotation.ComponentScan;
//import org.springframework.context.annotation.Configuration;

/**
 * @create-time 2023/4/11
 * @description ...
 */
//@ComponentScan
//@Configuration
//@ConfigurationProperties(
//    prefix="service-config.tomcat-server"
//    , ignoreUnknownFields = true
//)
public class TomcatEmbedServerProperties { //应用程序的自定义配置实体
    private Integer port;

    private Integer minSpareThreads;

    private Integer maxThreads;

    private Integer acceptCount;

    private Integer maxConnections;

    private Integer maxKeepAliveRequests;

    private Integer keepAliveTimeout;

    private Integer connectionTimeout;

    public TomcatEmbedServerProperties(Integer port, Integer minSpareThreads, Integer maxThreads, Integer acceptCount, Integer maxConnections, Integer maxKeepAliveRequests, Integer keepAliveTimeout, Integer connectionTimeout) {
        this.port = port;
        this.minSpareThreads = minSpareThreads;
        this.maxThreads = maxThreads;
        this.acceptCount = acceptCount;
        this.maxConnections = maxConnections;
        this.maxKeepAliveRequests = maxKeepAliveRequests;
        this.keepAliveTimeout = keepAliveTimeout;
        this.connectionTimeout = connectionTimeout;
    }

    public TomcatEmbedServerProperties() {
    }

    public Integer getPort() {
        return port;
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    public Integer getMinSpareThreads() {
        return minSpareThreads;
    }

    public void setMinSpareThreads(Integer minSpareThreads) {
        this.minSpareThreads = minSpareThreads;
    }

    public Integer getMaxThreads() {
        return maxThreads;
    }

    public void setMaxThreads(Integer maxThreads) {
        this.maxThreads = maxThreads;
    }

    public Integer getAcceptCount() {
        return acceptCount;
    }

    public void setAcceptCount(Integer acceptCount) {
        this.acceptCount = acceptCount;
    }

    public Integer getMaxConnections() {
        return maxConnections;
    }

    public void setMaxConnections(Integer maxConnections) {
        this.maxConnections = maxConnections;
    }

    public Integer getMaxKeepAliveRequests() {
        return maxKeepAliveRequests;
    }

    public void setMaxKeepAliveRequests(Integer maxKeepAliveRequests) {
        this.maxKeepAliveRequests = maxKeepAliveRequests;
    }

    public Integer getKeepAliveTimeout() {
        return keepAliveTimeout;
    }

    public void setKeepAliveTimeout(Integer keepAliveTimeout) {
        this.keepAliveTimeout = keepAliveTimeout;
    }

    public Integer getConnectionTimeout() {
        return connectionTimeout;
    }

    public void setConnectionTimeout(Integer connectionTimeout) {
        this.connectionTimeout = connectionTimeout;
    }

    private static boolean isPositive(int value) {
        return value > 0;
    }

    @Override
    public String toString() {
        return "TomcatEmbedServerProperties{" +
                "port=" + port +
                ", minSpareThreads=" + minSpareThreads +
                ", maxThreads=" + maxThreads +
                ", acceptCount=" + acceptCount +
                ", maxConnections=" + maxConnections +
                ", maxKeepAliveRequests=" + maxKeepAliveRequests +
                ", keepAliveTimeout=" + keepAliveTimeout +
                ", connectionTimeout=" + connectionTimeout +
                '}';
    }
}

WebServerConfiguration : 应用程序的自定义 Tomcat Server 配置 Bean


案例3: Order 转 OrderDTO

Order

@Data
public class Order {
    private Long id;

    private BigDecimal totalAmout;

    private Integer status;

    private Long userId;

    private LocalDateTime createTime;
}

OrderDTO

@Data
public class OrderDTO {
    private Long id;

    private BigDecimal totalAmout;

    private Integer status;

    private Long userId;

    private String createTime;
}

使用 PropertyMapper 转换

Order order = new Order();
order.setId(1L);
order.setStatus(1);
order.setTotalAmout(BigDecimal.ONE);
order.setUserId(100L);
order.setCreateTime(LocalDateTime.now());

PropertyMapper propertyMapper = PropertyMapper.get();
OrderDTO orderDTO = new OrderDTO();

propertyMapper.from(order::getId).to(orderDTO::setId);
// 如果from获取到的元素不是null,则执行to里面的动作
propertyMapper.from(order::getStatus).whenNonNull().to(orderDTO::setStatus);
propertyMapper.from(order::getUserId).to(orderDTO::setUserId);
propertyMapper.from(order::getTotalAmout).to(orderDTO::setTotalAmout);

// 因为Order里面的createTime是LocalDateTime类型,OrderDTO里面则是String类型,需要转换一下
propertyMapper.from(order::getCreateTime).as(createTime -> {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    return createTime.format(formatter);
}).to(orderDTO::setCreateTime);

这样一来就可以通过 PropertyMapper 将 Order 对象的值 set 到 OrderDTO 对象中。

3 PropertyMapper API

  • <T> Source<T> from(Supplier<T> supplier) :提供值的来源,入参为Supplier
  • <T> Source<T> from(T value) :一种重载形式,入参可以为一个对象
  • void to(Consumer<T> consumer) :通过将任何未过滤的值传递给指定的使用者来完成映射
  • <R> R toInstance(Function<T, R> factory) :通过从未过滤的值创建新实例来完成映射
  • void toCall(Runnable runnable) :当值还没有时,通过调用指定的方法来完成映射
  • <R> Source<R> as(Function<T, R> adapter) :将T类型的入参转成R类型的出参,类似于Stream中的map
  • Source<T> when... :这一系列方法,都是过滤用的。在from后面调用,如果满足条件,就直接to方法
  • static PropertyMapper get() :提供PropertyMapper实例

PropertyMapper 类内部维护一个静态实例,我们一开始只能通过获取它得到 PropertyMapper 实例
get() 方法始终返回的是其内部的 PropertyMapper final static 实例, PropertyMapper 对象本身的内存开销不会太大

  • PropertyMapper alwaysApplying(SourceOperator operator) :自定义过滤规则,参考代码

alwaysApplying 用于向 PropertyMapper 添加一个 when 条件判定,这个判定在每次 from 方法中都被调用。
alwaysApplyingWhenNonNull 则是对这个方法的包装。

  • PropertyMapper alwaysApplyingWhenNonNull() :提供实例时,当前实例就过滤掉from之后是null的元素。PropertyMapper.get().alwaysApplyingWhenNonNull();
//使当前 PropertyMapper 只会映射 LocalDateTime 类型的字段
PropertyMapper propertyMapper = PropertyMapper.get()
	.alwaysApplying( new PropertyMapper.SourceOperator() {
		@Override
		public <T> PropertyMapper.Source<T> apply(PropertyMapper.Source<T> source) {
			return source.when(t -> t instanceof LocalDateTime);
		}
	} );

//注意:如果from方法后面有when条件,则 alwaysApplying 中设置的初始化提交将会失效

Y 推荐文献

  • [Java] Java 对象拷贝与转换 - 博客园/千千寰宇

X 参考文献

  • [小工具]PropertyMapper使用 - 稀土掘金
  • 简化 Java 代码 ——(一)使用 PropertyMapper - 博客园 【不推荐】