1 /*
2 * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24 /*
25 * @test
26 * @bug 8388318
27 * @summary Test serialization of value classes
28 * @enablePreview
29 * @modules java.base/jdk.internal.value
30 * @library /test/lib
31 * @compile ValueSerializationTest.java
32 * @build jdk.test.lib.helpers.StrictInit jdk.test.lib.helpers.StrictProcessor
33 * @comment run the StrictProcessor over the classes that use \@StrictInit to
34 * generate classfiles with STRICT_INIT access flags for its annotated fields
35 * @run driver jdk.test.lib.helpers.StrictProcessor
36 * ValueSerializationTest$IdentityStrictPoint
37 * @run junit/othervm -DdeserializerOnBootclasspath=false ${test.main.class}
38 * @run junit/bootclasspath/othervm -DdeserializerOnBootclasspath=true ${test.main.class}
39 */
40
41 import java.io.ByteArrayInputStream;
42 import java.io.ByteArrayOutputStream;
43 import java.io.DataOutputStream;
44 import java.io.Externalizable;
45 import java.io.IOException;
46 import java.io.InvalidClassException;
47 import java.io.InvalidObjectException;
48 import java.io.NotSerializableException;
49 import java.io.ObjectInput;
50 import java.io.ObjectInputStream;
51 import java.io.ObjectOutput;
52 import java.io.ObjectOutputStream;
53 import java.io.ObjectStreamClass;
54 import java.io.ObjectStreamException;
55 import java.io.Serial;
56 import java.io.Serializable;
57 import java.util.stream.Stream;
58
59 import jdk.internal.value.Deserializer;
60 import jdk.test.lib.helpers.StrictInit;
61 import org.junit.jupiter.api.BeforeAll;
62 import org.junit.jupiter.params.ParameterizedTest;
63 import org.junit.jupiter.params.provider.Arguments;
64 import org.junit.jupiter.params.provider.MethodSource;
65 import static java.io.ObjectStreamConstants.SC_EXTERNALIZABLE;
66 import static java.io.ObjectStreamConstants.SC_SERIALIZABLE;
67 import static java.io.ObjectStreamConstants.STREAM_MAGIC;
68 import static java.io.ObjectStreamConstants.STREAM_VERSION;
69 import static java.io.ObjectStreamConstants.TC_CLASSDESC;
70 import static java.io.ObjectStreamConstants.TC_ENDBLOCKDATA;
71 import static java.io.ObjectStreamConstants.TC_NULL;
72 import static java.io.ObjectStreamConstants.TC_OBJECT;
73 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
74 import static org.junit.jupiter.api.Assertions.assertEquals;
75 import static org.junit.jupiter.api.Assertions.assertThrows;
76 import static org.junit.jupiter.api.Assertions.assertTrue;
77
78 public class ValueSerializationTest {
79
80 private static final boolean DESERIALIZER_ON_BOOTCLASSPATH = Boolean.getBoolean("deserializerOnBootclasspath");
81 private static final Class<NotSerializableException> NSE = NotSerializableException.class;
82 private static final Class<InvalidClassException> ICE = InvalidClassException.class;
83
84 @BeforeAll
85 static void setup() {
86 System.out.println("deserializerOnBootclasspath: " + DESERIALIZER_ON_BOOTCLASSPATH);
87 }
88
89 static Stream<Arguments> serializationFailingInstances() {
90 return DESERIALIZER_ON_BOOTCLASSPATH ? serializationAlwaysFailingInstances()
91 : Stream.concat(serializationAlwaysFailingInstances(),
92 // Unrecognized deserializer leads to ICE
93 deserializerInstances().map(a -> Arguments.of(a, ICE)));
94 }
95
96 static Stream<Arguments> serializationAlwaysFailingInstances() {
97 return Stream.of(
98 Arguments.of(
99 new NonSerializableValue(10, 100),
100 NSE
101 ),
102
103 Arguments.of(
104 new ValueWithNoDeserializer(10, 100),
105 ICE
106 ),
107
108 Arguments.of(
109 new IdentityStrictPoint(),
110 ICE
111 ),
112
113 Arguments.of(
114 new StrictInSuper(),
115 ICE
116 ),
117
118 Arguments.of(
119 new StrictInTwiceSuper(),
120 ICE
121 ),
122
123 Arguments.of(
124 new StrictInAbstractValueSuper(),
125 ICE
126 ),
127
128 // an array of non-serializable value objects
129 Arguments.of(
130 new NonSerializableValue[]{
131 new NonSerializableValue(1, 5)
132 },
133 NSE
134 ),
135
136 Arguments.of(
137 new Object[]{
138 new NonSerializableValue(3, 7)
139 },
140 NSE
141 ),
142
143 Arguments.of(
144 new ExternalizableValue(12, 102),
145 ICE
146 ),
147
148 Arguments.of(
149 new ExternalizableValue[]{
150 new ExternalizableValue(3, 7),
151 new ExternalizableValue(2, 8)
152 },
153 ICE
154 ),
155
156 Arguments.of(
157 new Object[]{
158 new ExternalizableValue(13, 17),
159 new ExternalizableValue(14, 18)
160 },
161 ICE
162 )
163 );
164 }
165
166 /*
167 * Verifies that the given obj that isn't expected to be serializable
168 * throws the expected exception from ObjectOutputStream.writeObject()
169 */
170 @ParameterizedTest
171 @MethodSource("serializationFailingInstances")
172 void testSerializationFails(Object obj, Class<? extends Exception> expectedException)
173 throws Exception {
174 // expect serialization to fail
175 try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {
176 assertThrows(expectedException, () -> oos.writeObject(obj));
177 }
178 }
179
180 static Stream<Object> deserializerInstances() {
181 return Stream.of(
182 new ValueWithDeserializer(11, 101),
183
184 new ValueWithDeserializer[]{
185 new ValueWithDeserializer(1, 5),
186 new ValueWithDeserializer(2, 6)
187 },
188
189 new Object[]{
190 new ValueWithDeserializer(3, 7),
191 new ValueWithDeserializer(4, 8)
192 }
193 );
194 }
195
196 static Stream<Object> serializingInstances() {
197 return DESERIALIZER_ON_BOOTCLASSPATH ? Stream.concat(deserializerInstances(), alwaysSerializingInstances())
198 : alwaysSerializingInstances();
199 }
200
201 static Stream<Object> alwaysSerializingInstances() {
202 return Stream.of(
203 new ValueWriteReplaceWithIdentity(45),
204
205 new ValueWriteReplaceWithIdentity[]{
206 new ValueWriteReplaceWithIdentity(46)
207 },
208
209 new ExtValueWithIdentityReplacement("hello"),
210
211 new ExtValueWithIdentityReplacement[]{
212 new ExtValueWithIdentityReplacement("there")
213 },
214
215 new CustomNumberWithIdentity(16, 42),
216
217 new CustomNumberWithIdentity[] {
218 new CustomNumberWithIdentity(-6, 77),
219 new CustomNumberWithIdentity(54, -79),
220 }
221 );
222 }
223
224 /*
225 * Verifies that a value object that implements java.io.Serializable and is associated with
226 * the JDK internal jdk.internal.value.Deserializer can be serialized and deserialized through
227 * the use of ObjectOutputStream.writeObject() and ObjectInputStream.readObject() successfully.
228 * The deserialized object is then compared with the given obj to verify that they are equal.
229 */
230 @ParameterizedTest
231 @MethodSource("serializingInstances")
232 void testSerDeserSucceeds(Object obj) throws IOException, ClassNotFoundException {
233 // serialize
234 ByteArrayOutputStream baos = new ByteArrayOutputStream();
235 try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
236 oos.writeObject(obj);
237 }
238 byte[] bytes = baos.toByteArray();
239 Object actual;
240 // deserialize
241 try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
242 actual = ois.readObject();
243 }
244 // compare the deserialized with the original
245 if (obj.getClass().isArray()) {
246 assertArrayEquals((Object[]) obj, (Object[]) actual);
247 } else {
248 assertEquals(obj, actual);
249 }
250 }
251
252 static Stream<Arguments> classes() {
253 return Stream.of(
254 Arguments.of(
255 ExtValueWithIdentityReplacement.class,
256 SC_EXTERNALIZABLE,
257 ICE
258 ),
259
260 Arguments.of(
261 ExtValueWithIdentityReplacement.class,
262 SC_SERIALIZABLE,
263 ICE
264 ),
265
266 Arguments.of(
267 IdentityStrictPoint.class,
268 SC_SERIALIZABLE,
269 ICE
270 ),
271
272 Arguments.of(
273 StrictInSuper.class,
274 SC_SERIALIZABLE,
275 ICE
276 ),
277
278 Arguments.of(
279 StrictInTwiceSuper.class,
280 SC_SERIALIZABLE,
281 ICE
282 ),
283
284 Arguments.of(
285 StrictInAbstractValueSuper.class,
286 SC_SERIALIZABLE,
287 ICE
288 ),
289
290 Arguments.of(
291 ValueWithDeserializer.class,
292 SC_EXTERNALIZABLE,
293 ICE
294 ),
295
296 Arguments.of(
297 ValueWithDeserializer.class,
298 SC_SERIALIZABLE,
299 DESERIALIZER_ON_BOOTCLASSPATH ? null : ICE
300 ),
301
302 Arguments.of(
303 CustomNumberWithIdentity.class,
304 SC_SERIALIZABLE,
305 null
306 )
307 );
308 }
309
310 /*
311 * A byte stream is generated containing a reference to the given class
312 * with the given flags and a serial version UID determined in the test method.
313 * The byte stream is then read using ObjectInputStream.readObject() and the test verifies
314 * that if an exception is expected to be thrown then it is thrown, or if the deserialization
315 * is expected to complete normally, then it verifies that no exception is thrown.
316 */
317 @ParameterizedTest
318 @MethodSource("classes")
319 void testDeser(Class<?> clazz, byte flags, Class<? extends Exception> expectedException)
320 throws Exception {
321 ObjectStreamClass clsDesc = ObjectStreamClass.lookup(clazz);
322 long uid = clsDesc == null ? 0L : clsDesc.getSerialVersionUID();
323 byte[] serialBytes = byteStreamFor(clazz.getName(), uid, flags);
324 // deserialize
325 try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialBytes))) {
326 if (expectedException != null) {
327 assertThrows(expectedException, () -> ois.readObject());
328 } else {
329 ois.readObject();
330 }
331 }
332 }
333
334 // Generate a byte stream containing a reference to the named class with the SVID and flags.
335 private static byte[] byteStreamFor(String className, long uid, byte flags) throws Exception {
336 ByteArrayOutputStream baos = new ByteArrayOutputStream();
337 DataOutputStream dos = new DataOutputStream(baos);
338 dos.writeShort(STREAM_MAGIC);
339 dos.writeShort(STREAM_VERSION);
340 dos.writeByte(TC_OBJECT);
341 dos.writeByte(TC_CLASSDESC);
342 dos.writeUTF(className);
343 dos.writeLong(uid);
344 dos.writeByte(flags);
345 dos.writeShort(0); // number of fields
346 dos.writeByte(TC_ENDBLOCKDATA); // no annotations
347 dos.writeByte(TC_NULL); // no superclasses
348 dos.close();
349 return baos.toByteArray();
350 }
351
352 /**
353 * A concrete value class that doesn't implement Serializable (or Externalizable) interface
354 */
355 public static value class NonSerializableValue {
356 public int x;
357 public int y;
358
359 public NonSerializableValue(int x, int y) {
360 this.x = x;
361 this.y = y;
362 }
363
364 @Override
365 public String toString() {
366 return "[NonSerializableValue x=" + x + " y=" + y + "]";
367 }
368 }
369
370 /**
371 * An identity class with strict initialized instance fields
372 */
373 public static class IdentityStrictPoint implements Serializable {
374 static {
375 for (var f : IdentityStrictPoint.class.getDeclaredFields()) {
376 assertTrue(f.isStrictInit(), "missing strict init on field: " + f.getName());
377 }
378 }
379
380 @StrictInit
381 public int x;
382 @StrictInit
383 public int y;
384
385 public IdentityStrictPoint() {
386 x = 3;
387 y = 5;
388 super();
389 }
390
391 @Override
392 public String toString() {
393 return "[IdentityStrictPoint x=" + x + " y=" + y + "]";
394 }
395 }
396
397 /**
398 * A concrete value class that implements java.io.Serializable and doesn't have any
399 * jdk.internal.value.Deserializer on its constructor.
400 */
401 public static value class ValueWithNoDeserializer implements Serializable {
402 public int x;
403 public int y;
404
405 // Note: Must NOT have @Deserializer annotation
406 public ValueWithNoDeserializer(int x, int y) {
407 this.x = x;
408 this.y = y;
409 }
410
411 @Override
412 public String toString() {
413 return "[ValueWithNoDeserializer x=" + x + " y=" + y + "]";
414 }
415 }
416
417 /**
418 * A concrete value class which implements java.io.Externalizable and doesn't
419 * implement writeReplace().
420 */
421 static value class ExternalizableValue implements Externalizable {
422 public int x;
423 public int y;
424
425 public ExternalizableValue() {
426 this.x = 0;
427 this.y = 0;
428 }
429
430 ExternalizableValue(int x, int y) {
431 this.x = x;
432 this.y = y;
433 }
434
435 @Override
436 public void readExternal(ObjectInput in) {
437 // concrete value class isn't expected to be deserializable, so we don't
438 // expect this method to be invoked during deserialization.
439 throw new AssertionError("not expected to be invoked on " + this);
440 }
441
442 @Override
443 public void writeExternal(ObjectOutput out) {
444 // concrete value class isn't expected to be serializable, so we don't
445 // expect this method to be invoked during serialization.
446 throw new AssertionError("not expected to be invoked on " + this);
447 }
448
449 @Override
450 public String toString() {
451 return "[ExternalizableValue x=" + x + " y=" + y + "]";
452 }
453 }
454
455
456 /**
457 * A concrete value class which implements java.io.Serializable and has a
458 * jdk.internal.value.Deserializer associated with its constructor.
459 * It may be serialized only if it is on the boot class path.
460 */
461 static value class ValueWithDeserializer implements Serializable {
462 public int x;
463 public int y;
464
465 @Deserializer({"x", "y"})
466 private ValueWithDeserializer(int x, int y) {
467 this.x = x;
468 this.y = y;
469 }
470
471 @Override
472 public String toString() {
473 return "[ValueWithDeserializer x=" + x + " y=" + y + "]";
474 }
475 }
476
477 /**
478 * A concrete value class which implements java.io.Serializable
479 * and implements the writeReplace() method to return an identity
480 * object.
481 */
482 static value class ValueWriteReplaceWithIdentity implements Serializable {
483 public int x;
484
485 ValueWriteReplaceWithIdentity(int x) {
486 this.x = x;
487 }
488
489 @Serial
490 Object writeReplace() throws ObjectStreamException {
491 return new IdentityRecord(x);
492 }
493
494 @Serial
495 private void readObject(ObjectInputStream s) throws InvalidObjectException {
496 // the writeReplace() implementation of this class, when the serialization side
497 // is preparing to write this object to the stream, has replaced this object
498 // with an instance of a different class, so we don't expect deserialization
499 // to invoke this method.
500 throw new AssertionError("not expected to be invoked on " + this);
501 }
502
503 @Override
504 public String toString() {
505 return "[ValueWriteReplaceWithIdentity x=" + x + "]";
506 }
507
508 private record IdentityRecord(int x) implements Serializable {
509 @Serial
510 Object readResolve() throws ObjectStreamException {
511 return new ValueWriteReplaceWithIdentity(x);
512 }
513 }
514 }
515
516 /**
517 * A concrete value class which implements java.io.Externalizable and implements
518 * the writeReplace() method to return an identity object.
519 */
520 static value class ExtValueWithIdentityReplacement implements Externalizable {
521 public String s;
522
523 ExtValueWithIdentityReplacement(String s) {
524 this.s = s;
525 }
526
527 @Override
528 public boolean equals(Object other) {
529 return other instanceof ExtValueWithIdentityReplacement foo && s.equals(foo.s);
530 }
531
532 @Serial
533 Object writeReplace() throws ObjectStreamException {
534 return new IdentityRecord(s);
535 }
536
537 private record IdentityRecord(String s) implements Serializable {
538 @Serial
539 Object readResolve() throws ObjectStreamException {
540 return new ExtValueWithIdentityReplacement(s);
541 }
542 }
543
544 @Override
545 public void readExternal(ObjectInput in) {
546 // the writeReplace() implementation of this class, when the serialization side
547 // is preparing to write this object to the stream, has replaced this object
548 // with an instance of a different class, so we don't expect deserialization
549 // to invoke this method.
550 throw new AssertionError("not expected to be invoked on " + this);
551 }
552
553 @Override
554 public void writeExternal(ObjectOutput out) {
555 // the writeReplace() implementation of this class, when the serialization side
556 // is preparing to write this object to the stream, has replaced this object
557 // with an instance of a different class, so we don't expect this method to
558 // play any role during serialization.
559 throw new AssertionError("not expected to be invoked on " + this);
560 }
561
562 @Override
563 public String toString() {
564 return "[ExtValueWithIdentityReplacement s=" + s + "]";
565 }
566 }
567
568 /**
569 * A plain identity class that does not declare any strictly initialized
570 * instance field but its immediate superclasses, which implement Serializable,
571 * does.
572 */
573 public static class StrictInSuper extends IdentityStrictPoint {
574 // Declares no field, inherits strictly-initialized instance fields
575 // IdentityStrictPoint.x and y
576 public StrictInSuper() {
577 super();
578 }
579
580 @Override
581 public String toString() {
582 return "[StrictInSuper x=" + x + " y=" + y + "]";
583 }
584 }
585
586 /**
587 * A plain identity class that does not declare any strictly initialized
588 * instance field but one of its non-immediate superclasses that implement
589 * Serializable does.
590 */
591 public static class StrictInTwiceSuper extends StrictInSuper {
592 // Declares no field, inherits strictly-initialized instance fields
593 // IdentityStrictPoint.x and y
594 public StrictInTwiceSuper() {
595 super();
596 }
597
598 @Override
599 public String toString() {
600 return "[StrictInTwiceSuper x=" + x + " y=" + y + "]";
601 }
602 }
603
604 /**
605 * An abstract value class that declares instance fields. Such fields are
606 * always strictly initialized, making none of their subclasses serializable
607 * without jdk.internal.value.Deserializer.
608 */
609 public abstract static value class HasFieldAbstractValue implements Serializable {
610 public int x;
611 public int y;
612
613 public HasFieldAbstractValue(int x, int y) {
614 this.x = x;
615 this.y = y;
616 }
617
618 @Override
619 public String toString() {
620 return "[HasFieldAbstractValue x=" + x + " y=" + y + "]";
621 }
622 }
623
624 /**
625 * An identity class that inherits strictly-initialized instance fields from
626 * its abstract value superclass that is serializable. Therefore, this class
627 * is not serializable without jdk.internal.value.Deserializer.
628 */
629 public static class StrictInAbstractValueSuper extends HasFieldAbstractValue {
630 public StrictInAbstractValueSuper() {
631 super(42, -3);
632 }
633
634 @Override
635 public String toString() {
636 return "[StrictInAbstractValueSuper x=" + x + " y=" + y + "]";
637 }
638 }
639
640 /**
641 * An identity class that does not inherit any strictly-initialized instance
642 * field from its abstract value superclass that is serializable, in this
643 * case the migrated java.lang.Number. This class is accepted by default
644 * serialization.
645 */
646 public static class CustomNumberWithIdentity extends Number {
647 public int x;
648 public int y;
649
650 public CustomNumberWithIdentity(int x, int y) {
651 this.x = x;
652 this.y = y;
653 }
654
655 @Override
656 public int intValue() {
657 return (int) longValue();
658 }
659
660 @Override
661 public long longValue() {
662 return (long) x << 32 | y;
663 }
664
665 @Override
666 public float floatValue() {
667 return longValue();
668 }
669
670 @Override
671 public double doubleValue() {
672 return longValue();
673 }
674
675 @Override
676 public final boolean equals(Object o) {
677 if (!(o instanceof CustomNumberWithIdentity that))
678 return false;
679
680 return x == that.x && y == that.y;
681 }
682
683 @Override
684 public int hashCode() {
685 int result = x;
686 result = 31 * result + y;
687 return result;
688 }
689
690 @Override
691 public String toString() {
692 return "[CustomNumberWithIdentity x=" + x + " y=" + y + "]";
693 }
694 }
695 }