Reputation: 966
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
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
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