英文:
Different Java code compilation based on Dependency
问题
我想编写一段Java代码,可以使用两种不同的依赖(或依赖的不同版本)来执行。具体来说,我指的是org.apache.poi。代码必须能够在版本=2和版本=3或org.apache.poi的系统上运行。
不幸的是,在版本2和3之间,一些接口发生了变化,代码必须稍微不同地构建,并且无法将两个系统都升级到相同的org.apache.poi版本。
所以我的问题是:
* 是否有办法编译代码以避免遇到编译错误?
* 是否有一种方法可以根据可用的org.apache.poi版本来执行正确的代码?
* 解决这个问题的适当方法是什么?
作为一种补充:
我正在构建一段代码,可以为提供不同版本接口的两个应用程序工作(依赖的Maven范围是provided)。
如果我在Maven中有这两个依赖,它会选择任何一个依赖,而IF条件将无法编译,因为所选依赖中没有`Cell.CELL_TYPE_STRING`或`CellType.STRING`。
我希望代码在插入应用程序的任何依赖时都能正常工作。
// 使用旧的poi接口进行操作
if (cell != null && cell.getCellType() == Cell.CELL_TYPE_STRING
&& cell.getRichStringCellValue().getString().trim().equals(cellContent)) {
return row;
}
// 使用新的poi接口进行操作
if (cell != null && cell.getCellType() == CellType.STRING
&& cell.getRichStringCellValue().getString().trim().equals(cellContent)) {
return row;
}
<details>
<summary>英文:</summary>
I want to write a piece of Java code which can be executed with 2 different kinds of dependencies (or version of a dependency). Namely speaking about org.apache.poi. The code must run on a system with version=2 as well as version=3 or org.apache.poi.
Unfortunately between the versions 2 & 3 some interfaces have changed, code must be build slightly different and there is no way to upgrade both system to the same org.apache.poi version.
So my questions are:
* Is there a way to compile the code with both versions to not run into compiler errors?
* Is there a way to execute the right code based on the available org.apache.poi version?
* What would be an appropriate approach to solve this issue?
As an amendment:
I'm building a code which shall work for two applications which provides an interface in different versions (maven scope of the dependency is provided).
If I have both dependencies in maven, it takes any of the dependencies and IF clauses will fail to compile as `Cell.CELL_TYPE_STRING` or `CellType.STRING` is not available in the chosen dependency.
And I would like to have the code working regardless of which dependency is plugged in the application.
// working with old poi interface
if (cell != null && cell.getCellType() == Cell.CELL_TYPE_STRING
&& cell.getRichStringCellValue().getString().trim().equals(cellContent)) {
return row;
}
// working with new poi interface
if (cell != null && cell.getCellType() == CellType.STRING
&& cell.getRichStringCellValue().getString().trim().equals(cellContent)) {
return row;
}
</details>
# 答案1
**得分**: 2
这可能是基于个人观点的,但似乎是合理的。
首先,您需要创建一个通用的接口,您将使用该接口来完成工作。
其次,您需要创建适配器类,实现该接口,并使用特定版本的POI库执行所需的工作。
第三,您将编写适配器工厂,该工厂将返回适配器的正确实例。适配器本身应该提供一个“isSupported”方法,该方法将基于当前加载的POI类的类型(通过反射检测 - 必须存在某些特定版本的类或其他标记)来检测是否可以使用给定的适配器。
然后,您将每个适配器放入单独的Maven模块中,这样每个模块可以独立编译(因此不会有类冲突)。每个模块将在“provided”范围中具有所支持适配器要使用的POI依赖版本。
要么模块在您的主模块中向工厂注册自身,要么工厂本身检测所有可用的适配器(类似于Spring中的@ComponentScan)。
然后,您将所有内容打包到单个应用程序包中。主模块将仅使用通用接口。总而言之,这将是一种可扩展的插件系统。
<details>
<summary>英文:</summary>
This i probably opinion based, but it seams legit.
First, you will have to create common interface that you will use to do your job.
Second, you will have to create adapter classes that implements that interface and will do required job using particular version of POI library
Third, you will write adapter factory that will return proper instance of adapter.
Adapter itself should provide "isSupported" method that will detect if given adapter can be used based on what kind of POI classes are currently loaded (detect by reflection - there must be some version specific classes or other markers)
Then you will put each adapter into separate maven module, so each module can be compiled independently (thus you will have no class conflicts). Each module will have POI dependency in "provided" scope in version that this adapter is going to support
Either module registers itself with the factory in your main module, or factory itself detects all adapters that are available (like @ComponentScan in Spring).
Then you will pack everything into single app bundle. Main module will use only common interface. All in all it will be kind of extensible plugin system
</details>
# 答案2
**得分**: 2
我不认为有一个单一的“最佳方式”。
尽管如此,在我们的几个共享公共库的应用中,我们遇到了类似的问题。我最终采用了[@Antoniossss][1]的变体,不过这个库本身并不使用依赖注入(父应用可能使用也可能不使用,但库本身不涉及)。
更具体地说,由于传递性依赖关系,我们的一些应用程序需要某个特定版本的Apache Lucene(例如7.x.y或更高版本),而其他应用程序则停留在较旧的版本(5.5.x)。
因此,我们需要一种方法,在我们的情况下使用Maven,来针对这些版本之一构建我们的库。
我们最终采用了以下原则:
- 我们共享一些代码,这些代码在所有版本的Lucene之间是通用的
- 我们针对每个目标版本的Lucene都有特定的代码,这些代码具有不兼容的API(例如包更改、不存在的方法等)
- 我们构建与支持的每个Lucene版本相对应的JAR,命名方案为`groupId:artifact-luceneVersion:version`
- 在使用该库的地方,直接访问Lucene API被替换为访问我们的特定类
例如,在Lucene v5中有一个`org.apache.lucene.analysis.synonym.SynonymFilterFactory`工具。在v7中,相同的功能使用`org.apache.lucene.analysis.synonym.SynonymGraphFilterFactory`来实现,即相同的包,但是不同的类。
我们最终提供了一个`com.mycompany.SynonymFilterFactoryAdapter`。在v5 JAR中,这个类`extends`了Lucene v5的类,而在v7或任何其他版本中也是如此。
在最终的应用程序中,我们始终实例化`com.mycompany`对象,它的行为与本机的`org.apache`类完全相同。
项目结构
-----------
由于构建系统是Maven,我们按照以下方式构建它
项目根目录
|- pom.xml
|-- 共享
|---|- src/main/java
|---|- src/test/java
|-- v5
|---|- pom.xml
|-- v7
|---|- pom.xml
根pom
-----------
根pom是一个经典的多模块pom,但它不声明`shared`文件夹(请注意,共享文件夹没有pom)。
<modules>
<module>v5</module>
<module>v7</module>
</modules>
共享文件夹
---------------
`shared`文件夹存储所有与版本无关的代码和测试。此外,当需要版本特定的类时,它不会针对此类的API编写代码(例如,它不会导入org.apache.VersionSpecificStuff),而是针对`com.mycompany.VersionSpecificStuffAdapter`编写代码。
此适配器的实现留给了特定版本的文件夹。
版本特定文件夹
----------------
v5文件夹在其artifact id中声明了它编译到的Lucene版本,并且当然将其声明为依赖
....
<artifactId>myartifact-lucene-5.5.0</artifactId>
....
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>5.5.0</version>
</dependency>
但真正的“诀窍”在于,它使用`build-helper-maven-plugin`为类和测试声明了一个外部源文件夹:请参阅下面如何将`shared`文件夹中的源代码导入,就好像它是该项目本身的一部分。
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>add-5.5.0-src</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>../shared/src/main/java</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-5.5.0-test</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>../shared/src/test/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
为了使整个实现正常工作,它在自己的源文件夹`src/main/java`中提供了适配器的实现,例如
package com.mycompany
public class VersionSpecificStuffAdapter extends org.apache.VersionSpecificStuff {
}
如果v5和v7包都以相同的方式执行,那么使用`com.mycompany.xxxAdapter`的客户端代码将始终编译,并且在幕后获取相应的库实现。
这是一种方法。您还可以按照先前的建议,定义全新的接口,并使您的库的客户端代码针对您自己的接口编写代码。这样做可能更清晰,但根据情况可能需要更多工作。
在您的编辑中,您提到引用了以不同方式定义的常量,例如`CellType.TYPE_XX`。
在版本特定的代码中,您可以创建另一个常量`MyCellType.TYPE_XX`,以一个稳定的名称复制实际的常量。
如果是枚举类型,您可以创建一个名为`CellTypeChecker`的工具,其中包含一个方法`isCellTypeXX(cell)`,这个方法将以特定于版本的方式实现。
v7文件夹
------------
它的结构几
<details>
<summary>英文:</summary>
I do not think there is a single "best way".
Nonetheless, we faced a similar issue in a few of our apps that share a common library. I ended up with a variant of [@Antoniossss][1]'s variant, except that the library itself does not use dependency injection (the parent app may or may not, but the library is free of it).
To be more specific, and due to transitive dependencies, some of our apps need a certain version of Apache Lucene (e.g. 7.x.y, or more) and other are stuck on older versions (5.5.x).
So we needed a way to build one of our lib against those versions, using maven in our case.
What we ended uses the following principles :
- We share some code, which is common between all versions of Lucene
- We have specific code, for each target version of Lucene that has an incompatible API (e.g. package change, non existing methods, ...)
- We build as many jars as there are supported versions of lucene, with a naming scheme such as `groupId:artifact-luceneVersion:version`
- Where the lib is used, direct access to the Lucene API is replaced by access to our specific classes
For exemple, un Lucene v5 there is a `org.apache.lucene.analysis.synonym.SynonymFilterFactory` facility. In v7 the same functionnality is implemented using `org.apache.lucene.analysis.synonym.SynonymGraphFilterFactory` e.g. same package, but different class.
What we end up with is providing a `com.mycompany.SynonymFilterFactoryAdapter`. In the v5 JAR, this class `extends` the Lucene v5 class, and respectively with v7 or any other version.
In the final app, we always instantiate the `com.mycompany` object, that behaves just the same as the native `org.apache` class.
Project structure
-----------
The build system being maven, we build it as follow
project root
|- pom.xml
|-- shared
|---|- src/main/java
|---|- src/test/java
|-- v5
|---|- pom.xml
|-- v7
|---|- pom.xml
Root pom
-----------
The root pom is a classic multimodule pom, but it does not declare the `shared` folder (notice that the shared folder has no pom).
<modules>
<module>v5</module>
<module>v7</module>
</modules>
The shared folder
---------------
The `shared` folder stores all non-version specific code and the tests. On top of that, when a version specific class is needed, it does not code against the API of this class (e.g. it does not import org.apache.VersionSpecificStuff), it does against `com.mycompany.VersionSpecificStuffAdapter`).
The implementation of this Adapter being left to the version specific folders.
Version specific folders
----------------
The v5 folder declares in its artifact id the Lucene version it compiles to, and of course declares it as a dependency
....
<artifactId>myartifact-lucene-5.5.0</artifactId>
....
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>5.5.0</version>
</dependency>
But the real "trick" is that it declares an external source folder for classes and tests using the `build-helper-maven-plugin` : see below how the source code from the `shared` folder is imported "as if" it was from this project itself.
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>add-5.5.0-src</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>../shared/src/main/java</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-5.5.0-test</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>../shared/src/test/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
For the whole implementation to work, it provides the Adapter implementations in its own source folder `src/main/java`, e.g.
package com.mycompany
public class VersionSpecificStuffAdapter extends org.apache.VersionSpecificStuff {
}
If both the v5 and the v7 package do it the same way, then client code using the `com.mycompany.xxxAdapter` will always compile, and under the hood, get the corresponding implementation of the library.
This is one way to do it. You can also, as already suggested, define your whole new interfaces and have clients of your lib code against your own interface. This is kind of cleaner, but depending on the case, may imply more work.
In your edit, you mention refering to constants that are not defined the same way, e.g. `CellType.TYPE_XX`.
In the version specific code, you could either produce another constant `MyCellType.TYPE_XX` that would duplicate the actual constant, under a stable name.
In case of an enum, you could create a `CellTypeChecker` util with a method `isCellTypeXX(cell)`, that would be implemented in a version specific way.
v7 folder
------------
It's pretty much the same structure, you just swap what changed between v5 and v7.
Caveats
------------
This may not always scale.
If you have hundreds of types you need to adapt, this is cumbersome to say the least.
If you have 2 or more libs you need to cross-compile against (e.g. mylib-poi-1.0-lucene-5.5-guava-19-....) it's a mess.
If you have final classes to adapt, it gets harder.
You have to test to make sure every JAR has all the adapters. I do that by testing each Adapted class in the shared test folder.
[1]: https://stackoverflow.com/a/64119170/2131074
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论