mapstruct这么用,同事也开始模仿

前言

hi,大家好,我是大鱼七成饱。

前几天同事review我的代码,发现mapstruct有这么多好用的技巧,遇到POJO转换的问题经常过来沟通。考虑到不可能每次都一对一,所以我来梳理五个场景,谁在过来问,直接甩出总结。

环境准备

由于日常使用都是spring,所以后面的示例都是在springboot框架中运行的。关键pom依赖如下:

java 复制代码
<properties>
        <java.version>1.8</java.version>
        <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
        <org.projectlombok.version>1.18.30</org.projectlombok.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>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <version>0.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

场景一:常量转换

这是最简单的一个场景,比如需要设置字符串、整形和长整型的常量,有的又需要日期,或者新建类型。下面举个例子,演示如何转换

vbnet 复制代码
//实体类
@Data
public class Source {
    private String stringProp;
    private Long longProp;
}
@Data
public class Target {
    private String stringProperty;
    private long longProperty;
    private String stringConstant;
    private Integer integerConstant;
    private Long  longWrapperConstant;
    private Date dateConstant;
}
  • 设置字符串常量
  • 设置long常量
  • 设置java内置类型默认值,比如date

那么mapper这么设置就可以

java 复制代码
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface SourceTargetMapper {

    @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
    @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1l")
    @Mapping(target = "stringConstant", constant = "Constant Value")
    @Mapping(target = "integerConstant", constant = "14")
    @Mapping(target = "longWrapperConstant", constant = "3001L")
    @Mapping(target = "dateConstant", dateFormat = "yyyy-MM-dd", constant = "2023-09-01-")
    Target sourceToTarget(Source s);
}

解释下,constant用来设置常量值,source的值如果没有设置,则会使用defaultValue的值,日期可以按dateFormat解析。

Talk is cheap, show me the code !废话不多说,自动生成的转换类如下:

java 复制代码
@Component
public class SourceTargetMapperImpl implements SourceTargetMapper {
    public SourceTargetMapperImpl() {
    }

    public Target sourceToTarget(Source s) {
        if (s == null) {
            return null;
        } else {
            Target target = new Target();
            if (s.getStringProp() != null) {
                target.setStringProperty(s.getStringProp());
            } else {
                target.setStringProperty("undefined");
            }

            if (s.getLongProp() != null) {
                target.setLongProperty(s.getLongProp());
            } else {
                target.setLongProperty(-1L);
            }

            target.setStringConstant("Constant Value");
            target.setIntegerConstant(14);
            target.setLongWrapperConstant(3001L);

            try {
                target.setDateConstant((new SimpleDateFormat("dd-MM-yyyy")).parse("09-01-2014"));
                return target;
            } catch (ParseException var4) {
                throw new RuntimeException(var4);
            }
        }
    }
}

是不是一目了然

场景二:转换中调用表达式

比如id不存在使用UUID生成一个,或者使用已有参数新建一个对象作为属性。当然可以用after mapping,qualifiedByName等实现,感觉还是不够优雅,这里介绍个雅的(代码少点的)。

实体类如下:

typescript 复制代码
@Data
public class CustomerDto {
    public Long id;
    public String customerName;

    private String format;
    private Date time;
}
@Data
public class Customer {
    private String id;
    private String name;
    private TimeAndFormat timeAndFormat;
}
@Data
public class TimeAndFormat {
    private Date time;
    private String format;

    public TimeAndFormat(Date time, String format) {
        this.time = time;
        this.format = format;
    }
}

Dto转customer,加创建TimeAndFormat作为属性,mapper实现如下:

java 复制代码
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, imports = UUID.class)
public interface CustomerMapper {

    @Mapping(target = "timeAndFormat",
            expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
    @Mapping(target = "id", source = "id", defaultExpression = "java( UUID.randomUUID().toString() )")
    Customer toCustomer(CustomerDto s);

}

解释下,id为空则走默认的defaultExpression,通过imports引入,java括起来调用。新建对象直接new TimeAndFormat。有的小伙伴喜欢用qualifiedByName自定义方法,可以对比下,哪个合适用哪个,都能调用转换方法。

生成代码如下:

java 复制代码
@Component
public class CustomerMapperImpl implements CustomerMapper {
    public CustomerMapperImpl() {
    }

    public Customer toCustomer(CustomerDto s) {
        if (s == null) {
            return null;
        } else {
            Customer customer = new Customer();
            if (s.getId() != null) {
                customer.setId(String.valueOf(s.getId()));
            } else {
                customer.setId(UUID.randomUUID().toString());
            }

            customer.setTimeAndFormat(new TimeAndFormat(s.getTime(), s.getFormat()));
            return customer;
        }
    }
}

场景三:类共用属性,如何复用

比如下面的Bike和车辆类,都有id和creationDate属性,我又不想重复写mapper属性注解

arduino 复制代码
public class Bike {
    /**
     * 唯一id
     */
    private String id;

    private Date creationDate;

    /**
     * 品牌
     */
    private String brandName;
}

public class Car {
    /**
     * 唯一id
     */
    private String id;

    private Date creationDate;
    /**
     * 车牌号
     */
    private String chepaihao;
 }

解决起来很简单,写个共用的注解,使用的时候引入就可以,示例如下:

less 复制代码
//通用注解
@Retention(RetentionPolicy.CLASS)
//自动生成当前日期
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
//忽略id
@Mapping(target = "id", ignore = true)
public @interface ToEntity { }

//使用
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface TransportationMapper {
    @ToEntity
    @Mapping( target = "brandName", source = "brand")
    Bike map(BikeDto source);

    @ToEntity
    @Mapping( target = "chepaihao", source = "plateNo")
    Car map(CarDto source);
}

这里Retention修饰ToEntity注解,表示ToEntity注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期,辅助生成mapper实现类。上面定义了creationDate和id的转换规则,新建日期,忽略id。

生成的mapper实现类如下:

typescript 复制代码
@Component
public class TransportationMapperImpl implements TransportationMapper {
    public TransportationMapperImpl() {
    }

    public Bike map(BikeDto source) {
        if (source == null) {
            return null;
        } else {
            Bike bike = new Bike();
            bike.setBrandName(source.getBrand());
            bike.setCreationDate(new Date());
            return bike;
        }
    }

    public Car map(CarDto source) {
        if (source == null) {
            return null;
        } else {
            Car car = new Car();
            car.setChepaihao(source.getPlateNo());
            car.setCreationDate(new Date());
            return car;
        }
    }
}

坚持一下,还剩俩场景,剩下的俩更有意思

场景四:lombok和mapstruct冲突了

啥冲突?用了builder注解后,mapstuct转换不出来了。哎,这个问题困扰了我那同事两天时间。

解决方案如下:

xml 复制代码
 <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <version>0.2.0</version>
 </dependency>

加上lombok-mapstruct-binding就可以了,看下生成的效果:

kotlin 复制代码
@Builder
@Data
public class Person {
    private String name;
}
@Data
public class PersonDto {
    private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface PersonMapper {

    Person map(PersonDto dto);
}
@Component
public class PersonMapperImpl implements PersonMapper {
    public PersonMapperImpl() {
    }

    public Person map(PersonDto dto) {
        if (dto == null) {
            return null;
        } else {
            Person.PersonBuilder person = Person.builder();
            person.name(dto.getName());
            return person.build();
        }
    }
}

从上面可以看到,mapstruct匹配到了lombok的builder方法。

场景五:说个难点的,转换的时候,如何注入springBean

有时候转换方法比不是静态的,他可能依赖spring bean,这个如何导入?

这个使用需要使用抽象方法了,上代码:

kotlin 复制代码
@Component
public class SimpleService {
    public String formatName(String name) {
        return  "您的名字是:" + name;
    }
}
@Data
public class Student {
    private String name;
}
@Data
public class StudentDto {
    private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class StudentMapper {

    @Autowired
    protected SimpleService simpleService;

    @Mapping(target = "name", expression = "java(simpleService.formatName(source.getName()))")
    public abstract StudentDto map(StudentDto source);
}

接口是不支持注入的,但是抽象类可以,所以采用抽象类解决,后面expression直接用皆可以了,生成mapperimpl如下:

scala 复制代码
@Component
public class StudentMapperImpl extends StudentMapper {
    public StudentMapperImpl() {
    }

    public StudentDto map(StudentDto source) {
        if (source == null) {
            return null;
        } else {
            StudentDto studentDto = new StudentDto();
            studentDto.setName(this.simpleService.formatName(source.getName()));
            return studentDto;
        }
    }
}

思考

以上场景肯定还有其他解决方案,遵循合适的原则就可以。驾驭不了的代码,可能带来更多问题,先简单实现,后续在迭代优化可能适合更多的业务场景。

另外文章中的示例代码地址是:github.com/dayuqicheng...

相关推荐
小灰灰__11 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭14 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果35 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林40 分钟前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
duration~2 小时前
Maven随笔
java·maven
zmgst2 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD2 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵2 小时前
设计模式-工厂设计模式
java·开发语言·设计模式