diff --git a/pom.xml b/pom.xml index 1b6781b..fca9b33 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.remondis resample - 0.0.11 + 0.0.12 jar ReSample https://github.com/remondis-it/resample @@ -257,7 +257,7 @@ org.jacoco jacoco-maven-plugin - 0.8.2 + 0.8.12 default-prepare-agent @@ -291,6 +291,8 @@ com.remondis.resample.supplier.Suppliers com.remondis.resample.AutoSamplingException com.remondis.resample.Samples.Default + com.remondis.resample.InvocationSensor + com.remondis.resample.InterceptionHandler CLASS @@ -419,7 +421,42 @@ - + + + com.remondis.resample.InvocationSensor + + CLASS + + + BRANCH + COVEREDRATIO + 0.75 + + + INSTRUCTION + COVEREDRATIO + 0.97 + + + + + + com.remondis.resample.InterceptionHandler + + CLASS + + + BRANCH + COVEREDRATIO + 0.66 + + + INSTRUCTION + COVEREDRATIO + 0.96 + + + @@ -505,7 +542,7 @@ net.bytebuddy byte-buddy - 1.12.8 + 1.14.18 diff --git a/src/main/java/com/remondis/resample/InterceptionHandler.java b/src/main/java/com/remondis/resample/InterceptionHandler.java new file mode 100644 index 0000000..120ce2c --- /dev/null +++ b/src/main/java/com/remondis/resample/InterceptionHandler.java @@ -0,0 +1,46 @@ +package com.remondis.resample; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import static java.util.Collections.unmodifiableList; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +public class InterceptionHandler { + + private T proxyObject; + private final ThreadLocal> threadLocalPropertyNames = ThreadLocal.withInitial(LinkedList::new); + + public void setProxyObject(T proxyObject) { + this.proxyObject = proxyObject; + } + + public T getProxyObject() { + return proxyObject; + } + + public List getTrackedPropertyNames() { + List list = threadLocalPropertyNames.get(); + // Reset thread local after access. + reset(); + return isNull(list) ? Collections.emptyList() : unmodifiableList(list); + } + + public List getThreadLocalPropertyNames() { + return threadLocalPropertyNames.get(); + } + + /** + * Resets the thread local list of property names. + */ + void reset() { + threadLocalPropertyNames.remove(); + } + + public boolean hasTrackedProperties() { + return nonNull(threadLocalPropertyNames.get()) && !threadLocalPropertyNames.get() + .isEmpty(); + } +} diff --git a/src/main/java/com/remondis/resample/InvocationSensor.java b/src/main/java/com/remondis/resample/InvocationSensor.java index 1f8df48..48321b6 100644 --- a/src/main/java/com/remondis/resample/InvocationSensor.java +++ b/src/main/java/com/remondis/resample/InvocationSensor.java @@ -4,13 +4,15 @@ import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import java.lang.reflect.Method; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import static com.remondis.resample.ReflectionUtil.defaultValue; import static com.remondis.resample.ReflectionUtil.invokeMethodProxySafe; import static com.remondis.resample.ReflectionUtil.toPropertyName; +import static java.lang.ClassLoader.getSystemClassLoader; +import static java.util.Objects.isNull; import static net.bytebuddy.implementation.InvocationHandlerAdapter.of; import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; import static net.bytebuddy.matcher.ElementMatchers.isGetter; @@ -24,30 +26,42 @@ */ class InvocationSensor { - private T proxyObject; + static Map, InterceptionHandler> interceptionHandlerCache = new ConcurrentHashMap<>(); - private List propertyNames = new LinkedList<>(); + private InterceptionHandler interceptionHandler; InvocationSensor(Class superType) { - Class proxyClass = new ByteBuddy().subclass(superType) - .method(isGetter()) - .intercept(of((proxy, method, args) -> markPropertyAsCalled(method))) - .method(isDeclaredBy(Object.class)) - .intercept(of((proxy, method, args) -> invokeMethodProxySafe(method, this, args))) - .method(not(isGetter()).and(not(isDeclaredBy(Object.class)))) - .intercept(of((proxy, method, args) -> { - throw ReflectionException.notAGetter(method); - })) - .make() - .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.INJECTION) - .getLoaded(); - - proxyObject = superType.cast(ReflectionUtil.newInstance(proxyClass)); + ClassLoader classLoader; + if (isNull(superType) || isNull(superType.getClassLoader())) { + classLoader = getSystemClassLoader(); + } else { + classLoader = superType.getClassLoader(); + } + if (interceptionHandlerCache.containsKey(superType)) { + this.interceptionHandler = (InterceptionHandler) interceptionHandlerCache.get(superType); + } else { + Class proxyClass = new ByteBuddy().subclass(superType) + .method(isGetter()) + .intercept(of((proxy, method, args) -> markPropertyAsCalled(method))) + .method(isDeclaredBy(Object.class)) + .intercept(of((proxy, method, args) -> invokeMethodProxySafe(method, this, args))) + .method(not(isGetter()).and(not(isDeclaredBy(Object.class)))) + .intercept(of((proxy, method, args) -> { + throw ReflectionException.notAGetter(method); + })) + .make() + .load(classLoader, ClassLoadingStrategy.Default.INJECTION) + .getLoaded(); + this.interceptionHandler = new InterceptionHandler<>(); + this.interceptionHandler.setProxyObject(superType.cast(ReflectionUtil.newInstance(proxyClass))); + interceptionHandlerCache.put(superType, this.interceptionHandler); + } } private Object markPropertyAsCalled(Method method) { String propertyName = toPropertyName(method); - propertyNames.add(propertyName); + interceptionHandler.getThreadLocalPropertyNames() + .add(propertyName); return nullOrDefaultValue(method.getReturnType()); } @@ -57,7 +71,7 @@ private Object markPropertyAsCalled(Method method) { * @return The proxy. */ T getSensor() { - return proxyObject; + return interceptionHandler.getProxyObject(); } /** @@ -66,24 +80,21 @@ T getSensor() { * @return Returns the tracked property names. */ List getTrackedPropertyNames() { - return Collections.unmodifiableList(propertyNames); + return interceptionHandler.getTrackedPropertyNames(); } /** * Checks if there were any properties accessed by get calls. * - * @return Returns true if there were at least one interaction with - * a property. Otherwise false is returned. + * @return Returns true if there were at least one interaction with a property. Otherwise + * false is returned. */ boolean hasTrackedProperties() { - return !propertyNames.isEmpty(); + return interceptionHandler.hasTrackedProperties(); } - /** - * Resets all tracked information. - */ void reset() { - propertyNames.clear(); + interceptionHandler.reset(); } private static Object nullOrDefaultValue(Class returnType) { diff --git a/src/test/java/com/remondis/resample/Dummy.java b/src/test/java/com/remondis/resample/Dummy.java index b537282..7d681e2 100644 --- a/src/test/java/com/remondis/resample/Dummy.java +++ b/src/test/java/com/remondis/resample/Dummy.java @@ -1,12 +1,17 @@ package com.remondis.resample; +import java.util.Objects; + public class Dummy { private String field; - public Dummy(String field) { + private String anotherField; + + public Dummy(String field, String anotherField) { super(); this.field = field; + this.anotherField = anotherField; } public Dummy() { @@ -21,12 +26,17 @@ public void setField(String field) { this.field = field; } + public String getAnotherField() { + return anotherField; + } + + public void setAnotherField(String anotherField) { + this.anotherField = anotherField; + } + @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((field == null) ? 0 : field.hashCode()); - return result; + return Objects.hash(field, anotherField); } @Override @@ -38,12 +48,12 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; Dummy other = (Dummy) obj; - if (field == null) { - if (other.field != null) - return false; - } else if (!field.equals(other.field)) - return false; - return true; + return Objects.equals(field, other.field) && Objects.equals(anotherField, other.anotherField); + } + + @Override + public String toString() { + return "Dummy [field=" + field + ", anotherField=" + anotherField + "]"; } } diff --git a/src/test/java/com/remondis/resample/InvocationSensorTest.java b/src/test/java/com/remondis/resample/InvocationSensorTest.java index 5f359d2..e42dccb 100644 --- a/src/test/java/com/remondis/resample/InvocationSensorTest.java +++ b/src/test/java/com/remondis/resample/InvocationSensorTest.java @@ -2,11 +2,15 @@ import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.List; +import java.util.concurrent.Semaphore; import org.junit.Before; import org.junit.Test; @@ -20,6 +24,7 @@ public class InvocationSensorTest { public void setup() { this.sensor = new InvocationSensor<>(TestBean.class); this.sensorObject = this.sensor.getSensor(); + InvocationSensor.interceptionHandlerCache.clear(); } @Test @@ -73,4 +78,87 @@ public void shouldDelegateToMethodsFromObject() { assertFalse(sensor.hasTrackedProperties()); } + @Test + public void shouldHandleClassLoaderNull() { + InvocationSensor invocationSensor = new InvocationSensor<>(NoClassLoaderBean.class); + assertNotNull(invocationSensor.getSensor()); + } + + @Test + public void shouldHandleEmptyCache() { + InvocationSensor invocationSensor = new InvocationSensor<>(Dummy.class); + assertNotNull(invocationSensor.getSensor()); + assertTrue(InvocationSensor.interceptionHandlerCache.containsKey(Dummy.class)); + } + + @Test + public void shouldReturnDefaultValueForPrimitiveType() { + Object result = sensorObject.getPrimitiveInt(); + assertEquals(0, result); + } + + @Test + public void shouldReturnNullForObjectType() { + Object result = sensorObject.getObject(); + assertNull(result); + } + + @Test + public void shouldCacheThreadSafe() { + + Semaphore s1 = new Semaphore(1); + s1.acquireUninterruptibly(); + + Semaphore s2 = new Semaphore(1); + s2.acquireUninterruptibly(); + + InterceptionHandler interceptionHandler = InvocationSensor.interceptionHandlerCache.get(Dummy.class); + + Thread t1 = new Thread(() -> { + InvocationSensor invocationSensor = new InvocationSensor<>(Dummy.class); + Dummy sensor = invocationSensor.getSensor(); + sensor.getField(); + s2.release(); + s1.acquireUninterruptibly(); + }); + t1.start(); + + // Hier warte bis t1 mindestens getString() aufgerufen hast + s2.acquireUninterruptibly(); + InvocationSensor invocationSensor = new InvocationSensor<>(Dummy.class); + List trackedPropertyNames = invocationSensor.getTrackedPropertyNames(); + assertTrue(trackedPropertyNames.isEmpty()); + s1.release(); + } + + @Test + public void shouldCache() { + assertTrue(InvocationSensor.interceptionHandlerCache.isEmpty()); + InvocationSensor invocationSensor = new InvocationSensor<>(Dummy.class); + assertFalse(InvocationSensor.interceptionHandlerCache.isEmpty()); + assertTrue(InvocationSensor.interceptionHandlerCache.containsKey(Dummy.class)); + assertNotNull(InvocationSensor.interceptionHandlerCache.get(Dummy.class)); + + InterceptionHandler interceptionHandler = InvocationSensor.interceptionHandlerCache.get(Dummy.class); + + Dummy sensor = invocationSensor.getSensor(); + sensor.getField(); + + List trackedPropertyNames = interceptionHandler.getTrackedPropertyNames(); + assertEquals(1, trackedPropertyNames.size()); + assertTrue(trackedPropertyNames.contains("field")); + + sensor.getAnotherField(); + trackedPropertyNames = interceptionHandler.getTrackedPropertyNames(); + assertEquals(1, trackedPropertyNames.size()); + assertTrue(trackedPropertyNames.contains("anotherField")); + + sensor.getField(); + sensor.getAnotherField(); + trackedPropertyNames = interceptionHandler.getTrackedPropertyNames(); + assertEquals(2, trackedPropertyNames.size()); + assertTrue(trackedPropertyNames.contains("field")); + assertTrue(trackedPropertyNames.contains("anotherField")); + + } } diff --git a/src/test/java/com/remondis/resample/NoClassLoaderBean.java b/src/test/java/com/remondis/resample/NoClassLoaderBean.java new file mode 100644 index 0000000..2944038 --- /dev/null +++ b/src/test/java/com/remondis/resample/NoClassLoaderBean.java @@ -0,0 +1,7 @@ +package com.remondis.resample; + +class NoClassLoaderBean { + public static ClassLoader getClassLoader() { + return null; + } +} \ No newline at end of file diff --git a/src/test/java/com/remondis/resample/TestBean.java b/src/test/java/com/remondis/resample/TestBean.java index a74af28..c7270a3 100644 --- a/src/test/java/com/remondis/resample/TestBean.java +++ b/src/test/java/com/remondis/resample/TestBean.java @@ -98,6 +98,14 @@ public void setPrimitiveLong(long primitiveLong) { this.primitiveLong = primitiveLong; } + public int getPrimitiveInt() { + return 0; + } + + public Object getObject() { + return null; + } + @Override public String toString() { return "TestBean [strings=" + strings + ", dummies=" + dummies + ", wrapperBoolean=" + wrapperBoolean