Skip to content

Commit d21252a

Browse files
committed
Merge branch 'master' of ssh://github.com/ExpediaGroup/bull into develop
2 parents 537f8cc + d3801e9 commit d21252a

24 files changed

Lines changed: 821 additions & 175 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
### [3.0.4] 2026.04.27
6+
* Replaces 12-branch if-else chains in `ConversionProcessorFactory` and `ConversionAnalyzer` with switch expressions and cached processor singletons, eliminating per-call object allocation in the type conversion hot path
7+
* Removes `AtomicReference` allocation in `ReflectionUtils.getRealTarget` using instanceof pattern matching; eliminates redundant `isAnnotationPresent` check before `getAnnotation`
8+
* Replaces stream-based type checks in `ClassUtils.isSpecialType` and `isCustomSpecialType` with plain loops to avoid lambda allocation on frequently-called paths
9+
* Replaces `LinkedList` with `ArrayList` in `ClassUtils.getMethods` for better cache locality; replaces intermediate `collect(toList())` with `toList()` throughout
10+
* Replaces inner stream+lambda in `AbstractTransformer.withFieldMapping` and `withFieldTransformer` with plain for-each loops
11+
* Replaces `keySet().stream().collect(toMap(...))` in `MapPopulator` with an `entrySet()` imperative loop and pre-sized `HashMap`; applies instanceof pattern matching to eliminate `getClass().equals()` calls
12+
* Adds JMH microbenchmarks covering simple-bean, complex-bean, map, and type-conversion transformation paths
13+
514
### [3.0.3] 2026.04.01
615
* Refactors `TransformerImpl` to use direct field access via reflection utilities instead of string-literal field names, improving maintainability and robustness
716
* Eliminates redundant `ThreadLocal` initialisation by reusing the existing `rootSourceStack` instance

benchmark.sh

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env bash
2+
# Builds the JMH benchmark fat jar and runs it, then prints a formatted
3+
# transformation-time summary.
4+
# Any arguments are forwarded to JMH, e.g.:
5+
# ./benchmark.sh # full run (~10-15 min)
6+
# ./benchmark.sh -f 1 -wi 2 -i 3 # quick smoke run (~2 min)
7+
# ./benchmark.sh mutableSimpleBean # single benchmark
8+
# ./benchmark.sh -rf json -rff out.json # write results to JSON
9+
10+
set -euo pipefail
11+
12+
MODULES="bull-common,bull-converter,bull-bean-transformer,bull-benchmark"
13+
JAR="bull-benchmark/target/benchmarks.jar"
14+
MVN="./mvnw"
15+
16+
if [[ "$(uname -s)" == MINGW* || "$(uname -s)" == CYGWIN* ]]; then
17+
MVN="./mvnw.cmd"
18+
fi
19+
20+
echo "==> Building benchmark jar..."
21+
BUILD_LOG=$(mktemp)
22+
trap 'rm -f "$TMPOUT" "$BUILD_LOG"' EXIT
23+
if ! "$MVN" package -pl "$MODULES" -DskipTests -Djacoco.skip=true -q >"$BUILD_LOG" 2>&1; then
24+
cat "$BUILD_LOG"
25+
exit 1
26+
fi
27+
28+
TMPOUT=$(mktemp)
29+
30+
echo "==> Running benchmarks..."
31+
java -jar "$JAR" "$@" 2>&1 \
32+
| grep -v \
33+
-e "^NOTE:" \
34+
-e "^WARNING:" \
35+
-e "^Processing [0-9]" \
36+
-e "^Writing out" \
37+
| tee "$TMPOUT"
38+
39+
# -----------------------------------------------------------------------
40+
# Pretty-print a transformation-time summary from the JMH result lines.
41+
# Scores are in µs/op; we convert to ms for readability.
42+
# Result lines look like either of:
43+
# TransformerBenchmark.mutableSimpleBean avgt 2 0.536 us/op
44+
# TransformerBenchmark.mutableSimpleBean avgt 20 0.536 ± 0.012 us/op
45+
# -----------------------------------------------------------------------
46+
echo ""
47+
echo "==> Transformation times summary (ms/op):"
48+
echo ""
49+
50+
awk '
51+
function us_to_ms(v, r) {
52+
r = v / 1000
53+
# print with enough decimals to show non-zero values even for fast paths
54+
return sprintf("%.6f", r)
55+
}
56+
/^TransformerBenchmark\./ {
57+
split($1, parts, ".")
58+
name = parts[2]
59+
score = $4
60+
if ($5 == "±") { error = "± " us_to_ms($6); unit = $7 }
61+
else { error = ""; unit = $5 }
62+
scores[name] = us_to_ms(score)
63+
errors[name] = error
64+
# classify into bean transformations vs type conversions
65+
if (name ~ /typeConversion/) {
66+
conv[length(conv)+1] = name
67+
} else {
68+
beans[length(beans)+1] = name
69+
}
70+
}
71+
END {
72+
fmt = " %-44s %12s %-16s\n"
73+
sep = " " sprintf("%-44s", "--------------------------------------------") \
74+
" " sprintf("%-12s", "------------") \
75+
" " sprintf("%-16s", "----------------")
76+
77+
print " Bean transformations:"
78+
printf fmt, " Benchmark", "Time (ms/op)", "Error (ms/op)"
79+
print sep
80+
for (i = 1; i <= length(beans); i++) {
81+
n = beans[i]
82+
printf fmt, " " n, scores[n], errors[n]
83+
}
84+
85+
print ""
86+
print " Type conversions:"
87+
printf fmt, " Benchmark", "Time (ms/op)", "Error (ms/op)"
88+
print sep
89+
for (i = 1; i <= length(conv); i++) {
90+
n = conv[i]
91+
printf fmt, " " n, scores[n], errors[n]
92+
}
93+
}
94+
' "$TMPOUT"

bull-bean-transformer/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<parent>
99
<groupId>com.expediagroup.beans</groupId>
1010
<artifactId>bean-utils-library-parent</artifactId>
11-
<version>3.0.4-SNAPSHOT</version>
11+
<version>3.0.5-SNAPSHOT</version>
1212
</parent>
1313

1414
<dependencies>

bull-bean-transformer/src/main/java/com/expediagroup/beans/populator/MapPopulator.java

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
*/
1616
package com.expediagroup.beans.populator;
1717

18-
import static java.util.stream.Collectors.toMap;
19-
2018
import java.lang.reflect.Field;
19+
import java.util.HashMap;
2120
import java.util.Map;
2221

2322
import com.expediagroup.beans.transformer.BeanTransformer;
@@ -58,16 +57,14 @@ class MapPopulator extends Populator<Map<?, ?>> {
5857
final MapElemType elemType = mapType.getElemType();
5958
final boolean keyIsPrimitive = isPrimitive(keyType);
6059
final boolean elemIsPrimitive = isPrimitive(elemType);
61-
Map<?, ?> populatedObject;
6260
if (keyIsPrimitive && elemIsPrimitive) {
63-
populatedObject = fieldValue;
64-
} else {
65-
populatedObject = fieldValue.keySet()
66-
.stream()
67-
// .parallelStream()
68-
.collect(toMap(
69-
key -> getElemValue(keyType, keyIsPrimitive, key),
70-
key -> getElemValue(elemType, elemIsPrimitive, fieldValue.get(key))));
61+
return fieldValue;
62+
}
63+
Map<Object, Object> populatedObject = new HashMap<>(fieldValue.size());
64+
for (Map.Entry<?, ?> entry : fieldValue.entrySet()) {
65+
populatedObject.put(
66+
getElemValue(keyType, keyIsPrimitive, entry.getKey()),
67+
getElemValue(elemType, elemIsPrimitive, entry.getValue()));
7168
}
7269
return populatedObject;
7370
}
@@ -78,7 +75,7 @@ class MapPopulator extends Populator<Map<?, ?>> {
7875
* @return true if it's primitive, false otherwise
7976
*/
8077
private boolean isPrimitive(final MapElemType mapElemType) {
81-
return mapElemType.getClass().equals(ItemType.class) && classUtils.isPrimitiveOrSpecialType(((ItemType) mapElemType).getObjectClass());
78+
return mapElemType instanceof ItemType itemType && classUtils.isPrimitiveOrSpecialType(itemType.getObjectClass());
8279
}
8380

8481
/**
@@ -91,16 +88,12 @@ private boolean isPrimitive(final MapElemType mapElemType) {
9188
*/
9289
@SuppressWarnings("unchecked")
9390
private <T> T getElemValue(final MapElemType mapElemType, final boolean elemIsPrimitiveType, final T value) {
94-
final T elemValue;
9591
if (elemIsPrimitiveType || classUtils.isPrimitiveOrSpecialType(value.getClass())) {
96-
elemValue = value;
97-
} else {
98-
if (mapElemType.getClass().equals(ItemType.class)) {
99-
elemValue = (T) transform(value, ((ItemType) mapElemType).getObjectClass(), ((ItemType) mapElemType).getGenericClass());
100-
} else {
101-
elemValue = (T) getPopulatedObject((Map) value, (MapType) mapElemType);
102-
}
92+
return value;
93+
}
94+
if (mapElemType instanceof ItemType itemType) {
95+
return (T) transform(value, itemType.getObjectClass(), itemType.getGenericClass());
10396
}
104-
return elemValue;
97+
return (T) getPopulatedObject((Map<?, ?>) value, (MapType) mapElemType);
10598
}
10699
}

bull-bean-transformer/src/main/java/com/expediagroup/beans/transformer/AbstractBeanTransformer.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
*/
1616
package com.expediagroup.beans.transformer;
1717

18-
import static java.util.Arrays.asList;
19-
2018
import static com.expediagroup.transformer.validator.Validator.notNull;
2119

2220
import com.expediagroup.beans.conversion.analyzer.ConversionAnalyzer;
@@ -150,8 +148,8 @@ public final <T, K> void transform(final T sourceObj, final K targetObject) {
150148
*/
151149
@Override
152150
public BeanTransformer skipTransformationForField(final String... fieldName) {
153-
if (fieldName.length != 0) {
154-
settings.getFieldsToSkip().addAll(asList(fieldName));
151+
for (String field : fieldName) {
152+
settings.getFieldsToSkip().add(field);
155153
}
156154
return this;
157155
}

0 commit comments

Comments
 (0)