Dozer
Dozer is a popular bean-to-bean mapping library. This functionality is typically needed when switching between domain models, e.g. to isolate the internal representation of a domain model from the model published to clients through a REST API.
Dozer employs a class loader for loading at least the following resources:
- Mapping definition XML files. A mapping definition file contains custom mapping rules.
- Classes based on the fully qualified class name using Class.forName().
Dozer has a pluggable class loading configuration which allows to set a custom class loader for classes and resources. However Dozer was not designed with OSGi in mind. Custom class loaders are set on a Dozer singleton. This combined with the lazy loading of mapping definition files can generate unexpected results.
Demonstrating the problem
In an OSGi environment, each bundle’s classes are loaded by the bundle’s class loader. The set of classes available to a bundle includes the classes packaged in the bundle and the classes available through the Import-Package directive.
In order to use Dozer in a bundle, the following directive will need to be added to the MANIFEST.MF:
Import-Package: org.dozer;version="[5.5,6)"
The mapping definition file dozer.xml can be placed in the bundle, e.g. in the bundle’s top-level directory.
In the bundle, the DozerBeanMapper will have to be instantiated and provided with one or more file paths to the mapping definition files:
DozerBeanMapper dozerBeanMapper = new DozerBeanMapper(); dozerBeanMapper.setMappingFiles( new ArrayList<String>(Arrays.asList(new String[]{"dozer.xml"})));
Then the mapper can be used to map class com.domain.a.A to com.domain.b.B with the following code:
A a = new A(); B b = dozerBeanMapper.map(a, B.class);
Unfortunately this will fail. Upon calling DozerBeanMapper.map(), the method will attempt to initialize the mappings by loading the mapping definition files and will fail with an org.dozer.MappingException: Unable to locate dozer mapping file [dozer.xml] in the classpath.
Setting the thread class loader (TCCL) to the bundle’s class loader and Wrapping the call with the TCCL will fix this particular problem creates a recipe for a non-portable fragile implementation. E.g. if Dozer was to be wrapped by a facade class then the facade would always have to be initialized while wrapped by the TCCL otherwise the embedded DozerBeanMapper would fail.
// Provided that 'this' is a bundle class, getClass().getClassLoader() // references the bundle class loader ClassLoader threadCl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); try { B b = dozerBeanMapper.map(a, B.class); } finally { Thread.currentThread().setContextClassLoader(threadCl); }
The above approach does not address the core issue which is the fact that Dozer’s default class loader cannot resolve neither the bundle’s classes / file resources, nor any imported classes.
Even if the mapping file is placed at a known location in the filesystem instead of being embedded in the bundle, Dozer will still fail when it will parse the mapping file and it will try to load the referenced classes using its org.dozer.util.DefaultClassLoader.
A workaround
Fortunately Dozer allows to replace the default class loader with a custom one:
org.dozer.util.DozerClassLoader classLoader=...; BeanContainer.getInstance().setClassLoader(classLoader);
The downside is that the custom class loader is set on a singleton, thus it affects all bundles that import Dozer. However if the following conditions are valid then an acceptable workaround can be implemented:
- Bundles are started sequentially.
- Dozer is setup completely during the bundle’s startup phase.
The first condition will prevent the class loader property of the BeanContainer to be overriden until a bundle has started.
The second condition will allow Dozer to load any custom mapping files and all classes referenced in them before the BeanContainer singleton is reset by another bundle.
The only missing pieces are the custom Dozer class loader, and the Dozer initialization in the bundle.
The Dozer class loader that delegates to the bundle’s class loader is shown below:
public class OsgiDozerClassLoader implements DozerClassLoader { private BundleContext context; @Override public Class<?> loadClass(String className) { try { return context.getBundle().loadClass(className); } catch (ClassNotFoundException e) { return null; } } @Override public URL loadResource(String uri) { URL url; url = context.getBundle().getResource(uri); if (url == null) { url = DozerClassLoader.class.getClassLoader().getResource(uri); } return url; } public void setContext(BundleContext context) { this.context = context; } }
Then during bundle startup – e.g. in a blueprint eagerly created bean constructor or init-method – setup the custom classloader, the mapping files and force parsing of the mapping files:
public DozerBeanMapper getMapper(List<String> files) { BeanContainer.getInstance().setClassLoader(classLoader); DozerBeanMapper mapper = new DozerBeanMapper(); mapper.setMappingFiles(files); // Force loading of the dozer.xml now instead of loading it // upon the first mapping call mapper.getMappingMetadata(); return mapper; }
The above approach is far from optimal. In practice it seems to work and its a viable workaround until singletons are removed & class loading becomes more OSGi-friendly in Dozer.
Acknowledgements
This technique is inspired by a similar approach used in the camel-dozer component.