Re: Pre-processing during build

From: Christopher BROWN <brown(at)reflexe(dot)fr>
To: pgsql-jdbc(at)postgresql(dot)org
Subject: Re: Pre-processing during build
Date: 2015-06-17 12:24:24
Message-ID: CAHL_zcNyU=DUBQ6VY-TsQqcp02NBqdUNpggJQ60KVYLAGHrf=g@mail.gmail.com
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: 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(at)jackdb(dot)com> wrote:

> On Wed, Jun 17, 2015 at 6:15 AM, Dave Cramer <pg(at)fastcrypt(dot)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:
>
> PreparedStatment.setObject(int parameterIndex, Object x, SQLType
> targetSqlType):
> https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-java.sql.SQLType-
>
> SQLType (Java 8 only):
> https://docs.oracle.com/javase/8/docs/api/java/sql/SQLType.html
>
> 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/
>
>

In response to

Responses

Browse pgsql-jdbc by date

  From Date Subject
Next Message Dave Cramer 2015-06-17 13:10:24 Help reviewing PR's
Previous Message Vladimir Sitnikov 2015-06-17 12:03:30 Re: Pre-processing during build