0

I try to call methods by reflection in kotlin and it doesn't work.

My code (simplyfied, null-checks and catch-exceptions omitted):

class MyCallerClass() {

  val allCallableMethods: List<KFunction<Unit>> = ... 

  // request: we're within a web-page. The user can see the callable methods and click which one to call 
  fun handleRequest(request: HttpServletRequest) {

    val callableMethodName = request.getParameter(callableMethodNameParam)
    if (callableMethodName != null) {
      for (method in allCallableMethods) {
        if (method.name == callableMethodName) {
          // we found the method the user wants to call!

          val paramMap: MutableMap<KParameter, Any> = mutableMapOf()
            
          // this part I added after I got an IllegalArgumentException. See below
          if (method is CallableReference) {
            paramMap[method.instanceParameter!!] = method.owner
          }

          for (param in method.valueParameters) {
            val paramName = if (param.name != null) {
              param.name!!
            }

            val paramValue: String? = request.getParameter(paramName)
            if(paramValue != null) {
              paramMap[param] = paramValue
            }
          }

          method.callBy(paramMap)
        }
      }
    }
  }
}

So first I only collected all params in the paramMap, but that resulted in "java.lang.IllegalArgumentException: No argument provided for a required parameter: instance parameter of fun...".

Luckily I found questions here that helped me on:

So I added the part with if (method is CallableReference). But now I get a "java.lang.IllegalArgumentException: object is not an instance of declaring class".


I also tried to use method.call() instead of method.callBy():

...
var paramArray = arrayOf<Any>()
...
paramArray = paramArray.plus(paramValue)
...
method.call(*paramArray)

But that gave the same results. Without the instance-parameter I get a "java.lang.IllegalArgumentException: Callable expects 2 arguments, but 1 were provided.". With the instance-parameter the same "java.lang.IllegalArgumentException: object is not an instance of declaring class"


Then I tried to call a method within the same class: I created a new method in MyCallerClass:

fun myLocalMethod(strParam: String, intParam: Int): String {
  return strParam
}

and then I added this in fun handleRequest:

val myLocalMethodFunc = ::myLocalMethod
val returnStr = myLocalMethodFunc.call("test", 3)

And it worked! o_O


So can anyone explain

  • why the call to the local function works and to the remote function doesn't? They're both of the type KFunction.
  • how I can call the remote functions? How do I provide the instanceParameter in the correct way?
  • why I can't debug into method.callBy() or method.call()? That would help me a lot, but IntelliJ just jumps over it.
nyx
  • 477
  • 1
  • 4
  • 10
  • Two questions: 1. What do you mean by local and remote functions? 2. What is the concrete content of your `allCallableMethods` variable? May be a `KFunction0` or `KFunction1`. This is important for eg. the instance parameter. – wartoshika Jun 07 '22 at 11:28
  • local function = the function "myLocalMethod" that I created within my own class MyCallerClass for testing. It's a local function because I call it within the same class where the function is created. remote function = the functions that are collected in allCallableMethods. They are from all over the project, none of them is defined in MyCallerClass. – nyx Jun 07 '22 at 11:34
  • The concrete content of allCallableMethods: I have several classes scattered all over my project that implement an interface. With the help of spring beans I inject a list of all classes that implement that interface. All the classes that implement that interface have a method "getCallableMethods" that return a list of KFunction. They all do something like `return listOf(MyImplementingClass::myCallableMethod)`. So I can collect all callable methods of all classes in my project. – nyx Jun 07 '22 at 11:39
  • This sort of headache is why I always recommend that people use reflection only as a last resort! You need it for frameworks, plug-ins, build tools, and suchlike; but for general application coding, there's almost always a better (simpler, faster, more robust, more maintainable, more secure) way. – gidds Jun 07 '22 at 16:51
  • Usually I'd say the same. But in that case it helps with decoupling. With the interface each class can decide on their own if and how they provide a callable method for the page. And on the page I can have one general piece of code that provides input fields and a button so any method can be called by click. Otherwise I'd have to hard-code all methods in one place. – nyx Jun 08 '22 at 06:57

2 Answers2

1

I found the solution! The answer of Wartoshika lead me to it: My functions aren't static and they are located within singletons.

The problem is in the place where I collect the functions. As I wrote in the comment to my question, the classes, that provide the functions to be called, do all something like return listOf(MyImplementingClass::myCallableMethod). That is the problem. It should be return listOf(::myCallableMethod) instead.

Then the line paramMap[method.instanceParameter!!] = method.owner results in a NullPointerException. And by removing that line, the code works :)

And when there's no error, I can also debug into the callBy(). I couldn't debug into it, because the code wasn't able to find the function to jump to.

nyx
  • 477
  • 1
  • 4
  • 10
0

Ill try to give an answer.

concider three important states:

  1. Function reference to a non static non object Java or Kotlin classes method
  2. Function reference to a static Java method
  3. Function reference to a Kotlin object classes method

Every of there states has to be handled differently as they require some extra knowledge of wich parameters are required.

1. Non static non object Java or Kotlin classes method

The first parameter to the call method has to be a not nullable instance of that class or an instance that derived from the reflecting class.

Example:

class TestClass {
  fun test(stringParam: String): Int {
    return stringParam.toInt()
  }
}

TestClass::test.call(TestClass(), "5") // returns 5 (Int)

2. Static Java method

When accessing a static Java method you have to pass null as the instance parameter. This is possible since you call a static method without a concrete instance.

Example:

public class TestClass {
  public static Integer test(String stringParam) {
    return Integer.parseInt(stringParam, 10)
  }
}
TestClass::test.call(null, "5") // returns 5 (Int)

3. Kotlin object classes method

I don't know why but for singleton objects no instance parameter is required. I think that the reason behind this is that a reference to such methods holds information about its class. Kotlin then can receive the instance variable on its own because only one instance can exists within the whole jvm.

Example:

object TestClass {
  fun test(stringParam: String): Int {
   return stringParam.toInt()
  }
}

TestClass::test.call("5") shouldBe 5 // returns 5 (Int)

This exact behaviour applies when you try to access a local function via reference operator. Consider the following example:

    class TestClass {

        fun test(stringParam: String): Int {
            return ::otherTest.call(stringParam.toInt())
        }

        fun otherTest(intParam: Int): Int {
            return intParam + 2
        }
    }

TestClass::test.call(TestClass(), "5") // returns 7 (Int)

Ideas to your Question:

why I can't debug into method.callBy() or method.call()? That would help me a lot, but IntelliJ just jumps over it.

When i have created a very simple environment to test this it worked fine enter image description here. There are multiple possabilities. One i can think of:

  • When running within a suspend function, IDEA sometimes skips executions when they will be executed in another coroutine scope

Maybe this is a kickstart to your questions. Please reply if one of your question is not answered.

wartoshika
  • 531
  • 3
  • 10
  • "The first parameter to the call method has to be a not nullable instance of that class or an instance that derived from the reflecting class." That's what I'm trying to do with `paramMap[method.instanceParameter!!] = method.owner`. "When accessing a static Java method you have to pass null as the instance parameter." My methods aren't static. "for singleton objects no instance parameter is required" Now that's interesting information! My classes, that provide the methods I want to call, are singletons! But then why do I get the error "No argument provided for a required parameter" – nyx Jun 07 '22 at 15:24
  • i think this has something to do with the `callBy` method. Ill try digging into the reflection. The `method.owner` has to be wrong in that case. Ill let you know – wartoshika Jun 07 '22 at 15:28