Spring-hateoas: ControllerLinkBuilder doesn't work with Groovy/Kotlin

Created on 27 Jul 2014  路  14Comments  路  Source: spring-projects/spring-hateoas

The InvocationRecordingMethodInterceptor overflows the stack when using non-statically compiled Groovy. The problem exists because the interceptor is returning a new proxy with each invocation, and Groovy's method dispatching workflow inspects the metaClass of each object to determine the appropriate call site to use.

Simple example demonstrating the issue:

@Configuration
@ComponentScan
@EnableAutoConfiguration
@EnableJpaRepositories
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
class Main {
  static void main(_) {
     SpringApplication.run this, [] as String[]
  }
}

@javax.persistence.Entity
class Foo {
  @javax.persistence.Id
  Long id
  String name
}

@Repository
interface Foos extends CrudRepository<Foo, Long> {
}

@RestController
@RequestMapping("/foo")
class FooController {

  @Autowired
  Foos foos

  @RequestMapping(method = GET)
  Resources<Foo> list() {
    def foos = foos.findAll()
    def resources = new Resources<>(foos)
    if (resources) {
      resources.add(linkTo(methodOn(this.class).list()).withSelfRel())
    }
    resources
  }
}

A "fix" would be to statically compile the FooController, but this means that we can't use the metaClass:

@CompileStatic
@RestController
@RequestMapping("/foo")
class FooController {
  ...
}

I'm not sure what the best solution to this is... It seems like there should be a way to ascertain if the current invocation is identical to the last invocation, though I'm not sure how that could be determined given a new proxy object is returned each time. Probably need help from @melix on this one.

kotlin

Most helpful comment

Same issue with groovy 2.4.6 and spring boot 1.3.5.RELEASE (running on OSX, java 1.8)

I am basing my groovy code on https://github.com/spring-guides/gs-rest-hateoas
the issue is StackOverflow from greeting.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel())

With the @CompileStatic workaround I get
Error:(26, 29) Groovyc: [Static type checking] - Cannot find matching method java.lang.Object#greeting(java.lang.String). Please check if the declared type is right and if the method exists.

All 14 comments

/cc @glaforge

I tried using the @CompileStatic work around and the stack overvlow still occurrs. @danveloper were you able to verify that statically compiling the controller worked for you? For our project, using statically compiled controllers would probably work for us.

Yeah -- @CompileStatic works

Any news? @CompileStatic does not work for me. :-(

Using @CompileStatic gave me a org.gradle.api.internal.tasks.compile.CompilationFailedException. It couldn't find the method referenced by methodOn - error message: Please check if the declared type is right and if the method exists

Still the bug with @CompileStatic, groovy 2.4.5 and spring boot 1.3.3.RELEASE
Any others solutions ?

java.lang.StackOverflowError: null
at java.lang.reflect.Proxy$KeyX.(Proxy.java:500) ~[na:1.8.0_72]
at java.lang.reflect.Proxy$KeyFactory.apply(Proxy.java:548) ~[na:1.8.0_72]
at java.lang.reflect.Proxy$KeyFactory.apply(Proxy.java:539) ~[na:1.8.0_72]
at java.lang.reflect.WeakCache.get(WeakCache.java:120) ~[na:1.8.0_72]
at java.lang.reflect.Proxy.getProxyClass0(Proxy.java:419) ~[na:1.8.0_72]
at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:719) ~[na:1.8.0_72]
at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:121) ~[spring-aop-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:111) ~[spring-aop-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:96) ~[spring-aop-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.hateoas.core.DummyInvocationUtils.getProxyWithInterceptor(DummyInvocationUtils.java:162) ~[spring-hateoas-0.19.0.RELEASE.jar:na]
at org.springframework.hateoas.core.DummyInvocationUtils.access$100(DummyInvocationUtils.java:38) ~[spring-hateoas-0.19.0.RELEASE.jar:na]
at org.springframework.hateoas.core.DummyInvocationUtils$InvocationRecordingMethodInterceptor.intercept(DummyInvocationUtils.java:100) ~[spring-hateoas-0.19.0.RELEASE.jar:na]
at hello.GreetingController$$EnhancerByCGLIB$$33120faf.getMetaClass() ~[main/:na]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.createPogoSite(CallSiteArray.java:147) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.createCallSite(CallSiteArray.java:164) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.4.5.jar:2.4.5]
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.4.5.jar:2.4.5]

Same issue exists when using Hateoas with Kotlin.

Same issue with groovy 2.4.6 and spring boot 1.3.5.RELEASE (running on OSX, java 1.8)

I am basing my groovy code on https://github.com/spring-guides/gs-rest-hateoas
the issue is StackOverflow from greeting.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel())

With the @CompileStatic workaround I get
Error:(26, 29) Groovyc: [Static type checking] - Cannot find matching method java.lang.Object#greeting(java.lang.String). Please check if the declared type is right and if the method exists.

@compwron Looks like your GreetingController is casted to an Object. Try

greeting.add(linkTo(((GreetingController)methodOn(GreetingController.class)).greeting(name)).withSelfRel())

That looks cool! Unfortunately I no longer have access to the project code that might be fixed by this and I am too lazy to set up a new project from scratch right now to test it. I hope that people in the future come here and find this solution helpful.

cc/ @dudadornelles

I'm seeing this issue when using Kotlin. Does anyone know of a workaround? @JAnderton possibly?

A reasonable workaround for this issue in Kotlin is to use a different linkTo method, such as linkTo(Method, Object...).

Still occurs StackOverflowError.

Environment

  • Groovy 2.5.0-beta-1
  • Spring Boot 2.0.0.M3, Spring 5.0.0.RC3

__GreetingController.groovy__
````groovy
@GetMapping("/greeting2")
HttpEntity greeting2(@RequestParam(name = "name", required = false, defaultValue = "World") String name) {
Greeting greeting = new Greeting(String.format(TEMPLATE, name))

    GroovyGreetingController greetingController = methodOn(GroovyGreetingController)
    HttpEntity<Greeting> httpEntity = greetingController.greeting(name)  // exception here
    ControllerLinkBuilder controllerLinkBuilder = linkTo(httpEntity)
    Link link = controllerLinkBuilder.withSelfRel()

    greeting.add(link)
    return new ResponseEntity<>(greeting, HttpStatus.OK)
}

````

__Error log__
error java.lang.StackOverflowError: null at java.lang.reflect.Proxy$KeyX.<init>(Proxy.java:500) ~[na:1.8.0_121] at java.lang.reflect.Proxy$KeyFactory.apply(Proxy.java:548) ~[na:1.8.0_121] at java.lang.reflect.Proxy$KeyFactory.apply(Proxy.java:539) ~[na:1.8.0_121] at java.lang.reflect.WeakCache.get(WeakCache.java:120) ~[na:1.8.0_121] at java.lang.reflect.Proxy.getProxyClass0(Proxy.java:419) ~[na:1.8.0_121] at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:719) ~[na:1.8.0_121] at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:123) ~[spring-aop-5.0.0.RC3.jar:5.0.0.RC3] at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:113) ~[spring-aop-5.0.0.RC3.jar:5.0.0.RC3] at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:97) ~[spring-aop-5.0.0.RC3.jar:5.0.0.RC3] at org.springframework.hateoas.core.DummyInvocationUtils.getProxyWithInterceptor(DummyInvocationUtils.java:174) ~[spring-hateoas-0.23.0.RELEASE.jar:na] at org.springframework.hateoas.core.DummyInvocationUtils.access$000(DummyInvocationUtils.java:44) ~[spring-hateoas-0.23.0.RELEASE.jar:na] at org.springframework.hateoas.core.DummyInvocationUtils$InvocationRecordingMethodInterceptor.intercept(DummyInvocationUtils.java:112) ~[spring-hateoas-0.23.0.RELEASE.jar:na] at hello.GroovyGreetingController$$EnhancerByCGLIB$$e844180e.getMetaClass(<generated>) ~[classes/:na] at org.codehaus.groovy.runtime.callsite.CallSiteArray.createPogoSite(CallSiteArray.java:147) ~[groovy-all-2.5.0-beta-1.jar:2.5.0.beta-1] at org.codehaus.groovy.runtime.callsite.CallSiteArray.createCallSite(CallSiteArray.java:164) ~[groovy-all-2.5.0-beta-1.jar:2.5.0.beta-1] at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.5.0-beta-1.jar:2.5.0.beta-1] at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.5.0-beta-1.jar:2.5.0.beta-1] at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) ~[groovy-all-2.5.0-beta-1.jar:2.5.0.beta-1] at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57) ~[groovy-all-2.5.0-beta-1.jar:2.5.0.beta-1] // below omitted

__Breakpoint.png__
breakpoint

I was getting a similar issue in my implementation.

My class def (kotlin format)

@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonRootName("content")
class WebSocketInfo (

        @ApiModelProperty(example = "1")
        val id : String,

        @ApiModelProperty(example = "localhost/127.0.0.1:8080")
        val local_address : String,

        @ApiModelProperty(example = "/0:0:0:0:0:0:0:1:56978")
        val remote_address : String,

        @ApiModelProperty(example = "/sddf")
        val uri: String) : ResourceSupport() {

    constructor(webSocketSession: WebSocketSession) : this(webSocketSession.id,
            webSocketSession.localAddress.toString(),
            webSocketSession.remoteAddress.toString(),
            webSocketSession.uri!!.toASCIIString())

    init {
        add(ControllerLinkBuilder.linkTo(ControllerLinkBuilder.methodOn(SocketController::class.java).getSocketById("0")).withRel("peer_id"))

    }
}

My initial function

 @GetMapping("/socket/{id}")
    @ApiOperation(value = "Retrieve information on a specific websocket", produces = "application/json")
    fun getSocketById(

            @ApiParam(name = "id", value = "The unique ID of the websocket in question", required = true)
            @PathVariable(value = "id") socketID: String): WebSocketInfo {

        val filteredSession = globals.connectedSockets.filter { it.id == socketID }
        if (filteredSession.isNotEmpty()) {
            return WebSocketInfo(filteredSession[0])
        }
        throw Exception("Socket with ID $socketID not found")
    }

What i did was change the return type from: WebSocketInfo to Resource< WebSocketInfo>

return Resource(WebSocketInfo(filteredSession[0]))

And the error went away - perhaps wrapping whatever object you re returning in a Resource will fix your errors.

Was this page helpful?
0 / 5 - 0 ratings