简体   繁体   English

如何升级Tomcat Java Applet以使用JNLP?

[英]How do you upgrade a Tomcat Java Applet to use JNLP?

I have a set of Java Applet programs that run on Tomcat. 我有一组在Tomcat上运行的Java Applet程序。 These programs keep track of informal golf "tournament" events, including friendly player competitions. 这些节目跟踪非正式的高尔夫“锦标赛”活动,包括友好的球员比赛。

While the program details aren't important, the code set includes over 30,000 source lines. 虽然程序细节并不重要,但代码集包含超过30,000个源代码行。 I chose Java as the implementation language for portability and to avoid maintenance problems. 我选择Java作为可移植性的实现语言并避免维护问题。 I use Tomcat to deploy the application and javascript to invoke the applets. 我使用Tomcat部署应用程序和javascript来调用applet。 All my applets use parameters, such as event name, course name, and date of play. 我的所有applet都使用参数,例如事件名称,课程名称和播放日期。

Unfortunately, java and browser changes have now caused maintenance problems for my application. 不幸的是,Java和浏览器的更改现在已经导致我的应用程序的维护问题 The first problem was that java added a requirement that jar files be signed. 第一个问题是java添加了一个jar文件签名的要求。 The second problem was that first Chrome and now Firefox have removed support for NPAPI plugins, which essentially removed Java Applet support from html. 第二个问题是第一个Chrome和现在的Firefox已经删除了对NPAPI插件的支持,这实际上从html中删除了Java Applet支持。

JNLP ( Java Web Start ) is the new replacement. JNLPJava Web Start )是新的替代品。 Both problems were somewhat difficult to fix because there were no clear step-by-step documentation detailing exactly what actually needs to be done. 这两个问题都难以解决,因为没有明确的逐步文档详细说明实际需要做什么。

There may be different, even better, ways to migrate applets to JNLP but the procedures described here work and are complete. 可能有不同的,甚至更好的方法将applet迁移到JNLP,但这里描述的过程是有效的并且是完整的。 However, in describing them I have to assume that you already know how to create a java web application since there's no need to update something you don't already have. 但是,在描述它们时,我必须假设您已经知道如何创建Java Web应用程序,因为不需要更新您还没有的内容。

I work with Tomcat in the Windows Cygwin environment. 我在Windows Cygwin环境中使用Tomcat。 My example mkJavaKey script explicitly uses that environment but all the Java and javascript code is portable. 我的示例mkJavaKey脚本显式使用该环境,但所有Java和javascript代码都是可移植的。 Tomcat uses web.xml to define how Servlets are invoked. Tomcat使用web.xml来定义如何调用Servlet。 If you use a different deployment method, my web.xml file should at least work as a starting point. 如果您使用不同的部署方法,我的web.xml文件至少应该作为起点。

Why do you need to sign a jar file? 你为什么需要签一个jar文件?

I can't answer this part of the question. 我无法回答这部分问题。 But, for any non-trivial application you will need to go at least through the procedure to self-sign your jar files even though this self-signing provides no real additional security. 但是,对于任何非平凡的应用程序,您至少需要通过该过程自行签署您的jar文件, 即使此自签名不提供真正的额外安全性。 Anyone can self-sign an application using tools provided in the Java Development Kit. 任何人都可以使用Java Development Kit中提供的工具自行签署应用程序。 Self-signed certificates are fine for development work, but you will have to click a risk acceptance check box each time you run your application. 自签名证书适用于开发工作,但每次运行应用程序时都必须单击风险验收复选框。

OK, my application is non-trivial and I need my jar files signed. 好的,我的应用程序是非常重要的,我需要我的jar文件签名。 What's the procedure? 程序是什么?

Here's the quick answer: It's a two step process. 这是快速回答:这是一个两步过程。 You first use the keytool program to create the necessary credentials and then use the jarsigner tool to sign your jar files. 首先使用keytool程序创建必要的凭据,然后使用jarsigner工具对jar文件进行签名。 You only need to create credentials once in a while but need to sign every deployed jar file. 您只需要偶尔创建一次凭证,但需要签署每个部署的jar文件。

To create these credentials (a self-signed certificate) use: 要创建这些凭据(自签名证书),请使用:

$JAVA_HOME/bin/keytool -genkeypair -keyalg RSA -keysize 2048 -alias mydomain -validity 1825

This creates a certificate named .keystore in your home directory that's good for five years. 这将在您的主目录中创建一个名为.keystore的证书,该证书.keystore为五年。 You have to respond to its prompts and I used "password" as the password. 您必须回复其提示,我使用“密码”作为密码。 Since I only use this certificate for self-signing jar files, security is not a big issue. 由于我只使用此证书进行自签名jar文件,因此安全性不是一个大问题。 The validity parameter specifies how long (in days) the certificate is valid. validity参数指定证书有效的时间(以天为单位)。

Each time you update a jar file you will need to sign it. 每次更新jar文件时都需要对其进行签名。 Assuming that you are in your distribution directory and need to sign applet.jar , use: 假设您在分发目录中并需要签署applet.jar ,请使用:

$JAVA_HOME/bin/jarsigner -tsa http://timestamp.digicert.com -storepass password applet.jar mydomain

The "password" after -storepass matches the password you used with keytool, and the "mydomain" matches the keytool -alias parameter. -storepass之后的“password”与您使用keytool的密码匹配,“mydomain”与keytool -alias参数匹配。 You will need to specify the -tsa (Time Stamp Authority) parameter and http://timestamp.digicert.com is (or at least was) one publicly available. 您需要指定-tsa(时间戳授权)参数,并且http://timestamp.digicert.com是(或至少是)公开可用的参数。 I don't know exactly what a TSA does or why you need one, but jarsigner isn't happy without it, won't default it, and doesn't directly document how to find one. 我不确切知道TSA的作用或者你为什么需要它,但是没有它,jarsigner不高兴,不会默认它,也不会直接记录如何找到它。

You can now either use or ignore the following batch file. 您现在可以使用或忽略以下批处理文件。 I created it because when I needed to create a new certificate (my original certificate expired) I had forgotten how to create it. 我创建它是因为当我需要创建一个新证书(我的原始证书已过期)时,我忘记了如何创建它。 Hopefully we'll be able to find this batch file the next time we need it, perhaps five years from now. 希望我们能够在下次需要时找到这个批处理文件,也许是从现在开始的五年。

#!/bin/bash
#
# Title-
#        mkJavaKey
#
# Function-
#        Create a new key using $JAVA_HOME/bin/keytool
#
# Usage-
#        mkJavaKey ## CYGWIN ONLY ##
#        (This is required when jarsigner complains about an expired key.)
#        NOTE: This *REMOVES* and *REPLACES* your existing .keystore file!
#
#######

##########################################################################
# Environment check
if [ -z "$JAVA_HOME" ] ; then
  . setupJAVA ## (This personal script sets JAVA_HOME)
  if [ -z "$JAVA_HOME" ] ; then
    echo "JAVA_HOME environment variable missing"
    exit 1
  fi
fi

if [ -z "$HOMEPATH" ] ; then
  echo "HOMEPATH environment variable missing"
  echo "Try export HOMEPATH=\Users\myname"
  exit 1
fi

home_path=`cygpath --path --unix C:$HOMEPATH`
PGM=$JAVA_HOME/bin/keytool
if [ ! -x "$PGM" ] ; then
  echo "$PGM not executable"
  exit 1
fi

##########################################################################
# Create a new .keystore
set -x
rm -Rf $home_path/.keystore
$PGM -genkeypair -keyalg RSA -keysize 2048 -alias mydomain -validity 1825
exit $?

Notes: My setupJAVA script sets the JAVA_HOME environment variable. 注意:我的setupJAVA脚本设置了JAVA_HOME环境变量。 For Linux, use $HOME instead of $HOMEPATH and skip the cygpath sections. 对于Linux,使用$HOME而不是$HOMEPATH并跳过cygpath部分。 These convert between Linux and Windows filename formats in the Cygwin environment. 这些在Cygwin环境中在Linux和Windows文件名格式之间进行转换。

You'll need to sign your jar files every time you install them. 每次安装时都需要对jar文件进行签名。 To automate this, I modified my Makefile to do this. 为了自动执行此操作,我修改了Makefile来执行此操作。 Here's the make code snippet I used: 这是我使用的make代码段:

.PHONY: golfer.install
golfer.install: test golfer
: (Not relevant to discussion)
cp -p $(OBJDIR)/usr/fne/golfer/Applet/applet.jar $(DEPLOYDIR)/webapps/golfer/.
jarsigner -tsa http://timestamp.digicert.com -storepass password "$(shell cygpath --path --windows "$(DEPLOYDIR)/webapps/golfer/applet.jar")" mydomain
: (Not relevant to discussion)

The $(OBDIR) and $(DEPLOYDIR) variables are not relevant to this discussion. $(OBDIR)$(DEPLOYDIR)变量与此讨论无关。 They are directory paths set within my Makefile based build environment. 它们是在基于Makefile的构建环境中设置的目录路径。

How do you migrate Applets to the new JNLP environment? 如何将Applet迁移到新的JNLP环境?

Now that we have self-signed jarfiles, we can start figuring out how to run them. 既然我们有自签名的jar文件,我们就可以开始弄清楚如何运行它们了。 Many browsers no longer support NPAPI so the <applet> tag won't work. 许多浏览器不再支持NPAPI,因此<applet>标记不起作用。 Neither will deployJava.runApplet(). deployJava.runApplet()也不会。 I won't get into why NPAPI support was dropped, just what needs to be done to make your existing applications run. 我不会理解为什么NPAPI支持被删除,只需要做什么来使现有的应用程序运行。

The biggest problem I found migrating my code was that, eventually, I had to create .jnlp files rather than .html files. 我发现迁移代码的最大问题是,最终,我必须创建.jnlp文件而不是.html文件。 I'll show you how to do this, describing the code I modified and added. 我将向您展示如何执行此操作,描述我修改和添加的代码。

This is the (now obsolete) javascript code I used to generate html: 这是我用来生成html的(现在过时的)javascript代码:

//------------------------------------------------------------------------
//
// Title-
//       applet.js
//
// Purpose-
//       Common applet javascript.
//
// Last change date-
//       2010/10/19
//
//------------------------------------------------------------------------
var out;     // Output document

//------------------------------------------------------------------------
// appHead
//
// Generate html header for application.
//------------------------------------------------------------------------
function appHead(title,cname,height,width)
{
   var todoWindow= window.open('','','');
   out= todoWindow.document;
   out.write('<html>');
   out.write('<head><title>' + title + '</title></head>');
   out.write('<body>\n');
   out.write('<applet code="' + cname + '.class"');
   out.write('    codebase="./"')
   out.write('    archive="applet.jar,jars/common.jar"');
   out.write('    width="' + width + '" height="' + height + '">\n');
}

//------------------------------------------------------------------------
// appParm
//
// Add parameter information
//------------------------------------------------------------------------
function appParm(name, value)
{
     out.write('        <param-name="' + name + '" value="' + value + '"/>\n');
}

//------------------------------------------------------------------------
// appTail
//
// Generate html trailer information.
//------------------------------------------------------------------------
function appTail()
{
   out.write('Your browser is completely ignoring the &lt;APPLET&gt; tag!\n');
   out.write('</applet>');
   out.write('<form>');
   out.write('<input type="button" value="Done" onclick="window.close()">');
   out.write('</form>');
   out.write('</body>');
   out.write('</html>');
   out.close();
   out= null;
}

//------------------------------------------------------------------------
// cardEvents
//
// Display scorecard for selected date.
//------------------------------------------------------------------------
function cardEvents(eventsID, obj) 
{
   if( obj.selectedIndex == 0 )
   {
     alert("No date selected");
     return;
   }
   appHead('Score card', 'EventsCard', '100%', '100%');
   appParm('events-nick', eventsID);
   appParm('events-date', obj[obj.selectedIndex].value);
   appTail();
   reset();
}

We don't need to see html generated by my servlet that includes the form button used to invoke the cardEvents function. 我们不需要查看我的servlet生成的html,其中包含用于调用cardEvents函数的表单按钮。 It's similar to the "DONE" button generation and didn't need to be changed. 它与“DONE”按钮生成类似,不需要更改。

It should have been pretty straight-forward to just convert this javascript to generate a jnlp file. 将这个javascript转换为生成jnlp文件应该非常简单。 This was not possible, or at least I couldn't find any working examples of how to do it and couldn't figure out a way to do it modifying any of the broken examples. 这是不可能的,或者至少我找不到任何有关如何做的工作示例,也无法找到修改任何破坏的示例的方法。 The window.open() statement would always add <html> and <body> sections even though I only wanted to generate jnlp xml. window.open()语句总是会添加<html><body>部分,即使我只想生成jnlp xml。 I also tried document.open("application/x-java-jnlp-file") . 我也尝试过document.open("application/x-java-jnlp-file") Even though the mime-type was specified, the unwanted html and body sections were still present. 尽管指定了mime-type,但仍然存在不需要的html和body部分。

None of the documentation I found showed how to dynamically generate the .jnlp file I needed, which included user-selected applet parameters. 我找到的所有文档都没有显示如何动态生成我需要的.jnlp文件,其中包括用户选择的applet参数。 Here's the work-around I used instead. 这是我用过的解决方法。

I replaced the html generation in applet.js with this: 我用这个替换了applet.js中的html生成:

//------------------------------------------------------------------------
//
// Title-
//       applet.js
//
// Purpose-
//       Common applet javascript.
//
// Last change date-
//       2017/03/15
//
//------------------------------------------------------------------------
var out;     // Output URL

//------------------------------------------------------------------------
// appHead
//
// Generate application URL header.
//------------------------------------------------------------------------
function appHead(title,cname,height,width)
{
   out= cname + ',' + title;
}

//------------------------------------------------------------------------
// appParm
//
// Generate html parameter information.
//------------------------------------------------------------------------
function appParm(name, value)
{
   out= out + ',' + name + '=' + value;
}

//------------------------------------------------------------------------
// appTail
//
// Generate html trailer information.
//------------------------------------------------------------------------
function appTail()
{
   var specs= 'menubar=yes,toolbar=yes';
   window.open('Applet.jnlp?' + out, '_self', specs);
}

//------------------------------------------------------------------------
// cardEvents
//
// Display scorecard for selected date.
//------------------------------------------------------------------------
function cardEvents(eventsID, obj)
{
    // (UNCHANGED!)
}

This generates a URL in the form of Applet.jnlp,className,description,parm=value,parm=value,... . Applet.jnlp,className,description,parm=value,parm=value,...的形式生成URL。

I then created a new Servlet named AppletServlet.java. 然后我创建了一个名为AppletServlet.java的新Servlet。 The URL passed to it provides all the information needed to generate the .jnlp file. 传递给它的URL提供了生成.jnlp文件所需的所有信息。 This code follows the standard sample Servlet structure where doGet is called to handle the request. 此代码遵循标准样本Servlet结构,其中调用doGet来处理请求。 Here's the code: 这是代码:

//------------------------------------------------------------------------
//
// Method-
//       AppletServlet.doGet
//
// Purpose-
//       Called for each HTTP GET request.
//
//------------------------------------------------------------------------
public void
   doGet(                           // Handle HTTP "GET" request
     HttpServletRequest  req,       // Request information
     HttpServletResponse res)       // Response information
   throws ServletException, IOException
{
   String q= req.getQueryString();
   if( debug ) log("doGet("+q+")");

   res.setContentType("text/html");

   query(req, res);
}

//------------------------------------------------------------------------
//
// Method-
//       AppletServlet.putError
//
// Purpose-
//       Generate error response.
//
//------------------------------------------------------------------------
public void
   putError(                        // Generate error response
     PrintWriter       out,         // The response writer
     String            msg)         // The error message
{       out.println("<HTML>");
   out.println("<HEAD><TITLE>" + msg + "</TITLE></HEAD>");
   out.println("<BODY>");
   out.println("<H1 align=\"center\">" + msg + "</H1>");
   out.println("</BODY>");
   out.println("</HTML>");
}

//------------------------------------------------------------------------
//
// Method-
//       AppletServlet.query
//
// Purpose-
//       Handle a query.
//
//------------------------------------------------------------------------
protected void
   query(                           // Handle a query
     HttpServletRequest  req,       // Request information
     HttpServletResponse res)       // Response information
   throws ServletException, IOException
{
   String q= req.getQueryString();
   if( debug ) log("query("+q+")");

   PrintWriter out = res.getWriter();
   String BOGUS= "<br> Malformed request: query: '" + q + "'";

   //=====================================================================
   // Applet.jnlp?classname,title,parm=value,parm=value,...
   int index= q.indexOf(',');
   if( index < 0 || index == (q.length() - 1) )
   {
     putError(out, BOGUS);
     return;
   }
   String invoke= q.substring(0, index);

   q= q.substring(index+1);
   index= q.indexOf(',');
   if( index < 0 )
     index= q.length();
   String title= q.substring(0, index);
   title= java.net.URLDecoder.decode(title, "UTF-8");

   // Parameter extraction
   Vector<String> param= new Vector<String>();
   if( index < q.length() )
   {
     q= q.substring(index+1);
     for(;;)
     {
       index= q.indexOf(',');
       if( index < 0 )
         index= q.length();

       String s= q.substring(0, index);
       int x= s.indexOf('=');
       if( x < 0 )
       {
         putError(out, BOGUS);
         return;
       }

       param.add(s);
       if( index >= q.length() )
         break;

       q= q.substring(index+1);
     }
   }

   //---------------------------------------------------------------------
   // We now have enough information to generate the response
   //---------------------------------------------------------------------
   res.setContentType("application/x-java-jnlp-file");
   out.println("<?xml version='1.0' encoding='utf-8'?>");
   out.println("<jnlp spec='1.0+' codebase='http://localhost:8080/golfer'>");
   out.println(" <information>");
   out.println("  <title>" + title + "</title>");
   out.println("  <vendor>My Name</vendor>");
   out.println("  <description>" + title + "</description>");
   out.println(" </information>");
   out.println(" <security><all-permissions/></security>");
   out.println(" <resources>");
   out.println("  <j2se version='1.7+'/>");
   out.println("  <jar href='applet.jar'/>");
   out.println("  <jar href='jars/common.jar'/>");
   out.println(" </resources>");
   out.println(" <applet-desc main-class='" + invoke + "' name='" + title + "'" +
                " height='90%' width='98%'>");

   // Insert applet parameters
   for(int i= 0; i<param.size(); i++)
   {
     String s= param.elementAt(i);
     int    x= s.indexOf('=');
     String n= s.substring(0,x);
     String v= s.substring(x+1);
     out.println("  <param name='" + n+ "' value='" + v + "'/>");
   }
   out.println(" </applet-desc>");
   out.println("</jnlp>");
}

Notes: debug is my "debug enabled" flag, and log() writes a debugging message to stdout. 注意: debug是我的“debug enabled”标志, log()将调试消息写入stdout。 In this new code version the height and width are not passed as parameters, but are hard-coded instead. 在这个新的代码版本中,高度和宽度不作为参数传递,而是硬编码。 It turned out that in the HTML version "100%" was always used as the height and width and worked well. 事实证明,在HTML版本中,“100%”始终用作高度和宽度并且运行良好。 For some (unknown to me) reason my applet windows are truncated at the bottom and possibly on the right when called using .jnlp code with 100% height and width. 对于一些(我不知道)的原因,我的applet窗口在底部被截断,当使用具有100%高度和宽度的.jnlp代码调用时可能在右边。 I use these new height and width parameters to work around this formatting problem. 我使用这些新的高度和宽度参数来解决此格式问题。

In order to invoke my new AppletServlet, I modified my web.xml file: 为了调用我的新AppletServlet,我修改了我的web.xml文件:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
   <servlet>
     <servlet-name>Applet</servlet-name>
     <servlet-class>usr.fne.golfer.AppletServlet</servlet-class>
     <init-param>
       <param-name>property-path</param-name>
       <param-value>profile</param-value>
     </init-param>
     <init-param>
       <param-name>property-file</param-name>
       <param-value>golfer.pro</param-value>
     </init-param>
     <load-on-startup>30</load-on-startup>
   </servlet>

   <servlet-mapping>
     <servlet-name>Applet</servlet-name>
     <url-pattern>/Applet.jnlp</url-pattern>
   </servlet-mapping>

   : (Other Servlets unchanged) 
</web-app>

This causes AppletServlet to be invoked for any Applet.jnlp URL. 这会导致为任何Applet.jnlp URL调用AppletServlet。 Browsers ignore the query string and treat the result as if the file name is Applet.jnlp. 浏览器忽略查询字符串并将结果视为文件名为Applet.jnlp。

For smoother operation, you'll need to set your Windows file associations so that .jnlp files invoke Java(TM) Web Start Launcher. 为了更顺畅的操作,您需要设置Windows文件关联,以便.jnlp文件调用Java(TM)Web Start Launcher。 In Windows, your JWS Launcher is C:\\Program Files\\java\\jre*\\bin\\javaws.exe (Use your latest jre folder.) Also, if you use Chrome, your download directory will contain the generated Applet.jnlp files. 在Windows中,JWS Launcher是C:\\Program Files\\java\\jre*\\bin\\javaws.exe (使用最新的jre文件夹。)此外,如果您使用Chrome,则下载目录将包含生成的Applet.jnlp文件。 You'll need to clean them out now and then. 你需要偶尔清理它们。

This completes the migration process. 这样就完成了迁移过程。 No applets were harmed (or changed) in this migration, so the bulk of the 30,000 source lines remained unchanged. 在此迁移中没有小程序受到伤害(或更改),因此30,000个源代码行中的大部分保持不变。

While I used cut and paste from operational code to create the examples, it's possible that typos could have snuck in. Please comment if you find anything incorrect, missing, or unclear. 虽然我使用操作代码中的剪切和粘贴来创建示例,但错别字可能会被窃听。如果您发现任何不正确,缺失或不清楚的内容,请发表评论。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM