如何为不同的Java版本支持不同版本的主(和测试)源集(6,7,8)

时间:2014-10-04 17:24:56

标签: java maven java-8 conditional-compilation

我有一个Java项目库。我想实现,测试并可能发布该项目的几个版本,意图与不同的Java版本一起使用:6,7,8。

最简单的方法就是复制粘贴项目并支持多个源树,但我想避免这种情况,因为它很乏味且容易出错。

另一种可能的方法是考虑" base"项目,以及几个特定于Java版本的项目。版本略有不同,但我不知道在类层次结构中反映这一技术发展问题的内容。

所以我正在寻找

  • 一种预编译器
  • 和/或标准Maven选项
  • 和/或Maven插件

可以帮助从单个源树支持几个特定于Java版本的库,并且对lib用户透明地支持。

8 个答案:

答案 0 :(得分:8)

您可以从单个pom.xml文件为每个Java版本(6,7,8)生成一个jar。

所有相关工作都在maven-compiler-plugin:compile mojo。

进行

诀窍是执行mojo 3次,每次将结果文件写入不同的outputFileName。这将导致编译器多次运行,每次使用不同的版本并吐出适当命名的文件。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <executions>
        <execution>
            <id>compile-1.6</id>
            <goals>
                <id>compile</id>
            </goals>
            <phase>compile</phase>
            <configuration>
                <source>1.6</source>
                <target>1.6</target>
                <executable>${env.JAVA_HOME_6}/bin/javac</executable>
                <outputFileName>mylib-1.6.jar</outputFileName>
            </configuration>
<!-- START EDIT -->
            <dependencies>
                <dependency>
                    <groupId>com.mycompany</groupId>
                    <artifactId>ABC</artifactId>
                    <version>1.0</version>
                </dependency>
            </dependencies>
<!-- END EDIT -->
        </execution>
        <execution>
            <id>compile-1.7</id>
            <phase>compile</phase>
            <goals>
                <id>compile</id>
            </goals>
            <configuration>
                <source>1.7</source>
                <target>1.7</target>
                <executable>${env.JAVA_HOME_7}/bin/javac</executable>
                <outputFileName>mylib-1.7.jar</outputFileName>
            </configuration>
<!-- START EDIT -->
            <dependencies>
                <dependency>
                    <groupId>com.mycompany</groupId>
                    <artifactId>XYZ</artifactId>
                    <version>2.0</version>
                </dependency>
            </dependencies>
<!-- END EDIT -->
        </execution>

        <!-- one more execution for 1.8, elided to save space -->

    </executions>
</plugin>

希望有所帮助。

修改

RE:额外要求每次运行针对不同来源进行编译。

请参阅上面对pom片段的编辑。

每个执行都可以定义自己的依赖库列表。

所以JDK6构建取决于ABC.jar,但JDK7依赖于XYZ.jar。

答案 1 :(得分:4)

您可以通过为每个Java版本创建单独的配置文件来有条件地包含一些源目录。您可以使用配置文件名称运行maven,它将定义您要使用哪个版本的源。在这种情况下,所有常见的源都保留在src/main/java中,而依赖于Java版本的文件放在/src/main/java-X.X directories中。

<profiles>
    <profile>
        <id>java-6</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>build-helper-maven-plugin</artifactId>
                    <version>1.5</version>
                    <executions>
                        <execution>
                            <id>add-sources</id>
                            <phase>generate-sources</phase>
                            <goals>
                                <goal>add-source</goal>
                            </goals>
                            <configuration>
                                <sources>
                                    <source>${basedir}/src/main/java-1.6</source>
                                </sources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
    <profile>
        <id>java-7</id>
        (...)
                                    <source>${basedir}/src/main/java-1.7</source>
    </profile>
    <profile>
        <id>java-8</id>
        (...)
                                    <source>${basedir}/src/main/java-1.8</source>
    </profile>
</profiles>

你可以通过用属性替换硬编码的java-X.X来更加动态,你将与profile一起传递给maven。这将是:

    <profile>
        <id>conditional-java</id>
        (...)
                                    <source>${basedir}/src/main/java-${my.java.version}</source>
    </profile>
</profiles> 

稍后当你运行它时,你只需通过mvn -Pconditional-java -Dmy.java.version=1.6

这要求您将java版本相关文件放在不同的目录中。在IDE中针对特定版本的java进行开发时,只需将与java版本相关的目录标记为源文件夹(因为默认情况下,IDE将只识别src / main / java作为源目录)。

您可以将编译器级别传递给maven编译器插件的方式相同:

<project>
  [...]
  <build>
    [...]
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>${my.java.version}</source>
          <target>${my.java.version}</target>
        </configuration>
      </plugin>
    </plugins>
    [...]
  </build>
  [...]
</project>

答案 2 :(得分:3)

代码中嵌入了一种方法:来自log4j2源代码:

   //  JDK 1.1 doesn't support readResolve necessary for the assertion 
   if (!System.getProperty("java.version").startsWith("1.1.")) {
        assertTrue(obj == Level.INFO);
    }

您还可以使用https://github.com/raydac/java-comment-preprocessor并根据java版本设置变量来更改代码。虽然会在很少的地方做到这一点,因为它很难调试。或至少在运行动态代码之前打印日志,以便了解哪个版本/实际行有问题。

答案 3 :(得分:3)

为什么不收集每个版本的java中应该有所不同的方法,将它们包装在&#34;实用程序&#34;项目,你自己的主代码可以调用api,并在发布时添加你想要的实用工具?

类似的东西:

util-api.jar(主项目调用的方法)

util-1.6.jar(无论哪种实现适用,甚至&#34;无操作&#34;如果需要什么都不需要)

我已经成功完成了很多次,遇到了与现在类似的问题。

答案 4 :(得分:3)

Hibernate工作的人在决定迁移到基于Gradle的版本之前,已经解决了这个问题一段时间。

有关详细信息,请查看Gradle: why?

答案 5 :(得分:2)

对于这样的事情,我会

  • 将依赖JVM版本的文件移动到他们自己的包中
  • 或者,使用命名约定来标识特定于JVM的文件
  • 使用Ant而不是Maven进行编译。 Ant可以根据名称或位置轻松排除源文件。您还可以使用Ant轻松创建多个目标。
  • 如果库对所有JVM版本使用相同的API,我将为特定于JVM的类创建接口,然后根据JVM版本在运行时实例化相应的实现。

答案 6 :(得分:1)

您需要做的是定义基本API,然后将新版Java可用的功能添加为额外方法(可以从原始API方法调用)。这允许API的基本完整性从旧实现到新实现保持不变。

例如:

public void forEach(Consumer<T> c);

是您原始的API方法,其中Consumer是您的org.mylib.Consumer。您可以在Java 8中使用它执行两项操作。您可以保留旧的实现并为该方法添加新方法作为便捷方法,或者您可以添加新实现并添加后卫方法调用新方法。无论哪种方式,实现API的遗留代码都将保持有效。

便利方法示例:

// No need to change the old Java 6 style implementation
public void forEach(Consumer<T> c); 

default void forEach(java.util.function.Consumer<T> c){
    // Your Consumer needs a constructor for j.u.f.C
    Consumer<T> myC = new Consumer<T>(c);
    forEach(myC);
}

后卫方法示例:

// Consumer extends j.u.f.C so this is more specific and will override
default void forEach(Consumer<T> c){ 
    // All we need to call the other method is an explicit cast
    forEach((java.util.function.Consumer<T>) c);
}

//All new Java 8 style implementation here
public void forEach(java.util.function.Consumer<T> c);

显然,对于Java 7版本,您无法在接口上定义默认方法,因此您必须在抽象基类中实现这些方法,但概念几乎相同。从理论上讲,在Java 8中你不需要防御者,因为调用可以完全相同,因为你的Consumer的任何有效实例都是j.u.f.C的有效实例。

Java 8首次意味着将新方法添加到现有接口是完全有效的。但是,你不应该在Java 6和7之间拥有不同的API签名。

对于Java 6和7,只需避免源代码中的以下内容即可编译为:

  • 钻石运营商&lt;&gt;。它很方便,但不使用它意味着J6和J7的一个代码库
  • switch语句(几乎在所有情况下,如果你有一个switch语句,你可能应该有多个类和一个工厂代替)。
  • 尝试与 - 资源。它是我最喜欢的J7功能之一,但最后块仍然完全有效。
  • 多异常捕获。
  • J7 Path api - 旧的File API更适合遗留编码
  • Fork and Join API

我真的不知道为J6编写的任何代码如果为J8编译都无法在J8 JVM上运行,但我知道您可能希望库的新版本能够利用J8的改进,如流和lambdas。

您可能还想从Java 7查看Java Services提供程序api。它允许您在基本模块中定义API并将实现安装为JVM可以检测到的Java服务插件jar文件因为他们在你的应用程序的类路径中。然后,您可以在新功能可用时简单地定义新的服务插件罐。当然,这对您的Java 6实现没有帮助,但您可以使用J6 API作为基础,为J7 API添加服务附加功能,并在JVM更新时继续添加和替换服务。

答案 7 :(得分:1)

我在这里回答了Leventov关于要求明确演员的最后评论,并提供了这个建议。这可能是也可能不是不好的做法,但我发现在某些情况下我想要注入一些预处理或后期处理或者提供我自己的抽象层而不是其他人来定义我自己的接口非常有用。 s(例如,我们为Hibernate的Work类定义了这样的一个,这样如果Hibernate的API将来发生变化,我们的实现就不必 - 只有我们接口中的默认方法) ,使用相同界面的两个版本可能会让生活变得更轻松。

考虑一下:功能界面的优点在于它只有一个抽象方法,所以你可以传递lambda作为该方法的实现。

但是当你扩展一个你仍然希望拥有相同功能的接口(传递一个lambda,并在所有情况下工作)时,Java 8的另一个优点就在于:Defender方法。没有任何地方说你必须像父类一样留下SAME方法摘要。您可以将接口扩展为一种拦截器接口。所以你可以像这样定义你的MyConsumer:

public interface MyConsumer<T> extends Consumer<T> {
    default void accept(T t){ // Formerly the abstract functional method
        getConsumer().accept(t);
    }
    public Consumer<T> getConsumer(); //Our new abstract functional method
}

我们的接口不是将accept(T)定义为抽象方法,而是实现accept(T),并定义一个抽象方法getConsumer()。这使得lambda实例化MyConsumer不同于实例化j.u.f.Consumer所需的MyConsumer,并消除了编译器的类冲突。 然后,您可以定义实现Iterable的类,而不是实现扩展Iterable的自定义接口。

public interface MyIterable<T> extends Iterable<T> {    
    default void each(MyConsumer<? super T> action){
        //Iterable.super.forEach((Consumer<? super T>) action);
        //
        // Or whatever else we need to do for our special
        // class processing
    }       
    default void each(Consumer<? super T> action){
        if (action instanceof MyConsumer){
            each((MyConsumer<? super T>) action);
        } else {
            Iterable.super.forEach(action);
        }
    }
    @Override
    default void forEach(Consumer<? super T> action){
        each(action);   
    }
}

它仍然有一个forEach方法,允许它符合可迭代的iterface,但是你的所有代码都可以在所有版本的YOUR api中调用each()而不是forEach()。通过这种方式,您还可以对代码进行部分未来验证 - 如果基础Java api在几年后被弃用,您可以修改默认的each()方法以新方式执行操作,但在其他所有位置 all您现有的实施代码仍然在功能上正确

因此,当你调用api.each而不需要显式转换时,你只需将lambda传递给另一个方法......在MyConsumer中,该方法返回一个使用者,所以你的lambda非常简单,你只需添加lambda zero-arg构造函数到您之前的语句。 Consumer中的accept()方法接受一个参数并返回一个void,所以如果你定义它没有参数,Java知道它想要一个接口,它有一个不带参数的抽象方法,而这个lambda实例化MyConsumer。

api.each(()->System.out::println);

这个实例化j.u.f.Consumer

api.each(System.out::println);

因为原始抽象方法(accept)在那里并且已经实现,它仍然是Consumer的有效实例,并且在所有情况下都可以工作,但是因为我们调用了0-arg构造函数,所以我们可以使用它。我明确地将它作为我们自定义界面的一个实例。这样,我们仍然履行消费者的接口合同,但我们可以将我们的界面签名与消费者区分开。