Groovy - making existing objects refreshable
In the last post I described the basic principles I found behind the scenes of GroovyScript refresh. Now imagine that you want to create your own long living Groovy instances with auto-refresh behaviour when source code changes. You can use out-of-the-box Spring support - but there are some limitations I stated in the previous article.
In this post I am going to present an alternative solution that addresses some of the painful issues I noticed. As I stated before, key is to wrap the reference to Groovy instance into an another object managed by the Java class loader and that is exactly the main point of the solution presented.
My inspiration was based on the Spring's approach to the issue. It wraps each and every groovy instance into the JDK proxy, that is safe to handle out and store anywhere developer wants (even in the long living scopes). Implementation of this proxy delegates method call to inner Groovy instance, but is also able to check whether the underlying code haven't changed. If it has, it drops current Groovy instance and creates new one, that immediately initializes by configured dependency injection rules.
To overcome the main issues connected with Spring's solution, I decided to:
- base my solution on CgLib proxy instead of JDK Proxy - this way we could also use methods of the Java classes our Groovy class extends from and not only methods declared on interfaces
- use GroovyScripting engine instead of simple GroovyClassloader to reflect changes not only in the instantiated class itself, but also in classes it uses
- add easy method for unwrapping inner Groovy instance - so we could easily use methods declared on the Groovy class (for example in templating engines) without need of having Java interface that contains them (though programming for interfaces is a good approach, I don't see and advantage of having interface ever having only single implementation)
- provide callback for custom initialization logic after instance recreation (refresh)
Note: please, be warned, this post contains a lot of code that might not be as readable as I have wished it to be. If you feel that you don't get the point, download the sources with JUnit tests and feel free to fiddle with it a little bit. I am sure the topic is not so hard to undestand as it looks for the first sight.
Enough talking - let's look at the code. I use unified my own interface ScriptingFactory for creating new scripting instances (I try to abstract from the underlying Groovy scripting support at the base interfaces):
{% highlight java %} /** * This interface conceals logic connected with instantiating scripting (Groovy, JRuby ...) classes. */ @SuppressWarnings({"RawUseOfParameterizedType"}) public interface ScriptingFactory { /** * Returns scripting classloader. * @return this factory classloader */ ClassLoader getScriptingClassLoader(); /** * Returns true if class is loaded by ScriptingClassloader. * @param clazz to analyze * @return true if specified class is instantiated by this scripting factory classloader */ boolean isClassLoadedByScriptingClassloader(Class clazz); /** * Loads class of specified name. * @param className full class name with package * @return class for specified name * @throws ClassLoadingException whrown when source file cannot be found or is corrupted */ Class loadClass(String className) throws ClassLoadingException; /** * Instantiates object of specified class. * @param className full class name with package * @param args constructor arguments * @return class instance * @throws ClassInstantiationException thrown when class instantiation fails */ Object createInstance(String className, Object... args) throws ClassInstantiationException; /** * Instantiates object of specified class. * @param clazz to be instantiated * @param args constructor arguments * @return class instance * @throws ClassInstantiationException thrown when class instantiation fails */ Object createInstance(Class clazz, Object... args) throws ClassInstantiationException; /** * Returns interval in miliseconds after that a class validity according to underlying source should be examined. * @return refresh interval in miliseconds */ long getRefreshInterval(); /** * Closes ScriptingFactory, so no more queries will be handled by it. */ void close(); /** * Returns state of the scripting factory. * @return true if factory is closed */ boolean isClosed(); } {% endhighlight %}For this interface there are two Groovy implementations - ProductionGroovyFactory and DevelopmentGroovyFactory.
ProductionGroovyFactory creates instances of Groovy classes that never refresh. Returned instances are pure instances of specified Groovy classes. References passed around will potentialy prevent Groovy class and classloader being garbage collected, but that wouldn't be likely necessary in production (no refreshes occur there). On the other hand this implementation represents easiest and most performant way to providing Groovy instances. This implementantion is not interesting for the sake of this article, so I skip it.
DevelopmentGroovyFactory creates instances of Groovy classes wrapped in proxies. This will allow us to safely hand around references to those instances without worrying about refresh behaviour when underlying source code changes. Proxies will maintain refreshing logic, checking source code change in specified intervals and eventually freeing old instances and creating new fresh one based on new classes corresponding to latest source code state.
Moreover the system is setup the way that programmer doesn't need to take care of proper reference handling to avoid PermGenSpace memory leaks. Returned references doesn't have anything common with Groovy class - they are derived from first Java ancestor class in an ancestor hierarchy and implement all interfaces loaded by pure Java classloader. All Groovy related things are located in the proxy, so the swapping can be handled safely.
@SuppressWarnings({"RawUseOfParameterizedType"})
public class DevelopmentGroovyFactory implements ScriptingFactory {
private static final Log log = LogFactory.getLog(DevelopmentGroovyFactory.class);
private final GroovyScriptEngine engine;
private final long refreshInterval;
private ResourceConnector resourceConnector;
private boolean closed;
public DevelopmentGroovyFactory(ClassLoader parentClassLoader, String[] urls, long refreshInterval) {
try {
this.engine = new GroovyScriptEngine(urls, parentClassLoader);
this.engine.getGroovyClassLoader().setShouldRecompile(true);
this.refreshInterval = refreshInterval * 1000L;
GroovyGarbageCollectorMonitor.addGroovyScriptEngineToMonitoring(engine);
} catch(IOException ex) {
String msg = "Invalid source urls: " + ex.getLocalizedMessage();
log.error(msg);
throw new RuntimeException(msg, ex);
}
}
public DevelopmentGroovyFactory(ClassLoader parentClassLoader, URL[] urls, long refreshInterval) {
this.engine = new GroovyScriptEngine(urls, parentClassLoader);
this.engine.getGroovyClassLoader().setShouldRecompile(true);
this.refreshInterval = refreshInterval * 1000L;
GroovyGarbageCollectorMonitor.addGroovyScriptEngineToMonitoring(engine);
}
public DevelopmentGroovyFactory(ClassLoader parentClassLoader, ResourceConnector rc, long refreshInterval) {
this.engine = new GroovyScriptEngine(rc, parentClassLoader);
this.engine.getGroovyClassLoader().setShouldRecompile(true);
this.refreshInterval = refreshInterval * 1000L;
this.resourceConnector = rc;
GroovyGarbageCollectorMonitor.addGroovyScriptEngineToMonitoring(engine);
}
/**
* Returns Groovy classloader.
*
* @return
*/
public ClassLoader getScriptingClassLoader() {
checkClosed();
return engine.getGroovyClassLoader();
}
/**
* Returns true if class is loaded by GroovyClassloader.
*
* @param clazz
* @return
*/
public boolean isClassLoadedByScriptingClassloader(Class clazz) {
checkClosed();
ClassLoader parameterClassLoader = clazz.getClassLoader();
ClassLoader engineClassLoader = engine.getGroovyClassLoader();
do {
if (parameterClassLoader == engineClassLoader) {
return true;
}
parameterClassLoader = parameterClassLoader.getParent();
} while (parameterClassLoader != ClassLoader.getSystemClassLoader());
return false;
}
/**
* @see #createInstance(Class, Object[])
*
* @param className
* @param args
* @return
* @throws ScriptException
* @throws ResourceException
* @throws InstantiationException
* @throws IllegalAccessException
*/
public Object createInstance(String className, Object... args) throws ClassInstantiationException {
checkClosed();
Class aClass = loadClass(className);
return createInstance(aClass, args);
}
/**
* Creates groovy class instance. Returned instance is not directly groovy class instance but rather dynamic Proxy of
* that class. This proxy wraps original groovy class instance but extends not from this instance, but from nearest
* superclass that is loaded by Java classloader, also this proxy implements all interfaces, that are comming out of
* Java classloaders. Proxy itself encapsulates refreshing logic, so that if underlying instance doesn't conform to
* the source script it is automatically discarded and new instance is created instead. Reference to this proxy can
* be safely stored in long living memory scopes without having to fear that refreshing logic would cause pergenspace
* leaking. When you need to call method, that is defined only on the Groovy class but has no backing in Java superclass
* or any of interfaces, you can use the unwrap() method that returns reference of the wrapped Groovy instance. This
* reference must not be stored anywhere as it is a key to hell of permgenspace leaks.
*
* Single limitation of this approach is that you cannot change java superclass or java interface implementation set
* on the fly. Once generated proxy cannot adapt to this change.
*
* @param className
* @param args
* @return
* @throws ClassNotFoundException
* @throws InstantiationException
* @throws IllegalAccessException
* @throws ScriptException
* @throws ResourceException
*/
public Object createInstance(Class className, Object... args) throws ClassInstantiationException {
checkClosed();
Object instance;
try {
//load a groovy class and make an instance
if (args.length == 0) {
instance = className.newInstance();
} else {
Class[] parameterTypes = new Class[args.length];
for(int i = 0; i < args.length; i++) {
Object arg = args[i];
parameterTypes[i] = arg.getClass();
}
Constructor constructor = className.getConstructor(parameterTypes);
instance = constructor.newInstance(args);
}
} catch (Exception ex) {
String msg = "Cannot create instance of class: " + className.getName() + " (" + ex.getLocalizedMessage() + ')';
log.error(msg, ex);
throw new ClassInstantiationException(msg, ex, className);
}
if (isClassLoadedByScriptingClassloader(className)) {
//analyze class and create proxy
BuildProxyDescriptor bpd = getJavaAncestorsForProxy(className, engine.getParentClassLoader());
ProxyFactory factory = new ProxyFactory(new Class[0]);
factory.setTargetSource(
new JavaRealmHotswappableTargetSource(instance, bpd.getSuperClassToExtend())
);
factory.setInterfaces(bpd.getInterfacesToImplement());
factory.addAdvisor(new DefaultIntroductionAdvisor(new GroovyProxyMixin(this, instance)));
factory.setProxyTargetClass(true);
factory.setExposeProxy(true);
factory.setFrozen(true);
return factory.getProxy();
} else {
return instance;
}
}
/**
* Returns refresh interval passed to this object via constructor.
* @return
*/
public long getRefreshInterval() {
checkClosed();
return refreshInterval;
}
/**
* Loads class from GroovyScriptingEngine - respecting code source changes of this and dependent classes.
* Might return each time different class object instance - due to source code modifications.
* @param className
* @return
* @throws ScriptException
* @throws ResourceException
*/
public Class loadClass(String className) throws ClassLoadingException {
checkClosed();
try {
//firstly imitate GroovyScriptingEngine and find out whether there is script of this name
String scriptName = className.replace('.', File.separatorChar) + ".groovy";
URLConnection connection;
if (resourceConnector != null) {
connection = resourceConnector.getResourceConnection(scriptName);
} else {
connection = engine.getResourceConnection(scriptName);
}
InputStream is = null;
try {
//if so load class from script
is = connection.getInputStream();
IOUtils.closeQuietly(is);
Class groovyClass = engine.loadScriptByName(className);
GroovyGarbageCollectorMonitor.addGroovyClassToMonitoring(groovyClass);
return groovyClass;
} catch(Exception ignored) {
//ups no script of this name found - lets load class on parent classloader
return engine.getParentClassLoader().loadClass(className);
} finally {
IOUtils.closeQuietly(is);
}
} catch (Exception ex) {
String msg = "Cannot load class: " + className + " (" + ex.getLocalizedMessage() + ')';
log.error(msg, ex);
throw new ClassLoadingException(msg, ex);
}
}
/**
* Closes GroovyFactory, so no more queries will be handled by it.
*/
public void close() {
this.closed = true;
}
/**
* Returns true if factory is closed.
*
* @return
*/
public boolean isClosed() {
return closed;
}
/**
* Checks whether this GroovyFactory is closed and when so - IllegalStateException is thrown.
* @throws IllegalStateException
*/
private void checkClosed() throws IllegalStateException {
if (closed) {
throw new IllegalStateException("This GroovyFactory is closed - no more operations will be accepted.");
}
}
/**
* Finds first superclass, that is loaded by Java classloader and all interfaces groovy class implements, that are
* also loaded by Java classloader.
*
* @param groovyClass
* @param javaClassLoader
* @return
*/
private BuildProxyDescriptor getJavaAncestorsForProxy(Class groovyClass, ClassLoader javaClassLoader) {
//find first ancestor that is loaded by pure Java classloader
Class superClassToExtend = null;
Class currentClass = groovyClass;
do {
currentClass = currentClass.getSuperclass();
if (ClassUtils.isVisible(currentClass, javaClassLoader)) {
superClassToExtend = currentClass;
}
} while (currentClass != null && superClassToExtend == null);
//gather all interfaces, that are not loaded by Groovy classloader
Class[] interfaces = ClassUtils.getAllInterfacesForClass(groovyClass, javaClassLoader);
return new BuildProxyDescriptor(superClassToExtend, interfaces);
}
/**
* Contains Java class description needed for Groovy proxy creation.
*/
public static class BuildProxyDescriptor {
private final Class superClassToExtend;
private final Class[] interfacesToImplement;
public BuildProxyDescriptor(Class superClassToExtend, Class[] interfacesToImplement) {
this.superClassToExtend = superClassToExtend;
this.interfacesToImplement = interfacesToImplement;
}
public Class getSuperClassToExtend() {
return superClassToExtend;
}
public Class[] getInterfacesToImplement() {
return interfacesToImplement;
}
}
/**
* Custom implementation of HotSwappableTargetSource that returns first Java parent class available for inner
* Groovy class instead of returning Groovy class itself.
*/
public static class JavaRealmHotswappableTargetSource extends HotSwappableTargetSource {
private static final long serialVersionUID = -6962339257859210715L;
private final Class superClassToExtend;
public JavaRealmHotswappableTargetSource(Object target, Class superClassToExtend) {
super(target);
this.superClassToExtend = superClassToExtend;
}
@Override
public synchronized Class getTargetClass() {
return superClassToExtend;
}
}
}
DevelopmentGroovyFactory wraps Groovy instances into the AOP dynamic proxy. This proxy always implements this interface:
/**
* Groovy proxy interface declares method available for wrap and unwrap inner scripting instance, and set instance initializer
* that initializes instance after eventual inner instance reinstantiation when source code changes.
*/
public interface ScriptingProxy extends RawTargetAccess {
/**
* Sets instance initalizer that initializes reinstantiated object after source code change.
* @param factory for initializing new inner scripting instance
*/
void setInstanceInitializer(ProxyInstanceFactory factory);
/**
* Returns set instance initializer.
* @see #setInstanceInitializer(ProxyInstanceFactory)
* @return instance initializer that should be used to newly created inner scripting instance
*/
ProxyInstanceFactory getInstanceInitializer();
/**
* Sets inner scripting instance.
* @param scriptingInstance to wrap in this proxy
*/
void wrap(Object scriptingInstance);
/**
* Returns inner scripting instance.
* Please do not store reference to this instance in any long living scope (static variables, session, servlet
* context, singletons and so on)
* @return pure scripting instance hidden inside this proxy
*/
Object unwrap();
/**
* Performs up to date check and potentialy updated outdated inner instance.
*
* @return false when inner scripting instance was updated
*/
boolean isUpToDate();
}
As you can see proxy uses so called ProxyInstanceFactory for creating new inner instances of the Groovy classes when corresponding source code changes. This interface could be optionaly used not only for instantiation itself, but also for initial setup of the instance, such as dependency injection and so on.
/**
* This interface allows to create instance created after refresh of the scripting class inside ScriptingProxy.
*/
public interface ProxyInstanceFactory {
/**
* Reinstantiate inner groovy instance.
* @param clazz
* @return new instance
*/
Object createInstance(Class clazz) throws ClassInstantiationException;
}
Last piece of the puzzle is GroovyProxyMixin, that implements ScriptingProxy interface and REALLY does the magic around monitoring source code changes and reinstantiation of the inner Groovy instance. Let's examine the code:
/**
* Basic and probably unique GroovyProxy implementation.
*/
@SuppressWarnings({"serial", "NonSerializableFieldInSerializableClass", "AccessToStaticFieldLockedOnInstance"})
public class GroovyProxyMixin extends DelegatingIntroductionInterceptor implements ScriptingProxy {
private static final Log log = LogFactory.getLog(GroovyProxyMixin.class);
private static final ProxyInstanceFactory DEFAULT_INSTANCE_FACTORY = new DefaultProxyInstanceFactory();
private final WeakReference groovyFactory;
private final Object monitor = new Object();
private Object groovyInstance;
private ProxyInstanceFactory instanceFactory;
private long nextCheckTimestamp;
public GroovyProxyMixin(ScriptingFactory groovyFactory, Object groovyInstance) {
this.groovyFactory = new WeakReference(groovyFactory);
this.nextCheckTimestamp = System.currentTimeMillis() + groovyFactory.getRefreshInterval();
wrap(groovyInstance);
}
public void setInstanceInitializer(ProxyInstanceFactory factory) {
this.instanceFactory = factory;
}
public ProxyInstanceFactory getInstanceInitializer() {
if (instanceFactory == null) {
return DEFAULT_INSTANCE_FACTORY;
} else {
return instanceFactory;
}
}
public void wrap(Object scriptingInstance) {
this.groovyInstance = scriptingInstance;
}
public Object unwrap() {
return groovyInstance;
}
public boolean isUpToDate() {
//check groovy factory is still living
ScriptingFactory groovyFactory = this.groovyFactory.get();
if (groovyFactory == null || groovyFactory.isClosed()) {
this.groovyInstance = null;
this.instanceFactory = null;
throw new IllegalStateException("Groovy factory is already closed - this object reference is simply dead.");
}
//check inner groovy instance uptodate status
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp > nextCheckTimestamp) {
nextCheckTimestamp = currentTimestamp + groovyFactory.getRefreshInterval();
synchronized(monitor) {
Class oldGroovyClass = groovyInstance.getClass();
Class groovyClass = groovyFactory.loadClass(oldGroovyClass.getName());
if(oldGroovyClass == groovyClass) {
return true;
} else {
if(log.isDebugEnabled()) {
log.debug("Class " + groovyClass.getName() + " changed - refreshing instance!");
}
GroovyGarbageCollectorMonitor.addGroovyClassToMonitoring(groovyClass);
//underlying script has changed and Groovy classloader reloaded the class
groovyInstance = getInstanceInitializer().createInstance(groovyClass);
//change proxy target for the next call
Advised advised = (Advised)AopContext.currentProxy();
HotSwappableTargetSource targetSource = (HotSwappableTargetSource)advised.getTargetSource();
targetSource.swap(groovyInstance);
//clear introspection cache to be able to free classes
CachedIntrospectionResults.clearClassLoader(groovyFactory.getScriptingClassLoader());
Introspector.flushFromCaches(oldGroovyClass);
return false;
}
}
}
return true;
}
@Override
protected Object doProceed(MethodInvocation mi) throws Throwable {
if(isUpToDate()) {
return super.doProceed(mi);
} else {
//as we cannot change method invocation for this call, manually run method on newly created object
//and immitate that the result goes from the existing MethodInvocation
//possible exeptions of NoSuchMethodException and SecurityException propagate
Class groovyClass = groovyInstance.getClass();
Method originalMethod = mi.getMethod();
Method newMethod = groovyClass.getMethod(originalMethod.getName(), originalMethod.getParameterTypes());
return newMethod.invoke(groovyInstance, mi.getArguments());
}
}
/**
* Default implementation.
*/
public static class DefaultProxyInstanceFactory implements ProxyInstanceFactory {
public Object createInstance(Class clazz) throws ClassInstantiationException {
try {
return clazz.newInstance();
}
catch(Exception e) {
String msg = "Cannot reinstantiate class: " + clazz.getName() + " (" + e.getLocalizedMessage() + ")";
log.error(msg);
throw new ClassInstantiationException(msg, e, clazz);
}
}
}
}
Example of the API usage
Thought horrible it might look like - the important side of the matter is how it'll be used by the client code that will use this API. And there, I think, we are at the much more solid ground:
public void testRealWorldUsage() throws Exception {
Resource rootFSR = new FileSystemResource("/www/project/classes/");
groovyFactory = new DevelopmentGroovyFactory(
Thread.currentThread().getContextClassLoader(), //parent classloader
new URL[] {rootFSR.getURL()}, //source codes
0 //refresh interval
);
//create groovy instance
MySpecificJavaClass groovyInstance = (MySpecificJavaClass)
groovyFactory.createInstance("com.fg.mock.MainGroovyClass");
/** uncomment this to set up custom intance factory
* (possibly intitializing the instance for example by dependency injection)
* ((ScriptingProxy)groovyInstance).setInstanceInitializer(... custom factory ...);
**/
assertTrue(groovyInstance instanceof JavaHelloWorldInterface);
assertEquals("Hello Universe.", ((JavaHelloWorldInterface)groovyInstance).sayHello());
}
This doesn't look difficult, does it?
References that you receive from the DevelopmentScriptingFactory could be passed around without fear of PermGenSpace leaks and yet the behaviour would dynamically adapt source code changes. In Java code we could safely cast to any of Java interfaces or Java classes our Groovy class extends from. When we need to use created Groovy instance in templating engine (such as Freemarker or Velocity) we could unwrap original Groovy instance from the returned ScriptingProxy, so the engine could call any method declared on the Groovy class by the reflection. Unwrapping inner instance is somewhat dangerous, but for the purpose of the templating activities we could afford this. Template engines usually create a context used for single request processing only, so the unwrapped instance references stored in such context will soon fade off as the request gets processed and context itself dies. Similarly we could safely store unwrapped Groovy instances into the request attributes or other short lived objects, making profit of the direct access to the Groovy class instance.
Source code with tests download
Here you can download source code with IntelliJ Idea project files to examine. I recommend to play with it a little bit in a debug mode, as well as creating some more tests on your own. After all, I could be terribly wrong, thought the tests confirm my theories.
What comes next ...
There is yet another part of this serie, where I will present an integration of current solutions into the Spring via implementing BeanFactory and simple solution for refreshing Spring context trees when Spring configuration or any of the monitored bean changes. Will I make it till Christmas? I don't know as there isn't a comma of it written. Be patient, please ...
Komentáře