Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NullPointerException with @RequestMapping on Kotlin property accessors #31856

Closed
wolfseifert opened this issue Dec 18, 2023 · 8 comments
Closed
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support type: bug A general bug
Milestone

Comments

@wolfseifert
Copy link

wolfseifert commented Dec 18, 2023

I was able to reproduce the issue with one of spring-boots own examples. Following Getting Started > Building an Application with Spring Boot:

$ git clone https://github.com/spring-guides/gs-spring-boot.git
$ cd gs-spring-boot/initial

Rename HelloController.java to HelloController.kt and change it into:

package com.example.springboot

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HelloController {
  val index: String
    @GetMapping("/")
    get() = "Greetings from Spring Boot!"
}

Change build.gradle into:

plugins {
  id 'org.springframework.boot' version '3.2.0'
  id 'io.spring.dependency-management' version '1.1.4'
  id 'org.jetbrains.kotlin.jvm' version '1.9.21'
}

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  runtimeOnly 'org.jetbrains.kotlin:kotlin-reflect:1.9.21'
}

Then run it with ./gradlew bootRun:

2023-12-17T15:43:49.074+01:00 ERROR 27483 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.NullPointerException] with root cause

java.lang.NullPointerException: null
  at java.base/java.util.Objects.requireNonNull(Objects.java:209) ~[na:na]
  at org.springframework.web.method.support.InvocableHandlerMethod$KotlinDelegate.invokeFunction(InvocableHandlerMethod.java:302) ~[spring-web-6.1.1.jar:6.1.1]
  at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:251) ~[spring-web-6.1.1.jar:6.1.1]
  at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:182) ~[spring-web-6.1.1.jar:6.1.1]
  at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:917) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:829) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.16.jar:6.0]
  at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.1.jar:6.1.1]
  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.16.jar:6.0]
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.1.jar:6.1.1]
  at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.1.jar:6.1.1]
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.1.jar:6.1.1]
  at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.1.jar:6.1.1]
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.1.jar:6.1.1]
  at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.1.jar:6.1.1]
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:340) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
  at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]

curl http://localhost:8080:

{"timestamp":"2023-12-17T14:44:52.410+00:00","status":500,"error":"Internal Server Error","path":"/"}

Removing runtimeOnly 'org.jetbrains.kotlin:kotlin-reflect:1.9.21' from build.gradle resolves the issue:

$ ./gradlew bootRun
$ curl http://localhost:8080
Greetings from Spring Boot!

So the problem appears only with kotlin-reflect on the (runtime) classpath.
This is queried in org.springframework.web.method.support.InvocableHandlerMethod:246
and leads to an exception in InvocableHandlerMethod:302 if the case.
The JVM method getIndex() is not found in the list of Kotlin methods (equals() etc.).

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 18, 2023
@philwebb
Copy link
Member

@bclozel could you please transfer this one to the Framework issue tracker.

@bclozel bclozel transferred this issue from spring-projects/spring-boot Dec 18, 2023
@bclozel bclozel added in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support labels Dec 18, 2023
@snicoll
Copy link
Member

snicoll commented Dec 18, 2023

Then run it:

Thanks for the detailed explanation but there's no added value in us replicating that sample from the steps you've described. Our time would be better spent looking at the final example as you see it and understand what could be the issue.

Can you please first upgrade your sample to use Spring Framework 6.1.2-SNAPSHOT as we've fixed numerous things in that area (you can also upgrade the sample to Spring Boot 3.2.1-SNAPSHOT). If the issue persists, please attach the sample here as a zip or push the updated code to a separate GitHub repository.

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Dec 18, 2023
@wolfseifert
Copy link
Author

I do not understand your problem as the "final example" is in the issue description - just copy and paste!

Anyway, here are the zips
spring-boot-3.2.0.zip
spring-boot-3.2.1-SNAPSHOT.zip

(Same problem with 3.2.1-SNAPSHOT)

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 18, 2023
@snicoll
Copy link
Member

snicoll commented Dec 18, 2023

I do not understand your problem as the "final example" is in the issue description - just copy and paste!

I don't have a problem. You're making it sound way simpler than it often is. If you have the repro as a small sample, describing the steps to get to there is really a bad idea as a step might be missing or misleading and we'd end up in a back and forth trying to figure out the missing bit, and that's frustrating for everybody.

As I've already explained, I suppose we can both agree you prefer us investigating the actual problem rather than spending time rebuilding the pieces to reproduce it.

@snicoll
Copy link
Member

snicoll commented Dec 18, 2023

I am not a kotlin expert but what is the purpose of that val index there?

val index: String
    @GetMapping("/")
    get() = "Greetings from Spring Boot!"

get() isn't a function of HelloController. Of course we shouldn't end up in an NPE but I suspect the method shouldn't have been detected in the first place.

@snicoll snicoll added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Dec 18, 2023
@wolfseifert
Copy link
Author

Kotlin's

val index: String
  @GetMapping("/")
  get() = "Greetings from Spring Boot!"

produces something like Java's

  @GetMapping("/")
  String getIndex() { 
    return "Greetings from Spring Boot!";
  }

See the last paragraph of my initial issue description for an error analysis. I think a Kotlin expert is needed here.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 18, 2023
@wolfseifert
Copy link
Author

Here is a patch for the issue (for spring-framework main):
patch.txt

@sdeleuze sdeleuze self-assigned this Dec 19, 2023
@sdeleuze sdeleuze changed the title When using Kotlin-Reflect: "Internal Server Error" NullPointerException with @RequestMapping on Kotlin property accessors Dec 19, 2023
@sdeleuze sdeleuze added this to the 6.1.3 milestone Dec 19, 2023
@sdeleuze sdeleuze added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Dec 19, 2023
@sbrannen sbrannen changed the title NullPointerException with @RequestMapping on Kotlin property accessors NullPointerException with @RequestMapping on Kotlin property accessors Dec 19, 2023
sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Dec 19, 2023
The variant in org.springframework.web.method.support in
order to allow testing with a various classes.

See spring-projectsgh-31856
sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Dec 19, 2023
This commit refines InvocableHandlerMethod (both Servlet and
Reactive variants) in order to support annotated property
accessors as they translate into regular Java methods, instead
of throwing a NullPointerException.

Closes spring-projectsgh-31856
@sdeleuze
Copy link
Contributor

There was 2 main approaches possible to solve this issue:

  • Filter out Kotlin property accessors at RequestMappingHandlerMapping level to skip them
  • Add support for such use case in InvocableHandlerMethod

I chose the later because conceptually, those Kotlin property accessors translate into plain Java methods that we are unable to distinguish from regular Java methods unless Kotlin reflection is used. That also avoid to introduce an unnecessary performance overhead.

I also chose to not introduce a dependency on Kotlin reflection in KotlinDetector as kotlin-reflect is not a mandatory dependency of Spring, and as the checks performed at KotlinDetector are expected to be fast and only rely on JVM bytecode. The check in KotlinDelegate is also more efficient as the return value of ReflectJvmMapping.getKotlinFunction can be reused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support type: bug A general bug
Projects
None yet
Development

No branches or pull requests

6 participants