Aurasphere
Aurasphere

Reputation: 4021

Spring inject XML unmarshalled entity

I'm working on a Spring application and I'd like to know if there's any way I could specify in my configuration the path of an XML file, having it automatically unmarshalled into a Java object through JAXB (I may consider other libraries though) and then inject it into a bean.

A Google search yields different results but they seem more about injecting a marshaller/unmarshaller in your bean and then doing the work yourself (like this one https://www.intertech.com/Blog/jaxb-tutorial-how-to-marshal-and-unmarshal-xml/) and I'm more interested in delegating this boilerplate to Spring.

Thanks

Upvotes: 2

Views: 718

Answers (1)

Michał Ziober
Michał Ziober

Reputation: 38710

You can implement your custom resource loader based on this article: Spicy Spring: Create your own ResourceLoader. It requires some assumptions:

  • Classes you want to load have all required annotation used by JAXB which allow deserialisation.
  • You can build JaxbContext using given list of classes.
  • You need to check yourself whether loaded class is what you expect.

Step 0 - create POJO

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "User")
@XmlAccessorType(XmlAccessType.FIELD)
public class User {

    @XmlElement(name = "firstName")
    private String firstName;

    @XmlElement(name = "lastName")
    private String lastName;

    // getters, setters, toString
}

You need to predefine POJO model which will be loaded from XML files. Above example just present one class but it should be similar for all other POJO classes.

Step 1 - create unmarshaller

import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

@Component
public class JaxbResourceUnmarshaller {

    private JAXBContext context;

    public JaxbResourceUnmarshaller() {
        try {
            context = JAXBContext.newInstance(User.class);
        } catch (JAXBException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public Object read(Resource resource) {
        try {
            Unmarshaller unmarshaller = context.createUnmarshaller();

            return unmarshaller.unmarshal(resource.getInputStream());
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}

Simple unmarshaller implementation where you need to create JAXBContext. You need to provide all root classes.

Step 2 - create class resource

import org.springframework.core.io.AbstractResource;

import java.io.IOException;
import java.io.InputStream;

public class ClassResource extends AbstractResource {

    private final Object instance;

    public ClassResource(Object instance) {
        this.instance = instance;
    }

    public Object getInstance() {
        return instance;
    }

    @Override
    public String getDescription() {
        return "Resource for " + instance;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return null;
    }
}

I could not find any specific class which could allow to return POJO instance. Above class has simple job to transfer class from deserialiser to Spring bean. You can try to find better implementation or improve this one if needed.

Step 3 - create JAXB resource loader

import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

public class JaxbResourceLoader implements ResourceLoader {

    private static final String DB_URL_PREFIX = "jaxb:";
    private final ApplicationContext applicationContext;
    private final ResourceLoader delegate;

    public JaxbResourceLoader(ApplicationContext applicationContext, ResourceLoader delegate) {
        this.applicationContext = applicationContext;
        this.delegate = delegate;
    }

    @Override
    public Resource getResource(String location) {
        if (location.startsWith(DB_URL_PREFIX)) {
            JaxbResourceUnmarshaller unmarshaller = this.applicationContext.getBean(JaxbResourceUnmarshaller.class);
            String resourceName = location.replaceFirst(DB_URL_PREFIX, "");
            Resource resource = applicationContext.getResource("classpath:" + resourceName);

            Object instance = unmarshaller.read(resource);

            return new ClassResource(instance);
        }
        return this.delegate.getResource(location);
    }

    @Override
    public ClassLoader getClassLoader() {
        return this.delegate.getClassLoader();
    }
}

In case resource definition starts from jaxb: let's try to handle it. In other case postpone to default implementation. Only classpath resources are supported.

Step 4 - register JAXB resource loader

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.Ordered;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;

@Component
public class ResourceLoaderBeanPostProcessor implements BeanPostProcessor, BeanFactoryPostProcessor, Ordered,
        ResourceLoaderAware, ApplicationContextAware {

    private ResourceLoader resourceLoader;
    private ApplicationContext applicationContext;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        if (bean instanceof ResourceLoaderAware) {
            ((ResourceLoaderAware) bean).setResourceLoader(this.resourceLoader);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        this.resourceLoader = new JaxbResourceLoader(this.applicationContext, this.resourceLoader);
        beanFactory.registerResolvableDependency(ResourceLoader.class, this.resourceLoader);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
}

This is just a copy of register class from article with only some changes. Probably could be much improved with latest Spring version.

Step 5 - simple usage

Assume you have pojos/user.xml file in resource folder which looks like below:

<User>
    <firstName>Rick</firstName>
    <lastName>Bartez</lastName>
</User>

You can inject it into Spring context like below:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;

@Configuration
public class JaxbAwareConfiguration {

    @Bean
    public AppOwner appOwner(ResourceLoader resourceLoader) {
        ClassResource resource = (ClassResource) resourceLoader.getResource("jaxb:pojos/user.xml");
        User user = (User) resource.getInstance();

        return new AppOwner(user);
    }
}

A little bit unpleasant is casting resource to ClassResource and instance to User class but it is a downside of this solution.

Upvotes: 2

Related Questions