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 extends T> 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 extends T> 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