From d9db8a4c0f5ebdba18a6cc118f61dc74babaf308 Mon Sep 17 00:00:00 2001 From: jackygurui Date: Fri, 17 Jun 2016 01:04:05 +0100 Subject: [PATCH] WIP --- pom.xml | 5 + .../redisson/RedissonLiveObjectService.java | 143 +++++++++++++++--- .../org/redisson/codec/JsonJacksonCodec.java | 3 + .../org/redisson/liveobject/RLiveObject.java | 31 +++- .../liveobject/RLiveObjectService.java | 114 ++++++++++++-- .../liveobject/annotation/REntity.java | 4 +- .../core/LiveObjectInterceptor.java | 27 +++- .../liveobject/misc/Introspectior.java | 2 - ...RedissonAttachedLiveObjectServiceTest.java | 61 ++++++++ 9 files changed, 343 insertions(+), 47 deletions(-) diff --git a/pom.xml b/pom.xml index 8fd1e9c37..3015361d7 100644 --- a/pom.xml +++ b/pom.xml @@ -233,6 +233,11 @@ byte-buddy 1.3.19 + + org.jodd + jodd-bean + 3.7.1 + org.springframework spring-context diff --git a/src/main/java/org/redisson/RedissonLiveObjectService.java b/src/main/java/org/redisson/RedissonLiveObjectService.java index 420699bb7..8b1c268f6 100644 --- a/src/main/java/org/redisson/RedissonLiveObjectService.java +++ b/src/main/java/org/redisson/RedissonLiveObjectService.java @@ -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 classCache; private final RedissonClient redisson; - + private final ObjectMapper objectMapper = JsonJacksonCodec.INSTANCE.getObjectMapper(); private final CodecProvider codecProvider; public RedissonLiveObjectService(RedissonClient redisson, Map classCache, CodecProvider codecProvider) { @@ -50,32 +58,117 @@ public class RedissonLiveObjectService implements RLiveObjectService { // map.expire(timeToLive, timeUnit); // return instance; // } - + @Override public T get(Class 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 attach(T detachedObject) { + Class entityClass = (Class) detachedObject.getClass(); + try { + Class 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 Class getProxyClass(Class entityClass) throws Exception { + @Override + public T merge(T detachedObject) { + T attachedObject = attach(detachedObject); + copy(detachedObject, attachedObject); + return attachedObject; + } + + @Override + public 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 detach(T attachedObject) { + try { + //deep copy + return objectMapper.convertValue(attachedObject, (Class)attachedObject.getClass().getSuperclass()); + } catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + @Override + public void remove(T attachedObject) { + asLiveObject(attachedObject).delete(); + } + + @Override + public void remove(Class entityClass, K id) { + asLiveObject(get(entityClass, id)).delete(); + } + + @Override + public RLiveObject asLiveObject(T instance) { + return (RLiveObject) instance; + } + + @Override + public boolean isLiveObject(T instance) { + return instance instanceof RLiveObject; + } + + private 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 instantiateLiveObject(Class proxyClass, K id) throws Exception { + T instance = instantiate(proxyClass, id); + asLiveObject(instance).setLiveObjectId(id); + return instance; + } + + private T instantiate(Class cls, K id) throws Exception { + T instance; + try { + instance = cls.newInstance(); + } catch (Exception exception) { + instance = cls.getDeclaredConstructor(id.getClass()).newInstance(id); + } + return instance; + } + + private Class getProxyClass(Class entityClass) throws Exception { if (!classCache.containsKey(entityClass)) { + validateClass(entityClass); registerClass(entityClass); } return classCache.get(entityClass); } - private void registerClass(Class entityClass) throws Exception { + private void validateClass(Class 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 void registerClass(Class entityClass) throws Exception { DynamicType.Builder 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())) diff --git a/src/main/java/org/redisson/codec/JsonJacksonCodec.java b/src/main/java/org/redisson/codec/JsonJacksonCodec.java index e90c5cef2..6d66fdda5 100755 --- a/src/main/java/org/redisson/codec/JsonJacksonCodec.java +++ b/src/main/java/org/redisson/codec/JsonJacksonCodec.java @@ -159,4 +159,7 @@ public class JsonJacksonCodec implements Codec { return getMapValueEncoder(); } + public ObjectMapper getObjectMapper() { + return mapObjectMapper; + } } diff --git a/src/main/java/org/redisson/liveobject/RLiveObject.java b/src/main/java/org/redisson/liveobject/RLiveObject.java index 4ae4b3a05..9ab075f5d 100644 --- a/src/main/java/org/redisson/liveobject/RLiveObject.java +++ b/src/main/java/org/redisson/liveobject/RLiveObject.java @@ -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(); + } diff --git a/src/main/java/org/redisson/liveobject/RLiveObjectService.java b/src/main/java/org/redisson/liveobject/RLiveObjectService.java index a9a5f07af..56aa83c61 100644 --- a/src/main/java/org/redisson/liveobject/RLiveObjectService.java +++ b/src/main/java/org/redisson/liveobject/RLiveObjectService.java @@ -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 except the followings: + *
    + *
  1. An array i.e. byte[], int[], Integer[], etc.
  2. + *
  3. or a RObject i.e. RedissonMap
  4. + *
  5. or a Class with REntity annotation.
  6. + *
+ * + * * @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. + * @return Always returns a proxy class. Even it does not exist in redis. + */ + T get(Class 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 blank proxied instance + * with the same RId field value will be created. + * + * @param Entity type + * @param detachedObject + * @return + */ + T attach(T detachedObject); + + /** + * Returns proxied attached object for the detached object. Transfers all the + * NON NULL 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 Entity type + * @param detachedObject + * @return + */ + T merge(T detachedObject); + + /** + * Returns proxied attached object for the detached object. Transfers all the + * NON NULL 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 Entity type + * @param detachedObject + * @return + */ + T persist(T detachedObject); + + /** + * Returns unproxied detached object for the attached object. + * + * @param Entity type + * @param attachedObject + * @return + */ + T detach(T attachedObject); + + /** + * Deletes attached object including all nested objects. + * + * @param Entity type + * @param attachedObject + */ + void remove(T attachedObject); + + /** + * Deletes object by class and id including all nested objects + * + * @param Entity type + * @param Key type + * @param entityClass + * @param id */ - public T get(Class entityClass, K id); + void remove(Class entityClass, K id); + /** + * + * @param + * @param instance + * @return + */ + RLiveObject asLiveObject(T instance); + + /** + * + * @param + * @param instance + * @return + */ + boolean isLiveObject(T instance); } diff --git a/src/main/java/org/redisson/liveobject/annotation/REntity.java b/src/main/java/org/redisson/liveobject/annotation/REntity.java index bf717eae4..00f893edd 100644 --- a/src/main/java/org/redisson/liveobject/annotation/REntity.java +++ b/src/main/java/org/redisson/liveobject/annotation/REntity.java @@ -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) { diff --git a/src/main/java/org/redisson/liveobject/core/LiveObjectInterceptor.java b/src/main/java/org/redisson/liveobject/core/LiveObjectInterceptor.java index 7935cd824..254a5e5c1 100644 --- a/src/main/java/org/redisson/liveobject/core/LiveObjectInterceptor.java +++ b/src/main/java/org/redisson/liveobject/core/LiveObjectInterceptor.java @@ -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(); } diff --git a/src/main/java/org/redisson/liveobject/misc/Introspectior.java b/src/main/java/org/redisson/liveobject/misc/Introspectior.java index 7bc80afc5..be4b9598e 100644 --- a/src/main/java/org/redisson/liveobject/misc/Introspectior.java +++ b/src/main/java/org/redisson/liveobject/misc/Introspectior.java @@ -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; diff --git a/src/test/java/org/redisson/RedissonAttachedLiveObjectServiceTest.java b/src/test/java/org/redisson/RedissonAttachedLiveObjectServiceTest.java index 359c58f8d..e801952c5 100644 --- a/src/test/java/org/redisson/RedissonAttachedLiveObjectServiceTest.java +++ b/src/test/java/org/redisson/RedissonAttachedLiveObjectServiceTest.java @@ -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 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()); + } } }