Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I programmatically perform feature detection in Java?

I'd like to write a java framework, that supports JRE7 as a baseline, but takes advantage of JRE8 features, if being run in the context of a JRE8 (upwards compatible??).

(Or maybe I have this backwards... i.e. JRE8 is the baseline, but degrades gracefully to support JRE7).

Does Java provide a way to do this?

My attempt / example:

I thought I could address this in a similar manner to javascript feature detection, I could programmatically test for the presence of my Java8 method, if it exists programmatically invoke it, otherwise fallback to Java7 libraries.

IMO this is a very arduous approach. It'd be great if this "feature toggling/detection" could be handled by the Java Runtime Environment / Java Compiler. Is this possible, or am I taking crazy pills?

Disclaimer: I haven't addressed compilation, which would rule out the usefulness of this solution (if I compile with Java7, I can't add -parameters to the compiled classes).

public class Index {

    void tellme(String yourname) {
        /* ... */
    }

    public static void main(String[] args) throws Exception {
        Method tellme = Index.class.getDeclaredMethod("tellme", String.class);
        Method java8Params = null;
        try {
            java8Params = Method.class.getMethod("getParameters");
        } catch (NoSuchMethodException t) { /* ignore */ }
        if (java8Params != null) {
            // java 1.8 !!
            Object param = ((Object[]) java8Params.invoke(tellme))[0];
            System.out.printf("(java8) %s %s(%s %s){/*...*/}",
                    tellme.getReturnType(),
                    tellme.getName(),
                    param.getClass().getMethod("getType").invoke(param),
                    param.getClass().getMethod("getName").invoke(param));
        } else {
            // java 1.7 ...
            System.out.printf("(java7) %s %s(%s %s){/*...*/}",
                    tellme.getReturnType(),
                    tellme.getName(),
                    tellme.getParameterTypes()[0].getName(),
                    "arg0");
        }
    }
}
like image 358
Nick Grealy Avatar asked Jun 05 '15 05:06

Nick Grealy


1 Answers

First, it’s correct to resort to Reflection to ensure that there are no code dependencies between your general code and the optional API. Since there are different strategies allowed regarding when to resolve class and member references, non-reflective lazy creation that happens to work on one JVM may fail at another.

But this raises the problem that certain operations are hard to implement when every API usage has to be coded via Reflection, especially as you loose compile-time checking, and the runtime performance may suffer as well.

The general solution is to use the OO technology that Java offers from its very beginning. Create an interface or an abstract class defining the feature. Implement a Java 8 solution and a Java 7 fallback. During your application initialization or when the feature is needed the first time, you make one reflective check whether the feature, your optimal solution relies on, is available and if so, instantiate the optimal implementation, but instantiate the fall-back otherwise. From then on, you use the implementation via the interface like an ordinary object.

In simple cases you may reduce the number of classes to two, the base class defining the feature and providing the fall-back behavior, and the specialized subclass overriding the implementation with its specialized one.

Keep the Java 8 implementation in a separate source folder and compile them using a Java 8 compiler. The other code will be compiled using the Java 7 compiler ensuring that there are no dependencies to the Java 8 implementations.

General Java 7 compatible code:

import java.lang.reflect.Method;

public class Index {
  static final MethodDescription DESC_ACCESSOR;
  static {
    MethodDescription md;
    try {
      Method.class.getMethod("getParameters");// test JRE feature
      // instantiate specialized solution
      md=(MethodDescription) Class.forName("MethodDescriptionJ8").newInstance();
    } catch(ReflectiveOperationException|LinkageError ex) {
      md=new MethodDescription(); // J7 fall-back
    }
    DESC_ACCESSOR=md;
  }

  void tellme(String yourname) {
      /* ... */
  }

  public static void main(String[] args) throws Exception {
      Method tellme = Index.class.getDeclaredMethod("tellme", String.class);
      String desc=DESC_ACCESSOR.get(tellme);
      System.out.println(desc);
  }
}
class MethodDescription {// base class defining application specific feature
    public String get(Method tellme) {// and providing fall-back implementation
      return String.format("(java7) %s %s(%s %s){/*...*/}",
          tellme.getReturnType(),
          tellme.getName(),
          tellme.getParameterTypes()[0].getName(),
          "arg0");
    }
}

compile separately using Java 8:

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class MethodDescriptionJ8 extends MethodDescription {
    @Override
    public String get(Method tellme) {
      Parameter param = tellme.getParameters()[0];
      return String.format("(java8) %s %s(%s %s){/*...*/}",
                    tellme.getReturnType(),
                    tellme.getName(),
                    param.getType(),
                    param.getName());
    }
}

But note that regarding this specific feature the result might be disappointing. Parameter names are only available if the introspected class was compiled using Java 8 with the -parameters flag. Therefore, inspecting the Java 7 compatible classes won’t give you the parameter names, even when using the Java 8 methods at runtime.

like image 197
Holger Avatar answered Sep 29 '22 07:09

Holger