Problem
ObjectMapperFactory.getObjectMapper has no dedicated path for "leaf value types" like UUID, String, BigDecimal, boxed primitives, and java.time.*. When a query maps to one of these as a scalar (e.g. query.getSingleResult(UUID.class)), the factory falls through every special case and lands in the constructor-scan loop:
for (Constructor<?> constructor : type.getDeclaredConstructors()) {
if (constructor.getParameterTypes().length == columnCount) {
return Optional.of(wrapConstructor(constructor));
}
}
It then calls setAccessible(true) on the first matching constructor. For UUID.class with columnCount == 1, the only 1-arg constructor is the private UUID(byte[]) in java.base, and the JDK module system refuses access:
java.lang.reflect.InaccessibleObjectException: Unable to make private java.util.UUID(byte[]) accessible:
module java.base does not "opens java.util" to unnamed module @685f4c2e
at st.orm.core.template.impl.ObjectMapperFactory.construct(ObjectMapperFactory.java:172)
For other leaf types the same path "works" only by luck of getDeclaredConstructors() ordering — e.g. String.class happens to land on the public String(String) copy-constructor, and we pay one allocation per row to wrap a value that's already correct. For BigDecimal.class and others, behavior depends on whichever 1-arg constructor the JVM returns first.
Affected APIs
Any call that maps a single column to a leaf type, including:
- Query.getSingleResult(UUID.class) / getResultStream(UUID.class)
- Query.getRefStream(EntityType.class, UUID.class) (PK type lookup)
- Single-column projections / aggregates with these target types
Proposed fix
Add a ValueMapper next to PrimitiveMapper / EnumMapper that provides a single-column pass-through for types QueryImpl.readColumnValue already returns directly:
- Boxed primitives: Boolean, Byte, Short, Integer, Long, Float, Double
- String, BigDecimal, ByteBuffer, UUID
- java.util.Date, Calendar, java.sql.Date, Time, Timestamp
- LocalDateTime, LocalDate, LocalTime, Instant, OffsetDateTime, ZonedDateTime
Dispatch to ValueMapper.getFactory(columnCount, type) in ObjectMapperFactory.getObjectMapper before the constructor-scan fallback. The mapper just returns args[0] — no reflection, no allocation, no module-access issues.
Benefits
- Fixes InaccessibleObjectException for UUID and any other JDK type whose 1-arg constructors are private.
- Removes brittle reliance on getDeclaredConstructors() ordering for String, BigDecimal, etc.
- Eliminates one allocation per row for String.class queries (no more new String(s)).
- Aligns scalar value mapping with what readColumnValue already produces.
Test coverage
Add regression tests under EntityRepositoryIntegrationTest (or a new dedicated test) for:
- getSingleResult(UUID.class) on a single UUID column.
- getResultStream(UUID.class) on a multi-row UUID column.
Problem
ObjectMapperFactory.getObjectMapper has no dedicated path for "leaf value types" like UUID, String, BigDecimal, boxed primitives, and java.time.*. When a query maps to one of these as a scalar (e.g. query.getSingleResult(UUID.class)), the factory falls through every special case and lands in the constructor-scan loop:
It then calls setAccessible(true) on the first matching constructor. For UUID.class with columnCount == 1, the only 1-arg constructor is the private UUID(byte[]) in java.base, and the JDK module system refuses access:
For other leaf types the same path "works" only by luck of getDeclaredConstructors() ordering — e.g. String.class happens to land on the public String(String) copy-constructor, and we pay one allocation per row to wrap a value that's already correct. For BigDecimal.class and others, behavior depends on whichever 1-arg constructor the JVM returns first.
Affected APIs
Any call that maps a single column to a leaf type, including:
Proposed fix
Add a ValueMapper next to PrimitiveMapper / EnumMapper that provides a single-column pass-through for types QueryImpl.readColumnValue already returns directly:
Dispatch to ValueMapper.getFactory(columnCount, type) in ObjectMapperFactory.getObjectMapper before the constructor-scan fallback. The mapper just returns args[0] — no reflection, no allocation, no module-access issues.
Benefits
Test coverage
Add regression tests under EntityRepositoryIntegrationTest (or a new dedicated test) for: