#710: ProxyIdRefMarshaller.java

projectforge-business/src/main/java/org/projectforge/framework/persistence/xstream/ProxyIdRefMarshaller.java XStream marshaller — ID-based reference marshaller with CGLIB proxy stripping, Hibernate proxy unwrapping, and circular reference prevention, projectforge-business/src/main/java/org/projectforge/framework/persistence/xstream/ProxyIdRefMarshaller.java 91 lines · 54 code · 24 comments · 13 blank
Extends XStream's ReferenceByIdMarshaller to provide robust entity graph serialization with three layers of proxy handling: (1) strips Spring CGLIB Enhancer proxies by walking the superclass chain, (2) unwraps Hibernate proxies via HibernateProxyHelper.get(), and (3) assigns incremental numeric IDs to every mutable object and writes ID/IDREF references when the same object is encountered again. This prevents infinite recursion from bidirectional JPA associations and ensures that entity identity (database ID) is preserved in the serialized XML — the same entity appearing in multiple association paths is serialized only once, with subsequent appearances rendered as <entity reference="3"/>. This is the most sophisticated proxy-aware marshaller in the ProjectForge XStream bridge layer.

Architecture

Class Hierarchy

TreeMarshaller
  └── AbstractReferenceMarshaller       (adds object reference tracking)
       ├── ReferenceByXPathMarshaller   (#707 — XPath-based references)
       └── ReferenceByIdMarshaller      (ID/IDREF-based references)
            └── ProxyIdRefMarshaller    (this class — adds proxy stripping + CGLIB handling)

Fields

FieldTypePurpose
referencesObjectIdDictionaryTracks every object that has been serialized, mapping object identity → generated ID. Used to detect circular/duplicate references.
idGeneratorIDGeneratorGenerates unique sequential IDs. Defaults to SequenceGenerator(1) producing IDs "1", "2", "3", ...
classMapperMapperCached reference to the XStream class mapper (not the mapper from the superclass, which shadows it).
logorg.slf4j.LoggerStatic SLF4J logger for debug-level tracing of marshalled objects.

Constructors

Two constructor overloads:

  1. Full constructor: Accepts HierarchicalStreamWriter, ConverterLookup, Mapper, and IDGenerator. Allows custom ID generation strategies.
  2. Convenience constructor: Omits the IDGenerator parameter and defaults to SequenceGenerator(1) — produces sequential numeric IDs starting at 1.

convertAnother(Object item) — Core Algorithm

This is the overridden method where all proxy handling and reference tracking happens. It replaces XStream's default convertAnother() with proxy-aware logic:

  1. CGLIB Enhancer stripping (lines ~56-59):
    Class<?> targetClass = item.getClass();
    while (Enhancer.isEnhanced(targetClass)) {
        targetClass = targetClass.getSuperclass();
    }
    Walks up the superclass chain stripping Spring CGLIB proxies (created by @Transactional, @Cacheable, etc.). Enhancer.isEnhanced() returns true for CGLIB-generated subclasses. This is done before converter lookup so the correct entity converter is selected.
  2. Converter lookup (line ~61):
    Converter converter = converterLookup.lookupConverterForType(targetClass);
    Finds the appropriate XStream converter for the real (un-proxied) entity class.
  3. Hibernate proxy unwrapping (line ~62):
    Object realItem = HibernateProxyHelper.get(item);
    Uses HibernateProxyHelper to unwrap Hibernate lazy-loading proxies. After this step, realItem is the actual entity instance with no proxy wrappers.
  4. Immutable value type fast path (lines ~64-66):
    if (classMapper.isImmutableValueType(realItem.getClass())) {
        converter.marshal(item, writer, this);
    }
    Strings, integers, dates, enums, and other immutable types are marshalled directly without ID tracking. This avoids polluting the reference dictionary with values that can never form circular references.
  5. Duplicate reference detection (lines ~67-70):
    Object idOfExistingReference = references.lookupId(realItem);
    if (idOfExistingReference != null) {
        writer.addAttribute("reference", idOfExistingReference.toString());
        return;
    }
    Checks the ObjectIdDictionary to see if realItem has already been serialized. If so, writes a reference attribute (e.g., reference="3") pointing to the earlier serialization by its generated ID. This is the mechanism that prevents infinite recursion in bidirectional associations — when XStream traverses back to a parent from a child, it finds the parent already in the dictionary and emits a reference instead of re-serializing.
  6. First-time serialization (lines ~71-81):
    String newId = idGenerator.next(realItem);
    writer.addAttribute("id", newId);
    references.associateId(realItem, newId);
    converter.marshal(realItem, writer, this);
    Generates a new unique ID, stores it in the reference dictionary, writes it as an id attribute on the XML element, then delegates to the converter for full serialization. The item is now registered so future encounters will hit step 5.

ObjectIdDictionary

XStream's ObjectIdDictionary is an identity-based map (uses System.identityHashCode() and == comparison, not .equals()). This is critical because JPA entities often implement equals() based on database ID, which may be null for transient entities. Identity-based tracking ensures each object instance is tracked uniquely regardless of its equals() implementation.

Hibernate proxy identity caveat: If two different Hibernate proxy instances wrap the same underlying entity (possible when the entity is loaded through different sessions or associations), the ObjectIdDictionary would see them as different objects (different identity hashes) even after HibernateProxyHelper.get(). This can cause the same database row to be serialized twice with different generated IDs. This is a known limitation of identity-based tracking — the alternative (value-based tracking using database IDs) would require special handling for transient entities.

Integration Points

Design Decisions & Gotchas

Git History

868d6abb7 2025 -> 2026
63081666f Source file headers: 2024-> 2025.
b6092df09 Copyright 2023 -> 2024
ab45d51fa Copyright 2001-2022 -> 2001-2023.
5f7ef41b8 Copyright 2021 -> 2022
ceb63e8a1 Source code header: (C) 2001-2021.
7c79f1922 Copyright of source header -> 2020.
32f634b88 Optimize imports
000ca723d Remove pointless boolean expressions (business)
dd5ca38ac CopyRight of all java file-header updated or created.
a954d7f7d Changes from Java 9 branch
a5bbdca6a Change logger to slf4j
9ebb88522 Initial commit