捉蟲記之dozer對映父類屬性被重寫

qixiaobo發表於2018-01-05

title: 捉蟲記之dozer對映父類屬性被重寫 tags:

  • dozer
  • cache
  • super
  • classmapping
  • bug categories: 工作日誌 date: 2017-06-25 18:18:54

Dozer是我們常用的Java的拷貝工具,一般用來做屬性的對映。我們通常封裝如下:

    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;
        }
複製程式碼

我們在使用時通常如下

    StockInDetailVo stockInDetailVo = DozerHelper.convert(maintainPartDetail, StockInDetailVo.class);
    TsStockInDetail infoDetail = DozerHelper.convert(costPartDetail, TsStockInDetail.class);
複製程式碼

簡單描述一下問題:dozer做對映時發現某個屬性對映出錯成同名屬性而不是我們在xml中指定的屬性導致發生資料出錯 交代一下dozer的座標

    <dependency>
        <groupId>net.sf.dozer</groupId>
        <artifactId>dozer</artifactId>
        <version>5.2.0</version>
    </dependency>
複製程式碼

我們先來貼一下類圖

183222_mFgS_871390.png

問題描述完成後我們來看一下產生的原因。

出錯的程式碼在於

    StockInDetailVo stockInDetailVo = DozerHelper.convert(maintainPartDetail, StockInDetailVo.class);
    TsStockInDetail infoDetail = DozerHelper.convert(costPartDetail, TsStockInDetail.class);
複製程式碼

如上述問題我們可以抽象出兩個對映轉換,物件到父類的轉換和物件到子類的轉換。便於說明把父類叫做super 子類叫做child

假設兩種場景

  1. 優先呼叫物件到super的轉換,再呼叫物件到child的轉換
  2. 優先呼叫物件到child的轉換,再呼叫物件到super的轉換

Dozer在做bean的轉換時執行如下方法

    public <T> T map(Object source, Class<T> destinationClass, String mapId) throws MappingException {
      return getMappingProcessor().map(source, destinationClass, mapId);
    }
複製程式碼

真正的核心轉換程式碼

    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);
      }
    }
複製程式碼

從上述程式碼可以看到,dozer會對應超類進行處理,而獲取超類轉換的方法如下

    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;
    }
複製程式碼

首先check快取中是否存在對應的超類集合,如果沒有那麼就從classMapper中取同時放入快取,否則返回快取。

當快取不存在查詢父類的classMapper如下

    private void checkForClassMapping(Class<?> srcClass, List<ClassMap> superClasses, Class<?> superDestClass) {
      ClassMap srcClassMap = classMappings.find(srcClass, superDestClass);
      if (srcClassMap != null) {
        superClasses.add(srcClassMap);
      }
    }
複製程式碼

其實就是在classMapping中找到對應的型別。那麼如果在classMapping是如何建立對應的型別呢?

在MappingProcessor中存在方法如下

    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;
    }
複製程式碼

該方法呼叫classMappings.find(srcClass, destClass, mapId); 注意包含第三個引數

該方法如下

    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;
    }

複製程式碼

當查詢classMappings中包含指定型別直接返回,否則先處理interface介面(當轉換型別為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;
    }
複製程式碼

那麼當destClass為父類的情況下

這段邏輯將會返回目的型別為子類的源型別相同的mapping

    if (destClass.isAssignableFrom(mappingDestClass) ||
            (mappingDestClass.isInterface() && mappingDestClass.isAssignableFrom(destClass))) {
          if (srcClass.equals(mappingSrcClass)) {
            return map;
          }
        }
複製程式碼

本意應該是針對父介面(返回子類的實現),但是此處由於開發沒有判斷是否是介面或者抽象類導致出現返回了到子類的mapping

那麼如果使用者在返回撥用父類轉換之前曾經呼叫過子類的轉換,那麼此時當xml沒有自定義型別轉換時將會返回到子型別的轉換對映(此處應修正)

這邊考慮可能會產生的問題如下:

  1. 如果呼叫父類轉換的時候存在之類的轉換 (並且xml沒有自定義的父類轉換以及mapid)那麼將會返回子類轉換 換言之必然先呼叫了子類轉換(或者高併發場景)===》此時子類查詢父類cache尚未完成,而父類可以應用到子類的mapping

    基於我們系統中比較容易復現的場景,先去除這種可能性(由於併發)

  2. 那麼假設此處查詢子類mapping是沒有得到返回會是何種結果呢?dozer仍然會建立基於父類的對映

       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;
       }
``` plain
   如上述程式碼,即使classmapping無法找到對應的mapping依然會建立

       ClassMapBuilder.createDefaultClassMap(globalConfiguration, srcClass, destClass)那麼下次將可以找到

    

   考慮場景如果先呼叫父類的轉換 那麼classmapping中將存在對應的父類轉換,當呼叫子類的轉換時將找到對應的父類轉換並放入cache(此處放入快取的資料由前面是否呼叫過父類決定)

   dozer處理父類介面直接呼叫

   ```java
   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;
   }</code></pre> <p>注意此時呼叫map方法傳遞的引數bypassSuperMappings為true</p> <p>&nbsp;</p> <p>那麼這段程式碼執行結果</p> <pre><code>if (!bypassSuperMappings) {
     Collection&lt;ClassMap> superMappings = new ArrayList&lt;ClassMap>();

     Collection&lt;ClassMap> superClasses = checkForSuperTypeMapping(srcClass, destClass);
     //List&lt;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 &amp;&amp; mappedParentFields.contains(key)) {
       continue;
     }
     mapField(fieldMapping, srcObj, destObj);
   }
``` plain


   當存在父類(多個)的場景 那麼mappedParentFields在呼叫父類迴圈是一直為空,那麼可能導致第二次呼叫父類的迴圈時會重新寫入第一次父類已經寫過的值<br> 而dozer在最終處理子類的時候是通過mappedParentFields是否包含已經處理過的key來決定的。

   測試用例如下

   ```java
   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的xml如下

   ```xml
    <mapping type="one-way">
  <class-a>com.air.tqb.model.TsMaintainPartDetail&lt;/class-a>
  <class-b>com.air.tqb.model.TsStockInDetail&lt;/class-b>
  <field>
      <a>stockOutNumber&lt;/a>
      <b>number&lt;/b>
  </field>
  <field>
      <a>stockOutNumber&lt;/a>
      <b>sourceNumber&lt;/b>
  </field>
  <field>
      <a>id&lt;/a>
      <b>idSourceDetail&lt;/b>
  </field>
  <field>
      <a>avgPrice&lt;/a>
      <b>price&lt;/b>
  </field>
  <field>
      <a>avgPriceNoTax&lt;/a>
      <b>noTaxPrice&lt;/b>
  </field>
  <field>
      <a>idMaintain&lt;/a>
      <b>idSourceBill&lt;/b>
  </field>
複製程式碼
```

結果如果junit的兩句執行順序變化將會有不一樣的結果> 捉蟲記之dozer對映父類屬性被重寫因此在不升級版本的情況下優先

在對應的mapper中自定義該轉換型別,可以避免問題

 

參考一下程式碼  github.com/qixiaobo/do…

看了一下程式碼5.4.0已經修復  github.com/DozerMapper…>

相關文章