Overcome J2SE 1.3-1.4 incompatibilities

Get help from the Reflection API and Ant

Java has added numerous APIs, like Java Database Connectivity (JDBC), to its standard set of libraries. This helps a wider audience adopt the APIs since optional packages don't need to be bundled with deployments. For groups writing implementations of these popular APIs, wider adoption makes their offerings more valuable. However, these groups might wish their APIs were still optional (as opposed to being included with Java's standard library set) when a newer Java version ships with an updated API that depends on classes and methods unavailable in previous Java versions. Suddenly you must maintain two versions of the implementation—one that complies with the old API and one that complies with the new API. This is exactly what happened with the JDBC API in Java 2 Platform, Standard Edition (J2SE) 1.4. Because of changes to the JDBC API, an implementation of java.sql.Connection cannot compile under both J2SE 1.3 and 1.4.

You might have found yourself in the same predicament as me: I needed to implement JDBC interfaces such as java.sql.Connection, but my code needed to compile under both J2SE 1.3 and 1.4. I didn't want to maintain different source files for J2SE 1.3 and 1.4, so I looked for a better solution.

Unfortunately, the famous Write Once, Run Anywhere (WORA) Java mantra does not include WOCA (Write Once, Compile Anywhere) if you rely on javac to do your compilation. Luckily, code tricks with the Reflection API and compile tricks with Ant can come to the rescue. I can have just one set of .java source files and Ant helps me compile it on both J2SE 1.3 and 1.4. Ant lets me modify the .java files on-the-fly to make changes appropriate for the Java version used for compilation. But before I can explain the full solution, I must explain the full problem.

Poor man's connection pool

Two years ago, my company needed a JDBC connection pool but wouldn't pay for one. At the time, we couldn't find a good free alternative, so we wrote an in-house connection pool. To better track how connections were being used throughout our applications, we created com.icentris.sql.ConnectionWrapper, which implements java.sql.Connection and some other wrapper classes that implement other java.sql interfaces. The wrapper classes only track database usage in our application and then pass through method calls to the real JDBC resource.

When J2SE 1.4 came along, we naturally wanted to migrate some of our clients to it so they could benefit from its many enhancements. But, of course, we still needed to support J2SE 1.3 for clients who saw no reason to upgrade. To our chagrin, ConnectionWrapper and our other JDBC wrapper classes would not compile on J2SE 1.4 without modification. To keep this article simple, I'll use ConnectionWrapper to demonstrate the techniques I applied to all classes that wouldn't compile under both J2SE 1.3 and 1.4. To comply with the updated JDBC API, I had to add several methods to ConnectionWrapper, which posed two big problems:

  1. Since my wrapper classes need to pass through method calls, I would have to call methods that don't exist in the J2SE 1.3 sql classes.
  2. Since some new methods rely on new classes, I would have to build in dependencies on classes that don't exist in J2SE 1.3.

Reflection to the rescue

Some code samples can best explain the first problem. Because my ConnectionWrapper wraps a java.sql.Connection, all my examples depend on the realConnection instance variable (in bold) set in the constructor:

  private java.sql.Connection realConnection = null;
  
  public ConnectionWrapper(java.sql.Connection connection) {
    realConnection = connection;
  }

To see what I would have done without incompatibility issues, let's consider setHoldability(int) (a new method to java.sql.Connection in J2SE 1.4):

  public void setHoldability(int holdability) throws SQLException {
    realConnection.setHoldability( holdability );
  }

Unfortunately, this code does not compile under J2SE 1.3 because java.sql.Connection doesn't have a setHoldability() method I can call under J2SE 1.3. But to compile under J2SE 1.4, I must have a setHoldability() method to properly implement the API. To solve this catch-22, I assumed my setHoldability() method would only be called under J2SE 1.4, so I could use the Reflection API to call the method:

  public void setHoldability(int holdability) throws SQLException {
    Class[] argTypes = new Class[] { Integer.TYPE };
    Object[] args = new Object[] {new Integer(holdability)};
    callJava14Method("setHoldability", realConnection, argTypes, args);
  }
  public static Object callJava14Method(String methodName, Object instance,
  Class[] argTypes, Object[] args)
    throws SQLException
  {
    try {
      Method method = instance.getClass().getMethod(methodName, argTypes);
      return method.invoke(instance, args );
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
      throw new SQLException("Error Invoking method (" + methodName + "): "
      + e);
    } catch (IllegalAccessException e) {
      e.printStackTrace();
      throw new SQLException("Error Invoking method (" + methodName + "): "
      + e);
    } catch (InvocationTargetException e) {
      e.printStackTrace();
      throw new SQLException("Error Invoking method (" + methodName + "): "
      + e);
    }
  }

Now I have a setHoldability() method, so I can compile under J2SE 1.4. I don't directly call the formerly nonexistent method on my java.sql.Connection, so I can compile under J2SE 1.3. My callJava14Method() method uses the Reflection API to call the method, then wraps any errors inside an SQLException since that is all I'm supposed to throw. I used this strategy for all the new J2SE 1.4 methods so my code would still work properly under 1.4, calling the wrapped method, yet compile under 1.3. Now I just needed to solve the second problem and find a way to depend on classes that do not exist in J2SE 1.3.

Ant is the answer

In J2SE 1.4, java.sql.Connection depends on a new class: java.sql.Savepoint. Since this new class resides in the java.sql package, you cannot add it to J2SE 1.3. Java does not allow any third-party additions to the set of core classes in the java.* or javax.* packages. So this was my challenge: use the new java.sql.Savepoint class to write my code so it works under J2SE 1.4, yet make sure the code compiles under J2SE 1.3 where that class doesn't exist. Simple, right? All who answered "Yes!" get a brownie point. Well, at least, it's simple now that I found the answer.

First, I included the following conditional import:

  // Comment_next_line_to_compile_with_Java_1.3
  import java.sql.Savepoint;

Then I found a way for Ant to comment that import when compiling under J2SE 1.3. Simplified, the key part of the Ant script is:

  <replace>
    <replacetoken>Comment_next_line_for_Java_1.3&#010;</replacetoken>
    <replacevalue>Comment_next_line_for_Java_1.3&#010;//</replacevalue>
  </replace>

This Ant <replace> tag has several options—you'll see more in my full example below—but the important part is that I search for <replacetoken> and replace it with <replacevalue>. The &#010; is the XML entity for "newline". When compiling under J2SE 1.4, Ant makes no change to the source file; and under J2SE 1.3, the import statement is commented:

  // Comment_next_line_to_compile_with_Java_1.3
  //import java.sql.Savepoint;

But I still have code in the body of my class that must depend on Savepoint:

  public Savepoint setSavepoint(String name) throws SQLException { . . .

Again, I only expect to use these new methods under J2SE 1.4, so they don't need to function under J2SE 1.3; they just need to compile. I discovered that if I have a Savepoint class in my package, my code can compile without an import statement. Yet when the import statement is uncommented (under J2SE 1.4 compilations), my Savepoint class is ignored because of the more specific import. So I created my own dummy class called com.icentris.sql.Savepoint, which (javadoc excluded) is probably the shortest valid class:

  package com.icentris.sql;
  /** Dummy class to allow ConnectionWrapper to implement java.sql.Connection
   * and still compile under J2SE 1.3 and J2SE 1.4. When compiled
   * under J2SE 1.3, this class compiles as a placeholder instead of the
   * missing java.sql.Savepoint (not in J2SE 1.3).  When compiled
   * under J2SE 1.4, this class is ignored and ConnectionWrapper uses the
   * java.sql.Savepoint that is new in J2SE 1.4.
   */
  public class Savepoint {}

Under J2SE 1.4, I can now properly import java.sql.Savepoint. Under J2SE 1.3, Ant comments the import line, so the Savepoint referred to in my code happens to be the dummy class sitting in the same package. Now I can add all the methods that reference Savepoint and still use the Reflection trick explained earlier:

  // Comment_next_line_to_compile_with_Java_1.3
  import java.sql.Savepoint;
 
  . . .
    public Savepoint setSavepoint() throws SQLException {
      Class[] argTypes = new Class[0];
      Object[] args = new Object[0];
      return (Savepoint) callJava14Method("setSavepoint", realConnection,
      argTypes, args);
    }
    public Savepoint setSavepoint(String name) throws SQLException {
      Class[] argTypes = new Class[] { String.class };
      Object[] args = new Object[] { name };
      return (Savepoint) callJava14Method("setSavepoint", realConnection,
      argTypes, args);
    }
    public void rollback(Savepoint savepoint) throws SQLException {
      Class[] argTypes = new Class[] { Savepoint.class };
      Object[] args = new Object[] { savepoint };
      callJava14Method("rollback", realConnection, argTypes, args);
    }
    public void releaseSavepoint(Savepoint savepoint) throws SQLException {
      Class[] argTypes = new Class[] { Savepoint.class };
      Object[] args = new Object[] { savepoint };
      callJava14Method("releaseSavepoint", realConnection, argTypes, args);
    }

Now all I need is my full Ant compile target to detect J2SE 1.3 and comment that import line on-the-fly when anyone attempts to use J2SE 1.3 to compile ConnectionWrapper:

  <target name="compile">
    <antcall target="undoJava13Tweaks" />
    <antcall target="doJava13Tweaks" />
    <javac srcdir="src" destdir="WEB-INF/classes" debug="on">
      <classpath>
        <fileset dir="WEB-INF/lib">
          <include name="*.jar"/>
        </fileset>
      </classpath>
    </javac>
    <antcall target="undoJava13Tweaks" />
  </target>
  <target description="Find out if we're being compiled on Java 1.3"
  name="isJava13">
    <echo message="java.specification.version=[${java.specification.version}]"/>
    <condition property="isJava13">
      <equals arg1="${java.specification.version}" arg2="1.3" />
    </condition>
  </target>
  <target description="There are a couple tweaks I have to do to compile under Java 1.3"
    name="doJava13Tweaks" depends="isJava13" if="isJava13">
    <echo message="This is Java 1.3, doing Tweaks!" />
    <replace dir="src/com/icentris/" summary="true">
      <include name="sql/ConnectionWrapper.java" />
      <replacetoken>Comment_next_line_for_Java_1.3&#010;</replacetoken>
      <replacevalue>Comment_next_line_for_Java_1.3&#010;//</replacevalue>
    </replace>
  </target>
  <target description="Let's undo Java 1.3 tweaks" name="undoJava13Tweaks">
    <replace dir="src/com/icentris/" summary="true">
      <include name="sql/ConnectionWrapper.java" />
      <replacetoken>Comment_next_line_for_Java_1.3&#010;//</replacetoken>
      <replacevalue>Comment_next_line_for_Java_1.3&#010;</replacevalue>
    </replace>
  </target>

Notice that the compile target calls undoJava13Tweaks both before and after doJava13Tweaks. This is in case the javac compilation target ever fails, and we have leftover replacements that didn't get cleaned up.

No need to maintain two implementations

It's common for a new API version to include new methods and new classes (or interfaces). This isn't a significant obstacle to implementers if they can implement the new API and still provide backwards-compatibility for older API users. However, when an API is part of the core Java classes, an API change can cause much more difficulty because Java does not allow any outside changes or additions in any java.* packages. Usually, this leads to a requirement to support different source trees for each version of the changed API.

However, as demonstrated above, you can compile one source tree on both Java versions. The Reflection API allows for calling methods that may not exist, and Ant allows imports to change on-the-fly to support dependencies on classes that may not exist under the compiling Java version. While the examples above are simplified for demonstration purposes, I have successfully used these and similar techniques to solve various problems associated with supporting both J2SE 1.3 and 1.4. Since so many Java APIs are constantly updated, you can use these techniques to help avoid the undesired requirement of maintaining two codebases.

1 2 Page 1
Page 1 of 2