1. Simplified the API
2. Added class validation
3. Nested RObject and LiveObject is now supported
pull/527/head
jackygurui 9 years ago
parent 7db9f30577
commit a8945c6ba0

@ -532,8 +532,8 @@ public class Redisson implements RedissonClient {
} }
@Override @Override
public <T, K> RedissonAttachedLiveObjectService<T, K> getAttachedLiveObjectService() { public RedissonAttachedLiveObjectService getAttachedLiveObjectService() {
return new RedissonAttachedLiveObjectService<T, K>(this, commandExecutor); return new RedissonAttachedLiveObjectService(this, commandExecutor);
} }
@Override @Override

@ -1,73 +1,110 @@
package org.redisson; package org.redisson;
import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.PlatformDependent;
import java.lang.reflect.Modifier;
import java.util.Map; import java.util.Map;
import net.bytebuddy.ByteBuddy; 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.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers; 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.RAttachedLiveObjectService;
import org.redisson.liveobject.annotation.REntity;
import org.redisson.liveobject.annotation.RId; import org.redisson.liveobject.annotation.RId;
import org.redisson.liveobject.core.AccessorInterceptor; import org.redisson.liveobject.core.AccessorInterceptor;
import org.redisson.liveobject.misc.Introspectior; import org.redisson.liveobject.misc.Introspectior;
public class RedissonAttachedLiveObjectService<T, K> implements RAttachedLiveObjectService<T, K> { public class RedissonAttachedLiveObjectService implements RAttachedLiveObjectService {
private static final Map<Class, Class> classCache private static final Map<Class, Class> classCache
= PlatformDependent.<Class, Class>newConcurrentHashMap(); = PlatformDependent.<Class, Class>newConcurrentHashMap();
private static final Map<Class, Class> proxyCache private static final Map<Class, Class> proxyCache
= PlatformDependent.<Class, Class>newConcurrentHashMap(); = PlatformDependent.<Class, Class>newConcurrentHashMap();
private final RedissonClient redisson; 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.redisson = redisson;
this.commandExecutor = commandExecutor; this.commandExecutor = commandExecutor;
} }
//TODO: Support ID Generator //TODO: Support ID Generator
@Override @Override
public T get(Class<T> entityClass, K id, long ttl) { public <T, K> T get(Class<T> entityClass, K id, long ttl) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
} }
@Override @Override
public T get(Class<T> entityClass, K id) { public <T, K> T get(Class<T> entityClass, K id) {
try { try {
//TODO: support class with no arg constructor //TODO: support class with no arg constructor
return getProxyClass(entityClass).getConstructor(id.getClass()).newInstance(id); return getProxyClass(entityClass).getConstructor(id.getClass()).newInstance(id);
} catch (Exception ex) { } catch (Exception ex) {
unregisterClass(entityClass);
throw new RuntimeException(ex); throw new RuntimeException(ex);
} }
} }
private Class<? extends T> getProxyClass(Class<T> entityClass) throws Exception { private <T, K> Class<? extends T> getProxyClass(Class<T> entityClass) throws Exception {
if (!classCache.containsKey(entityClass)) { if (!classCache.containsKey(entityClass)) {
registerClass(entityClass); registerClass(entityClass);
} }
return classCache.get(entityClass); return classCache.get(entityClass);
} }
private void registerClass(Class<T> entityClass) throws Exception { private <T, K> void registerClass(Class<T> entityClass) throws Exception {
//TODO: check annotation on the entityClass if (entityClass.isAnonymousClass() || entityClass.isLocalClass()) {
String idFieldName = Introspectior.getFieldsWithAnnotation(entityClass, RId.class) throw new IllegalArgumentException(entityClass.getName() + " is not publically accessable.");
.getOnly() }
.getName(); if (!entityClass.isAnnotationPresent(REntity.class)) {
throw new IllegalArgumentException("REntity annotation is missing from class type declaration.");
}
FieldList<FieldDescription.InDefinedShape> 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() classCache.putIfAbsent(entityClass, new ByteBuddy()
.subclass(entityClass) .subclass(entityClass)
.method(ElementMatchers.not(ElementMatchers.isDeclaredBy(Object.class)) .method(ElementMatchers.not(ElementMatchers.isDeclaredBy(Object.class))
.and(ElementMatchers.isGetter() .and(ElementMatchers.isGetter()
.or(ElementMatchers.isSetter())) .or(ElementMatchers.isSetter()))
.and(ElementMatchers.isPublic())) .and(ElementMatchers.isPublic()))
.intercept(MethodDelegation.to(new AccessorInterceptor<T, K>(redisson, entityClass, idFieldName, commandExecutor))) .intercept(MethodDelegation.to(new AccessorInterceptor(redisson, entityClass, idFieldName, commandExecutor)))
.make().load(getClass().getClassLoader(), .make().load(getClass().getClassLoader(),
ClassLoadingStrategy.Default.WRAPPER) ClassLoadingStrategy.Default.WRAPPER)
.getLoaded()); .getLoaded());
proxyCache.putIfAbsent(classCache.get(entityClass), entityClass); 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) { public static Class getActualClass(Class proxyClass) {
return proxyCache.get(proxyClass); return proxyCache.get(proxyClass);
} }

@ -639,7 +639,13 @@ public interface RedissonClient {
*/ */
RKeys getKeys(); RKeys getKeys();
<T, K> RedissonAttachedLiveObjectService<T, K> getAttachedLiveObjectService(); /**
* Returns RedissonAttachedLiveObjectService which can be used to
* retrieve live REntity(s)
*
* @return
*/
RedissonAttachedLiveObjectService getAttachedLiveObjectService();
/** /**
* Shuts down Redisson instance <b>NOT</b> Redis server * Shuts down Redisson instance <b>NOT</b> Redis server

@ -67,7 +67,9 @@ public class RedissonReference {
* @return the codec * @return the codec
*/ */
public Codec getCodec() throws Exception { public Codec getCodec() throws Exception {
return (Codec) Class.forName(codec).newInstance(); return codec == null
? null
: (Codec) Class.forName(codec).newInstance();
} }
/** /**

@ -4,10 +4,8 @@ package org.redisson.liveobject;
* *
* @author ruigu * @author ruigu
* *
* @param <T> Entity type
* @param <K> Key type
*/ */
public interface RAttachedLiveObjectService<T, K> extends RLiveObjectService<T, K> { public interface RAttachedLiveObjectService extends RLiveObjectService {
/** /**
* Finds the entity from Redis with the id. * Finds the entity from Redis with the id.
@ -18,8 +16,10 @@ public interface RAttachedLiveObjectService<T, K> extends RLiveObjectService<T,
* of this object will renew this. If it is not been accessed * of this object will renew this. If it is not been accessed
* before the ttl reaches. This object is then expires and * before the ttl reaches. This object is then expires and
* removed from redis. Think of it is been garbage collected. * removed from redis. Think of it is been garbage collected.
* @param <T> Entity type
* @param <K> Key type
* @return In ATTACHED Mode, this always returns a proxy class. Even it does * @return In ATTACHED Mode, this always returns a proxy class. Even it does
* not exist in redis. * not exist in redis.
*/ */
public T get(Class<T> entityClass, K id, long ttl); public <T, K> T get(Class<T> entityClass, K id, long ttl);
} }

@ -9,21 +9,7 @@ import io.netty.util.concurrent.Future;
* @param <T> Entity type * @param <T> Entity type
* @param <K> Key type * @param <K> Key type
*/ */
public interface RDetachedLiveObjectService<T, K> extends RLiveObjectService<T, K> { public interface RDetachedLiveObjectService<T, K> 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<T> entityClass, K id);
/** /**
* Finds the entity from Redis with the id. * Finds the entity from Redis with the id.
@ -34,8 +20,6 @@ public interface RDetachedLiveObjectService<T, K> extends RLiveObjectService<T,
* not exist in redis. * not exist in redis.
* In DETACHED Mode, this returns an instance of the entity class. * In DETACHED Mode, this returns an instance of the entity class.
* IF it doesn't exist in redis, a runtime exception is thrown. * IF it doesn't exist in redis, a runtime exception is thrown.
* @throws org.redisson.liveobject.REntityNotFoundException A Runtime
* exception is thrown when the entity does not exist in Redis.
*/ */
public Future<T> getAsync(Class<T> entityClass, K id); public Future<T> getAsync(Class<T> entityClass, K id);

@ -10,22 +10,22 @@ package org.redisson.liveobject;
* Redis when update is called. * Redis when update is called.
* *
* @author ruigu * @author ruigu
* *
* @param <T> Entity type
* @param <K> Key type
*/ */
public interface RLiveObjectService<T, K> { public interface RLiveObjectService {
/** /**
* Finds the entity from Redis with the id. * Finds the entity from Redis with the id.
* *
* @param entityClass Entity class * @param entityClass Entity class
* @param id identifier * @param id identifier
* @param <T> Entity type
* @param <K> Key type
* @return In ATTACHED Mode, this always returns a proxy class. Even it does * @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 * 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 * entity class. IF it doesn't exist in redis, a runtime exception is
* thrown. * thrown.
*/ */
public T get(Class<T> entityClass, K id); public <T, K> T get(Class<T> entityClass, K id);
} }

@ -28,7 +28,7 @@ public @interface REntity {
@Override @Override
public String getName(Class cls, String idFieldName, Object id) { 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() + "}";
} }
} }

@ -12,7 +12,7 @@ import org.redisson.RedissonClient;
import org.redisson.RedissonReference; import org.redisson.RedissonReference;
import org.redisson.client.RedisException; import org.redisson.client.RedisException;
import org.redisson.client.codec.Codec; 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.RMap;
import org.redisson.core.RObject; import org.redisson.core.RObject;
import org.redisson.liveobject.annotation.REntity; import org.redisson.liveobject.annotation.REntity;
@ -23,25 +23,28 @@ import org.redisson.liveobject.misc.Introspectior;
* *
* @author ruigu * @author ruigu
*/ */
public class AccessorInterceptor<T, K> { public class AccessorInterceptor {
private final RedissonClient redisson; private final RedissonClient redisson;
private final Class originalClass; private final Class originalClass;
private final String idFieldName; private final String idFieldName;
private final REntity.NamingScheme namingScheme; private final REntity.NamingScheme namingScheme;
private final CommandExecutor commandExecutor; private final CommandAsyncExecutor commandExecutor;
private RMap liveMap; 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.redisson = redisson;
this.originalClass = entityClass; this.originalClass = entityClass;
this.idFieldName = idFieldName; this.idFieldName = idFieldName;
this.commandExecutor = commandExecutor; this.commandExecutor = commandExecutor;
this.namingScheme = ((REntity) entityClass.getAnnotation(REntity.class)).namingScheme().newInstance(); this.namingScheme = ((REntity) entityClass.getAnnotation(REntity.class))
.namingScheme().newInstance();
} }
@RuntimeType @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)) { if (isGetter(method, idFieldName)) {
return superMethod.call(); return superMethod.call();
} }
@ -49,7 +52,7 @@ public class AccessorInterceptor<T, K> {
if (isSetter(method, idFieldName)) { if (isSetter(method, idFieldName)) {
superMethod.call(); superMethod.call();
try { try {
liveMap.rename(getMapKey((K) args[0])); liveMap.rename(getMapKey(args[0]));
} catch (RedisException e) { } catch (RedisException e) {
if (e.getMessage() == null || !e.getMessage().startsWith("ERR no such key")) { if (e.getMessage() == null || !e.getMessage().startsWith("ERR no such key")) {
throw e; throw e;
@ -62,10 +65,15 @@ public class AccessorInterceptor<T, K> {
if (isGetter(method, fieldName)) { if (isGetter(method, fieldName)) {
Object result = liveMap.get(fieldName); Object result = liveMap.get(fieldName);
if (method.getReturnType().isAnnotationPresent(REntity.class)) { if (method.getReturnType().isAnnotationPresent(REntity.class)) {
return redisson.getAttachedLiveObjectService().get((Class<Object>) method.getReturnType(), result); return redisson.getAttachedLiveObjectService()
.get((Class<Object>) method.getReturnType(), result);
} else if (result instanceof RedissonReference) { } else if (result instanceof RedissonReference) {
RedissonReference r = ((RedissonReference) result); 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; return result;
} }
@ -81,7 +89,7 @@ public class AccessorInterceptor<T, K> {
return superMethod.call(); return superMethod.call();
} }
private void initLiveMapIfRequired(K id) { private void initLiveMapIfRequired(Object id) {
if (liveMap == null) { if (liveMap == null) {
liveMap = redisson.getMap(getMapKey(id)); liveMap = redisson.getMap(getMapKey(id));
} }
@ -101,18 +109,18 @@ public class AccessorInterceptor<T, K> {
&& method.getName().endsWith(getFieldNameSuffix(fieldName)); && method.getName().endsWith(getFieldNameSuffix(fieldName));
} }
private String getMapKey(K id) { private String getMapKey(Object id) {
return namingScheme.getName(originalClass, idFieldName, id); return namingScheme.getName(originalClass, idFieldName, id);
} }
private K getId(T me) throws Exception { private Object getId(Object me) throws Exception {
return (K) originalClass.getDeclaredMethod("get" + getFieldNameSuffix(idFieldName)).invoke(me); return originalClass.getDeclaredMethod("get" + getFieldNameSuffix(idFieldName)).invoke(me);
} }
private static String getFieldNameSuffix(String fieldName) { private static String getFieldNameSuffix(String fieldName) {
return fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); return fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
} }
private static Object getFieldValue(Object o, String fieldName) throws Exception { private static Object getFieldValue(Object o, String fieldName) throws Exception {
return RedissonAttachedLiveObjectService.getActualClass(o.getClass()).getDeclaredMethod("get" + getFieldNameSuffix(fieldName)).invoke(o); return RedissonAttachedLiveObjectService.getActualClass(o.getClass()).getDeclaredMethod("get" + getFieldNameSuffix(fieldName)).invoke(o);
} }

@ -56,10 +56,142 @@ public class RedissonAttachedLiveObjectServiceTest extends BaseTest {
} }
} }
@REntity
public static class TestREntityWithRMap implements Comparable<TestREntityWithRMap>, 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<TestREntityIdNested>, 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<TestREntityValueNested>, 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 @Test
public void test() { public void test() {
RedissonAttachedLiveObjectService<TestREntity, String> s = redisson.<TestREntity, String>getAttachedLiveObjectService(); RedissonAttachedLiveObjectService s = redisson.getAttachedLiveObjectService();
TestREntity t = s.get(TestREntity.class, "1"); TestREntity t = s.<TestREntity, String>get(TestREntity.class, "1");
assertEquals("1", t.getName()); assertEquals("1", t.getName());
assertTrue(!redisson.getMap(REntity.DefaultNamingScheme.INSTANCE.getName(TestREntity.class, "name", "1")).isExists()); assertTrue(!redisson.getMap(REntity.DefaultNamingScheme.INSTANCE.getName(TestREntity.class, "name", "1")).isExists());
t.setName("3333"); t.setName("3333");
@ -71,4 +203,53 @@ public class RedissonAttachedLiveObjectServiceTest extends BaseTest {
assertTrue(!redisson.getMap(REntity.DefaultNamingScheme.INSTANCE.getName(TestREntity.class, "name", "1")).isExists()); 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")); 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.<TestREntityWithRMap, String>get(TestREntityWithRMap.class, "2");
RMap<String, String> map = redisson.<String, String>getMap("testMap");
t.setValue(map);
map.put("field", "123");
assertEquals("123",
s.<TestREntityWithRMap, String>get(TestREntityWithRMap.class, "2")
.getValue().get("field"));
s.get(TestREntityWithRMap.class, "2").getValue().put("field", "333");
assertEquals("333",
s.<TestREntityWithRMap, String>get(TestREntityWithRMap.class, "2")
.getValue().get("field"));
}
@Test
public void testLiveObjectWithNestedLiveObjectAsId() {
RedissonAttachedLiveObjectService s = redisson.getAttachedLiveObjectService();
TestREntity t1 = s.<TestREntity, String>get(TestREntity.class, "1");
try {
s.<TestREntityIdNested, TestREntity>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.<TestREntityWithRMap, String>get(TestREntityWithRMap.class, "111");
TestREntityValueNested t2 = s.<TestREntityValueNested, String>get(TestREntityValueNested.class, "122");
RMap<String, String> map = redisson.<String, String>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.<TestREntityWithRMap, String>get(TestREntityWithRMap.class, "111")
.getValue().get("field"));
System.out.println("555");
assertEquals("123",
s.<TestREntityValueNested, String>get(TestREntityValueNested.class, "122")
.getValue().getValue().get("field"));
}
} }

Loading…
Cancel
Save