Initial commit for LiveObject

Proof of concept, work in progress.
pull/527/head
jackygurui 9 years ago
parent f8dfdb7154
commit 7db9f30577

@ -228,7 +228,11 @@
<artifactId>zero-allocation-hashing</artifactId>
<version>0.5</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.3.19</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>

@ -531,6 +531,11 @@ public class Redisson implements RedissonClient {
return new RedissonBatch(evictionScheduler, connectionManager);
}
@Override
public <T, K> RedissonAttachedLiveObjectService<T, K> getAttachedLiveObjectService() {
return new RedissonAttachedLiveObjectService<T, K>(this, commandExecutor);
}
@Override
public void shutdown() {
connectionManager.shutdown();

@ -0,0 +1,75 @@
package org.redisson;
import io.netty.util.internal.PlatformDependent;
import java.util.Map;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import org.redisson.command.CommandExecutor;
import org.redisson.liveobject.RAttachedLiveObjectService;
import org.redisson.liveobject.annotation.RId;
import org.redisson.liveobject.core.AccessorInterceptor;
import org.redisson.liveobject.misc.Introspectior;
public class RedissonAttachedLiveObjectService<T, K> implements RAttachedLiveObjectService<T, K> {
private static final Map<Class, Class> classCache
= PlatformDependent.<Class, Class>newConcurrentHashMap();
private static final Map<Class, Class> proxyCache
= PlatformDependent.<Class, Class>newConcurrentHashMap();
private final RedissonClient redisson;
private final CommandExecutor commandExecutor;
public RedissonAttachedLiveObjectService(RedissonClient redisson, CommandExecutor commandExecutor) {
this.redisson = redisson;
this.commandExecutor = commandExecutor;
}
//TODO: Support ID Generator
@Override
public T get(Class<T> 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<T> entityClass, K id) {
try {
//TODO: support class with no arg constructor
return getProxyClass(entityClass).getConstructor(id.getClass()).newInstance(id);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private Class<? extends T> getProxyClass(Class<T> entityClass) throws Exception {
if (!classCache.containsKey(entityClass)) {
registerClass(entityClass);
}
return classCache.get(entityClass);
}
private void registerClass(Class<T> entityClass) throws Exception {
//TODO: check annotation on the entityClass
String idFieldName = Introspectior.getFieldsWithAnnotation(entityClass, RId.class)
.getOnly()
.getName();
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<T, K>(redisson, entityClass, idFieldName, commandExecutor)))
.make().load(getClass().getClassLoader(),
ClassLoadingStrategy.Default.WRAPPER)
.getLoaded());
proxyCache.putIfAbsent(classCache.get(entityClass), entityClass);
}
public static Class getActualClass(Class proxyClass) {
return proxyCache.get(proxyClass);
}
}

@ -21,6 +21,7 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.redisson.client.codec.Codec;
import org.redisson.command.CommandExecutor;
import org.redisson.core.ClusterNode;
import org.redisson.core.Node;
import org.redisson.core.NodesGroup;
@ -638,6 +639,8 @@ public interface RedissonClient {
*/
RKeys getKeys();
<T, K> RedissonAttachedLiveObjectService<T, K> getAttachedLiveObjectService();
/**
* Shuts down Redisson instance <b>NOT</b> Redis server
*

@ -0,0 +1,81 @@
package org.redisson;
import org.redisson.client.codec.Codec;
import org.redisson.core.RObject;
/**
*
* @author ruigu
*/
public class RedissonReference {
private String type;
private Object keyName;
private String codec;
public RedissonReference() {
}
public RedissonReference(Class<? extends RObject> type, Object keyName) {
this.type = type.getCanonicalName();
this.keyName = keyName;
this.codec = null;
}
public RedissonReference(Class<? extends RObject> type, Object keyName, Codec codec) {
this.type = type.getCanonicalName();
this.keyName = keyName;
this.codec = codec.getClass().getCanonicalName();
}
public boolean isDefaultCodec() {
return codec == null;
}
/**
* @return the type
*/
public Class<? extends RObject> getType() {
try {
return (Class<? extends RObject>) Class.forName(type);
} catch (ClassNotFoundException ex) {
return null;
}
}
/**
* @param type the type to set
*/
public void setType(Class<? extends RObject> type) {
this.type = type.getCanonicalName();
}
/**
* @return the keyName
*/
public Object getKeyName() {
return keyName;
}
/**
* @param keyName the keyName to set
*/
public void setKeyName(Object keyName) {
this.keyName = keyName;
}
/**
* @return the codec
*/
public Codec getCodec() throws Exception {
return (Codec) Class.forName(codec).newInstance();
}
/**
* @param codec the codec to set
*/
public void setCodec(Codec codec) {
this.codec = codec.getClass().getCanonicalName();
}
}

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

@ -0,0 +1,90 @@
package org.redisson.liveobject;
import io.netty.util.concurrent.Future;
/**
*
* @author ruigu
*
* @param <T> Entity type
* @param <K> Key type
*/
public interface RDetachedLiveObjectService<T, K> extends RLiveObjectService<T, K> {
/**
* 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.
*
* @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, 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);
/**
* Persist the instance into redis
*
* @param instance the instance to be persisted
* @return K The id of the object.
*/
public K persist(T instance);
/**
* Persist the instance into redis
*
* @param instance the instance to be persisted
* @return K The id of the object.
*/
public Future<K> persistAsync(T instance);
/**
* Persist the instance into redis with specified time to live.
*
* @param instance the instance to be persisted
* @param ttl the time to live of the instance
* @return K The id of the object.
*/
public K persist(T instance, long ttl);
/**
* Persist the instance into redis with specified time to live.
*
* @param instance
* @param ttl the time to live of the instance
* @return K The id of the object.
*/
public Future<K> persistAsync(T instance, long ttl);
/**
* Remove the instance from redis by specifying the id
*
* @param id
*/
public void remove(K id);
/**
* Remove the instance from redis by specifying the id
*
* @param id
* @return Future.
*/
public Future<Void> removeAsync(K id);
}

@ -0,0 +1,31 @@
package org.redisson.liveobject;
/**
* The pre-registration of each entity class is not necessary.
*
* In ATTACHED Mode, entity's getters and setters propagate operations to Redis
* automatically.
*
* In DETACHED Mode, entity's field values are kept local and only pushed to
* Redis when update is called.
*
* @author ruigu
*
* @param <T> Entity type
* @param <K> Key type
*/
public interface RLiveObjectService<T, K> {
/**
* 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, a runtime exception is
* thrown.
*/
public T get(Class<T> entityClass, K id);
}

@ -0,0 +1,35 @@
package org.redisson.liveobject.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* @author ruigu
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface REntity {
Class<? extends NamingScheme> namingScheme() default DefaultNamingScheme.class;
public interface NamingScheme {
public String getName(Class cls, String idFieldName, Object id);
}
public class DefaultNamingScheme implements NamingScheme {
public static final DefaultNamingScheme INSTANCE = new DefaultNamingScheme();
@Override
public String getName(Class cls, String idFieldName, Object id) {
return "redisson_live_object:{class=" + cls.getCanonicalName() + ", " + idFieldName + "=" + id.toString() + "}";
}
}
}

@ -0,0 +1,17 @@
package org.redisson.liveobject.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.redisson.liveobject.resolver.FieldValueAsIdGenerator;
/**
*
* @author ruigu
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface RId {
Class generator() default FieldValueAsIdGenerator.class;
}

@ -0,0 +1,128 @@
package org.redisson.liveobject.core;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import org.redisson.RedissonAttachedLiveObjectService;
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.core.RMap;
import org.redisson.core.RObject;
import org.redisson.liveobject.annotation.REntity;
import org.redisson.liveobject.annotation.RId;
import org.redisson.liveobject.misc.Introspectior;
/**
*
* @author ruigu
*/
public class AccessorInterceptor<T, K> {
private final RedissonClient redisson;
private final Class originalClass;
private final String idFieldName;
private final REntity.NamingScheme namingScheme;
private final CommandExecutor commandExecutor;
private RMap liveMap;
public AccessorInterceptor(RedissonClient redisson, Class entityClass, String idFieldName, CommandExecutor commandExecutor) throws Exception {
this.redisson = redisson;
this.originalClass = entityClass;
this.idFieldName = idFieldName;
this.commandExecutor = commandExecutor;
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 {
if (isGetter(method, idFieldName)) {
return superMethod.call();
}
initLiveMapIfRequired(getId(me));
if (isSetter(method, idFieldName)) {
superMethod.call();
try {
liveMap.rename(getMapKey((K) args[0]));
} catch (RedisException e) {
if (e.getMessage() == null || !e.getMessage().startsWith("ERR no such key")) {
throw e;
}
}
liveMap = null;
return null;
}
String fieldName = getFieldName(method);
if (isGetter(method, fieldName)) {
Object result = liveMap.get(fieldName);
if (method.getReturnType().isAnnotationPresent(REntity.class)) {
return redisson.getAttachedLiveObjectService().get((Class<Object>) 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 result;
}
if (isSetter(method, fieldName)) {
if (method.getParameterTypes()[0].isAnnotationPresent(REntity.class)) {
return liveMap.fastPut(fieldName, getREntityId(args[0]));
} else if (args[0] instanceof RObject) {
RObject ar = (RObject) args[0];
return liveMap.fastPut(fieldName, new RedissonReference((Class<RObject>) args[0].getClass(), ar.getName()));
}
return liveMap.fastPut(fieldName, args[0]);
}
return superMethod.call();
}
private void initLiveMapIfRequired(K id) {
if (liveMap == null) {
liveMap = redisson.getMap(getMapKey(id));
}
}
private String getFieldName(Method method) {
return method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4);
}
private boolean isGetter(Method method, String fieldName) {
return method.getName().startsWith("get")
&& method.getName().endsWith(getFieldNameSuffix(fieldName));
}
private boolean isSetter(Method method, String fieldName) {
return method.getName().startsWith("set")
&& method.getName().endsWith(getFieldNameSuffix(fieldName));
}
private String getMapKey(K id) {
return namingScheme.getName(originalClass, idFieldName, id);
}
private K getId(T me) throws Exception {
return (K) 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);
}
private static Object getREntityId(Object o) throws Exception {
String idName = Introspectior
.getFieldsWithAnnotation(RedissonAttachedLiveObjectService.getActualClass(o.getClass()), RId.class)
.getOnly()
.getName();
return getFieldValue(o, idName);
}
}

@ -0,0 +1,52 @@
package org.redisson.liveobject.misc;
import io.netty.util.internal.PlatformDependent;
import java.lang.annotation.Annotation;
import java.util.concurrent.ConcurrentMap;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatchers;
/**
*
* @author ruigu
*/
public class Introspectior {
private static final ConcurrentMap<Class, TypeDescription.ForLoadedType> tCache = PlatformDependent.newConcurrentHashMap();
public static TypeDescription.ForLoadedType getTypeDescription(Class c) {
if (!tCache.containsKey(c)) {
return new TypeDescription.ForLoadedType(c);
}
return tCache.get(c);
}
public static MethodDescription getMethodDescription(Class c, String method) {
if (method == null || method.isEmpty()) {
return null;
}
return getTypeDescription(c)
.getDeclaredMethods()
.filter(ElementMatchers.hasMethodName(method))
.getOnly();
}
public static FieldDescription getFieldDescription(Class c, String field) {
if (field == null || field.isEmpty()) {
return null;
}
return getTypeDescription(c)
.getDeclaredFields()
.filter(ElementMatchers.named(field))
.getOnly();
}
public static FieldList<FieldDescription.InDefinedShape> getFieldsWithAnnotation(Class c, Class<? extends Annotation> a) {
return getTypeDescription(c)
.getDeclaredFields()
.filter(ElementMatchers.isAnnotatedWith(a));
}
}

@ -0,0 +1,18 @@
package org.redisson.liveobject.resolver;
import org.redisson.liveobject.annotation.RId;
/**
*
* @author ruigu
*/
public class FieldValueAsIdGenerator implements Resolver<Object, RId, String>{
public static final FieldValueAsIdGenerator INSTANCE = new FieldValueAsIdGenerator();
@Override
public String resolve(Object value, RId index) {
return value.toString();
}
}

@ -0,0 +1,15 @@
package org.redisson.liveobject.resolver;
import java.lang.annotation.Annotation;
/**
*
* @author ruigu
* @param <T> Field instance
* @param <A> Annotation to resolve
*/
public interface Resolver<T, A extends Annotation, V> {
public V resolve(T value, A index);
}

@ -0,0 +1,74 @@
package org.redisson;
import java.io.Serializable;
import static org.junit.Assert.*;
import org.junit.Test;
import org.redisson.core.RMap;
import org.redisson.liveobject.annotation.REntity;
import org.redisson.liveobject.annotation.RId;
/**
*
* @author ruigu
*/
public class RedissonAttachedLiveObjectServiceTest extends BaseTest {
@REntity
public static class TestREntity implements Comparable<TestREntity>, Serializable {
@RId
private String name;
private String value;
public TestREntity(String name) {
this.name = name;
}
public TestREntity(String name, String value) {
super();
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public void setName(String name) {
this.name = name;
}
public void setValue(String value) {
this.value = value;
}
@Override
public int compareTo(TestREntity o) {
int res = name.compareTo(o.name);
if (res == 0) {
return value.compareTo(o.value);
}
return res;
}
}
@Test
public void test() {
RedissonAttachedLiveObjectService<TestREntity, String> s = redisson.<TestREntity, String>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");
assertEquals("3333", t.getName());
assertTrue(!redisson.getMap(REntity.DefaultNamingScheme.INSTANCE.getName(TestREntity.class, "name", "3333")).isExists());
t.setValue("111");
assertEquals("111", t.getValue());
assertTrue(redisson.getMap(REntity.DefaultNamingScheme.INSTANCE.getName(TestREntity.class, "name", "3333")).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"));
}
}
Loading…
Cancel
Save