Dozer is a common Java copy tool, which is commonly used to map attributes. We usually encapsulate it as follows:
public class DozerHelper { private static Mapper mapper; public static <P> P clone(P base) { if (base == null) { return null; } if (ClassUtils.isPrimitiveOrWrapper(base.getClass()) || base instanceof String) { return base; } else { Mapper mapper = getMapper(); return (P) mapper.map(base, base.getClass()); } } public static <P> List<P> cloneList(List<P> baseList) { if (baseList == null) { return null; } else { List<P> targetList = Lists.newArrayListWithExpectedSize(baseList.size()); for (P p : baseList) { targetList.add(clone(p)); } return targetList; } } public static <P> Set<P> cloneSet(Set<P> baseSet) { if (baseSet == null) { return null; } else { Set<P> targetSet = Sets.newHashSetWithExpectedSize(baseSet.size()); for (P p : baseSet) { targetSet.add(clone(p)); } return targetSet; } } public static <V, P> P convert(V base, Class<P> target) { if (base == null) { return null; } else { Mapper mapper = getMapper(); return mapper.map(base, target); } } public static <V, P> P convert(V base, P target) { if (base != null) { Mapper mapper = getMapper(); mapper.map(base, target); return target; } return target; } public static <V, P> List<P> convertList(List<V> baseList, Class<P> target) { if (baseList == null) { return null; } else { List<P> targetList = Lists.newArrayListWithExpectedSize(baseList.size()); for (V vo : baseList) { targetList.add(convert(vo, target)); } return targetList; } } public static Mapper getMapper() { return mapper; } public static void setMapper(Mapper mapper) { DozerHelper.mapper = mapper; }
Usually we use it as follows
StockInDetailVo stockInDetailVo = DozerHelper.convert(maintainPartDetail, StockInDetailVo.class); TsStockInDetail infoDetail = DozerHelper.convert(costPartDetail, TsStockInDetail.class);
Describe the problem briefly: When dozer maps, it is found that an attribute mapped incorrectly into the same-name attribute instead of the attribute we specified in xml causes a data error to account for the coordinates of dozer.
<dependency> <groupId>net.sf.dozer</groupId> <artifactId>dozer</artifactId> <version>5.2.0</version> </dependency>
Let's stick up the class diagram first.
After the problem description is completed, let's look at the causes.
The error code is
StockInDetailVo stockInDetailVo = DozerHelper.convert(maintainPartDetail, StockInDetailVo.class); TsStockInDetail infoDetail = DozerHelper.convert(costPartDetail, TsStockInDetail.class);
As mentioned above, we can abstract two mapping transformations, object-to-parent and object-to-child. It's easy to explain that the parent class is called super subclass and the child is called super subclass.
Assume two scenarios
- Conversion from object to super is invoked first, and then from object to child is invoked.
- The object-to-child transformation is invoked first, and then the object-to-super transformation is invoked.
Dozer performs the following methods when doing bean transformation
public <T> T map(Object source, Class<T> destinationClass, String mapId) throws MappingException { return getMappingProcessor().map(source, destinationClass, mapId); }
Real Core Conversion Code
private void map(ClassMap classMap, Object srcObj, Object destObj, boolean bypassSuperMappings, String mapId) { // 1596766 - Recursive object mapping issue. Prevent recursive mapping // infinite loop. Keep a record of mapped fields // by storing the id of the sourceObj and the destObj to be mapped. This can // be referred to later to avoid recursive mapping loops mappedFields.put(srcObj, destObj); // If class map hasnt already been determined, find the appropriate one for // the src/dest object combination if (classMap == null) { classMap = getClassMap(srcObj.getClass(), destObj.getClass(), mapId); } Class<?> srcClass = srcObj.getClass(); Class<?> destClass = destObj.getClass(); // Check to see if custom converter has been specified for this mapping // combination. If so, just use it. Class<?> converterClass = MappingUtils.findCustomConverter(converterByDestTypeCache, classMap.getCustomConverters(), srcClass, destClass); if (converterClass != null) { mapUsingCustomConverter(converterClass, srcClass, srcObj, destClass, destObj, null, true); return; } // Now check for super class mappings. Process super class mappings first. List<String> mappedParentFields = null; if (!bypassSuperMappings) { Collection<ClassMap> superMappings = new ArrayList<ClassMap>(); Collection<ClassMap> superClasses = checkForSuperTypeMapping(srcClass, destClass); //List<ClassMap> interfaceMappings = classMappings.findInterfaceMappings(srcClass, destClass); superMappings.addAll(superClasses); //superMappings.addAll(interfaceMappings); if (!superMappings.isEmpty()) { mappedParentFields = processSuperTypeMapping(superMappings, srcObj, destObj, mapId); } } // Perform mappings for each field. Iterate through Fields Maps for this class mapping for (FieldMap fieldMapping : classMap.getFieldMaps()) { //Bypass field if it has already been mapped as part of super class mappings. String key = MappingUtils.getMappedParentFieldKey(destObj, fieldMapping); if (mappedParentFields != null && mappedParentFields.contains(key)) { continue; } mapField(fieldMapping, srcObj, destObj); } }
As you can see from the above code, dozer will process the corresponding superclass, and the way to get the superclass transformation is as follows
private Collection<ClassMap> checkForSuperTypeMapping(Class<?> srcClass, Class<?> destClass) { // Check cache first Object cacheKey = CacheKeyFactory.createKey(destClass, srcClass); Collection<ClassMap> cachedResult = (Collection<ClassMap>) superTypeCache.get(cacheKey); if (cachedResult != null) { return cachedResult; } // If no existing cache entry is found, determine super type mappings. // Recursively walk the inheritance hierarchy. List<ClassMap> superClasses = new ArrayList<ClassMap>(); // Need to call getRealSuperclass because proxied data objects will not return correct // superclass when using basic reflection List<Class<?>> superSrcClasses = MappingUtils.getSuperClassesAndInterfaces(srcClass); List<Class<?>> superDestClasses = MappingUtils.getSuperClassesAndInterfaces(destClass); // add the actual classes to check for mappings between the original and the opposite // super classes superSrcClasses.add(0, srcClass); superDestClasses.add(0, destClass); for (Class<?> superSrcClass : superSrcClasses) { for (Class<?> superDestClass : superDestClasses) { if (!(superSrcClass.equals(srcClass) && superDestClass.equals(destClass))) { checkForClassMapping(superSrcClass, superClasses, superDestClass); } } } Collections.reverse(superClasses); // Done so base classes are processed first superTypeCache.put(cacheKey, superClasses); return superClasses; }
First, check whether there is a corresponding superclass set in the cache, if not, take it from the classMapper and put it into the cache at the same time, otherwise return to the cache.
When the cache does not exist, find the parent class classMapper as follows
private void checkForClassMapping(Class<?> srcClass, List<ClassMap> superClasses, Class<?> superDestClass) { ClassMap srcClassMap = classMappings.find(srcClass, superDestClass); if (srcClassMap != null) { superClasses.add(srcClassMap); } }
In fact, it is to find the corresponding type in the classMapping. So how do classMapping create the corresponding type?
The following methods exist in Mapping Processor
private ClassMap getClassMap(Class<?> srcClass, Class<?> destClass, String mapId) { ClassMap mapping = classMappings.find(srcClass, destClass, mapId); if (mapping == null) { // If mapId was specified and mapping was not found, then throw an // exception if (!MappingUtils.isBlankOrNull(mapId)) { MappingUtils.throwMappingException("Class mapping not found for map-id : " + mapId); } // If mapping not found in existing custom mapping collection, create // default as an explicit mapping must not // exist. The create default class map method will also add all default // mappings that it can determine. mapping = ClassMapBuilder.createDefaultClassMap(globalConfiguration, srcClass, destClass); classMappings.add(srcClass, destClass, mapping); } return mapping; }
This method calls classMappings.find(srcClass, destClass, mapId); note that there is a third parameter
The method is as follows.
public ClassMap find(Class<?> srcClass, Class<?> destClass, String mapId) { ClassMap mapping = classMappings.get(keyFactory.createKey(srcClass, destClass, mapId)); if (mapping == null) { mapping = findInterfaceMapping(destClass, srcClass, mapId); } // one more try... // if the mapId is not null looking up a map is easy if (mapId != null && mapping == null) { // probably a more efficient way to do this... for (Entry<String, ClassMap> entry : classMappings.entrySet()) { ClassMap classMap = entry.getValue(); if (StringUtils.equals(classMap.getMapId(), mapId) && classMap.getSrcClassToMap().isAssignableFrom(srcClass) && classMap.getDestClassToMap().isAssignableFrom(destClass)) { return classMap; } else if (StringUtils.equals(classMap.getMapId(), mapId) && srcClass.equals(destClass)) { return classMap; } } log.info("No ClassMap found for mapId:" + mapId); } return mapping; }
When looking for classMappings that contain the specified type, return directly, otherwise process the interface interface (when the conversion type is interface)
// Look for an interface mapping private ClassMap findInterfaceMapping(Class<?> destClass, Class<?> srcClass, String mapId) { // Use object array for keys to avoid any rare thread synchronization issues // while iterating over the custom mappings. // See bug #1550275. Object[] keys = classMappings.keySet().toArray(); for (Object key : keys) { ClassMap map = classMappings.get(key); Class<?> mappingDestClass = map.getDestClassToMap(); Class<?> mappingSrcClass = map.getSrcClassToMap(); if ((mapId == null && map.getMapId() != null) || (mapId != null && !mapId.equals(map.getMapId()))) { continue; } if (mappingSrcClass.isInterface() && mappingSrcClass.isAssignableFrom(srcClass)) { if (mappingDestClass.isInterface() && mappingDestClass.isAssignableFrom(destClass)) { return map; } else if (destClass.equals(mappingDestClass)) { return map; } } if (destClass.isAssignableFrom(mappingDestClass) || (mappingDestClass.isInterface() && mappingDestClass.isAssignableFrom(destClass))) { if (srcClass.equals(mappingSrcClass)) { return map; } } } return null; }
So when destClass is the parent class
This logic will return mapping with the same source type as the target type as the subclass
if (destClass.isAssignableFrom(mappingDestClass) || (mappingDestClass.isInterface() && mappingDestClass.isAssignableFrom(destClass))) { if (srcClass.equals(mappingSrcClass)) { return map; } }
The intent should be to target the parent interface (returning the implementation of the subclass), but the mapping that returns to the subclass occurs here because the development does not determine whether it is an interface or an abstract class?
If the user has invoked the transformation of a subclass before returning to invoke the parent transformation, then when xml does not customize the type conversion, it will return to the transformation mapping of the subtype (which should be amended here).
The problems that may arise from this consideration are as follows:
- If there are such transformations when calling parent transformations (and xml does not have custom parent transformations and mapid), then subclass transformations will be returned
In other words, the subclass transformation (or high concurrency scenario) ==== must be invoked first, when the subclass search for the parent class cache is not complete, and the parent class can be applied to the mapping of the subclass.
Based on the scenarios that are easy to reproduce in our system, remove this possibility first (due to concurrency) -
So what would it be like to look for a subclass mapping here without returning it? dozer will still create parent-based mappings
private ClassMap getClassMap(Class<?> srcClass, Class<?> destClass, String mapId) { ClassMap mapping = classMappings.find(srcClass, destClass, mapId); if (mapping == null) { // If mapId was specified and mapping was not found, then throw an // exception if (!MappingUtils.isBlankOrNull(mapId)) { MappingUtils.throwMappingException("Class mapping not found for map-id : " + mapId); } // If mapping not found in existing custom mapping collection, create // default as an explicit mapping must not // exist. The create default class map method will also add all default // mappings that it can determine. mapping = ClassMapBuilder.createDefaultClassMap(globalConfiguration, srcClass, destClass); classMappings.add(srcClass, destClass, mapping); } return mapping; }
As mentioned above, even if the class mapping cannot find the corresponding mapping, it will still be created.
ClassMapBuilder. createDefaultClassMap (global Configuration, srcClass, destClass) will be available next time
Consider the scenario where the parent class transformation is invoked first, then there will be a corresponding parent transformation in the class mapping. When the transformation of the child class is invoked, the corresponding parent transformation will be found and put into the cache (the data put in the cache here is determined by whether the parent class has been invoked before).
dozer handles direct calls to parent interfaces
private List<String> processSuperTypeMapping(Collection<ClassMap> superClasses, Object srcObj, Object destObj, String mapId) { List<String> mappedFields = new ArrayList<String>(); for (ClassMap map : superClasses) { map(map, srcObj, destObj, true, mapId); for (FieldMap fieldMapping : map.getFieldMaps()) { String key = MappingUtils.getMappedParentFieldKey(destObj, fieldMapping); mappedFields.add(key); } } return mappedFields; }
Note that the parameter bypassSuperMappings passed by calling the map method at this time is true
So the result of this code execution
if (!bypassSuperMappings) { Collection<ClassMap> superMappings = new ArrayList<ClassMap>(); Collection<ClassMap> superClasses = checkForSuperTypeMapping(srcClass, destClass); //List<ClassMap> interfaceMappings = classMappings.findInterfaceMappings(srcClass, destClass); superMappings.addAll(superClasses); //superMappings.addAll(interfaceMappings); if (!superMappings.isEmpty()) { mappedParentFields = processSuperTypeMapping(superMappings, srcObj, destObj, mapId); } } // Perform mappings for each field. Iterate through Fields Maps for this class mapping for (FieldMap fieldMapping : classMap.getFieldMaps()) { //Bypass field if it has already been mapped as part of super class mappings. String key = MappingUtils.getMappedParentFieldKey(destObj, fieldMapping); if (mappedParentFields != null && mappedParentFields.contains(key)) { continue; } mapField(fieldMapping, srcObj, destObj); }
When there are scenarios where the parent class (multiple) exists, mappedParentFields will always be empty when calling the parent loop, which may result in a second call to the parent loop rewriting the values already written by the first parent class.
When dozer finally processes subclasses, it is determined by whether mappedParentFields contain key s that have already been processed.For example
import com.air.tqb.dozer.DozerHelper; import com.air.tqb.model.TsStockInDetail; import com.air.tqb.test.base.BaseTest; import com.air.tqb.vo.StockInDetailVo; import com.air.tqb.vo.TsMaintainPartDetailVO; import org.dozer.DozerBeanMapper; import org.dozer.Mapper; import org.dozer.cache.CacheManager; import org.dozer.cache.DozerCacheType; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.lang.reflect.Field; /** * Created by qixiaobo on 2017/6/27. */ public class DozerTest extends BaseTest { @Autowired private Mapper mapper; @Before public void clearSuperCache() { if (mapper instanceof DozerBeanMapper) { try { Field cacheManager = DozerBeanMapper.class.getDeclaredField("cacheManager"); cacheManager.setAccessible(true); ((CacheManager) cacheManager.get(mapper)).getCache(DozerCacheType.SUPER_TYPE_CHECK.name()).clear(); } catch (NoSuchFieldException | IllegalAccessException e) { logger.error(e.getMessage(), e); } } } @Before public void clearCLassMapping() { if (mapper instanceof DozerBeanMapper) { try { Field customMappings = DozerBeanMapper.class.getDeclaredField("customMappings"); customMappings.setAccessible(true); customMappings.set(mapper, null); } catch (NoSuchFieldException | IllegalAccessException e) { logger.error(e.getMessage(), e); } } } @Test public void testDozer() { TsMaintainPartDetailVO vo = new TsMaintainPartDetailVO(); vo.setPrice(100d); vo.setAvgPrice(200d); TsStockInDetail convertParent = DozerHelper.convert(vo, TsStockInDetail.class); StockInDetailVo convertChild = DozerHelper.convert(vo, StockInDetailVo.class); Assert.assertTrue(convertParent.getPrice().equals(convertChild.getPrice())); } @Test public void testDozer2() { TsMaintainPartDetailVO vo = new TsMaintainPartDetailVO(); vo.setPrice(100d); vo.setAvgPrice(200d); StockInDetailVo convertChild = DozerHelper.convert(vo, StockInDetailVo.class); TsStockInDetail convertParent = DozerHelper.convert(vo, TsStockInDetail.class); Assert.assertTrue(convertParent.getPrice().equals(convertChild.getPrice())); }
dozer's xml is as follows
<mapping type="one-way"> <class-a>com.air.tqb.model.TsMaintainPartDetail</class-a> <class-b>com.air.tqb.model.TsStockInDetail</class-b> <field> <a>stockOutNumber</a> <b>number</b> </field> <field> <a>stockOutNumber</a> <b>sourceNumber</b> </field> <field> <a>id</a> <b>idSourceDetail</b> </field> <field> <a>avgPrice</a> <b>price</b> </field> <field> <a>avgPriceNoTax</a> <b>noTaxPrice</b> </field> <field> <a>idMaintain</a> <b>idSourceBill</b> </field> </mapping>
As a result, if the execution order of the two sentences of junit changes, there will be different results.
So you can avoid the problem by customizing the transformation type in the corresponding mapper first without updating the version.