Thursday, June 25, 2009

JDBC class loading failed from JAX-WS annotated class

Creating a JAX-WS based webservice either using JAX-WS RI or Apache CXF is simplified by using annotation, for example creating interface annotated with @WebService is a simple as the following:
@WebService
public interface AService {
   @WebMethod
   String getThingsFromJDBC(String key);
}
Then the interface is implemented as the following:
@WebService
public class AServiceImpl implements AService {
    @WebMethod
    public String getThingsFromJDBC(String key) {
      // some impl here
    }  
}
Then you will have your web service running in no time (if you follow the guidance provided by CXF -- let say we are using apache CXF). Now, let say we want to access JDBC based database from our implementation -- we want a light weight approach, neither Hibernate nor JPA, we usually load the driver first like:
   bool initiated = false;
   Connection connection = null;
   private void initJDBC() {
      try {
         Class.forName("oracle.jdbc.driver.OracleDriver");
         connection = DriverManager.getConnection(jdbcUrl);
         initiated = true;
      } catch (Exception ex) {
        ex.printStackTrace();
      }
   }

   public String getThingsFromJDBC(String key) {
      String ret= null;
      if (initiated && connection!=null)
      {
         // do your query here
      }
      return ret;
   }

   

It should be OK right? Not OK in some occasions, the some drivers won't be loaded such as Oracle driver. Why, there is a class loading problem. The JAX-WS webservice implementation is actually loaded by the CXFServlet below the scope of the webapplication, whereas the path of the JDBC library is only visible from web application context. Thus, your JDBC driver should be loaded by the webapplication classloader. So, how can we solve this? One of the approach I have taken was to create a separate class to deal JDBC data access, instantiate it with Spring and then inject it to the JAX-WS service implementation. Example:
public class MyJDBCAccess {
    bool initiated = false;
    Connection connection = null;
    public void init()
    {
      try {
         Class.forName("oracle.jdbc.driver.OracleDriver");
         connection = DriverManager.getConnection(jdbcUrl);
         initiated = true;
      } catch (Exception ex) {         
        ex.printStackTrace();
      }
    }

   public String doGetThingsFromJDBC(String key) {
      String ret= null;
      if (initiated && connection!=null)
      {
         // do your query here
      }
      return ret;
   }
}
In order to be wire MyJDBCAccess instant to AServiceImpl, we need to define a property for MyJDBCAccess instance. E.g add the following snippet into AServiceImpl.java:

    private MyJDBCAccess bean = null;

    public void setMyJDBCAccess(MyJDBCAccess b)
    {
       this.bean = b;
    }

    public MyJDBCAccess getMyJDBCAccess() 
    {
       return this.bean;
    }
    // we need to change the implementation of getThingsFromJDBC(String key)
    public String getThingsFromJDBC(String key) {
      String ret=null;
      try {
          ret=bean.doGetThingsFromJDBC(key);
      } catch (Exception ex) {
          // use a good logging mechanism here
          ex.printStackTrace();
      }
      return ret;
    }


Now.. how to wire it? A snippet of typical CXF webservice bean definition is like this:
<bean id="aservice" class="AServiceImpl" />

<jaxws:endpoint id="AService"
    implementor="#aservice"
    address="/a_service"/>
To inject MyJDBCAccess to aservice bean we need to define an instance of MyJDBCService like the following:
<bean id="jdbcAccess" class="MyJDBCAccess" />
<bean id="aservice" class="AServiceImpl" >
    <property name='myJDBCAccess' ref='jdbcAccess'/>
</bean>

<jaxws:endpoint id="AService"
    implementor="#aservice"
    address="/a_service"/>
Then your jdbc data access should be ok. Hope this could help someone who caught similar problem.

No comments: