1. 概述
日常Java開發項目中,我們經常需要將對象轉換成其他形式的對象,因此我們需要編寫映射代碼將對象中的屬性值從一種類型轉換成另一種類型。
進行這種轉換除了手動編寫大量的get/set
代碼,還可以使用一些方便的類庫:
apache的BeanUtils
spring的BeanUtils
cglib的BeanCopier
。
2.比較
2.1 BeanUtils
BeanUtils一套開發包,Apache公司提供 ,專門進行javabean操作,在web層各種框架中被使用,例如:struts 使用BeanUtils操作JavaBean 。
實例
1、下載BeanUtils的jar :commons-beanutils 、commons-logging,需要同時下載兩個jar包。(BeanUtils依賴Logging的jar包 )
2、將beanutils和logging的 jar包復制 工程/WebContent/WEB-INF/lib
apache的BeanUtils
和spring的BeanUtils
中拷貝方法的原理都是先用jdk中 java.beans.Introspector
類的getBeanInfo()
方法獲取對象的屬性信息及屬性get/set方法,接着使用反射(Method
的invoke(Object obj, Object... args)
)方法進行賦值。apache支持名稱相同但類型不同的屬性的轉換,spring支持忽略某些屬性不進行映射,他們都設置了緩存保存已解析過的BeanInfo
信息。
commons.beanutils-1.8.3.jar
spring.beans-4.2.3.RELEASE.jar
<dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.3</version> </dependency>
import org.apache.commons.beanutils.BeanUtils;
public class TestBeanUtils extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.設置編碼
req.setCharacterEncoding("utf-8");
//2.獲取數據
Map<String, String[]> params = req.getParameterMap();
//System.out.println(params);
//3.使用BeanUtils工具類封裝User對象
Users user = new Users();
try {
BeanUtils.populate(user, params);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
System.out.println(user);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
}
2.2 BeanCopier
cglib的BeanCopier
采用了不同的方法:它不是利用反射對屬性進行賦值,而是直接使用ASM的MethodVisitor
直接編寫各屬性的get/set
方法(具體過程可見BeanCopier
類的generateClass(ClassVisitor v)
方法)生成class文件,然后進行執行。由於是直接生成字節碼執行,所以BeanCopier
的性能較采用反射的BeanUtils
有較大提高,這一點可在后面的測試中看出。
2.3 Dozer
使用以上類庫雖然可以不用手動編寫get/set
方法,但是他們都不能對不同名稱的對象屬性進行映射。在定制化的屬性映射方面做得比較好的有Dozer,Dozer支持簡單屬性映射、復雜類型映射、雙向映射、隱式映射以及遞歸映射。可使用xml或者注解進行映射的配置,支持自動類型轉換,使用方便。但Dozer底層是使用reflect包下Field
類的set(Object obj, Object value)
方法進行屬性賦值,執行速度上不是那么理想。
2.4 Orika
那么有沒有特性豐富,速度又快的Bean映射工具呢,這就是下面要介紹的Orika,Orika是近期在github活躍的項目,底層采用了javassist類庫生成Bean映射的字節碼,之后直接加載執行生成的字節碼文件,因此在速度上比使用反射進行賦值會快很多,下面詳細介紹Orika的使用方法。
3. Orika使用
依賴
<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>1.5.2</version><!-- or latest version --> </dependency>
簡單映射
- 構造一個MapperFactory
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
- 注冊字段映射
mapperFactory.classMap(PersonSource.class, PersonDestination.class) .field("firstName", "givenName") .field("lastName", "sirName") .byDefault() .register();
- 進行映射
MapperFacade mapper = mapperFactory.getMapperFacade(); PersonSource source = new PersonSource(); // set some field values ... // map the fields of 'source' onto a new instance of PersonDest PersonDest destination = mapper.map(source, PersonDest.class);
在第二步進行的字段映射是雙向的,我們可以從目標類型映射回源類型,byDefault()
方法用於注冊名稱相同的屬性(如果所有屬性名稱都相同則可以省略第2步),如果不希望某個字段參與映射,可以使用exclude
方法
復雜映射
數組和List的映射
如果在目標類和目的類中分別有下面的屬性
class BasicPerson { private List<String> nameParts; // getters/setters omitted } class BasicPersonDto { private String firstName; private String lastName; // getters/setters omitted }
可以使用下面的方式進行映射:
mapperFactory.classMap(BasicPerson.class, BasicPersonDto.class) .field("nameParts[0]", "firstName") .field("nameParts[1]", "lastName") .register();
類類型的映射
class Name { private String first; private String last; private String fullName; // getters/setters } class BasicPerson { private Name name; // getters/setters omitted } class BasicPersonDto { private String firstName; // getters/setters omitted }
使用:
mapperFactory.classMap(BasicPerson.class, BasicPersonDto.class) .field("name.first", "firstName") .register();
自定義轉換器
orika同樣支持自定義轉換器,將指定類型或指定名稱的屬性做映射時添加自定義操作,例如,將String類型的或某個屬性映射后加一個前綴,或者將Integer類型映射后加1等:
public class MyConverter extends CustomConverter<Date,MyDate> { public MyDate convert(Date source, Type<? extends MyDate> destinationType) { // return a new instance of destinationType with all properties filled //example:source + 1; } }
Date
為源類型中要做轉換的屬性數據類型,例如String
、Integer
等,MyDate
為目標類型中要做轉換的屬性數據類型。
如果需要定義全局范圍的轉換:
ConverterFactory converterFactory = mapperFactory.getConverterFactory(); converterFactory.registerConverter(new MyConverter());
如果僅需要某幾個屬性使用轉換器:
ConverterFactory converterFactory = mapperFactory.getConverterFactory(); converterFactory.registerConverter("myConverterIdValue", new MyConverter()); mapperFactory.classMap( Source.class, Destination.class ) .fieldMap("sourceField1", "sourceField2").converter("myConverterIdValue").add() ... .register();
其他說明
-
Orika支持遞歸映射,將映射嵌套類直到用“簡單”類型完成映射。它還包含故障保險,以正確處理正在嘗試映射的對象中的遞歸引用。
-
在於spring集成時,可以將MapperFactory設置為單例
各映射工具的性能測試
構造一個包含普通類型及類類型的Bean對象,使用jmh微基准框架進行測試。由於jvm會對熱點代碼進行優化:方法反射調用次數超過閾值時會生成一個專用的MethodAccessor實現類,生成其中的invoke()方法的字節碼進行執行。
故測試時每種方法先預熱執行15次,而后再執行100次獲取每次執行的平均時間:
Benchmark Mode Samples Score Score error Units
o.s.MyBenchmark.apache avgt 100 25.246 0.535 us/op
o.s.MyBenchmark.beanCopier avgt 100 0.004 0.000 us/op
o.s.MyBenchmark.byHand avgt 100 0.004 0.000 us/op
o.s.MyBenchmark.dozer avgt 100 5.855 0.260 us/op
o.s.MyBenchmark.orika avgt 100 0.353 0.017 us/op
o.s.MyBenchmark.spring avgt 100 0.627 0.020 us/op
統計報告中Units單位為微秒/次,由Score項可以看出,基於ASM的cglib BeanCopier拷貝速度基本和手寫get/set方法的速度無異,其次的就是基於javassist的Orika了,Orika的速度是spring BeanUtils的兩倍,Dozer的20倍,Apache BeanUtils的120倍。
綜上,當屬性名和屬性類型完全相同時使用BeanCopier是最好的選擇,當存在屬性名稱不同或者屬性名稱相同但屬性類型不同的情況時,使用Orika是一種不錯的選擇。如果你對Orika感到不放心,實際應用前可以寫個測試類查看它的轉換結果是否符合預期。