Re: Pre-processing during build - Mailing list pgsql-jdbc

From Christopher BROWN
Subject Re: Pre-processing during build
Date
Msg-id CAHL_zcNyU=DUBQ6VY-TsQqcp02NBqdUNpggJQ60KVYLAGHrf=g@mail.gmail.com
Whole thread Raw
In response to Re: Pre-processing during build  (Sehrope Sarkuni <sehrope@jackdb.com>)
Responses Re: Pre-processing during build  (dmp <danap@ttc-cmc.net>)
Re: Pre-processing during build  ("Markus KARG" <markus@headcrashing.eu>)
List pgsql-jdbc
As has been said by Markus KARG and others, you CAN produce a driver using bytecode for Java-6 / JDBC-4.1 including Java-8 / JDBC-4.2 (and Java-7 / JDBC 4.1) types and method signatures.  I've used this technique in many production applications, and have tried out this specific case just now, as a check.

So, for example, if you use "javac" from Java-8 and even include the Java-8 (JDBC-4.2) definition of PreparedStatement, compiling with source=1.6 and target=1.6 options, your JDBC-4.2 implementation of PreparedStatement WILL load into Java-6 with JDBC-4.  You can even annotate your implementations with @Override, no problem.  You will NOT have a problem with clients that expect a Java-6 / JDBC-4 API because there is no way you can compile such a client to invoke a JDBC-4.2 method (it would be a compiler error).  Java only tries to resolve classes on-demand, that is when it runs a code branch in a method body that refers to a type or invokes a method with such a type as part of its signature, and NOT when loading or instantiating your class.  If you never call it, you'll never have a problem.

You WILL have problems however in the following (avoidable) cases:

- if your implementation of a JDBC-4 driver calls code that in turn refers to types, fields, or methods that depend on a more recent method of the Java API, for example :
  - static initialization
  - constructor calls to code that depends on a more recent API version
  - an implementation of a JDBC-4 method that calls a JDBC-4.1 or -4.2 method (typically method overloading with the noble intention of avoiding copying-and-pasting code)
  - as has been suggested, the safest workaround is to just use "extends" where appropriate, instance of generating code from templates
- use of reflection (or proxies) to examine classes or invoke methods
- use of BeansIntrospector

The problem is not in compiling, it's about ensuring that once client code invokes a JDBC-4 method, that your implementation of that method doesn't call in turn any code that it shouldn't.

Code coverage metrics are an additional guarantee but you'd have to be very sure you've got correct coverage for all version-dependant code paths.  Compiler constraints are probably safer ; I'll discuss that in a moment.

First, a few remarks concerning some of the previous posts :

https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-java.sql.SQLType- is actually implemented as a Java-8 "default" method.  You don't need to implement it directly in the driver.

https://gist.github.com/vlsi/aeeb4a61d9c2b67ad213 is a doomed-to-fail example, not due to bytecode versions but because Java uses reflection (see above list of problems) to find your "main" method, and so trips up on the method using Java-8 types.  Restructured as follows (two classes with separate source files), it works:

----8<---- Jre7Test.java ----8<----

public class Jre7Test {
    public static void main(String args[]) {
        System.out.println(Jre7TestCompanion.greeting());
    }
}

----8<---- Jre7TestCompanion.java ----8<----

import java.time.Duration;
import java.util.Optional;
 
public class Jre7TestCompanion {
    public static Optional<Duration> optional(java.time.Duration duration) {
        return Optional.of(duration);
    }
 
    public static String greeting() {
        return "Hello, world";
    }
}

----8<--------8<----

(the above compiled and run with the exact same commands on Mac OS X too).

The safest way is to use incremental compilation (all integrated into a single automated build, with no preference for build tool).  Using fictional package and class names to demonstrate the idea, here's how it could be done.

For example, produce an intermediate "pgjdbc_4.0.jar" using a "JDBC-4" package (1.6 API as bootstrap classpath for "javac", with 1.8 compiler and source/target 1.6):

jdbc_4_0.PGDriver_4_0
jdbc_4_0.PGPreparedStatement_4_0
jdbc_4_0.PGResultSet_4_0
...etc

Add the resulting "jar" to the classpath for the next step, with classes that extend the above, producing "pgjdbc_4.1.jar" (useless unless "pgjdbc_4.0.jar" is also in the classpath).  (1.7 API as bootstrap classpath for "javac", with 1.8 compiler and source/target 1.6)

jdbc_4_1.PGDriver_4_1 extends jdbc_4_0.PGDriver_4_0
jdbc_4_1.PGPreparedStatement_4_1 extends jdbc_4_0.PGPreparedStatement_4_0
jdbc_4_1.PGResultSet_4_1 extends jdbc_4_0.PGResultSet_4_0
...etc

Then add the both resulting "jar" to the classpath for the next step, with classes that extend the above, producing "pgjdbc_4.2.jar" (useless unless "pgjdbc_4.0.jar" is also in the classpath, along with "pgjdbc_4.1.jar"). (1.8 API as bootstrap classpath for "javac", with 1.8 compiler and source/target 1.6)

jdbc_4_2.PGDriver_4_2 extends jdbc_4_1.PGDriver_4_1
jdbc_4_2.PGPreparedStatement_4_2 extends jdbc_4_1.PGPreparedStatement_4_1
jdbc_4_2.PGResultSet_4_2 extends jdbc_4_1.PGResultSet_4_1
...etc

Then, merge all JARs into a single JAR.  Clients could then refer to the specific driver version they require in code, or use a generic Driver class that (in the constructor) detects the appropriate JDBC version and fixes a "final" int or Enum field, used thereafter in "switch" blocks to call the appropriate driver version, acting as a lightweight proxy when the specific driver version can't be referred to (for backwards compatibility).  More adventurous developers might even suggest usage of method handles from Java 7 onwards to eliminate the negligeable overhead of a switch statements, but I'd personally rely on the JVM to optimise that away. Note that this is only necessary for the Driver implementation, as no-one (apart from the driver implementors) should ever call "new PreparedStatement" or whatever.

Hope that helps ; hope it's not redundant with regards to messages sent since I started typing away my 2 cents...  In any case, I regularly use these techniques in production code with no accidents.

--
Christopher



On 17 June 2015 at 13:05, Sehrope Sarkuni <sehrope@jackdb.com> wrote:
On Wed, Jun 17, 2015 at 6:15 AM, Dave Cramer <pg@fastcrypt.com> wrote:
I'm not sure this is a great example as Optional itself is a java 8 construct.

Either way Spring is able to do this, as are others?
 
The approach used by Spring won't work for the JDBC driver. The crux of the issue is that the newest version of the JDBC spec include Java 8 types in method signatures of public interfaces that exist in Java . Spring doesn't do that.

The public interfaces and classes for the older JDK versions they support (i.e. 6 or 7) only expose types that exist in those JDK versions. For older classes they've added internal support for Java 8 types that is dynamically checked, but it's done by wrapping the integration in an inner class. Here's an example: https://github.com/spring-projects/spring-framework/blob/f41de12cf62aebf1be9b30be590c12eb2c030853/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java#L1041

There's no way to make that work when a public interface exposes classes that won't exist on the run time. It may have been possible with older upgrades to the JDBC spec (ex: 4 to 4.1) as there weren't any JDK 1.7-only classes used in methods signatures of existing public interfaces. Compiling with an older bytecode target would allow and older JDK to simply ignore those methods as they would not be part of the public signature.

In JDBC 4.2 that's not true though. For example the JDBC 4.2 PreparedStatement class has a new setObject(...) that uses a Java 8 only class:



That method signature can't appear in a driver that is going to be used in JDK 6 or 7. There's no way to hide it internally as it's part of the public signature.

We're going to need some kind of preprocessing step to handle things like this.

Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/


pgsql-jdbc by date:

Previous
From: Vladimir Sitnikov
Date:
Subject: Re: Pre-processing during build
Next
From: Dave Cramer
Date:
Subject: Help reviewing PR's