diff --git a/microsphere-redis-parent/pom.xml b/microsphere-redis-parent/pom.xml index 2c7f8c0..9a61329 100644 --- a/microsphere-redis-parent/pom.xml +++ b/microsphere-redis-parent/pom.xml @@ -19,7 +19,10 @@ Microsphere Redis Parent - ${revision} + 0.0.1 + 3.1.7 + 1.19.5 + 1.37 @@ -34,6 +37,31 @@ import + + + io.smallrye + jandex + ${jandex.version} + + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + diff --git a/microsphere-redis-replicator-spring/pom.xml b/microsphere-redis-replicator-spring/pom.xml index 6e12a2d..57d81ae 100644 --- a/microsphere-redis-replicator-spring/pom.xml +++ b/microsphere-redis-replicator-spring/pom.xml @@ -28,7 +28,7 @@ io.github.microsphere-projects microsphere-spring-context - ${revision} + 0.0.1 @@ -76,7 +76,7 @@ io.github.microsphere-projects microsphere-spring-test - ${revision} + 0.0.1 test @@ -92,6 +92,57 @@ test - + + org.springframework.boot + spring-boot-starter-data-redis + test + + + + org.testcontainers + junit-jupiter + test + + + org.mockito + mockito-inline + test + + + + org.openjdk.jmh + jmh-core + + + org.openjdk.jmh + jmh-generator-annprocess + provided + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + benchmarks + + + org.openjdk.jmh.Main + + + + + + + + \ No newline at end of file diff --git a/microsphere-redis-replicator-spring/src/main/java/io/microsphere/redis/replicator/spring/RedisCommandReplicator.java b/microsphere-redis-replicator-spring/src/main/java/io/microsphere/redis/replicator/spring/RedisCommandReplicator.java index 2bbaa5c..15e2471 100644 --- a/microsphere-redis-replicator-spring/src/main/java/io/microsphere/redis/replicator/spring/RedisCommandReplicator.java +++ b/microsphere-redis-replicator-spring/src/main/java/io/microsphere/redis/replicator/spring/RedisCommandReplicator.java @@ -1,17 +1,23 @@ package io.microsphere.redis.replicator.spring; -import io.microsphere.redis.spring.event.RedisCommandEvent; import io.microsphere.redis.replicator.spring.event.RedisCommandReplicatedEvent; +import io.microsphere.redis.replicator.spring.event.handler.RedisCommandReplicatedEventHandler; +import io.microsphere.redis.spring.event.RedisCommandEvent; +import io.microsphere.spring.util.SpringFactoriesLoaderUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.util.ReflectionUtils; import java.lang.reflect.Method; import java.util.function.Function; +import static io.microsphere.redis.replicator.spring.config.RedisReplicatorConfiguration.CONSUMER_EVENT_HANDLE_PROPERTY_NAME; +import static io.microsphere.redis.replicator.spring.event.handler.RedisCommandReplicatedEventHandler.EventHandleName.REFLECT; import static io.microsphere.redis.spring.beans.Wrapper.tryUnwrap; import static io.microsphere.redis.spring.metadata.RedisMetadataRepository.findWriteCommandMethod; import static io.microsphere.redis.spring.metadata.RedisMetadataRepository.getRedisCommandBindingFunction; @@ -23,7 +29,8 @@ * @author Mercy * @since 1.0.0 */ -public class RedisCommandReplicator implements ApplicationListener { +public class RedisCommandReplicator implements ApplicationListener, + ApplicationContextAware { private static final Logger logger = LoggerFactory.getLogger(RedisCommandReplicator.class); @@ -35,6 +42,7 @@ public RedisCommandReplicator(RedisConnectionFactory redisConnectionFactory) { this.redisConnectionFactory = tryUnwrap(redisConnectionFactory, RedisConnectionFactory.class); } + @Override public void onApplicationEvent(RedisCommandReplicatedEvent event) { try { @@ -53,12 +61,26 @@ private void handleRedisCommandEvent(RedisCommandReplicatedEvent event) throws T Object[] args = redisCommandEvent.getArgs(); Function bindingFunction = getRedisCommandBindingFunction(interfaceNme); Object redisCommandObject = bindingFunction.apply(redisConnection); - // TODO: Native method implementation - ReflectionUtils.invokeMethod(method, redisCommandObject, args); + // Native method implementation + // ReflectionUtils.invokeMethod(method, redisCommandObject, args); + eventHandler.handleEvent(method, redisCommandObject, args); } } private RedisConnection getRedisConnection() { return redisConnectionFactory.getConnection(); } + + RedisCommandReplicatedEventHandler eventHandler; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + String eventHandleType = applicationContext.getEnvironment() + .getProperty(CONSUMER_EVENT_HANDLE_PROPERTY_NAME, String.class, REFLECT.name()); + eventHandler = SpringFactoriesLoaderUtils.loadFactories(applicationContext, RedisCommandReplicatedEventHandler.class) + .stream() + .filter(eventHandle -> eventHandle.name().equals(eventHandleType)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No eventHandle found")); + } } diff --git a/microsphere-redis-replicator-spring/src/main/java/io/microsphere/redis/replicator/spring/config/RedisReplicatorConfiguration.java b/microsphere-redis-replicator-spring/src/main/java/io/microsphere/redis/replicator/spring/config/RedisReplicatorConfiguration.java index b7859b1..f98ac97 100644 --- a/microsphere-redis-replicator-spring/src/main/java/io/microsphere/redis/replicator/spring/config/RedisReplicatorConfiguration.java +++ b/microsphere-redis-replicator-spring/src/main/java/io/microsphere/redis/replicator/spring/config/RedisReplicatorConfiguration.java @@ -63,10 +63,11 @@ public class RedisReplicatorConfiguration implements ApplicationListener redisContainer; + RedisStringCommands redisStringCommands; + Method method; + RedisCommandReplicatedEventHandler methodHandleHandler; + RedisCommandReplicatedEventHandler reflectHandler; + + byte[] key; + byte[] value; + + @Setup(Level.Trial) + public void setup() throws Exception { + // 启动 PostgreSQL 容器 + redisContainer = new GenericContainer<>(parse("redis:latest")).withExposedPorts(6379); + redisContainer.start(); + + String interfaceName = "org.springframework.data.redis.connection.RedisStringCommands"; + String methodName = "set"; + String[] parameterTypes = new String[]{"[B", "[B"}; + method = findWriteCommandMethod(interfaceName, methodName, parameterTypes); + + LettuceConnectionFactory redisConnectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisContainer.getHost(), redisContainer.getFirstMappedPort())); + redisConnectionFactory.afterPropertiesSet(); + RedisConnection connection = redisConnectionFactory.getConnection(); + redisStringCommands = connection.stringCommands(); + + methodHandleHandler = new MethodHandleRedisCommandReplicatedEventHandler(); + reflectHandler = new ReflectRedisCommandReplicatedEventHandler(); + key = "key".getBytes(StandardCharsets.UTF_8); + value = "value".getBytes(StandardCharsets.UTF_8); + } + + @Benchmark + public void benchmarkDirect() throws Throwable { + redisStringCommands.set(key, value); + } + + @Benchmark + public void benchmarkMethodHandleRedisCommandReplicatedEventHandler() throws Throwable { + methodHandleHandler.handleEvent(method, redisStringCommands, key, value); + } + + @Benchmark + public void benchmarkReflectRedisCommandReplicatedEventHandler() throws Throwable { + reflectHandler.handleEvent(method, redisStringCommands, key, value); + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + if (redisContainer != null) { + redisContainer.stop(); + } + } +} diff --git a/microsphere-redis-replicator-spring/src/test/java/io/microsphere/redis/benchmark/runner/BenchmarkRunner.java b/microsphere-redis-replicator-spring/src/test/java/io/microsphere/redis/benchmark/runner/BenchmarkRunner.java new file mode 100644 index 0000000..d018d22 --- /dev/null +++ b/microsphere-redis-replicator-spring/src/test/java/io/microsphere/redis/benchmark/runner/BenchmarkRunner.java @@ -0,0 +1,20 @@ +package io.microsphere.redis.benchmark.runner; + +import io.microsphere.redis.benchmark.RedisCommandReplicatedEventHandlerBenchmark; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +public class BenchmarkRunner { + public static void main(String[] args) throws RunnerException { + Options options = new OptionsBuilder() + .include(RedisCommandReplicatedEventHandlerBenchmark.class.getSimpleName()) + .warmupIterations(3) + .measurementIterations(5) + .forks(1) + .build(); + + new Runner(options).run(); + } +} diff --git a/microsphere-redis-replicator-spring/src/test/java/io/microsphere/redis/replicator/spring/HandleRedisCommandReplicatedEventTest.java b/microsphere-redis-replicator-spring/src/test/java/io/microsphere/redis/replicator/spring/HandleRedisCommandReplicatedEventTest.java new file mode 100644 index 0000000..33a1b3d --- /dev/null +++ b/microsphere-redis-replicator-spring/src/test/java/io/microsphere/redis/replicator/spring/HandleRedisCommandReplicatedEventTest.java @@ -0,0 +1,115 @@ +package io.microsphere.redis.replicator.spring; + +import io.microsphere.redis.replicator.spring.event.RedisCommandReplicatedEvent; +import io.microsphere.redis.replicator.spring.event.handler.RedisCommandReplicatedEventHandler; +import io.microsphere.redis.spring.event.RedisCommandEvent; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.lang.reflect.Method; + +import static io.microsphere.redis.spring.metadata.RedisMetadataRepository.findWriteCommandMethod; +import static io.microsphere.redis.spring.serializer.RedisCommandEventSerializer.VERSION_V1; +import static org.assertj.core.api.Assertions.assertThat; +import static org.testcontainers.utility.DockerImageName.parse; + +@Testcontainers +class HandleRedisCommandReplicatedEventTest { + @Container + static GenericContainer redisContainer = new GenericContainer<>(parse("redis:latest")).withExposedPorts(6379); + + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.redis.host", () -> redisContainer.getHost()); + registry.add("spring.redis.port", () -> redisContainer.getMappedPort(6379)); + } + + @Nested + @SpringJUnitConfig(classes = { + RedisCommandReplicator.class, + RedisAutoConfiguration.class + }) + @TestPropertySource(properties = { + "microsphere.redis.replicator.consumer.event.handler=REFLECT" + }) + class ReflectEventHandleTest { + @Autowired + ApplicationContext applicationContext; + + @Autowired + RedisTemplate redisTemplate; + + @Autowired + RedisCommandReplicator redisCommandReplicator; + + @Test + void invokeMethodByReflect() { + String key = "Reflect"; + String expected = "Reflect"; + RedisCommandReplicatedEvent redisCommandReplicatedEvent = getRedisCommandReplicatedEvent(key, expected); + applicationContext.publishEvent(redisCommandReplicatedEvent); + + assertThat(redisCommandReplicator.eventHandler.name()) + .isEqualTo(RedisCommandReplicatedEventHandler.EventHandleName.REFLECT.name()); + String value = redisTemplate.opsForValue().get(key); + assertThat(value).isEqualTo(expected); + } + } + + @Nested + @SpringJUnitConfig(classes = { + RedisCommandReplicator.class, + RedisAutoConfiguration.class + }) + @TestPropertySource(properties = { + "microsphere.redis.replicator.consumer.event.handler=METHOD_HANDLE" + }) + class MethodHandleEventHandleTest { + + @Autowired + ApplicationContext applicationContext; + + @Autowired + RedisTemplate redisTemplate; + + @Autowired + RedisCommandReplicator redisCommandReplicator; + + @Test + void invokeMethodByMethodHandle() { + String key = "MethodHandle"; + String expected = "MethodHandle"; + RedisCommandReplicatedEvent redisCommandReplicatedEvent = getRedisCommandReplicatedEvent(key, expected); + applicationContext.publishEvent(redisCommandReplicatedEvent); + + assertThat(redisCommandReplicator.eventHandler.name()) + .isEqualTo(RedisCommandReplicatedEventHandler.EventHandleName.METHOD_HANDLE.name()); + String value = redisTemplate.opsForValue().get(key); + assertThat(value).isEqualTo(expected); + } + } + + + private static RedisCommandReplicatedEvent getRedisCommandReplicatedEvent(String key, String value) { + String interfaceName = "org.springframework.data.redis.connection.RedisStringCommands"; + String methodName = "set"; + String[] parameterTypes = new String[]{"[B", "[B"}; + Method method = findWriteCommandMethod(interfaceName, methodName, parameterTypes); + + RedisCommandEvent redisCommandEvent = RedisCommandEvent.Builder.source("test").applicationName("test-application").method(method).args(key.getBytes(), value.getBytes()).serializationVersion(VERSION_V1).build(); + + return new RedisCommandReplicatedEvent(redisCommandEvent, "domain"); + } + +} diff --git a/microsphere-redis-spring/pom.xml b/microsphere-redis-spring/pom.xml index ed300f1..e3de934 100644 --- a/microsphere-redis-spring/pom.xml +++ b/microsphere-redis-spring/pom.xml @@ -66,6 +66,10 @@ snakeyaml + + io.smallrye + jandex + junit @@ -82,7 +86,7 @@ io.github.microsphere-projects microsphere-spring-test - ${revision} + 0.0.1 test @@ -98,5 +102,16 @@ test + + org.mockito + mockito-inline + test + + + + org.junit.jupiter + junit-jupiter + test + \ No newline at end of file diff --git a/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/RedisCommandsMethodHandles.java b/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/RedisCommandsMethodHandles.java new file mode 100644 index 0000000..f5f31b5 --- /dev/null +++ b/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/RedisCommandsMethodHandles.java @@ -0,0 +1,298 @@ +package io.microsphere.redis.spring.metadata; + +import io.microsphere.redis.spring.metadata.exception.MethodHandleNotFoundException; +import io.microsphere.util.ClassLoaderUtils; +import org.apache.commons.lang3.StringUtils; +import org.jboss.jandex.ArrayType; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.MethodParameterInfo; +import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.Type; +import org.jboss.jandex.VoidType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.connection.RedisCommands; +import org.springframework.data.redis.connection.RedisConnectionCommands; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.connection.RedisHashCommands; +import org.springframework.data.redis.connection.RedisHyperLogLogCommands; +import org.springframework.data.redis.connection.RedisKeyCommands; +import org.springframework.data.redis.connection.RedisListCommands; +import org.springframework.data.redis.connection.RedisPubSubCommands; +import org.springframework.data.redis.connection.RedisScriptingCommands; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.connection.RedisSetCommands; +import org.springframework.data.redis.connection.RedisStreamCommands; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.connection.RedisTxCommands; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamOffset; + +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static io.microsphere.redis.spring.metadata.RedisMetadataRepository.redisCommandMethodsCache; +import static java.util.stream.Collectors.toMap; + +public class RedisCommandsMethodHandles { + + private static final Logger logger = LoggerFactory.getLogger(RedisCommandsMethodHandles.class); + + private static final MethodHandles.Lookup PUBLIC_LOOKUP = MethodHandles.publicLookup(); + + private static final ClassLoader CURRENT_CLASS_LOADER = RedisCommandsMethodHandles.class.getClassLoader(); + + private static final List> TARGET_CLASSES; + private static final Index REDIS_COMMANDS_INDEX; + private static final Map METHOD_HANDLE_MAP; + private static final Map METHOD_METHOD_INFO_MAP; + private static final Map METHOD_METHOD_HANDLE_MAP; + + private RedisCommandsMethodHandles() { + } + + /** + * find MethodHandle from METHOD_HANDLE_MAP + * + * @param methodSignature {@link MethodInfo#toString()} + * @return a MethodHandle + * @throws MethodHandleNotFoundException + */ + public static MethodHandle getMethodHandleBy(String methodSignature) throws MethodHandleNotFoundException { + MethodHandle methodHandle = METHOD_HANDLE_MAP.get(methodSignature); + if (Objects.isNull(methodHandle)) { + logger.error("can't find MethodHandle from RedisCommands methodSignature:{}", methodSignature); + throw new MethodHandleNotFoundException("can't find MethodHandle from RedisCommands", methodSignature); + } + return methodHandle; + } + + public static MethodHandle getMethodHandleBy(Method method) throws MethodHandleNotFoundException { + MethodHandle methodHandle = METHOD_METHOD_HANDLE_MAP.get(method); + if (Objects.isNull(methodHandle)) { + logger.error("can't find MethodHandle from RedisCommands methodSignature:{}", method.getName()); + throw new MethodHandleNotFoundException("can't find MethodHandle from RedisCommands", method.toString()); + } + return methodHandle; + } + + /** + * find MethodInfo from METHOD_MAP + * + * @param method + * @return {@link MethodInfo#toString()} + */ + public static String transferMethodToMethodSignature(Method method) { + MethodInfo methodInfo = METHOD_METHOD_INFO_MAP.get(method); + if (Objects.isNull(methodInfo)) { + throw new IllegalArgumentException(); + } + return methodInfo.toString(); + } + + /** + * get all public methods from {@link RedisCommands}
+ * exclude {@link RedisCommands#execute}
+ * exclude private lambda method from {@link RedisStreamCommands} + *
  • lambda$xDel$1
  • + *
  • lambda$xAck$0
  • + * + * @return List of RedisCommands all MethodInfo(include super interface) + */ + static List getAllRedisCommandMethods() { + return REDIS_COMMANDS_INDEX.getClassByName(RedisCommands.class) + .interfaceNames() + .stream() + .map(REDIS_COMMANDS_INDEX::getClassByName) + .flatMap(classInfo -> classInfo.methods().stream()) + .filter(methodInfo -> Modifier.isPublic(methodInfo.flags())) + .collect(Collectors.toList()); + } + + static Map initRedisCommandMethodHandle() { + return getAllRedisCommandMethods() + .stream() + .map(methodInfo -> new MethodRecord(methodInfo.toString(), findMethodHandleBy(methodInfo))) + .collect(toMap(MethodRecord::methodSignature, MethodRecord::methodHandle)); + } + + static MethodHandle findMethodHandleBy(MethodInfo methodInfo) { + Class klass = getClassBy(ClassType.create(methodInfo.declaringClass().name())); + + String methodName = methodInfo.name(); + + MethodType methodType = getMethodType(methodInfo); + try { + return RedisCommandsMethodHandles.PUBLIC_LOOKUP.findVirtual(klass, methodName, methodType); + } catch (NoSuchMethodException | IllegalAccessException e) { + logger.error("Error occurred when find MethodHandle.\n methodInfo:{}", methodInfo, e); + throw new RuntimeException(e); + } + } + + private static MethodType getMethodType(MethodInfo methodInfo) { + Class returnTypeKlass = getClassBy(methodInfo.returnType()); + + MethodParameterInfo[] array = methodInfo.parameters().toArray(new MethodParameterInfo[]{}); + Class[] parameterKlass = new Class[array.length]; + for (int i = 0; i < array.length; i++) { + parameterKlass[i] = getClassBy(array[i].type()); + } + + return MethodType.methodType(returnTypeKlass, parameterKlass); + } + + static Class getClassBy(Type type) { + if (type instanceof VoidType) { + return void.class; + } + + if (type instanceof PrimitiveType) { + return TypeHelper.PRIMITIVE_TYPE_CLASS_TABLE.get(type.asPrimitiveType().primitive()); + } + + if (type instanceof ArrayType) { + ArrayType arrayType = type.asArrayType(); + // NOTE: arrayType.elementType().name() + // example java.lang.String + // when use jdk21 local() value is "String" prefix() is "java.lang" + // when use jdk8 local() value is "java.lang.String" prefix() is null + String local = arrayType.elementType().name().local(); + String elementType; + if (local.lastIndexOf(".") != -1) { + elementType = local.substring(local.lastIndexOf(".") + 1); + } else { + elementType = local; + } + // NOTE: use String.repeat() when use jdk11+ and remove Apache commons lang3 StringUtils dependency + String repeat = StringUtils.repeat("[]", arrayType.dimensions()); + Class klass = TypeHelper.ARRAY_TYPE_CLASS_TABLE.get(elementType + repeat); + + if (Objects.isNull(klass)) { + throw new RuntimeException("need to add Class"); + } + return klass; + } + + return ClassLoaderUtils.loadClass(type.name().toString(), CURRENT_CLASS_LOADER); + } + + static Map initRedisCommandMethodInfo() { + return redisCommandMethodsCache.values() + .stream() + .collect(toMap( + Function.identity(), + method -> REDIS_COMMANDS_INDEX.getClassByName(method.getDeclaringClass()) + .method(method.getName(), getParameterTypes(method.getParameterTypes())) + )); + } + + private static Type[] getParameterTypes(Class[] parameterTypes) { + return Arrays.stream(parameterTypes) + .map(parameterType -> { + if (parameterType.isArray()) { + return Type.create(DotName.createSimple(parameterType), Type.Kind.ARRAY); + } else { + return Type.create(DotName.createSimple(parameterType), Type.Kind.CLASS); + } + }).toArray(Type[]::new); + } + + static { + // NOTE: use List.of() to simplify the initial logic + TARGET_CLASSES = new ArrayList<>(); + TARGET_CLASSES.add(RedisCommands.class); + TARGET_CLASSES.add(RedisKeyCommands.class); + TARGET_CLASSES.add(RedisStringCommands.class); + TARGET_CLASSES.add(RedisListCommands.class); + TARGET_CLASSES.add(RedisSetCommands.class); + TARGET_CLASSES.add(RedisZSetCommands.class); + TARGET_CLASSES.add(RedisHashCommands.class); + TARGET_CLASSES.add(RedisTxCommands.class); + TARGET_CLASSES.add(RedisPubSubCommands.class); + TARGET_CLASSES.add(RedisConnectionCommands.class); + TARGET_CLASSES.add(RedisServerCommands.class); + TARGET_CLASSES.add(RedisStreamCommands.class); + TARGET_CLASSES.add(RedisScriptingCommands.class); + TARGET_CLASSES.add(RedisGeoCommands.class); + TARGET_CLASSES.add(RedisHyperLogLogCommands.class); + + try { + REDIS_COMMANDS_INDEX = Index.of(TARGET_CLASSES); + } catch (IOException e) { + logger.error("Index RedisCommands Error", e); + throw new RuntimeException(e); + } + METHOD_HANDLE_MAP = initRedisCommandMethodHandle(); + METHOD_METHOD_INFO_MAP = initRedisCommandMethodInfo(); + METHOD_METHOD_HANDLE_MAP = METHOD_METHOD_INFO_MAP.keySet() + .stream() + .collect(toMap(Function.identity(), key -> METHOD_HANDLE_MAP.get(METHOD_METHOD_INFO_MAP.get(key).toString()))); + } + + /** + * NOTE: Use Record Class when use jdk 17+ + */ + static class MethodRecord { + String methodSignature; + MethodHandle methodHandle; + + public MethodRecord(String methodSignature, MethodHandle methodHandle) { + this.methodSignature = methodSignature; + this.methodHandle = methodHandle; + } + + public String methodSignature() { + return methodSignature; + } + + public MethodHandle methodHandle() { + return methodHandle; + } + } + + static class TypeHelper { + private static final EnumMap> PRIMITIVE_TYPE_CLASS_TABLE = new EnumMap<>(PrimitiveType.Primitive.class); + private static final Map> ARRAY_TYPE_CLASS_TABLE = new HashMap<>(); + + private TypeHelper() { + } + + static { + // NOTE: use new EnumMap(Map.of()) to simplify the code when use jdk11+ + PRIMITIVE_TYPE_CLASS_TABLE.put(PrimitiveType.Primitive.BOOLEAN, boolean.class); + PRIMITIVE_TYPE_CLASS_TABLE.put(PrimitiveType.Primitive.BYTE, byte.class); + PRIMITIVE_TYPE_CLASS_TABLE.put(PrimitiveType.Primitive.SHORT, short.class); + PRIMITIVE_TYPE_CLASS_TABLE.put(PrimitiveType.Primitive.INT, int.class); + PRIMITIVE_TYPE_CLASS_TABLE.put(PrimitiveType.Primitive.LONG, long.class); + PRIMITIVE_TYPE_CLASS_TABLE.put(PrimitiveType.Primitive.FLOAT, float.class); + PRIMITIVE_TYPE_CLASS_TABLE.put(PrimitiveType.Primitive.DOUBLE, double.class); + PRIMITIVE_TYPE_CLASS_TABLE.put(PrimitiveType.Primitive.CHAR, char.class); + + ARRAY_TYPE_CLASS_TABLE.put("byte[]", byte[].class); + ARRAY_TYPE_CLASS_TABLE.put("byte[][]", byte[][].class); + ARRAY_TYPE_CLASS_TABLE.put("int[]", int[].class); + ARRAY_TYPE_CLASS_TABLE.put("String[]", String[].class); + ARRAY_TYPE_CLASS_TABLE.put("RecordId[]", RecordId[].class); + ARRAY_TYPE_CLASS_TABLE.put("StreamOffset[]", StreamOffset[].class); + } + + } +} diff --git a/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/RedisMetadataRepository.java b/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/RedisMetadataRepository.java index 8580ec5..c3e4971 100644 --- a/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/RedisMetadataRepository.java +++ b/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/RedisMetadataRepository.java @@ -13,6 +13,9 @@ import org.yaml.snakeyaml.Yaml; import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Method; import java.util.Arrays; @@ -67,6 +70,12 @@ public class RedisMetadataRepository { */ static final Map writeCommandMethodsCache = new HashMap<>(256); + private static final MethodHandles.Lookup PUBLIC_LOOKUP = MethodHandles.publicLookup(); + /** + * Method Simple signature with {@link MethodHandle} object caching (reduces reflection cost) + */ + static final Map writeCommandMethodHandlesCache = new HashMap<>(256); + /** * MethodMetadata cache *
      @@ -227,6 +236,11 @@ public static Set getWriteCommandMethods() { return writeCommandMethodsMetadata.keySet(); } + public static MethodHandle getWriteCommandMethodHandle(Method method) { + String id = buildCommandMethodId(method); + return writeCommandMethodHandlesCache.get(id); + } + /** * Gets the {@link RedisCommands} command interface for the specified Class name {@link Class} * @@ -270,6 +284,7 @@ private static void initWriteCommandMethod(Method method) { } if (initWriteCommandMethodParameterMetadata(method, parameterTypes)) { initWriteCommandMethodCache(method, parameterTypes); + initWriteCommandMethodHandleCache(method); } } catch (Throwable e) { logger.error("Unable to initialize write command method[{}], Reason: {}", method, e.getMessage()); @@ -300,4 +315,25 @@ private static void initWriteCommandMethodCache(Method method, Class[] parame } } + + private static void initWriteCommandMethodHandleCache(Method method) { + String id = buildCommandMethodId(method); + + MethodHandle methodHandle; + try { + methodHandle = PUBLIC_LOOKUP.findVirtual( + method.getDeclaringClass(), + method.getName(), + MethodType.methodType(method.getReturnType(), method.getParameterTypes())); + } catch (NoSuchMethodException | IllegalAccessException e) { + logger.error("The Redis Write Command MethodHandle[{}] can't find", id); + return; + } + + if (writeCommandMethodHandlesCache.putIfAbsent(id, methodHandle) == null) { + logger.debug("Caches the Redis Write Command MethodHandle : {}", id); + } else { + logger.warn("The Redis Write Command MethodHandle[{}] was cached", id); + } + } } diff --git a/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/exception/MethodHandleNotFoundException.java b/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/exception/MethodHandleNotFoundException.java new file mode 100644 index 0000000..0fae483 --- /dev/null +++ b/microsphere-redis-spring/src/main/java/io/microsphere/redis/spring/metadata/exception/MethodHandleNotFoundException.java @@ -0,0 +1,16 @@ +package io.microsphere.redis.spring.metadata.exception; + +public class MethodHandleNotFoundException extends RuntimeException { + + private final String methodSignature; + + public MethodHandleNotFoundException(String message, String methodSignature) { + super(message + ", methodSignature is " + methodSignature); + this.methodSignature = methodSignature; + } + + public String getMethodSignature() { + return methodSignature; + } + +} diff --git a/microsphere-redis-spring/src/test/java/io/microsphere/redis/spring/metadata/RedisCommandsMethodHandlesTest.java b/microsphere-redis-spring/src/test/java/io/microsphere/redis/spring/metadata/RedisCommandsMethodHandlesTest.java new file mode 100644 index 0000000..dad8590 --- /dev/null +++ b/microsphere-redis-spring/src/test/java/io/microsphere/redis/spring/metadata/RedisCommandsMethodHandlesTest.java @@ -0,0 +1,184 @@ +package io.microsphere.redis.spring.metadata; + +import io.microsphere.redis.spring.metadata.exception.MethodHandleNotFoundException; +import org.jboss.jandex.ArrayType; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.Index; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.VoidType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.data.redis.connection.RedisCommands; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamOffset; + +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static io.microsphere.redis.spring.metadata.RedisCommandsMethodHandles.findMethodHandleBy; +import static io.microsphere.redis.spring.metadata.RedisCommandsMethodHandles.getAllRedisCommandMethods; +import static io.microsphere.redis.spring.metadata.RedisCommandsMethodHandles.getClassBy; +import static io.microsphere.redis.spring.metadata.RedisCommandsMethodHandles.getMethodHandleBy; +import static io.microsphere.redis.spring.metadata.RedisCommandsMethodHandles.initRedisCommandMethodHandle; +import static io.microsphere.redis.spring.metadata.RedisCommandsMethodHandles.initRedisCommandMethodInfo; +import static io.microsphere.redis.spring.metadata.RedisCommandsMethodHandles.transferMethodToMethodSignature; +import static io.microsphere.redis.spring.metadata.RedisMetadataRepository.redisCommandMethodsCache; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.jboss.jandex.ArrayType.builder; +import static org.jboss.jandex.DotName.createSimple; +import static org.jboss.jandex.Type.Kind.CLASS; +import static org.jboss.jandex.Type.create; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class RedisCommandsMethodHandlesTest { + + static int methodCount = RedisCommands.class.getMethods().length - 1; + + @Test + void shouldGetAllMethodInfoFromRedisCommand() { + List list = getAllRedisCommandMethods(); + + assertThat(list) + .isNotNull() + .hasSize(methodCount); + } + + @Test + void shouldGetMethodHandleMapFromMethodInfo() { + Map map = initRedisCommandMethodHandle(); + assertThat(map) + .isNotNull() + .hasSize(methodCount); + } + + @Test + void shouldNewMethodHandleInstanceByMethodInfo() { + MethodInfo methodInfo = getMethodInfo(); + + MethodHandle methodHandle = findMethodHandleBy(methodInfo); + assertThat(methodHandle) + .isNotNull(); + } + + @Test + void shouldGetMethodHandleByMethodSignature() { + String methodSignature = getMethodInfo().toString(); + MethodHandle methodHandle = getMethodHandleBy(methodSignature); + assertThat(methodHandle) + .isNotNull(); + } + + @Test + void shouldThrowMethodHandleNotFoundExceptionWhenMiss() { + MethodHandleNotFoundException missingMethodSignature = catchThrowableOfType( + () -> getMethodHandleBy("MissingMethodSignature"), + MethodHandleNotFoundException.class); + + assertThat(missingMethodSignature) + .hasMessageContaining("can't find MethodHandle from RedisCommands") + .hasMessageContaining("methodSignature"); + assertThat(missingMethodSignature.getMethodSignature()) + .isEqualTo("MissingMethodSignature"); + } + + @Test + void shouldGetAllMethodInfoFromRedisCommandMethodsCache() { + Map map = initRedisCommandMethodInfo(); + assertThat(map) + .isNotNull() + .hasSize(redisCommandMethodsCache.size()); + + } + + @Test + void shouldTransferMethodToMethodHandleSignature() throws NoSuchMethodException { + MethodInfo methodInfo = getMethodInfo(); + Method setMethod = RedisStringCommands.class.getMethod("set", byte[].class, byte[].class); + + String signature = transferMethodToMethodSignature(setMethod); + assertThat(signature) + .isEqualTo(methodInfo.toString()); + } + + @ParameterizedTest(name = "test: {0}") + @MethodSource + void shouldGetClassWhenTypeIsPrimitiveClass(PrimitiveType primitiveType, Class expected) { + Class klass = getClassBy(primitiveType); + assertThat(klass).isEqualTo(expected); + } + + static Stream shouldGetClassWhenTypeIsPrimitiveClass() { + return Stream.of( + arguments(named("boolean", PrimitiveType.BOOLEAN), boolean.class), + arguments(named("byte", PrimitiveType.BYTE), byte.class), + arguments(named("short", PrimitiveType.SHORT), short.class), + arguments(named("int", PrimitiveType.INT), int.class), + arguments(named("long", PrimitiveType.LONG), long.class), + arguments(named("float", PrimitiveType.FLOAT), float.class), + arguments(named("double", PrimitiveType.DOUBLE), double.class), + arguments(named("char", PrimitiveType.CHAR), char.class) + ); + } + + @ParameterizedTest(name = "test: {0}") + @MethodSource + void shouldGetClassWhenTypeIsArrayClass(ArrayType arrayType, Class expected) { + Class klass = getClassBy(arrayType); + assertThat(klass).isEqualTo(expected); + } + + static Stream shouldGetClassWhenTypeIsArrayClass() { + return Stream.of( + arguments(named("byte[]", builder(PrimitiveType.BYTE, 1).build()), byte[].class), + arguments(named("byte[][]", builder(PrimitiveType.BYTE, 2).build()), byte[][].class), + arguments(named("int[]", builder(PrimitiveType.INT, 1).build()), int[].class), + arguments(named("String[]", builder(create(createSimple(String.class), CLASS), 1).build()), String[].class), + arguments(named("RecordId[]", builder(create(createSimple(RecordId.class), CLASS), 1).build()), RecordId[].class), + arguments(named("StreamOffset[]", builder(create(createSimple(StreamOffset.class), CLASS), 1).build()), StreamOffset[].class) + ); + } + + @Test + void shouldGetVoidClass() { + Class klass = getClassBy(VoidType.VOID); + assertThat(klass).isEqualTo(void.class); + } + + @Test + void shouldGetClassWhenTypeIsParameterizedType() { + ParameterizedType parameterizedType = ParameterizedType.builder(List.class).addArgument(ClassType.create(String.class)).build(); + Class klass = getClassBy(parameterizedType); + assertThat(klass).isEqualTo(List.class); + } + + @Test + void shouldGetClassWhenTypeIsClassType() { + ClassType classType = ClassType.create(Object.class); + Class klass = getClassBy(classType); + assertThat(klass).isEqualTo(Object.class); + } + + private static MethodInfo getMethodInfo() { + try { + Index index = Index.of(RedisStringCommands.class); + ClassInfo classInfo = index.getClassByName(RedisStringCommands.class); + Optional setMethod = classInfo.methods().stream().filter(methodInfo -> methodInfo.name().equals("set")).findFirst(); + return setMethod.get(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +}