App Engine Java 11 无法在实时服务器上找到或加载 main class

App Engine Java 11 could not find or load main class on live server

tl;dr: 为什么这在本地有效,但当我部署到我的实时 App Engine 项目时却无效?

我正在尝试使用 Java 11 版本的 App Engine 创建一个准系统的基于 servlet 的 Web 应用程序。在 this guide. I'm also using this guide and this example 之后,我正在将一些项目从 Java 8 更新到 Java 11。我的目标是使用 Jetty 运行 一个非常简单的 Web 应用程序,在 App Engine 中提供一个静态 HTML 文件和一个 servlet 文件。

当我在本地 运行 时,我的网络应用程序工作正常:

mvn clean install
mvn exec:java -Dexec.args="target/app-engine-hello-world-1.war"

当我 运行 这些命令时,我的 index.html 和我的 servlet URL 都工作正常。

但是当我部署到我的实时站点时:

mvn package appengine:deploy

...命令成功,但是当我导航到我的实时 URL 时,HTML 文件和 servlet URL 都收到此错误:"Error: Server Error. The server encountered an error and could not complete your request. Please try again in 30 seconds." 如果我查看云控制台中的日志,我会看到此错误:

Error: Could not find or load main class io.happycoding.Main
Caused by: java.lang.ClassNotFoundException: io.happycoding.Main

我的设置有问题,但我没有发现任何明显的错误。

这是我项目中的文件:

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>io.happycoding</groupId>
  <artifactId>app-engine-hello-world</artifactId>
  <version>1</version>
  <packaging>war</packaging>

  <properties>
    <!-- App Engine currently supports Java 11 -->
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <failOnMissingWebXml>false</failOnMissingWebXml>
  </properties>

  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
    </dependency>

    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-server</artifactId>
      <version>9.4.31.v20200723</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-webapp</artifactId>
      <version>9.4.31.v20200723</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-util</artifactId>
      <version>9.4.31.v20200723</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-annotations</artifactId>
      <version>9.4.31.v20200723</version>
      <type>jar</type>
    </dependency>

  </dependencies>

  <build>
    <plugins>

      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.0.0</version>
        <executions>
          <execution>
            <goals>
              <goal>java</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <mainClass>io.happycoding.Main</mainClass>
        </configuration>
      </plugin>

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>appengine-maven-plugin</artifactId>
        <version>2.2.0</version>
        <configuration>
          <projectId>happy-coding-gcloud</projectId>
          <version>1</version>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

src/main/appengine/app.yaml

runtime: java11
entrypoint: 'java -cp "*" io.happycoding.Main app-engine-hello-world-1.war'

src/main/java/io/happycoding/Main.java

package io.happycoding;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.webapp.Configuration.ClassList;
import org.eclipse.jetty.webapp.WebAppContext;
import io.happycoding.servlets.HelloWorldServlet;

/** Simple Jetty Main that can execute a WAR file when passed as an argument. */
public class Main {

  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage: need a relative path to the war file to execute");
      System.exit(1);
    }
    System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StrErrLog");
    System.setProperty("org.eclipse.jetty.LEVEL", "INFO");

    Server server = new Server(8080);

    WebAppContext webapp = new WebAppContext();
    webapp.setContextPath("/");
    webapp.setWar(args[0]);
    ClassList classlist = ClassList.setServerDefault(server);

    // Enable Annotation Scanning.
    classlist.addBefore(
        "org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
        "org.eclipse.jetty.annotations.AnnotationConfiguration");

    server.setHandler(webapp);
    server.join();
  }
}

src/main/webapp/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Google Cloud Hello World</title>
  </head>
  <body>
    <h1>Google Cloud Hello World</h1>
    <p>This is a sample HTML file. Click <a href="/hello">here</a> to see content served from a servlet.</p>
    <p>Learn more at <a href="https://happycoding.io">HappyCoding.io</a>.</p>
  </body>
</html>

src/main/java/io/happycoding/servlets/HelloWorldServlet.java

package io.happycoding.servlets;

import java.io.IOException;

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

@WebServlet("/hello")
public class HelloWorldServlet extends HttpServlet {

  @Override
  public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    response.setContentType("text/html;");
    response.getOutputStream().println("<h1>Hello world!</h1>");
  }
}

我猜我设置实时站点 class 路径的方式有问题,但我没有发现任何明显的错误。

pom.xml 中的 packaging 属性 设置为 war,我得到一个包含以下内容的 .war 文件:

index.html
META-INF/MANIFEST.MF
META-INF/maven/io.happycoding/app-engine-hello-world/pom.properties
META-INF/maven/io.happycoding/app-engine-hello-world/pom.xml
WEB-INF/classes/io/happycoding/Main.class
WEB-INF/classes/io/happycoding/servlets/HelloWorldServlet.class
WEB-INF/classes/lib/asm-7.3.1.jar
WEB-INF/classes/lib/asm-analysis-7.3.1.jar
WEB-INF/classes/lib/asm-commons-7.3.1.jar
WEB-INF/classes/lib/asm-tree-7.3.1.jar
WEB-INF/classes/lib/javax.annotation-api-1.3.jar
WEB-INF/classes/lib/javax.servlet-api-4.0.1.jar
WEB-INF/classes/lib/jetty-annotations-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-http-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-io-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-jndi-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-plus-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-security-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-server-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-servlet-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-util-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-webapp-9.4.31.v20200723.jar
WEB-INF/classes/lib/jetty-xml-9.4.31.v20200723.jar

如果我将 pom.xml 中的 packaging 属性 更改为 jar,那么我会得到一个包含以下内容的 .jar 文件:

io/happycoding/Main.class
io/happycoding/servlets/HelloWorldServlet.class
META-INF/MANIFEST.MF
META-INF/maven/io.happycoding/app-engine-hello-world/pom.properties
META-INF/maven/io.happycoding/app-engine-hello-world/pom.xml

我在实时站点的日志中收到此错误:

Error: Unable to initialize main class io.happycoding.Main 
Caused by: java.lang.NoClassDefFoundError: org/eclipse/jetty/server/Handler 

这感觉像是进步,但后来我的实时服务器也出现 404 错误,所以我觉得很卡。

我需要对上述设置进行哪些更改才能使其在本地和我的实时服务器上运行?

编辑: 我可以在 App Engine 调试器中看到以下文件:

我尝试将其添加到我的 pom.xml 文件中:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-dependency-plugin</artifactId>
  <version>3.1.2</version>
  <executions>
    <execution>
      <id>copy</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>copy-dependencies</goal>
      </goals>
      <configuration>
        <outputDirectory>
          ${project.build.directory}/appengine-staging
        </outputDirectory>
      </configuration>
    </execution>
  </executions>
</plugin>

然后我在 App Engine 调试器中看到这些文件:

但我仍然得到同样的错误。

我认为问题是由于我的 Main class 位于 .war 文件中,该文件对 class 路径没有影响,这就是为什么找不到。

如何打包我的项目以便它在本地和我的实时服务器上运行?

我认为您的问题是您将 Main class 包含在 war 本身中,而 App Engine 无法找到它。

如您在 GCP migration guide 中所见,Main class 是在名为 simple-jetty-main.

的外部依赖项中定义的

随着 maven-dependency-plugin 的执行,此依赖项被复制到 appengine-staging 目录,使其可以从 Java class 路径访问。

这就是为什么在app.yamlentrypoint中执行命令时可以在指南中提出的示例中找到Mainclass的原因:

entrypoint: 'java -cp "*" com.example.appengine.demo.jettymain.Main helloworld.war'

因此,解决方案是将您的 Main class 包含在另一个库中,独立于您需要部署的 war 文件。

也许您可以创建一个库 - 正如 Google 对 simple-jetty-main 所做的那样 - 可以在您的 GCP 项目中重复使用此任务。

只是为了测试,为了确认这一点,您可以使用 simple-jetty-main 库本身(您可以从 https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/appengine-java11/appengine-simple-jetty-main 克隆所需的代码)。安装它,在 pom.xml 中包含依赖项,还包含 maven-dependency-plugin,并按如下方式定义 entrypoint

entrypoint: 'java -cp "*" com.example.appengine.demo.jettymain.Main app-engine-hello-world-1.war'

对于您的评论,您不希望将 Main class 和其余代码分开。

为了满足该要求,我们必须首先更改 Main class,以便 Jetty 可以提供 HelloWorldSevlet 和静态内容。该代码实际上与您提供的代码非常相似。请原谅设置的简单性,它基于 web.xml 文件;如有必要,可以进行进一步的开发以处理注释或任何认为合适的内容:

package io.happycoding;

import java.net.URL;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class Main {

  public static final String WEBAPP_RESOURCES_LOCATION = "META-INF/resources";

  public static void main(String[] args) throws Exception {
    System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StrErrLog");
    System.setProperty("org.eclipse.jetty.LEVEL", "INFO");

    Server server = new Server(8080);

    URL webAppDir = Thread.currentThread().getContextClassLoader().getResource(WEBAPP_RESOURCES_LOCATION);
    if (webAppDir == null) {
      throw new RuntimeException(String.format("Unable to find %s directory into the JAR file", WEBAPP_RESOURCES_LOCATION));
    }

    WebAppContext webAppContext = new WebAppContext();
    webAppContext.setContextPath("/");
    webAppContext.setDescriptor(WEBAPP_RESOURCES_LOCATION + "/WEB-INF/web.xml");
    webAppContext.setResourceBase(webAppDir.toURI().toString());
    webAppContext.setParentLoaderPriority(true);

    server.setHandler(webAppContext);

    server.start();

    server.join();
  }
}

静态资源可以从您选择的目录加载(它将在pom.xml中参数化)。

例如,我创建了 src/main/webapp 文件夹来存储静态内容。

在此文件夹中,您还需要定义 - 在本例中,由于我们设置 Jetty 的方式 - 一个包含此 web.xml 文件的 WEB-INF 目录:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">

    <servlet>
        <servlet-name>HelloWorldServlet</servlet-name>
        <servlet-class>io.happycoding.servlets.HelloWorldServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>HelloWorldServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>

</web-app>

这是我的源代码设置的 tree

pom.xml 文件与您提供的文件非常相似。我只包含 maven-resources-plugin 以将 Web 应用程序静态内容复制到 jar 文件,以及 maven-shade-plugin 以生成 UberJar:

<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.happycoding</groupId>
    <artifactId>app-engine-hello-world</artifactId>
    <version>1</version>
    <packaging>jar</packaging>

    <properties>
        <!-- App Engine currently supports Java 11 -->
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <failOnMissingWebXml>false</failOnMissingWebXml>
        <!-- Directory where static content resides -->
        <webapp.dir>./src/main/webapp</webapp.dir>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>9.4.31.v20200723</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>9.4.31.v20200723</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-util</artifactId>
            <version>9.4.31.v20200723</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-annotations</artifactId>
            <version>9.4.31.v20200723</version>
            <type>jar</type>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>io.happycoding.Main</mainClass>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.7</version>
                <executions>
                    <execution>
                        <id>copy-web-resources</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/classes/META-INF/resources</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${webapp.dir}</directory>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>io.happycoding.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
              <groupId>com.google.cloud.tools</groupId>
              <artifactId>appengine-maven-plugin</artifactId>
              <version>2.2.0</version>
              <configuration>
                <projectId>happy-coding-gcloud</projectId>
                <version>1</version>
              </configuration>
            </plugin>
        </plugins>
    </build>


</project>

使用此设置,您可以通过执行以下命令在本地 运行 应用程序:

mvn exec:java

您还可以直接从 java 工具 运行 本地程序:

java -jar appengine-deploy-sample-1.jar

抱歉,我无法在 GCP 中测试设置,但我认为,根据迁移指南,您可以尝试部署应用程序而无需在 app.yaml 中指示 entrypoint

如果它不起作用,您可以通过配置类似于以下内容的 entrypoint 来尝试 运行 应用程序:

entrypoint: 'java -jar appengine-deploy-sample-1.jar'

或者也许:

entrypoint: 'java -cp "*" -jar appengine-deploy-sample-1.jar'