The bad news is that OSGi is not making this easier. The OSGi framework was designed with green field applications in mind that would follow proper modularity rules and communicate through services. In a perfect OSGi world, bundles are highly cohesive and are coupled through their services. In the real world, class loaders are (ab)used for configuration and ad-hoc plugin systems. Some of those problems are quite complex.
Today, I was asked about the following problem.
A company is using Spring. One of their bundles referred to a service that another bundle had registered. Spring has decided that services are too jumpy, so they proxy the service object for their clients and add some mixins in the proxy that handle the situations when a service goes away (Spring also adds some other mixins).
Creating a proxy is class loader magic. You take a set of interfaces (the mixins) and generate the byte codes for a class that implements all these interfaces. The implementation of the interface methods forwards the call to a handler object using the object, the method that was called and the arguments.
If you generate the byte codes for a class, you need to define this class in a class loader and this is where the modularity comes into play. In a traditional Java application you can just take the application class loader and define this new class in this class loader. The application class loader usually has full visibility. In a modular system, however, visibility is restricted to what you need to see. In OSGi, the imported packages are all a bundle can see (at least, when you want to work modular and not use dynamic imports buddy class loading).
The catch is that the proxy requires access to all the mixin classes. However, some of these mixin classes come from one bundle, and some come from another bundle. There is likely no class loader that can see both bundle class spaces unless that bundle specifically imports the correct interfaces. However, if a client bundle gets one of his objects proxified, then he has no knowledge of the mixins (that is the whole idea). It would be fundamentally wrong to import the interface classes while he is oblivious of them. However, the bundle that generates the proxy might know the mixin classes but it is unlikely it knows the classes of the target object. If the proxy generator was implemented as a service (as it should be) then there would even be 3 bundles involved: the proxy generator, the bundle needing the proxy, and the client bundle.
Spring solved this problem with a special class loader that chained the class loader from the client bundle together with the class loader of the Spring bundle. They made the assumption that the Spring bundle would have the proper imports for the mixin classes it would provide and the client bundle would have knowledge of the proxied object's classes.
So all should work fine? Well obviously not or I would not write this blog.
The generated proxy class can see the Spring bundle with the interface classes and it can see the all the classes that the client bundle can see. In the following picture, the client bundle imports the service interface from package p, but it does not import package q which is used in package p. If you create a proxy for a class in package p, it will need access to package q, and the client bundle can not provide this.
For example, assume the following case:
javax.swing.event.DocumentEvent de =
(DocumentEvent) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<>[]{DocumentEvent.class}, this );
The
DocumentEvent
is from the javax.swing.event
package. However, it uses classes from the javax.swing.text
package. Obviously the bnd tool correctly calculates the import for the DocumentEvent
class. However, when the generated proxy class is instantiated, it will need access to all the classes that the DocumentEvent
refers to: the super class, classes used as parameters in method calls, exceptions thrown, etc. These auxiliary classes are crucial for the DocumentEvent
class, but they are irrelevant for the client bundle, unless these classes are actually used in the client bundle, where they would be picked up by bnd. So, if the client bundle does not import
javax.swing.text
then you will get a ClassDefNotFoundError when you try to instantiate the proxy class. This error gets generated when the VM sees the reference to class in the javax.swing.text
package and tries to load it through the proxy class' class loader (that is why this is an error, it happens deep down in the VM).To be specific, this is exactly as it should be in a proper modular system. The client should not be required to import classes that it does not need. The onus is on the proxy generating code to do it right.
Fortunately, the solution is not that hard but it highlights how easy it is to make the wrong assumption, believe me, these Spring guys are quite clever.
Just take a step back and look at the problem.
Do we need visibility on the bundle level here? Look at the information given to the proxy generator: a set of mixin classes. It does not really matter from what bundle these interfaces classes come from; the class loader that we need must be able to see each of the interface classes class loaders. By definition, those class loaders can see the right class space.
So instead of chaining the bundle class loaders, the problem can be solved by having a class loader that searches each of the class loaders of the used mixin classes.
For the Advanced
The sketched model is too simplistic because there is the cost of a class loader per generated proxy. Even though enterprise developers seem to live in a wonderful world where memory is abundant, as an old embedded geezer such abuse of class loaders worries me. Though it is possible to use a single combined class loader by keep adding the required class loaders but this raises the question of life cycle. Such an über class loader would pin an awful lot of bundle class loaders in memory and it will be very hard to not run into class space consistency problems because over time this class loader will see all class spaces. That is, this approach brings us back to all the problems of today's application servers.
I have not tried this, but the solution is likely to make the proxy generator service based. Not only will this make it easier to plugin different generators, it also means that the generator is bundle aware. It can then create a combining class loader for each bundle that uses the Proxy Generator service. This class loader will then be associated with the class space of that bundle and should therefore not be polluted with classes from other class spaces. In the Spring case, this would be the client bundle, not the spring bundle. The reason is, is that the spring bundle could be used from different class spaces.
However, the proxy generator must track any bundle that it depends on. That is, it must find out which bundle's export the interface classes and track their life cycle. If any of these bundles stop, it must clear the client's combining class loader and create a new one when needed. This will allow the classes from the stopped bundle to be garbage collected.
Peter Kriens
For the hard core aficionados, some code that demonstrates the problem:
First a base class that acts as activator. This base class calls a createProxy method in the start method. This is then later extended with a proxy generation method that fails and one that works ok.
package aQute.bugs.proxyloaderror;
import java.lang.reflect.*;
import javax.swing.event.*;
import org.osgi.framework.*;
public abstract class ProxyVisibility implements BundleActivator,
InvocationHandler {
public void start(BundleContext context) throws Exception {
try {
DocumentEvent de = createProxy(this, DocumentEvent.class);
System.out.println("Succesfully created proxy " + de.getClass());
} catch (Throwable e) {
System.out.println("Failed to create proxy" + e);
}
}
public void stop(BundleContext context) throws Exception {
// TODO Auto-generated method stub
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return null;
}
abstract protectedT createProxy(InvocationHandler handler,
Classprimary, Class... remainder);
}
The following class uses the base activator but uses only the client bundle's class loader. Trying to create the proxy will therefore fail because this class loader can not see the javax.swing.text package.
package aQute.bugs.proxyloaderror;
import java.lang.reflect.*;
public class ProxyLoadError extends ProxyVisibility {
@SuppressWarnings("unchecked")
publicT createProxy(InvocationHandler h,
Classprimary, Class... others) {
Class parms[] = new Class[1 + others.length];
parms[0] = primary;
System.arraycopy(others, 0, parms, 1, others.length);
return (T) Proxy.newProxyInstance(getClass().getClassLoader(), parms,
h);
}
}
And at last, the solution. The following class extends the base activator and creates a proxy that uses a Combined Class Loader. This loader traverses all the class loaders of the mixin classes.
package aQute.bugs.proxyloaderror;
import java.lang.reflect.*;
import java.lang.reflect.Proxy;
import java.net.*;
import java.util.*;
public class ProxyLoadOk extends ProxyVisibility {
@SuppressWarnings("unchecked")
publicT createProxy(InvocationHandler h, Class primary,
Class... others) {
CombinedLoader loader = new CombinedLoader();
Class parms[] = new Class[1 + others.length];
parms[0] = primary;
System.arraycopy(others, 0, parms, 1, others.length);
for (Class c : parms) {
loader.addLoader(c);
}
return (T) Proxy.newProxyInstance(loader, parms, h);
}
static class CombinedLoader extends ClassLoader {
Setloaders = new HashSet ();
public void addLoader(ClassLoader loader) {
loaders.add(loader);
}
public void addLoader(Class clazz) {
addLoader(clazz.getClass().getClassLoader());
}
public Class findClass(String name) throws ClassNotFoundException {
for (ClassLoader loader : loaders) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException cnfe) {
// Try next
}
}
throw new ClassNotFoundException(name);
}
public URL getResource(String name) {
for (ClassLoader loader : loaders) {
URL url = loader.getResource(name);
if (url != null)
return url;
}
return null;
}
}
}
Hi Peter,
ReplyDeleteThanks for looking into this and reporting the problem. A fix has been committed in Spring-DM and should be available in the next nightly build (those interested can monitor the issue at http://jira.springframework.org/browse/OSGI-597).
Regarding the proxying example, I have one remark - for simple JDK proxies (such as the one above), there is a simple fix, that doesn't require a chained classloader, when using classes from the same bundle (classloader):
Instead of getClass().getClassLoader(),
use the interface classloader:
[code]
ClassLoader cl = javax.swing.event.DocumentEvent.class.getClassLoader();
if (cl == null)
cl = ClassLoader.getSystemClassLoader();
DocumentEvent de =
(DocumentEvent) Proxy.newProxyInstance(cl,
new Class<>[]{DocumentEvent.class}, this );
[/code]
Cheers,
Costin Leau
Interesting post - we use something similar to the combined classloader in the latest Guice code, although we have a special map with weak keys and values so we can eagerly unload proxy classes.
ReplyDeleteSome documentation is here and the code is here.
Great minds think alike Peter! I did something similar back in July :)
ReplyDeleteCan this source code be compiled and run from Ecliepse environment??
ReplyDeleteAre there any other framework required for this to work??
Thanks for the info.
http://www.interview-questions-tips-forum.net/
Is this anyways related to ServiceLocator and
ReplyDeleteProxy pattern??
And what will be the use of this ProxyLocator in a Web based MVC architectural pattern??
Thanks,
Amit
http://www.interview-questions-tips-forum.net