Skip to content

Commit

Permalink
ConfigurationUtility.isMethodOverwrite inefficient reflection
Browse files Browse the repository at this point in the history
Add lazy initialized cache for declared methods for each queried class.
Additionally fetching all declared methods of a class avoids
catching NoSuchMethodExceptions when trying to find a method.

359087
  • Loading branch information
paolobazzi committed Aug 15, 2024
1 parent 5e44e36 commit 85000fc
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.scout.rt.platform.reflect;

import static org.junit.Assert.*;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.scout.rt.platform.util.CollectionUtility;
import org.junit.Test;

/**
* Testcases for ConfigurationUtility
*/
public class ConfigurationUtilityTest {

@Test
public void testGetDeclaredMethod() {
runTestGetDeclaredMethod(Fixture.class, "privateMethod", int.class);
runTestGetDeclaredMethod(Fixture.class, "defaultMethod", long.class);
runTestGetDeclaredMethod(Fixture.class, "protectedMethod", float.class);
runTestGetDeclaredMethod(Fixture.class, "publicMethod", double.class);

runTestGetDeclaredMethod(SubFixture.class, "privateSubMethod", int.class);
runTestGetDeclaredMethod(SubFixture.class, "defaultSubMethod", long.class);
runTestGetDeclaredMethod(SubFixture.class, "protectedSubMethod", float.class);
runTestGetDeclaredMethod(SubFixture.class, "publicSubMethod", double.class);
runTestGetDeclaredMethod(SubFixture.class, "publicMethod", double.class);

assertNull(ConfigurationUtility.getDeclaredMethod(SubFixture.class, "privateMethod", new Class[]{int.class}));
assertNull(ConfigurationUtility.getDeclaredMethod(SubFixture.class, "defaultMethod", new Class[]{long.class}));
assertNull(ConfigurationUtility.getDeclaredMethod(SubFixture.class, "protectedMethod", new Class[]{float.class}));
}

@Test
public void testGetAllDeclaredMethods() {
Method[] methods = ConfigurationUtility.getAllDeclaredMethods(Fixture.class);
// NOTE: do not assert method count and exact method set since test framework agents could add additional methods (e.g. $jacocoInit method used for profiling)
assertTrue(CollectionUtility.containsAll(Arrays.stream(methods).map(Method::getName).collect(Collectors.toSet()), Set.of("privateMethod", "defaultMethod", "protectedMethod", "publicMethod")));
assertSame(methods, ConfigurationUtility.getAllDeclaredMethods(Fixture.class));

Method[] methodsSub = ConfigurationUtility.getAllDeclaredMethods(SubFixture.class);
// NOTE: do not assert method count and exact method set since test framework agents could add additional methods (e.g. $jacocoInit method used for profiling)
assertTrue(CollectionUtility.containsAll(Arrays.stream(methodsSub).map(Method::getName).collect(Collectors.toSet()), Set.of("privateSubMethod", "defaultSubMethod", "protectedSubMethod", "publicSubMethod", "publicMethod")));
assertSame(methodsSub, ConfigurationUtility.getAllDeclaredMethods(SubFixture.class));
}

@Test
public void testIsMethodOverride() {
assertTrue(ConfigurationUtility.isMethodOverwrite(Fixture.class, "publicMethod", new Class[]{double.class}, SubFixture.class));
assertTrue(ConfigurationUtility.isMethodOverwrite(Fixture.class, "protectedMethod", new Class[]{float.class}, SubSubFixture.class));

// method overridden by SubFixture
assertTrue(ConfigurationUtility.isMethodOverwrite(Fixture.class, "publicMethod", new Class[]{double.class}, SubSubFixture.class));

// method not overridden
assertFalse(ConfigurationUtility.isMethodOverwrite(Fixture.class, "protectedMethod", new Class[]{float.class}, SubFixture.class));

// method on same class
assertFalse(ConfigurationUtility.isMethodOverwrite(Fixture.class, "publicMethod", new Class[]{double.class}, Fixture.class));

// non-existent method
assertFalse(ConfigurationUtility.isMethodOverwrite(Fixture.class, "nonExistentMethod", new Class[]{double.class}, SubFixture.class));

// wrong parameters
assertFalse(ConfigurationUtility.isMethodOverwrite(Fixture.class, "publicMethod", new Class[]{int.class}, SubFixture.class));
}

protected void runTestGetDeclaredMethod(Class clazz, String methodName, Class paramType) {
Method m = ConfigurationUtility.getDeclaredMethod(clazz, methodName, new Class[]{paramType});
assertEquals(methodName, m.getName());
assertEquals(paramType, m.getParameterTypes()[0]);
assertEquals(void.class, m.getReturnType());
}

@SuppressWarnings("unused")
private static class Fixture {
private void privateMethod(int i) {
}

void defaultMethod(long l) {
}

protected void protectedMethod(float f) {
}

public void publicMethod(double d) {
}
}

@SuppressWarnings("unused")
private static class SubFixture extends Fixture {
private void privateSubMethod(int i) {
}

void defaultSubMethod(long l) {
}

protected void protectedSubMethod(float f) {
}

public void publicSubMethod(double d) {
}

@Override
public void publicMethod(double d) {
// overridden method fixture
super.publicMethod(d);
}
}

@SuppressWarnings("unused")
private static class SubSubFixture extends SubFixture {
@Override
protected void protectedMethod(float f) {
// overridden method fixture
super.protectedMethod(f);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
*/
package org.eclipse.scout.rt.platform.reflect;

import static org.eclipse.scout.rt.platform.util.Assertions.assertNotNull;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand All @@ -28,7 +31,6 @@
import org.eclipse.scout.rt.platform.classid.ClassId;
import org.eclipse.scout.rt.platform.exception.PlatformExceptionTranslator;
import org.eclipse.scout.rt.platform.extension.InjectFieldTo;
import org.eclipse.scout.rt.platform.util.Assertions;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
import org.eclipse.scout.rt.platform.util.CompositeObject;
import org.eclipse.scout.rt.platform.util.StringUtility;
Expand All @@ -46,6 +48,11 @@ public final class ConfigurationUtility {
*/
private static final ConcurrentHashMap<Class, Class[]> declaredPublicClassesCache = new ConcurrentHashMap<>();

/**
* Cache for declared methods of a class.
*/
private static final Map<Class<?>, Method[]> S_DECLARED_METHOD_CACHE = new ConcurrentHashMap<>();

private ConfigurationUtility() {
}

Expand Down Expand Up @@ -210,32 +217,44 @@ public static <T> T newInnerInstance(Object instance, Class<T> innerClass) {
* @return true if the declared method is overwritten in implementationType
*/
public static boolean isMethodOverwrite(Class<?> declaringType, String methodName, Class[] parameterTypes, Class<?> implementationType) {
Assertions.assertNotNull(declaringType, "declaringType must not be null");
Assertions.assertNotNull(methodName, "methodName must not be null");
Method declaredMethod;
try {
declaredMethod = declaringType.getDeclaredMethod(methodName, parameterTypes);
}
catch (NoSuchMethodException | SecurityException e) {
LOG.error("cannot find declared method {}.{}", declaringType.getName(), methodName, e);
assertNotNull(declaringType, "declaringType must not be null");
assertNotNull(methodName, "methodName must not be null");
Method declaredMethod = getDeclaredMethod(declaringType, methodName, parameterTypes);
if (declaredMethod == null) {
LOG.error("cannot find declared method {}.{}", declaringType.getName(), methodName);
return false;
}
Class<?> c = implementationType;
while (c != null && c != declaringType) {
try {
//check if method is avaliable
c.getDeclaredMethod(declaredMethod.getName(), declaredMethod.getParameterTypes());
Method method = getDeclaredMethod(c, declaredMethod.getName(), declaredMethod.getParameterTypes());
if (method != null) {
return true;
}
catch (NoSuchMethodException | SecurityException e) { // NOSONAR
//nop
}
//up
c = c.getSuperclass();
}
return false;
}

/**
* @return {@link Method} of class {@code clazz} with given {@code methodName} and {@code parameterTypes} or
* {@code null} if no matching method could be found.
*/
public static Method getDeclaredMethod(Class<?> clazz, String methodName, Class[] parameterTypes) {
for (Method method : getAllDeclaredMethods(clazz)) {
if (method.getName().equals(methodName) && Arrays.equals(method.getParameterTypes(), parameterTypes)) {
return method;
}
}
return null;
}

/**
* @return array of {@link Method} containing all declared methods of given {@code clazz}.
*/
public static Method[] getAllDeclaredMethods(Class<?> clazz) {
return S_DECLARED_METHOD_CACHE.computeIfAbsent(clazz, Class::getDeclaredMethods);
}

/**
* Returns a new array without those classes, that are replaced by another class. The returned array is a new
* instance, except there are no replacing classes. Replacing classes are annotated with {@link Replace}. Replacing
Expand Down

0 comments on commit 85000fc

Please sign in to comment.