从 Web 服务器智能地提供 jar 文件

Intelligently serving jar files from a web server

我正在编写一个简单的(通用的)包装器 Java class,它将在与部署的 Web 服务器分开的各种计算机上执行。我想下载最新版本的 jar 文件,该文件是来自相关 Web 服务器(当前为 Jetty 8)的应用程序。

我有这样的代码:

// Get the jar URL which contains the application
URL jarFileURL = new URL("jar:http://localhost:8081/myapplication.jar!/");
JarURLConnection jcl = (JarURLConnection) jarFileURL.openConnection();

Attributes attr = jcl.getMainAttributes();
String mainClass = (attr != null)
            ? attr.getValue(Attributes.Name.MAIN_CLASS)
            : null;
if (mainClass != null)  // launch the program

除了 myapplication.jar 是一个大的 jar 文件(一个 OneJar jar 文件,里面有很多东西)之外,这很好用。我希望这尽可能高效。 jar 文件不会经常更改。

  1. jar 文件可以保存到磁盘吗(我看到如何获取 JarFile object,但不能保存它)?
  2. 更重要的是,但与#1 相关,jar 文件是否可以以某种方式缓存?

    2.1 我可以(轻松地)在 Web 服务器上请求 jar 文件的 MD5 并仅在更改后才下载它吗?
    2.2 如果没有,是否有另一种缓存机制,也许只请求Manifest? Version/Build 信息可以存储在那里。

如果有人做过类似的事情,你能详细描述一下该怎么做吗?

每个初始响应的更新

建议在请求中使用 If-Modified-Since header 并在 URL 上使用 openStream 方法来获取要保存的 jar 文件。

根据此反馈,我添加了一条重要信息和一些更有针对性的问题。

我上面描述的java程序运行是从引用的jar文件下载的程序。该程序将 运行 从大约 30 秒到 5 分钟左右。然后它完成并退出。有些用户可能每天多次 运行 这个程序(甚至最多 100 次),其他人可能 运行 每隔一周一次。它应该仍然足够聪明,知道它是否有最新版本的 jar 文件。

更有针对性的问题:

If-Modified-Since header 在此用法中仍然有效吗?如果是这样,我是否需要完全不同的代码来添加它?也就是说,你能告诉我如何修改提供的代码以包含它吗?关于保存 jar 文件的相同问题 - 最终我真的很惊讶(沮丧!)我可以获得 JarFile object,但无法保留它 - 我什至需要 JarURL连接 class?

赏金问题

我最初并没有意识到我想问的确切问题。是这样的:

如何在 command-line 程序中将 Web 服务器的 jar 文件保存到本地,该程序退出并仅在该 jar 文件在服务器上发生更改时更新?

任何通过代码示例展示如何完成的答案都将获得赏金。

  1. 可以,文件可以存盘,可以使用URL class.

    中的openStream()方法获取输入流
  2. 根据@fge 提到的评论,有一种方法可以检测文件是否被修改。

示例代码:

private void launch() throws IOException {
    // Get the jar URL which contains the application
    String jarName = "myapplication.jar";
    String strUrl = "jar:http://localhost:8081/" + jarName + "!/";

    Path cacheDir = Paths.get("cache");
    Files.createDirectories(cacheDir);
    Path fetchUrl = fetchUrl(cacheDir, jarName, strUrl);
    JarURLConnection jcl = (JarURLConnection) fetchUrl.toUri().toURL().openConnection();

    Attributes attr = jcl.getMainAttributes();
    String mainClass = (attr != null) ? attr.getValue(Attributes.Name.MAIN_CLASS) : null;
    if (mainClass != null) {
        // launch the program
    }
}

private Path fetchUrl(Path cacheDir, String title, String strUrl) throws IOException {
    Path cacheFile = cacheDir.resolve(title);
    Path cacheFileDate = cacheDir.resolve(title + "_date");
    URL url = new URL(strUrl);
    URLConnection connection = url.openConnection();
    if (Files.exists(cacheFile) && Files.exists(cacheFileDate)) {
        String dateValue = Files.readAllLines(cacheFileDate).get(0);
        connection.addRequestProperty("If-Modified-Since", dateValue);

        String httpStatus = connection.getHeaderField(0);
        if (httpStatus.indexOf(" 304 ") == -1) { // assuming that we get status 200 here instead
            writeFiles(connection, cacheFile, cacheFileDate);
        } else { // else not modified, so do not do anything, we return the cache file
            System.out.println("Using cached file");
        }
    } else {
        writeFiles(connection, cacheFile, cacheFileDate);
    }

    return cacheFile;
}

private void writeFiles(URLConnection connection, Path cacheFile, Path cacheFileDate) throws IOException {
    System.out.println("Creating cache entry");
    try (InputStream inputStream = connection.getInputStream()) {
        Files.copy(inputStream, cacheFile, StandardCopyOption.REPLACE_EXISTING);
    }
    String lastModified = connection.getHeaderField("Last-Modified");
    Files.write(cacheFileDate, lastModified.getBytes());
    System.out.println(connection.getHeaderFields());
}

我可以在我们的团队中分享解决相同问题的经验。我们有几个用 java 编写的桌面产品,它们会定期更新。

几年前,我们为每个产品和以下更新过程设置了单独的更新服务器:客户端应用程序有一个更新程序包装器,它在主逻辑之前启动,并存储在 udpater.jar 中。在开始之前,应用程序发送请求以使用 application.jar 文件的 MD5 散列更新服务器。服务器将收到的哈希值与它拥有的哈希值进行比较,如果哈希值不同,则将新的 jar 文件发送给更新程序。

但在许多情况下,我们混淆了现在正在生产的版本,以及更新服务器故障,我们切换到 continuous integration practice with TeamCity 之上。

开发人员所做的每一次提交现在都由构建服务器跟踪。编译和测试通过后,构建服务器将构建号分配给应用程序并在本地网络中共享应用程序分发。

更新服务器现在是一个具有特殊静态文件结构的简单网络服务器:

$WEB_SERVER_HOME/
   application-builds/
      987/
      988/
      989/
          libs/
          app.jar
          ...
          changes.txt  <- files, that changed from last build
      lastversion.txt  <- last build number

客户端更新程序通过 HttpClient 请求 lastversion.txt,检索最后的内部版本号并将其与存储在 manifest.mf 中的客户端内部版本号进行比较。 如果需要更新,更新程序会收集自上次更新以来所做的所有更改,遍历 application-builds/$BUILD_NUM/changes.txt 文件。之后,更新程序下载收获的文件列表。可能有 jar 文件、配置文件、其他资源等。

这个方案对于客户端更新器来说似乎很复杂,但在实践中它非常清晰和健壮。

还有一个 bash 脚本组成更新服务器上的文件结构。脚本每分钟请求 TeamCity 以获取新构建并计算构建之间的差异。我们现在还升级此解决方案以与项目管理系统(Redmine、Youtrack 或 Jira)集成。目的是让产品经理能够标记已批准更新的版本。

更新。

我已将更新程序移至 github,请在此处查看:github.com/ancalled/simple-updater

项目包含 Java 上的更新程序客户端、服务器端 bash 脚本(从构建服务器检索更新)和示例应用程序以在其上测试更新。

How can I save a jar file from a web server locally in a command-line program that exits and ONLY update that jar file when it has been changed on the server?

JWS。它有一个 API,因此您可以从现有代码控制它。它已经有了版本控制和缓存,并带有一个 JAR 服务 servlet。

我假设 .md5 文件在本地和 Web 服务器上都可用。如果您希望这是一个版本控制文件,同样的逻辑将适用。

以下代码中给出的 url 需要根据您的网络服务器位置和应用上下文进行更新。这是您的命令行代码的运行方式

public class Main {

public static void main(String[] args) {
    String jarPath = "/Users/nrj/Downloads/local/";
    String jarfile = "apache-storm-0.9.3.tar.gz";
    String md5File = jarfile + ".md5";

    try {
        // Update the URL to your real server location and application
        // context
        URL url = new URL(
                "http://localhost:8090/JarServer/myjar?hash=md5&file="
                        + URLEncoder.encode(jarfile, "UTF-8"));

        BufferedReader in = new BufferedReader(new InputStreamReader(
                url.openStream()));
        // get the md5 value from server
        String servermd5 = in.readLine();
        in.close();

        // Read the local md5 file
        in = new BufferedReader(new FileReader(jarPath + md5File));
        String localmd5 = in.readLine();
        in.close();

        // compare
        if (null != servermd5 && null != localmd5
                && localmd5.trim().equals(servermd5.trim())) {
            // TODO - Execute the existing jar
        } else {
            // Rename the old jar
            if (!(new File(jarPath + jarfile).renameTo((new File(jarPath + jarfile
                    + String.valueOf(System.currentTimeMillis())))))) {
                System.err
                        .println("Unable to rename old jar file.. please check write access");
            }
            // Download the new jar
            System.out
                    .println("New jar file found...downloading from server");
            url = new URL(
                    "http://localhost:8090/JarServer/myjar?download=1&file="
                            + URLEncoder.encode(jarfile, "UTF-8"));
            // Code to download
            byte[] buf;
            int byteRead = 0;
            BufferedOutputStream outStream = new BufferedOutputStream(
                    new FileOutputStream(jarPath + jarfile));

            InputStream is = url.openConnection().getInputStream();
            buf = new byte[10240];
            while ((byteRead = is.read(buf)) != -1) {
                outStream.write(buf, 0, byteRead);
            }
            outStream.close();
            System.out.println("Downloaded Successfully.");

            // Now update the md5 file with the new md5
            BufferedWriter bw = new BufferedWriter(new FileWriter(md5File));
            bw.write(servermd5);
            bw.close();

            // TODO - Execute the jar, its saved in the same path
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}
}

以防万一您也可以控制 servlet 代码,这就是 servlet 代码的运行方式:-

@WebServlet(name = "jarervlet", urlPatterns = { "/myjar" })
public class JarServlet extends HttpServlet {

private static final long serialVersionUID = 1L;
// Remember to have a '/' at the end, otherwise code will fail
private static final String PATH_TO_FILES = "/Users/nrj/Downloads/";

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

    String fileName = req.getParameter("file");
    if (null != fileName) {
        fileName = URLDecoder.decode(fileName, "UTF-8");
    }
    String hash = req.getParameter("hash");
    if (null != hash && hash.equalsIgnoreCase("md5")) {
        resp.getWriter().write(readMd5Hash(fileName));
        return;
    }

    String download = req.getParameter("download");
    if (null != download) {
        InputStream fis = new FileInputStream(PATH_TO_FILES + fileName);
        String mimeType = getServletContext().getMimeType(
                PATH_TO_FILES + fileName);
        resp.setContentType(mimeType != null ? mimeType
                : "application/octet-stream");
        resp.setContentLength((int) new File(PATH_TO_FILES + fileName)
                .length());
        resp.setHeader("Content-Disposition", "attachment; filename=\""
                + fileName + "\"");

        ServletOutputStream os = resp.getOutputStream();
        byte[] bufferData = new byte[10240];
        int read = 0;
        while ((read = fis.read(bufferData)) != -1) {
            os.write(bufferData, 0, read);
        }
        os.close();
        fis.close();
        // Download finished
    }

}

private String readMd5Hash(String fileName) {
    // We are assuming there is a .md5 file present for each file
    // so we read the hash file to return hash
    try (BufferedReader br = new BufferedReader(new FileReader(
            PATH_TO_FILES + fileName + ".md5"))) {
        return br.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

}