When working on projects, it is often necessary to switch between PO, VO, and DTO. Simple object conversion, using BeanUtils is basically enough, but if you use it for complex conversion, you have to write a bunch of Getter and Setter methods. Today I recommend an automatic object mapping tool MapStruct
, which is really powerful!
SpringBoot actual combat e-commerce project mall (50k+star) address: https://github.com/macrozheng/mall
About BeanUtils
I usually use the BeanUtil class in Hutool to implement object conversion. After using it too much, I found some shortcomings:
- Object attribute mapping is realized by reflection, and the performance is relatively low;
- For different names or different types of properties cannot be converted, you have to write Getter and Setter methods separately;
- For nested sub-objects that also need to be converted, you have to deal with it yourself;
- When converting collection objects, you have to use loops and copy them one by one.
For these shortcomings, MapStruct can solve it, and it is worthy of being a powerful object mapping tool!
Introduction to MapStruct
MapStruct is an object attribute mapping tool based on Java annotations. There is already 4.5K+Star on Github. When using it, as long as we define the object attribute mapping rules in the interface, it can automatically generate the mapping implementation class, does not use reflection, has excellent performance, and can achieve various complex mappings.
IDEA plug-in support
As a very popular object mapping tool, MapStruct also provides a special IDEA plug-in, we can install the plug-in before using it.
Project integration
Integrating MapStruct in SpingBoot is very simple, just add the following two dependencies, here is the 1.4.2.Final
version.
<dependency>
<!--MapStruct相关依赖-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
Basic use
After integrating MapStruct, let's experience its functions and see what its magic is!
Basic mapping
Let's take a quick start, experience the basic functions of MapStruct, and talk about its implementation principles.
- First, we prepare the member PO object
Member
to be used;
/**
* 购物会员
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
private Long id;
private String username;
private String password;
private String nickname;
private Date birthday;
private String phone;
private String icon;
private Integer gender;
}
- Then prepare the member's DTO object
MemberDto
, we need to convert theMember
object to theMemberDto
object;
/**
* 购物会员Dto
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberDto {
private Long id;
private String username;
private String password;
private String nickname;
//与PO类型不同的属性
private String birthday;
//与PO名称不同的属性
private String phoneNumber;
private String icon;
private Integer gender;
}
- Then create a mapping interface
MemberMapper
to realize the mapping of attributes of the same name and type, attributes of different names, and attributes of different types;
/**
* 会员对象映射
* Created by macro on 2021/10/21.
*/
@Mapper
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
@Mapping(source = "phone",target = "phoneNumber")
@Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
MemberDto toDto(Member member);
}
- Next, create a test interface in the Controller, and directly call the conversion method
toDto
INSTANCE
instance in the interface;
/**
* MapStruct对象转换测试Controller
* Created by macro on 2021/10/21.
*/
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "基本映射")
@GetMapping("/baseMapping")
public CommonResult baseTest() {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
MemberDto memberDto = MemberMapper.INSTANCE.toDto(memberList.get(0));
return CommonResult.success(memberDto);
}
}
- After running the project, I tested the interface in Swagger and found that all properties of PO have been successfully converted to DTO. Swagger access address: http://localhost:8088/swagger-ui
- In fact, the implementation principle of MapStruct is very simple, that is, according to the
@Mapper
and@Mapping
we used in the Mapper interface, to generate the implementation class of the interface at runtime, we can open the 06181ed35af95a directory of thetarget
- The following is the
MemberMapper
, you can say goodbye to the handwritten Getter and Setter!
public class MemberMapperImpl implements MemberMapper {
public MemberMapperImpl() {
}
public MemberDto toDto(Member member) {
if (member == null) {
return null;
} else {
MemberDto memberDto = new MemberDto();
memberDto.setPhoneNumber(member.getPhone());
if (member.getBirthday() != null) {
memberDto.setBirthday((new SimpleDateFormat("yyyy-MM-dd")).format(member.getBirthday()));
}
memberDto.setId(member.getId());
memberDto.setUsername(member.getUsername());
memberDto.setPassword(member.getPassword());
memberDto.setNickname(member.getNickname());
memberDto.setIcon(member.getIcon());
memberDto.setGender(member.getGender());
return memberDto;
}
}
}
Collection mapping
MapStruct also provides the function of collection mapping, which can directly convert a PO list into a DTO list, and no longer need to convert objects one by one!
- Add the
toDtoList
MemberMapper
interface for list conversion;
/**
* 会员对象映射
* Created by macro on 2021/10/21.
*/
@Mapper
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
@Mapping(source = "phone",target = "phoneNumber")
@Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
List<MemberDto> toDtoList(List<Member> list);
}
- Create a test interface in the Controller, and call the conversion method
toDtoList
INSTANCE
instance in the Mapper interface;
/**
* MapStruct对象转换测试Controller
* Created by macro on 2021/10/21.
*/
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "集合映射")
@GetMapping("/collectionMapping")
public CommonResult collectionMapping() {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
List<MemberDto> memberDtoList = MemberMapper.INSTANCE.toDtoList(memberList);
return CommonResult.success(memberDtoList);
}
}
- After calling the interface test in Swagger, the PO list has been converted to a DTO list.
Sub-object mapping
MapStruct also supports the situation where the object contains sub-objects that also need to be converted.
- For example, we have an order PO object
Order
, nested withMember
andProduct
objects;
/**
* 订单
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Order {
private Long id;
private String orderSn;
private Date createTime;
private String receiverAddress;
private Member member;
private List<Product> productList;
}
- We need to convert to
OrderDto
object,OrderDto
containsMemberDto
andProductDto
two sub-objects also need to be converted;
/**
* 订单Dto
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class OrderDto {
private Long id;
private String orderSn;
private Date createTime;
private String receiverAddress;
//子对象映射Dto
private MemberDto memberDto;
//子对象数组映射Dto
private List<ProductDto> productDtoList;
}
- We only need to create a Mapper interface, and then inject the conversion Mapper of the child object
uses
, and then set the attribute mapping rules@Mapping
/**
* 订单对象映射
* Created by macro on 2021/10/21.
*/
@Mapper(uses = {MemberMapper.class,ProductMapper.class})
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
@Mapping(source = "member",target = "memberDto")
@Mapping(source = "productList",target = "productDtoList")
OrderDto toDto(Order order);
}
- Next, create a test interface in the Controller, and directly call the conversion method
toDto
INSTANCE
instance in the Mapper;
/**
* MapStruct对象转换测试Controller
* Created by macro on 2021/10/21.
*/
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "子对象映射")
@GetMapping("/subMapping")
public CommonResult subMapping() {
List<Order> orderList = getOrderList();
OrderDto orderDto = OrderMapper.INSTANCE.toDto(orderList.get(0));
return CommonResult.success(orderDto);
}
}
- After calling the interface test in Swagger, you can find that the sub-object attributes have been converted.
Merge mapping
MapStruct also supports mapping multiple object properties to one object.
- For example, here we map some attributes of
Member
andOrder
MemberOrderDto
;
/**
* 会员商品信息组合Dto
* Created by macro on 2021/10/21.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberOrderDto extends MemberDto{
private String orderSn;
private String receiverAddress;
}
- Then add the
toMemberOrderDto
method to the Mapper. It should be noted that because the parameters have two attributes, you needsource
parameter name. The name of the attribute to prevent conflicts (the two parameters have id attributes);
/**
* 会员对象映射
* Created by macro on 2021/10/21.
*/
@Mapper
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
@Mapping(source = "member.phone",target = "phoneNumber")
@Mapping(source = "member.birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
@Mapping(source = "member.id",target = "id")
@Mapping(source = "order.orderSn", target = "orderSn")
@Mapping(source = "order.receiverAddress", target = "receiverAddress")
MemberOrderDto toMemberOrderDto(Member member, Order order);
}
- Next, create a test interface in the Controller, and directly call the conversion method
toMemberOrderDto
INSTANCE
instance in the Mapper;
/**
* MapStruct对象转换测试Controller
* Created by macro on 2021/10/21.
*/
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "组合映射")
@GetMapping("/compositeMapping")
public CommonResult compositeMapping() {
List<Order> orderList = LocalJsonUtil.getListFromJson("json/orders.json", Order.class);
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
Member member = memberList.get(0);
Order order = orderList.get(0);
MemberOrderDto memberOrderDto = MemberMapper.INSTANCE.toMemberOrderDto(member,order);
return CommonResult.success(memberOrderDto);
}
}
- After calling the interface test in Swagger, you can find that the attributes in Member and Order have been mapped to MemberOrderDto.
Advanced use
Through the above basic usage, you can already play MapStruct, let's introduce some advanced usage.
Use dependency injection
Above we all call methods through the INSTANCE instance in the Mapper interface, and we can also use dependency injection in Spring.
- To use dependency injection, we only need to
@Mapper
thecomponentModel
parameter ofspring
, so that when the interface implementation class is generated, MapperStruct will add the@Component
annotation to it;
/**
* 会员对象映射(依赖注入)
* Created by macro on 2021/10/21.
*/
@Mapper(componentModel = "spring")
public interface MemberSpringMapper {
@Mapping(source = "phone",target = "phoneNumber")
@Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
MemberDto toDto(Member member);
}
@Autowired
annotation injection in the Controller to use;
/**
* MapStruct对象转换测试Controller
* Created by macro on 2021/10/21.
*/
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
@Autowired
private MemberSpringMapper memberSpringMapper;
@ApiOperation(value = "使用依赖注入")
@GetMapping("/springMapping")
public CommonResult springMapping() {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
MemberDto memberDto = memberSpringMapper.toDto(memberList.get(0));
return CommonResult.success(memberDto);
}
}
- Under the test of calling the interface in Swagger, it can be found that it can be used normally as before.
Use constants, default values, and expressions
When using MapStruct to map attributes, we can set the attributes as constants or default values, or we can write expressions in Java to automatically generate attributes.
- For example, the following product class Product object;
/**
* 商品
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Product {
private Long id;
private String productSn;
private String name;
private String subTitle;
private String brandName;
private BigDecimal price;
private Integer count;
private Date createTime;
}
- Product ProductDto we want to convert the object,
id
property is set to a constant,count
set the default value. 1,productSn
arranged to generate a UUID;
/**
* 商品Dto
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class ProductDto {
//使用常量
private Long id;
//使用表达式生成属性
private String productSn;
private String name;
private String subTitle;
private String brandName;
private BigDecimal price;
//使用默认值
private Integer count;
private Date createTime;
}
- Create the
ProductMapper
interface, and set the mapping rules@Mapping
,defaultValue
, andexpression
in the annotations ofconstant
/**
* 商品对象映射
* Created by macro on 2021/10/21.
*/
@Mapper(imports = {UUID.class})
public interface ProductMapper {
ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);
@Mapping(target = "id",constant = "-1L")
@Mapping(source = "count",target = "count",defaultValue = "1")
@Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
ProductDto toDto(Product product);
}
- Next, create a test interface in the Controller, and directly call the conversion method
toDto
INSTANCE
instance in the interface;
/**
* MapStruct对象转换测试Controller
* Created by macro on 2021/10/21.
*/
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "使用常量、默认值和表达式")
@GetMapping("/defaultMapping")
public CommonResult defaultMapping() {
List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
Product product = productList.get(0);
product.setId(100L);
product.setCount(null);
ProductDto productDto = ProductMapper.INSTANCE.toDto(product);
return CommonResult.success(productDto);
}
}
- Under the test of calling the interface in Swagger, the object has been successfully converted.
Customize before and after mapping
MapStruct also supports some custom operations before and after mapping, similar to the aspect in AOP.
- At this time, since we need to create custom processing method to create an abstract class
ProductRoundMapper
by@BeforeMapping
annotation before custom mapping operation by@AfterMapping
the annotation custom mapping operation;
/**
* 商品对象映射(自定义处理)
* Created by macro on 2021/10/21.
*/
@Mapper(imports = {UUID.class})
public abstract class ProductRoundMapper {
public static ProductRoundMapper INSTANCE = Mappers.getMapper(ProductRoundMapper.class);
@Mapping(target = "id",constant = "-1L")
@Mapping(source = "count",target = "count",defaultValue = "1")
@Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
public abstract ProductDto toDto(Product product);
@BeforeMapping
public void beforeMapping(Product product){
//映射前当price<0时设置为0
if(product.getPrice().compareTo(BigDecimal.ZERO)<0){
product.setPrice(BigDecimal.ZERO);
}
}
@AfterMapping
public void afterMapping(@MappingTarget ProductDto productDto){
//映射后设置当前时间为createTime
productDto.setCreateTime(new Date());
}
}
- Next, create a test interface in the Controller, and directly call the conversion method
toDto
INSTANCE
instance in the Mapper;
/**
* MapStruct对象转换测试Controller
* Created by macro on 2021/10/21.
*/
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "在映射前后进行自定义处理")
@GetMapping("/customRoundMapping")
public CommonResult customRoundMapping() {
List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
Product product = productList.get(0);
product.setPrice(new BigDecimal(-1));
ProductDto productDto = ProductRoundMapper.INSTANCE.toDto(product);
return CommonResult.success(productDto);
}
}
- After calling the interface test in Swagger, you can find that the custom operation has been applied.
Handling the mapping exception
Exceptions will inevitably occur when the code runs, and MapStruct also supports handling mapping exceptions.
- We need to create a custom exception class first;
/**
* 商品验证异常类
* Created by macro on 2021/10/22.
*/
public class ProductValidatorException extends Exception{
public ProductValidatorException(String message) {
super(message);
}
}
- Then create a verification class, and throw our custom exception
price
setless than 0;
/**
* 商品验证异常处理器
* Created by macro on 2021/10/22.
*/
public class ProductValidator {
public BigDecimal validatePrice(BigDecimal price) throws ProductValidatorException {
if(price.compareTo(BigDecimal.ZERO)<0){
throw new ProductValidatorException("价格不能小于0!");
}
return price;
}
}
- After we passed
@Mapper
annotationsuses
use to verify class properties;
/**
* 商品对象映射(处理映射异常)
* Created by macro on 2021/10/21.
*/
@Mapper(uses = {ProductValidator.class},imports = {UUID.class})
public interface ProductExceptionMapper {
ProductExceptionMapper INSTANCE = Mappers.getMapper(ProductExceptionMapper.class);
@Mapping(target = "id",constant = "-1L")
@Mapping(source = "count",target = "count",defaultValue = "1")
@Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
ProductDto toDto(Product product) throws ProductValidatorException;
}
- Then add a test interface to the Controller, set
price
to-1
, at this time an exception will be thrown during the mapping;
/**
* MapStruct对象转换测试Controller
* Created by macro on 2021/10/21.
*/
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "处理映射异常")
@GetMapping("/exceptionMapping")
public CommonResult exceptionMapping() {
List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
Product product = productList.get(0);
product.setPrice(new BigDecimal(-1));
ProductDto productDto = null;
try {
productDto = ProductExceptionMapper.INSTANCE.toDto(product);
} catch (ProductValidatorException e) {
e.printStackTrace();
}
return CommonResult.success(productDto);
}
}
- After calling the interface test in Swagger, it is found that the custom exception information has been printed in the running log.
Summarize
Through the above experience of using MapStruct, we can find that MapStruct is far more powerful than BeanUtils. When we want to implement more complex object mapping, it can save the process of writing Getter and Setter methods. Of course, the above only introduces some common functions of MapStruct. Its functions are much more than that. If you are interested, you can check the official documents.
Reference
Official document: https://mapstruct.org/documentation/stable/reference/html
Project source code address
https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-mapstruct
This article GitHub https://github.com/macrozheng/mall-learning has been included, welcome to Star!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。