#711: ProxyIdRefMarshallingStrategy.java
Purpose: When serializing JPA entities to XML (for database history storage, XML backup/restore, and plugin data exchange), Hibernate creates proxy objects (cglib-enhanced subclasses) for lazy-loaded associations. Standard XStream marshalling would serialize these proxies with their cglib wrapper class names and internal fields, producing unreadable, non-deserializable XML. This strategy intercepts the marshalling process to unwrap proxies and serialize only real entity data with reference tracking (ID-based deduplication).
Architecture
Strategy Pattern — Composite Delegation
The class extends ReferenceByIdMarshallingStrategy (from XStream) and overrides only two methods:
marshal() → Creates a new ProxyIdRefMarshaller (NOT the standard ReferenceByIdMarshaller). The ProxyIdRefMarshaller unwraps Hibernate proxies via HibernateProxyHelper.get(), strips cglib enhancements via Enhancer.isEnhanced(), and handles reference tracking using an internal ObjectIdDictionary.
unmarshal() → Uses the standard ReferenceByIdUnmarshaller from XStream. Deserialization already receives clean XML (proxies were stripped during marshal), so no special handling is needed on the read side.
This asymmetric design reflects the reality that proxy problems exist only at serialization time — the XML output is always «clean».
Inheritance Hierarchy
ReferenceByIdMarshallingStrategy (XStream base)
└── ProxyIdRefMarshallingStrategy (#709 — this file)
├── marshal() → ProxyIdRefMarshaller (#708)
└── unmarshal() → ReferenceByIdUnmarshaller (XStream standard)
The Hibernate Proxy Problem
What happens without this strategy:
1. JPA entity AddressDO has a @ManyToOne(fetch = LAZY) to AddressbookDO
2. Hibernate creates a cglib proxy: AddressbookDO$$EnhancerByCGLIB$$a1b2c3d4@123
3. Standard XStream serializes this as: <addressbook class="AddressbookDO$$EnhancerByCGLIB$$a1b2c3d4">...hibernate internals...</addressbook>
4. Result: unreadable XML, breaks deserialization, leaks Hibernate internals
With ProxyIdRefMarshallingStrategy:
1. Enhancer.isEnhanced(targetClass) → detects cglib proxy, walks up to superclass
2. HibernateProxyHelper.get(item) → extracts real entity behind proxy
3. references.lookupId(realItem) → if already seen, emits reference="ID" attribute (no data duplication)
4. Otherwise, emits id="ID" and serializes the real entity data
Result: clean XML with real class names, no Hibernate internals, and reference-based deduplication.
Reference Tracking — Avoiding Circular References
The ProxyIdRefMarshaller maintains an ObjectIdDictionary (identity hash map). For each non-immutable object it encounters:
• First encounter: Assigns an ID via SequenceGenerator, stores in dictionary, serializes full object
• Subsequent encounters: Emits only reference="ID" attribute, skipping the object body
This is critical because JPA entity graphs often contain cycles: AddressDO → AddressbookDO → owner → addresses → AddressDO. Without reference tracking, XStream would enter infinite recursion. With it, cyclic references become compact <addressbook reference="42"/> tags.
Immutables are excluded from reference tracking (classMapper.isImmutableValueType()): Strings, Integers, Dates, etc. always serialized inline. This avoids polluting the reference table with thousands of value-type entries.
Git History
868d6abb7 2025 → 2026 (copyright year update)
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
dd5ca38ac CopyRight of all java file-header updated or created
9ebb88522 Initial commit (project creation)
The file has existed since the very first commit of the project. It's part of the foundational XStream integration — one of the earliest design decisions in ProjectForge's architecture.
Key Commits
9ebb88522 (initial commit): The ProxyIdRefMarshallingStrategy was introduced alongside the entire XStream persistence layer. At the time, ProjectForge used XStream for:
• Database History: Serializing entity snapshots before/after modifications into a history table column
• XML Import/Export: Plugin data exchange and migration scripts
• User Preferences: Serializing user settings as XML blobs
The strategy was a prerequisite for these features — without proxy-safe marshalling, history snapshots of entities with lazy-loaded relations would be corrupted.
No logic changes since initial commit: The file has only received copyright year updates. The algorithm (cglib detection → proxy unwrap → reference tracking) has proven stable across 5+ years of Hibernate and XStream version upgrades.
Design Decisions
| Decision | Why | Trade-off |
Extend ReferenceByIdMarshallingStrategy rather than implementing from scratch |
Reuses XStream's reference-by-ID wire format (id/reference attributes) which is well-documented and understood |
Tight coupling to XStream's internal class hierarchy — could break on major XStream upgrades |
| Asymmetric marshal/unmarshal (custom only on write) |
Proxy problems only exist at serialization time; deserialized objects are never proxies |
If deserialization starts receiving proxy-containing XML (e.g., from external sources), issues would arise |
Use Enhancer.isEnhanced() (cglib) directly |
XStream 1.4.x didn't have built-in Hibernate proxy detection; this was the only reliable way |
Direct cglib dependency — would need update if Hibernate 6+ switches from cglib to ByteBuddy proxies |
Separate ProxyIdRefMarshaller class (not inline) |
Marshaller is the «heavy» class with ~70 lines of proxy logic; Strategy is a thin 25-line delegation class |
Adds one extra file to the namespace, but follows Single Responsibility |
Gotcha — Hibernate 6+ ByteBuddy proxies: ProjectForge 8.x uses Hibernate 5.x with cglib. If upgrading to Hibernate 6.x, Enhancer.isEnhanced() won't detect ByteBuddy proxies (Hibernate 6 replaced cglib with ByteBuddy). The proxy detection logic would need to be updated to support both: cglib Enhancer.isEnhanced() and ByteBuddy's proxy detection (Hibernate.isInitialized() + Hibernate.unproxy()). This is a known migration risk for future Hibernate upgrades.
Where it's used: Registered as the default XStream marshalling strategy in XmlRegistry. Every XStream operation in ProjectForge — history snapshots, XML backup/restore, user preferences serialization — goes through this strategy. The XmlObjectWriter and XmlObjectReader components (the xmlstream framework) use XStream under the hood, so this strategy is effectively the «gateway» for all XML I/O in the application.