This rather long post analyses four solutions to the problem of using data sources, and more in general JNDI in Virgo, the 4th being the one I recommend and decided to use, which consists in leveraging Tomcat's built-in JNDI provider in Eclipse Virgo Server for Apache Tomcat.
If you are not interested in the reasons why I dropped the first three, jump directly to the fourth.
Even if the post is mostly focused on JDBC data sources, once Tomcat JNDI provider is exposed to the application it can be used for any type of resource, not only data sources.
Most of the credits for this solution go to my colleague Stefano Malimpensa.
1. JDBC data sources in OSGi
The most correct approach to obtain a JDBC data source in a pure OSGi enterprise application consists in using the OSGi JDBC Service (see the official OSGi JDBC specification). In Virgo, that means using Gemini DBAccess.However, in my humble opinion Gemini DBAccess is not an optimal solution for a number of reasons:
- Gemini DBAccess is currently available
only for Derby, and to use a different database you need to write your own implementation. Not a big issue but some extra effort anyway.
- To integrate DBAccess with EclipseLink you should probably use Gemini JPA, which is affected by a bug that prevents connection pooling
from working and is therefore not usable in production https://bugs.eclipse.org/bugs/show_bug.cgi?id=379397
- When using DBAccess with Gemini JPA you need configure
connection parameters in each bundle's persistence.xml. I find this inconvenient because it is necessary to repackage the bundles every time the
connection parameters change, and due to the modular nature of OSGi one complex application may include several bundles with persistence units.
- If DBAccess is used without GeminiJPA, DBAccess will provide only a data source factory, and it will be the responsibility of your code to instantiate and configure the data source (e.g. pass in user name, password etc). In such case you would need to support a configuration file to let system administrators easily change connnection parameters, which is again extra effort.
- DBAccess requires the OSGi registry, which means it would not work with legacy code or third party libraries written for J2EE.
2. Full JNDI in OSGi
At this point you may want to try Gemini Naming, which implements the OSGi JNDI service specification. Even Gemini Naming is in my humble opinion not an optimal solution:- There is no configuration console, nor a configuration file: the only way you can bind resources in the JNDI namespace is programmatically. This implies a lot of boring initialisation code, and you probably need to support a configuration file to let system administrators easily change configuration parameters, which is again extra effort.
- Gemini Naming requires the OSGi registry, which means it would not work with legacy code or third party libraries written for J2EE.
3. Local JNDI declared inside a Web App
Virgo supports JNDI lookups for data sources inside a Web App. To achieve this you have to:
- Include in the Web application the JDBC driver(s) and the pool implementation (e.g. Apache DBCP or Tomcat JDBC) as jars in your WEB-INF/lib folder
- Configure web.xml to list the usual JNDI resource-refs
- Include a Tomcat context.xml file in the Web App and configure it as explained here and here
- You must include the JDBC driver and pool in every Web App of yours. This means that each Web App will have its own pool, even if they connect to the same database, and that you must repackage the WAR if you need to update the JDBC driver
- JNDI lookup will work only in a thread originated by a HTTP request. This means that application bundles that are not WARs will not be able to obtain the data source via a JNDI lookup, unless their code is executed by a thread started by the Web container. In fact, the JNDI lookup will fail from threads created by Equinox: this is for example the case of code that observes OSGi framework lifecycle events (BundleListener) and need access the database when a bundle is installed or uninstalled.
4. Tomcat global JNDI registry
Another option is available, which is not affected by any of the above issues and limitations and it consists in using Tomcat's global JNDI support. You gain a general purpose well tested and well documented JNDI registry capable of deploying any type of resource, not only data-sources (can even be extended to support custom resource types http://tomcat.apache.org/tomcat-6.0-doc/jndi-resources-howto.html#Adding_Custom_Resource_Factories).
Please note that this solution will make the global JNDI namespace available, disabling the Web App java:comp/env context. In order words, you cannot use the java:comp/env prefix in your JNDI lookups. This should be an acceptable limitation, given that the java:comp/env prefix in any case would work only within a Web App and not from a plain OSGi bundle.
In order to use the Tomcat JNDI registry for data sources in Virgo the following mandatory steps are required:
- Create a bundle fragment for Catalina to extend Virgo's Tomcat with connection pooling support. The Virgo distribution contains in fact a stripped down version of Tomcat that removes the libraries required for JDBC pooling. Luckily enough, you can create a fragment to contribute the libraries back to Catalina. You just need to make sure that your fragments are placed in the bundle repository folder, not in pickup.
- Create a bundle fragment for Catalina to make the required JDBC driver(s) available to the server and the application
- Create a fragment that gets the JNDI context from Tomcat and that registers in the JVM a global InitialContextFactoryBuilder
- Edit tomcat-server.xml and define your global resources
All the above fragments are a convenient method for extending Tomcat/Catalina to use third party libraries whose Java packages were not originally imported by the Virgo bundles. For further details refer to this post by Glyn Normington, the project lead of Virgo. If you are working with Virgo his personal blog is a must read!
1. The pool bundle fragment
Here is a sample for Apache DBCP. The MANIFEST.MF below is added to the DBCP JAR, that's why there is no Bundle-Classpath. Mind the fragment host header.Manifest-Version: 1.0 Fragment-Host: com.springsource.org.apache.catalina Bundle-ManifestVersion: 2 Bundle-Name: Tomcat DBCP Bundle-SymbolicName: org.apache.tomcat.dbcp Bundle-Version: 7.0.27 Bundle-Vendor: apache Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Import-Package: javax.management, javax.management.openmbean, javax.naming, javax.sql, org.apache.juli.logging
2. The JDBC driver bundle fragment
Here is a sample for PostgreSQL. The MANIFEST.MF below is added to the PostgreSQL driver JAR, that's why there is no Bundle-Classpath. Mind the fragment host header.Manifest-Version: 1.0 Fragment-Host: com.springsource.org.apache.catalina Bundle-ManifestVersion: 2 Bundle-Name: PostgreSQL JDBC Driver Bundle-SymbolicName: org.postgresql.jdbc.catalina Bundle-Version: 9.2.1000 Bundle-Vendor: postgresql Bundle-RequiredExecutionEnvironment: JavaSE-1.6
3. JNDI bridge fragment
Create a fragment with the following MANIFEST.MF that includes the two classes below and place it in the repository folder.Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: JNDITomcatBridge Bundle-SymbolicName: jndi.tomcat.bridge Bundle-Version: 1.0.0.qualifier Bundle-Vendor: org.example Fragment-Host: com.springsource.org.apache.catalina;bundle-version="7.0.26" Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Import-Package: org.apache.catalina.mbeans;version="7.0.26"
The code below consists of two classes.
The first, GlobalJNDILifecycleListener, is a GlobalResourcesLifecycleListener subclass that downcasts the server instance to get the global JNDI context and that registers in the JVM NamingManager a custom InitialContextFactoryBuilder which wraps the JNDI context obtained from Tomcat.
Other options are of course possible, including doing everything in the custom implementation of InitialContextFactoryBuilder without subclassing the listener, but whatever approach you adopt, it is important to make sure that the invocation to NamingManager.setInitialContextFactoryBuilder occurs only once in the life of a Virgo server instance, because the method will fail and raise a runtime exception if it is called more than once.
import javax.naming.Binding; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.spi.NamingManager; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.Server; import org.apache.catalina.mbeans.GlobalResourcesLifecycleListener; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; public class GlobalJNDILifecycleListener extends GlobalResourcesLifecycleListener { private static final Log log = LogFactory.getLog(GlobalJNDILifecycleListener.class); @Override public void lifecycleEvent(LifecycleEvent event) { super.lifecycleEvent(event); if (Lifecycle.START_EVENT.equals(event.getType())) { Server server = (Server) event.getLifecycle(); Context ctx = server.getGlobalNamingContext(); ContextFactory factory = new ContextFactory(ctx); try { NamingManager.setInitialContextFactoryBuilder(factory); log.info("Published Global Naming as default InitialContext"); logJNDIEntries(ctx, null); } catch (NamingException e) { log.error("Naming Exception:", e); } } } private void logJNDIEntries(Context context, String prefix) throws NamingException { NamingEnumeration<Binding> namingEnumeration = context.listBindings(""); while (namingEnumeration.hasMoreElements()) { Binding binding = namingEnumeration.next(); String nameEntry = binding.getName(); String fullName = (prefix == null || prefix.equals("") ? nameEntry : prefix + "/" + nameEntry); String entryClassName = binding.getClassName(); if (Context.class.getName().equals(entryClassName)) { Context ctx = (Context) binding.getObject(); logJNDIEntries(ctx, fullName); } else { log.info("Found: " + fullName); } } } }
import java.util.Hashtable; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.spi.InitialContextFactory; import javax.naming.spi.InitialContextFactoryBuilder; /** * Simple implementation of {@link InitialContextFactory} that returns the global {@link InitialContext} * obtained from Tomcat * * @author giamma, stefano * */ public class ContextFactory implements InitialContextFactoryBuilder, InitialContextFactory { private Context context; public ContextFactory(Context context) { this.context = context; } @Override public InitialContextFactory createInitialContextFactory(Hashtable environment) throws NamingException { return this; } @Override public Context getInitialContext(Hashtable environment) throws NamingException { return context; } }
4. tomcat-server.xml
Replace the listener with yours and declare your JNDI resources:
<-- Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> --> <Listener className="com.example.catalina.mbeans.GlobalJNDILifecycleListener"/>
<GlobalNamingResources> <Resource name="jdbc/db" type="javax.sql.DataSource" username="user" password="password" factory="org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory" driverClassName="org.postgresql.Driver" url="jdbc:postgresql://127.0.0.1/db" maxActive="20" maxIdle="10"/> </GlobalNamingResources>
You can now happily perform plain old new InitialContext().lookup() from every method of every class of your OSGi application deployed in Virgo, regardless of the origin of the thread.
This post is some months old, isn't yet any standard (and faster) way to get what your fourth alternative provides?
ReplyDeleteSorry for the late reply.
ReplyDeleteNo, the situation has not improved in the last few months. If you want "traditional" JNDI support that works in every thread the fourth option is the only viable solution.