尽管Java到目前为止已经发布了版本13,但是有许多生产安装都与Java 8一起运行。作为专家,即使是最近几天,我也多次开发Java 8代码,我必须为这不是Java 6而感到高兴。另一方面,作为开放源代码开发人员,我可以自由使用Java 11、12甚至13开发Java代码。 确实如此。
但是,另一方面,我希望使用我的代码。 开发诸如License3j或Java :: Geci之类的工具,这是一种发布与Java 11兼容的字节代码的库,切断了所有可能使用这些库的基于Java 8的应用程序。
我希望这些库可以从Java 8获得。
一种解决方案是在Git存储库中保持两个分支平行,并拥有Java 11+和Java 8版本的代码。 这是我对Java :: Geci 1.2.0版本所做的工作。 这很麻烦,容易出错,并且需要很多工作。 我之所以拥有这些代码,是因为我的儿子也是Java开发人员,他的职业生涯始于他的志愿工作。
(不,我没有向他施加压力。他的英语说和写的都比我好。他会定期审阅这些文章,以解决我的语言破裂问题。如果他对压力有不同的看法,则可以在此处随意添加任何注释,直到结束为止。括号,我不会删除或修改它。注意:)
NOTE:
和)
之间的任何内容都是他的意见。
另一种可能性是使用Jabel 。
在本文中,我将介绍如何在Java :: Geci项目中使用Jabel。 Jabel的文档很简短,但仍然很完整,对于更简单的项目,它的工作原理确实如此。 例如,对于Licenese3j项目,我实际上只需要向pom.xml
添加几行。 对于一年来开发的更复杂的项目,而没有考虑对Java 8兼容性的任何妥协,则要复杂一些。
关于贾贝尔
Jabel是一个开源项目,可从https://github.com/bsideup/jabel获得 。 如果您有Java 9+项目源,则可以将Jabel配置为编译过程的一部分。 它是一种注释处理器,它可以参与编译过程,并且可以使编译器接受各种技巧,从而可以接受Java 9+特性(因为Java 8具有这些特性)。编译器可以工作并生成Java 8,Jabel不会干扰字节码的生成。 ,因此这是真实的,因为它可以新鲜而热情地出现在Java编译器之外。 它仅指示编译器在编译代码时不要迷恋Java 9+功能。
该项目的GitHub页面上很好地说明了它的工作方式以及为什么可以工作。 我在上面写的内容可能甚至不够精确。
移植问题
使用针对Java 8 JVM的Java 9+功能创建Java代码时,我们不仅要关注字节码版本。 使用Java 8 JVM执行的代码将使用Java 8版本的JDK,如果我们碰巧使用了某些在那里不可用的类或方法,则该代码将无法运行。 因此,我们有两个任务:
- 配置构建以使用Jabel生成Java 8字节码
- 消除Java 8中不可用的JDK调用。
配置构建
在这里我不会描述如何使用Maven将Jabel配置为构建的一部分。 它记录在网站上,非常简单。
在Java :: Geci的情况下,我想要一些不同的东西。 我想要一个可用于创建Java 8和Java 11目标的Maven项目。 我之所以这样,是因为我希望Java :: Geci像以前一样支持JPMS,并且还为在Java 11或更高版本上运行的那些项目创建最新的字节码(例如,类嵌套而不是桥接方法)。
第一步,我创建了一个名为JVM8
的配置文件。 Jabel仅配置为仅在此配置文件处于活动状态时运行。
此配置文件还将发布设置为
<release>8< /release >
因此,当编译器第一次看到module-info.java
文件时,吓坏了。 幸运的是,我可以排除JVM8
概要文件中POM文件中的文件。 我还排除了javax0/geci/log/LoggerJDK9.java
,稍后再讨论。
我还尝试使用Maven自动将版本号配置为具有-JVM8
后缀(如果它与JVM8
配置文件一起运行),但无法实现。 Maven是一种多功能工具,可以完成许多事情,如果是一个简单的项目,则应该这样做。 对于Java :: Geci,我无法执行此操作,因为Java:Geci是一个多模块项目。
多模块项目相互引用。 至少子模块引用父模块。 子模块的版本可能与父模块的版本不同。 这是合乎逻辑的,因为它们的演变和发展并不一定要结合在一起。 但是,通常是这样。 在像Java :: Geci这样的项目中,它具有七个子模块,每个子模块的版本号与父模块相同,子模块可以从父类继承所有参数,依赖项,编译器选项等,但版本除外。 它不能继承版本,因为它不知道从哪个父版本继承。 这是一个陷阱22。
Java :: Geci开发通过使用Jamal预处理程序来维护八个pom.xml
文件来解决此问题。 每当构建配置发生更改时,都必须在pom.xml.jam
文件之一或包含的*.jim
文件之一中对其进行编辑,然后命令行mvn -f genpom.xml clean
将重新生成所有新的pom.xml
文件。 这也节省了一些重复的代码,因为预处理的Jamal文件不如相应的XML文件那么冗长。 这样做的代价是必须维护使用的宏。
Java :: Geci有一个version.jim
文件,其中包含项目的版本作为宏。 以Java 8发行版为目标时,必须将此文件中的版本更改为xyz-JVM8
并且必须执行命令mvn -f genpom.xml clean
。 不幸的是,这是我可能忘记的手动步骤。 创建Java 8目标后,我可能也忘记删除-JVM8
后缀。
为了减轻这种人为错误的风险,我开发了一个单元测试,以检查版本号与编译配置文件是否一致。 它标识了读取/javax0/geci/compilation.properties
文件的编译配置文件。 这是项目中由Maven过滤的资源文件,包含
projectVersion=${project.version} profile=${profile}
测试运行时,属性将替换为项目中定义的实际值。 project.version
是项目版本。 属性profile
在两个配置文件(默认和JVM8
)中定义为该配置文件的名称。
如果版本和配置文件不匹配,则测试失败。 遵循Java :: Geci的理念,当测试本身也可以修复错误时,测试不仅命令程序员修复“错误”。 它将修改version.jim
文件,使其包含正确的版本。 但是,它不会运行生成Jamal宏的pom文件。
因此,在第二次构建之后,我将通过一些手动编辑工作获得xyz
版本以及xyz-JVM8
版本的发行文件。
消除Java 8+ JDK调用
简单的通话
乍看之下,这是一项简单的任务。 您不得使用Java 8 JDK中没有的方法。 我们可以使用Java 8进行任何操作,因此这绝对是可能的任务。
例如每个
" " .repeat(tab)
必须消除。 为此,我创建了一个包含静态方法的类JVM8Tools
。 例如:
public static String space( int n){ final StringBuilder sb = new StringBuilder( /*20 spaces*/ " " ); while ( sb.length() < n){ sb.append(sb); } return sb.substring( 0 ,n).toString(); }
被定义在那里,使用这种方法我可以写
space(tab)
而不是调用String::repeat
方法。 这部分很简单。
模仿
更加困难的是实现getNestHost()
方法。 Java 8中没有这样的东西,但是Java :: Geci的工具模块中包含的选择器表达式使您可以使用表达式,例如
Selector.compile( "nestHost -> (!null & simpleName ~ /^Map/)" ).match(Map.Entry. class )
检查是否在Map
内声明了Entry
类,这很简单。 即使在有人选择这样做的Java 8环境中使用此表达式也是有意义的,但我不想执行截肢手术以从Java :: Geci删除此功能。 它必须被实施。
该实现检查实际的运行时,并且如果该方法在JDK中存在,则它通过反射进行调用。 在其他情况下,它使用类的名称并尝试查找分隔内部类名称和封闭类名称的$
字符来模仿功能。 当使用不同的类加载器加载同一类结构的多个实例时,在极少数情况下,这可能导致错误的结果。 我认为像Java :: Geci这样的工具可以使用它,在执行单元测试时几乎不会发生这种情况。
Class#getNestHost
反射方式调用Class#getNestHost
方法还有一个速度缺陷。 如果有真正的需求,我决定修复它。
记录支持
最后一个问题是日志记录。 Java 9引入了一个日志外观,强烈建议库使用它。 在Java环境中,日志记录是一个长期存在的问题。 问题不在于没有任何东西。 恰恰相反。 太多了 有Apache Commons Logging,Log4j,Logback和JDK内置的Java util日志记录。 一个独立的应用程序可以选择它使用的日志记录框架,但是如果一个库使用了一个不同的库,那么即使不是不可能,也很难将不同的日志消息集中到同一流中。
Java 9因此引入了一个新的Facade,库可以使用它发送日志,应用程序可以将输出通过Facade传递到所需的任何日志框架。 Java :: Geci使用此外观,并通过它为生成器提供日志记录API。 如果是JVM8环境,则不可能。 在这种情况下,Java :: Geci将日志消息传递到标准Java记录器中。 要做到这一点有一个新的界面LoggerJDK
由两个类实现LoggerJVM8
和LoggerJDK9
。 如果目标是Java 8,则后者的源代码将从编译中排除。
实际的记录器尝试通过反射获取javax0.geci.log.LoggerJDK9#factory
。 如果存在,则可以使用Java 9日志记录。 如果不存在,则记录器将返回工厂到javax0.geci.log.LoggerJVM8#factory
。 这样,通过反射仅调用记录器工厂,每个反射器仅发生一次。 日志记录本身已简化,并使用目标日志记录而没有任何反射,因此不会影响速度。
带走
可以在大多数库项目中支持Java 8,而不会做出无法接受的妥协。 我们可以从同一源创建两个不同的二进制文件,这些二进制文件支持两个不同的版本,而支持Java 9及更高版本的版本不会“受苦”旧的字节码。 有一些妥协。 您必须避免调用Java 9+ API,并且在绝对需要的情况下,您可以提供一个后备解决方案,并且可以提供基于反射的运行时检测解决方案。
翻译自: https://www.javacodegeeks.com/2019/11/supporting-java-8.html