2

I need to create a map of our domain classes simple names to their fully canonical names. I want to do this only for classes that are under our package structure, and that implement Serializable.

In serialization we use the canonical names of classes alot --it's a good default behaviour as its a very conservative approach, but our model objects are going to move around between packages, and I don't want that to represent a breaking change requiring migration scripts, so I'd like this map. I've already tooled our serializer to use this map, now I just need a good strategy for populating it. Its been frustrating.

First alternative: have each class announce itself statically

the most obvious and most annoying: edit each class in question to include the code

static{
    Bootstrapper.classAliases.put(
        ThisClass.class.getSimpleName(), 
        ThisClass.class.getCanonicalName()
    );
}

I knew I could do this from the get-go, I started on it, and I really hate it. There's no way this is going to be maintained properly, new classes will be introduced, somebody will forget to add this line, and I'll get myself in trouble.

Second alternative: read through the jar

traverse the jar our application is in, load each class, and see if it should be added to this map. This solution smelled pretty bad -- I'm disturbing the normal loading order and I'm coupled tightly to a particular deployment scheme. Gave up on this fairly quickly.

Third alternative: use java.lang.Instrumentation

requires me to run java with a java agent. More specifics about deployment.

Fourth alternative: hijack class loaders

My first idea was to see if I could add a listener to the class loaders, and then listen for my desired classes being loaded, adding them to this map as they're loaded into the JVM. strictly speaking this isn't doing this statically, but its close enough.

After discovering the tree-like nature of class loaders, and the various different schemes used by the different threads and different libraries, I thought that implementing this solution would be both too complicated and lead to bugs.

Fifth alternative: leverage the build system & a properties file

This one seems like one of the better solutions but I don't have the ant skill to do it. My plan would be to search each file for the pattern

//using human readable regex
[whitespace]* package [whitespace]* com.mycompany [char]*;
[char not 'class']* 
class [whitespace]+ (<capture:"className">[nameCharacter]+) [char not '{']* implements [char not '{'] Serializable [char not '{'] '{'
//using notepad++'s regex
\s*package\s+([A-Za-z\._]*);.*class\s+(\w+)\s+implements\s+[\w,_<>\s]*Serializable

and then write out each matching entry in the form [pathFound][className]=[className] to a properties file.

Then I add some fairly simple code to load this properties file into a map at runtime.

am I missing something obvious? Why is this so difficult to do? I know that the lazy nature of java classes means that the language is antithetical to code asking the question "what classes are there", and I guess my problem is a derivative of this question, but still, I'm surprised at how much I'm having to scratch my brain to do this.

So I suppose my question is 2 fold:

  • how would you go about making this map?
  • If it would be with your build system, what is the ant code needed to do it? Is this worth converting to gradle for?

Thanks for any help

Community
  • 1
  • 1
Groostav
  • 3,170
  • 1
  • 23
  • 27

1 Answers1

0

I would start with your fifth alternative. So, there is a byte code manipulation project called - javassist which lets you load .class files and deal with them using java objects. For example, you can load a "Foo.class" and start asking it things like give me your package, public methods etc.

Checkout the ClassPool & CtClass objects.

List<CtClass> classes = new ArrayList<>();

// Using apache commons I/O you can use a glob pattern to populate ALL_CLASS_FILES_IN_PROJECT
for (File file : ALL_CLASS_FILES_IN_PROJECT) {
  ClassPool default = ClassPool.getDefault();
  classes.add(default.makeClass(new FileInputStream(file.getPath())));
}

The classes list will have all the classes ready for you to now deal with. You can add this to a static block in some entry point class that always gets loaded.

If this doesn't work for you, the next bet is to use the javaagent to do this. Its not that hard to do it, but it will have some implication on your deployment (the agent lib jar should be made available & the -javaagent added to the startup args).

Pavan
  • 1,245
  • 7
  • 10