ProxyIdRefMarshaller.javaReferenceByIdMarshaller 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.TreeMarshaller
└── AbstractReferenceMarshaller (adds object reference tracking)
├── ReferenceByXPathMarshaller (#707 — XPath-based references)
└── ReferenceByIdMarshaller (ID/IDREF-based references)
└── ProxyIdRefMarshaller (this class — adds proxy stripping + CGLIB handling)
| Field | Type | Purpose |
|---|---|---|
references | ObjectIdDictionary | Tracks every object that has been serialized, mapping object identity → generated ID. Used to detect circular/duplicate references. |
idGenerator | IDGenerator | Generates unique sequential IDs. Defaults to SequenceGenerator(1) producing IDs "1", "2", "3", ... |
classMapper | Mapper | Cached reference to the XStream class mapper (not the mapper from the superclass, which shadows it). |
log | org.slf4j.Logger | Static SLF4J logger for debug-level tracing of marshalled objects. |
Two constructor overloads:
HierarchicalStreamWriter, ConverterLookup, Mapper, and IDGenerator. Allows custom ID generation strategies.IDGenerator parameter and defaults to SequenceGenerator(1) — produces sequential numeric IDs starting at 1.This is the overridden method where all proxy handling and reference tracking happens. It replaces XStream's default convertAnother() with proxy-aware logic:
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.Converter converter = converterLookup.lookupConverterForType(targetClass);Finds the appropriate XStream converter for the real (un-proxied) entity class.
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.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.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.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.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.
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.createMarshallingContext() to instantiate this marshaller instead of the default ReferenceByIdMarshaller.MarshallingStrategy is configured.Enhancer for CGLIB proxy detection (compile dependency on spring-core).org.springframework.cglib.proxy.Enhancer creates a hard dependency on Spring Framework, which is acceptable since ProjectForge is a Spring application. However, this class would not work outside a Spring context without modification.classMapper.isImmutableValueType() check is an important optimization. Without it, every string literal, integer, and date in the entity graph would be assigned an ID and added to the reference dictionary, massively bloating memory usage and XML output.SequenceGenerator(1) produces IDs within a single marshalling context. If two separate ProxyIdRefMarshaller instances serialize different object graphs within the same XML document (unlikely but theoretically possible with XStream's MarshallingContext nesting), IDs could collide. This is not a practical concern because each marshalling context uses its own marshaller instance.mapper field. The two-argument constructor sets this.classMapper but the three-argument constructor sets both. The code consistently uses this.classMapper for the locally stored reference, avoiding confusion with the inherited field.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