rdehuyss
rdehuyss

Reputation: 966

Identical Java SerializedLambda returns different result for implMethodKind

I'm Ronald, the author of JobRunr. JobRunr is a background job scheduling library that uses SerializedLambda and ASM to analyze a Java 8 lambda and converts it to a background job.

Recently, an error was reported and I tried reproducing it in JobRunr so that I can write a test to prevent regression.

The funny thing is that on the same Java version (17.0.2), I cannot reproduce it even if I copy the exact code.

In this project, the generated SerializedLambda has an implMethodKind equal to 5 (REF_invokeVirtual).

Yet, in JobRunr itself, the generated SerializedLambda has an implMethodKind equal to 7 (REF_invokeSpecial).

The actual code to generate the SerializedLambda is as follows:

public class GeoService {
    Logger LOG = LoggerFactory.getLogger(GeoService.class);

    public void executeGeoTreeJob(JobContext jobContext, long geoNameId, UserId userId) {
        LOG.error("Running: " + geoNameId);
    }

    public void run() {
        LOG.error("Starting job");
        UserId userId = new UserId();
        userId.setValue("test");
        long geoNameId = 1234;

        JobLambda jobLambda = () -> executeGeoTreeJob(JobContext.Null, geoNameId, userId);

        SerializedLambda serializedLambda = SerializedLambdaConverter.toSerializedLambda(jobLambda);
        System.out.println("=======");
        System.out.println("serializedLambda " + serializedLambda.getImplMethodKind());
        System.out.println("=======");

        BackgroundJob.enqueue(() -> executeGeoTreeJob(JobContext.Null, geoNameId, userId));
    }
}

In this project, the generated SerializedLambda has an implMethodKind equal to 5 (REF_invokeVirtual).

Yet, in JobRunr itself, the generated SerializedLambda has an implMethodKind equal to 7 (REF_invokeSpecial).

Why do I get different values for implMethodKind? Or, put differently, what do I need to do to the setup / JVM / ... to have the same results as in the example project.

Update:

I create the SerializedLambda as follows:

public class SerializedLambdaConverter {

    private SerializedLambdaConverter() {

    }

    public static <T> SerializedLambda toSerializedLambda(T value) {
        if (!value.getClass().isSynthetic()) {
            throw new IllegalArgumentException("Please provide a lambda expression (e.g. BackgroundJob.enqueue(() -> myService.doWork()) instead of an actual implementation.");
        }

        if (!(value instanceof Serializable)) {
            throw new JobRunrException("The lambda you provided is not Serializable. Please make sure your functional interface is Serializable or use the JobLambda interface instead.");
        }

        try {
            Method writeReplaceMethod = value.getClass().getDeclaredMethod("writeReplace");
            makeAccessible(writeReplaceMethod);
            return (SerializedLambda) writeReplaceMethod.invoke(value);
        } catch (Exception shouldNotHappen) {
            throw shouldNotHappenException(shouldNotHappen);
        }
    }
}

Below the output of javap:

Compiled from "GeoService.java"
public class org.jobrunr.tests.e2e.services.GeoService {
  org.slf4j.Logger LOG;
  public org.jobrunr.tests.e2e.services.GeoService();
  public void executeGeoTreeJob(org.jobrunr.jobs.context.JobContext, long, org.jobrunr.tests.e2e.services.UserId);
  public void run();
}

Attached the headers found in the output of javap -verbose (the full output is a bit too much for here in SO):

Classfile /Users/rdehuyss/Projects/Personal/jobrunr/jobrunr/tests/e2e-vm-jdk/build/classes/java/main/org/jobrunr/tests/e2e/services/GeoService.class
  Last modified 20 Feb 2024; size 4064 bytes
  SHA-256 checksum 595969292bcac503d33a4e54c1b2a4ffa7517f8aa6c50a8a1470400981e08ecb
  Compiled from "GeoService.java"
public class org.jobrunr.tests.e2e.services.GeoService
  minor version: 0
  major version: 52

Upvotes: 3

Views: 272

Answers (2)

Holger
Holger

Reputation: 298459

The SerializedLambda reflects how the lambda expression has been compiled which is, as explained in this answer, compiler dependent. Therefore, the result does not dependent on the Java runtime version but the compiler used for the class containing the lambda expression and there is no runtime option to alter the outcome.

Besides the choice of compiling the body into an instance method or into a static method receiving this as a parameter, the compiler is free to encode the invocation of the private instance method as an invokespecial or invokevirtual behavior. Both is equally valid.

We can use the following program to self-inspect the encoded invocation:

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.spi.ToolProvider;

public class LambdaBinary {
    public static void main(String[] args) {
        ToolProvider.findFirst("javap").ifPresent(new LambdaBinary()::print);
    }
  
    private void print(ToolProvider tp) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        tp.run(pw, pw, "-v", LambdaBinary.class.getName());
        StringBuffer b = sw.getBuffer();
        System.out.append(b,
            b.lastIndexOf("BootstrapMethods:"),
            b.lastIndexOf("InnerClasses:"));
    }
}

When compiled with javac version 8 to 14 or using Eclipse’s compiler, the result will be something like

BootstrapMethods:
  0: #85 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #92 (Ljava/lang/Object;)V
      #94 REF_invokeSpecial LambdaBinary.print:(Ljava/util/spi/ToolProvider;)V
      #97 (Ljava/util/spi/ToolProvider;)V

when compiled with javac of JDK 15 or newer, it prints something like

BootstrapMethods:
  0: #85 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #92 (Ljava/lang/Object;)V
      #94 REF_invokeVirtual LambdaBinary.print:(Ljava/util/spi/ToolProvider;)V
      #97 (Ljava/util/spi/ToolProvider;)V

As said, this difference between REF_invokeSpecial and REF_invokeVirtual is reflected by the SerializedLambda and the only thing you can do, is to adapt your code to handle both values the same way.

Upvotes: 1

Nataliia Dziubenko
Nataliia Dziubenko

Reputation: 36

Just to check, could you try and modify your .idea/compiler.xml, this part:

   <bytecodeTargetLevel target="17">
      <module name="JobRunr.tests.e2e-elasticsearch-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-elasticsearch-jackson.test" target="11" />
      <module name="JobRunr.tests.e2e-json-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-mariadb-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-mariadb-jackson.test" target="11" />
      <module name="JobRunr.tests.e2e-mongo-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-mongo-jackson.test" target="11" />
      <module name="JobRunr.tests.e2e-mysql-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-mysql-jackson.test" target="11" />
      <module name="JobRunr.tests.e2e-oracle-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-oracle-jackson.test" target="11" />
      <module name="JobRunr.tests.e2e-postgres-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-postgres-jackson.test" target="11" />
      <module name="JobRunr.tests.e2e-redis-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-redis-jackson.test" target="11" />
      <module name="JobRunr.tests.e2e-sqlserver-gson.test" target="11" />
      <module name="JobRunr.tests.e2e-sqlserver-jackson.test" target="11" />
      <module name="JobRunr.tests.e2e-ui.main" target="11" />
      <module name="JobRunr.tests.e2e-ui.test" target="11" />
      <module name="JobRunr.tests.e2e-vm-jdk.test" target="11" /> <----- here
    </bytecodeTargetLevel>

Upvotes: 1

Related Questions