pull/527/head
jackygurui 9 years ago
parent 708d049145
commit d9db8a4c0f

@ -233,6 +233,11 @@
<artifactId>byte-buddy</artifactId>
<version>1.3.19</version>
</dependency>
<dependency>
<groupId>org.jodd</groupId>
<artifactId>jodd-bean</artifactId>
<version>3.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>

@ -1,6 +1,13 @@
package org.redisson;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.io.IOException;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jodd.bean.BeanCopy;
import jodd.bean.BeanUtil;
//import java.util.concurrent.TimeUnit;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.field.FieldDescription;
@ -10,6 +17,7 @@ import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.FieldProxy;
import net.bytebuddy.matcher.ElementMatchers;
import org.redisson.codec.JsonJacksonCodec;
//import org.redisson.core.RExpirable;
//import org.redisson.core.RExpirableAsync;
//import org.redisson.core.RMap;
@ -30,7 +38,7 @@ public class RedissonLiveObjectService implements RLiveObjectService {
private final Map<Class, Class> classCache;
private final RedissonClient redisson;
private final ObjectMapper objectMapper = JsonJacksonCodec.INSTANCE.getObjectMapper();
private final CodecProvider codecProvider;
public RedissonLiveObjectService(RedissonClient redisson, Map<Class, Class> classCache, CodecProvider codecProvider) {
@ -50,32 +58,117 @@ public class RedissonLiveObjectService implements RLiveObjectService {
// map.expire(timeToLive, timeUnit);
// return instance;
// }
@Override
public <T, K> T get(Class<T> entityClass, K id) {
try {
T instance;
try {
instance = getProxyClass(entityClass).newInstance();
} catch (Exception exception) {
instance = getProxyClass(entityClass).getDeclaredConstructor(id.getClass()).newInstance(id);
}
((RLiveObject) instance).setLiveObjectId(id);
return instance;
return instantiateLiveObject(getProxyClass(entityClass), id);
} catch (Exception ex) {
unregisterClass(entityClass);
throw new RuntimeException(ex);
}
}
@Override
public <T> T attach(T detachedObject) {
Class<T> entityClass = (Class<T>) detachedObject.getClass();
try {
Class<? extends T> proxyClass = getProxyClass(entityClass);
String idFieldName = getRIdFieldName(detachedObject.getClass());
return instantiateLiveObject(proxyClass,
BeanUtil.pojo.getSimpleProperty(detachedObject, idFieldName));
} catch (Exception ex) {
unregisterClass(entityClass);
throw new RuntimeException(ex);
}
}
private <T, K> Class<? extends T> getProxyClass(Class<T> entityClass) throws Exception {
@Override
public <T> T merge(T detachedObject) {
T attachedObject = attach(detachedObject);
copy(detachedObject, attachedObject);
return attachedObject;
}
@Override
public <T> T persist(T detachedObject) {
T attachedObject = attach(detachedObject);
if (asLiveObject(attachedObject).isPhantom()) {
copy(detachedObject, attachedObject);
return attachedObject;
}
throw new IllegalStateException("This REntity already exists.");
}
@Override
public <T> T detach(T attachedObject) {
try {
//deep copy
return objectMapper.<T>convertValue(attachedObject, (Class<T>)attachedObject.getClass().getSuperclass());
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
@Override
public <T> void remove(T attachedObject) {
asLiveObject(attachedObject).delete();
}
@Override
public <T, K> void remove(Class<T> entityClass, K id) {
asLiveObject(get(entityClass, id)).delete();
}
@Override
public <T> RLiveObject asLiveObject(T instance) {
return (RLiveObject) instance;
}
@Override
public <T> boolean isLiveObject(T instance) {
return instance instanceof RLiveObject;
}
private <T> void copy(T detachedObject, T attachedObject) {
String idFieldName = getRIdFieldName(detachedObject.getClass());
BeanCopy.beans(detachedObject, attachedObject)
.ignoreNulls(true)
.exclude(idFieldName)
.copy();
}
private String getRIdFieldName(Class cls) {
return Introspectior.getFieldsWithAnnotation(cls, RId.class)
.getOnly()
.getName();
}
private <T, K> T instantiateLiveObject(Class<T> proxyClass, K id) throws Exception {
T instance = instantiate(proxyClass, id);
asLiveObject(instance).setLiveObjectId(id);
return instance;
}
private <T, K> T instantiate(Class<T> cls, K id) throws Exception {
T instance;
try {
instance = cls.newInstance();
} catch (Exception exception) {
instance = cls.getDeclaredConstructor(id.getClass()).newInstance(id);
}
return instance;
}
private <T> Class<? extends T> getProxyClass(Class<T> entityClass) throws Exception {
if (!classCache.containsKey(entityClass)) {
validateClass(entityClass);
registerClass(entityClass);
}
return classCache.get(entityClass);
}
private <T, K> void registerClass(Class<T> entityClass) throws Exception {
private <T> void validateClass(Class<T> entityClass) throws Exception {
if (entityClass.isAnonymousClass() || entityClass.isLocalClass()) {
throw new IllegalArgumentException(entityClass.getName() + " is not publically accessable.");
}
@ -98,6 +191,9 @@ public class RedissonLiveObjectService implements RLiveObjectService {
if (entityClass.getDeclaredField(idFieldName).getType().isAssignableFrom(RObject.class)) {
throw new IllegalArgumentException("Field with RId annotation cannot be a type of RObject");
}
}
private <T> void registerClass(Class<T> entityClass) throws Exception {
DynamicType.Builder<T> builder = new ByteBuddy()
.subclass(entityClass);
for (FieldDescription.InDefinedShape field
@ -109,23 +205,24 @@ public class RedissonLiveObjectService implements RLiveObjectService {
Introspectior.getTypeDescription(RLiveObject.class))
.and(ElementMatchers.isGetter().or(ElementMatchers.isSetter())))
.intercept(MethodDelegation.to(
new LiveObjectInterceptor(redisson, codecProvider, entityClass, idFieldName))
new LiveObjectInterceptor(redisson, codecProvider, entityClass,
getRIdFieldName(entityClass)))
.appendParameterBinder(FieldProxy.Binder
.install(LiveObjectInterceptor.Getter.class,
LiveObjectInterceptor.Setter.class)))
.implement(RLiveObject.class)
// .method(ElementMatchers.isDeclaredBy(RExpirable.class)
// .or(ElementMatchers.isDeclaredBy(RExpirableAsync.class))
// .or(ElementMatchers.isDeclaredBy(RObject.class))
// .or(ElementMatchers.isDeclaredBy(RObjectAsync.class)))
// .intercept(MethodDelegation.to(ExpirableInterceptor.class))
// .implement(RExpirable.class)
// .method(ElementMatchers.isDeclaredBy(RExpirable.class)
// .or(ElementMatchers.isDeclaredBy(RExpirableAsync.class))
// .or(ElementMatchers.isDeclaredBy(RObject.class))
// .or(ElementMatchers.isDeclaredBy(RObjectAsync.class)))
// .intercept(MethodDelegation.to(ExpirableInterceptor.class))
// .implement(RExpirable.class)
.method(ElementMatchers.not(ElementMatchers.isDeclaredBy(Object.class))
.and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RLiveObject.class)))
// .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RExpirable.class)))
// .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RExpirableAsync.class)))
// .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RObject.class)))
// .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RObjectAsync.class)))
// .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RExpirable.class)))
// .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RExpirableAsync.class)))
// .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RObject.class)))
// .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(RObjectAsync.class)))
.and(ElementMatchers.isGetter()
.or(ElementMatchers.isSetter()))
.and(ElementMatchers.isPublic()))

@ -159,4 +159,7 @@ public class JsonJacksonCodec implements Codec {
return getMapValueEncoder();
}
public ObjectMapper getObjectMapper() {
return mapObjectMapper;
}
}

@ -29,13 +29,38 @@ public interface RLiveObject {
// public RMap getLiveObjectLiveMap();
/**
* @return the liveObjectId
* Returns the value of the field that has the RId annotation.
* @return liveObjectId
*/
public Object getLiveObjectId();
Object getLiveObjectId();
/**
* Change the value of the field that has the RId annotation. Since the
* liveObjectId is encoded as a part of the name of the underlying RMap,
* this action will result in renaming the underlying RMap based on the
* naming scheme specified in the REntity annotation of the instance class.
*
* @param liveObjectId the liveObjectId to set
* @see org.redisson.core.RMap
*/
public void setLiveObjectId(Object liveObjectId);
void setLiveObjectId(Object liveObjectId);
/**
* Returns true if this object holds no other values apart from the field
* annotated with RId. This involves in invoking the isExist() method on the
* underlying RMap. Since the field with RId annotation is encoded in the
* name of the underlying RMap, so to ensure the map exist in redis, set a
* non null value to any of the other fields.
*
* @return
* @see org.redisson.core.RMap
*/
boolean isPhantom();
/**
* Deletes the underlying RMap.
* @return
*/
boolean delete();
}

@ -18,12 +18,9 @@ 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
* Entity's getters and setters operations gets redirected to Redis
* automatically.
*
* In DETACHED Mode, entity's field values are kept local and only pushed to
* Redis when update is called.
*
* @author Rui Gu (https://github.com/jackygurui)
*
*/
@ -32,15 +29,114 @@ public interface RLiveObjectService {
/**
* Finds the entity from Redis with the id.
*
* The entityClass should have a field annotated with RId, and the
* entityClass itself should have REntity annotated. The type of the RId can
* be anything <b>except</b> the followings:
* <ol>
* <li>An array i.e. byte[], int[], Integer[], etc.</li>
* <li>or a RObject i.e. RedissonMap</li>
* <li>or a Class with REntity annotation.</li>
* </ol>
*
*
* @param entityClass Entity class
* @param id identifier
* @param <T> Entity type
* @param <K> 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.
* @return Always returns a proxy class. Even it does not exist in redis.
*/
<T, K> T get(Class<T> entityClass, K id);
/**
* Returns proxied attached object for the detached object. Discard all the
* field values already in the instance.
*
* The class representing this object should have a field annotated with
* RId, and the object should hold a non null value in that field.
*
* If this object is not in redis then a new <b>blank</b> proxied instance
* with the same RId field value will be created.
*
* @param <T> Entity type
* @param detachedObject
* @return
*/
<T> T attach(T detachedObject);
/**
* Returns proxied attached object for the detached object. Transfers all the
* <b>NON NULL</b> field values to the redis server. It does not remove any
* existing data in redis in case of the field value is null.
*
* The class representing this object should have a field annotated with
* RId, and the object should hold a non null value in that field.
*
* If this object is not in redis then a new hash key will be created to
* store it.
*
* @param <T> Entity type
* @param detachedObject
* @return
*/
<T> T merge(T detachedObject);
/**
* Returns proxied attached object for the detached object. Transfers all the
* <b>NON NULL</b> field values to the redis server. Only when the it does
* not already exist.
*
* The class representing this object should have a field annotated with
* RId, and the object should hold a non null value in that field.
*
* If this object is not in redis then a new hash key will be created to
* store it.
*
* @param <T> Entity type
* @param detachedObject
* @return
*/
<T> T persist(T detachedObject);
/**
* Returns unproxied detached object for the attached object.
*
* @param <T> Entity type
* @param attachedObject
* @return
*/
<T> T detach(T attachedObject);
/**
* Deletes attached object including all nested objects.
*
* @param <T> Entity type
* @param attachedObject
*/
<T> void remove(T attachedObject);
/**
* Deletes object by class and id including all nested objects
*
* @param <T> Entity type
* @param <K> Key type
* @param entityClass
* @param id
*/
public <T, K> T get(Class<T> entityClass, K id);
<T, K> void remove(Class<T> entityClass, K id);
/**
*
* @param <T>
* @param instance
* @return
*/
<T> RLiveObject asLiveObject(T instance);
/**
*
* @param <T>
* @param instance
* @return
*/
<T> boolean isLiveObject(T instance);
}

@ -98,7 +98,7 @@ public @interface REntity {
}
}
public static String bytesToHex(byte[] bytes) {
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
@ -108,7 +108,7 @@ public @interface REntity {
return new String(hexChars);
}
public static byte[] hexToBytes(String s) {
private static byte[] hexToBytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {

@ -1,17 +1,17 @@
/**
* Copyright 2014 Nikita Koksharov, Nickolay Borbit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.redisson.liveobject.core;
@ -82,6 +82,9 @@ public class LiveObjectInterceptor {
//TODO: distributed locking maybe required.
String idKey = getMapKey(args[0]);
if (map != null) {
if (map.getName().equals(idKey)) {
return null;
}
try {
map.rename(getMapKey(args[0]));
} catch (RedisException e) {
@ -109,6 +112,14 @@ public class LiveObjectInterceptor {
return map;
}
if ("isPhantom".equals(method.getName())) {
return !map.isExists();
}
if ("delete".equals(method.getName())) {
return map.delete();
}
throw new NoSuchMethodException();
}

@ -15,9 +15,7 @@
*/
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;

@ -343,25 +343,86 @@ public class RedissonAttachedLiveObjectServiceTest extends BaseTest {
}
public static class ObjectId implements Serializable {
private int id;
public ObjectId() {
}
public ObjectId(int id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ObjectId)) {
return false;
}
return id == ((ObjectId) obj).id;
}
@Override
public int hashCode() {
int hash = 3;
hash = 11 * hash + this.id;
return hash;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
@Override
public String toString() {
return "" + id;
}
}
@Test
public void testSerializerable() {
RLiveObjectService liveObjectService = redisson.getLiveObjectService();
TestSerializable t = liveObjectService.get(TestSerializable.class, "55555");
assertTrue(Objects.equals("55555", t.getId()));
t = liveObjectService.get(TestSerializable.class, 90909l);
assertTrue(Objects.equals(90909l, t.getId()));
t = liveObjectService.get(TestSerializable.class, 90909);
assertTrue(Objects.equals(90909, t.getId()));
t = liveObjectService.get(TestSerializable.class, new ObjectId(9090909));
assertTrue(Objects.equals(new ObjectId(9090909), t.getId()));
t = liveObjectService.get(TestSerializable.class, new Byte("0"));
assertEquals(new Byte("0"), Byte.valueOf(t.getId().toString()));
t = liveObjectService.get(TestSerializable.class, (byte) 90);
assertEquals((byte) 90, Byte.parseByte(t.getId().toString()));
t = liveObjectService.get(TestSerializable.class, Arrays.asList(1, 2, 3, 4));
List<Integer> l = new ArrayList();
l.addAll(Arrays.asList(1, 2, 3, 4));
assertTrue(l.removeAll((List) t.getId()));
assertTrue(l.isEmpty());
try {
liveObjectService.get(TestSerializable.class, new int[]{1, 2, 3, 4, 5});
} catch (Exception e) {
assertEquals("RId value cannot be an array.", e.getCause().getMessage());
}
try {
liveObjectService.get(TestSerializable.class, new byte[]{1, 2, 3, 4, 5});
} catch (Exception e) {
assertEquals("RId value cannot be an array.", e.getCause().getMessage());
}
}
}

Loading…
Cancel
Save