最近プロジェクトコードをスキャンしたところ、BeanUtils.copyPropertiesに小さな問題が見つかりました。当時のコメントは次のとおりでした。今は対処していません。
この手がかりによると、好きな人を探し始めました。探しました。彼女の何千人ものバイドゥスのために、突然その人がそこにいたことを振り返りました。薄暗い場所で、あぐらをかいて、生きている神のように喫煙して、私と彼の間の話は始まったばかり
です。次は彼の経験です。開発プロセスで、データベースでクエリされたPOオブジェクトの20を超える属性を変換するためにsetメソッドを呼び出す小さなパートナーが、次のようにVoオブジェクトにコピーされるのを見ました。
PoクラスとVoクラスのほとんどのフィールドは同じであることがわかります。繰り返して時間のかかる操作を行うために、setメソッドを1つずつ呼び出します。オブジェクトの属性が多すぎるため、この操作は非常にエラーが発生しやすく、1つまたは2つが見落とされる可能性があり、肉眼で検出するのは困難です。
このような操作は簡単に思い浮かび、振り返ることで解決できます。実際、このようなユニバーサル関数はBeanUtilsツールクラスで処理できます。
そこで、この小さなパートナーがApache BeanUtils.copyPropertiesを使用してプロパティをコピーすることを提案しました。これにより、プログラムに穴が開いてしまいました。
Aliコード仕様
Aliコードスキャンプラグインを有効にしたときに、Apache BeanUtils.copyPropertiesを使用してプロパティをコピーすると、非常に深刻な警告が表示されます。Apache BeanUtilsのパフォーマンスが低いため、代わりにSpringBeanUtilsまたはCglibBeanCopierを使用できます。
このような警告を見るのは少し不快です。有名なApacheによって提供されたパッケージには実際にパフォーマンスの問題があるため、Aliは深刻な警告を出しました。
では、このパフォーマンスの問題はどれほど深刻ですか?結局のところ、私たちのアプリケーションシナリオでは、パフォーマンスの低下がごくわずかであるが、それが大きな利便性をもたらす可能性がある場合でも、それは許容範囲内です。
この質問で。それを検証するために実験をしてみましょう。
特定のテストメソッドに興味がない場合は、スキップして結果を直接確認できます〜
テストメソッドのインターフェイスと実装の定義
まず、テストの便宜のために、インターフェースを定義し、いくつかの実装を統合しましょう。
public interface PropertiesCopier {
void copyProperties(Object source, Object target) throws Exception;
}
public class CglibBeanCopierPropertiesCopier implements PropertiesCopier {
@Override
public void copyProperties(Object source, Object target) throws Exception {
BeanCopier copier = BeanCopier.create(source.getClass(), target.getClass(), false);
copier.copy(source, target, null);
}
}
// 全局静态 BeanCopier,避免每次都生成新的对象
public class StaticCglibBeanCopierPropertiesCopier implements PropertiesCopier {
private static BeanCopier copier = BeanCopier.create(Account.class, Account.class, false);
@Override
public void copyProperties(Object source, Object target) throws Exception {
copier.copy(source, target, null);
}
}
public class SpringBeanUtilsPropertiesCopier implements PropertiesCopier {
@Override
public void copyProperties(Object source, Object target) throws Exception {
org.springframework.beans.BeanUtils.copyProperties(source, target);
}
}
public class CommonsBeanUtilsPropertiesCopier implements PropertiesCopier {
@Override
public void copyProperties(Object source, Object target) throws Exception {
org.apache.commons.beanutils.BeanUtils.copyProperties(target, source);
}
}
public class CommonsPropertyUtilsPropertiesCopier implements PropertiesCopier {
@Override
public void copyProperties(Object source, Object target) throws Exception {
org.apache.commons.beanutils.PropertyUtils.copyProperties(target, source);
}
}
単体テスト
次に、パラメーター化された単体テストを記述します。
@RunWith(Parameterized.class)
public class PropertiesCopierTest {
@Parameterized.Parameter(0)
public PropertiesCopier propertiesCopier;
// 测试次数
private static List<Integer> testTimes = Arrays.asList(100, 1000, 10_000, 100_000, 1_000_000);
// 测试结果以 markdown 表格的形式输出
private static StringBuilder resultBuilder = new StringBuilder("|实现|100|1,000|10,000|100,000|1,000,000|\n").append("|----|----|----|----|----|----|\n");
@Parameterized.Parameters
public static Collection<Object[]> data() {
Collection<Object[]> params = new ArrayList<>();
params.add(new Object[]{
new StaticCglibBeanCopierPropertiesCopier()});
params.add(new Object[]{
new CglibBeanCopierPropertiesCopier()});
params.add(new Object[]{
new SpringBeanUtilsPropertiesCopier()});
params.add(new Object[]{
new CommonsPropertyUtilsPropertiesCopier()});
params.add(new Object[]{
new CommonsBeanUtilsPropertiesCopier()});
return params;
}
@Before
public void setUp() throws Exception {
String name = propertiesCopier.getClass().getSimpleName().replace("PropertiesCopier", "");
resultBuilder.append("|").append(name).append("|");
}
@Test
public void copyProperties() throws Exception {
Account source = new Account(1, "test1", 30D);
Account target = new Account();
// 预热一次
propertiesCopier.copyProperties(source, target);
for (Integer time : testTimes) {
long start = System.nanoTime();
for (int i = 0; i < time; i++) {
propertiesCopier.copyProperties(source, target);
}
resultBuilder.append((System.nanoTime() - start) / 1_000_000D).append("|");
}
resultBuilder.append("\n");
}
@AfterClass
public static void tearDown() throws Exception {
System.out.println("测试结果:");
System.out.println(resultBuilder);
}
}
テスト結果
ミリ秒単位の時間単位
実装
結果は、CglibのBeanCopierのコピー速度が最速であることを示しています。これは、100万コピーであっても、10ミリ秒しかかかりません。比較すると、最悪の場合はCommonsパッケージのBeanUtils.copyPropertiesメソッドです。100回のコピーテストは、最高のパフォーマンスを発揮するCglibとは400倍異なります。何百万ものコピーには2800倍のパフォーマンスの違いがあります!
結果は本当に衝撃的です。
しかし、なぜそれらはそれほど異なるのですか?
原因分析
ソースコードを見ると、CommonsBeanUtilsには主に次の時間のかかる側面があることがわかります。
- 大量のログデバッグ情報を出力する
- 重複オブジェクトタイプチェック
- 型変換
public void copyProperties(final Object dest, final Object orig)
throws IllegalAccessException, InvocationTargetException {
// 类型检查
if (orig instanceof DynaBean) {
...
} else if (orig instanceof Map) {
...
} else {
final PropertyDescriptor[] origDescriptors = ...
for (PropertyDescriptor origDescriptor : origDescriptors) {
...
// 这里每个属性都调一次 copyProperty
copyProperty(dest, name, value);
}
}
}
public void copyProperty(final Object bean, String name, Object value)
throws IllegalAccessException, InvocationTargetException {
...
// 这里又进行一次类型检查
if (target instanceof DynaBean) {
...
}
...
// 需要将属性转换为目标类型
value = convertForCopy(value, type);
...
}
// 而这个 convert 方法在日志级别为 debug 的时候有很多的字符串拼接
public <T> T convert(final Class<T> type, Object value) {
if (log().isDebugEnabled()) {
log().debug("Converting" + (value == null ? "" : " '" + toString(sourceType) + "'") + " value '" + value + "' to type '" + toString(targetType) + "'");
}
...
if (targetType.equals(String.class)) {
return targetType.cast(convertToString(value));
} else if (targetType.equals(sourceType)) {
if (log().isDebugEnabled()) {
log().debug("No conversion required, value is already a " + toString(targetType));
}
return targetType.cast(value);
} else {
// 这个 convertToType 方法里也需要做类型检查
final Object result = convertToType(targetType, value);
if (log().isDebugEnabled()) {
log().debug("Converted to " + toString(targetType) + " value '" + result + "'");
}
return targetType.cast(result);
}
}
特定のパフォーマンスとソースコードの分析については、次の記事を参照してください。
- いくつかのcopyPropertiesツールのパフォーマンス比較:www.jianshu.com/p/bcbacab3b ...
- CGLIBでのBeanCopierのソースコード実装:www.jianshu.com/p/f8b892e08 .. ..
- Java Bean Copyフレームワークのパフォーマンス比較:yq.aliyun.com/articles/39 ...
もう1つ
パフォーマンスの問題に加えて、CommonsBeanUtilsを使用するときに特別な注意が必要な他の落とし穴があります!
パッケージのデフォルト値
属性をコピーする場合、CommonsBeanUtilsはデフォルトで元のパッケージングクラスにデフォルト値を割り当てませんが、下位バージョン(1.8.0以下)を使用する場合、クラスにDate型属性があり、ソースオブジェクトの属性が値がnullの場合、例外が発生します:
org.apache.commons.beanutils.ConversionException:「
Date 」に値が指定されていませんこの問題の解決策は、DateConverterを登録することです:
ConvertUtils.register(new DateConverter(null)、java。 util .Date.class);
ただし、このステートメントにより、ラッパータイプには元のタイプのデフォルト値が割り当てられます。たとえば、このフィールドの値がの場合でも、Integerプロパティにはデフォルトで値0が割り当てられます。ソースオブジェクトがnullです。
上位バージョン(1.9.3)では、日付のnull値の問題とパッケージングクラスへのデフォルト値の割り当ての問題が修正されました。
このシナリオは、パッケージングクラス属性がnullの場合に特別な意味を持ち、非常に簡単に実行できます。たとえば、検索条件オブジェクトの場合、通常、null値はフィールドが制限されていないことを意味し、0はフィールドの値が0でなければならないことを意味します。
他のツールに切り替える場合
Aliのヒントを見るとき、またはこの記事を読んだ後、CommonsBeanUtilsのパフォーマンスの問題を理解し、SpringのBeanUtilsに切り替えたい場合は、注意してください:
org.apache.commons.beanutils.BeanUtils.copyProperties(Object target、Source Object) ;
org.springframework.beans.BeanUtils.copyProperties(Source Object、Object target);
コードのコピー
は、署名のメソッド、これら2つのツールの同じクラスの名前から確認できます。メソッドも同じであり、パラメータ番号、タイプ、および名前はすべて同じです。ただし、パラメーターの位置は逆になります。したがって、変更する場合は、ターゲットとソースのパラメーターも変更することを忘れないでください。
さらに、さまざまな理由により、取得したスタック情報が不完全で問題を見つけることができないため、次の点に注意し
てください。java.lang.IllegalArgumentExceptionが発生した場合:ソースはnullまたはjava.lang.IllegalArgumentExceptionであってはなりません。 :ターゲットnullであってはならないなどの例外メッセージがどこでも理由を見つけることができない場合、それを探す必要はありません。これは、copyPropertiesを実行するときにnull値を渡すことが原因です。
オリジナル:https://juejin.im/post/5d0b68a36fb9a07ee1692ed9