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

时间:2015-01-07 17:29:35

标签: java webserver jetty jnlp

我正在编写一个简单的(通用的)包装Java类,它将在与已部署的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对象,但不保存它)?
  2. 更重要的是,但与#1相关,是否可以以某种方式缓存jar文件?

    2.1我可以(轻松)在Web服务器上请求jar文件的MD5,只有在更改后才下载吗?
    2.2如果没有另外的缓存机制,可能只请求清单?版本/构建信息可以存储在那里。

  3. 如果有人做过类似的事情,你可以详细描述该怎么做吗?

    更新每项初步回应

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

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

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

    更集中的问题:

    If-Modified-Since标头是否仍适用于此用途?如果是这样,我是否需要完全不同的代码才能添加?也就是说,你能告诉我如何修改所包含的代码吗?关于保存jar文件的相同问题 - 最终我真的很惊讶(沮丧!)我可以获得一个JarFile对象,但无法坚持它 - 我是否还需要JarURLConnection类?

    赏金问题

    我最初没有意识到我试图提出的确切问题。就是这样:

    如何在命令行程序中本地保存来自Web服务器的jar文件,该程序退出并仅在服务器上更改该jar文件时更新该文件?

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

4 个答案:

答案 0 :(得分:2)

  1. 是的,文件可以保存到磁盘,您可以使用URL类中的openStream()方法获取输入流。

  2. 根据@fge提到的评论,有一种方法可以检测文件是否被修改。

  3. 示例代码:

    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());
    }
    

答案 1 :(得分:2)

  

如何在命令行程序中本地保存来自Web服务器的jar文件,该程序退出并仅在服务器上更改该jar文件时更新该文件?

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

答案 2 :(得分:1)

我假设.md5文件在本地和Web服务器上都可用。如果您希望将其作为版本控制文件,则将应用相同的逻辑。

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

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;
}

}

答案 3 :(得分:0)

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

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

但是在很多情况下,我们混淆了哪个版本现在处于生产阶段,以及更新服务器失败,我们切换到continuous integration练习TeamCity

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

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

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

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

此方案对于客户端更新程序来说似乎很复杂,但在实践中它非常清晰且强大。

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

更新。

我已将我们的更新程序移至github,请点击此处:github.com/ancalled/simple-updater

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