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);
+ }
+ }
+}