8275338: Add JFR events for notable serialization situations
Reviewed-by: rriggs, egahlin
This commit is contained in:
parent
4c1a0fc58f
commit
bfd2afe5ad
@ -55,6 +55,8 @@ import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import jdk.internal.event.SerializationMisdeclarationEvent;
|
||||
import jdk.internal.misc.Unsafe;
|
||||
import jdk.internal.reflect.CallerSensitive;
|
||||
import jdk.internal.reflect.Reflection;
|
||||
@ -459,6 +461,10 @@ public final class ObjectStreamClass implements Serializable {
|
||||
}
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
if (SerializationMisdeclarationEvent.enabled() && serializable) {
|
||||
SerializationMisdeclarationChecker.checkMisdeclarations(cl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright (c) 2023, 2024, 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. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package java.io;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static jdk.internal.event.SerializationMisdeclarationEvent.*;
|
||||
import static java.lang.reflect.Modifier.*;
|
||||
|
||||
final class SerializationMisdeclarationChecker {
|
||||
|
||||
private static final String SUID_NAME = "serialVersionUID";
|
||||
private static final String SERIAL_PERSISTENT_FIELDS_NAME = "serialPersistentFields";
|
||||
private static final String WRITE_OBJECT_NAME = "writeObject";
|
||||
private static final String READ_OBJECT_NAME = "readObject";
|
||||
private static final String READ_OBJECT_NO_DATA_NAME = "readObjectNoData";
|
||||
private static final String WRITE_REPLACE_NAME = "writeReplace";
|
||||
private static final String READ_RESOLVE_NAME = "readResolve";
|
||||
|
||||
private static final Class<?>[] WRITE_OBJECT_PARAM_TYPES = {ObjectOutputStream.class};
|
||||
private static final Class<?>[] READ_OBJECT_PARAM_TYPES = {ObjectInputStream.class};
|
||||
|
||||
/*
|
||||
* The sharing of a single Class<?>[] instance here is just to avoid wasting
|
||||
* space, and should not be considered as a conceptual sharing of types.
|
||||
*/
|
||||
private static final Class<?>[] READ_OBJECT_NO_DATA_PARAM_TYPES = {};
|
||||
private static final Class<?>[] WRITE_REPLACE_PARAM_TYPES = READ_OBJECT_NO_DATA_PARAM_TYPES;
|
||||
private static final Class<?>[] READ_RESOLVE_PARAM_TYPES = READ_OBJECT_NO_DATA_PARAM_TYPES;
|
||||
|
||||
static void checkMisdeclarations(Class<?> cl) {
|
||||
checkSerialVersionUID(cl);
|
||||
checkSerialPersistentFields(cl);
|
||||
|
||||
checkPrivateMethod(cl, WRITE_OBJECT_NAME,
|
||||
WRITE_OBJECT_PARAM_TYPES, Void.TYPE);
|
||||
checkPrivateMethod(cl, READ_OBJECT_NAME,
|
||||
READ_OBJECT_PARAM_TYPES, Void.TYPE);
|
||||
checkPrivateMethod(cl, READ_OBJECT_NO_DATA_NAME,
|
||||
READ_OBJECT_NO_DATA_PARAM_TYPES, Void.TYPE);
|
||||
|
||||
checkAccessibleMethod(cl, WRITE_REPLACE_NAME,
|
||||
WRITE_REPLACE_PARAM_TYPES, Object.class);
|
||||
checkAccessibleMethod(cl, READ_RESOLVE_NAME,
|
||||
READ_RESOLVE_PARAM_TYPES, Object.class);
|
||||
}
|
||||
|
||||
private static void checkSerialVersionUID(Class<?> cl) {
|
||||
Field f = privilegedDeclaredField(cl, SUID_NAME);
|
||||
if (f == null) {
|
||||
if (isOrdinaryClass(cl)) {
|
||||
commitEvent(cl, SUID_NAME + " should be declared explicitly" +
|
||||
" as a private static final long field");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cl.isEnum()) {
|
||||
commitEvent(cl, SUID_NAME + " should not be declared in an enum class");
|
||||
}
|
||||
if (!isPrivate(f)) {
|
||||
commitEvent(cl, SUID_NAME + " should be private");
|
||||
}
|
||||
if (!isStatic(f)) {
|
||||
commitEvent(cl, SUID_NAME + " must be static");
|
||||
}
|
||||
if (!isFinal(f)) {
|
||||
commitEvent(cl, SUID_NAME + " must be final");
|
||||
}
|
||||
if (f.getType() != Long.TYPE) {
|
||||
commitEvent(cl, SUID_NAME + " must be of type long");
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkSerialPersistentFields(Class<?> cl) {
|
||||
Field f = privilegedDeclaredField(cl, SERIAL_PERSISTENT_FIELDS_NAME);
|
||||
if (f == null) {
|
||||
return;
|
||||
}
|
||||
if (cl.isRecord()) {
|
||||
commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME +
|
||||
" should not be declared in a record class");
|
||||
} else if (cl.isEnum()) {
|
||||
commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME +
|
||||
" should not be declared in an enum class");
|
||||
}
|
||||
if (!isPrivate(f)) {
|
||||
commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be private");
|
||||
}
|
||||
if (!isStatic(f)) {
|
||||
commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be static");
|
||||
}
|
||||
if (!isFinal(f)) {
|
||||
commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be final");
|
||||
}
|
||||
if (f.getType() != ObjectStreamField[].class) {
|
||||
commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME +
|
||||
" should be of type ObjectStreamField[]");
|
||||
}
|
||||
if (!isStatic(f)) {
|
||||
return;
|
||||
}
|
||||
f.setAccessible(true);
|
||||
Object spf = objectFromStatic(f);
|
||||
if (spf == null) {
|
||||
commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " should be non-null");
|
||||
return;
|
||||
}
|
||||
if (!(spf instanceof ObjectStreamField[])) {
|
||||
commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME +
|
||||
" must be an instance of ObjectStreamField[]");
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkPrivateMethod(Class<?> cl,
|
||||
String name, Class<?>[] paramTypes, Class<?> retType) {
|
||||
for (Method m : privilegedDeclaredMethods(cl)) {
|
||||
if (m.getName().equals(name)) {
|
||||
checkPrivateMethod(cl, m, paramTypes, retType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkPrivateMethod(Class<?> cl,
|
||||
Method m, Class<?>[] paramTypes, Class<?> retType) {
|
||||
if (cl.isEnum()) {
|
||||
commitEvent(cl, "method " + m + " should not be declared in an enum class");
|
||||
} else if (cl.isRecord()) {
|
||||
commitEvent(cl, "method " + m + " should not be declared in a record class");
|
||||
}
|
||||
if (!isPrivate(m)) {
|
||||
commitEvent(cl, "method " + m + " must be private");
|
||||
}
|
||||
if (isStatic(m)) {
|
||||
commitEvent(cl, "method " + m + " must be non-static");
|
||||
}
|
||||
if (m.getReturnType() != retType) {
|
||||
commitEvent(cl, "method " + m + " must have return type " + retType);
|
||||
}
|
||||
if (!Arrays.equals(m.getParameterTypes(), paramTypes)) {
|
||||
commitEvent(cl, "method " + m + " must have parameter types " + Arrays.toString(paramTypes));
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkAccessibleMethod(Class<?> cl,
|
||||
String name, Class<?>[] paramTypes, Class<?> retType) {
|
||||
for (Class<?> superCl = cl; superCl != null; superCl = superCl.getSuperclass()) {
|
||||
for (Method m : privilegedDeclaredMethods(superCl)) {
|
||||
if (m.getName().equals(name)) {
|
||||
checkAccessibleMethod(cl, superCl, m, paramTypes, retType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkAccessibleMethod(Class<?> cl,
|
||||
Class<?> superCl, Method m, Class<?>[] paramTypes, Class<?> retType) {
|
||||
if (superCl.isEnum()) {
|
||||
commitEvent(cl, "method " + m + " should not be declared in an enum class");
|
||||
}
|
||||
if (isAbstract(m)) {
|
||||
commitEvent(cl, "method " + m + " must be non-abstract");
|
||||
}
|
||||
if (isStatic(m)) {
|
||||
commitEvent(cl, "method " + m + " must be non-static");
|
||||
}
|
||||
if (m.getReturnType() != retType) {
|
||||
commitEvent(cl, "method " + m + " must have return type " + retType);
|
||||
}
|
||||
if (!Arrays.equals(m.getParameterTypes(), paramTypes)) {
|
||||
commitEvent(cl, "method " + m + " must have parameter types " + Arrays.toString(paramTypes));
|
||||
}
|
||||
if (isPrivate(m) && cl != superCl
|
||||
|| isPackageProtected(m) && !isSamePackage(cl, superCl)) {
|
||||
commitEvent(cl, "method " + m + " is not accessible");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSamePackage(Class<?> cl0, Class<?> cl1) {
|
||||
return cl0.getClassLoader() == cl1.getClassLoader()
|
||||
&& cl0.getPackageName().equals(cl1.getPackageName());
|
||||
}
|
||||
|
||||
private static boolean isOrdinaryClass(Class<?> cl) {
|
||||
/* class Enum and class Record are not considered ordinary classes */
|
||||
return !(cl.isRecord() || cl.isEnum() || cl.isArray()
|
||||
|| Enum.class == cl || Record.class == cl
|
||||
|| Proxy.isProxyClass(cl));
|
||||
}
|
||||
|
||||
private static boolean isPrivate(Member m) {
|
||||
return (m.getModifiers() & PRIVATE) != 0;
|
||||
}
|
||||
|
||||
private static boolean isPackageProtected(Member m) {
|
||||
return (m.getModifiers() & (PRIVATE | PROTECTED | PUBLIC)) == 0;
|
||||
}
|
||||
|
||||
private static boolean isAbstract(Member m) {
|
||||
return (m.getModifiers() & ABSTRACT) != 0;
|
||||
}
|
||||
|
||||
private static boolean isFinal(Member m) {
|
||||
return (m.getModifiers() & FINAL) != 0;
|
||||
}
|
||||
|
||||
private static boolean isStatic(Member m) {
|
||||
return (m.getModifiers() & STATIC) != 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private static Field privilegedDeclaredField(Class<?> cl, String name) {
|
||||
if (System.getSecurityManager() == null) {
|
||||
return declaredField(cl, name);
|
||||
}
|
||||
return AccessController.doPrivileged((PrivilegedAction<Field>) () ->
|
||||
declaredField(cl, name));
|
||||
}
|
||||
|
||||
private static Field declaredField(Class<?> cl, String name) {
|
||||
try {
|
||||
return cl.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private static Method[] privilegedDeclaredMethods(Class<?> cl) {
|
||||
if (System.getSecurityManager() == null) {
|
||||
return cl.getDeclaredMethods();
|
||||
}
|
||||
return AccessController.doPrivileged(
|
||||
(PrivilegedAction<Method[]>) cl::getDeclaredMethods);
|
||||
}
|
||||
|
||||
private static Object objectFromStatic(Field f) {
|
||||
try {
|
||||
return f.get(null);
|
||||
} catch (IllegalAccessException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void commitEvent(Class<?> cl, String msg) {
|
||||
commit(timestamp(), cl, msg);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2023, 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. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package jdk.internal.event;
|
||||
|
||||
/**
|
||||
* A JFR event for serialization misdeclarations.
|
||||
* This event is mirrored in {@code jdk.jfr.events.SerializationMisdeclarationEvent}
|
||||
* where the metadata for the event is provided with annotations.
|
||||
* Some of the methods are replaced by generated methods when jfr is enabled.
|
||||
* Note that the order of the arguments of the {@link #commit(long,Class,String)}
|
||||
* method must be the same as the order of the fields.
|
||||
*/
|
||||
public class SerializationMisdeclarationEvent extends Event {
|
||||
|
||||
public Class<?> misdeclaredClass;
|
||||
public String message;
|
||||
|
||||
/**
|
||||
* Commit a serialization misdeclaration event.
|
||||
* The implementation of this method is generated automatically if jfr is enabled.
|
||||
* The order of the fields must be the same as the parameters in this method.
|
||||
* {@code commit(long,Class,String)}
|
||||
*
|
||||
* @param start timestamp of the start of the operation
|
||||
* @param misdeclaredClass the affected class
|
||||
* @param message the specific event message
|
||||
*/
|
||||
public static void commit(long start, Class<?> misdeclaredClass, String message) {
|
||||
// Generated by JFR
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an event should be emitted. The duration of the operation
|
||||
* must exceed some threshold in order to commit the event. The implementation
|
||||
* of this method is generated automatically if jfr is enabled.
|
||||
*
|
||||
* @param duration time in nanoseconds to complete the operation
|
||||
* @return true if the event should be commited
|
||||
*/
|
||||
public static boolean shouldCommit(long duration) {
|
||||
// Generated by JFR
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this kind of event is enabled. The implementation
|
||||
* of this method is generated automatically if jfr is enabled.
|
||||
*
|
||||
* @return whether serialization misdeclaration events are enabled
|
||||
*/
|
||||
public static boolean enabled() {
|
||||
// Generated by JFR
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current timestamp in nanoseconds. This method is used
|
||||
* to determine the start and end of an operation. The implementation
|
||||
* of this method is generated automatically if jfr is enabled.
|
||||
*
|
||||
* @return the current timestamp value
|
||||
*/
|
||||
public static long timestamp() {
|
||||
// Generated by JFR
|
||||
return 0L;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2023, 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. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package jdk.jfr.events;
|
||||
|
||||
import jdk.jfr.Category;
|
||||
import jdk.jfr.Description;
|
||||
import jdk.jfr.Label;
|
||||
import jdk.jfr.Name;
|
||||
import jdk.jfr.internal.MirrorEvent;
|
||||
import jdk.jfr.internal.RemoveFields;
|
||||
import jdk.jfr.internal.Type;
|
||||
|
||||
@Name(Type.EVENT_NAME_PREFIX + "SerializationMisdeclaration")
|
||||
@Label("Serialization Misdeclaration")
|
||||
@Category({"Java Development Kit", "Serialization"})
|
||||
@Description("Methods and fields misdeclarations." +
|
||||
" The checks are usually performed just once per serializable class," +
|
||||
" the first time it is used by serialization." +
|
||||
" Under high memory pressure, a class might be re-checked again.")
|
||||
@MirrorEvent(className = "jdk.internal.event.SerializationMisdeclarationEvent")
|
||||
@RemoveFields({"duration", "stackTrace", "eventThread"})
|
||||
public final class SerializationMisdeclarationEvent extends AbstractJDKEvent {
|
||||
|
||||
@Label("Misdeclared Class")
|
||||
public Class<?> misdeclaredClass;
|
||||
|
||||
@Label("Message")
|
||||
public String message;
|
||||
|
||||
}
|
@ -35,6 +35,7 @@ import jdk.jfr.events.ExceptionThrownEvent;
|
||||
import jdk.jfr.events.ProcessStartEvent;
|
||||
import jdk.jfr.events.SecurityPropertyModificationEvent;
|
||||
import jdk.jfr.events.SecurityProviderServiceEvent;
|
||||
import jdk.jfr.events.SerializationMisdeclarationEvent;
|
||||
import jdk.jfr.events.SocketReadEvent;
|
||||
import jdk.jfr.events.SocketWriteEvent;
|
||||
import jdk.jfr.events.TLSHandshakeEvent;
|
||||
@ -52,6 +53,7 @@ public final class MirrorEvents {
|
||||
ProcessStartEvent.class,
|
||||
SecurityPropertyModificationEvent.class,
|
||||
SecurityProviderServiceEvent.class,
|
||||
SerializationMisdeclarationEvent.class,
|
||||
SocketReadEvent.class,
|
||||
SocketWriteEvent.class,
|
||||
ThreadSleepEvent.class,
|
||||
|
@ -75,6 +75,7 @@ public final class JDKEvents {
|
||||
jdk.internal.event.ProcessStartEvent.class,
|
||||
jdk.internal.event.SecurityPropertyModificationEvent.class,
|
||||
jdk.internal.event.SecurityProviderServiceEvent.class,
|
||||
jdk.internal.event.SerializationMisdeclarationEvent.class,
|
||||
jdk.internal.event.SocketReadEvent.class,
|
||||
jdk.internal.event.SocketWriteEvent.class,
|
||||
jdk.internal.event.ThreadSleepEvent.class,
|
||||
|
@ -756,6 +756,10 @@
|
||||
<setting name="stackTrace">true</setting>
|
||||
</event>
|
||||
|
||||
<event name="jdk.SerializationMisdeclaration">
|
||||
<setting name="enabled">false</setting>
|
||||
</event>
|
||||
|
||||
<event name="jdk.InitialSecurityProperty">
|
||||
<setting name="enabled">true</setting>
|
||||
<setting name="period">beginChunk</setting>
|
||||
|
@ -756,6 +756,10 @@
|
||||
<setting name="stackTrace">true</setting>
|
||||
</event>
|
||||
|
||||
<event name="jdk.SerializationMisdeclaration">
|
||||
<setting name="enabled">true</setting>
|
||||
</event>
|
||||
|
||||
<event name="jdk.InitialSecurityProperty">
|
||||
<setting name="enabled">true</setting>
|
||||
<setting name="period">beginChunk</setting>
|
||||
|
@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Copyright (c) 2023, 2024 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.
|
||||
*/
|
||||
|
||||
package jdk.jfr.event.io;
|
||||
|
||||
import jdk.jfr.consumer.RecordedEvent;
|
||||
import jdk.jfr.consumer.RecordingStream;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.ObjectStreamClass;
|
||||
import java.io.ObjectStreamField;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static jdk.test.lib.jfr.EventNames.SerializationMisdeclaration;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8275338
|
||||
* @summary Check generation of JFR events for misdeclared fields and methods
|
||||
* relevant to serialization
|
||||
* @key jfr
|
||||
* @requires vm.hasJFR
|
||||
* @library /test/lib
|
||||
* @run junit/othervm jdk.jfr.event.io.TestSerializationMisdeclarationEvent
|
||||
*/
|
||||
public class TestSerializationMisdeclarationEvent {
|
||||
|
||||
private static final List<RecordedEvent> events = new ArrayList<>();
|
||||
|
||||
@BeforeAll
|
||||
static void recordEvents() {
|
||||
try (var rs = new RecordingStream()) {
|
||||
rs.enable(SerializationMisdeclaration);
|
||||
rs.onEvent(SerializationMisdeclaration, events::add);
|
||||
rs.startAsync();
|
||||
doLookups();
|
||||
rs.stop();
|
||||
}
|
||||
}
|
||||
|
||||
static Arguments[] testSingleClassMisdeclarations() {
|
||||
return new Arguments[] {
|
||||
arguments(NoSUID.class, new String[] {"serialVersionUID", "should", "explicitly"}),
|
||||
arguments(NoSUID.class, new String[] {"serialPersistentFields", "should", "non-null"}),
|
||||
|
||||
arguments(BadClass.class, new String[] {"serialVersionUID", "should", "private"}),
|
||||
arguments(BadClass.class, new String[] {"serialVersionUID", "must", "type", "long"}),
|
||||
arguments(BadClass.class, new String[] {"serialVersionUID", "must", "final"}),
|
||||
arguments(BadClass.class, new String[] {"serialVersionUID", "must", "static"}),
|
||||
arguments(BadClass.class, new String[] {"serialPersistentFields", "must", "private"}),
|
||||
arguments(BadClass.class, new String[] {"serialPersistentFields", "must", "static"}),
|
||||
arguments(BadClass.class, new String[] {"serialPersistentFields", "must", "final"}),
|
||||
arguments(BadClass.class, new String[] {"serialPersistentFields", "should", "type", "ObjectStreamField[]"}),
|
||||
arguments(BadClass.class, new String[] {"method", "writeObject", "must", "private"}),
|
||||
arguments(BadClass.class, new String[] {"method", "writeObject", "must", "non-static"}),
|
||||
arguments(BadClass.class, new String[] {"method", "writeObject", "must", "return"}),
|
||||
arguments(BadClass.class, new String[] {"method", "writeObject", "must", "parameter"}),
|
||||
arguments(BadClass.class, new String[] {"method", "readObject(", "must", "parameter"}),
|
||||
arguments(BadClass.class, new String[] {"method", "readObjectNoData", "must", "parameter"}),
|
||||
|
||||
arguments(EnumClass.class, new String[] {"serialVersionUID", "enum"}),
|
||||
arguments(EnumClass.class, new String[] {"serialPersistentFields", "enum"}),
|
||||
arguments(EnumClass.class, new String[] {"method", "writeObject", "enum"}),
|
||||
arguments(EnumClass.class, new String[] {"method", "readResolve", "enum"}),
|
||||
|
||||
arguments(RecordClass.class, new String[] {"serialPersistentFields", "record"}),
|
||||
arguments(RecordClass.class, new String[] {"method", "record"}),
|
||||
|
||||
arguments(C.class, new String[] {"method", "not", "accessible"}),
|
||||
|
||||
arguments(Acc.class, new String[] {"serialPersistentFields", "should", "type", "ObjectStreamField[]"}),
|
||||
arguments(Acc.class, new String[] {"serialPersistentFields", "must", "instance", "ObjectStreamField[]"}),
|
||||
arguments(Acc.class, new String[] {"method", "readResolve", "must", "non-abstract"}),
|
||||
arguments(Acc.class, new String[] {"method", "writeReplace", "must", "non-static"}),
|
||||
arguments(Acc.class, new String[] {"method", "writeReplace", "must", "return"}),
|
||||
arguments(Acc.class, new String[] {"method", "writeReplace", "must", "parameter"}),
|
||||
};
|
||||
}
|
||||
|
||||
static Arguments[] testGoodClass() {
|
||||
return new Arguments[] {
|
||||
arguments(A.class),
|
||||
arguments(B.class),
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
public void testSingleClassMisdeclarations(Class<?> cls, String[] keywords) {
|
||||
singleClassEvent(cls, keywords);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
public void testGoodClass(Class<?> cls) {
|
||||
assertEquals(0, getEventsFor(cls).size(), cls.getName());
|
||||
}
|
||||
|
||||
private static void doLookups() {
|
||||
ObjectStreamClass.lookup(NoSUID.class);
|
||||
ObjectStreamClass.lookup(BadClass.class);
|
||||
ObjectStreamClass.lookup(EnumClass.class);
|
||||
ObjectStreamClass.lookup(RecordClass.class);
|
||||
ObjectStreamClass.lookup(Acc.class);
|
||||
|
||||
ObjectStreamClass.lookup(A.class);
|
||||
ObjectStreamClass.lookup(B.class);
|
||||
ObjectStreamClass.lookup(C.class);
|
||||
}
|
||||
|
||||
private static void singleClassEvent(Class<?> cls, String[] keywords) {
|
||||
assertEquals(1, getEventsFor(cls, keywords).size(), cls.getName());
|
||||
}
|
||||
|
||||
private static List<RecordedEvent> getEventsFor(Class<?> cls, String[] keywords) {
|
||||
return events.stream()
|
||||
.filter(e -> e.getClass("misdeclaredClass").getName().equals(cls.getName())
|
||||
&& matchesAllKeywords(e.getString("message"), keywords))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static boolean matchesAllKeywords(String msg, String[] keywords) {
|
||||
return Arrays.stream(keywords).allMatch(msg::contains);
|
||||
}
|
||||
|
||||
private static List<RecordedEvent> getEventsFor(Class<?> cls) {
|
||||
return events.stream()
|
||||
.filter(e -> e.getClass("misdeclaredClass").getName().equals(cls.getName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static class A implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 0xAAAAL;
|
||||
|
||||
@Serial
|
||||
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
|
||||
|
||||
@Serial
|
||||
private void writeObject(ObjectOutputStream oos) {
|
||||
}
|
||||
|
||||
@Serial
|
||||
private void readObject(ObjectInputStream ois) {
|
||||
}
|
||||
|
||||
@Serial
|
||||
private void readObjectNoData() {
|
||||
}
|
||||
|
||||
@Serial
|
||||
Object writeReplace() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class B extends A {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 0xBBBBL;
|
||||
|
||||
@Serial
|
||||
private Object readResolve() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class C extends B {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 0xCCCCL;
|
||||
|
||||
/*
|
||||
* readResolve() in superclass is not accessible
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
private static final class NoSUID implements Serializable {
|
||||
|
||||
/*
|
||||
* should declare serialVersionUID
|
||||
*/
|
||||
|
||||
/*
|
||||
* value should be non-null
|
||||
*/
|
||||
private static final ObjectStreamField[] serialPersistentFields = null;
|
||||
|
||||
}
|
||||
|
||||
private static final class BadClass implements Serializable {
|
||||
/*
|
||||
* should be private
|
||||
* must be long
|
||||
* must be final
|
||||
*/
|
||||
Object serialVersionUID = 1.2;
|
||||
|
||||
/*
|
||||
* must be private
|
||||
* must be static
|
||||
* must be final
|
||||
* should be ObjectStreamField[]
|
||||
*/
|
||||
Object serialPersistentFields = new String[0];
|
||||
|
||||
/*
|
||||
* must be private
|
||||
* must be non-static
|
||||
* must return void
|
||||
* must accept ObjectOutputStream
|
||||
*/
|
||||
static int writeObject(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* must accept ObjectInputStream
|
||||
*/
|
||||
private void readObject(ObjectOutputStream oos) {
|
||||
}
|
||||
|
||||
/*
|
||||
* must not accept parameters
|
||||
*/
|
||||
private void readObjectNoData(ObjectInputStream ois) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private enum EnumClass implements Serializable {
|
||||
__; // ignored constant
|
||||
|
||||
/*
|
||||
* non-effective on enum
|
||||
*/
|
||||
private static final long serialVersionUID = 0xABCDL;
|
||||
|
||||
/*
|
||||
* non-effective on enum
|
||||
*/
|
||||
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
|
||||
|
||||
/*
|
||||
* non-effective on enum
|
||||
*/
|
||||
private void writeObject(ObjectOutputStream oos) {
|
||||
}
|
||||
|
||||
/*
|
||||
* non-effective on enum
|
||||
*/
|
||||
public Object readResolve() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record RecordClass() implements Serializable {
|
||||
|
||||
/*
|
||||
* allowed on records
|
||||
*/
|
||||
private static final long serialVersionUID = 0x1234L;
|
||||
|
||||
/*
|
||||
* non-effective on records
|
||||
*/
|
||||
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
|
||||
|
||||
/*
|
||||
* non-effective on records
|
||||
*/
|
||||
static int writeObject(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private abstract static class Acc implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 0x5678L;
|
||||
|
||||
private static final Object serialPersistentFields = new String[0];
|
||||
/*
|
||||
* must be non-abstract
|
||||
*/
|
||||
abstract Object readResolve();
|
||||
|
||||
/*
|
||||
* must be non-static
|
||||
*/
|
||||
static Object writeReplace() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* must return Object
|
||||
* must have empty parameter types
|
||||
*/
|
||||
String writeReplace(String s) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -209,6 +209,7 @@ public class EventNames {
|
||||
public static final String SecurityProviderService = PREFIX + "SecurityProviderService";
|
||||
public static final String DirectBufferStatistics = PREFIX + "DirectBufferStatistics";
|
||||
public static final String Deserialization = PREFIX + "Deserialization";
|
||||
public static final String SerializationMisdeclaration = PREFIX + "SerializationMisdeclaration";
|
||||
public static final String VirtualThreadStart = PREFIX + "VirtualThreadStart";
|
||||
public static final String VirtualThreadEnd = PREFIX + "VirtualThreadEnd";
|
||||
public static final String VirtualThreadPinned = PREFIX + "VirtualThreadPinned";
|
||||
|
Loading…
Reference in New Issue
Block a user