I've attempted to write a unit test (in Groovy using Spock) which can check that a given interface for use with RMI is in fact fully serializable - all the parameters, exceptions, and possible implementations of types defined in the methods.
It seems to work for me so far, however, this is a bit fiddly to do and there may be cases this doesn't cover, so use at your own risk!
You will need to replace the example interfaces Notification
etc. with your own. The example includes one unserializable field as an illustration.
package example
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import spock.lang.Specification
import java.lang.reflect.*
import java.rmi.Remote
import java.rmi.RemoteException
/** This checks that the a remoting API NotifierServer is safe
*
* It attempts to flush out any parameter classes which are
* not Serializable. This isn't checked at compile time!
*
*/
@CompileStatic
class RemotableInterfaceTest extends Specification {
static class NotificationException extends RuntimeException {
Object unserializable
}
static interface Notification {
String getMessage()
Date getDate()
}
static interface Notifier extends Remote {
void accept(Notification notification) throws RemoteException, NotificationException
}
static interface NotifierServer extends Remote {
void subscribe(Notification notifier) throws RemoteException
void notify(Notification message) throws RemoteException
}
// From https://www.javaworld.com/article/2077477/learn-java/java-tip-113--identify-subclasses-at-runtime.html
/**
* Scans all classes accessible from the context class loader which belong to the given package and subpackages.
*
* @param packageName The base package
* @return The classes
* @throws ClassNotFoundException
* @throws IOException
*/
static Class[] getClasses(String packageName)
throws ClassNotFoundException, IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader()
assert classLoader != null
String path = packageName.replace('.', '/')
Enumeration resources = classLoader.getResources(path)
List<File> dirs = new ArrayList()
while (resources.hasMoreElements()) {
URL resource = resources.nextElement()
dirs.add(new File(resource.getFile()))
}
ArrayList classes = new ArrayList()
for (File directory : dirs) {
classes.addAll(findClasses(directory, packageName))
}
return classes.toArray(new Class[classes.size()])
}
/**
* Recursive method used to find all classes in a given directory and subdirs.
*
* @param directory The base directory
* @param packageName The package name for classes found inside the base directory
* @return The classes
* @throws ClassNotFoundException
*/
static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
List<Class> classes = new ArrayList()
if (!directory.exists()) {
return classes
}
File[] files = directory.listFiles()
for (File file : files) {
if (file.isDirectory()) {
//assert !file.getName().contains(".");
classes.addAll(findClasses(file, packageName + "." + file.getName()))
} else if (file.getName().endsWith(".class")) {
classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)))
}
}
return classes
}
/** Finds all known subclasses of a class */
@CompileDynamic
static List<Class> getSubclasses(Class type) {
allClasses
.findAll { Class it ->
!Modifier.isAbstract(it.modifiers) &&
it != type &&
type.isAssignableFrom(it)
}
}
/** Checks if a type is nominally serializable or remotable.
*
* Notes:
* <ul>
* <li> primitives are implicitly serializable
* <li> interfaces are serializable or remotable by themselves, but we
* assume that since #getSerializedTypes checks derived types of interfaces,
* we can safely assume that all implementations will be checked
*</ul>
*
* @param it
* @return
*/
static boolean isSerializableOrRemotable(Class<?> it) {
return it.primitive || it.interface || Serializable.isAssignableFrom(it) || Remote.isAssignableFrom(it)
}
/** Recursively finds all (new) types associated with a given type
* which need to be serialized because they are fields, parameterized
* types, implementations, etc. */
static void getSerializedTypes(final Set<Class<?>> types, Type... it) {
for(Type type in it) {
println "type: $type.typeName"
if (type instanceof GenericArrayType) {
type = ((GenericArrayType)type).genericComponentType
}
if (type instanceof ParameterizedType) {
ParameterizedType ptype = (ParameterizedType)type
getSerializedTypes(types, ptype.actualTypeArguments)
break
}
if (type instanceof Class) {
Class ctype = (Class)type
if (ctype == Object)
break
if (types.contains(type))
break
types << ctype
for (Field field : ctype.declaredFields) {
println "${ctype.simpleName}.${field.name}: ${field.type.simpleName}"
if (Modifier.isVolatile(field.modifiers) ||
Modifier.isTransient(field.modifiers) ||
Modifier.isStatic(field.modifiers))
continue
Class<?> fieldType = field.type
if (fieldType.array)
fieldType = fieldType.componentType
if (types.contains(fieldType))
continue
types << fieldType
if (!fieldType.primitive)
getSerializedTypes(types, fieldType)
}
if (ctype.genericSuperclass) {
getSerializedTypes(types, ctype.genericSuperclass)
}
getSubclasses(ctype).each { Class c -> getSerializedTypes(types, c) }
break
}
}
}
/** Recursively checks a type's methods for related classes which
* need to be serializable if the type is remoted */
static Set<Class<?>> getMethodTypes(Class<?> it) {
Set<Class<?>> types = []
for(Method method: it.methods) {
println "method: ${it.simpleName}.$method.name"
getSerializedTypes(types, method.genericParameterTypes)
getSerializedTypes(types, method.genericReturnType)
getSerializedTypes(types, method.genericExceptionTypes)
}
return types
}
/** All the known defined classes */
static List<Class> allClasses = Package.packages.collectMany { Package p -> getClasses(p.name) as Collection<Class> }
@CompileDynamic
def "NotifierServer interface should only expose serializable or remotable types"() {
given:
Set<Class> types = getMethodTypes(NotifierServer)
Set<Class> nonSerializableTypes = types.findAll { !isSerializableOrRemotable(it) }
expect:
nonSerializableTypes.empty
}
}