/meetup-dubai

Materials for Dubai meetup on 2019-10-30

Primary LanguageJavaApache License 2.0Apache-2.0

Dubai Meetup on Oct 30th, 2019

Prerequisites

  • Maven 3.5+
  • Java 8+
  • Docker
  • Any kind of Java IDE

Helidon MP

Create project

  1. Create a working folder (ex. helidon-dubai) and make it active.

  2. Generate project using Maven archetype:

mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-mp \
    -DarchetypeVersion=1.3.1 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-mp \
    -Dpackage=io.helidon.examples.quickstart.mp

Windows users must make it in one row by deleting backslashes and end of lines

  1. Open project in your favorite IDE.
cd helidon-quickstart-mp
idea pom.xml
  1. Build the project
mvn clean package
  1. Run the project
java -jar target/helidon-quickstart-mp.jar
  1. Test the project

In another terminal tab:

curl -X GET http://localhost:8080/greet
curl -X GET http://localhost:8080/greet/Dmitry
curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : “Ahoj"}' http://localhost:8080/greet/greeting
curl -X GET http://localhost:8080/greet/Dmitry
  1. Build a Docker image
docker build -t helidon-quickstart-mp .
  1. Run project in Docker
docker run --rm -p 8080:8080 helidon-quickstart-mp:latest

HealthCheck

HealthCheck endpoints

  1. HealthCheck 1.0 endpoint
curl -X GET http://localhost:8080/health | jq .
  1. HealthCheck 2.0 liveness and readiness checks
curl -X GET http://localhost:8080/health/live | jq .
curl -X GET http://localhost:8080/health/ready | jq .

Adding a custom readiness health check which is always 'DOWN'

  1. Create a new java class:
package io.helidon.examples.quickstart.mp;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;

import javax.enterprise.context.ApplicationScoped;

@Readiness
@ApplicationScoped
public class StateHealthCheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named("state")
                .down()
                .build();
    }
}
  1. Recompile and rerun the app
mvn clean package -DskipTests
java -jar target/helidon-quickstart-mp.jar
  1. Test
curl -X GET http://localhost:8080/health/ready | jq .

Read state from a configuration property

  1. Add app.state property to META-INF\microprofile-config.properties file.
app.state=Down
  1. Modify StateHealthCheck to read state from configuration
@Readiness
@ApplicationScoped
public class StateHealthCheck implements HealthCheck {

    @Inject
    @ConfigProperty(name = "app.state")
    private String state;

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named(“state")
                .state("up".equalsIgnoreCase(state))
                .build();
    }
}
  1. Recompile and rerun the app
mvn clean package -DskipTests
java -jar target/helidon-quickstart-mp.jar
  1. Test
curl -X GET http://localhost:8080/health/ready | jq .
  1. Pass state using a system property.
java -Dapp.state=Up -jar target/helidon-quickstart-mp.jar

Make application get notified when configuration property is changed

  1. Remove app.state property from META-INF/microprofile-config.properties file.

  2. Create mp.yaml configuration file in conf directory of your working folder.

  3. Update mp.yaml

app:
  status: "down"
  1. Crete custom configuration in Main.java
static Server startServer() {
    Config config = Config.builder()
            .sources(
                    file("../conf/mp.yaml")
                        .pollingStrategy(PollingStrategies::watch)
                        .optional(),
                    classpath("META-INF/microprofile-config.properties"))
            .build();

    return Server.builder().config(config).build().start();
}
  1. Update StateHealthCheck to support dynamic properties
@Readiness
@ApplicationScoped
public class StateHealthCheck implements HealthCheck {

    @Inject
    @ConfigProperty(name = "app.state")
    private Supplier<String> state;

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named("state")
                .state("up".equalsIgnoreCase(state.get()))
                .build();
    }
}
  1. Recompile and rerun the app
mvn clean package -DskipTests
java -jar target/helidon-quickstart-mp.jar
  1. Test (must be down)
curl -X GET http://localhost:8080/health/ready | jq .
  1. Without stopping the app update mp.yaml file. Change app.state property to up.
app:
  status: "up"
  1. Test again (must be UP)
curl -X GET http://localhost:8080/health/ready | jq .

It may take some time to propagate the change. If status is not changed to UP from the first try, re-run the command.

Metrics

Metrics endpoint

  1. Metrics in Prometeus format
curl -X GET http://localhost:8080/metrics
  1. Metrics in JSON
curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics | jq .
  1. Base metrics only
curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics/base | jq .
  1. Metrics metadata
curl -H 'Accept: application/json' -X OPTIONS http://localhost:8080/metrics | jq .

Adding custom Metrics

  1. Add @Counted metric to getDefaultMessage method
@GET
@Counted(name = "defaultCount",
    absolute = true,
    displayName = "Default Message Counter",
    description = "Number of times default message called")
@Produces(MediaType.APPLICATION_JSON)
public JsonObject getDefaultMessage() {
    return createResponse("World");
}
  1. Add @Timed metric to getMessage method
@Path("/{name}")
@GET
@Timed
@Produces(MediaType.APPLICATION_JSON)
public JsonObject getMessage(@PathParam("name") String name) {
    return createResponse(name);
}
  1. Recompile and rerun the app
mvn clean package -DskipTests
java -jar target/helidon-quickstart-mp.jar
  1. Run commands below multiple times to fill metrics data
curl -X GET http://localhost:8080/greet
curl -X GET http://localhost:8080/greet/Dmitry
  1. Test it
curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics/application | jq .
curl -H 'Accept: application/json' -X OPTIONS http://localhost:8080/metrics/application | jq .

JPA

  1. Run H2 database in docker
docker run -d -p 9092:9082 -p 8082:8082 --name=h2 nemerosa/h2
  1. Add dependencies
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>2.2.2</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.transaction</groupId>
    <artifactId>javax.transaction-api</artifactId>
    <version>1.2</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-hibernate</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-jpa</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-jta-weld</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-datasource-hikaricp</artifactId>
    <scope>runtime</scope>
</dependency>
  1. Add META-INF/persistence.xml
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
  <persistence-unit name="test" transaction-type="JTA">
    <jta-data-source>test</jta-data-source>
    <class>io.helidon.examples.quickstart.mp.Greeting</class>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
      <property name="hibernate.hbm2ddl.auto" value="create-drop" />
      <property name="show_sql" value="true"/>
      <property name="hibernate.temp.use_jdbc_metadata_defaults" value="false"/>
    </properties>
  </persistence-unit>
</persistence>
  1. Configure data source in the application configuration. Add the following to META-INF/microprofile-config.xml
javax.sql.DataSource.test.dataSourceClassName=org.h2.jdbcx.JdbcDataSource
javax.sql.DataSource.test.dataSource.url=jdbc:h2:mem:test;INIT=CREATE TABLE IF NOT EXISTS GREETING (NAME VARCHAR NOT NULL, GREETING VARCHAR NOT NULL, PRIMARY KEY (NAME))\\;MERGE INTO GREETING (NAME, GREETING) VALUES ('world', 'hello')
javax.sql.DataSource.test.dataSource.user=sa
javax.sql.DataSource.test.dataSource.password=
  1. ​​Add an entity Greeting.java
import javax.persistence.*;
import java.util.Objects;

@Access(AccessType.FIELD)
@Entity(name = "Greeting")
@Table(name = "GREETING")
public class Greeting {

    @Id
    @Column(name = "NAME", insertable = true, nullable = false, updatable = false)
    private String name;

    @Basic(optional = false)
    @Column(name = "GREETING", insertable = true, nullable = false, updatable = true)
    private String greeting;

    @Deprecated
    protected Greeting() {
        super();
    }

    public Greeting(final String name, final String greeting) {
        super();
        this.name = Objects.requireNonNull(name);
        this.greeting = Objects.requireNonNull(greeting);
    }

    public void setGreeting(final String greeting) {
        this.greeting = Objects.requireNonNull(greeting);
    }

    @Override
    public String toString() {
        return this.greeting;
    }

    public String getGreeting() {
        return greeting;
    }
}
  1. Modify GreetResource.createResponse method
@PersistenceContext
private EntityManager entityManager;

private JsonObject createResponse(String who) {
    Greeting greeting = this.entityManager.find(Greeting.class, who);
    String message;
    if (null == greeting) {
        // not in database
        message = greetingProvider.getMessage();
    } else {
        message = greeting.getGreeting();
    }
    String msg = String.format("%s %s!", message, who);

    return JSON.createObjectBuilder()
            .add("message", msg)
            .build();
}
  1. Add GreetResource.dbCreateMapping method
@Path("/db/{name}")
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response dbCreateMapping(@PathParam("name") String name, String greeting) {
    Greeting g = new Greeting(name, greeting);
    this.entityManager.persist(g);

    return Response.created(URI.create("/greet/" + name)).build();
}
  1. Test it
curl -i -X POST -H 'Content-Type:text/plain' -d 'Use' http://localhost:8080/greet/db/helidon

curl -i http://localhost:8080/greet/helidon
  1. Add GreetResource.dbUpdateMapping method
@Path("/db/{name}")
@PUT
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response dbUpdateMapping(@PathParam("name") String name, String greeting) {
    try {
        Greeting g = this.entityManager.getReference(Greeting.class, name);
        g.setGreeting(greeting);
    } catch (EntityNotFoundException e) {
        return Response.status(404).entity("Mapping for " + name + " not found").build();
    }

    return Response.ok(name).build();
}
  1. Test it
curl -i -X PUT -H 'Content-Type:text/plain' -d 'I am using' http://localhost:8080/greet/db/helidon

curl -i http://localhost:8080/greet/helidon

Helidon SE

Create project

  1. Create Helidon MP Quickstart project:
mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion=1.3.1 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-se \
    -Dpackage=io.helidon.examples.quickstart.se
  1. Open it in IDE of your choice
idea pom.xml
  1. Build the project
mvn package
  1. Run the app
java -jar target/helidon-quickstart-se.jar
  1. In a new terminal window demonstrate that all endpoints work
curl -X GET http://localhost:8080/greet
curl -X GET http://localhost:8080/greet/Dmitry
curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : “Ahoj"}' http://localhost:8080/greet/greeting
curl -X GET http://localhost:8080/greet/Dmitry
  1. Health check works too
curl -X GET http://localhost:8080/health
  1. and metrics
curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics | json_pp

Make GraalVM native image

  1. Download GraalVM 19.2.1 and extract it to ~/graalvm directory.

  2. Install native-image feature

sudo yum install gcc, zlib-devel
~/graalvm/graalvm-ce-19.2.1/bin/gu install native-image
  1. Create GRAALVM_HOME environment variable
export GRAALVM_HOME=~/graalvm/graalvm-ce-19.2.1
  1. Build a native image
mvn package -DskipTests -Pnative-image
  1. Run it
./target/helidon-quickstart-se
  1. Test endpoints
curl -X GET http://localhost:8080/greet
curl -X GET http://localhost:8080/greet/Dmitry
curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : “Ahoj"}' http://localhost:8080/greet/greeting
curl -X GET http://localhost:8080/greet/Dmitry

Compare startup times on JVM and GraalVM

  1. Modify source code the way that app stops after web server initialization
server.start()
    .thenAccept(ws -> {
        System.exit(0);
        System.out.println(
                "WEB server is up! http://localhost:" + ws.port() + "/greet");
        ws.whenShutdown().thenRun(()
            -> System.out.println("WEB server is DOWN. Good bye!"));
        })
    .exceptionally(t -> {
        System.err.println("Startup failed: " + t.getMessage());
        t.printStackTrace(System.err);
        return null;
    });
  1. Compile and run on JVM
mvn package -DskipTests
time java -jar target/helidon-quickstart-se.jar
  1. Compile and run on GraalVM
mvn package -DskipTests -Pnative-image
time ./target/helidon-quickstart-se