Boldly Go Where No SQL Mapper Has Gone Before

The goal of the day is to use MyBatis in OSGi. Specifically, I'd like to use interface-based mappers and configure a SqlSessionFactory using Blueprint. Fortunately, MyBatis is currently shipping with all the necessary OSGi headers to get modularity junkies up and running in no time. To start off, here's one possible way create a SqlSessionFactory and register some mappers in code:

Configuration config = 
  new Configuration(
    new Environment.Builder("default")
      .dataSource(ds)
      .transactionFactory(new JdbcTransactionFactory())
      .build());

config.addMapper(Foo.class);
config.addMapper(Bar.class);

SqlSessionFactory factory = 
  new SqlSessionFactoryBuilder()
    .build(config);

From here, it's not hard to imagine holding critical elements of the configuration in class fields, implementing SqlSessionFactory, and forwarding calls to a (lazy-initialized) delegate instance.

public SqlSessionFactoryBean
  implements SqlSessionFactory
{
  private DataSource ds;
  private Set<Class<?>> mappers;
 
  private SqlSessionFactory delegate;

  ...
 
  private SqlSessionFactory getDelegate()
  {
    if(delegate == null)
    {
      Configuration config = 
        new Configuration(
          new Environment.Builder("default")
            .dataSource(dataSource)
            .transactionFactory(new JdbcTransactionFactory())
            .build());

      for(Class<?> mapper : mappers)
      {
        config.addMapper(mapper);
      }

      delegate = 
        new SqlSessionFactoryBuilder()
          .build(config);
    }
 
    return delegate;
  }
 
  @Override
  public SqlSession openSession()
  {
    return getDelegate().openSession();
  }
 
  @Override
  ...
}

Nothing particularly interesting—I surmise MyBatis-Spring might use a similar approach. There's a small problem though: we expect Class instances for the mappers, but with XML, we can really only supply Strings.

<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 
      http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
 
  <reference id="dataSourceFactory" 
    interface="org.osgi.service.jdbc.DataSourceFactory"/>
 
  <bean id="sqlSessionFactory" 
    class="com.greysphere.jdbc.SqlSessionFactoryBean">
    <argument>
      <bean factory-ref="dataSourceFactory" factory-method="createDataSource">
        <argument>
          <props>
            <prop key="serverName" value="..."/>
            <prop key="databaseName" value="..."/>
            <prop key="user" value="..."/>
            <prop key="password" value="..."/>
          </props>
        </argument>
      </bean>
    </argument>
    <argument>
      <set>
        <value>com.greysphere.app.Foo</value>
        <value>com.greysphere.app.Bar</value>
      </set>
    </argument>
  </bean>
 
</blueprint>

The immediate temptation is to accommodate in the following manner:

public SqlSessionFactoryBean
  implements SqlSessionFactory
{
  private DataSource ds;
  private Set<String> mappers;
 
  private SqlSessionFactory delegate;

  ...
 
  private SqlSessionFactory getDelegate()
  {
    if(delegate == null)
    {
      Configuration config = 
        new Configuration(
          new Environment.Builder("default")
            .dataSource(dataSource)
            .transactionFactory(new JdbcTransactionFactory())
            .build());

      for(String mapper : mappers)
      {
        config.addMapper(Class.forName(mapper));
      }

      delegate = 
        new SqlSessionFactoryBuilder()
          .build(config);
    }
 
    return delegate;
  }
 
  @Override
  public SqlSession openSession()
  {
    return getDelegate().openSession();
  }
 
  @Override
  ...
}

This will obviously fail since we are targeting OSGi: the delegate SqlSessionFactory implementation actually resides within a different bundle from this code, and there would be runtime exceptions due to missing classes. What we really need here is a method of specifying who the consuming bundle is in order to make use of its classloader and get at the mapper interfaces inside.

public SqlSessionFactoryBean
  implements SqlSessionFactory
{
  private DataSource ds;
  private Set<String> mappers;
  private Bundle sourceBundle;
 
  private SqlSessionFactory delegate;

  ...
 
  private SqlSessionFactory getDelegate()
  {
    if(delegate == null)
    {
      Configuration config = 
        new Configuration(
          new Environment.Builder("default")
            .dataSource(dataSource)
            .transactionFactory(new JdbcTransactionFactory())
            .build());

      for(String mapper : mappers)
      {
        config.addMapper((Class<?>) sourceBundle.loadClass(mapper));
      }

      delegate = 
        new SqlSessionFactoryBuilder()
          .build(config);
    }
 
    return delegate;
  }
 
  @Override
  public SqlSession openSession()
  {
    return getDelegate().openSession();
  }
 
  @Override
  ...
}

But how to declare injection of the consumer bundle in the XML? It turns out the R4 Enterprise spec (section 121.11.1 Environment Managers) requires implementors to provide access to the current bundle via a hardcoded reference with an ID of blueprintBundle. Let's leverage this feature to finally pass the consumer bundle to SqlSessionFactoryBean and complete the configuration:

<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 
      http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
 
  <reference id="dataSourceFactory" 
    interface="org.osgi.service.jdbc.DataSourceFactory"/>
 
  <bean id="sqlSessionFactory" 
    class="com.greysphere.jdbc.SqlSessionFactoryBean">
    <argument ref="blueprintBundle"/>
    <argument>
      <bean factory-ref="dataSourceFactory" factory-method="createDataSource">
        <argument>
          <props>
            <prop key="serverName" value="..."/>
            <prop key="databaseName" value="..."/>
            <prop key="user" value="..."/>
            <prop key="password" value="..."/>
          </props>
        </argument>
      </bean>
    </argument>
    <argument>
      <set>
        <value>com.greysphere.app.Foo</value>
        <value>com.greysphere.app.Bar</value>
      </set>
    </argument>
  </bean>
 
</blueprint>

With this, the regular annotation introspection process occurs, producing a working SqlSessionFactory which can safely be injected into more interesting, domain-specific classes like repositories. And I can get back to real work.