views:

343

answers:

8

I'm sure this is a dumb question, but.. We have the same Java source files and we want to use a different version of a Java API (jar file) depending on the client we are building our app for.

The newer version of the API has the methods setAAA() and setBBB() which we reference in our Java source:

if (...) {
  api.setAAA(a);
  api.setBBB(b);
}

This code will fail if compiled with the old API has the old API doesn't have these setters. Is there any way to conditionalize this code to only compile the setter lines if we are using the new API?

Thanks.

A: 

You can use java introspection. Look at the package :

java.lang.reflect

It has a class called Method. You can get all the public methods of a Class using :

Method[] methodList = obj.getClass().getMethods();

Since it is an API, the setters would be public. Then you can run through the array methodList and check for those methods which have the same name as the setters. If you find them, use them. Otherwise, you know this is an earlier version.

Also, most of the well developed API's have a function which returns the value of the current version of the JAR file.

For example :

String currentVersion = api.SomeClass.version() ;

Try to check if there is a function like that in the API you are using. This would be easier.

euphoria83
I don't think this would help, because even if the API version is detected the compiler will compliant when the version use hasn't the methods.
OscarRyz
i agree. this will work if u put the two versions of the user code in different conditional blocks based on the existence of the setters. I don't think Java supports conditional compilation.
euphoria83
+5  A: 

The safest approach is to fall back to the lowest version you need to support. That assumes all versions are backwards compatible which isn't necessarily the case.

If that solution isn't appropriate or desirable then I fall back to dependency injection. The Spring framework is by far the most popular and common DI framework but no means the only one. Guice is another one. You can even roll your own if it's undesirable to add a complete framework for this.

But I have problems envisioning a Java application--particularly a Web/J2EE application--that I do without using Spring. It's simply too useful.

Let's say there are 4 versions of a relevant jar. The API has changed twice in that time so you have 3 different API versions. You need to abstract use of that jar to an API of your own that is consistent across all these versions and then create three implementations of it: one for each different API version.

In Spring you create an application context, which defines all your beans and how they're injected into other beans. There is no reason you can't choose or build and application context as part of a build process. Often properties are used for this but you could also include part of the application context this way too.

The key point here is that even though the APIs are different you need to abstract away those differences as far as your code is concerned. If you don't you're just asking for trouble and it just gets messier.

cletus
+1  A: 

You could also keep separate branches of your version control system that holds the customer specific (i.e., version specific) code

John Ellinwood
+2  A: 

Java really wasn't meant for this conditional compilation (unlike C++), and it honestly sounds like a recipe for ending up in "classpath hell".

While you could manually start dealing with functions that return the version of your API, you then have a classfile that fits a specific version, but with no indications that it may be incompatible.

I've encountered this situation before (e.g., working with different versions of Eclipse) and it's not pretty. What I ended up doing is having an interface with two different implementations, one for each API, put each of them in a separate project (a plug-in in my case), and then tried to load them with a factory or an injection. Isolate them as well as you can.

Uri
+1  A: 

What I've done in the past is: As cleanly as possible write the minimum amount of code that interacts with version dependent aspects of the library. Have a version of this code for each version of the library. Have them all implement the same interface. The bulk of your application should try to (with Class.forName and possibly a little reflection for construction) dynamically load the version suitable for the latest library. If that fails, fall back to a statically linked version for the old library.

By appropriate use of sourcepath and classpath, you can arrange for your core code to be prevented from using the new library.

Tom Hawtin - tackline
A: 

You may try

  • Reflection based invocation or Code generation or the old preprocessing technique or

  • Strategy pattern to encapsulate what varies.

class ThirdPartyApi {
     void foo(){}  // available in all versions
     void bar(){}  // available only in new version
}

ThirdPartyApiV1 extends ThirdPartyApi {
     void foo() {
        thirdpartyV1Object.foo();
     }
}

ThirdPartyApiV2 extends ThirdPartyApi {
     void foo() {
        thirdpartyV2Object.foo();
     }
     void bar() {
        thirdpartyV2Object.bar();
     }
}

Use a DependencyInjection to inject the correct version of ThridPartyApi implementation. Otherwise use a ThirdPartyApiFactory to create the appropriate instance based on a configuration or system property value.

Manoj
Hey, whoever voted Manoj down should leave a comment to say why.
andersoj
A: 

I have had this same need, since we have code that needs to run on all versions of Java from Java 1.2, but some code needs to take advantage of newer API's if they are available.

After various permutations using reflection to obtain Method objects and invoking them dynamically, I have settled on a wrapper style approach as best, in general (although under some circumstances, just storing the reflected Method as a static and invoking it is better - it depends).

Following is an example "System Utility" class which exposes certain newer API's. This example uses a Singleton, but could easily instantiate multiple objects if the underlying API needed that.

There are two classes:

  • SysUtil
  • SysUtil_J5

The latter is the one used if the run-time JVM is Java 5 or later. Otherwise fallback methods which are compatible in contract are used from the default implementation in SysUtil which utilizes only Java 4 or earlier APIs. Each class is compiled with the specific version's compiler, so that there is no accidental usage of a Java 5+ API in the Java 4 class:

SysUtil (compiled with the Java 4 compiler)

import java.io.*;
import java.util.*;

/**
 * Masks direct use of select system methods to allow transparent use of facilities only
 * available in Java 5+ JVM.
 *
 * Threading Design : [ ] Single Threaded  [x] Threadsafe  [ ] Immutable  [ ] Isolated
 */

public class SysUtil
extends Object
{

/** Package protected to allow subclass SysUtil_J5 to invoke it. */
SysUtil() {
    super();
    }

// *****************************************************************************
// INSTANCE METHODS - SUBCLASS OVERRIDE REQUIRED
// *****************************************************************************

/** Package protected to allow subclass SysUtil_J5 to override it. */
int availableProcessors() {
    return 1;
    }

/** Package protected to allow subclass SysUtil_J5 to override it. */
long milliTime() {
    return System.currentTimeMillis();
    }

/** Package protected to allow subclass SysUtil_J5 to override it. */
long nanoTime() {
    return (System.currentTimeMillis()*1000000L);
    }

// *****************************************************************************
// STATIC PROPERTIES
// *****************************************************************************

static private final SysUtil            INSTANCE;
static {
    SysUtil                             instance=null;

    try                  { instance=(SysUtil)Class.forName("SysUtil_J5").newInstance(); } // can't use new SysUtil_J5() - compiler reports "class file has wrong version 49.0, should be 47.0"
    catch(Throwable thr) { instance=new SysUtil();                                                                    }
    INSTANCE=instance;
    }

// *****************************************************************************
// STATIC METHODS
// *****************************************************************************

/**
 * Returns the number of processors available to the Java virtual machine.
 * <p>
 * This value may change during a particular invocation of the virtual machine. Applications that are sensitive to the
 * number of available processors should therefore occasionally poll this property and adjust their resource usage
 * appropriately.
 */
static public int getAvailableProcessors() {
    return INSTANCE.availableProcessors();
    }

/**
 * Returns the current time in milliseconds.
 * <p>
 * Note that while the unit of time of the return value is a millisecond, the granularity of the value depends on the
 * underlying operating system and may be larger. For example, many operating systems measure time in units of tens of
 * milliseconds.
 * <p>
 * See the description of the class Date for a discussion of slight discrepancies that may arise between "computer time"
 * and coordinated universal time (UTC).
 * <p>
 * @return         The difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC.
 */
static public long getMilliTime() {
    return INSTANCE.milliTime();
    }

/**
 * Returns the current value of the most precise available system timer, in nanoseconds.
 * <p>
 * This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock
 * time. The value returned represents nanoseconds since some fixed but arbitrary time (perhaps in the future, so values
 * may be negative). This method provides nanosecond precision, but not necessarily nanosecond accuracy. No guarantees
 * are made about how frequently values change. Differences in successive calls that span greater than approximately 292
 * years (263 nanoseconds) will not accurately compute elapsed time due to numerical overflow.
 * <p>
 * For example, to measure how long some code takes to execute:
 * <p><pre>
 *    long startTime = SysUtil.getNanoTime();
 *    // ... the code being measured ...
 *    long estimatedTime = SysUtil.getNanoTime() - startTime;
 * </pre>
 * <p>
 * @return          The current value of the system timer, in nanoseconds.
 */
static public long getNanoTime() {
    return INSTANCE.nanoTime();
    }

} // END PUBLIC CLASS

SysUtil_J5 (compiled with the Java 5 compiler)

import java.util.*;

class SysUtil_J5
extends SysUtil
{

private final Runtime                   runtime;

SysUtil_J5() {
    super();

    runtime=Runtime.getRuntime();
    }

// *****************************************************************************
// INSTANCE METHODS
// *****************************************************************************

int availableProcessors() {
    return runtime.availableProcessors();
    }

long milliTime() {
    return System.currentTimeMillis();
    }

long nanoTime() {
    return System.nanoTime();
    }

} // END PUBLIC CLASS
Software Monkey
+1  A: 

You can compile to the lowest common denominator, then use reflection to call the method that is only available on later APIs. For example, supposing that on the class com.foo.Bar, the method "getFoogle()" was superseded in later versions of your API by the method "getFiggle()". And let's suppose the method (in either variant) takes an int and a double and returns an int. You make a wrapper call as follows:

public int getFoogleFiggle(Bar bar, int n, double d) {
  try {
    Class clz = Class.forName("com.foo.Bar");
    Method m = clz.getMethod("getFiggle", new Class[] {Integer.class, Double.class});
    return (Integer) m.invoke(bar, new Object[] {n, d});
  } catch (NoSuchMethodException nsme) {
    return getFoogle(n, d);
  } catch (various other spurious exceptions) {
    ... deal with in intesresting ways ...
  }
}

Note that at compile time, the compiler doesn't care whether or not the class coo.foo.Bar and/or the method getFiggle exist.

Neil Coffey