Skip to content

Commit

Permalink
Merge pull request #12 from remondis-it/proxy-class-cashing-bug
Browse files Browse the repository at this point in the history
Proxy class cashing bug
  • Loading branch information
muhamadkheder authored Jul 25, 2024
2 parents c03fc71 + 64f8246 commit 2ad1dbd
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 43 deletions.
45 changes: 41 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.remondis</groupId>
<artifactId>resample</artifactId>
<version>0.0.11</version>
<version>0.0.12</version>
<packaging>jar</packaging>
<name>ReSample</name>
<url>https://github.com/remondis-it/resample</url>
Expand Down Expand Up @@ -257,7 +257,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<version>0.8.12</version>
<executions>
<execution>
<id>default-prepare-agent</id>
Expand Down Expand Up @@ -291,6 +291,8 @@
<exclude>com.remondis.resample.supplier.Suppliers</exclude>
<exclude>com.remondis.resample.AutoSamplingException</exclude>
<exclude>com.remondis.resample.Samples.Default</exclude>
<exclude>com.remondis.resample.InvocationSensor</exclude>
<exclude>com.remondis.resample.InterceptionHandler</exclude>
</excludes>
<element>CLASS</element>
<limits>
Expand Down Expand Up @@ -419,7 +421,42 @@
</limit>
</limits>
</rule>

<rule>
<includes>
<include>com.remondis.resample.InvocationSensor</include>
</includes>
<element>CLASS</element>
<limits>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.75</minimum>
</limit>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.97</minimum>
</limit>
</limits>
</rule>
<rule>
<includes>
<include>com.remondis.resample.InterceptionHandler</include>
</includes>
<element>CLASS</element>
<limits>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.66</minimum>
</limit>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.96</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
Expand Down Expand Up @@ -505,7 +542,7 @@
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.8</version>
<version>1.14.18</version>
</dependency>

<!-- Test dependencies -->
Expand Down
46 changes: 46 additions & 0 deletions src/main/java/com/remondis/resample/InterceptionHandler.java
Original file line number Diff line number Diff line change
@@ -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<T> {

private T proxyObject;
private final ThreadLocal<List<String>> threadLocalPropertyNames = ThreadLocal.withInitial(LinkedList::new);

public void setProxyObject(T proxyObject) {
this.proxyObject = proxyObject;
}

public T getProxyObject() {
return proxyObject;
}

public List<String> getTrackedPropertyNames() {
List<String> list = threadLocalPropertyNames.get();
// Reset thread local after access.
reset();
return isNull(list) ? Collections.emptyList() : unmodifiableList(list);
}

public List<String> 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();
}
}
67 changes: 39 additions & 28 deletions src/main/java/com/remondis/resample/InvocationSensor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,30 +26,42 @@
*/
class InvocationSensor<T> {

private T proxyObject;
static Map<Class<?>, InterceptionHandler<?>> interceptionHandlerCache = new ConcurrentHashMap<>();

private List<String> propertyNames = new LinkedList<>();
private InterceptionHandler<T> interceptionHandler;

InvocationSensor(Class<T> 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<T>) 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());
}

Expand All @@ -57,7 +71,7 @@ private Object markPropertyAsCalled(Method method) {
* @return The proxy.
*/
T getSensor() {
return proxyObject;
return interceptionHandler.getProxyObject();
}

/**
Expand All @@ -66,24 +80,21 @@ T getSensor() {
* @return Returns the tracked property names.
*/
List<String> getTrackedPropertyNames() {
return Collections.unmodifiableList(propertyNames);
return interceptionHandler.getTrackedPropertyNames();
}

/**
* Checks if there were any properties accessed by get calls.
*
* @return Returns <code>true</code> if there were at least one interaction with
* a property. Otherwise <code>false</code> is returned.
* @return Returns <code>true</code> if there were at least one interaction with a property. Otherwise
* <code>false</code> 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) {
Expand Down
32 changes: 21 additions & 11 deletions src/test/java/com/remondis/resample/Dummy.java
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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
Expand All @@ -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 + "]";
}

}
Loading

0 comments on commit 2ad1dbd

Please sign in to comment.