From a8945c6ba00755bbc08b1df5e2b4beb685041447 Mon Sep 17 00:00:00 2001 From: jackygurui Date: Sat, 4 Jun 2016 01:51:52 +0100 Subject: [PATCH] WIP 1. Simplified the API 2. Added class validation 3. Nested RObject and LiveObject is now supported --- src/main/java/org/redisson/Redisson.java | 4 +- .../RedissonAttachedLiveObjectService.java | 67 +++++-- .../java/org/redisson/RedissonClient.java | 8 +- .../java/org/redisson/RedissonReference.java | 4 +- .../RAttachedLiveObjectService.java | 8 +- .../RDetachedLiveObjectService.java | 18 +- .../liveobject/RLiveObjectService.java | 10 +- .../liveobject/annotation/REntity.java | 2 +- .../liveobject/core/AccessorInterceptor.java | 36 ++-- ...RedissonAttachedLiveObjectServiceTest.java | 185 +++++++++++++++++- 10 files changed, 280 insertions(+), 62 deletions(-) diff --git a/src/main/java/org/redisson/Redisson.java b/src/main/java/org/redisson/Redisson.java index f735b0c65..b3d4f33b2 100755 --- a/src/main/java/org/redisson/Redisson.java +++ b/src/main/java/org/redisson/Redisson.java @@ -532,8 +532,8 @@ public class Redisson implements RedissonClient { } @Override - public RedissonAttachedLiveObjectService getAttachedLiveObjectService() { - return new RedissonAttachedLiveObjectService(this, commandExecutor); + public RedissonAttachedLiveObjectService getAttachedLiveObjectService() { + return new RedissonAttachedLiveObjectService(this, commandExecutor); } @Override diff --git a/src/main/java/org/redisson/RedissonAttachedLiveObjectService.java b/src/main/java/org/redisson/RedissonAttachedLiveObjectService.java index 6351981ea..f719598da 100644 --- a/src/main/java/org/redisson/RedissonAttachedLiveObjectService.java +++ b/src/main/java/org/redisson/RedissonAttachedLiveObjectService.java @@ -1,73 +1,110 @@ package org.redisson; import io.netty.util.internal.PlatformDependent; +import java.lang.reflect.Modifier; import java.util.Map; import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatchers; -import org.redisson.command.CommandExecutor; +import org.redisson.command.CommandAsyncExecutor; +import org.redisson.core.RObject; import org.redisson.liveobject.RAttachedLiveObjectService; +import org.redisson.liveobject.annotation.REntity; import org.redisson.liveobject.annotation.RId; import org.redisson.liveobject.core.AccessorInterceptor; import org.redisson.liveobject.misc.Introspectior; -public class RedissonAttachedLiveObjectService implements RAttachedLiveObjectService { +public class RedissonAttachedLiveObjectService implements RAttachedLiveObjectService { private static final Map classCache = PlatformDependent.newConcurrentHashMap(); private static final Map proxyCache = PlatformDependent.newConcurrentHashMap(); + private final RedissonClient redisson; - private final CommandExecutor commandExecutor; + private final CommandAsyncExecutor commandExecutor; - public RedissonAttachedLiveObjectService(RedissonClient redisson, CommandExecutor commandExecutor) { + public RedissonAttachedLiveObjectService(RedissonClient redisson, CommandAsyncExecutor commandExecutor) { this.redisson = redisson; this.commandExecutor = commandExecutor; } //TODO: Support ID Generator - @Override - public T get(Class entityClass, K id, long ttl) { + public T get(Class entityClass, K id, long ttl) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override - public T get(Class entityClass, K id) { + public T get(Class entityClass, K id) { try { //TODO: support class with no arg constructor return getProxyClass(entityClass).getConstructor(id.getClass()).newInstance(id); } catch (Exception ex) { + unregisterClass(entityClass); throw new RuntimeException(ex); } } - private Class getProxyClass(Class entityClass) throws Exception { + private Class getProxyClass(Class entityClass) throws Exception { if (!classCache.containsKey(entityClass)) { registerClass(entityClass); } return classCache.get(entityClass); } - private void registerClass(Class entityClass) throws Exception { - //TODO: check annotation on the entityClass - String idFieldName = Introspectior.getFieldsWithAnnotation(entityClass, RId.class) - .getOnly() - .getName(); + private void registerClass(Class entityClass) throws Exception { + if (entityClass.isAnonymousClass() || entityClass.isLocalClass()) { + throw new IllegalArgumentException(entityClass.getName() + " is not publically accessable."); + } + if (!entityClass.isAnnotationPresent(REntity.class)) { + throw new IllegalArgumentException("REntity annotation is missing from class type declaration."); + } + FieldList fieldsWithRIdAnnotation = Introspectior.getFieldsWithAnnotation(entityClass, RId.class); + if (fieldsWithRIdAnnotation.size() == 0) { + throw new IllegalArgumentException("RId annotation is missing from class field declaration."); + } + if (fieldsWithRIdAnnotation.size() > 1) { + throw new IllegalArgumentException("Only one field with RId annotation is allowed in class field declaration."); + } + FieldDescription.ForLoadedField idField = (FieldDescription.ForLoadedField) fieldsWithRIdAnnotation.getOnly(); + String idFieldName = idField.getName(); + if (entityClass.getDeclaredField(idFieldName).getType().isAnnotationPresent(REntity.class)) { + throw new IllegalArgumentException("Field with RId annotation cannot be a type of which class is annotated with REntity."); + } + if (entityClass.getDeclaredField(idFieldName).getType().isAssignableFrom(RObject.class)) { + throw new IllegalArgumentException("Field with RId annotation cannot be a type of RObject"); + } classCache.putIfAbsent(entityClass, new ByteBuddy() .subclass(entityClass) .method(ElementMatchers.not(ElementMatchers.isDeclaredBy(Object.class)) .and(ElementMatchers.isGetter() .or(ElementMatchers.isSetter())) .and(ElementMatchers.isPublic())) - .intercept(MethodDelegation.to(new AccessorInterceptor(redisson, entityClass, idFieldName, commandExecutor))) + .intercept(MethodDelegation.to(new AccessorInterceptor(redisson, entityClass, idFieldName, commandExecutor))) .make().load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded()); proxyCache.putIfAbsent(classCache.get(entityClass), entityClass); } - + + public static void unregisterProxy(Class proxy) { + Class cls = proxyCache.remove(proxy); + if (cls != null) { + classCache.remove(cls); + } + } + + public static void unregisterClass(Class cls) { + Class proxy = classCache.remove(cls); + if (proxy != null) { + proxyCache.remove(proxy); + } + } + public static Class getActualClass(Class proxyClass) { return proxyCache.get(proxyClass); } diff --git a/src/main/java/org/redisson/RedissonClient.java b/src/main/java/org/redisson/RedissonClient.java index 3f7bf4184..3b02a8243 100755 --- a/src/main/java/org/redisson/RedissonClient.java +++ b/src/main/java/org/redisson/RedissonClient.java @@ -639,7 +639,13 @@ public interface RedissonClient { */ RKeys getKeys(); - RedissonAttachedLiveObjectService getAttachedLiveObjectService(); + /** + * Returns RedissonAttachedLiveObjectService which can be used to + * retrieve live REntity(s) + * + * @return + */ + RedissonAttachedLiveObjectService getAttachedLiveObjectService(); /** * Shuts down Redisson instance NOT Redis server diff --git a/src/main/java/org/redisson/RedissonReference.java b/src/main/java/org/redisson/RedissonReference.java index e6c1447a3..c32c6e230 100644 --- a/src/main/java/org/redisson/RedissonReference.java +++ b/src/main/java/org/redisson/RedissonReference.java @@ -67,7 +67,9 @@ public class RedissonReference { * @return the codec */ public Codec getCodec() throws Exception { - return (Codec) Class.forName(codec).newInstance(); + return codec == null + ? null + : (Codec) Class.forName(codec).newInstance(); } /** diff --git a/src/main/java/org/redisson/liveobject/RAttachedLiveObjectService.java b/src/main/java/org/redisson/liveobject/RAttachedLiveObjectService.java index 66fab5b76..5f39befe8 100644 --- a/src/main/java/org/redisson/liveobject/RAttachedLiveObjectService.java +++ b/src/main/java/org/redisson/liveobject/RAttachedLiveObjectService.java @@ -4,10 +4,8 @@ package org.redisson.liveobject; * * @author ruigu * - * @param Entity type - * @param Key type */ -public interface RAttachedLiveObjectService extends RLiveObjectService { +public interface RAttachedLiveObjectService extends RLiveObjectService { /** * Finds the entity from Redis with the id. @@ -18,8 +16,10 @@ public interface RAttachedLiveObjectService extends RLiveObjectService Entity type + * @param Key type * @return In ATTACHED Mode, this always returns a proxy class. Even it does * not exist in redis. */ - public T get(Class entityClass, K id, long ttl); + public T get(Class entityClass, K id, long ttl); } diff --git a/src/main/java/org/redisson/liveobject/RDetachedLiveObjectService.java b/src/main/java/org/redisson/liveobject/RDetachedLiveObjectService.java index 1f0accddd..b33206ce5 100644 --- a/src/main/java/org/redisson/liveobject/RDetachedLiveObjectService.java +++ b/src/main/java/org/redisson/liveobject/RDetachedLiveObjectService.java @@ -9,21 +9,7 @@ import io.netty.util.concurrent.Future; * @param Entity type * @param Key type */ -public interface RDetachedLiveObjectService extends RLiveObjectService { - - - /** - * Finds the entity from Redis with the id. - * - * @param entityClass Entity class - * @param id identifier - * @return In ATTACHED Mode, this always returns a proxy class. Even it does - * not exist in redis. - * In DETACHED Mode, this returns an instance of the entity class. - * IF it doesn't exist in redis, null is returned. - */ - @Override - public T get(Class entityClass, K id); +public interface RDetachedLiveObjectService extends RLiveObjectService { /** * Finds the entity from Redis with the id. @@ -34,8 +20,6 @@ public interface RDetachedLiveObjectService extends RLiveObjectService getAsync(Class entityClass, K id); diff --git a/src/main/java/org/redisson/liveobject/RLiveObjectService.java b/src/main/java/org/redisson/liveobject/RLiveObjectService.java index 08062ebdb..65c5b8c26 100644 --- a/src/main/java/org/redisson/liveobject/RLiveObjectService.java +++ b/src/main/java/org/redisson/liveobject/RLiveObjectService.java @@ -10,22 +10,22 @@ package org.redisson.liveobject; * Redis when update is called. * * @author ruigu - * - * @param Entity type - * @param Key type + * */ -public interface RLiveObjectService { +public interface RLiveObjectService { /** * Finds the entity from Redis with the id. * * @param entityClass Entity class * @param id identifier + * @param Entity type + * @param Key type * @return In ATTACHED Mode, this always returns a proxy class. Even it does * not exist in redis. In DETACHED Mode, this returns an instance of the * entity class. IF it doesn't exist in redis, a runtime exception is * thrown. */ - public T get(Class entityClass, K id); + public T get(Class entityClass, K id); } diff --git a/src/main/java/org/redisson/liveobject/annotation/REntity.java b/src/main/java/org/redisson/liveobject/annotation/REntity.java index 3624d9aa9..957ced00b 100644 --- a/src/main/java/org/redisson/liveobject/annotation/REntity.java +++ b/src/main/java/org/redisson/liveobject/annotation/REntity.java @@ -28,7 +28,7 @@ public @interface REntity { @Override public String getName(Class cls, String idFieldName, Object id) { - return "redisson_live_object:{class=" + cls.getCanonicalName() + ", " + idFieldName + "=" + id.toString() + "}"; + return "redisson_live_object:{class=" + cls.getName() + ", " + idFieldName + "=" + id.toString() + "}"; } } diff --git a/src/main/java/org/redisson/liveobject/core/AccessorInterceptor.java b/src/main/java/org/redisson/liveobject/core/AccessorInterceptor.java index 2c77164d4..433a5aa7e 100644 --- a/src/main/java/org/redisson/liveobject/core/AccessorInterceptor.java +++ b/src/main/java/org/redisson/liveobject/core/AccessorInterceptor.java @@ -12,7 +12,7 @@ import org.redisson.RedissonClient; import org.redisson.RedissonReference; import org.redisson.client.RedisException; import org.redisson.client.codec.Codec; -import org.redisson.command.CommandExecutor; +import org.redisson.command.CommandAsyncExecutor; import org.redisson.core.RMap; import org.redisson.core.RObject; import org.redisson.liveobject.annotation.REntity; @@ -23,25 +23,28 @@ import org.redisson.liveobject.misc.Introspectior; * * @author ruigu */ -public class AccessorInterceptor { +public class AccessorInterceptor { private final RedissonClient redisson; private final Class originalClass; private final String idFieldName; private final REntity.NamingScheme namingScheme; - private final CommandExecutor commandExecutor; + private final CommandAsyncExecutor commandExecutor; private RMap liveMap; - public AccessorInterceptor(RedissonClient redisson, Class entityClass, String idFieldName, CommandExecutor commandExecutor) throws Exception { + public AccessorInterceptor(RedissonClient redisson, Class entityClass, + String idFieldName, CommandAsyncExecutor commandExecutor) throws Exception { this.redisson = redisson; this.originalClass = entityClass; this.idFieldName = idFieldName; this.commandExecutor = commandExecutor; - this.namingScheme = ((REntity) entityClass.getAnnotation(REntity.class)).namingScheme().newInstance(); + this.namingScheme = ((REntity) entityClass.getAnnotation(REntity.class)) + .namingScheme().newInstance(); } @RuntimeType - public Object intercept(@Origin Method method, @SuperCall Callable superMethod, @AllArguments Object[] args, @This T me) throws Exception { + public Object intercept(@Origin Method method, @SuperCall Callable superMethod, + @AllArguments Object[] args, @This Object me) throws Exception { if (isGetter(method, idFieldName)) { return superMethod.call(); } @@ -49,7 +52,7 @@ public class AccessorInterceptor { if (isSetter(method, idFieldName)) { superMethod.call(); try { - liveMap.rename(getMapKey((K) args[0])); + liveMap.rename(getMapKey(args[0])); } catch (RedisException e) { if (e.getMessage() == null || !e.getMessage().startsWith("ERR no such key")) { throw e; @@ -62,10 +65,15 @@ public class AccessorInterceptor { if (isGetter(method, fieldName)) { Object result = liveMap.get(fieldName); if (method.getReturnType().isAnnotationPresent(REntity.class)) { - return redisson.getAttachedLiveObjectService().get((Class) method.getReturnType(), result); + return redisson.getAttachedLiveObjectService() + .get((Class) method.getReturnType(), result); } else if (result instanceof RedissonReference) { RedissonReference r = ((RedissonReference) result); - return r.getType().getConstructor(Codec.class, CommandExecutor.class, String.class).newInstance(r.getCodec(), commandExecutor, r.getKeyName()); + return r.getType() + .getConstructor(Codec.class, CommandAsyncExecutor.class, String.class) + .newInstance(r.isDefaultCodec() + ? commandExecutor.getConnectionManager().getCodec() + : r.getCodec(), commandExecutor, r.getKeyName()); } return result; } @@ -81,7 +89,7 @@ public class AccessorInterceptor { return superMethod.call(); } - private void initLiveMapIfRequired(K id) { + private void initLiveMapIfRequired(Object id) { if (liveMap == null) { liveMap = redisson.getMap(getMapKey(id)); } @@ -101,18 +109,18 @@ public class AccessorInterceptor { && method.getName().endsWith(getFieldNameSuffix(fieldName)); } - private String getMapKey(K id) { + private String getMapKey(Object id) { return namingScheme.getName(originalClass, idFieldName, id); } - private K getId(T me) throws Exception { - return (K) originalClass.getDeclaredMethod("get" + getFieldNameSuffix(idFieldName)).invoke(me); + private Object getId(Object me) throws Exception { + return originalClass.getDeclaredMethod("get" + getFieldNameSuffix(idFieldName)).invoke(me); } private static String getFieldNameSuffix(String fieldName) { return fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); } - + private static Object getFieldValue(Object o, String fieldName) throws Exception { return RedissonAttachedLiveObjectService.getActualClass(o.getClass()).getDeclaredMethod("get" + getFieldNameSuffix(fieldName)).invoke(o); } diff --git a/src/test/java/org/redisson/RedissonAttachedLiveObjectServiceTest.java b/src/test/java/org/redisson/RedissonAttachedLiveObjectServiceTest.java index f18385a7b..70571a89e 100644 --- a/src/test/java/org/redisson/RedissonAttachedLiveObjectServiceTest.java +++ b/src/test/java/org/redisson/RedissonAttachedLiveObjectServiceTest.java @@ -56,10 +56,142 @@ public class RedissonAttachedLiveObjectServiceTest extends BaseTest { } } + @REntity + public static class TestREntityWithRMap implements Comparable, Serializable { + + @RId + private String name; + private RMap value; + + public TestREntityWithRMap(String name) { + this.name = name; + } + + public TestREntityWithRMap(String name, RMap value) { + super(); + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public RMap getValue() { + return value; + } + + public void setName(String name) { + this.name = name; + } + + public void setValue(RMap value) { + this.value = value; + } + + @Override + public int compareTo(TestREntityWithRMap o) { + int res = name.compareTo(o.name); + if (res == 0 || value != null || o.value != null) { + if (value.getName() == null) { + return -1; + } + return value.getName().compareTo(o.value.getName()); + } + return res; + } + } + + @REntity + public static class TestREntityIdNested implements Comparable, Serializable { + + @RId + private TestREntity name; + private String value; + + public TestREntityIdNested(TestREntity name) { + this.name = name; + } + + public TestREntityIdNested(TestREntity name, String value) { + super(); + this.name = name; + this.value = value; + } + + public TestREntity getName() { + return name; + } + + public String getValue() { + return value; + } + + public void setName(TestREntity name) { + this.name = name; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public int compareTo(TestREntityIdNested o) { + int res = name.compareTo(o.name); + if (res == 0 || value != null || o.value != null) { + return value.compareTo(o.value); + } + return res; + } + } + + @REntity + public static class TestREntityValueNested implements Comparable, Serializable { + + @RId + private String name; + private TestREntityWithRMap value; + + public TestREntityValueNested(String name) { + this.name = name; + } + + public TestREntityValueNested(String name, TestREntityWithRMap value) { + super(); + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public TestREntityWithRMap getValue() { + return value; + } + + public void setName(String name) { + this.name = name; + } + + public void setValue(TestREntityWithRMap value) { + this.value = value; + } + + @Override + public int compareTo(TestREntityValueNested o) { + int res = name.compareTo(o.name); + if (res == 0 || value != null || o.value != null) { + return value.compareTo(o.value); + } + return res; + } + } + @Test public void test() { - RedissonAttachedLiveObjectService s = redisson.getAttachedLiveObjectService(); - TestREntity t = s.get(TestREntity.class, "1"); + RedissonAttachedLiveObjectService s = redisson.getAttachedLiveObjectService(); + TestREntity t = s.get(TestREntity.class, "1"); assertEquals("1", t.getName()); assertTrue(!redisson.getMap(REntity.DefaultNamingScheme.INSTANCE.getName(TestREntity.class, "name", "1")).isExists()); t.setName("3333"); @@ -71,4 +203,53 @@ public class RedissonAttachedLiveObjectServiceTest extends BaseTest { assertTrue(!redisson.getMap(REntity.DefaultNamingScheme.INSTANCE.getName(TestREntity.class, "name", "1")).isExists()); assertEquals("111", redisson.getMap(REntity.DefaultNamingScheme.INSTANCE.getName(TestREntity.class, "name", "3333")).get("value")); } + + @Test + public void testLiveObjectWithRObject() { + RedissonAttachedLiveObjectService s = redisson.getAttachedLiveObjectService(); + TestREntityWithRMap t = s.get(TestREntityWithRMap.class, "2"); + RMap map = redisson.getMap("testMap"); + t.setValue(map); + map.put("field", "123"); + assertEquals("123", + s.get(TestREntityWithRMap.class, "2") + .getValue().get("field")); + s.get(TestREntityWithRMap.class, "2").getValue().put("field", "333"); + assertEquals("333", + s.get(TestREntityWithRMap.class, "2") + .getValue().get("field")); + } + + @Test + public void testLiveObjectWithNestedLiveObjectAsId() { + RedissonAttachedLiveObjectService s = redisson.getAttachedLiveObjectService(); + TestREntity t1 = s.get(TestREntity.class, "1"); + try { + s.get(TestREntityIdNested.class, t1); + } catch (Exception e) { + assertEquals("Field with RId annotation cannot be a type of which class is annotated with REntity.", e.getCause().getMessage()); + } + } + + @Test + public void testLiveObjectWithNestedLiveObjectAsValue() throws InterruptedException { + RedissonAttachedLiveObjectService s = redisson.getAttachedLiveObjectService(); + TestREntityWithRMap t1 = s.get(TestREntityWithRMap.class, "111"); + TestREntityValueNested t2 = s.get(TestREntityValueNested.class, "122"); + RMap map = redisson.getMap("32123"); + System.out.println("111"); + t2.setValue(t1); + System.out.println("222"); + t2.getValue().setValue(map); + System.out.println("333"); + map.put("field", "123"); + System.out.println("444"); + assertEquals("123", + s.get(TestREntityWithRMap.class, "111") + .getValue().get("field")); + System.out.println("555"); + assertEquals("123", + s.get(TestREntityValueNested.class, "122") + .getValue().getValue().get("field")); + } }