diff --git a/pom.xml b/pom.xml
index 1e09ffc38..2a7e1d307 100644
--- a/pom.xml
+++ b/pom.xml
@@ -228,7 +228,11 @@
zero-allocation-hashing
0.5
-
+
+ net.bytebuddy
+ byte-buddy
+ 1.3.19
+
org.springframework
spring-context
diff --git a/src/main/java/org/redisson/Redisson.java b/src/main/java/org/redisson/Redisson.java
index 4d6f929ab..f735b0c65 100755
--- a/src/main/java/org/redisson/Redisson.java
+++ b/src/main/java/org/redisson/Redisson.java
@@ -531,6 +531,11 @@ public class Redisson implements RedissonClient {
return new RedissonBatch(evictionScheduler, connectionManager);
}
+ @Override
+ public RedissonAttachedLiveObjectService getAttachedLiveObjectService() {
+ return new RedissonAttachedLiveObjectService(this, commandExecutor);
+ }
+
@Override
public void shutdown() {
connectionManager.shutdown();
diff --git a/src/main/java/org/redisson/RedissonAttachedLiveObjectService.java b/src/main/java/org/redisson/RedissonAttachedLiveObjectService.java
new file mode 100644
index 000000000..6351981ea
--- /dev/null
+++ b/src/main/java/org/redisson/RedissonAttachedLiveObjectService.java
@@ -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 implements RAttachedLiveObjectService {
+
+ private static final Map classCache
+ = PlatformDependent.newConcurrentHashMap();
+ private static final Map proxyCache
+ = PlatformDependent.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 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) {
+ 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 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();
+ 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)))
+ .make().load(getClass().getClassLoader(),
+ ClassLoadingStrategy.Default.WRAPPER)
+ .getLoaded());
+ proxyCache.putIfAbsent(classCache.get(entityClass), entityClass);
+ }
+
+ 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 f2aa29c16..3f7bf4184 100755
--- a/src/main/java/org/redisson/RedissonClient.java
+++ b/src/main/java/org/redisson/RedissonClient.java
@@ -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();
+ 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
new file mode 100644
index 000000000..e6c1447a3
--- /dev/null
+++ b/src/main/java/org/redisson/RedissonReference.java
@@ -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();
+ }
+
+
+}
diff --git a/src/main/java/org/redisson/liveobject/RAttachedLiveObjectService.java b/src/main/java/org/redisson/liveobject/RAttachedLiveObjectService.java
new file mode 100644
index 000000000..66fab5b76
--- /dev/null
+++ b/src/main/java/org/redisson/liveobject/RAttachedLiveObjectService.java
@@ -0,0 +1,25 @@
+package org.redisson.liveobject;
+
+/**
+ *
+ * @author ruigu
+ *
+ * @param Entity type
+ * @param Key type
+ */
+public interface RAttachedLiveObjectService extends RLiveObjectService {
+
+ /**
+ * 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 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
new file mode 100644
index 000000000..1f0accddd
--- /dev/null
+++ b/src/main/java/org/redisson/liveobject/RDetachedLiveObjectService.java
@@ -0,0 +1,90 @@
+package org.redisson.liveobject;
+
+import io.netty.util.concurrent.Future;
+
+/**
+ *
+ * @author ruigu
+ *
+ * @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);
+
+ /**
+ * 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 getAsync(Class 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 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 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 removeAsync(K id);
+}
diff --git a/src/main/java/org/redisson/liveobject/RLiveObjectService.java b/src/main/java/org/redisson/liveobject/RLiveObjectService.java
new file mode 100644
index 000000000..08062ebdb
--- /dev/null
+++ b/src/main/java/org/redisson/liveobject/RLiveObjectService.java
@@ -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 Entity type
+ * @param Key type
+ */
+public interface 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, a runtime exception is
+ * thrown.
+ */
+ 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
new file mode 100644
index 000000000..3624d9aa9
--- /dev/null
+++ b/src/main/java/org/redisson/liveobject/annotation/REntity.java
@@ -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() + "}";
+ }
+
+ }
+}
diff --git a/src/main/java/org/redisson/liveobject/annotation/RId.java b/src/main/java/org/redisson/liveobject/annotation/RId.java
new file mode 100644
index 000000000..0dfbf6aef
--- /dev/null
+++ b/src/main/java/org/redisson/liveobject/annotation/RId.java
@@ -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;
+}
diff --git a/src/main/java/org/redisson/liveobject/core/AccessorInterceptor.java b/src/main/java/org/redisson/liveobject/core/AccessorInterceptor.java
new file mode 100644
index 000000000..2c77164d4
--- /dev/null
+++ b/src/main/java/org/redisson/liveobject/core/AccessorInterceptor.java
@@ -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 {
+
+ 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