1.toString实现
测试不同的toString实现方式的效率,目前包括的case:
1.String的+连接生成;
2.StringBuilder生成;
3.StringBuffer生成;
4.Guava的ToStringHelper生成;
5.Apache.commons.lang3的lang3的ToStringBuilder生成;
6.Apache.commons.lang3的ReflectionToStringBuilder生成;
toString的对象包含各种类型的属性:
1
2
3
4
5
6
7
8
9
10
11
private int age;
private String name;
private String address;
private List<String> phone;
private BigDecimal salary;
private Map<String, Object> attrs;
各个toString的实现版本,其中ReflectionToStringBuilder是我们项目中使用最多的,因为它最简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 使用字符串+
* @return
*/
public String concatToString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", address='" + address + '\'' +
", phone=" + phone +
", salary=" + salary +
", attrs=" + attrs +
'}';
}
/**
* 使用StringBuilder
* @return
*/
public String stringBuilderToString() {
StringBuilder builder = new StringBuilder("Person{");
builder.append("age=").append(age)
.append(", name='").append(name).append("'")
.append(", address='").append(address).append("'")
.append(", phone=").append(phone)
.append(", salary=").append(salary)
.append(", attrs=").append(attrs)
.append("}");
return builder.toString();
}
/**
* 使用StringBuffer
* @return
*/
public String stringBufferToString() {
StringBuffer buffer = new StringBuffer("Person{");
buffer.append("age=").append(age)
.append(", name='").append(name).append("'")
.append(", address='").append(address).append("'")
.append(", phone=").append(phone)
.append(", salary=").append(salary)
.append(", attrs=").append(attrs)
.append("}");
return buffer.toString();
}
/**
* Guava的Objects,即ToStringHelper
* @return
*/
public String toStringHelperToString() {
return Objects.toStringHelper(this)
.add("age", age)
.add("name", name)
.add("address", address)
.add("phone", phone)
.add("salary", salary)
.add("attrs", attrs)
.toString();
}
/**
* apache.commons.lang3的ToStringBuilder
* @return
*/
public String toStringBuilderToString() {
return new ToStringBuilder(this)
.append("age", age)
.append("name", name)
.append("address", address)
.append("phone", phone)
.append("salary", salary)
.append("attrs", attrs)
.toString();
}
/**
* apache.commons.lang3的ReflectionToStringBuilder
* @return
*/
public String reflectionToStringBuilderToString() {
return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE);
}
OS是Ubuntu 14.04 LTS版本,Java版本是1.7.0_80。跑1000次,记录最大最小平均时间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Concat Min(微秒)=25.385
Concat Max(微秒)=2218.851
Concat Avg(微秒)=56.728432999999995
StringBuilder Min(微秒)=22.501
StringBuilder Max(微秒)=6934.066
StringBuilder Avg(微秒)=50.724183
StringBuffer Min(微秒)=29.553
StringBuffer Max(微秒)=3159.808
StringBuffer Avg(微秒)=64.113464
ToStringHelper Min(微秒)=44.893
ToStringHelper Max(微秒)=9503.836
ToStringHelper Avg(微秒)=112.351293
ToStringBuilder Min(微秒)=62.806
ToStringBuilder Max(微秒)=20228.186
ToStringBuilder Avg(微秒)=194.728656
ReflectionToStringBuilder Min(微秒)=37.057
ReflectionToStringBuilder Max(微秒)=11402.172
ReflectionToStringBuilder Avg(微秒)=219.219879
整理成表格,数据如下:
toString策略
平均(微秒)
最大(微秒)
最小(微秒)
Concat
56.728433
2218.851
25.385
StringBuilder
50.724183
6934.066
22.501
StringBuffer
64.113464
3159.808
29.553
ToStringHelper
112.351293
9503.836
44.893
ToStringBuilder
194.728656
20228.186
62.806
ReflectionToStringBuilder
219.219879
11402.172
37.057
通过上面的数据,concat和StringBuilder及StringBuffer的速度差不多。其他集中语法糖普遍慢一些,我们最常用的ReflectionToStringBuilder实际上是最慢的一个版本。
1.1 实现原理
简单分析一下各个toString方式的实现原理。
没什么说的,使用的是字符串的拼接。
内部使用char数组,每次append都会先检查数组空间释放足够,不够的话先申请一块临时数组,大小是原来的两倍大小,再用Arrays.copyOf将数据拷贝。
和StringBuilder一样,也是继承自AbstractStringBuilder类,唯一区别是为了保证线程安全,变更数据的方法都加上了synchronized关键字。
Guava的ToStringHelper内部实现是一个简单链表,每个链表节点包含name,value以及下一个节点的指针。
1
2
3
4
5
6
7
8
private static final class ValueHolder {
String name;
Object value;
Objects.ToStringHelper.ValueHolder next;
private ValueHolder() {
}
}
ToStringHelper类持有链表头节点和尾节点。每次调用add都是在链表尾部加一个节点,toString() 的时候遍历整个链表讲内容加到一个StringBuilder中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static final class ToStringHelper {
private final String className;
private Objects.ToStringHelper.ValueHolder holderHead;
private Objects.ToStringHelper.ValueHolder holderTail;
public Objects.ToStringHelper add(String name, long value) {
return this.addHolder(name, String.valueOf(value));
}
private Objects.ToStringHelper addHolder(String name, @Nullable Object value) {
Objects.ToStringHelper.ValueHolder valueHolder = this.addHolder();
valueHolder.value = value;
valueHolder.name = (String)Preconditions.checkNotNull(name);
return this;
}
private Objects.ToStringHelper.ValueHolder addHolder() {
Objects.ToStringHelper.ValueHolder valueHolder = new Objects.ToStringHelper.ValueHolder();
this.holderTail = this.holderTail.next = valueHolder;
return valueHolder;
}
ToStringBuilder内部使用一个StringBuffer,默认大小为512.另外一个style设定来toString的样式,每次执行append都跳转到对应的ToStringStyle的append函数实现上,并将当前的StringBuffer带过去(StringBuffer来存储数据,ToStringStyle来保证格式):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ToStringBuilder implements Builder<String> {
private static volatile ToStringStyle defaultStyle;
private final StringBuffer buffer;
private final Object object;
private final ToStringStyle style;
static {
defaultStyle = ToStringStyle.DEFAULT_STYLE;
}
public ToStringBuilder append(String fieldName, int value) {
this.style.append(this.buffer, fieldName, value);
return this;
}
ToStringStyle的append实现,每个append都很简单,就是往StringBuffer中append数据:
1
2
3
4
5
6
7
8
9
public void append(StringBuffer buffer, String fieldName, int value) {
this.appendFieldStart(buffer, fieldName);
this.appendDetail(buffer, fieldName, value);
this.appendFieldEnd(buffer, fieldName);
}
protected void appendDetail(StringBuffer buffer, String fieldName, int value) {
buffer.append(value);
}
6.ReflectionToStringBuilder
ReflectionToStringBuilder实际上是继承自ToStringBuilder,每次都构建一个ReflectionToStringBuilder对象,toString的实现是使用反射完成,首先反射当前类的所以属性,再逐级往上获取父类并反射获取其属性,每拿到一个属性,调用一次ToStringBuilder的append方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public static <T> String toString(
final T object, final ToStringStyle style, final boolean outputTransients,
final boolean outputStatics, final Class<? super T> reflectUpToClass) {
return new ReflectionToStringBuilder(object, style, null, reflectUpToClass, outputTransients, outputStatics)
.toString();
}
public String toString() {
if (this.getObject() == null) {
return this.getStyle().getNullText();
}
Class<?> clazz = this.getObject().getClass();
this.appendFieldsIn(clazz);
while (clazz.getSuperclass() != null && clazz != this.getUpToClass()) {
clazz = clazz.getSuperclass();
this.appendFieldsIn(clazz);
}
return super.toString();
}
protected void appendFieldsIn(final Class<?> clazz) {
if (clazz.isArray()) {
this.reflectionAppendArray(this.getObject());
return;
}
final Field[] fields = clazz.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);
for (final Field field : fields) {
final String fieldName = field.getName();
if (this.accept(field)) {
try {
// Warning: Field.get(Object) creates wrappers objects
// for primitive types.
final Object fieldValue = this.getValue(field);
this.append(fieldName, fieldValue);
} catch (final IllegalAccessException ex) {
//this can't happen. Would get a Security exception
// instead
//throw a runtime exception in case the impossible
// happens.
throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage());
}
}
}
}
2.beanCopy
beanCopy由很多种方式,Spring和Apache都有实现版本,这里测试各个版本的性能.包括:
1.原始的get/set
2.Spring的BeanUtils
3.cglib的BeanCopier
4.apache的BeanUtils
5.apache的PropertyUtils
原因是前段时间项目上很简单的接口时间很慢,最后同事跟到的问题是使用Spring的beanCopy方式,因为接口涉及上百个bean的属性拷贝,因此比较明显的反应了各种beancopy实现方式的差异;我们最常用的应该是Spring版本的BeanUtils。apache的BeanUtils的设计有点反人类(第一个参数是dest,第二个参数是source)。
各个包的版本:
1.Spring : 3.2.1
2.cglib : 3.1
3.commons-beanutils : 1.9.2
各种方式的实现很简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class GetSetCopy {
public static HotelVo copy(HotelEntity entity) {
HotelVo vo = new HotelVo();
vo.setId(entity.getId());
vo.setName(entity.getName());
vo.setAddress(entity.getAddress());
vo.setPhone(entity.getPhone());
vo.setCreateTime(entity.getCreateTime());
vo.setUpdateTime(entity.getUpdateTime());
vo.setPrice(entity.getPrice());
vo.setKp(entity.getKp());
return vo;
}
}
public class ApacheBeanUtilsCopy {
public static HotelVo copy(HotelEntity entity) throws Exception {
HotelVo vo = new HotelVo();
BeanUtils.copyProperties(vo, entity);
return vo;
}
}
public class ApachePropertyUtilsCopy {
public static HotelVo copy(HotelEntity entity) throws Exception {
HotelVo vo = new HotelVo();
PropertyUtils.copyProperties(vo, entity);
return vo;
}
}
public class CglibBeanCopy {
public static HotelVo copy(HotelEntity entity) {
HotelVo vo = new HotelVo();
BeanCopier copier = BeanCopier.create(entity.getClass(), HotelVo.class, false);
copier.copy(entity, vo, null);
return vo;
}
}
public class SpringBeanUtilsCopy {
public static HotelVo copy(HotelEntity entity) {
HotelVo vo = new HotelVo();
BeanUtils.copyProperties(entity, vo);
return vo;
}
}
测试的一个结果,每个copy运行10000次(各个Copy的第一次copy都很慢,跑10000次基本可以抹平这些差异):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GetSetCopy Min(微秒)=1.466
GetSetCopy Max(微秒)=804.236
GetSetCopy Avg(微秒)=1.7923529
Spring-BeanUtils Min(微秒)=3.339
Spring-BeanUtils Max(微秒)=99795.719
Spring-BeanUtils Avg(微秒)=28.2915534
Apache-BeanUtils Min(微秒)=9.812
Apache-BeanUtils Max(微秒)=92416.696
Apache-BeanUtils Avg(微秒)=79.0868131
Apache-PropertyUtils Min(微秒)=7.083
Apache-PropertyUtils Max(微秒)=6405.457
Apache-PropertyUtils Avg(微秒)=22.114272
Cglib-BeanCopy Min(微秒)=7.707
Cglib-BeanCopy Max(微秒)=156932.173
Cglib-BeanCopy Avg(微秒)=33.135989
copy策略
平均(微秒)
最大(微秒)
最小(微秒)
GetSet
1.7923529
804.236
1.466
Spring-BeanUtils
28.2915534
99795.719
3.339
Apache-BeanUtils
79.0868131
6405.457
9.812
Apache-PropertyUtils
22.114272
9503.836
7.083
Cglib-BeanCopy
33.135989
156932.173
7.707
原始的get/set是最快的,而且是快一个数量级。Apache-BeanUtils是最慢的。Spring-BeanUtils的性能还是比其他几个强一些(这是个错觉,其实Cglib-BeanCopy比Spring-BeanUtils快,见下面的分析)。
2.1 实现原理
简单分析一下各个bean copy方式的实现原理。
没什么好说的,最原始。
Spring的BeanUtils可以使用String [] ignoreProperties 指定忽略某些属性的复制,当我们需要手动处理一些特定属性的时候挺有用。
copyProperties的原理很我们想想的一样:
1.获取target类的属性列表;
2.循环遍历target属性,对每个属性,如果由write方法并且不在ignoreProperties里面,调用soure对应属性的read方法获取属性值,再调用target的write方法;
细节:
1.缓存
获取target类的属性列表的时候有缓存,见CachedIntrospectionResults类。这也再一定成都上解释了为什么第一次调用会慢一些。
2.Accessible
注意,source的read和target的write方法可以不是Accessible的,即可以是private等修饰的方法;
3.浅拷贝
注意其拷贝是浅拷贝。
大致的代码(精简了一些):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static void copyProperties(Object source, Object target, Class<?> editable, String[] ignoreProperties) throws BeansException {
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); //获取target的属性描述符,有缓存
List ignoreList = ignoreProperties != null?Arrays.asList(ignoreProperties):null;
PropertyDescriptor[] arr$ = targetPds;
int len$ = targetPds.length;
for(int i$ = 0; i$ < len$; ++i$) {
PropertyDescriptor targetPd = arr$[i$];
if(targetPd.getWriteMethod() != null && (ignoreProperties == null || !ignoreList.contains(targetPd.getName()))) { //有write方法并且不在ignoreProperties中
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if(sourcePd != null && sourcePd.getReadMethod() != null) {
try {
Method ex = sourcePd.getReadMethod(); //获取source的read方法
if(!Modifier.isPublic(ex.getDeclaringClass().getModifiers())) {
ex.setAccessible(true);
}
Object value = ex.invoke(source, new Object[0]); //调用source的read方法获取属性对应的value
Method writeMethod = targetPd.getWriteMethod();
if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, new Object[]{value}); //调用target的write方法
} catch (Throwable var15) {
throw new FatalBeanException("Could not copy properties from source to target", var15);
}
}
}
}
}
BeanCopier是CGLIB包中的一个工具类,使用基于动态代理机制的方式进行Bean Copy。因为使用生成的子类中的方法进行属性复制,所以会比使用反射机制的复制在效率上会高出很多。前面一直想写一片代理方面的Blog,一直没完成,尽量他会补上,到时候会分析CGLIB的原理。
我们这里的测试结果显示BeanCopier还不如Spring的BeanUtils,原因说BeanCopier.create的创建效率会比较低,所以在使用过程中建议将 BeanCopier.create 创建的对象声明为 static,避免使用中每次都去创建新的 BeanCopier 对象。我测试中发现,如果我们只显示的调用一次create方法,发现起速度明显快于Spring的BeanUtils(起最小值甚至小于get/set方式的最小值):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GetSetCopy Min(微秒)=1.46
GetSetCopy Max(微秒)=2149.855
GetSetCopy Avg(微秒)=2.0715596
Spring-BeanUtils Min(微秒)=3.669
Spring-BeanUtils Max(微秒)=101262.127
Spring-BeanUtils Avg(微秒)=29.613658
Apache-BeanUtils Min(微秒)=10.174
Apache-BeanUtils Max(微秒)=80342.749
Apache-BeanUtils Avg(微秒)=82.383995
Apache-PropertyUtils Min(微秒)=5.468
Apache-PropertyUtils Max(微秒)=17185.561
Apache-PropertyUtils Avg(微秒)=26.5104518
Cglib-BeanCopy Min(微秒)=0.745
Cglib-BeanCopy Max(微秒)=130832.581
Cglib-BeanCopy Avg(微秒)=14.6252958
主要代码大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
BeanUtilsBean.getInstance().copyProperties(dest, orig);
}
public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
int i;
String name;
Object e;
if(orig instanceof DynaBean) { //DynaBean ??
DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties();
for(i = 0; i < origDescriptors.length; ++i) {
name = origDescriptors[i].getName();
if(this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {
e = ((DynaBean)orig).get(name);
this.copyProperty(dest, name, e);
}
}
} else if(orig instanceof Map) { //原始数据是一个Map,即支持从一个Map到target属性的拷贝
Map var8 = (Map)orig;
Iterator var10 = var8.entrySet().iterator();
while(var10.hasNext()) {
Entry var11 = (Entry)var10.next();
String var12 = (String)var11.getKey();
if(this.getPropertyUtils().isWriteable(dest, var12)) {
this.copyProperty(dest, var12, var11.getValue());
}
}
} else {
PropertyDescriptor[] var9 = this.getPropertyUtils().getPropertyDescriptors(orig); //获取target的属性描述符,有缓存
for(i = 0; i < var9.length; ++i) {
name = var9[i].getName();
//要求source的read方法是Accessible,target的write方法是Accessible
if(!"class".equals(name) && this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {
try {
e = this.getPropertyUtils().getSimpleProperty(orig, name); //获取source对应属性的value
this.copyProperty(dest, name, e); //设置target对应属性
} catch (NoSuchMethodException var7) {
;
}
}
}
}
}
细节:
1.BeanUtilsBean实例
BeanUtilsBean需要第一次new实例,因此第一次调用会慢一些。
2.缓存
获取target类的属性列表的时候有缓存,见BeanIntrospectionData类。这也再一定成都上解释了为什么第一次调用会慢一些。
3.Accessible
注意,source的read和target的write方法要求都是Accessible的;
4.浅拷贝
其拷贝是浅拷贝。
5.apache的PropertyUtils
PropertyUtils的实现和BeanUtils的实现基本一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
PropertyUtilsBean.getInstance().copyProperties(dest, orig);
}
public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
int i;
String name;
Object e;
if(orig instanceof DynaBean) {
DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties();
for(i = 0; i < origDescriptors.length; ++i) {
name = origDescriptors[i].getName();
if(this.isReadable(orig, name) && this.isWriteable(dest, name)) {
try {
e = ((DynaBean)orig).get(name);
if(dest instanceof DynaBean) {
((DynaBean)dest).set(name, e);
} else {
this.setSimpleProperty(dest, name, e);
}
} catch (NoSuchMethodException var9) {
if(this.log.isDebugEnabled()) {
this.log.debug("Error writing to \'" + name + "\' on class \'" + dest.getClass() + "\'", var9);
}
}
}
}
} else if(orig instanceof Map) { //sorcce是一个Map
Iterator var10 = ((Map)orig).entrySet().iterator();
while(true) {
Entry var12;
do {
if(!var10.hasNext()) {
return;
}
var12 = (Entry)var10.next();
name = (String)var12.getKey();
} while(!this.isWriteable(dest, name)); //要求target对应属性是Accessible的
try {
if(dest instanceof DynaBean) {
((DynaBean)dest).set(name, var12.getValue());
} else {
this.setSimpleProperty(dest, name, var12.getValue());
}
} catch (NoSuchMethodException var8) {
if(this.log.isDebugEnabled()) {
this.log.debug("Error writing to \'" + name + "\' on class \'" + dest.getClass() + "\'", var8);
}
}
}
} else {
PropertyDescriptor[] var11 = this.getPropertyDescriptors(orig); //获取target对象属性的描述符,有缓存
for(i = 0; i < var11.length; ++i) {
name = var11[i].getName();
if(this.isReadable(orig, name) && this.isWriteable(dest, name)) { //要求source的read方法是Accessible,target的write方法是Accessible
try {
e = this.getSimpleProperty(orig, name); //获取source对应属性的值
if(dest instanceof DynaBean) {
((DynaBean)dest).set(name, e);
} else {
this.setSimpleProperty(dest, name, e); //设置target对应属性的值
}
} catch (NoSuchMethodException var7) {
if(this.log.isDebugEnabled()) {
this.log.debug("Error writing to \'" + name + "\' on class \'" + dest.getClass() + "\'", var7);
}
}
}
}
}
}
3.问题
碰到一个小问题,因为涉及到同一个方法的多次调用,发现第一次调用的时间远大于后面的调用:
1
2
3
4
5
6
7
8
9
10
GetSetCopy-1(微秒) : 1114.744
GetSetCopy-2(微秒) : 2.087
GetSetCopy-3(微秒) : 1.684
GetSetCopy-4(微秒) : 1.694
GetSetCopy-5(微秒) : 1.828
GetSetCopy-6(微秒) : 1.742
GetSetCopy-7(微秒) : 1.705
GetSetCopy-8(微秒) : 1.68
GetSetCopy-9(微秒) : 1.669
GetSetCopy-10(微秒) : 1.693
怀疑:
1.GetSetCopy类的初始化;
2.拷贝的目标对象类(这里即HotelVo)初始化;
3.方法调用的缓存;
尝试发现如果调用前先显示的new一个GetSetCopy对象和一个HotelVo对象,可以将第一次的时间下降到50微妙左右。如果再外面显示的调用一次copy方法,则循环内的第一次copy会降到和其他次的拷贝时间一样。
3.1 原理
待补充,会涉及到JVM一些JIT的优化等问题,自己也很期待到底什么原因…