Search code examples
javaspocklombokintellij-lombok-plugin

Lombok and Spock: @RequiredArgsConstructor doesn't hide default no-args constructor for a field with a type of interface


It seems that @RequiredArgsConstructor is not working in the code below - but only in a test using Spock framework, and only for a field which is of type of interface Dao.
Strictly speaking - the code is working while it shouldn't work in my opinion, taking into account that similar test under JUnit5 doesn't compile at all.

Could someone explain is it a bug, or a feature ?

package brumba;
public interface Dao {
    Integer getValueFor(Integer value);
}

package brumba;

import com.sun.istack.internal.NotNull;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class Brumba {

    @NotNull
    final private Dao dao;

//  If you uncomment the below 2 lines, then the test fails
//    @NotNull
//    final private String name;

    public Integer twice(Integer x){
        return x * 2;
    }

    public Integer twiceDao(Integer x){
        return dao.getValueFor(x);
    }
}

The below code works fine - but only in Spock (a similar test under JUnit5 doesn't compile).
It seems that Spock test somehow sees a default no-args constructor (while JUnit test doesn't see this constructor)
But when the 2 commented lines above were uncommented, then the test failed with the below error:

groovy.lang.GroovyRuntimeException: Could not find matching constructor for: brumba.Brumba()

package brumba

import spock.lang.Specification

class BrumbaTest extends Specification {

    def "twice should multiply argument by 2"() {
        given:
            def testedObject = new Brumba();

        expect:
            y == testedObject.twice( x )

        where:
            x | y
            0 | 0
            1 | 2
            2 | 4
            3 | 6
    }
}

And this JUnit test doesn't compile at all:

package brumba;

class BrumbaJUnit5Test {

    @org.junit.jupiter.api.Test
    void shouldTwice() {
        Brumba br = new Brumba();
    }
} 

the error is:

Error:(7, 21) java: constructor Brumba in class brumba.Brumba cannot be applied to given types;
  required: brumba.Dao,java.lang.String
  found: no arguments

Here are the dependencies I am using for this project:

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.2-groovy-2.5</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.4</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.3.0-M1</version>
    <scope>test</scope>
</dependency>


Solution

  • First of all, I can confirm that this happens for me, too. I never noticed before.

    I had to debug through source code and look at decompiled files in order to understand at least a bit what is going on here. I can tell you a few things:

    • This is unrelated to Lombok. It also happens to any Java class with a single-argument constructor taking an object type (i.e. not a primitive like int), e.g. String or your Dao.
    • It is unrelated to Spock because it also happens outside of Spock.
    • It seems to be related to dynamic Groovy runtime features.
    • I would rather call it a subtle bug than a feature, but I am not sure.

    Java class:

    package de.scrum_master.stackoverflow;
    
    public class Brumba {
      public Brumba(String name) {}
    }
    

    Groovy class:

    package de.scrum_master.stackoverflow
    
    class BrumbaApp {
      static void main(String[] args) {
        new Brumba()
      }
    }
    

    Decompiled Groovy class:

    package de.scrum_master.stackoverflow;
    
    import groovy.lang.GroovyObject;
    import groovy.lang.MetaClass;
    import org.codehaus.groovy.runtime.callsite.CallSite;
    
    public class BrumbaApp implements GroovyObject {
      public BrumbaApp() {
        CallSite[] var1 = $getCallSiteArray();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
      }
    
      public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        var1[0].callConstructor(Brumba.class);
      }
    }
    

    The Groovy runtime class CallSite is actually an interface, but there is AbstractCallSite implementing it. If we look at this method

    public Object callConstructor(Object receiver) throws Throwable {
        return callConstructor(receiver, CallSiteArray.NOPARAM);
    }
    

    and this definition

    public final class CallSiteArray {
        // ...
        public static final Object [] NOPARAM = new Object[0];
        // ...
    

    we understand that actually this method will be called

    public Object callConstructor(Object receiver, Object[] args) throws Throwable {
        return CallSiteArray.defaultCallConstructor(this, receiver, args);
    }
    

    and so forth. I think what happens is that the Object[] of size 0 will be passed through as a constructor parameter somehow and the absence of an element interpreted as a null argument. This is also what you see in a debugger after object instantiation if like in your code the parameter is assigned to a member: The member will have the value null.