创建多版本支持的jar包

10 min read,created at 2024-07-21
java多版本javax

1 版本兼容的谎言

java一直以来有很臃肿的历史包袱,因为他需要兼容老版本,所以有很多设计是放不开手脚的。但是即使这样仍然还是有很多版本的不兼容。

比如这段代码

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * @author Frank
 * @date 2024/7/21 16:57
 */
@XmlRootElement
public class Main {
    @XmlElement
    int num = 0;

    public static void main(String[] args)throws Exception {
        JAXBContext context = JAXBContext.newInstance(Main.class);
        Marshaller marshaller = context.createMarshaller();
        marshaller.marshal(new Main(), System.out); // 输出到控制台
    }
}

通过java8运行没问题,但是java9+就会报错。

# java8 正常运行
$ "C:\Users\sunwu\.jdks\corretto-1.8.0_412\bin\javac.exe" Main.java
$ "C:\Users\sunwu\.jdks\corretto-1.8.0_412\bin\java.exe" Main
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><main><num>0</num></main>

# java11报错,包不存在
$ "C:\Program Files\Eclipse Adoptium\jdk-11.0.17.8-hotspot\bin\javac.exe" Main.java
Main.java:1: 错误: 程序包javax.xml.bind不存在
import javax.xml.bind.JAXBContext;
                     ^
Main.java:2: 错误: 程序包javax.xml.bind不存在
import javax.xml.bind.Marshaller;
                     ^
Main.java:3: 错误: 程序包javax.xml.bind.annotation不存在
import javax.xml.bind.annotation.XmlElement;
                                ^
Main.java:4: 错误: 程序包javax.xml.bind.annotation不存在
import javax.xml.bind.annotation.XmlRootElement;
                                ^
......
9 个错误

2 javax的迁移

这个问题本质不是java的兼容性导致的,而是jdk的改动导致的,jre11确实可以完全兼容java8代码的运行,但是jdk11却不能兼容jdk8

jdk9的一些主要变动,围绕javax下的几个包展开的:

  • JAXB xml的序列化和反序列化的包,javax.xml.bind.XXX,这个包从jdk移除了,成为一个外部库,需要自己从maven下载。
  • JAX-WSJAXB有点交集,他是XML WebScervice的库,同样成为外部库了。
  • JAVA-EE 部分功能移除到外部库Jakarta EE,例如javax.servlet
  • JAVA-Mail 移除到外部库,javax.mail.XX

下次看到报错javax.XXX或者javax.xml.xxx,找不到类之类的报错,立马要想到可能是jdk版本的原因导致的,这些基本都迁移到jakarta外部第三方库下了,如下。 jakarta的第三方库,可以从maven下载,例如jakarta.xml.bind-apijakarta.jws-apijakarta.mail,但是如果用这个库的话,包名不是javax.XXX,而是jakarta.XXX,所以需要修改代码,下面是几个依赖的例子,具体可以到maven上面去搜,javax或者jakarta关键字,缺少什么搜什么即可。

pom.xml
<dependencies>
    <!-- JAXB -->
    <dependency>
        <groupId>jakarta.xml.bind</groupId>
        <artifactId>jakarta.xml.bind-api</artifactId>
        <version>4.0.2</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jaxb</groupId>
        <artifactId>jaxb-runtime</artifactId>
        <version>4.0.2</version>
    </dependency>
    <!-- JAX-WS -->
    <dependency>
        <groupId>jakarta.jws</groupId>
        <artifactId>jakarta.jws-api</artifactId>
        <version>3.0.0</version>
    </dependency>
    <!-- JavaMail -->
    <dependency>
        <groupId>com.sun.mail</groupId>
        <artifactId>jakarta.mail</artifactId>
        <version>2.0.1</version>
    </dependency>

    <!-- Annotation API-->
    <dependency>
        <groupId>jakarta.annotation</groupId>
        <artifactId>jakarta.annotation-api</artifactId>
        <version>3.0.0</version>
    </dependency>

    <!-- Servlet API -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.1.0</version>
        <scope>provided</scope> <!-- Provided scope,表示在服务器中提供 -->
    </dependency>
</dependencies>

上面JAXB的例子就要改import

Main.java
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;

/**
 * @author Frank
 * @date 2024/7/21 16:57
 */
@XmlRootElement
public class Main {
    @XmlElement
    int num = 0;

    public static void main(String[] args)throws Exception {
        JAXBContext context = JAXBContext.newInstance(Main.class);
        Marshaller marshaller = context.createMarshaller();
        marshaller.marshal(new Main(), System.out); // 输出到控制台
    }
}

jakarta的包存在两个问题:

  • 1 新的版本都是基于java11编译,不能在java8中引入jakarta的包
  • 2 包名变了,javax->jakarta,旧项目改造的话,要改代码。

所以一般通过一些javax的第三方库,来兼容java8java11的项目,例如jaxb要使用javax开头的这个包,虽然他提示说已经迁移到jakarta了,但是还是得用javax兼容。

img

用这样两个版本,这俩库会间接依赖,把一共7个jar包引入进来。

pom.xml
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.1</version>
</dependency>

3 tools.jar

tools.jarjdk自带的jar包,里面包含了一些工具类,很多工具中都会使用tools.jar,然而在java8以及之前的版本,tools.jarjdk的lib目录下,如果要使用,需要自己手动引入该jar包。java9之后,tools.jar被移除,整合到jre环境中了

这是java8的目录结构:

jdk8/
├── bin/
├── lib/
│   ├── tools.jar
│   └── ...
├── jre/  // 包含 Java 运行环境
│   ├── bin/
│   └── lib/
└── ...

img

img

这是java11的目录结构:

jdk11/
├── bin/
├── lib/
│   ├── jrt-fs.jar
│   ├── ...
├── jmods/  // 新增的目录,.jmod文件包含模块信息
└── ...

jdk的各种模块,class文件等,都是放到了jmod格式的文件中,这个文件以模块名命名,包含了模块的所有信息,例如模块的依赖,模块的class文件等。而tools相关的也被放到jdk.attach等几个模块中了。

img

知道了这些背景之后呢,就引出了一个问题,如果我们项目中使用到了tools.jar,如何兼容java8java11呢?

如果只是java9+,那么不需要引入任何依赖,但是要判断是jdk而非jre即可,代码如下,这里java.lang.module是java9之后才有的,所以只能用java9+进行编译。

import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.util.Optional;

Optional<ModuleReference> moduleReference = ModuleFinder.ofSystem()
                .find("jdk.attach");
if (moduleReference.isPresent()) {
    System.out.println("当前环境包含jdk.attach模块,是jdk环境");
} else {
    System.out.println("当前非jdk环境");
}

另一种更简单的判断方式,是直接看类存不存在

try {
    Class.forName("javax.tools.ToolProvider");
    System.out.println("当前环境是 JDK");
} catch (ClassNotFoundException e) {
    System.out.println("当前环境是 JRE");
}

如果是java8环境,那么就需要自行引入tools.jar,在代码编写阶段,需要在pom依赖中,这样写,来手动引入。

<dependency>
	<groupId>com.sun</groupId>
	<artifactId>tools</artifactId>
	<version>1.8</version>
	<scope>system</scope>
	<systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>

但在运行时,不会一起打包,所以运行时还需要自己把tools.jar放到cp中。

$ java -cp /path/to/tools.jar:app.jar Main

编写代码时,都好说,代码中使用tools.jar/jdk.attach,然后在不同的java环境下都能简单的用java -jar app.jar来运行,如何实现。编写的java版本只能是低版本,所以选择java8来编写。

思路是:main方法启动的时候,就判断当前的java版本,如果是java9+,并且是jdk环境,则直接继续运行;如果是java8,则需要动态加载tools.jar,然后运行,动态加载的方式是新建一个ClassLoader,来同时加载tools.jar和当前的类,重新加载当前类,并运行main方法。

public class Main {
    public static void main(String[] args) throws Exception {
        if (javaVersion() < 9) {
            // 用自定义类加载器,加载tools.jar 然后重新运行main方法
            if (!Main.class.getClassLoader().toString().startsWith("ToolsClassLoader")) {
                ToolsClassLoader toolsClassLoader = new ToolsClassLoader(
                    new URL[]{toolsJarUrl(), currentUrl()},
                        ClassLoader.getSystemClassLoader().getParent()
                );
                Class<?> mainClass = Class.forName(Main.class.getName(), true, toolsClassLoader);
                Method mainMethod = mainClass.getMethod("main", String[].class);
                mainMethod.invoke(null, (Object) args);
                return;
            }
        }
        if (!toolsLoaded()) {
            throw new RuntimeException("tools.jar not loaded, make sure jdk env is used");
        }
        //....继续运行即可
    }
    public static int javaVersion() {
        String v = System.getProperty("java.version");
        // 1.8以下是1.开头的
        if (v.startsWith("1.")) {
            return Integer.parseInt(v.substring(2, 3));
        }
        // 9以上就是数字本身了
        return v.split("\\.")[0];
    }
    public static boolean toolsLoaded() {
        try {
            Class.forName("javax.tools.ToolProvider");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }
    
    private static URL toolsJarUrl() throws Exception {
        String javaHome = System.getProperty("java.home");
        File toolsJarFile = new File(javaHome, "../lib/tools.jar");
        if (!toolsJarFile.exists()) {
            throw new Exception("tools.jar not found at: " + toolsJarFile.getPath());
        }
        URL toolsJarUrl = toolsJarFile.toURI().toURL();
        return toolsJarUrl;
    }

    private static URL currentUrl() throws Exception {
        ProtectionDomain domain = Attach.class.getProtectionDomain();
        CodeSource codeSource = domain.getCodeSource();
        return codeSource.getLocation();
    }
    
    public static class ToolsClassLoader extends URLClassLoader {
        public static String namePrefix = "ToolsClassLoader";

        public ToolsClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }

        public String toString() {
            return namePrefix + ":" + super.toString();
        }
    }
}

4 多版本jar包

因为jdk升级导致的一些不兼容,在高版本和低版本的写法可能是不一样的,就像上面的tools.jar中类的应用,但是上面情况比较友好的类名没有变化。如果是java8java9中的用法完全不同,那么就需要写两个版本的jar包,一个是java8的,一个是java9的,就很复杂,所以就有了多版本jar包。即在一个jar包中,塞入java8和java9的class文件,然后在运行阶段,自动根据java版本,加载不同的版本目录下的class文件。

jackson这个常用的库为例,他的META-INF目录下,有version.9目录,就是当java的运行时环境是9+的时候,会自动加载version.9目录下的class文件,这里只有module-info.class,其实就是当9+的时候,自己会作为一个模块,很多第三方库为兼容老版java和新的模块化特性都会这么做。

img