Migrating your App Engine app from Java 8 to Java 11

In addition to supporting the Java 8 runtime, App Engine now supports hosting your applications using the Java 11 runtime. Learn how to migrate your existing App Engine app from Java 8 to Java 11 on the standard environment.

Key differences between the Java 8 and Java 11 runtimes

  • Starting with the Java 11 runtime, the App Engine standard environment no longer includes bundled App Engine services such as Memcache and Task Queues. Instead, Google Cloud provides standalone products that are equivalent to most of the bundled services in the Java 11 runtime. Each of these Google Cloud products provides idiomatic Cloud Java Client Libraries. For the bundled services that are not available as separate products in Google Cloud, such as image processing, search, and messaging, you can use third-party providers or other workarounds as suggested in this migration guide.

    Removing the bundled App Engine services enables the Java 11 runtime to support a fully idiomatic Java development experience. In the Java 11 runtime, you write a standard Java app that is fully portable and can run in any standard Java environment, including App Engine.

  • In the Java 11 runtime, configuration files are in the YAML format instead of the XML format. For example, you specify settings for your app in the app.yaml file instead of the appengine-web.xmlfile. You need to manually convert appengine-web.xmlfile to app.yaml file. For more information, see Creating an app.yaml file.

    For information about converting other types of App Engine configuration files to YAML, see Migrating XML to YAML file formats

  • The Java 11 runtime can execute any Java framework as long as you package a web server that is configured to respond to HTTP requests on the port specified by the PORT environment variable (recommended) or on port 8080. For example, the Java 11 runtime can run a Spring Boot UberJAR as is.

Creating an app.yaml file

In the Java 11 runtime, application settings are stored in the app.yaml file. You must remove the appengine-web.xml file from your existing application and replace it with an app.yaml file.

The only required element in an app.yaml file is the runtime element:

runtime: java11
# No need for an entrypoint with single fatjar with correct manifest class-path entry.

App Engine provides default values for all other settings, including the F1 instance class, which determines the memory and CPU resources that are available to your app, and automatic scaling, which controls how and when new instances of your app are created.

The gcloud app deploy command can create an app.yaml file when you deploy your app. The app.yaml file that App Engine creates contains only the runtime entry, and your app will use default values for all settings.

If you need to override the default settings, create an app.yaml file and specify the settings you need. For more information, see app.yaml file reference.

For a Maven project, the standard location for app.yaml file is under the src/main/appengine directory. The App Engine Maven plugin will create a correct target/appengine-staging directory containing your JAR artifacts and this app.yaml file, ready for deployment.

The following is a sample Maven project structure:

MyDir/
  pom.xml
  [index.yaml]
  [cron.yaml]
  [dispatch.yaml]
  src/main/
    appengine/
      app.yaml
    java/com.example.mycode/
      MyCode.java

If you have more than one JAR file in your project directory or want to specify a custom entrypoint, you must specify it in the entrypoint element of your app.yaml file.

Re-packaging a WAR file into a JAR file

To migrate to the Java 11 runtime, you must re-package your App Engine Java 8 web application into an executable JAR file.

Your application must have a Main class that starts a web server which responds to HTTP requests on the port specified by the PORT environment variable, typically 8081.

For example:

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class Main {

  public static void main(String[] args) throws IOException {
    // Create an instance of HttpServer bound to port defined by the 
    // PORT environment variable when present, otherwise on 8080.
    int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
    HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);

    // Set root URI path.
    server.createContext("/", (var t) -> {
      byte[] response = "Hello World from Google App Engine Java 11.".getBytes();
      t.sendResponseHeaders(200, response.length);
      try (OutputStream os = t.getResponseBody()) {
        os.write(response);
      }
    });

    // Start the server.
    server.start();
  }
}

WAR migration example

The following instructions demonstrate how to repackage an App Engine Java 8 hello-world application as a JAR to run on the Java 11 runtime.

The migration uses the appengine-simple-jetty-main artifact. This provides a Main class with a simple Jetty web server that loads a WAR file and packages your app into an executable JAR file:

  1. Clone the Embedded Jetty Server artifact to your local machine:

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples
    

    Alternatively, you can download the sample as a zip file and extract it.

  2. Change to the directory that contains the sample code:

    cd java-docs-samples/appengine-java11/appengine-simple-jetty-main/
    
  3. Install the dependency locally:

    mvn install
    
  4. Add the following code to your project pom.xml file:

    • appengine-simple-jetty-main dependency:
      <dependency>
        <groupId>com.example.appengine.demo</groupId>
        <artifactId>simple-jetty-main</artifactId>
        <version>1</version>
        <scope>provided</scope>
      </dependency>
    • maven-dependency plugin:
      <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 deploys files located in the ${build.directory}/appengine-staging directory. By adding the maven-dependency plugin to your build, App Engine installs your specified dependencies to the correct folder.
  5. Create an entrypoint element in your app.yaml file to call the appengine-simple-jetty-main object and pass your WAR file as an argument. For example, see the helloworld-servlet sample app.yaml file:

    runtime: java11
    entrypoint: 'java -cp "*" com.example.appengine.demo.jettymain.Main helloworld.war'
  6. To run your application locally:

    1. Package your application:

      mvn clean package
      
    2. Start the server with your WAR file as an argument.

      For example, you can start the server in the helloworld-servlet sample by running the following command from your java-docs-samples/appengine-java11/appengine-simple-jetty-main/ folder:

      mvn exec:java -Dexec.args="../helloworld-java8/target/helloworld.war"
      
    3. In your web browser, enter the following address:

      http://localhost:8080

  7. To deploy your application:

    gcloud tooling

    gcloud app deploy

    Maven plugin

    mvn package appengine:deploy -Dapp.deploy.projectId=PROJECT_ID

    Replace PROJECT_ID with the ID of your Cloud project. If your pom.xml file already specifies your project ID, you don't need to include the -Dapp.deploy.projectId property in the command you run.

Framework flexibility

The Java 11 runtime does not include any web-serving framework, meaning you're no longer limited to servlet-based frameworks.

There are hello world samples using popular Java web frameworks on the Google Cloud GitHub repository:

Migrating from the App Engine Java SDK

The Java 11 runtime does not support the App Engine Java SDK. The following lists some changes you may have to make to your existing App Engine Java 8 app and your deployment process in order to use the App Engine Java 11 runtime:

Migrating from the App Engine-specific APIs

Most of the functionality provided by the App Engine-specific APIs is now provided by the Google Cloud Client Library for Java. For more information, see the recommended alternatives listed below.

Blobstore

To store and retrieve data, use Cloud Storage through the Cloud Client Libraries. To get started, see Using Cloud Storage.

Datastore

For an alternative to a NoSQL key/value database like the Datastore API, use Cloud Firestore in Datastore mode. Firestore is the newest version of Datastore, and Datastore mode is recommended for databases that will be used primarily by App Engine apps.

Images

You can serve images from Cloud Storage, serve them directly, or use a third-party content delivery network (CDN).

To resize, convert, and manipulate images, use an image processing library such as ImageJ2, imgscalr, or thumbnailator. To use one of these third-party libraries, add the library as a dependency and update your code to call the library's APIs.

The App Engine Images service also provided functionality to avoid dynamic requests to your application by handling image resizing using a serving URL. If you want similar functionality, you can generate the re-sized images ahead of time and upload them to Cloud Storage for serving. Alternatively, you could use a third-party content delivery network (CDN) service that offers image resizing.

Logging

We recommend that you update your app to use Cloud Logging, which supports features such as viewing logs in the Logs Viewer, downloading logs, filtering messages by severity, and correlating app messages with specific requests. As an alternative, you can enable these features by writing log messages that contain specific data structured in a JSON object.

For more information, see Writing and viewing logs.

Mail

To send email, use a third-party mail provider such as SendGrid, Mailgun, or Mailjet. All of these services offer APIs to send emails from applications.

Memcache

To cache application data, use Memorystore for Redis.

Modules

To obtain information and modify your application's running services, use a combination of environment variables and the App Engine Admin API:

Service information How to access
Current application ID GAE_APPLICATION environment variable
Current project ID GOOGLE_CLOUD_PROJECT environment variable
Current service name GAE_SERVICE environment variable
Current service version GAE_VERSION environment variable
Current instance ID GAE_INSTANCE environment variable
Default hostname Admin API apps.get method
List of services Admin API apps.services.list method
List of versions for a service Admin API apps.services.versions.list method
Default version for a service, including any traffic splits Admin API apps.services.get method
List of running instances for a version Admin API apps.services.versions.instances.list method
For more information about the data available about your application's running services, see Java 11 Runtime Environment.

Namespaces

The Namespaces API enabled multitenant apps to partition data across tenants simply by specifying a unique namespace string for each tenant.

While Datastore supports multitenancy directly, other Google Cloud services do not. If your multitenant app uses other Google Cloud services, you will need to handle multitenancy manually. To have completely isolated instances of services, you can create new projects programmatically using the Cloud Resource Manager API and access resources across projects.

OAuth

Instead of using the App Engine OAuth service to verify OAuth 2.0 tokens, use the oauth2.tokeninfo method of the OAuth 2.0 API.

Host any full-text search database such as Elasticsearch on Compute Engine and access it from your service.

Task queue

Queue tasks for asynchronous code execution using the Cloud Tasks REST API, RPC API, or the Google Cloud Client library, and use a Java 11 App Engine standard service as a Push target. For more information, see Migrating from Task Queues to Cloud Tasks.

In many cases where you might use pull queues, such as queuing up tasks or messages that will be pulled and processed by separate workers, Pub/Sub can be a good alternative as it offers similar functionality and delivery guarantees. See the following sample program interacting with the Cloud Tasks API.

User authentication

For an alternative to the Users API, use any HTTP-based authentication mechanism such as:

Migrating XML to YAML file formats

Cloud SDK does not support the following file formats:

  • cron.xml
  • datastore-index.xml
  • dispatch.xml
  • queue.xml

The following examples demonstrate how to migrate your xml files to yaml files.

Migrating your files automatically

To migrate your xml files automatically:

  1. You must have Cloud SDK version 226.0.0 or later. To update to the latest version:

    gcloud components update
    
  2. For each file you'd like to migrate, specify one of the following subcommands (cron-xml-to-yaml, datastore-indexes-xml-to-yaml, dispatch-xml-to-yaml, queue-xml-to-yaml) and file name:

    gcloud beta app migrate-config queue-xml-to-yaml MY-QUEUE-XML-FILE.xml
    
  3. Manually double-check the converted file before deploying to production.

    For a successful sample xml to yaml file conversion, see the Migrating your files manually tabs.

Migrating your files manually

To manually migrate your xml files to yaml files:

cron.yaml

Create a cron.yaml file with a cron object containing a list of objects, each with fields that correspond to each of the <cron> tag attributes in your cron.xml file, as shown below.

Converted cron.yaml file:

cron:
- url: '/recache'
  schedule: 'every 2 minutes'
  description: 'Repopulate the cache every 2 minutes'
- url: '/weeklyreport'
  schedule: 'every monday 08:30'
  target: 'version-2'
  timezone: 'America/New_York'
  description: 'Mail out a weekly report'

Original cron.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
  <cron>
    <url>/recache</url>
    <description>Repopulate the cache every 2 minutes</description>
    <schedule>every 2 minutes</schedule>
  </cron>
  <cron>
    <url>/weeklyreport</url>
    <description>Mail out a weekly report</description>
    <schedule>every monday 08:30</schedule>
    <timezone>America/New_York</timezone>
    <target>version-2</target>
  </cron>
</cronentries>

For more information, see the cron.yaml reference documentation.

dispatch.yaml

Create a dispatch.yaml file with a dispatch object containing a list of objects, each with fields that correspond to each of the <dispatch> tag attributes in your dispatch.xml file, as shown below.

Converted dispatch.yaml file:

dispatch:
- url: '*/favicon.ico'
  module: default
- url: 'simple-sample.uc.r.appspot.com/'
  module: default
- url: '*/mobile/*'
  module: mobile-frontend

Original dispatch.xml file

<?xml version="1.0" encoding="UTF-8"?>
<dispatch-entries>
  <dispatch>
      <url>*/favicon.ico</url>
      <module>default</module>
  </dispatch>
  <dispatch>
      <url>simple-sample.uc.r.appspot.com/</url>
      <module>default</module>
  </dispatch>
  <dispatch>
      <url>*/mobile/*</url>
      <module>mobile-frontend</module>
  </dispatch>
</dispatch-entries>

For more information, see the dispatch.yaml reference documentation

index.yaml

Create an index.yaml file with an indexes object containing a list of objects, each with fields that correspond to each of the <datastore-index> tag attributes in your datastore-indexes.xml file, as shown below.

Converted index.yaml file:

indexes:
- ancestor: false
  kind: Employee
  properties:
  - direction: asc
    name: lastName
  - direction: desc
    name: hireDate
- ancestor: false
  kind: Project
  properties:
  - direction: asc
    name: dueDate
  - direction: desc
    name: cost

Original datastore-index.xml file:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes
 autoGenerate="true">
   <datastore-index kind="Employee" ancestor="false">
       <property name="lastName" direction="asc" />
       <property name="hireDate" direction="desc" />
   </datastore-index>
   <datastore-index kind="Project" ancestor="false">
       <property name="dueDate" direction="asc" />
       <property name="cost" direction="desc" />
   </datastore-index>
</datastore-indexes>

For more information, see the index.yaml reference documentation.

queue.yaml

Create a queue.yaml file with a queue object containing a list of objects, each with fields that correspond to each of the <queue> tag attributes in your queue.xml file, as shown below.

Converted queue.yaml file:

queue:
- name: fooqueue
  mode: push
  rate: 1/s
  retry_parameters:
    task_retry_limit: 7
    task_age_limit: 2d
- name: barqueue
  mode: push
  rate: 1/s
  retry_parameters:
    min_backoff_seconds: 10
    max_backoff_seconds: 200
    max_doublings: 0

Original queue.xml file:

<queue-entries>
  <queue>
    <name>fooqueue</name>
    <rate>1/s</rate>
    <retry-parameters>
      <task-retry-limit>7</task-retry-limit>
      <task-age-limit>2d</task-age-limit>
    </retry-parameters>
  </queue>
  <queue>
    <name>barqueue</name>
    <rate>1/s</rate>
    <retry-parameters>
      <min-backoff-seconds>10</min-backoff-seconds>
      <max-backoff-seconds>200</max-backoff-seconds>
      <max-doublings>0</max-doublings>
    </retry-parameters>
  </queue>
<queue-entries>