Manual copy! out
Background:
There are often some DO to BO or DTO conversions in the code.
For example, the user information queried by the database (the mapping model of the table) is UserDO, but we need to pass the user information to the client is UserVO. In this case, we need to assign the attributes of UserDO instance to UserVO instance one by one.
A large number of properties can be the same or different between these data structures.
Phase 1
Remember back in sophomore year that one of the most common ways to write code was to customize an convert method or a convert class that declares a bunch of static methods to implement specific object conversions.
public xxxBO xxxDOToxxxBO(xxxDO xxxdo){
xxxBO xxxbo=newxxxBO; xxxbo.setXXX(xxxdo.getXXX()); . . . }Copy the code
One of the benefits of this implementation is that you can reuse your code but you have to be very careful when you write your methods, and you can miss a field if you’re not very careful. If the field is an attribute that is not currently important, but will be important for future development. Problems are often hard to spot after a period of time.
Solution 1: Start at the bottom of the Set/Get method so that the properties that have already been got are at the top. Keep getting until you get the first duplicate object so there’s no missing.
Solution 2: Use idea plug-ins to automatically generate such as GenerateAllSetter, CodeHelper. generator, etc
Problems found: Some fields may need to be logically mapped, such as enumerations converted to strings. Or after certain business processing, the universality of the transformation class is reduced. Second, the number of conversion classes keeps increasing.
The stage 2
It has recently been discovered that there are a number of utility classes that can automatically copy performance graphs first
! [image-20201231233830513](/Users/coapeng/Library/Application Support/typora-user-images/image-20201231233830513.png)
There are also other frameworks such as Orika. In general, the three underlying technologies are reflection, dynamic proxy to modify bytecode files, and direct generation of bytecode files. In terms of efficiency, direct generation of bytecode files > Dynamic proxy (Cglib) > reflection
Manual get/set >mapstruct> Orika >cglib> Spring (BeanUtils)
org.springframework.beans.BeanUtils
;org.springframework.cglib.beans.BeanCopier
;ma.glasnost.orika
;org.mapstruct
(It is highly recommended).
Here’s a demo of the tool
Second,BeanUtils
BeanUtils in Spring, which is implemented in a very simple way, is a simple get/set on two objects of the same name attribute, only check the accessibility of the attribute.
BeanUtils source
As you can see, member variable assignments are based on the member list of the target object, and ignore and non-existent ones are skipped, so this method is safe and does not cause errors due to structural differences between the two objects, but two member variables with the same name must be of the same type.
2.1. Single object copy
We copy userdo.java from the database query to uservo.java. Use the beanutils.copyProperties () method directly.
@Test public void commonCopy() { UserDO userDO = new UserDO(1L, "Van", 18, 1); UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); log.info("userVO:{}",userVO); } Duplicate codeCopy the code
- Copy result:
. UserVO: userVO (userId=1, userName=Van, age=18, sex=null) copies the codeCopy the code
2.2 Collection copy
We just copied an object, but sometimes when we want to copy a set of Uerdo.java, which is a collection, we can’t do this directly. If this logic is followed, the following is true:
@Test public void listCopyFalse() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 18, 2)); List<UserVO> userVOList = new ArrayList(); BeanUtils.copyProperties(userDOList, userVOList); log.info("userVOList:{}",userVOList); } Duplicate codeCopy the code
- Copy result:
. UserVOList :[] Copies codeCopy the code
It can be found from the log that copying collection directly is invalid, so how to solve the problem?
2.3 Violent Copy (not recommended)
Traverse the collection that needs to be copied, violent copy.
@Test public void listCopyCommon() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = new ArrayList(); userDOList.forEach(userDO ->{ UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); userVOList.add(userVO); }); log.info("userVOList:{}",userVOList); } Duplicate codeCopy the code
- Copy result:
. UserVOList :[UserVO(userId=1, userName=Van, age=18, sex=null), UserVO(userId=2, userName=VanVan, age=20, sex=null)] Copy codeCopy the code
Although this method can be solved, it is not elegant at all, especially when it is difficult to write.
2.4 Elegant Copy (This article recommended)
Through the JDK 8 functional interfaces encapsulate org. Springframework. Beans. BeanUtils
- Define a functional interface
It is possible to include default methods in functional interfaces. Here we define default callback methods.
@FunctionalInterface public interface BeanUtilCopyCallBack <S, T> {@param T * @param s */ void callBack(s T, T s); } Duplicate codeCopy the code
- Encapsulate a utility class
BeanUtilCopy.java
Public class BeanUtilCopy extends BeanUtils {/** * a copy of collection data * @param sources: data source * @param target: target class ::new(eg: UserVO::new) * @return */ public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target) { return copyListProperties(sources, target, null); * @param sources: data source class * @param target: target class ::new(eg: UserVO::new) * @param callBack: * @return */ public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target, BeanUtilCopyCallBack<S, T> callBack) { List<T> list = new ArrayList<>(sources.size()); for (S source : sources) { T t = target.get(); copyProperties(source, t); list.add(t); if (callBack ! = null) {// callBack. CallBack (source, t); } } return list; }} Copy the codeCopy the code
- Simple copy test
@Test public void listCopyUp() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new); log.info("userVOList:{}",userVOList); } Duplicate codeCopy the code
- Copy result:
. UserVOList :[UserVO(userId=1, userName=Van, age=18, sex=null), UserVO(userId=2, userName=VanVan, age=20, sex=null)] Copy codeCopy the code
Through the above method, we basically realized the collection copy, but from the return result we can see: fields with different attributes cannot be copied.
Note: userdo. Java and uservo. Java last field sex type is different, respectively: Integer/String
To optimize the
- New gender enumeration class
Public enum SexEnum {UNKNOW(" not set ",0), MEN(" male ", 1), WOMAN(" female ",2); private String desc; private int code; SexEnum(String desc, int code) { this.desc = desc; this.code = code; } public static SexEnum getDescByCode(int code) { SexEnum[] typeEnums = values(); for (SexEnum value : typeEnums) { if (code == value.getCode()) { return value; } } return null; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public int getCode() { return code; } public void setCode(int code) { this.code = code; }} Copy the codeCopy the code
- A copy of the collection with a specific transformation
@Test public void listCopyUpWithCallback() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new, (userDO, UserVO) -> {// Here specific conversion rules can be defined uservo.setsex (SexEnum. GetDescByCode (userdo.getsex ()).getdesc ())); }); log.info("userVOList:{}",userVOList); } Duplicate codeCopy the code
- Copy result:
. UserVOList :[UserVO(userId=1, userName=Van, age=18, sex= male), UserVO(userId=2, userName=VanVan, age=20, sex= female)] copy codeCopy the code
The Integer sex in userdo. Java is copied to uservo. Java as a String boy/girl.
2.5 summary
This method is the most common scheme we use, here simple encapsulation, can facilitate the collection type of object copy, ordinary use of basic enough, just for reference.
- This section tests the code
- Full code address
Three,BeanCopier
BeanCopier is used to copy properties between two beans. BeanCopier supports two ways:
- One is not to use it
Converter
The way, just for twobean
Copy variables with identical attribute names and types; - The other one is introduced
Converter
, you can perform special operations on specific attribute values.
3.1 Common Use
@test public void normalCopy() {UserDO UserDO = datautil.createData (); Log.info (" before copy: userDO:{}", userDO); // First argument: source object, second argument: target object, third argument: BeanCopier b = beancopier.create (userdo.class, userTo.class, false); UserDTO userDTO = new UserDTO(); b.copy(userDO, userDTO, null); Log.info (" after copy: userDTO:{}", userDTO); } Duplicate codeCopy the code
- Copy result:
. UserDO: userDO (id=1, userName=Van, sex=0, gmtBroth=2019-11-02T18:24:24.077, balance=100)...... After copying: userDTO: userDTO (id=1, userName=Van, sex=null) Copy codeCopy the code
The sex of UserDO’s int cannot be copied to UserDTO’s Integer sex.
That is, BeanCopier copies only properties with the same name and type.
Even if the source type is primitive (int, short, char, etc.), the target type is its wrapper type (Integer, short, Character, etc.), or vice versa: it will not be copied.
3.2 Custom Converter
According to 3.1, when the source and target class have different attribute types, the attribute cannot be copied. In this case, we can customize the Converter by implementing the Converter interface
- Target object property class
@Data public class UserDomain { private Integer id; private String userName; /** * The following two fields are user simulated custom conversion */ private String gmtBroth; private String balance; } Duplicate codeCopy the code
- implementation
Converter
Interface custom property transformation
Public class userDomainConversion {/** ** DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); /** * custom attribute conversion * @param value Source object attribute class * @param target Target object attribute name,eg.setId * @param context target object attribute class * @return */ @Override public Object convert(Object value, Class target, Object context) { if (value instanceof Integer) { return value; } else if (value instanceof LocalDateTime) { LocalDateTime date = (LocalDateTime) value; return dtf.format(date); } else if (value instanceof BigDecimal) { BigDecimal bd = (BigDecimal) value; return bd.toPlainString(); } // More type conversions please customize return value; }} Copy the codeCopy the code
- The test method
/** * Converter */ @test public void converterTest() {UserDO UserDO = datautil.createData (); Log.info (" before copy: userDO:{}", userDO); BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, true); UserDomainConverter converter = new UserDomainConverter(); UserDomain userDomain = new UserDomain(); copier.copy(userDO, userDomain, converter); Log.info (" after copy: userDomain:{}", userDomain); } Duplicate codeCopy the code
- Copy result:
. UserDO: userDO (id=1, userName=Van, gmtBroth=2019-11-02T19:51:11.985, balance=100)...... UserDomain: userDomain (id=1, userName=Van, gmtBroth=2019-11-02 19:51:11, balance=100Copy the code
- Pay attention to
- Once the use
Converter
.BeanCopier
Use onlyConverter
Define rules to copy attributes, so inconvert()
All attributes are considered in a method; - There is no doubt that use
Converter
The object copy speed slows down.
3.3 the cacheBeanCopier
Instances improve performance
The BeanCopier copy speed is fast, and the performance bottleneck appears in the process of creating BeanCopier instances. So, put the created BeanCopier instances in the cache so they can be retrieved next time to improve performance.
- The test code
@Test public void beanCopierWithCache() { List<UserDO> userDOList = DataUtil.createDataList(10000); long start = System.currentTimeMillis(); List<UserDTO> userDTOS = new ArrayList<>(); userDOList.forEach(userDO -> { UserDTO userDTO = new UserDTO(); copy(userDO, userDTO); userDTOS.add(userDTO); }); } /** * private static final ConcurrentHashMap<String, BeanCopier> BEAN_COPIERS = new ConcurrentHashMap<>(); public void copy(Object srcObj, Object destObj) { String key = genKey(srcObj.getClass(), destObj.getClass()); BeanCopier copier = null; if (! BEAN_COPIERS.containsKey(key)) { copier = BeanCopier.create(srcObj.getClass(), destObj.getClass(), false); BEAN_COPIERS.put(key, copier); } else { copier = BEAN_COPIERS.get(key); } copier.copy(srcObj, destObj, null); } private String genKey(Class<? > srcClazz, Class<? > destClazz) { return srcClazz.getName() + destClazz.getName(); } Duplicate codeCopy the code
3.3 BeanCopier
conclusion
- When the source class and the target class have the same attribute name and type, copying is fine.
- If the source object and the target object have the same name but different types of attributes, the attributes with the same name but different types will not be copied. Note that the primitive type (
int
.short
.char
) and their package types are treated as different types here and therefore will not be copied. - Of the source or target class
setter
thangetter
Small, copy is ok, at this pointsetter
Redundant, but no error. - The source class and the target class have the same attributes
getter
Exists), but for the target classsetter
Does not exist, will be thrownNullPointerException
. - Add cache to improve copy speed.
3.4 Sample Code
- This section tests the code
- Full code address
Four,Orika
Orika is a Java Bean mapping framework that allows you to recursively copy data from one object to another. Its advantages are: the same name and different types can be directly copied.
4.1 Required Dependencies
< the dependency > < groupId > ma. Glasnost. Orika < / groupId > < artifactId > orika - core < / artifactId > < version > 1.5.4 < / version > </dependency> Copy the codeCopy the code
4.2 Mapping Tool Classes
Create a mapping utility class using the singleton pattern implemented by enumeration for easy testing.
Public enum MapperUtils {/** * INSTANCE */ INSTANCE; / * * * * the default field factory/private static final MapperFactory MAPPER_FACTORY = new DefaultMapperFactory. Builder (). The build (); Private static final MapperFacade MAPPER_FACADE = mapper_factory.getMapperfacade (); /** * Private static Map<String, MapperFacade> CACHE_MAPPER_FACADE_MAP = new ConcurrentHashMap<>(); Public <E, T> E map(Class<E> toClass, Class<E> toClass, T data) { return MAPPER_FACADE.map(data, toClass); } /** * mapping entity (custom configuration) ** @param toClass mapping object * @param data data (object) * @param configMap custom configuration * @return mapping object */ public <E, T> E map(Class<E> toClass, T data, Map<String, String> configMap) { MapperFacade mapperFacade = this.getMapperFacade(toClass, data.getClass(), configMap); return mapperFacade.map(data, toClass); } /** * mapping set (default field) ** @param toClass mapping object * @param data (set) * @return mapping set object */ public <E, T> List<E> mapAsList(Class<E> toClass, Collection<T> data) { return MAPPER_FACADE.mapAsList(data, toClass); } /** * mapping set (custom configuration) ** @param toClass mapping class * @param data data (set) * @param configMap custom configuration * @return mapping class object */ public <E, T> List<E> mapAsList(Class<E> toClass, Collection<T> data, Map<String, T T = {String > configMap) data. The stream () findFirst () orElseThrow (() - > new ExceptionInInitializerError (" set mapping, data collection is empty ")); MapperFacade mapperFacade = this.getMapperFacade(toClass, t.getClass(), configMap); return mapperFacade.mapAsList(data, toClass); @param dataClass @param dataClass @param configMap @param dataClass @param configMap @param dataClass T> MapperFacade getMapperFacade(Class<E> toClass, Class<T> dataClass, Map<String, String> configMap) { String mapKey = dataClass.getCanonicalName() + "_" + toClass.getCanonicalName(); MapperFacade mapperFacade = CACHE_MAPPER_FACADE_MAP.get(mapKey); if (Objects.isNull(mapperFacade)) { MapperFactory factory = new DefaultMapperFactory.Builder().build(); ClassMapBuilder classMapBuilder = factory.classMap(dataClass, toClass); configMap.forEach(classMapBuilder::field); classMapBuilder.byDefault().register(); mapperFacade = factory.getMapperFacade(); CACHE_MAPPER_FACADE_MAP.put(mapKey, mapperFacade); } return mapperFacade; }} Copy the codeCopy the code
- There are four main methods in this utility class:
map(Class toClass, T data)
: a normal mapping entity, with fields of the same main mapping name (and possibly different types);map(Class toClass, T data, Map<String, String> configMap)
: User-defined mapping. If the mapping name is different, you can customize the mapping name.mapAsList(Class toClass, Collection data)
: mapping of ordinary sets;mapAsList(Class toClass, Collection data, Map<String, String> configMap)
: User-defined collection mapping, the name of the user-defined mapping.
4.3 Simple Test
- Copy properties with the same name and type that can be different
@test public void normalCopy() {UserDO UserDO = datautil.createData (); Log.info (" before copy: userDO:{}", userDO); UserDTO UserDTO = mapperutils.instance.map (userTo.class, userDO); Log.info (" after copy: userDTO:{}", userDTO); } Duplicate codeCopy the code
- Field name is different, with translation
@test public void converterTest() {UserDO UserDO = datautil.createData (); Map<String, String> config = new HashMap<>(); Sub (balances) config.put("balance", "balances"); Log.info (" before copy: userDO:{}", userDO); UserDomain userDomain = MapperUtils.INSTANCE.map(UserDomain.class, userDO, config); Log.info (" after copy: userDomain:{}", userDomain); } Duplicate codeCopy the code
- Copy a collection
@Test public void beanCopierWithCache() { List<UserDO> userDOList = DataUtil.createDataList(3); Log.info (" before copy: userDOList:{}", userDOList); List<UserDTO> userDTOS = MapperUtils.INSTANCE.mapAsList(UserDTO.class,userDOList); Log.info (" After copy: userDTOS:{}", userDTOS); } Duplicate codeCopy the code
Five,MapStruct
MapStruct is a code generator that automatically generates bean mapping classes. MapStruct can also convert between different data types.
5.1 Required Dependencies
mapstruct-jdk8
Include the required comments, such as @mapping.
< the dependency > < groupId > org. Mapstruct < / groupId > < artifactId > mapstruct - jdk8 < / artifactId > < version > 1.3.0. The Final < / version > </dependency> Copy the codeCopy the code
mapstruct-processor
At compile, generate the annotation processor for the mapper implementation.
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version> 1.3.0.final </version> <scope>provided</scope> </dependency> Copies the codeCopy the code
5.2 How to Use it?
All you have to do is define a Mapper interface that declares any required mapping methods. At compile time, MapStruct generates an implementation of this interface. This implementation uses plain Java method calls to map between source and target objects.
- create
Mapper
The interface/abstract class is automatically mapped by MapStruct using the @mapper annotation, which automatically implements the internal interface methods only if the annotation exists.
- To obtain
Mapper
MapStruct provides a variety of ways to get a Mapper, using the default configuration: Mappers are used to get the Mapper implementation class using the dynamic factory internal reflection mechanism.
UserConvertUtils INSTANCE = Mappers.getMapper(UserConvertUtils.class); Copy the codeCopy the code
Complete a converter demo:
@Mapper public interface UserConvertUtils { UserConvertUtils INSTANCE = Mappers.getMapper(UserConvertUtils.class); /** * Common mapping ** @param userDO userDO data persistence layer class * @return data transfer class */ UserDTO doToDTO(userDO userDO); /** * @param userDO userDO data persistence layer class * @return data transfer class */ @mappings ({@mapping (target = "gmtBroth", source = "gmtBroth", dateFormat = "yyyy-MM-dd HH:mm:ss"), @Mapping(target = "balances", source = "balance"), }) UserDTO doToDtoWithConvert(UserDO userDO); } Duplicate codeCopy the code
- test
@test public void normalCopy() {UserDO UserDO = datautil.createData (); Log.info (" before copy: userDO:{}", userDO); UserDTO userDTO = UserConvertUtils.INSTANCE.doToDTO(userDO); Log.info (" after copy: userDTO:{}", userDTO); } @test public void doToDtoWithConvert() {UserDO UserDO = datautil.createData (); Log.info (" before copy: userDO:{}", userDO); UserDTO userDTO = UserConvertUtils.INSTANCE.doToDtoWithConvert(userDO); Log.info (" after copy: userDTO:{}", userDTO); } Duplicate codeCopy the code
- Printing mapping results
General copy:... UserDO: userDO (id=1, userName=Van, gmtBroth=2020-04-21T21:38:39.376, balance=100)... Sub: userDTO: userDTO (id=1, userName=Van, gmtBroth=2020-04-21T21:38:39.376, balances= NULL) UserDO: userDO (id=1, userName=Van, gmtBroth=2020-04-21T21:05:19.282, balance=100)... Replicates the code userDTO: userDTO (id=1, userName=Van, gmtBroth=2020-04-21 21:05:19, balances=100)Copy the code
By printing the result, you can see that, in contrast, the copy containing the conversion can customize the conversion attributes, time format, and so on.
5.3 MapStruct
Key words of annotations
@Mapper
: Only add this annotation to the interface,MapStruct
To implement the interface;@Mappings
: Configure multiple configurations.@Mapping
;@Mapping
: attribute mapping. If the source object attribute is the same as the target object name, the corresponding attribute will be automatically mapped:
source
: Source attribute;target
: Target attribute;dateFormat
: converts between strings and dates;ignore
: Ignore this, an attribute that does not want to be mapped can be addedignore=true
;
5.4 for a more
MapStruct can map objects of several types to another type, such as converting multiple DO objects to Dtos.
See:
UserDTO doAndInfoToDto(UserDO userDO, UserInfoDO userInfoDO); Copy the codeCopy the code
5.5 WhyMapStruct
MapStruct saves time by generating tedious and easy-to-write code, compared to writing mapping code by hand. Convention is better than configuration. MapStruct uses reasonable defaults, but takes action when configuring or implementing special behavior.
MapStruct has the following advantages over dynamic mapping frameworks:
- Fast execution by using normal method calls instead of reflection
- Compile-time type safety: Only objects and properties that map to each other are mapped, and no order entities are accidentally mapped to customers
DTO
And so on. - Clear error reports at build time if the mapping is incomplete (not all target attributes are mapped) the mapping is incorrect (no suitable mapping method or cast can be found)
5.6 Example Code
- This section tests the code
- Full code address
Six or more
Four methods of property copy, plus your own manual get/set, just give the following suggestions:
- Simple copy is used directly
get/set
; - Too many copies of property values already in use
Spring
In case of useBeanUtils
; - Attribute copy is cumbersome, there is translation and copy speed requirements for use
MapStruct
(Performance is almost the same as directget/set
).
For details, see article: Java Bean Copy Performance Comparison
The module complete Github sample source code
Refer to the blog
Refer to the blog
Refer to the blog
[Reference blog](