Code and Snow and Stuff

Musings on software and the quality of the white stuff…

Automatically Adding JAXB2 Classes to Spring using Annotations

Hi Again!

An annoying aspect of using the built-in Jaxb2Marshaller that is bundled with Spring is the fact that you have to manually add each class to be bound into the XML. This is sooooo boring and old skool :-) Thankfully, it’s not that hard to get Spring to scan all classes in a package for properly annotated classes and have those added to the JAXBContext automatically. I provide below, for your pleasure, an example bit of code that does the job.

Please note: Until (if?) Spring changes Jaxb2Marshaller, you will have to do some xml fudgery to get this to work:

<bean id="marshaller" class="AnnotationJaxb2Marshaller">
    <property name="classesToBeBound">
       <list><value>a.valid.jaxb2.annotated.class.ThatWillBeIgnored</value></list>
    </property>
    <property name="packagesToScan">
       <list><value>my.package.with.jaxb.classes</value></list>
    </property>
</bean>

The reason is that in Jaxb2Marshaller afterPropertiesSet is marked as final. However, in that method the JAXBContext is initialised, the code borks out if no classesToBeBound is set – therefore I set a dummy (but valid!) entry. The list that holds the set of classes to use (containing our dummy class) will be overwritten by the packagesToScan. Okay enough rabbling, here is the code:


import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlType;

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.oxm.UncategorizedMappingException;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;


/**
 * An extension to the Jaxb2Marshaller that scans the classpath for classes annotated
 * with the @XmlRootElement (and others) annotation. I don't like typing in the class in
 * classesToBeBound since I often forget which ones I've done!
 */
public class AnnotationJaxb2Marshaller extends Jaxb2Marshaller {

    private static final String RESOURCE_PATTERN = "/**/*.class";

    private String[] packagesToScan;
    private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
    private final TypeFilter[] jaxb2TypeFilters = new TypeFilter[]{
            new AnnotationTypeFilter(XmlRootElement.class, false),
            new AnnotationTypeFilter(XmlType.class, false),
            new AnnotationTypeFilter(XmlSeeAlso.class, false),
            new AnnotationTypeFilter(XmlEnum.class, false),
    };

    /**
     * Scan packages looking for any classes annotated with the @XmlRootElement annotation.
     */
    protected List<Class<?>> scanPackages() {
        final List<Class<?>> annotatedClasses = new ArrayList<Class<?>>();
        try {
            if (packagesToScan != null) {
                for (final String pkg : packagesToScan) {
                    final String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN;
                    final Resource[] resources = resourcePatternResolver.getResources(pattern);
                    final MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
                    for (final Resource resource : resources) {
                        final MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
                        final String className = metadataReader.getClassMetadata().getClassName();
                        if (matchesFilter(metadataReader, metadataReaderFactory)) {
                            final Class<?> jaxb2AnnotatedClass = resourcePatternResolver.getClassLoader().loadClass(className);
                            annotatedClasses.add(jaxb2AnnotatedClass);
                        }
                    }
                }
            }
        } catch (final IOException ex) {
            throw new UncategorizedMappingException("Failed to scan classpath for unlisted classes", ex);
        } catch (final ClassNotFoundException ex) {
            throw new UncategorizedMappingException("Failed to load annoted classes from classpath", ex);
        }
        return annotatedClasses;
    }

    /**
     * Determine if any of the classes matches our list of acceptable annotations.
     *
     * @param metadataReader for the resource.
     * @param metadataReaderFactory for the resource.
     * @return true if the class contains the annotation.
     * @throws IOException if anything goes wrong.
     */
    protected boolean matchesFilter(final MetadataReader metadataReader, final MetadataReaderFactory metadataReaderFactory) throws IOException {
        if (jaxb2TypeFilters != null) {
            for (final TypeFilter typeFilter : jaxb2TypeFilters) {
                if (typeFilter.match(metadataReader, metadataReaderFactory)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Why oh why is the afterPropertiesSet in the super class final???
     */
    @Override
    public synchronized JAXBContext getJaxbContext() {
        if (packagesToScan.length > 0) {
            // We will try *my* way :-)
            final List<Class<?>> annotatedClasses = scanPackages();
            if (annotatedClasses.size() > 0) {
                setClassesToBeBound(annotatedClasses.toArray(new Class<?>[0]));
            }
        }
        return super.getJaxbContext();
    }

    /**
     * Set packages to scan.
     */
    public void setPackagesToScan(final String[] packagesToScan) {
        Assert.notEmpty(packagesToScan, "'packagesToScan' must not be empty");
        this.packagesToScan = Arrays.copyOf(packagesToScan, packagesToScan.length);
    }

}

Enjoy!

-=david=-

About these ads

Written by dharrigan

January 3, 2012 at 11:10 pm

Posted in development, java

Tagged with ,

Follow

Get every new post delivered to your Inbox.

Join 54 other followers