-
Notifications
You must be signed in to change notification settings - Fork 7
Description
De MapToRecordStrategy ondersteunt geen geneste property paden (zoals "parent.id") bij het gebruik van @BeanProperty annotaties op record components.
Test case die faalt (BeanPropertyRecordTest.java:28):
public record NestedResultRecord(
Long id,
String name,
@BeanProperty("parent.id") Long parentId, // Wordt NIET correct gemapt
@BeanProperty("parent.name") String parentName // Wordt NIET correct gemapt
) {}
Architectuur Analyse
Strategy Hierarchy
MapStrategy (interface)
└── AbstractMapStrategy
└── MapToInstanceStrategy
└── MapToClassStrategy
└── MapToRecordStrategy
Huidige situatie: Twee gescheiden code-paden
MapToClassStrategy (voor normale classes):
- Roept getBeanMatch() aan → maakt BeanMatch via BeanMatchStore
- BeanMatchStore.getAllFields() gebruikt BeanPropertyCreator voor geneste paden (regel 138)
- processProperties() itereert over BeanMatch.targetNodes
- BeanPropertyMatch.getSourceObject() haalt waarden op via BeanProperty.getObject()
MapToRecordStrategy (voor records):
- Negeert getBeanMatch() volledig
- Eigen getSourcePropertyAccessors() - alleen directe properties
- Eigen getNamesOfRecordComponents() - leest @BeanProperty maar gebruikt het als directe naam
- Eigen getValuesOfFields() - accessors.get("parent.id") faalt want map bevat alleen top-level
Root Cause
MapToRecordStrategy implementeert een volledig parallelle mapping logica die de bestaande BeanMatch infrastructuur niet gebruikt. Dit zorgt voor:
- Geen ondersteuning voor geneste @BeanProperty paden
- Geen ondersteuning voor @BeanRoleSecured, @BeanLogicSecured etc.
- Code duplicatie en inconsistent gedrag
Oplossing: BeanMatch integreren in MapToRecordStrategy
In plaats van BeanPropertyCreator direct te gebruiken (wat een nieuwe code-pad zou creëren), integreren we de bestaande BeanMatch infrastructuur.
Wijzigingen in MapToRecordStrategy.java
- Nieuwe methode om BeanProperty chain waarde op te halen:
/**
* Resolves the value for a record component from the source object.
* Supports nested paths like "parent.id" via BeanMatch infrastructure.
*/
private <S> Object resolveSourceValue(S source, String componentName, BeanMatch beanMatch) {
// First check target nodes (for @BeanProperty mapped fields)
BeanProperty sourceProperty = beanMatch.getSourceNodes().get(componentName);
// Then check aliases (for @BeanAlias)
if (sourceProperty == null) {
sourceProperty = beanMatch.getAliases().get(componentName);
}
if (sourceProperty == null) {
return null;
}
// BeanProperty.getObject() handles nested traversal automatically
return sourceProperty.getObject(source);
}
- Aanpassing van map() methode om BeanMatch te gebruiken:
@Override
public <S, T> T map(final S source) {
Class<T> targetClass = this.getConfiguration().getTargetClass();
if (source.getClass() == targetClass) {
return targetClass.cast(source);
}
// Converter check blijft hetzelfde...
if (getConfiguration().isConverterChoosable()) {
BeanConverter converter = getConverterOptional(source.getClass(), targetClass);
if (converter != null) {
return converter.convert(getBeanMapper(), source, targetClass, null);
}
}
// NIEUW: Gebruik BeanMatch voor property resolution
BeanMatch beanMatch = getBeanMatch(source.getClass(), targetClass);
// Constructor selectie blijft hetzelfde
Map<String, PropertyAccessor> sourcePropertyAccessors = getSourcePropertyAccessors(source);
Constructor<T> constructor = (Constructor<T>) getSuitableConstructor(sourcePropertyAccessors, targetClass);
String[] fieldNamesForConstructor = getNamesOfConstructorParameters(targetClass, constructor);
// AANGEPAST: Gebruik BeanMatch voor waarde resolutie
List<Object> values = getValuesOfFieldsViaBeanMatch(source, beanMatch, fieldNamesForConstructor);
return targetClass.cast(constructTargetObject(constructor, values));
}
- Nieuwe methode voor waarde ophalen via BeanMatch:
private <S> List<Object> getValuesOfFieldsViaBeanMatch(S source, BeanMatch beanMatch, String[] fieldNames) {
List<Object> values = new ArrayList<>();
for (String fieldName : fieldNames) {
values.add(resolveSourceValue(source, fieldName, beanMatch));
}
return values;
}
- Aanpassing getNamesOfRecordComponents() - retourneer component naam, niet @BeanProperty value:
De @BeanProperty annotatie wordt nu verwerkt door BeanMatchStore, dus we hoeven hier alleen de record component naam te retourneren:
private <T> String[] getNamesOfRecordComponents(final Class<T> targetClass) {
return Arrays.stream(targetClass.getRecordComponents())
.map(RecordComponent::getName) // Simpelweg de component naam
.toArray(String[]::new);
}
De BeanMatchStore.getAllFields() leest de @BeanProperty annotatie en maakt de juiste BeanProperty chain aan via BeanPropertyCreator.
Waarom dit architectureel correct is
- Hergebruik van bestaande infrastructuur: BeanMatch en BeanPropertyCreator worden al gebruikt voor classes
- Geen code duplicatie: Geneste pad-resolutie gebeurt op één plek (BeanPropertyCreator)
- Feature pariteit: Records krijgen automatisch ondersteuning voor alle annotaties die BeanMatchStore verwerkt
- Consistentie: Dezelfde mapping logica voor classes en records
Te wijzigen bestanden
┌─────────────────────────────────────────────────────────────────────┬──────────────────────┐
│ Bestand │ Wijziging │
├─────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java │ BeanMatch integratie │
├─────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ src/test/java/io/beanmapper/annotations/BeanPropertyRecordTest.java │ Extra tests │
└─────────────────────────────────────────────────────────────────────┴──────────────────────┘
Imports toe te voegen
import io.beanmapper.core.BeanMatch;
import io.beanmapper.core.BeanProperty;
import java.util.ArrayList;
Tests
Bestaande test (moet slagen na fix)
- beanPropertyOnRecordComponentShouldMapNestedProperty() - geneste property mapping
Nieuwe tests toe te voegen
- Null parent test - @BeanProperty("parent.id") waar parent null is
- Diep genest pad - @BeanProperty("a.b.c.id") met meerdere niveaus
- Combinatie met @BeanAlias - record component met alias naar geneste source
- Backward compatibility - records zonder @BeanProperty moeten blijven werken
Verificatie
Run de specifieke test
mvn test -Dtest=BeanPropertyRecordTest
Run alle record-gerelateerde tests
mvn test -Dtest="Record"
Run alle tests voor backward compatibility
mvn test
Belangrijk Detail: @BeanProperty Target Types
@BeanProperty heeft @target({ ElementType.FIELD, ElementType.METHOD }) - het mist ElementType.RECORD_COMPONENT.
Dit is GEEN probleem omdat Java automatisch annotaties op record components propageert naar:
- Het backing field (als ElementType.FIELD aanwezig is) ✓
- De accessor method (als ElementType.METHOD aanwezig is) ✓
De CombinedPropertyAccessor.findAnnotation() zoekt in beide locaties en zal de annotatie vinden.
Risico's en Mitigaties
┌───────────────────────────────────────┬────────────────────────────────────────────────────────┐
│ Risico │ Mitigatie │
├───────────────────────────────────────┼────────────────────────────────────────────────────────┤
│ Constructor selectie conflict │ getSuitableConstructor() blijft ongewijzigd │
├───────────────────────────────────────┼────────────────────────────────────────────────────────┤
│ Performance door BeanMatch creatie │ BeanMatch wordt gecached in BeanMatchStore │
├───────────────────────────────────────┼────────────────────────────────────────────────────────┤
│ Bestaande record mappings breken │ Uitgebreide test suite draaien │
├───────────────────────────────────────┼────────────────────────────────────────────────────────┤
│ @BeanProperty niet gevonden op record │ Java propageert naar field, PropertyAccessor vindt het │
└───────────────────────────────────────┴────────────────────────────────────────────────────────┘