0

For performance reasons, I have a class that stores a Map whose key is a Class<?> and its value is function of that class's fields. The map is populated during code execution according to the type of the calling object. The above is a generalization/simplification

public class Cache {

    private static final Map<Class<?>, String> fieldsList = ...;


    //Synchronization omitted for brevity
    public String getHqlFor(Class<?> entity){
        if (!fieldsList.containsKey(entity))
            fieldsList.put(entity,createHql(entity));
        return fieldsList.get(entity);
    }

}

During development, thanks to the help of Jrebel, I often make modifications to classes by changing entire properties or just their names. I can continue development just fine. However, if I already put a value into the cache it will be stale forever.

What I am asking here is if it is possible to intercept the event that a class in the classpath has changed. Very broad... But my specific problem is very simple: since I have such a need only during development, I just want to wipe that cache in case any class in my classpath changes.

How can I accomplish this? I don't need to do anything special than intercepting the event and simply wiping the cache

usr-local-ΕΨΗΕΛΩΝ
  • 26,101
  • 30
  • 154
  • 305

2 Answers2

3

JRebel has a plugin API that you can use to trigger code on class reloads. The tutorial complete with example application and plugin available here: https://manuals.zeroturnaround.com/jrebel/advanced/custom.html

The JRebel plugin is a self-contained jar built against the JRebel SDK, which is attached to the running application via the JVM argument -Drebel.plugins=/path/to/my-plugin.jar. The JRebel agent attached to the application will load and start plugins from this argument.
If the application is not started with the JRebel agent, the plugin is simply not loaded.

In your example you want to register a ClassEventListener that will clear the Cache.fieldsList map. As it is a private field, you need to access it via reflection or add a get/clear method via a ClassBytecodeProcessor

public class MyPlugin implements Plugin {
  void preinit() {
    ReloaderFactory.getInstance().addClassReloadListener(new ClassEventListenerAdapter(0) {
      @Override
      public void onClassEvent(int eventType, Class<?> klass) throws Exception {
        Cache.clear();
      }
    });
  }
  // ... other methods ...
}

And to clear the map

public class CacheCBP extends JavassistClassBytecodeProcessor {
  public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) {
    ctClass.addMethod(CtMethod.make("public static void clear() { fieldsList.clear(); }", ctClass));
  }
}

However a better option is to only clear/recalculate the single class entry on class reload if possible. The example didn't display whether the info computed from one class depended on superclass infos, but if this is true, the JRebel SDK has methods to register a reload listener on the class hierarchy as well.

Murka
  • 353
  • 4
  • 10
  • It is my understanding that this requires the Jrebel SDK in the classpath. That, however, should never be deployed to production. I mean my boss will get upset if I ask to pollute the application's classpath with dependencies from JRebel. I'll take a deeper look into it – usr-local-ΕΨΗΕΛΩΝ Oct 06 '17 at 14:36
  • The JRebel SDK is already on the classpath when using JRebel. – Murka Oct 06 '17 at 14:56
  • Not when you compile using Ant's javac command with a well defined classpath. Anyway I just tried to type "com.zeroturnaround" in my auto complete and nothing showed. `Plugin` doesn't either show results from the JRebel world – usr-local-ΕΨΗΕΛΩΝ Oct 06 '17 at 15:00
  • The JRebel plugin is built against the SDK the same way one would build a JavaEE webapp against the JavaEE API without bundling it into a WAR. The actual SDK implementation is in the JRebel agent attached to the application, which adds these classes to the classloader. Clarified the answer to show how the plugin is used, but I suggest reading through the plugin tutorial to get a full overview. – Murka Oct 09 '17 at 14:05
0

There is an existing class ClassValue which already does the job for you:

public class Cache {

    private final ClassValue<String> backend = new ClassValue<String>() {
        @Override
        protected String computeValue(Class<?> entity) {
            return createHql(entity);
        }
    };

    public String getHqlFor(Class<?> entity){
        return backend.get(entity);
    }
}

When you call get, it will call computeValue if this is the first call for this specific Class argument or return the already existing value otherwise. It does already care thread safety and for allowing classes to get garbage collected. You don’t need to know when class unloading actually happens.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Looks good, but not applicable to my case. I have simplified my example a lot and the getHqlFor takes at least two classes as params. But I am looking on how to adapt this. Thank you – usr-local-ΕΨΗΕΛΩΝ Oct 06 '17 at 14:34
  • Looks applicable to my case now :-) – usr-local-ΕΨΗΕΛΩΝ Oct 11 '17 at 10:35
  • This won't work in the case of JRebel class reloads as the new version of class will compare equal to the previous one so it will not compute a new value. You can try reloading the example code here by uncommenting the new field and seeing the old result: https://pastebin.com/iKhA6RuX – Murka Oct 17 '17 at 14:01