|
1 | 1 | package com.cedarsoftware.util; |
2 | 2 |
|
| 3 | +import java.lang.reflect.Constructor; |
3 | 4 | import java.lang.reflect.Field; |
4 | 5 | import java.lang.reflect.Method; |
| 6 | +import java.util.concurrent.ConcurrentHashMap; |
| 7 | +import java.util.concurrent.ConcurrentMap; |
5 | 8 |
|
6 | 9 | import static com.cedarsoftware.util.ClassUtilities.trySetAccessible; |
7 | 10 |
|
8 | 11 | /** |
9 | | - * Wrapper for unsafe, decouples direct usage of sun.misc.* package. |
| 12 | + * Provides constructor-bypassing object instantiation using two strategies: |
| 13 | + * <ol> |
| 14 | + * <li><b>ReflectionFactory</b> (preferred) — creates a synthetic constructor that runs |
| 15 | + * {@code Object.<init>()} instead of the class's own constructors. This is the same |
| 16 | + * mechanism used by {@code ObjectInputStream}. Fields get Java default values and the |
| 17 | + * object header is properly initialized. May not be accessible on JDK 17+ without |
| 18 | + * {@code --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED}.</li> |
| 19 | + * <li><b>sun.misc.Unsafe</b> (fallback) — allocates raw memory without any constructor call. |
| 20 | + * Still accessible on current JDKs but deprecated for removal starting in JDK 26.</li> |
| 21 | + * </ol> |
10 | 22 | * |
11 | 23 | * @author Kai Hufenback |
| 24 | + * John DeRegnaucourt (jdereg@cedarsoft.com) |
12 | 25 | */ |
13 | 26 | final class Unsafe { |
| 27 | + private final Object reflectionFactory; |
| 28 | + private final Method newConstructorForSerialization; |
| 29 | + private final Constructor<?> objectConstructor; |
14 | 30 | private final Object sunUnsafe; |
15 | | - private final Method allocateInstance; |
| 31 | + private final Method unsafeAllocateInstance; |
| 32 | + private final ConcurrentMap<Class<?>, Constructor<?>> serializationConstructorCache = new ConcurrentHashMap<>(); |
16 | 33 |
|
17 | 34 | /** |
18 | | - * Constructs unsafe object, acting as a wrapper. |
| 35 | + * Constructs the wrapper, reflectively loading ReflectionFactory and sun.misc.Unsafe. |
| 36 | + * At least one must be available, otherwise throws IllegalStateException. |
19 | 37 | */ |
20 | 38 | public Unsafe() { |
| 39 | + // ── Strategy 1: ReflectionFactory (preferred, same as ObjectInputStream) ── |
| 40 | + Object rfInstance = null; |
| 41 | + Method ncsMethod = null; |
| 42 | + Constructor<?> objCtor = null; |
| 43 | + try { |
| 44 | + // JDK 9+: jdk.internal.reflect.ReflectionFactory |
| 45 | + // JDK 8: sun.reflect.ReflectionFactory |
| 46 | + Class<?> rfClass; |
| 47 | + try { |
| 48 | + rfClass = Class.forName("sun.reflect.ReflectionFactory"); |
| 49 | + } catch (ClassNotFoundException e) { |
| 50 | + rfClass = Class.forName("jdk.internal.reflect.ReflectionFactory"); |
| 51 | + } |
| 52 | + |
| 53 | + Method getFactory = rfClass.getDeclaredMethod("getReflectionFactory"); |
| 54 | + rfInstance = getFactory.invoke(null); |
| 55 | + ncsMethod = rfClass.getDeclaredMethod("newConstructorForSerialization", Class.class, Constructor.class); |
| 56 | + objCtor = Object.class.getDeclaredConstructor(); |
| 57 | + |
| 58 | + // Verify it works |
| 59 | + Constructor<?> test = (Constructor<?>) ncsMethod.invoke(rfInstance, Object.class, objCtor); |
| 60 | + if (test == null) { |
| 61 | + rfInstance = null; |
| 62 | + ncsMethod = null; |
| 63 | + objCtor = null; |
| 64 | + } |
| 65 | + } catch (Exception ignored) { |
| 66 | + rfInstance = null; |
| 67 | + ncsMethod = null; |
| 68 | + objCtor = null; |
| 69 | + } |
| 70 | + this.reflectionFactory = rfInstance; |
| 71 | + this.newConstructorForSerialization = ncsMethod; |
| 72 | + this.objectConstructor = objCtor; |
| 73 | + |
| 74 | + // ── Strategy 2: sun.misc.Unsafe (fallback, deprecated in JDK 23+) ── |
| 75 | + Object unsafeObj = null; |
| 76 | + Method allocMethod = null; |
21 | 77 | try { |
22 | 78 | Class<?> unsafeClass = ClassUtilities.forName("sun.misc.Unsafe", ClassUtilities.getClassLoader(Unsafe.class)); |
23 | 79 | Field f = unsafeClass.getDeclaredField("theUnsafe"); |
24 | 80 | trySetAccessible(f); |
25 | | - sunUnsafe = f.get(null); |
26 | | - allocateInstance = ReflectionUtils.getMethod(unsafeClass, "allocateInstance", Class.class); |
27 | | - } catch (Exception e) { |
28 | | - throw new IllegalStateException("Unable to use sun.misc.Unsafe to construct objects.", e); |
| 81 | + unsafeObj = f.get(null); |
| 82 | + allocMethod = ReflectionUtils.getMethod(unsafeClass, "allocateInstance", Class.class); |
| 83 | + } catch (Exception ignored) { |
| 84 | + // sun.misc.Unsafe not available (JDK 26+ or security restriction) |
| 85 | + } |
| 86 | + this.sunUnsafe = unsafeObj; |
| 87 | + this.unsafeAllocateInstance = allocMethod; |
| 88 | + |
| 89 | + if (reflectionFactory == null && sunUnsafe == null) { |
| 90 | + throw new IllegalStateException("Neither ReflectionFactory nor sun.misc.Unsafe is available for constructor-bypassing instantiation."); |
29 | 91 | } |
30 | 92 | } |
31 | 93 |
|
32 | 94 | /** |
33 | | - * Creates an object without invoking constructor or initializing variables. |
34 | | - * <b>Be careful using this with JDK objects, like URL or ConcurrentHashMap this may bring your VM into troubles.</b> |
| 95 | + * Creates an object without invoking the class's own constructors. |
| 96 | + * Tries ReflectionFactory first (safer), then falls back to sun.misc.Unsafe. |
35 | 97 | * |
36 | | - * @param clazz to instantiate |
| 98 | + * @param clazz the class to instantiate |
37 | 99 | * @return allocated Object |
| 100 | + * @throws IllegalArgumentException if the class cannot be instantiated |
38 | 101 | */ |
39 | 102 | public Object allocateInstance(Class<?> clazz) { |
40 | 103 | if (clazz == null || clazz.isInterface()) { |
41 | 104 | String name = clazz == null ? "null" : clazz.getName(); |
42 | 105 | throw new IllegalArgumentException("Unable to create instance of class: " + name); |
43 | 106 | } |
44 | 107 |
|
| 108 | + // Strategy 1: ReflectionFactory — serialization constructor cached per class |
| 109 | + if (reflectionFactory != null) { |
| 110 | + try { |
| 111 | + Constructor<?> ctor = serializationConstructorCache.get(clazz); |
| 112 | + if (ctor == null) { |
| 113 | + ctor = createSerializationConstructor(clazz); |
| 114 | + if (ctor != null) { |
| 115 | + serializationConstructorCache.put(clazz, ctor); |
| 116 | + return ctor.newInstance(); |
| 117 | + } |
| 118 | + } else { |
| 119 | + return ctor.newInstance(); |
| 120 | + } |
| 121 | + } catch (Exception ignored) { |
| 122 | + // ReflectionFactory failed for this class — fall through to Unsafe |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + // Strategy 2: sun.misc.Unsafe — raw allocation fallback |
| 127 | + if (sunUnsafe != null && unsafeAllocateInstance != null) { |
| 128 | + try { |
| 129 | + return ReflectionUtils.call(sunUnsafe, unsafeAllocateInstance, clazz); |
| 130 | + } catch (IllegalArgumentException e) { |
| 131 | + throw new IllegalArgumentException("Unable to create instance of class: " + clazz.getName(), e); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + throw new IllegalArgumentException("Unable to create instance of class: " + clazz.getName()); |
| 136 | + } |
| 137 | + |
| 138 | + private Constructor<?> createSerializationConstructor(Class<?> clazz) { |
45 | 139 | try { |
46 | | - return ReflectionUtils.call(sunUnsafe, allocateInstance, clazz); |
47 | | - } catch (IllegalArgumentException e) { |
48 | | - String name = clazz.getName(); |
49 | | - throw new IllegalArgumentException("Unable to create instance of class: " + name, e); |
| 140 | + Constructor<?> ctor = (Constructor<?>) newConstructorForSerialization.invoke(reflectionFactory, clazz, objectConstructor); |
| 141 | + if (ctor != null) { |
| 142 | + trySetAccessible(ctor); |
| 143 | + } |
| 144 | + return ctor; |
| 145 | + } catch (Exception e) { |
| 146 | + return null; |
50 | 147 | } |
51 | 148 | } |
52 | 149 | } |
0 commit comments