Skip to content

Commit

Permalink
Merge pull request #127 from ProjectMapK/fix/value-creator-cache
Browse files Browse the repository at this point in the history
ValueCreator modified to cache to KotlinValueInstantiator
  • Loading branch information
k163377 authored Aug 5, 2023
2 parents b41da2f + 0a20f65 commit 673ae04
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.github.projectmapk.jackson.module.kogera.deser.value_instantiator;

import kotlin.jvm.functions.Function0;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.ref.SoftReference;

// ported from kotlin.reflect.jvm.internal.ReflectProperties 887dc to use LazySoft
class ReflectProperties {
static abstract class Val<T> {
private static final Object NULL_VALUE = new Object() {
};

@SuppressWarnings({"UnusedParameters", "unused"})
final T getValue(Object instance, Object metadata) {
return invoke();
}

abstract T invoke();

protected Object escape(T value) {
return value == null ? NULL_VALUE : value;
}

@SuppressWarnings("unchecked")
protected T unescape(Object value) {
return value == NULL_VALUE ? null : (T) value;
}
}

// A delegate for a lazy property on a soft reference, whose initializer may be invoked multiple times
// including simultaneously from different threads
static class LazySoftVal<T> extends Val<T> implements Function0<T> {
private final Function0<T> initializer;
private volatile SoftReference<Object> value = null;

LazySoftVal(@Nullable T initialValue, @NotNull Function0<T> initializer) {
this.initializer = initializer;
if (initialValue != null) {
this.value = new SoftReference<Object>(escape(initialValue));
}
}

@Override
public T invoke() {
SoftReference<Object> cached = value;
if (cached != null) {
Object result = cached.get();
if (result != null) {
return unescape(result);
}
}

T result = initializer.invoke();
value = new SoftReference<Object>(escape(result));

return result;
}
}

@NotNull
static <T> LazySoftVal<T> lazySoft(@Nullable T initialValue, @NotNull Function0<T> initializer) {
return new LazySoftVal<T>(initialValue, initializer);
}

@NotNull
static <T> LazySoftVal<T> lazySoft(@NotNull Function0<T> initializer) {
return lazySoft(null, initializer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,19 @@ package io.github.projectmapk.jackson.module.kogera

import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
import com.fasterxml.jackson.databind.util.LRUMap
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.ConstructorValueCreator
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.MethodValueCreator
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.ValueCreator
import io.github.projectmapk.jackson.module.kogera.ser.ValueClassBoxConverter
import java.io.Serializable
import java.lang.reflect.Constructor
import java.lang.reflect.Executable
import java.lang.reflect.Method
import java.util.Optional

internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
companion object {
// Increment is required when properties that use LRUMap are changed.
@Suppress("ConstPropertyName")
private const val serialVersionUID = 1L
private const val serialVersionUID = 2L
}

// This cache is used for both serialization and deserialization, so reserve a larger size from the start.
private val classCache = LRUMap<Class<*>, JmClass>(reflectionCacheSize, reflectionCacheSize)
private val creatorCache: LRUMap<Executable, ValueCreator<*>>

// Initial size is 0 because the value class is not always used
private val valueClassReturnTypeCache: LRUMap<AnnotatedMethod, Optional<Class<*>>> =
Expand All @@ -32,21 +25,6 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
private val valueClassBoxConverterCache: LRUMap<Class<*>, ValueClassBoxConverter<*, *>> =
LRUMap(0, reflectionCacheSize)

init {
// The current default value of reflectionCacheSize is 512.
// If less than 512 is specified, initialEntries shall be half of the reflectionCacheSize,
// since it is assumed that the situation does not require a large amount of space from the beginning.
// Conversely, if reflectionCacheSize is increased,
// the amount of cache is considered to be a performance bottleneck,
// and therefore reflectionCacheSize is used as is for initialEntries.
val initialEntries = if (reflectionCacheSize <= KotlinModule.Builder.reflectionCacheSizeDefault) {
reflectionCacheSize / 2
} else {
reflectionCacheSize
}
creatorCache = LRUMap(initialEntries, reflectionCacheSize)
}

fun getJmClass(clazz: Class<*>): JmClass? {
return classCache[clazz] ?: run {
val kmClass = clazz.toKmClass() ?: return null
Expand All @@ -66,35 +44,6 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
}
}

/**
* return null if declaringClass is not kotlin class
*/
fun valueCreatorFromJava(creator: Executable): ValueCreator<*>? = when (creator) {
is Constructor<*> -> {
creatorCache.get(creator)
?: run {
getJmClass(creator.declaringClass)?.let {
val value = ConstructorValueCreator(creator, it)
creatorCache.putIfAbsent(creator, value) ?: value
}
}
}

is Method -> {
creatorCache.get(creator)
?: run {
getJmClass(creator.declaringClass)?.let {
val value = MethodValueCreator<Any?>(creator, it)
creatorCache.putIfAbsent(creator, value) ?: value
}
}
}

else -> throw IllegalStateException(
"Expected a constructor or method to create a Kotlin object, instead found ${creator.javaClass.name}"
)
} // we cannot reflect this method so do the default Java-ish behavior

private fun AnnotatedMethod.getValueClassReturnType(): Class<*>? {
val getter = this.member.apply {
// If the return value of the getter is a value class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.ConstructorValueCreator
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.MethodValueCreator
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.ValueCreator
import java.lang.reflect.Constructor
import java.lang.reflect.Executable
import java.lang.reflect.Method

private fun JsonMappingException.wrapWithPath(refFrom: Any?, refFieldName: String) =
JsonMappingException.wrapWithPath(this, refFrom, refFieldName)
Expand All @@ -29,13 +33,25 @@ internal class KotlinValueInstantiator(
private fun JavaType.requireEmptyValue() =
(nullToEmptyCollection && this.isCollectionLikeType) || (nullToEmptyMap && this.isMapLikeType)

private val valueCreator: ValueCreator<*>? by ReflectProperties.lazySoft {
val creator = _withArgsCreator.annotated as Executable
val jmClass = cache.getJmClass(creator.declaringClass) ?: return@lazySoft null

when (creator) {
is Constructor<*> -> ConstructorValueCreator(creator, jmClass)
is Method -> MethodValueCreator<Any?>(creator, jmClass)
else -> throw IllegalStateException(
"Expected a constructor or method to create a Kotlin object, instead found ${creator.javaClass.name}"
)
}
} // we cannot reflect this method so do the default Java-ish behavior

override fun createFromObjectWith(
ctxt: DeserializationContext,
props: Array<out SettableBeanProperty>,
buffer: PropertyValueBuffer
): Any? {
val valueCreator: ValueCreator<*> = cache.valueCreatorFromJava(_withArgsCreator.annotated as Executable)
?: return super.createFromObjectWith(ctxt, props, buffer)
val valueCreator: ValueCreator<*> = valueCreator ?: return super.createFromObjectWith(ctxt, props, buffer)

val bucket = valueCreator.generateBucket()

Expand Down

0 comments on commit 673ae04

Please sign in to comment.