|
| 1 | +# WW-5622: Cache Hibernate Class Presence in ProxyUtil |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Eliminate repeated `NoClassDefFoundError` exceptions in `ProxyUtil` when Hibernate is absent from the classpath, fixing severe performance degradation. |
| 6 | + |
| 7 | +**Architecture:** Add a one-time `Class.forName()` check at class-load time stored in a static `boolean HIBERNATE_AVAILABLE`. Guard all three Hibernate-touching methods with this flag to short-circuit immediately when Hibernate is absent. |
| 8 | + |
| 9 | +**Tech Stack:** Java, JUnit 4 |
| 10 | + |
| 11 | +**JIRA:** [WW-5622](https://issues.apache.org/jira/browse/WW-5622) |
| 12 | +**Spec:** `docs/superpowers/specs/2026-04-02-proxyutil-hibernate-presence-cache-design.md` |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +### File Map |
| 17 | + |
| 18 | +- **Modify:** `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java` — add `HIBERNATE_AVAILABLE` field, `detectHibernate()` method, and guard checks in 3 methods |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +### Task 1: Add HIBERNATE_AVAILABLE detection and guard all Hibernate methods |
| 23 | + |
| 24 | +**Files:** |
| 25 | +- Modify: `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java:46-53` (add field after constants) |
| 26 | +- Modify: `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java:155-161` (guard `isHibernateProxy`) |
| 27 | +- Modify: `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java:168-176` (guard `isHibernateProxyMember`) |
| 28 | +- Modify: `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java:305-311` (guard `getHibernateProxyTarget`) |
| 29 | + |
| 30 | +- [ ] **Step 1: Add the static detection field and method** |
| 31 | + |
| 32 | +After line 52 (`private static final int CACHE_INITIAL_CAPACITY = 256;`), add: |
| 33 | + |
| 34 | +```java |
| 35 | +private static final boolean HIBERNATE_AVAILABLE = detectHibernate(); |
| 36 | + |
| 37 | +private static boolean detectHibernate() { |
| 38 | + try { |
| 39 | + Class.forName("org.hibernate.proxy.HibernateProxy"); |
| 40 | + return true; |
| 41 | + } catch (ClassNotFoundException e) { |
| 42 | + return false; |
| 43 | + } |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +- [ ] **Step 2: Guard `isHibernateProxy`** |
| 48 | + |
| 49 | +In `isHibernateProxy(Object object)` (line 155), add guard before the try block. The method becomes: |
| 50 | + |
| 51 | +```java |
| 52 | +public static boolean isHibernateProxy(Object object) { |
| 53 | + if (!HIBERNATE_AVAILABLE) return false; |
| 54 | + try { |
| 55 | + return object != null && HibernateProxy.class.isAssignableFrom(object.getClass()); |
| 56 | + } catch (NoClassDefFoundError ignored) { |
| 57 | + return false; |
| 58 | + } |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +- [ ] **Step 3: Guard `isHibernateProxyMember`** |
| 63 | + |
| 64 | +In `isHibernateProxyMember(Member member)` (line 168), add guard before the try block. The method becomes: |
| 65 | + |
| 66 | +```java |
| 67 | +public static boolean isHibernateProxyMember(Member member) { |
| 68 | + if (!HIBERNATE_AVAILABLE) return false; |
| 69 | + try { |
| 70 | + Class<?> clazz = ClassLoaderUtil.loadClass(HIBERNATE_HIBERNATEPROXY_CLASS_NAME, ProxyUtil.class); |
| 71 | + return hasMember(clazz, member); |
| 72 | + } catch (ClassNotFoundException ignored) { |
| 73 | + } |
| 74 | + |
| 75 | + return false; |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +- [ ] **Step 4: Guard `getHibernateProxyTarget`** |
| 80 | + |
| 81 | +In `getHibernateProxyTarget(Object object)` (line 305), add guard before the try block. The method becomes: |
| 82 | + |
| 83 | +```java |
| 84 | +public static Object getHibernateProxyTarget(Object object) { |
| 85 | + if (!HIBERNATE_AVAILABLE) return object; |
| 86 | + try { |
| 87 | + return Hibernate.unproxy(object); |
| 88 | + } catch (NoClassDefFoundError ignored) { |
| 89 | + return object; |
| 90 | + } |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +- [ ] **Step 5: Run existing tests to verify no regression** |
| 95 | + |
| 96 | +Run: `./mvnw -pl core clean test -Dtest=com.opensymphony.xwork2.util.** -DfailIfNoTests=false` |
| 97 | +Expected: All tests PASS. Since Hibernate IS on the test classpath, `HIBERNATE_AVAILABLE` will be `true` and all code paths execute as before. |
| 98 | + |
| 99 | +Run: `./mvnw -pl plugins/spring clean test -Dtest=com.opensymphony.xwork2.spring.SpringProxyUtilTest` |
| 100 | +Expected: All 3 test methods PASS. |
| 101 | + |
| 102 | +- [ ] **Step 6: Commit** |
| 103 | + |
| 104 | +```bash |
| 105 | +git add core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java |
| 106 | +git commit -m "WW-5622 perf(core): cache Hibernate class presence to avoid repeated NoClassDefFoundError |
| 107 | +
|
| 108 | +Detect Hibernate availability once at class-load time via Class.forName() |
| 109 | +and short-circuit all Hibernate-related methods immediately when absent. |
| 110 | +This eliminates repeated NoClassDefFoundError exceptions that cause |
| 111 | +significant performance degradation in applications without Hibernate |
| 112 | +on the classpath." |
| 113 | +``` |
0 commit comments