[ Team LiB ] Previous Section Next Section

24.3 Making a Connection Pool Available to Application Components

The next part of the puzzle is how to make the DataSource available to the application components that need it. In principle, there are two ways to do this. The first one—using an application scope variable—works in any type of web container, while the second one—using JNDI—is more flexible but only works in a container that supports J2EE style resource access.

24.3.1 Using an Application Scope Variable

One place for resources that all components in an application need access to is the application scope, corresponding to ServletContext attributes in the servlet world. As I described in Chapter 19, the most appropriate component for initialization and release of this type of shared resources is the application lifecycle listener.

The container informs an application lifecycle listener when the application is started and stopped. It can create the resource objects and make them available to other application components in its contextInitialized( ) method before any user requests are received, and release them when the application is shut down in its contextDestroyed( ) method. Finally, a listener can use configuration data (defined as context parameters in the deployment descriptor) to work in different settings. To recap, here's an application lifecycle listener similar to the one used in Chapter 19:

package com.ora.jsp.servlets;
  
import javax.servlet.*;
import javax.servlet.http.*;
import oracle.jdbc.pool.*;
  
public class ResourceManagerListener2 implements 
    ServletContextListener {
    private OracleConnectionCacheImpl ds = null;
  
    public void contextInitialized(ServletContextEvent sce) {
  
        ServletContext application  = sce.getServletContext(  );
        String jdbcURL = application.getInitParameter("jdbcURL");
        String user = application.getInitParameter("user");
        String password = application.getInitParameter("password");
        String maxLimit = application.getInitParameter("maxLimit");
  
        try {
            ds = new OracleConnectionCacheImpl(  );
            ds.setURL(jdbcURL);
            ds.setMaxLimit(Integer.parseInt(maxLimit));
            ds.setUser("scott");
            ds.setPassword("tiger");
        }
        catch (Exception e) {
            application.log("Failed to create data source: " + 
                e.getMessage(  ));
        }
        application.setAttribute("appDataSource", ds);
    }
  
    public void contextDestroyed(ServletContextEvent sce) {
        ServletContext application  = sce.getServletContext(  );
        application.removeAttribute("appDataSource");
        // Close the connections in the DataSource
        try {
            ds.close(  );
        }
        catch (java.sql.SQLException e) {}
        ds = null;
    }
}

In the contextInitialized( ) method, the JDBC URL, database user, and password, and the maximal number of connections to keep in the pool are read from the deployment descriptor and used to create and configure an instance of Oracle's DataSource implementation that provides pooling capabilities: oracle.jdbc.pool.OracleConnectionCacheImpl. I'm using only some of its features here, so you should also read Oracle's documentation if you plan to use it in your application. When the data source has been configured, it's saved as a servlet context attribute named appDataSource. To refresh your memory on the implementation and configuration details, you may want to take a look at Chapter 19 again.

An application component, such as a servlet, can pick up the DataSource registered by the listener like this:

ServletContext application = getServletContext(  );
DataSource ds = (DataSource) application.getAttribute("appDataSource");

Servlet context attributes appear to JSP as application scope variables, so you can also tell the JSTL database actions to use this DataSource by specifying it with an EL expression for the dataSource attribute:

<sql:query dataSource="${appDataSource}" ... />

If you want to make the DataSource the default used by the JSTL database actions, you must use the application scope variable name they expect, controlled by the javax.servlet.jsp.jstl.core.Config class described in Chapter 23:

public void contextInitialized(ServletContextEvent sce) {
  
        ServletContext application  = sce.getServletContext(  );
        String jdbcURL = application.getInitParameter("jdbcURL");
        String user = application.getInitParameter("user");
        String password = application.getInitParameter("password");
        String maxLimit = application.getInitParameter("maxLimit");
  
        try {
            ds = new OracleConnectionCacheImpl(  );
            ds.setURL(jdbcURL);
            ds.setMaxLimit(Integer.parseInt(maxLimit));
            ds.setUser("scott");
            ds.setPassword("tiger");
        }
        catch (Exception e) {
            application.log("Failed to create data source: " + 
                e.getMessage(  ));
        }
        Config.set(application, Config.SQL_DATA_SOURCE, ds);
    }

Using the Config class set( ) method guarantees that the implementation-dependent variable is set so that the JSTL actions find and use this DataSource by default. Other components in the application can access it through the Config class get( ) method:

import javax.servlet.jsp.jstl.core.Config;
...
ServletContext application = getServletContext(  );
DataSource ds = 
  (DataSource) Config.get(application, Config.SQL_DATASOURCE);

The listener also implements the contextDestroyed( ) method, called by the container before the application is shut down. In this method, the context attribute is removed, and all connections in the data source are closed. How to gracefully shut down a DataSource isn't defined by the JDBC specification, but for Oracle, you do it by calling the close( ) method on the OracleConnectionCacheImpl instance.

24.3.2 Using JNDI

J2EE defines an even more flexible way to make a DataSource, or any other shared resource, available through a Java Naming and Directory Interface (JNDI) service. Through JNDI, the connection pool is available to all parts of the application, even to components that don't have access to the servlet context. This should therefore be your first choice for resource sharing, unless you need to target containers that don't support JNDI. All J2EE-compliant application servers support JNDI, and many pure web containers (containers without EJB support), such as Tomcat, JRun, Resin, and ServletExec, provide resource access through JNDI even though the servlet and JSP specifications don't require it.

To use JNDI, you first define the resource in the web application deployment descriptor, using the <resource-ref> element:

<web-app ...>
  ...
  <resource-ref>
    <description>
      JNDI DataSource for example database
    </description>
    <res-ref-name>jdbc/Example</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
    <res-sharing-scope>Sharable</res-sharing-scope>
  </resource-ref>

The optional <description> element describes the resource and may be used to help the person that deploys the application.

The <res-ref-name> element is mandatory and must contain the unique name that the application components use to retrieve the resource, as you will see shortly. For a data-source resource, the J2EE specification recommends that you use the naming convention shown here, i.e., a name in the JNDI jdbc subcontext.

The type of the resource must be defined by the <res-type> element. It must be the fully qualified class name for the resource, and for a data source, it's always javax.sql.DataSource.

Next comes the <res-auth> element. It accepts one of two values: Container or Application. Container means that database account information needed to get connections from the data source must be provided to the container when the data source is registered as a JNDI resource, so the container can take care of authentication. Application means that the application will provide this information every time it gets a connection. This boils down to whether the application will call getConnection( ) (in the container-controlled authentication case) or getConnection(String username, String password) (in the application-controlled case). In most cases you want the container to take care of it.

The <res-sharing-scope> element is optional and accepts one of Sharable or Unsharable. This element tells the data source if it should return the same connection when being asked for one multiple times within the same transaction (if the transaction is controlled by the container or the Java Transaction API, JTA) or if it should return a unique connection each time. If you use only the JDBC transaction control methods, commit( ) and rollback( ), this element doesn't matter because the connections can never be shared. The default is Sharable, and that's fine for almost all cases.

Application components—servlets, custom actions, beans, or any other type of class used by the application—use the JNDI API to grab the DataSource and a Connection like this:

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
...
  
    Context ctx = new InitialContext(  );
    DataSource ds = 
        (DataSource) ctx.lookup("java:comp/env/jdbc/Example");
    Connection conn = ds.getConnection(  );

The InitialContext represents the entry point to the container's JNDI resource naming service. The lookup( ) method argument is the path for the DataSource. The first part, java:comp/env/ is the base for all J2EE resources, followed by the value declared by the <res-ref-name> element in the deployment descriptor. With the DataSource retrieved through JNDI, the application gets a Connection by calling getConnection( ) as usual.

When you use the JSTL database actions, you can specify a JNDI path as the data source, either as the corresponding configuration setting as described in Chapter 23 or as the dataSource attribute value:

<sql:query dataSource="jdbc/Example" ... />

The path must be the path relative to the J2EE base; in other words, the same value as you define with the <res-ref-name> element.

All I've said about how to declare the resource in the deployment descriptor and get access to it through JNDI is defined by the J2EE and servlet specifications. How to register a DataSource with a container's naming service, however, is a process that differs between containers. I'll show you how it's done for Tomcat, but you need to read the documentation to see how to do it for other containers.

For Tomcat, resource registration is done in the conf/server.xml file. This is the main configuration file for Tomcat. To register the JNDI resource, you must use a <Context> element to declare your web application explicitly in the conf/server.xml file (just placing it in Tomcat's webapps directory isn't enough in this case) and add a nested <ResourceParams> element to register and configure the JNDI DataSource factory for your application:

<Server port="8005" shutdown="SHUTDOWN" debug="0">
  ...
  <Service name="Tomcat-Standalone">
    ...
    <Engine name="Standalone" defaultHost="localhost" debug="0">
      ...
      <Host name="localhost" debug="0" appBase="webapps" 
        unpackWARs="true">
        ...
        <!-- Book examples context -->
        <Context path="/ora" docBase="ora">
          <ResourceParams name="jdbc/Example">
            <parameter>
               <name>factory</name>
               <value>com.ora.jsp.sql.DataSourceFactory</value>
            </parameter>
            <parameter>
               <name>dataSourceClassName</name>
               <value>oracle.jdbc.pool.OracleConnectionCacheImpl</value>
            </parameter>
            <parameter>
               <name>maxLimit</name>
               <value>2</value>
            </parameter>
            <parameter>
               <name>URL</name>
               <value>jdbc:oracle:thin:@voyager2:1521:Oracle9i</value>
            </parameter>
            <parameter>
               <name>user</name>
               <value>scott</value>
            </parameter>
            <parameter>
               <name>password</name>
               <value>tiger</value>
            </parameter>
          </ResourceParams>
        </Context>
        ...

The Tomcat server can be configured in many different ways, with or without some of the higher-level elements shown here. I'm using the default configuration, so the <Context> element is nested within a <Host> element that defines the base directory for all applications, and the <Context> element defines its base directory (ora in this example) relative to the host's base (webapps in this example) and its context path (/ora in this example). To learn more about the conf/server.xml file and all its elements, I suggest you read the Tomcat Server Configuration Reference, available at http://jakarta.apache.org/tomcat/tomcat-5.0-doc/config/index.html.

The <ResourceParams> element's name attribute must be set to the same name as you defined for the resource with the <res-ref-name> element in the deployment descriptor. With exception for factory and dataSourceClassName, the nested <parameter> elements set the parameter values supported by the specific DataSource you use. In this example I use some of the parameters supported by the Oracle OracleConnectionCacheImpl data source.

The factory parameter identifies the JNDI object factory Tomcat uses to create the data source object. A JNDI object factory is a class that implements the single method defined by the javax.naming.spi.ObjectFactory interface. Some JDBC drivers may bundle an object factory that produces data-source objects for the implementations included with the driver, but here I use a generic data source factory that I implemented for this book. It uses introspection to set the parameters for any DataSource implementation, for instance the Oracle connection pool data source in this example.

The object factory source code is shown in Example 24-3.

Example 24-3. A generic DataSource factory class
package com.ora.jsp.sql;
  
import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import javax.naming.*;
import javax.naming.spi.ObjectFactory;
  
public class DataSourceFactory implements ObjectFactory {
    public Object getObjectInstance(Object obj, Name name, 
        Context nameCtx, Hashtable environment)
        throws NamingException {
  
        System.out.println("Generic factory called");
        Reference ref = (Reference) obj;
        RefAddr ra = ref.get("dataSourceClassName");
        if (ra == null) {
            throw new NamingException("No class name specified");
        }
  
        String className = (String) ra.getContent(  );
        Object ds = null;
        try {
            ds = Class.forName(className).newInstance(  );
        }
        catch (Exception e) {
            throw new NamingException("Can't create DataSource: "
                + e.getMessage(  ));
        }
  
        Enumeration addrs = ref.getAll(  );
        while (addrs.hasMoreElements(  )) {
            RefAddr addr = (RefAddr) addrs.nextElement(  );
            String prop = addr.getType(  );
            String value = (String) addr.getContent(  );
            if (!(prop.equals("dataSourceClassName") || 
                prop.equals("scope") ||
                prop.equals("auth") || prop.equals("factory"))) {
                setProperty(prop, value, ds);
            } 
        }
        return ds;
    }
    ...
}

Tomcat calls the getObjectInstance( ) method the first time the application asks for the JNDI resource with the name the factory is registered for. The method creates an instance of the DataSource class specified by the dataSourceClassName parameter in the config/server.xml file, calls all setter methods matching the parameters specified within the <ResourceParams> element, and returns the configured instance. A number of private methods, not shown here, use the Introspection API to find and call the setter methods for the parameters. The source code is bundled with the book examples, so you can look at these details at your leisure.

When you use JNDI, you must also place the JDBC driver classes in a directory that Tomcat itself can use: namely in the common/lib directory if they are packaged in a JAR file, otherwise in common/classes. Classes in the WEB-INF/lib and WEB-INF/classes directories are available only to the application, not the container, so they are no good in this case. The same goes for the factory class. The factory class shown in Example 24-3 is part of the oraclasses_2_0.jar file, located in the WEB-INF/lib directory for the book examples application. To use this factory, you must move the JAR file to the common/lib directory.

    [ Team LiB ] Previous Section Next Section