diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java index 40628709dfe..34e1f595752 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java @@ -106,6 +106,8 @@ import static com.sun.tools.javac.code.Kinds.Kind.TYP; import static com.sun.tools.javac.code.Kinds.Kind.VAR; import static com.sun.tools.javac.code.TypeTag.BOT; import static com.sun.tools.javac.code.TypeTag.VOID; +import com.sun.tools.javac.jvm.Target; +import com.sun.tools.javac.tree.JCTree.JCThrow; /** * This pass desugars lambda expressions into static methods @@ -128,6 +130,7 @@ public class LambdaToMethod extends TreeTranslator { private TreeMaker make; private final Types types; private final TransTypes transTypes; + private final Target target; private Env attrEnv; /** info about the current class being processed */ @@ -188,6 +191,7 @@ public class LambdaToMethod extends TreeTranslator { make = TreeMaker.instance(context); types = Types.instance(context); transTypes = TransTypes.instance(context); + target = Target.instance(context); Options options = Options.instance(context); dumpLambdaToMethodStats = options.isSet("debug.dumpLambdaToMethodStats"); dumpLambdaDeserializationStats = options.isSet("debug.dumpLambdaDeserializationStats"); @@ -248,7 +252,7 @@ public class LambdaToMethod extends TreeTranslator { /** * list of deserialization cases */ - private final Map> deserializeCases = new HashMap<>(); + private final Map deserializeCases = new HashMap<>(); /** * deserialize method symbol @@ -319,7 +323,7 @@ public class LambdaToMethod extends TreeTranslator { int prevPos = make.pos; try { make.at(tree); - kInfo.addMethod(makeDeserializeMethod()); + makeDeserializeMethod().forEach(kInfo::addMethod); } finally { make.at(prevPos); } @@ -647,24 +651,59 @@ public class LambdaToMethod extends TreeTranslator { return trans_block; } - private JCMethodDecl makeDeserializeMethod() { + // When an instance created for a "lambda" is serialized, the type that is + // serialized is java.lang.invoke.SerializedLambda. + // Its SerializedLambda.readResolve will call method $deserializeLambda$ + // on the class containing the lambda, passing the SerializedLambda as + // a parameter. The $deserializeLambda$ is responsible for recreating the + // appropriate instance. + // + // The $deserializeLambda$ looks like this: + // private static Object $deserializeLambda$(final java.lang.invoke.SerializedLambda lambda) { + // switch (lambda.getImplMethodName()) { + // case -> return $deserializeLambda$(lambda); + // } + // throw new IllegalArgumentException("Invalid lambda deserialization"); + // } + // + // The $deserializeLambda$ methods then look like: + // private static Object $deserializeLambda$(final java.lang.invoke.SerializedLambda lambda) { + // if (lambda.getImplMethodKind() == ... && + // lambda.getFunctionalInterfaceClass().equals(...) && + // lambda.getFunctionalInterfaceMethodName().equals(...) && + // lambda.getFunctionalInterfaceMethodSignature().equals(...) && + // lambda.getImplClass().equals(...) && + // lambda.getImplMethodSignature().equals(...) && + // lambda.getInstantiatedMethodType().equals(...)) return ; + // //any additional deserialization cases with the same implMethodName. + // throw new IllegalArgumentException("Invalid lambda deserialization"); + // } + // + // The $deserializeLambda$ may contain multiple if statements if + // there are multiple SerializedLambdas with the same implMethodName name. + // This may happen when a method references is serialized. + private List makeDeserializeMethod() { ListBuffer cases = new ListBuffer<>(); ListBuffer breaks = new ListBuffer<>(); - for (Map.Entry> entry : kInfo.deserializeCases.entrySet()) { + ListBuffer deserializeMethods = new ListBuffer<>(); + for (Map.Entry entry : kInfo.deserializeCases.entrySet()) { + deserializeMethods.append(createImplementationNameDeserializationMethod(entry.getValue())); + JCBreak br = make.Break(null); breaks.add(br); - List stmts = entry.getValue().append(br).toList(); + List stmts = List.of( + make.Return(make.App(make.QualIdent(entry.getValue().deserializationMethod), List.of(make.Ident(kInfo.deserParamSym)))), + br + ); cases.add(make.Case(JCCase.STATEMENT, List.of(make.ConstantCaseLabel(make.Literal(entry.getKey()))), null, stmts, null)); } - JCSwitch sw = make.Switch(deserGetter("getImplMethodName", syms.stringType), cases.toList()); + JCSwitch sw = make.Switch(deserGetter(kInfo.deserParamSym, "getImplMethodName", syms.stringType), cases.toList()); for (JCBreak br : breaks) { br.target = sw; } JCBlock body = make.Block(0L, List.of( sw, - make.Throw(makeNewClass( - syms.illegalArgumentExceptionType, - List.of(make.Literal("Invalid lambda deserialization")))))); + createThrowInvalidLambdaDeserialization())); JCMethodDecl deser = make.MethodDef(make.Modifiers(kInfo.deserMethodSym.flags()), names.deserializeLambda, make.QualIdent(kInfo.deserMethodSym.getReturnType().tsym), @@ -676,6 +715,32 @@ public class LambdaToMethod extends TreeTranslator { deser.sym = kInfo.deserMethodSym; deser.type = kInfo.deserMethodSym.type; //System.err.printf("DESER: '%s'\n", deser); + deserializeMethods.append(lower.translateMethod(attrEnv, deser, make)); + return deserializeMethods.toList(); + } + + private JCThrow createThrowInvalidLambdaDeserialization() { + return make.Throw(makeNewClass( + syms.illegalArgumentExceptionType, + List.of(make.Literal("Invalid lambda deserialization")))); + } + + private JCMethodDecl createImplementationNameDeserializationMethod(DeserializationCase deserializationCase) { + JCBlock body = make.Block(0L, + deserializationCase.stmts + .append(createThrowInvalidLambdaDeserialization()) + .toList()); + JCMethodDecl deser = make.MethodDef(make.Modifiers(deserializationCase.deserializationMethod().flags()), + deserializationCase.deserializationMethod().name, + make.QualIdent(deserializationCase.deserializationMethod().getReturnType().tsym), + List.nil(), + List.of(make.VarDef(deserializationCase.deserParamSym(), null)), + List.nil(), + body, + null); + deser.sym = deserializationCase.deserializationMethod(); + deser.type = deserializationCase.deserializationMethod().type; + //System.err.printf("DESER: '%s'\n", deser); return lower.translateMethod(attrEnv, deser, make); } @@ -716,41 +781,55 @@ public class LambdaToMethod extends TreeTranslator { } } String implClass = classSig(types.erasure(refSym.owner.type)); - String implMethodName = refSym.getQualifiedName().toString(); + Name implMethodNameAsName = refSym.getQualifiedName(); + String implMethodName = implMethodNameAsName.toString(); String implMethodSignature = typeSig(types.erasure(refSym.type)); String instantiatedMethodType = typeSig(types.erasure(samType)); int implMethodKind = refSym.referenceKind(); - JCExpression kindTest = eqTest(syms.intType, deserGetter("getImplMethodKind", syms.intType), + + DeserializationCase deserializationCase = kInfo.deserializeCases.computeIfAbsent(implMethodName, _ -> { + Name currentDeserializationMethodName = implMethodNameAsName == names.init + ? names.deserializeLambda.append(names.fromString("init")) + : names.deserializeLambda.append(target.syntheticNameChar(), implMethodNameAsName); + MethodSymbol caseDeserializationMethod = makePrivateSyntheticMethod(STATIC, currentDeserializationMethodName, + kInfo.deserMethodSym.type, kInfo.clazz.sym); + VarSymbol caseDeserializationParam = new VarSymbol(FINAL, names.fromString("lambda"), + syms.serializedLambdaType, caseDeserializationMethod); + return new DeserializationCase(caseDeserializationMethod, caseDeserializationParam, new ListBuffer<>()); + }); + VarSymbol deserParamSym = deserializationCase.deserParamSym(); + + JCExpression kindTest = eqTest(syms.intType, deserGetter(deserParamSym, "getImplMethodKind", syms.intType), make.Literal(implMethodKind)); ListBuffer serArgs = new ListBuffer<>(); int i = 0; for (Type t : indyType.getParameterTypes()) { List indexAsArg = new ListBuffer().append(make.Literal(i)).toList(); List argTypes = new ListBuffer().append(syms.intType).toList(); - serArgs.add(make.TypeCast(types.erasure(t), deserGetter("getCapturedArg", syms.objectType, argTypes, indexAsArg))); + serArgs.add(make.TypeCast(types.erasure(t), deserGetter(deserParamSym, "getCapturedArg", syms.objectType, argTypes, indexAsArg))); ++i; } JCStatement stmt = make.If( - deserTest(deserTest(deserTest(deserTest(deserTest(deserTest( - kindTest, - "getFunctionalInterfaceClass", functionalInterfaceClass), - "getFunctionalInterfaceMethodName", functionalInterfaceMethodName), - "getFunctionalInterfaceMethodSignature", functionalInterfaceMethodSignature), - "getImplClass", implClass), - "getImplMethodSignature", implMethodSignature), - "getInstantiatedMethodType", instantiatedMethodType), + deserTest(deserParamSym, + deserTest(deserParamSym, + deserTest(deserParamSym, + deserTest(deserParamSym, + deserTest(deserParamSym, + deserTest(deserParamSym, + kindTest, + "getFunctionalInterfaceClass", functionalInterfaceClass), + "getFunctionalInterfaceMethodName", functionalInterfaceMethodName), + "getFunctionalInterfaceMethodSignature", functionalInterfaceMethodSignature), + "getImplClass", implClass), + "getImplMethodSignature", implMethodSignature), + "getInstantiatedMethodType", instantiatedMethodType), make.Return(makeIndyCall( pos, syms.lambdaMetafactory, names.altMetafactory, staticArgs, indyType, serArgs.toList(), samSym.name)), null); - ListBuffer stmts = kInfo.deserializeCases.get(implMethodName); - if (stmts == null) { - stmts = new ListBuffer<>(); - kInfo.deserializeCases.put(implMethodName, stmts); - } if (dumpLambdaDeserializationStats) { log.note(pos, Notes.LambdaDeserializationStat( functionalInterfaceClass, @@ -762,7 +841,7 @@ public class LambdaToMethod extends TreeTranslator { implMethodSignature, instantiatedMethodType)); } - stmts.append(stmt); + deserializationCase.stmts().append(stmt); } private JCExpression eqTest(Type argType, JCExpression arg1, JCExpression arg2) { @@ -772,12 +851,12 @@ public class LambdaToMethod extends TreeTranslator { return testExpr; } - private JCExpression deserTest(JCExpression prev, String func, String lit) { + private JCExpression deserTest(VarSymbol deserParamSym, JCExpression prev, String func, String lit) { MethodType eqmt = new MethodType(List.of(syms.objectType), syms.booleanType, List.nil(), syms.methodClass); Symbol eqsym = rs.resolveQualifiedMethod(null, attrEnv, syms.objectType, names.equals, List.of(syms.objectType), List.nil()); JCMethodInvocation eqtest = make.Apply( List.nil(), - make.Select(deserGetter(func, syms.stringType), eqsym).setType(eqmt), + make.Select(deserGetter(deserParamSym, func, syms.stringType), eqsym).setType(eqmt), List.of(make.Literal(lit))); eqtest.setType(syms.booleanType); JCBinary compound = make.Binary(Tag.AND, prev, eqtest); @@ -786,16 +865,16 @@ public class LambdaToMethod extends TreeTranslator { return compound; } - private JCExpression deserGetter(String func, Type type) { - return deserGetter(func, type, List.nil(), List.nil()); + private JCExpression deserGetter(VarSymbol deserParamSym, String func, Type type) { + return deserGetter(deserParamSym, func, type, List.nil(), List.nil()); } - private JCExpression deserGetter(String func, Type type, List argTypes, List args) { + private JCExpression deserGetter(VarSymbol deserParamSym, String func, Type type, List argTypes, List args) { MethodType getmt = new MethodType(argTypes, type, List.nil(), syms.methodClass); Symbol getsym = rs.resolveQualifiedMethod(null, attrEnv, syms.serializedLambdaType, names.fromString(func), argTypes, List.nil()); return make.Apply( List.nil(), - make.Select(make.Ident(kInfo.deserParamSym).setType(syms.serializedLambdaType), getsym).setType(getmt), + make.Select(make.Ident(deserParamSym).setType(syms.serializedLambdaType), getsym).setType(getmt), args).setType(type); } @@ -1273,6 +1352,14 @@ public class LambdaToMethod extends TreeTranslator { } } + /** + * Deserialization statements for a given lambda implementation name, together + * with the (future) enclosing deserialization method. + */ + record DeserializationCase(MethodSymbol deserializationMethod, + VarSymbol deserParamSym, + ListBuffer stmts) {} + /** * **************************************************************** * Signature Generation diff --git a/test/langtools/tools/javac/lambda/ManyLambdasSerialization.java b/test/langtools/tools/javac/lambda/ManyLambdasSerialization.java new file mode 100644 index 00000000000..202cc5e4cd6 --- /dev/null +++ b/test/langtools/tools/javac/lambda/ManyLambdasSerialization.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8381812 + * @summary Check that serializable lambda desugaring can handle many serializable lambdas + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/com.sun.tools.javac.util + * @compile ManyLambdasSerialization.java + * @build toolbox.ToolBox + * @run junit ManyLambdasSerialization + */ + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import toolbox.JavacTask; +import toolbox.JavaTask; +import toolbox.Task; +import toolbox.ToolBox; + +public class ManyLambdasSerialization { + private static final int LAMBDA_COUNT = 1000; + private final ToolBox tb = new ToolBox(); + + @Test + public void testManySerializableLambdasDifferentImplMethodName() throws IOException { + List lambdaMethods = new ArrayList<>(); + List tests = new ArrayList<>(); + + for (int i = 0; i < LAMBDA_COUNT; i++) { + String index = Integer.toString(i); + + lambdaMethods.add(""" + private static Supplier create${INDEX}() { + return (Supplier & Serializable) () -> ${INDEX}; + } + """.replace("${INDEX}", index)); + tests.add(" runTest(${INDEX}, create${INDEX}());\n".replace("${INDEX}", index)); + } + + StringBuilder code = new StringBuilder(); + code.append(""" + import java.io.*; + import java.util.function.Supplier; + class Test { + """); + + lambdaMethods.forEach(code::append); + + code.append(""" + private static void runTest(int expectedResult, Supplier instance) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(instance); + oos.close(); + } + try (ByteArrayInputStream in = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(in)) { + int actual = ((Supplier) ois.readObject()).get(); + if (expectedResult != actual) { + throw new AssertionError("Expected: " + expectedResult + ", actual: " + actual); + } + } + } + + public static void main() throws Exception { + """); + + tests.forEach(code::append); + + code.append(""" + System.err.println("OK"); + } + } + """); + + new JavacTask(tb) + .sources(code.toString()) + .outdir(".") + .run() + .writeAll(); + + List output = new JavaTask(tb) + .classpath(".") + .className("Test") + .run() + .writeAll() + .getOutputLines(Task.OutputKind.STDERR); + + tb.checkEqual(List.of("OK"), output); + } + +// @Test The current deserialization does not support too many serialized lambdas with the same implementation method name + public void testManySerializableLambdasSameImplMethodName() { + List lambdaMethods = new ArrayList<>(); + List tests = new ArrayList<>(); + + for (int i = 0; i < LAMBDA_COUNT; i++) { + String index = Integer.toString(i); + + lambdaMethods.add(""" + private static Function create${INDEX}() { + return (Function & Serializable) Test::id; + } + record Box${INDEX}(int i) {} + """.replace("${INDEX}", index)); + tests.add(""" + runTest(create${INDEX}(), t -> { + int expectedResult = ${INDEX}; + //in case of a bad deserialization, the implicit cast here would fail: + int actual = t.apply(new Box${INDEX}(expectedResult)).i(); + if (expectedResult != actual) { + throw new AssertionError("Expected: " + expectedResult + ", actual: " + actual); + } + }); + """.replace("${INDEX}", index)); + } + + StringBuilder code = new StringBuilder(); + code.append(""" + import java.io.*; + import java.util.function.*; + class Test { + """); + + lambdaMethods.forEach(code::append); + + code.append(""" + private static T id(T t) { return t; } + private static void runTest(Function testInstance, Consumer> checker) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(testInstance); + oos.close(); + } + try (ByteArrayInputStream in = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(in)) { + checker.accept((Function) ois.readObject()); + } + } + + public static void main() throws Exception { + """); + + tests.forEach(code::append); + + code.append(""" + System.err.println("OK"); + } + } + """); + + new JavacTask(tb) + .sources(code.toString()) + .outdir(".") + .run() + .writeAll(); + + List output = new JavaTask(tb) + .classpath(".") + .className("Test") + .run() + .writeAll() + .getOutputLines(Task.OutputKind.STDERR); + + tb.checkEqual(List.of("OK"), output); + } + + @Test + public void testManySerializableLambdasCapturing() { + List lambdaMethods = new ArrayList<>(); + List tests = new ArrayList<>(); + + for (int i = 0; i < LAMBDA_COUNT; i++) { + String index = Integer.toString(i); + int capturedValues = 10; + + lambdaMethods.add(""" + private static Supplier create${INDEX}() { + ${DECLARATIONS} + return (Supplier & Serializable) () -> ${CAPTURED}; + } + """.replace("${INDEX}", index) + .replace("${DECLARATIONS}", Stream.iterate(0, v -> v + 1).limit(capturedValues).map(v -> "int v" + v + " = " + index + ";\n").collect(Collectors.joining())) + .replace("${CAPTURED}", Stream.iterate(0, v -> v + 1).limit(capturedValues).map(v -> "v" + v).collect(Collectors.joining(" + ")))); + tests.add(" runTest(${EXPECTED}, create${INDEX}());\n".replace("${INDEX}", index).replace("${EXPECTED}", String.valueOf(capturedValues * i))); + } + + StringBuilder code = new StringBuilder(); + code.append(""" + import java.io.*; + import java.util.function.Supplier; + class Test { + """); + + lambdaMethods.forEach(code::append); + + code.append(""" + private static void runTest(int expectedResult, Supplier instance) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(instance); + oos.close(); + } + try (ByteArrayInputStream in = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(in)) { + int actual = ((Supplier) ois.readObject()).get(); + if (expectedResult != actual) { + throw new AssertionError("Expected: " + expectedResult + ", actual: " + actual); + } + } + } + + public static void main() throws Exception { + """); + + tests.forEach(code::append); + + code.append(""" + System.err.println("OK"); + } + } + """); + + new JavacTask(tb) + .sources(code.toString()) + .outdir(".") + .run() + .writeAll(); + + List output = new JavaTask(tb) + .classpath(".") + .className("Test") + .run() + .writeAll() + .getOutputLines(Task.OutputKind.STDERR); + + tb.checkEqual(List.of("OK"), output); + } + + @Test + public void testVeryManySerializableLambdasCapturing() { + List lambdaMethods = new ArrayList<>(); + List tests = new ArrayList<>(); + + for (int i = 0; i < LAMBDA_COUNT; i++) { + String index = Integer.toString(i); + int capturedValues = 200; + + lambdaMethods.add(""" + private static Supplier create${INDEX}() { + ${DECLARATIONS} + return (Supplier & Serializable) () -> ${CAPTURED}; + } + """.replace("${INDEX}", index) + .replace("${DECLARATIONS}", Stream.iterate(0, v -> v + 1).limit(capturedValues).map(v -> "int v" + v + " = " + index + ";\n").collect(Collectors.joining())) + .replace("${CAPTURED}", Stream.iterate(0, v -> v + 1).limit(capturedValues).map(v -> "v" + v).collect(Collectors.joining(" + ")))); + tests.add(" runTest(${EXPECTED}, create${INDEX}());\n".replace("${INDEX}", index).replace("${EXPECTED}", String.valueOf(capturedValues * i))); + } + + StringBuilder code = new StringBuilder(); + code.append(""" + import java.io.*; + import java.util.function.Supplier; + class Test { + """); + + lambdaMethods.forEach(code::append); + + code.append(""" + private static void runTest(int expectedResult, Supplier instance) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(instance); + oos.close(); + } + try (ByteArrayInputStream in = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(in)) { + int actual = ((Supplier) ois.readObject()).get(); + if (expectedResult != actual) { + throw new AssertionError("Expected: " + expectedResult + ", actual: " + actual); + } + } + } + + public static void main() throws Exception { + """); + + tests.forEach(code::append); + + code.append(""" + System.err.println("OK"); + } + } + """); + + new JavacTask(tb) + .sources(code.toString()) + .options("-XDdeserializableLambdaCaseCountLimit=15") + .outdir(".") + .run() + .writeAll(); + + List output = new JavaTask(tb) + .classpath(".") + .className("Test") + .run() + .writeAll() + .getOutputLines(Task.OutputKind.STDERR); + + tb.checkEqual(List.of("OK"), output); + } + + @Test + public void testVerifyWeirdImplNameWorks() { + //make sure method references with name "init" and "" + //don't clash in deserialization method name: + String code = """ + import java.io.*; + import java.util.function.Supplier; + public class Test { + public static Test init() { return new Test("factory"); } + public Test() { this("constructor"); } + + private final String origin; + private Test(String origin) { this.origin = origin;} + public static void main() throws Exception { + Supplier fromConstructor = (Supplier & Serializable) Test::new; + Supplier fromFactory = (Supplier & Serializable) Test::init; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(fromConstructor); + oos.writeObject(fromFactory); + oos.close(); + } + try (ByteArrayInputStream in = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(in)) { + System.err.println(((Supplier) ois.readObject()).get().origin); + System.err.println(((Supplier) ois.readObject()).get().origin); + } + } + } + """; + new JavacTask(tb) + .sources(code.toString()) + .outdir(".") + .run() + .writeAll(); + + List output = new JavaTask(tb) + .classpath(".") + .className("Test") + .run() + .writeAll() + .getOutputLines(Task.OutputKind.STDERR); + + tb.checkEqual(List.of("constructor", "factory"), output); + } +}