如何让 ZK webfragment 与嵌入式 Jetty 9 一起工作?

How to get ZK webfragment working with embedded Jetty 9?

这个最小的嵌入式 Jetty 项目正确启动,扫描注释并找到并映射带注释的 TestServlet。

项目结构:

|-src/main/java/test
|  |-Test.java
|-webapp/
|  |-test.zul
|-pom.xml

Test.java:

package test;

import java.io.File;
import java.io.IOException;
import java.net.URI;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.FragmentConfiguration;
import org.eclipse.jetty.webapp.MetaInfConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;

public class Test {
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/test");
        webapp.setBaseResource(Resource.newResource(new File("webapp").getCanonicalFile()));
        // https://www.eclipse.org/jetty/documentation/jetty-9/index.html#configuring-webapps
        // the order is important
        webapp.setConfigurations(new Configuration[] { //
            new WebInfConfiguration(), //
            new WebXmlConfiguration(), //
            new MetaInfConfiguration(), //
            new FragmentConfiguration(), //
            // new EnvConfiguration(), // not needed
            // new PlusConfiguration(), // not needed
            new AnnotationConfiguration(), //
            // new JettyWebXmlConfiguration(), // no jetty-web.xml
        });
        webapp.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*");
        server.setHandler(webapp);
        server.setDumpAfterStart(true);
        server.start();
        java.awt.Desktop.getDesktop().browse(new URI("http://localhost:8080/test/TestServlet")) /* working */;
        java.awt.Desktop.getDesktop().browse(new URI("http://localhost:8080/test/test.zul")) /* not working */;
    }
    
    @WebServlet(urlPatterns = {"/TestServlet"})
    public static final class TestServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.getWriter().write("Test 1");
        }
    }
}

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>test</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-annotations</artifactId>
            <version>9.4.30.v20200611</version>
        </dependency>
        <dependency>
            <groupId>org.zkoss.zk</groupId>
            <artifactId>zkbind</artifactId>
            <version>9.6.0.1</version>
        </dependency>
    </dependencies>
</project>

test.zul:

<zk><label value="hello"/></zk>

zk 网页片段似乎 'noted' 不知何故:

|  +@ org.eclipse.jetty.webFragments.cache = java.util.concurrent.ConcurrentHashMap@bb35baa5{size=30}
|  |  +@ file:///C:/Users/r.hoehener/.m2/repository/org/zkoss/common/zcommon/9.6.0.1/zcommon-9.6.0.1.jar = org.eclipse.jetty.util.resource.EmptyResource@3fc39309
|  |  +@ file:///C:/Users/r.hoehener/.m2/repository/org/eclipse/jetty/jetty-annotations/9.4.30.v20200611/jetty-annotations-9.4.30.v20200611.jar = org.eclipse.jetty.util.resource.EmptyResource@3fc39309
|  |  +@ file:///C:/Users/r.hoehener/.m2/repository/org/eclipse/jetty/jetty-io/9.4.30.v20200611/jetty-io-9.4.30.v20200611.jar = org.eclipse.jetty.util.resource.EmptyResource@3fc39309
|  |  +@ file:///C:/Users/r.hoehener/.m2/repository/org/apache-extras/beanshell/bsh/2.0b6/bsh-2.0b6.jar = org.eclipse.jetty.util.resource.EmptyResource@3fc39309
|  |  +@ file:///C:/Users/r.hoehener/.m2/repository/org/zkoss/zk/zkwebfragment/9.6.0.1/zkwebfragment-9.6.0.1.jar = jar:file:///C:/Users/r.hoehener/.m2/repository/org/zkoss/zk/zkwebfragment/9.6.0.1/zkwebfragment-9.6.0.1.jar!/META-INF/web-fragment.xml
...

但是 test.zul 显示为纯文本。 ZK 引擎未初始化。

知道为什么吗?

编辑:为我进行配置的方式辩护:这直接来自 9.x 文档,上面写着 'You have a number of options for how to make Jetty use a different list of Configurations.',包括 'Setting the list directly on the WebAppContext':

<Configure class="org.eclipse.jetty.webapp.WebAppContext">
  <Set name="war"><SystemProperty name="jetty.base" default="."/>/webapps/my-cool-webapp</Set>
  <Set name="configurationClasses">
    <Array type="java.lang.String">
      <Item>org.eclipse.jetty.webapp.WebInfConfiguration</Item>
      <Item>org.eclipse.jetty.webapp.WebXmlConfiguration</Item>
      <Item>org.eclipse.jetty.webapp.MetaInfConfiguration</Item>
      <Item>org.eclipse.jetty.webapp.FragmentConfiguration</Item>
      <Item>org.eclipse.jetty.plus.webapp.EnvConfiguration</Item>
      <Item>org.eclipse.jetty.plus.webapp.PlusConfiguration</Item>
      <Item>org.eclipse.jetty.annotations.AnnotationConfiguration</Item>
      <Item>org.eclipse.jetty.webapp.JettyWebXmlConfiguration</Item>
    </Array>
  </Set>
</Configure>

首先,不要使用 Jetty 9.4.30,它现在有一些安全建议。

参见:https://www.eclipse.org/jetty/security_reports.php

至少使用 Jetty 9.4.44.v20210927.

接下来,检查您的 Jetty 服务器转储以查找 zk servlet ...

  • org.zkoss.zk.au.http.DHtmlUpdateServlet
  • org.zkoss.zk.ui.http.DHtmlLayoutServlet

如果这些存在于您的 WebAppContext 转储中,那么您的 zkwebfragment-<ver>.jar 已被 Jetty 正确发现并加载。此时,您剩下要做的就是如何使用 zk 技术正确配置您的 zk 库(从这里开始您可以忽略 Jetty 的具体细节)。

如果它们不存在,那么首先确保您自己的网络应用程序正在使用 Servlet 3.0(在您的 WEB-INF/web.xml 中声明)或更新版本以获得适当的 Web Fragment 支持(旧的 Servlet 规范不支持 Web Fragment) .

接下来,确保 zkwebfragment-<ver>.jar 出现在 WebApp 类加载器上,因为根据规范,不会从任何其他类加载器加载 Web 片段,即使是应用程序/服务器/容器类加载器。

如果您仍然看不到它们,请返回调整默认 Configuration 列表,而不是代码段中的硬编码列表。 (您的列表缺少必需的配置,并且成功的顺序错误,不要更改默认列表,不要在 webapp 上设置列表,仅更改服务器级别默认值)。

问问你自己,zk 需要什么? (例如:如果它需要 jndi,那么你也需要 jndi 特定的配置部分)。

如果你不卡在 Java 8,请使用 Jetty 10,因为整个 Configuration 层已经过重新设计,不再允许错误的配置(实际上是旧的 setConfiguration()方法甚至不存在,仅支持 jar 的存在就足以标记您需要该支持并在正确的位置使用正确的父依赖项启用它。

添加配置的方式为:

Configuration.ClassList classList = Configuration.ClassList.setServerDefault(server);
classList.addAfter(
                "org.eclipse.jetty.webapp.FragmentConfiguration",
                "org.eclipse.jetty.annotations.AnnotationConfiguration");

更新:

我试过了

    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-annotations</artifactId>
        <version>10.0.7</version>
    </dependency>

zul 无需添加任何配置即可运行。

所以,正如 Joakim 指出的那样,Servlet Specification 3.1 说:

If a framework wants its META-INF/web-fragment.xml honored in such a way that it augments a web application's web.xml, the framework must be bundled within the web application's WEB-INF/lib directory. [...] In other words, only JAR files bundled in a web application's WEB-INF/lib directory, but not those higher up in the class loading delegation chain, need to be scanned for web-fragment.xml

因此,我必须确保 zkwebfragments.jar 存在于 WebAppContext 的 class 路径中。

我将部署策略更改为如下所示:

/myapp/
  |-bin/
  |  |- project jar (including annotated servlets and listeners)
  |  |- all maven dependency jars (including jetty and zk libs)
  |-webapp/
  |  |-WEB-INF/
  |  |  |-lib/ (EMTPY!)
  |  |  |-web.xml
  |  |  |-zk.xml
  |  |-index.zul
  |  |-other static resources
  |-data/
     |-application data (docker mounted host volume)

在主 class 中,将 zkwebfragment.jar 添加到 WebAppContext classpath:

Server server = new Server(port);
WebAppContext webapp = new WebAppContext();
webapp.setContextPath("/myapp");
webapp.setBaseResource(Resource.newResource(new File(isDocker ? "/myapp/webapp" : "./webapp").getCanonicalFile()));
// order important: https://www.eclipse.org/jetty/documentation/jetty-9/index.html#configuring-webapps
webapp.setConfigurations(new Configuration[] { //
    new WebInfConfiguration(), //
    new WebXmlConfiguration(), //
    new MetaInfConfiguration(), //
    new FragmentConfiguration(), //
    new AnnotationConfiguration(), //
});
// servlet spec: fragments are only loaded if they are bundled in WEB-INF/lib
webapp.setExtraClasspath(Arrays.asList(((URLClassLoader) MyAppMain.class.getClassLoader()).getURLs()).stream().filter(
    u -> u.toString().contains("zkwebfragment")).findAny().get().toString());
// scan for annotations in the container classpath (AppClassLoader)
webapp.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*");
server.setHandler(webapp);
server.setDumpAfterStart(true);
server.start();
   

当 运行 来自 IDE 的主要 class 时,所有项目 classes 和所有 Maven 依赖项都会自动添加到 AppClassLoader。

当运行作为docker容器时,应用程序以:

启动
ENTRYPOINT ["java", "-cp", "/myapp/bin/*", "myapp.MyAppMain"]

这还将项目 jar 和所有 maven 依赖项 jar 添加到 AppClassLoader。

通过将所有 jar 从 /myapp/webapp/META-INF/lib/* 移动到 /myapp/bin/*,所有 jar 在 classloader 层次结构中只存在一次,这也解决了 zkwebfragment.jar 除外,它存在两次。码头转储摘录:

| |  +> WebAppClassLoader{1509563803}@59fa1d9b
| |  |  +> URLs size=1
| |  |  |  +> file:/myapp/bin/zkwebfragment-9.6.0.1.jar
| |  |  +> sun.misc.Launcher$AppClassLoader@2ff4acd0

再往下:

| +> sun.misc.Launcher$AppClassLoader@2ff4acd0
|    +> URLs size=52
|    |  +> file:/myapp/bin/myapp.jar
|    |  +> file:/myapp/bin/jetty-annotations-9.4.44.v20210927.jar
|    |  +> file:/myapp/bin/zkwebfragment-9.6.0.1.jar
|    |  +> ...
|    +> sun.misc.Launcher$ExtClassLoader@4ee285c6
|       +> URLs size=9
|          +> file:/opt/java/openjdk/lib/ext/sunpkcs11.jar
|          +> ...

None 如果我使用的是 Jetty 安装并删除捆绑为标准 WAR 的应用程序,那么这是必需的。但我真的很想尝试这种简单的 Java 方法(来自 Jetty 文档):

Jetty has a slogan, "Don’t deploy your application in Jetty, deploy Jetty in your application!" What this means is that as an alternative to bundling your application as a standard WAR to be deployed in Jetty, Jetty is designed to be a software component that can be instantiated and used in a Java program just like any POJO.