Skip to content

@BeanProperty op record components #219

@robert-bor

Description

@robert-bor

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):

  1. Roept getBeanMatch() aan → maakt BeanMatch via BeanMatchStore
  2. BeanMatchStore.getAllFields() gebruikt BeanPropertyCreator voor geneste paden (regel 138)
  3. processProperties() itereert over BeanMatch.targetNodes
  4. BeanPropertyMatch.getSourceObject() haalt waarden op via BeanProperty.getObject()

MapToRecordStrategy (voor records):

  1. Negeert getBeanMatch() volledig
  2. Eigen getSourcePropertyAccessors() - alleen directe properties
  3. Eigen getNamesOfRecordComponents() - leest @BeanProperty maar gebruikt het als directe naam
  4. 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:

  1. Geen ondersteuning voor geneste @BeanProperty paden
  2. Geen ondersteuning voor @BeanRoleSecured, @BeanLogicSecured etc.
  3. 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

  1. 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);
 }
  1. 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));
 }

  1. 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;
 }

  1. 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

  1. Hergebruik van bestaande infrastructuur: BeanMatch en BeanPropertyCreator worden al gebruikt voor classes
  2. Geen code duplicatie: Geneste pad-resolutie gebeurt op één plek (BeanPropertyCreator)
  3. Feature pariteit: Records krijgen automatisch ondersteuning voor alle annotaties die BeanMatchStore verwerkt
  4. 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

  1. Null parent test - @BeanProperty("parent.id") waar parent null is
  2. Diep genest pad - @BeanProperty("a.b.c.id") met meerdere niveaus
  3. Combinatie met @BeanAlias - record component met alias naar geneste source
  4. 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:

  1. Het backing field (als ElementType.FIELD aanwezig is) ✓
  2. 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 │
 └───────────────────────────────────────┴────────────────────────────────────────────────────────┘

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions