Ziemowit Stolarczyk
Ziemowit Stolarczyk

Reputation: 1039

Groovy + Spring - DI with no boilerplate (constructor) code

I have created spring-boot project which bases on Groovy instead of Java.

Now I have following @RestController:

@RestController
class HelloRest {

    private final HelloService helloService

    @GetMapping("hello")
    String hello(@RequestParam("name") String name) {
        helloService.createHelloMessage(name)
    }
}

Question is how to inject

@Service
class HelloService {...} 

in most simple way avoiding boilerplate (in this case the constructor) code?

In Java I would use: @lombok.RequiredArgsConstructor and in fact it works also if I use it in my groovy project.

On the other hand for example the @Immutable annotation from groovy.transform doesn't work as it creates in fact more than single constructor. Whereas Spring expects single constructor to be able automatically @Autowired the dependencies.

As far I see 2 solutions:

Is there any solution build into Groovy which could be used here instead?

Upvotes: 4

Views: 1377

Answers (2)

Szymon Stepniak
Szymon Stepniak

Reputation: 42234

At this moment there is no Groovy mechanism that does same thing as @lombok.RequiredArgsConstructor. The main problem in your case is that Groovy always generates no-args default constructor for all currently known features like @Immutable annotation. The closest (but not accurate) way is to use @TupleConstructor like:

@RestController
@TupleConstructor(includes = ['helloService'], includeFields = true, includeProperties = false, force = true)
class HelloRest {

    private final HelloService helloService

    @GetMapping("hello")
    String hello(@RequestParam("name") String name) {
        return helloService.createHelloMessage(name)
    }
}

This Groovy code will produce bytecode similar to this Java code:

@RestController
@TupleConstructor(
    includeFields = true,
    force = true,
    includeProperties = false,
    includes = {"helloService"}
)
public class HelloRest implements GroovyObject {
    private final HelloService helloService;

    public HelloRest(HelloService helloService) {
        CallSite[] var2 = $getCallSiteArray();
        MetaClass var3 = this.$getStaticMetaClass();
        this.metaClass = var3;
        this.helloService = (HelloService)ScriptBytecodeAdapter.castToType(helloService, HelloService.class);
    }

    public HelloRest() {
        CallSite[] var1 = $getCallSiteArray();
        this((HelloService)null);
    }

    @GetMapping({"hello"})
    public String hello(@RequestParam("name") String name) {
        CallSite[] var2 = $getCallSiteArray();
        return (String)ShortTypeHandling.castToString(var2[0].call(this.helloService, name));
    }
}

It is almost what you need, except this default constructor that was generated as well.

Things are getting even more complicated when using @Immutable annotation, because this version would generate 3 constructors:

  • public HelloRest(HelloService helloService)
  • public HelloRest()
  • public HelloRest(HashMap args)

Of course in this case you would have to remove private final in front of HelloService field definition, because this AST transformation works only with fields that are not yet final.

In this case two options you have found (creating construct manually or using Lombok) are probably the best solutions to your problem.

Alternative solution

There is also one "dirty" solution that allows you to write less amount of code, but promotes injection by reflection. Consider following code:

@RestController
class HelloRest {

    @Autowired
    private final HelloService helloService

    @GetMapping("hello")
    String hello(@RequestParam("name") String name) {
        return helloService.createHelloMessage(name)
    }
}

It will generate bytecode similar to following Java code:

@RestController
public class HelloRest implements GroovyObject {
    @Autowired
    private final HelloService helloService;

    public HelloRest() {
        CallSite[] var1 = $getCallSiteArray();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    @GetMapping({"hello"})
    public String hello(@RequestParam("name") String name) {
        CallSite[] var2 = $getCallSiteArray();
        return (String)ShortTypeHandling.castToString(var2[0].call(this.helloService, name));
    }
}

Even though there is only single default constructor that does not even touch our helloService field, Spring bean will get injected by reflection. I share this option only to show all alternatives, although your initial instinct to use constructor injection is the best possible way to use dependency injection in practice.

Upvotes: 4

Marcin Grzejszczak
Marcin Grzejszczak

Reputation: 11179

You can use @Cannonical or @Immutable on the class. That way the constructor will be created for you

Upvotes: -1

Related Questions