1. 性能对比测试
基于JMH(1.32版本)进行性能测试(机器:MacBook Pro 13,16GB内存,M1芯片;JDK版本:zulu-8,1.8.0_302),分别比较原生的getXXX(),Apache BeanUtils的getProperty(),基于原生反射机制的field.get()和unsafe的getObject(),测试代码如下:
packagecom.xycode.paramcheck.service.domain; importlombok.Data; importlombok.EqualsAndHashCode; importlombok.ToString; /*** @author: lianguang* @date: 2021/8/15*/callSuper=true) (callSuper=true) (publicclassDomainextendsBaseDomain { privateIntegerage; privateStringname; publicDomain(Longid, Integerage, Stringname) { super(id); this.age=age; this.name=name; } }
packagecom.xycode.paramcheck.benchmark; importcom.xycode.paramcheck.service.domain.Domain; importorg.apache.commons.beanutils.BeanUtils; importorg.junit.jupiter.api.BeforeEach; importorg.junit.jupiter.api.Test; importorg.openjdk.jmh.annotations.*; importorg.openjdk.jmh.runner.Runner; importorg.openjdk.jmh.runner.RunnerException; importorg.openjdk.jmh.runner.options.Options; importorg.openjdk.jmh.runner.options.OptionsBuilder; importsun.misc.Unsafe; importjava.lang.reflect.Field; importjava.util.Map; importjava.util.concurrent.ConcurrentHashMap; importjava.util.concurrent.TimeUnit; /*** @author: lianguang* @date: 2021/11/3*/iterations=3) (iterations=5, time=5) (2) (TimeUnit.MICROSECONDS) (Scope.Benchmark) (publicclassBeanGetterBenchMark { publicUnsafeunsafe; publicUnsafegetUnsafe() { try { Fieldfield=Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (NoSuchFieldException|IllegalAccessExceptione) { e.printStackTrace(); } returnnull; } privateDomainsource=newDomain(1000L, 100, "source"); publicvoidinit() throwsException { unsafe=getUnsafe(); } Mode.Throughput) (publicvoidtestApacheBeanUtilsGetProperty() throwsException { BeanUtils.getProperty(source, "id"); } Mode.Throughput) (publicvoidgetPropertyReflect() throwsException { Fieldafield=Domain.class.getSuperclass().getDeclaredField("id"); afield.setAccessible(true); afield.get(source); } Mode.Throughput) (publicvoidgetPropertyUnsafe() throwsException { longaFieldOffset=unsafe.objectFieldOffset(Domain.class.getSuperclass().getDeclaredField("id")); unsafe.getObject(source, aFieldOffset); } Mode.Throughput) (publicvoidtestNativeGetter() { source.getId(); } publicstaticvoidmain(String[] args) throwsRunnerException { Optionsoptions=newOptionsBuilder() .include(BeanGetterBenchMark.class.getSimpleName()) .build(); newRunner(options).run(); }
测试结果:
Benchmark Mode Cnt Score Error Units BeanGetterBenchMark.getPropertyReflect thrpt 10 4.140 ± 0.019 ops/us BeanGetterBenchMark.getPropertyUnsafe thrpt 10 2.914 ± 0.126 ops/us BeanGetterBenchMark.testApacheBeanUtilsGetProperty thrpt 10 0.464 ± 0.014 ops/us BeanGetterBenchMark.testNativeGetter thrpt 10 1794.462 ± 14.348 ops/us
可见,Apache的BeanUtils的性能最差,原生反射的性能稍好一些,意外的是,unsafe方式性能低于原生反射方式。原生get的方式性能最好,是反射方式与unsafe方式性能的数百倍,有没有什么方法能够提升反射或unsafe的性能呢?稍加分析,反射方式和unsafe方式的过程大致都分为两个步骤,第一个步骤先获取属性的field,第二个步骤则根据field去获取属性值,又因为一个类的某个属性的field是相对不变的,由此可得一种提升性能的方式是将field缓存起来,当需要field时直接从缓存中获取,经过测试(在16GB,M1芯片的MacBook Pro13机器上),Java中Map的get性能大概在 200~300 ops/us,因此是有提升性能的空间的。
模拟缓存情况下的性能,测试代码如下:
packagecom.xycode.paramcheck.benchmark; importcom.xycode.paramcheck.service.domain.Domain; importorg.apache.commons.beanutils.BeanUtils; importorg.junit.jupiter.api.BeforeEach; importorg.junit.jupiter.api.Test; importorg.openjdk.jmh.annotations.*; importorg.openjdk.jmh.runner.Runner; importorg.openjdk.jmh.runner.RunnerException; importorg.openjdk.jmh.runner.options.Options; importorg.openjdk.jmh.runner.options.OptionsBuilder; importsun.misc.Unsafe; importjava.lang.reflect.Field; importjava.util.Map; importjava.util.concurrent.ConcurrentHashMap; importjava.util.concurrent.TimeUnit; /*** @author: lianguang* @date: 2021/11/3*/iterations=3) (iterations=5, time=5) (2) (TimeUnit.MICROSECONDS) (Scope.Benchmark) (publicclassBeanGetterBenchMark { publicUnsafeunsafe; publicUnsafegetUnsafe() { try { Fieldfield=Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (NoSuchFieldException|IllegalAccessExceptione) { e.printStackTrace(); } returnnull; } privateDomainsource=newDomain(1000L, 100, "source"); privateMap<String, Long>offsetMap=newConcurrentHashMap<>(); privateMap<String, Field>fieldMap=newConcurrentHashMap<>(); publicvoidinit() throwsException { unsafe=getUnsafe(); //模拟缓存优化Fieldfield=Domain.class.getSuperclass().getDeclaredField("id"); field.setAccessible(true); fieldMap.put("id",field); offsetMap.put("id",unsafe.objectFieldOffset(field)); } Mode.Throughput) (publicvoidtestApacheBeanUtilsGetProperty() throwsException { BeanUtils.getProperty(source, "id"); } Mode.Throughput) (publicvoidgetPropertyReflectWithCache() throwsException { fieldMap.get("id").get(source); } Mode.Throughput) (publicvoidgetPropertyUnsafeWithCache() { unsafe.getObject(source, offsetMap.get("id")); } Mode.Throughput) (publicvoidtestNativeGetter() { source.getId(); } publicstaticvoidmain(String[] args) throwsRunnerException { Optionsoptions=newOptionsBuilder() .include(BeanGetterBenchMark.class.getSimpleName()) .build(); newRunner(options).run(); } }
测试结果如下:
Benchmark(考虑到ConcurrentHashMap的开销) Mode Cnt Score Error Units BeanGetterBenchMark.getPropertyReflectWithCache thrpt 10 175.756 ± 3.043 ops/us BeanGetterBenchMark.getPropertyUnsafeWithCache thrpt 10 258.211 ± 3.122 ops/us BeanGetterBenchMark.testApacheBeanUtilsGetProperty thrpt 10 0.435 ± 0.056 ops/us BeanGetterBenchMark.testNativeGetter thrpt 10 1771.469 ± 30.345 ops/us
可以看出,在命中缓存的情况下,反射方式与unsafe方式的性能得到了极大地提升,有一点比较有意思,unsafe方式在缓存情况下的性能超过了反射方式。因为实际上unsafe.getObject()是一个Native方法,其性能是相当优秀的,在fieldOffset是现成的基础上,unsafe.getObject()性能与原生的getXXX相当,经过测试,它们之间的性能对比如下:(甚至有时的测试结果高于原生的getXXX)
BeanUtilsBenchMark.testNativeGetter 1784.362 ± 10.617 ops/us BeanUtilsBenchMark.testUnsafeGetter 1802.107 ± 4.319 ops/us
2. BeanOperationUtils的实现
通过以上的分析,加了缓存后能够极大地提升反射方式与unsafe方式的get的性能,并且在缓存下的unsafe方式性能反超原生反射方式,由此这里实现了一个工具类,代码如下所示:
packagecom.xycode.paramcheck.utils; importjava.util.Objects; /*** 缓存key** @author: lianguang* @date: 2021/11/3*/publicclassFieldCacheKey { /*** 类对象*/privateClass<?>clazz; /*** 类的属性名*/privateStringpropertyName; publicFieldCacheKey(Class<?>clazz, StringpropertyName) { this.clazz=clazz; this.propertyName=propertyName; } publicClass<?>getClazz() { returnclazz; } publicvoidsetClazz(Class<?>clazz) { this.clazz=clazz; } publicStringgetPropertyName() { returnpropertyName; } publicvoidsetPropertyName(StringpropertyName) { this.propertyName=propertyName; } publicbooleanequals(Objecto) { if (this==o) returntrue; if (o==null||getClass() !=o.getClass()) returnfalse; FieldCacheKeythat= (FieldCacheKey) o; returnObjects.equals(clazz, that.clazz) &&Objects.equals(propertyName, that.propertyName); } publicinthashCode() { returnObjects.hash(clazz, propertyName); } }
packagecom.xycode.paramcheck.utils; importsun.misc.Unsafe; importjava.lang.reflect.Field; importjava.util.Map; importjava.util.Objects; importjava.util.concurrent.ConcurrentHashMap; /*** bean的相关操作** @author: lianguang* @date: 2021/11/3*/publicclassBeanOperationUtils { privatestaticfinalUnsafeunsafe; privatestaticfinalMap<FieldCacheKey, Long>fieldOffsetCache=newConcurrentHashMap<>(1024); privatestaticfinalMap<FieldCacheKey, Field>fieldCache=newConcurrentHashMap<>(1024); static { unsafe=getUnsafe(); } privatestaticUnsafegetUnsafe() { try { Fieldfield=Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (NoSuchFieldException|IllegalAccessExceptione) { e.printStackTrace(); } returnnull; } /*** 因为可能有嵌套属性的存在, 需要递归向上获取field*/privatestaticvoidcollectField(finalClass<?>clazz, finalStringname, finalField[] fields) throwsException { if (Objects.equals(Object.class, clazz) ||Objects.nonNull(fields[0])) { return; } try { fields[0] =clazz.getDeclaredField(name); } catch (Exceptionignored) { } collectField(clazz.getSuperclass(), name, fields); } /*** 反射方式获取属性值, 性能远高于Apache的BeanUtils.getProperty(), 低于命中缓存情况下的unsafe方式** @param clazz 类* @param name 属性名*/privatestaticFieldgetFieldWithCache(finalClass<?>clazz, finalStringname) throwsException { Field[] fields=newField[1]; FieldCacheKeykey=newFieldCacheKey(clazz, name); fields[0] =fieldCache.get(key); if (Objects.isNull(fields[0])) { collectField(clazz, name, fields); if (Objects.nonNull(fields[0])) { fields[0].setAccessible(true); //设置缓存fieldCache.put(key, fields[0]); } } returnfields[0]; } /*** 反射方式获取属性值, 性能远高于Apache的BeanUtils.getProperty(), 低于命中缓存情况下的unsafe方式** @param clazz 类* @param name 属性名*/privatestaticFieldgetField(finalClass<?>clazz, finalStringname) throwsException { Field[] fields=newField[1]; fields[0] =null; collectField(clazz, name, fields); if (Objects.nonNull(fields[0])) { fields[0].setAccessible(true); } returnfields[0]; } /*** 检查对象中是否包含属性定义, 在进行getProperty()操作时, 需要先检查一下** @param clazz 类* @param name 属性名*/publicstaticbooleancontainsProperty(Class<?>clazz, Stringname) throwsException { if (Objects.isNull(clazz) ||Objects.isNull(name)) { returnfalse; } Fieldfield=getFieldWithCache(clazz, name); if (Objects.nonNull(field)) { returntrue; } else { returnfalse; } } /*** 反射方式获取属性值(带缓存FieldCache)* notice: 在进行getProperty()操作时, 需要调用containsProperty先检查一下, 若存在,才调用getProperty()** @param bean 对象* @param name 属性名*/publicstaticObjectgetPropertyWithFieldCache(Objectbean, Stringname) throwsException { if (Objects.isNull(bean) ||Objects.isNull(name)) { returnnull; } Fieldfield=getFieldWithCache(bean.getClass(), name); if (Objects.nonNull(field)) { returnfield.get(bean); } else { returnnull; } } /*** unsafe方式获取属性值(带fieldOffsetCache)** @param bean 对象* @param name 属性名* notice 在进行getProperty()操作时, 需要调用containsProperty先检查一下, 若存在,才调用getProperty()*/publicstaticObjectgetPropertyWithFieldOffsetCache(Objectbean, Stringname) throwsException { if (Objects.isNull(bean) ||Objects.isNull(name)) { returnnull; } FieldCacheKeykey=newFieldCacheKey(bean.getClass(), name); Longoffset=fieldOffsetCache.get(key); if (Objects.isNull(offset)) { //已经有fieldOffsetCache了,不用重复缓存了Fieldfield=getField(bean.getClass(), name); //设置缓存fieldOffsetCache.put(key, unsafe.objectFieldOffset(field)); returnfield.get(bean); } else { /*** unsafe.getObject()是native方法,性能与原生的object.getXXX()相差无几, 基于jmh的性能测试如下:* BeanUtilsBenchMark.testNativeGetter 1784.362 ± 10.617 ops/us* BeanUtilsBenchMark.testUnsafeGetter 1802.107 ± 4.319 ops/us*/returnunsafe.getObject(bean, offset); } } }
3. BeanOperationUtils的性能测试
BeanOperationUtils的性能测试代码如下:
packagecom.xycode.paramcheck.benchmark; importcom.xycode.paramcheck.service.domain.Domain; importcom.xycode.paramcheck.utils.BeanOperationUtils; importorg.openjdk.jmh.annotations.*; importorg.openjdk.jmh.runner.Runner; importorg.openjdk.jmh.runner.RunnerException; importorg.openjdk.jmh.runner.options.Options; importorg.openjdk.jmh.runner.options.OptionsBuilder; importjava.util.concurrent.TimeUnit; /*** @author: lianguang* @date: 2021/11/3*/iterations=3) (iterations=5, time=5) (2) (TimeUnit.MICROSECONDS) (Scope.Benchmark) (publicclassBeanOperationBenchmark { privateDomainsource=newDomain(1000L, 100, "source"); publicvoidinit() throwsException { } Mode.Throughput) (publicvoidtestReflectGetterWithCache() throwsException { //反射方式获取属性值BeanOperationUtils.getPropertyWithFieldCache(source, "id"); } Mode.Throughput) (publicvoidtestUnsafeGetterWithCache() throwsException { //unsafe方式获取属性值BeanOperationUtils.getPropertyWithFieldOffsetCache(source, "id"); } publicstaticvoidmain(String[] args) throwsRunnerException { Optionsoptions=newOptionsBuilder() .include(BeanOperationBenchmark.class.getSimpleName()) .build(); newRunner(options).run(); } }
测试结果如下:
Benchmark Mode Cnt Score Error Units BeanOperationBenchmark.testReflectGetterWithCache thrpt 10 86.110 ± 2.601 ops/us BeanOperationBenchmark.testUnsafeGetterWithCache thrpt 10 137.352 ± 2.046 ops/us
可见,BeanOperationUtils的性能远高于Apache BeanUtils的getProperty(),其中unsafe方式(命中fieldOffset缓存)的性能高于反射方式(命中field缓存)。