Sunday, July 22, 2012

Spring managed components interoperability with JPA/JAXB or others


Spring managed components interoperability with JPA/JAXB or others

As Spring is a de-facto standard in creating JEE application, one problem faced commonly is interoperability between Spring  managed beans and objects created by other  runtimes like JPA or JAXB. As most of the application is using JPA or JAXB we will look into ways to use Spring beans in other objects and injecting other objects in Spring beans. In This article I will refer objects created by other runtime like JAXB or JPA as external object and the classes as external class.

Configuring bean programmatically in Java class:

For configuring beans programmatically Spring provided their <beans> and <bean> equivalent in Java.

package interop.config.test;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfigurationClass {

      class SampleBean{
           
      }
     
      @Bean(name="sample")
      SampleBean getBean(@Value("#{systemProperties['user.dir']}")String param, ServiceClass service){
            return new SampleBean();
      }
}

1. Annotate the class with @Configuration
2. Annotate method for creating beans with @Bean

Spring will execute this method and register the returned object as bean with name as method name.

Parameters passed into these creator methods are of type any Spring bean or annotated as @Value. If this method requires other objects, then call other bean methods and those returned beans will be available to it.

@Configuration is equivalent to XML configuration and @Bean is equivalent to <bean/> and has all configuration options like Scope, Name etc are available.

Other useful annotations:
@ImportResource – Allows us to import a spring xml file or other @Configuration class for importing beans and resource like XML configuration.
@Value – Use SpEL expressions to inject properties or other bean attributes.

This class must be under component scan.

For more details:

Injecting Spring components (beans) into external classes (instantiated using new operator):

This is useful in using Spring components/services in external classes instantiated using new operator. Spring intercepts the autowired beans in the Configurable class and new calls for the class. There are several strategies for weaving those classes like weaving at compile or load time. Look resource #1.

This can be done using @Configurable class.

This needs following jars:
cglib-2.2.2.jar
asm-all-3.3.1.jar
aspectjrt-1.6.9.jar
aspectjweaver-1.6.9.jar
org.springframework.aspects-<VERSION>.jar
org.springframework.instrument-<VERSION>.jar

Add following in applicatioContext.xml
<context:spring-configured />
<context:load-time-weaver />

Run application with VM argument:
            -javaagent:lib/org.springframework.instrument-<VERSION>.jar

Sample Code:

@Configurable class:

package interop.config.test;

import interop.config.test.ConfigurationClass.SampleBean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class ConfigurableClass {

      @Autowired
      SampleBean sampleBean;
     
      public SampleBean getSampleBean() {
            return sampleBean;
      }
}

applicationContext.xml
                                   
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:context="http://www.springframework.org/schema/context"
          xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<context:component-scan base-package="interop.config.test" />

          <context:spring-configured />
          <context:load-time-weaver />
</beans>

@Configuration class to create the autowired bean is the class described in previous topic.

Issues with @Configurable

Resources:

Injecting external objects in Spring context to use them in Spring components:

Adding objects to application context [singletons]:
Singletons can be added the way described in resources listed #1 and dynamic bean definitions can be added as listed in Resources #2.

Sample code for registering singleton in ApplicationContext:           
public class ConfigTest {

          public static void main(String[] args) {
                   ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
                   //registering singleton
                   context.stop();
                   ConfigurableListableBeanFactory factory = context.getBeanFactory();
                   factory.registerSingleton("auto", new Auto("fgfdg", "fgfd"));
                   context.start();
                   System.out.println(Arrays.asList(context.getBean("auto")));
          }
         
          static class Auto{
                   String a;
                   String b;
                   public Auto(String a, String b) {
                             this.a=a;
                             this.b=b;
                   }
          }
}

Now how to register beans in ptototype scope dynamically?

Use Case:
  • Application is receiving data from external API (REST or Web Services) in XML format.
  • JAXB populates object graph from XML.
  • Application has @Service (s) which needs this object graph populated by JAXB. For example, populating data transfer object (DTO) by using SpEL (@Value annotated fields) or the @Service consuming this object graph by any other purpose.

How to make this object graph available to those services (Spring components)?

First of all such Service need to of scope prototype as it depends on data retrieved from external sources (will be different every time, so objects state depends on this data. This object is stateful). A Service can be singleton which is using that data only in a method and by this object graph as argument, but in scenario like populating DTO using SpEL where fields of class is annotated with @Value(<expression>). We will look into prototype scoped bean.
Scope is not necessarily be prototype, can be changed according to requirements.

We need following things:
  1. A Spring component bean in which we will store our object. This component will be singleton and will be in application context already. This will be annotated with @Component and will have fields for holding data.

package interop.config.test;

import java.util.Map;
import java.util.TreeMap;

import org.springframework.stereotype.Component;

@Component
public class ExternalObjectContainer {

public Map<String, Object> container = new TreeMap<String, Object>();
     
            public void addObject(String key, Object value){
                  container.put(key, value);
            }
     
            public Object getObject(String key){
                  return container.get(key);
            }
}

In this class currently there is only one field of type Map for holding data. This field can be of Object type or some specific JAXB root class type. Also, for concurrent access or storing more objects, data structure and storage methods can be changed as per requirement.

External class:

package interop.external;

public class Root {

            public Customer customer;
            public Order order;
            public Object object;
     
            public class Customer{
                  public String name;
                  public Customer(String name) {
                        this.name=name;
                  }
            }
     
            public class Order{
                  public int quantity;
                  public Order(int quantity) {
                        this.quantity=quantity;
                  }
            }
}

  1. The requester of the external API will set the object graph, populated from response received to the singleton component, created in previous step. This needs reference to the component. This reference can be acquired by autowiring the bean to this requester. If this requester is not a spring component then make this @Configurable.

package interop.config.test;

import interop.external.Root;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

@Service
public class ServiceClass {

      @Autowired
      ExternalObjectContainer container;
     
      @Autowired
      ApplicationContext context;
     
      public void service(){
            /*
             * In real this object will be created by data received from external sources, e.g.
             * ReST API response XML converted to object model by JAXB.
             */
            Root root = new Root();
            root.customer=root.new Customer("customeR");
            root.order=root.new Order(9);
            //the container we created has Map to store objects, so put any random key.
            //this depends on storage model.
            container.addObject("randomKey", root);
            /*
             * request the consumer of this data, prototype scoped bean from application context.
             * This will create new object with all mapped fields populated from external object.
             * Cannot autowire because this will instantiate on creation of this service class and will
             * not have data or may even fail to inject required dependency.
             */
            ExternalObjectConsumerComponent component = (ExternalObjectConsumerComponent) context.getBean("externalObjectConsumerComponent");
            //check data populated in the bean
            System.out.println(component);
            //Doing the same thing with non Spring component, a configurable class.
            ExternalObjectConsumerConfigurable configurable = new ExternalObjectConsumerConfigurable();
            System.out.println(configurable);
           
      }
}

  1. The @Service class which will populate the DTO from this object graph. This will be a prototype scoped bean as this will have fields annotated with @Value(<SpEL expression>). This is the actual DTO. This bean will be requested only after step #2 is done. Obviously, DTO can be populated only if one have the required data.
One can make this singleton by moving these annotations to other class.

Using this injected external object in Spring component:

package interop.config.test;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Service
@Scope("prototype")
public class ExternalObjectConsumerComponent {

      @Value("#{externalObjectContainer.container.get('randomKey').customer.name}")
      String name;
      @Value("#{externalObjectContainer.container.get('randomKey').order.quantity}")
      int qty;
     
      @Override
      public String toString() {
            return super.toString()+"{Name : "+name+", Quantity : "+qty+"}";
      }
}

Using this injected external object in external object:

package interop.config.test;

import javax.annotation.PostConstruct;

import interop.external.Root;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;

/**
 *This example is for using external object injected in Spring component in a non Spring/external object.
 *
 *This will be useful in using external object in external object but in flow there is Spring services in between.
 */
@Configurable
public class ExternalObjectConsumerConfigurable {

      @Autowired
      ExternalObjectContainer container;
      String name;
      int qty;

      /*
       * Rather than using SpEL the data can be accessed directly from the bean.
       * Put this initialization logic in in method annotated with PostConstruct to populate data
       * just after initializing object. This is just an alternative for SpEL and nothing to do with
       * it is Spring component or not.
       */
      @PostConstruct
      public void mapProperties(){
            Root root = (Root) container.getObject("randomKey");
            name=root.customer.name;
            qty=root.order.quantity;
      }
     
      @Override
      public String toString() {
            return super.toString()+"{Name : "+name+", Quantity : "+qty+"}";
      }
}

Resources:

Testing all this:

import interop.config.test.ConfigurableClass;
import interop.config.test.ServiceClass;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class Main {

      public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
            System.out.println("\n\n\n");
            //check sample bean is configured through @Configuration class
            System.out.println(context.getBean("sample"));
            System.out.println("\n\n\n");
            //check Spring components injected in external objects through @Configurable class
            ConfigurableClass configurable = new ConfigurableClass();
            System.out.println("\n\n\n");
            System.out.println(configurable.getSampleBean());
            System.out.println("\n\n\n");
            //Test injecting external objects into Spring components
            ServiceClass service = (ServiceClass)context.getBean("serviceClass");
            service.service();
            System.out.println("\n\n\n");
      }
     
}

Running this with java agent:

In Eclipse configure Run Configuration like this:


Project overview:


Injecting prototype scoped bean into Singleton bean such that every time singleton bean is requested it will have new instance of prototype scoped bean.
It can be achieved using proxy scoped bean. In @Scope annotation parameter set proxyMode parameter. For details look.

Alternatively, method injection can be used.

Get project code and add Spring framework libraries as in eclipse projects classpath.

No comments:

Post a Comment