Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import st.orm.Data;
import st.orm.PK;
import st.orm.PersistenceException;
Expand All @@ -42,17 +42,24 @@
import st.orm.mapping.RecordType;

public final class DefaultORMReflectionImpl implements ORMReflection {
private static final Map<Class<?>, Optional<RecordType>> TYPE_CACHE = new ConcurrentHashMap<>();
private static final Map<Class<?>, Optional<RecordField>> PK_FIELD_CACHE = new ConcurrentHashMap<>();
private static final Map<Class<?>, Optional<Constructor<?>>> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
private static final Map<Method, Accessor> ACCESSOR_CACHE = new ConcurrentHashMap<>();

private interface Accessor {
Object get(Object receiver) throws Throwable;
}

/**
* Accessors per declaring class, keyed by method. {@link ClassValue} ties each entry to the lifetime of the
* declaring class, so cached reflection artifacts never outlive the class or its class loader.
*/
private static final ClassValue<ConcurrentMap<Method, Accessor>> ACCESSOR_CACHE = new ClassValue<>() {
@Override
protected ConcurrentMap<Method, Accessor> computeValue(@Nonnull Class<?> type) {
return new ConcurrentHashMap<>();
}
};

private static Accessor accessorFor(Method m) {
return ACCESSOR_CACHE.computeIfAbsent(m, method -> {
return ACCESSOR_CACHE.get(m.getDeclaringClass()).computeIfAbsent(m, method -> {
try {
Class<?> owner = method.getDeclaringClass();
MethodType mt = MethodType.methodType(method.getReturnType(), method.getParameterTypes());
Expand All @@ -73,13 +80,21 @@ private static Accessor accessorFor(Method m) {
});
}

/** Primary key field per record class; empty when the record declares no {@link PK} field. */
private static final ClassValue<Optional<RecordField>> PK_FIELD_CACHE = new ClassValue<>() {
@Override
protected Optional<RecordField> computeValue(@Nonnull Class<?> type) {
return TYPE_CACHE.get(type)
.orElseThrow(() -> new PersistenceException("Record type expected: %s.".formatted(type.getName())))
.fields().stream()
.filter(field -> field.isAnnotationPresent(PK.class))
.findFirst();
}
};

@Override
public Object getId(@Nonnull Data data) {
return PK_FIELD_CACHE.computeIfAbsent(data.getClass(), ignore ->
getRecordType(data.getClass()).fields().stream()
.filter(field -> field.isAnnotationPresent(PK.class))
.findFirst()
)
return PK_FIELD_CACHE.get(data.getClass())
.map(field -> invoke(field, data))
.orElseThrow(() -> new PersistenceException("No PK found for %s.".formatted(data.getClass().getName())));
}
Expand All @@ -89,13 +104,14 @@ public Object getRecordValue(@Nonnull Object record, int index) {
return invoke(getRecordType(record.getClass()).fields().get(index), record);
}

@Override
public Optional<RecordType> findRecordType(@Nonnull Class<?> type) {
return TYPE_CACHE.computeIfAbsent(type, ignore -> {
/** Record type descriptor per class; empty when the class is not a record. */
private static final ClassValue<Optional<RecordType>> TYPE_CACHE = new ClassValue<>() {
@Override
protected Optional<RecordType> computeValue(@Nonnull Class<?> type) {
if (!type.isRecord()) {
return empty();
}
return findCanonicalConstructor(type)
return CONSTRUCTOR_CACHE.get(type)
.map(constructor -> new RecordType(
type,
constructor,
Expand All @@ -115,11 +131,18 @@ public Optional<RecordType> findRecordType(@Nonnull Class<?> type) {
.toList()
)
);
});
}
};

@Override
public Optional<RecordType> findRecordType(@Nonnull Class<?> type) {
return TYPE_CACHE.get(type);
}

private Optional<Constructor<?>> findCanonicalConstructor(@Nonnull Class<?> type) {
return CONSTRUCTOR_CACHE.computeIfAbsent(type, ignore -> {
/** Canonical constructor per record class; empty when no constructor matches the record components. */
private static final ClassValue<Optional<Constructor<?>>> CONSTRUCTOR_CACHE = new ClassValue<>() {
@Override
protected Optional<Constructor<?>> computeValue(@Nonnull Class<?> type) {
RecordComponent[] components = type.getRecordComponents();
Constructor<?>[] constructors = type.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
Expand All @@ -140,8 +163,8 @@ private Optional<Constructor<?>> findCanonicalConstructor(@Nonnull Class<?> type
}
}
return empty();
});
}
}
};

@Override
public Class<?> getType(@Nonnull Object o) {
Expand Down Expand Up @@ -198,24 +221,26 @@ private boolean isPrimitiveDefaultValue(Object o) {
return false;
}

private static final ConcurrentHashMap<Class<?>, List<RecordComponent>> RECORD_COMPONENT_CACHE
= new ConcurrentHashMap<>();
/** Record components per record class, cached to avoid repeated expensive reflection lookups. */
private static final ClassValue<List<RecordComponent>> RECORD_COMPONENT_CACHE = new ClassValue<>() {
@Override
protected List<RecordComponent> computeValue(@Nonnull Class<?> recordType) {
if (!recordType.isRecord()) {
throw new IllegalArgumentException("The specified class %s is not a record type.".formatted(recordType.getName()));
}
return List.of(recordType.getRecordComponents());
}
};

/**
* Returns the record components for the specified record type. The result is cached to avoid repeated expensive
* reflection lookups.
* Returns the record components for the specified record type.
*
* @param recordType the record type to obtain the record components for.
* @return the record components for the specified record type.
* @throws IllegalArgumentException if the record type is not a record.
*/
private static List<RecordComponent> getRecordComponents(@Nonnull Class<?> recordType) {
return RECORD_COMPONENT_CACHE.computeIfAbsent(recordType, ignore -> {
if (!recordType.isRecord()) {
throw new IllegalArgumentException("The specified class %s is not a record type.".formatted(recordType.getName()));
}
return List.of(recordType.getRecordComponents());
});
return RECORD_COMPONENT_CACHE.get(recordType);
}

private boolean areRecordComponentsDefault(Object record) {
Expand All @@ -239,7 +264,7 @@ public boolean isSupportedType(@Nonnull Object clazz) {
return clazz instanceof Class<?>;
}

private boolean isNonnull(@Nonnull RecordComponent component) {
private static boolean isNonnull(@Nonnull RecordComponent component) {
return component.isAnnotationPresent(PK.class)
|| component.getType().isPrimitive()
|| Nullability.isNonNull(component, component.getAnnotatedType(), null, component.getDeclaringRecord());
Expand Down
118 changes: 118 additions & 0 deletions storm-core/src/main/java/st/orm/core/spi/ClassLoaderCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2024 - 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package st.orm.core.spi;

import static java.lang.System.identityHashCode;
import static java.util.Objects.requireNonNull;

import jakarta.annotation.Nonnull;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

/**
* A cache scoped to a class loader, holding the loader weakly and the value softly.
*
* <p>Values computed for a class loader inherently reference that loader: service instances and loaded classes keep
* their defining loader reachable. A cache that held such a value strongly would pin the loader for the lifetime of
* the JVM, which leaks every redeployed application in a container that discards class loaders. Holding the value
* softly breaks the cycle: once a loader is otherwise unreachable, the cache entry is the only path to it, and the
* collector clears the soft reference before memory runs out, after which the loader and its classes are reclaimed.
* Entries of live loaders survive until memory pressure clears them, in which case the value is recomputed on the
* next access.</p>
*
* <p>Loaders are compared by identity. Stale keys are drained from a reference queue on each access.</p>
*
* @param <V> the type of the cached value; must not be {@code null}.
*/
final class ClassLoaderCache<V> {

/** A weak reference to a class loader with identity-based equality, usable as a map key. */
private static final class LoaderKey extends WeakReference<ClassLoader> {
private final int hash;

/** Creates a lookup key that is not registered with a reference queue. */
LoaderKey(@Nonnull ClassLoader loader) {
super(loader);
this.hash = identityHashCode(loader);
}

/** Creates a key for insertion, registered with the queue for cleanup when the loader is collected. */
LoaderKey(@Nonnull ClassLoader loader, @Nonnull ReferenceQueue<ClassLoader> queue) {
super(loader, queue);
this.hash = identityHashCode(loader);
}

@Override
public int hashCode() {
return hash;
}

@Override
public boolean equals(Object other) {
if (this == other) {
// Required to locate this key after its referent is cleared, so a stale entry can be removed.
return true;
}
return other instanceof LoaderKey key && get() != null && get() == key.get();
}
}

private final ReferenceQueue<ClassLoader> queue = new ReferenceQueue<>();
private final Map<LoaderKey, SoftReference<V>> map = new ConcurrentHashMap<>();

/**
* Returns the value for the given class loader, computing and caching it if absent or already reclaimed.
*
* <p>The compute function may run while holding an internal lock for the loader's entry; concurrent lookups of
* the same loader wait for the computation, matching {@link ConcurrentHashMap#computeIfAbsent} semantics.</p>
*
* @param loader the class loader to scope the value to.
* @param compute the function that computes the value; must not return {@code null}.
* @return the cached or computed value.
*/
V computeIfAbsent(@Nonnull ClassLoader loader, @Nonnull Function<? super ClassLoader, ? extends V> compute) {
drainQueue();
var reference = map.get(new LoaderKey(loader));
V value = reference == null ? null : reference.get();
while (value == null) {
var updated = map.compute(new LoaderKey(loader, queue), (key, existing) ->
existing != null && existing.get() != null
? existing
: new SoftReference<>(requireNonNull(compute.apply(loader),
"Compute function must not return null.")));
value = updated.get();
}
return value;
}

/**
* Removes stale entries whose class loader has been collected. Only the exact enqueued key matches its map
* entry, so a live entry for another loader is never removed.
*/
private void drainQueue() {
Reference<? extends ClassLoader> stale;
while ((stale = queue.poll()) != null) {
if (stale instanceof LoaderKey key) {
map.remove(key);
}
}
}
}
12 changes: 7 additions & 5 deletions storm-core/src/main/java/st/orm/core/spi/Instantiators.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import st.orm.mapping.Instantiator;

/**
Expand All @@ -38,9 +36,13 @@
*/
public final class Instantiators {

/** Instantiators per class loader, keyed by the record type they construct. */
private static final ConcurrentMap<ClassLoader, Map<Class<?>, Instantiator<?>>> INSTANTIATOR_CACHE =
new ConcurrentHashMap<>();
/**
* Instantiators per class loader, keyed by the record type they construct. The registered instantiators and
* record types keep their class loader reachable, so the registry is scoped to the loader's lifetime via
* {@link ClassLoaderCache} rather than pinned for the lifetime of the JVM.
*/
private static final ClassLoaderCache<Map<Class<?>, Instantiator<?>>> INSTANTIATOR_CACHE =
new ClassLoaderCache<>();

private Instantiators() {
}
Expand Down
26 changes: 0 additions & 26 deletions storm-core/src/main/java/st/orm/core/spi/Orderable.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,6 @@ static <T extends Orderable<?>> List<T> sort(@Nonnull List<T> orderables) {
return OrderableHelper.sort(orderables);
}

/**
* Sorts the provided {@code Orderable} instances based on the ordering constraints defined by the orderable
* constraints.
*
* @param <T> the type parameter for the {@code Orderable} instances.
* @param orderables the orderable instances to sort.
* @param cache use caching for improved performance.
* @return the sorted orderable instances.
*/
static <T extends Orderable<?>> List<T> sort(@Nonnull List<T> orderables, boolean cache) {
return OrderableHelper.sort(orderables, cache);
}

/**
* Sorts the provided {@code Orderable} instances based on the ordering constraints defined by the orderable
* constraints.
Expand All @@ -112,17 +99,4 @@ static <T extends Orderable<?>> List<T> sort(@Nonnull List<T> orderables, boolea
static <T extends Orderable<?>> Stream<T> sort(@Nonnull Stream<T> orderableStream) {
return OrderableHelper.sort(orderableStream);
}

/**
* Sorts the provided {@code Orderable} instances based on the ordering constraints defined by the orderable
* constraints.
*
* @param <T> the type parameter for the {@code Orderable} instances.
* @param cache use caching for improved performance.
* @param orderableStream the orderable instances to sort.
* @return the sorted orderable instances.
*/
static <T extends Orderable<?>> Stream<T> sort(@Nonnull Stream<T> orderableStream, boolean cache) {
return OrderableHelper.sort(orderableStream, cache);
}
}
Loading
Loading