Browse Source

Add Redkale Framework (#3550)

* update config
Redkale 7 years ago
parent
commit
6fe18978cd

+ 1 - 0
.travis.yml

@@ -94,6 +94,7 @@ env:
      - "TESTDIR=Java/play2-java"
      - "TESTDIR=Java/proteus"
      - "TESTDIR=Java/rapidoid"
+     - "TESTDIR=Java/redkale"
      - "TESTDIR=Java/restexpress"
      - "TESTDIR=Java/revenj-jvm"
      - "TESTDIR=Java/servlet"

+ 58 - 0
frameworks/Java/redkale/README.md

@@ -0,0 +1,58 @@
+# Vertx Benchmarking Test
+
+This is the Redkale portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+### Plaintext Test
+
+* [Plaintext test source](src/main/java/org/redkalex/benchmark/Servlet.java)
+
+### JSON Serialization Test
+
+* [JSON test source](src/main/java/org/redkalex/benchmark/Servlet.java)
+
+### Database Query Test
+
+* [Database Query test source](src/main/java/org/redkalex/benchmark/Servlet.java)
+
+### Database Queries Test
+
+* [Database Queries test source](src/main/java/org/redkalex/benchmark/Servlet.java)
+
+### Database Update Test
+
+* [Database Update test source](src/main/java/org/redkalex/benchmark/Servlet.java)
+
+### Template rendering Test
+
+* [Template rendering test source](src/main/java/org/redkalex/benchmark/Servlet.java)
+
+## Versions
+
+* [Java OpenJDK 1.8](http://openjdk.java.net/)
+* [Redkale 1.9.3](http://redkale.org/)
+
+## Test URLs
+
+### Plaintext Test
+
+    http://localhost:8080/plaintext
+
+### JSON Encoding Test
+
+    http://localhost:8080/json
+
+### Database Query Test
+
+    http://localhost:8080/db
+
+### Database Queries Test
+
+    http://localhost:8080/queries?queries=5
+
+### Database Update Test
+
+    http://localhost:8080/updates?queries=5
+
+### Template rendering Test
+
+    http://localhost:8080/fortunes

+ 47 - 0
frameworks/Java/redkale/benchmark_config.json

@@ -0,0 +1,47 @@
+{
+  "framework": "redkale",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "None",
+      "framework": "None",
+      "language": "Java",
+      "flavor": "None",
+      "orm": "Raw",
+      "platform": "Redkale",
+      "webserver": "Redkale",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "redkale",
+      "notes": "",
+      "versus": ""
+    },
+    "postgres": {
+      "setup_file": "setup_postgres",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "Postgres",
+      "framework": "None",
+      "language": "Java",
+      "flavor": "None",
+      "orm": "Raw",
+      "platform": "Redkale",
+      "webserver": "Redkale",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "redkale-postgres",
+      "notes": "",
+      "versus": ""
+    }
+  }]
+}

+ 25 - 0
frameworks/Java/redkale/conf/application.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<application port="8585">     
+    
+    <!--  see: http://redkale.org/redkale.html#redkale_confxml  -->
+    
+    <resources>
+		<properties>
+            <property name="system.property.http.response.header.server" value="redkale"/>
+        </properties>
+	</resources> 
+        
+    <server protocol="HTTP" host="0.0.0.0" port="8080" threads="128" lib="target">      
+                
+        <response>
+			<contenttype plain="text/plain" json="application/json"/>    
+		</response>
+
+        <services autoload="true"/>
+        
+        <servlets autoload="true"/>
+        
+    </server>
+    
+</application>

+ 24 - 0
frameworks/Java/redkale/conf/logging.properties

@@ -0,0 +1,24 @@
+
+
+handlers = java.util.logging.ConsoleHandler
+
+############################################################
+.level = INFO
+
+java.level = INFO
+javax.level = INFO
+com.sun.level = INFO
+sun.level = INFO
+jdk.level = INFO
+
+
+java.util.logging.FileHandler.level = INFO
+#10M
+java.util.logging.FileHandler.limit = 10485760
+java.util.logging.FileHandler.count = 10000
+java.util.logging.FileHandler.encoding = UTF-8
+java.util.logging.FileHandler.pattern = ${APP_HOME}/logs-%m/log-%d.log
+java.util.logging.FileHandler.unusual = ${APP_HOME}/logs-%m/log-warnerr-%d.log
+java.util.logging.FileHandler.append = true
+
+java.util.logging.ConsoleHandler.level = INFO

+ 16 - 0
frameworks/Java/redkale/conf/persistence.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
+    
+    <persistence-unit name="" transaction-type="RESOURCE_LOCAL">
+        <shared-cache-mode>ALL</shared-cache-mode>
+        <properties>
+            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://tfb-database:5432/hello_world"/>
+            <property name="javax.persistence.connections.limit" value="256"/>
+            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
+            <property name="javax.persistence.jdbc.user" value="benchmarkdbuser"/>
+            <property name="javax.persistence.jdbc.password" value="benchmarkdbpass"/>
+        </properties>
+    </persistence-unit>
+            
+</persistence>

+ 94 - 0
frameworks/Java/redkale/pom.xml

@@ -0,0 +1,94 @@
+<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>org.redkalex</groupId>
+	<artifactId>redkale-benchmark</artifactId>
+	<version>0.0.1</version>
+
+	<properties>
+		<!-- the main class -->
+		<main.class>org.redkalex.benchmark.Servlet</main.class>
+		<stack.version>0.0.1</stack.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.redkale</groupId>
+			<artifactId>redkale</artifactId>
+			<version>1.9.2</version>
+		</dependency>
+		<dependency>
+			<groupId>org.postgresql</groupId>
+			<artifactId>postgresql</artifactId>
+			<version>42.2.2</version>
+		</dependency>
+		<dependency>
+			<groupId>com.fizzed</groupId>
+			<artifactId>rocker-compiler</artifactId>
+			<version>0.24.0</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+                <version>3.5.1</version>                         
+				<configuration>
+                    <encoding>UTF-8</encoding>
+					<source>1.8</source>
+					<target>1.8</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>com.fizzed</groupId>
+				<artifactId>rocker-maven-plugin</artifactId>
+				<version>0.24.0</version>
+				<executions>
+					<execution>
+						<id>generate-rocker-templates</id>
+						<phase>generate-sources</phase>
+						<goals>
+							<goal>generate</goal>
+						</goals>
+						<configuration>
+							<javaVersion>1.8</javaVersion>
+							<templateDirectory>${basedir}/src/main/templates</templateDirectory>
+							<outputDirectory>${basedir}/target/generated-sources/rocker</outputDirectory>
+							<discardLogicWhitespace>false</discardLogicWhitespace>
+							<addAsSources>true</addAsSources>
+							<optimize>true</optimize>
+							<failOnError>true</failOnError>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-shade-plugin</artifactId>
+				<version>3.1.1</version>
+				<executions>
+					<execution>
+						<phase>package</phase>
+						<goals>
+							<goal>shade</goal>
+						</goals>
+						<configuration>
+							<transformers>
+								<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+								  <mainClass>${main.class}</mainClass>
+								</transformer>
+							  </transformers>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+
+	</build>
+
+</project>

+ 7 - 0
frameworks/Java/redkale/redkale-postgres.dockerfile

@@ -0,0 +1,7 @@
+FROM maven:3.5.3-jdk-10-slim
+WORKDIR /redkale
+COPY src src
+COPY conf conf
+COPY pom.xml pom.xml
+RUN mvn package -q
+CMD ["java", "-DAPP_HOME=./", "-jar", "target/redkale-benchmark-0.0.1.jar"]

+ 7 - 0
frameworks/Java/redkale/redkale.dockerfile

@@ -0,0 +1,7 @@
+FROM maven:3.5.3-jdk-10-slim
+WORKDIR /redkale
+COPY src src
+COPY conf conf
+COPY pom.xml pom.xml
+RUN mvn package -q
+CMD ["java", "-DAPP_HOME=./", "-jar", "target/redkale-benchmark-0.0.1.jar"]

+ 5 - 0
frameworks/Java/redkale/setup.bat

@@ -0,0 +1,5 @@
+@ECHO OFF
+
+call mvn clean package
+
+call java -DAPP_HOME=./ -jar target/redkale-benchmark-0.0.1.jar 

+ 5 - 0
frameworks/Java/redkale/source_code

@@ -0,0 +1,5 @@
+./redkale/src/main/java/org/redkalex/benchmark/Servlet.java
+./redkale/src/main/java/org/redkalex/benchmark/Service.java
+./redkale/src/main/java/org/redkalex/benchmark/Fortune.java
+./redkale/src/main/java/org/redkalex/benchmark/Message.java
+./redkale/src/main/java/org/redkalex/benchmark/World.java

+ 56 - 0
frameworks/Java/redkale/src/main/java/org/redkalex/benchmark/Fortune.java

@@ -0,0 +1,56 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.redkalex.benchmark;
+
+import javax.persistence.Id;
+import org.redkale.convert.json.JsonConvert;
+
+/**
+ *
+ * @author zhangjx
+ */
+public class Fortune implements Comparable<Fortune> {
+
+    @Id
+    private int id;
+
+    private String message = "";
+
+    public Fortune() {
+    }
+
+    public Fortune(int id, String message) {
+        this.id = id;
+        this.message = message;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    @Override
+    public int compareTo(Fortune o) {
+        return message.compareTo(o.message);
+    }
+
+    @Override
+    public String toString() {
+        return JsonConvert.root().convertTo(this);
+    }
+
+}

+ 37 - 0
frameworks/Java/redkale/src/main/java/org/redkalex/benchmark/Message.java

@@ -0,0 +1,37 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.redkalex.benchmark;
+
+import org.redkale.convert.json.JsonConvert;
+
+/**
+ *
+ * @author zhangjx
+ */
+public class Message { 
+
+    private String message;
+
+    public Message() {
+    }
+
+    public Message(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    @Override
+    public String toString() {
+        return JsonConvert.root().convertTo(this);
+    }
+}

+ 64 - 0
frameworks/Java/redkale/src/main/java/org/redkalex/benchmark/Service.java

@@ -0,0 +1,64 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.redkalex.benchmark;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import javax.annotation.Resource;
+import org.redkale.source.DataSource;
+
+/**
+ *
+ * @author zhangjx
+ */
+@SuppressWarnings("unchecked")
+public class Service extends org.redkale.service.AbstractService {
+
+    private final Random random = new Random();
+
+    @Resource
+    private DataSource source;
+
+    public World findWorld() {
+        return source.find(World.class, randomId());
+    }
+
+    public CompletableFuture<World[]> queryWorld(int count) {
+        count = Math.min(500, Math.max(1, count));
+        final World[] rs = new World[count];
+        final CompletableFuture<World>[] futures = new CompletableFuture[count];
+        for (int i = 0; i < count; i++) {
+            final int index = i;
+            futures[i] = source.findAsync(World.class, randomId()).whenComplete((w, t) -> rs[index] = w);
+        }
+        return CompletableFuture.allOf(futures).thenApply((r) -> rs);
+    }
+
+    public CompletableFuture<World[]> updateWorld(int count) {
+        count = Math.min(500, Math.max(1, count));
+        final World[] rs = new World[count];
+        final CompletableFuture<World>[] futures = new CompletableFuture[count];
+        for (int i = 0; i < count; i++) {
+            final int index = i;
+            futures[i] = source.findAsync(World.class, randomId()).whenComplete((w, t) -> {
+                rs[index] = w;
+                rs[index].setRandomNumber(randomId());
+            });
+        }
+        return CompletableFuture.allOf(futures).thenApply((r) -> {
+            source.update(rs);
+            return rs;
+        });
+    }
+
+    public List<Fortune> queryFortune() {
+        return source.queryList(Fortune.class);
+    }
+
+    private int randomId() {
+        return 1 + random.nextInt(10000);
+    }
+}

+ 89 - 0
frameworks/Java/redkale/src/main/java/org/redkalex/benchmark/Servlet.java

@@ -0,0 +1,89 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.redkalex.benchmark;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.util.*;
+import javax.annotation.Resource;
+import org.redkale.convert.json.JsonConvert;
+import org.redkale.net.http.*;
+
+/**
+ *
+ * @author zhangjx
+ */
+@WebServlet(value = {"/json", "/plaintext", "/db", "/queries", "/updates", "/fortunes"}, repair = false)
+public class Servlet extends HttpServlet {
+
+    private static final ByteBuffer helloBuffer = ByteBuffer.wrap("Hello, world!".getBytes()).asReadOnlyBuffer();
+
+    @Resource
+    private JsonConvert convert;
+
+    @Resource
+    private Service service;
+
+    @HttpMapping(url = "/json")
+    public void json(HttpRequest request, HttpResponse response) throws IOException {
+        ByteBuffer[] buffers = convert.convertTo(response.getBufferSupplier(), new Message("Hello, World!"));
+        response.setContentType("application/json").finish(buffers);
+    }
+
+    @HttpMapping(url = "/plaintext")
+    public void plaintext(HttpRequest request, HttpResponse response) throws IOException {
+        response.setContentType("text/plain").finish(helloBuffer.duplicate());
+    }
+
+    @HttpMapping(url = "/db")
+    public void db(HttpRequest request, HttpResponse response) throws IOException {
+        ByteBuffer[] buffers = convert.convertTo(response.getBufferSupplier(), service.findWorld());
+        response.setContentType("application/json").finish(buffers);
+    }
+
+    @HttpMapping(url = "/queries")
+    public void queries(HttpRequest request, HttpResponse response) throws IOException {
+        int count = getQueries(request);
+        service.queryWorld(count).whenComplete((obj, t) -> {
+            ByteBuffer[] buffers = convert.convertTo(response.getBufferSupplier(), obj);
+            response.setContentType("application/json").finish(buffers);
+        });
+    }
+
+    @HttpMapping(url = "/updates")
+    public void updates(HttpRequest request, HttpResponse response) throws IOException {
+        int count = getQueries(request);
+        service.updateWorld(count).whenComplete((obj, t) -> {
+            ByteBuffer[] buffers = convert.convertTo(response.getBufferSupplier(), obj);
+            response.setContentType("application/json").finish(buffers);
+        });
+    }
+
+    @HttpMapping(url = "/fortunes")
+    public void fortunes(HttpRequest request, HttpResponse response) throws IOException {
+        List<Fortune> fortunes = service.queryFortune();
+        fortunes.add(new Fortune(0, "Additional fortune added at request time."));
+        Collections.sort(fortunes);
+        response.setContentType("text/html; charset=UTF-8").finish(FortunesTemplate.template(fortunes).render().toString());
+    }
+
+    private static int getQueries(HttpRequest request) {
+        try {
+            return request.getIntParameter("queries", 1);
+        } catch (Exception e) {
+            return 1;
+        }
+    }
+
+    public static void main(String[] args) throws Throwable {
+        org.redkale.boot.Application.main(args);
+//        JavaGeneratorRunnable jgr = new JavaGeneratorRunnable();
+//        jgr.setTemplateDirectory(new File("D:\\Java-Projects\\FrameworkBenchmarks\\frameworks\\Java\\redkale\\src\\main\\templates"));
+//        jgr.setOutputDirectory(new File("D:\\Java-Projects\\RedkaleBenchmarkProject\\src"));
+//        jgr.run();
+    }
+
+}

+ 48 - 0
frameworks/Java/redkale/src/main/java/org/redkalex/benchmark/World.java

@@ -0,0 +1,48 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.redkalex.benchmark;
+
+import javax.persistence.Id;
+import org.redkale.convert.json.JsonConvert;
+
+/**
+ *
+ * @author zhangjx
+ */
+public class World implements Comparable<World> {
+
+    @Id
+    private int id;
+
+    private int randomNumber;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public int getRandomNumber() {
+        return randomNumber;
+    }
+
+    public void setRandomNumber(int randomNumber) {
+        this.randomNumber = randomNumber;
+    }
+
+    @Override
+    public int compareTo(World o) {
+        return Integer.compare(id, o.id);
+    }
+
+    @Override
+    public String toString() {
+        return JsonConvert.root().convertTo(this);
+    }
+
+}

+ 65 - 0
frameworks/Java/redkale/src/main/resources/create-postgres.sql

@@ -0,0 +1,65 @@
+
+DROP TABLE IF EXISTS world;
+CREATE TABLE  world (
+  id integer NOT NULL default 0,
+  randomNumber integer NOT NULL default 0,
+  PRIMARY KEY  (id)
+);
+GRANT SELECT, UPDATE ON world to benchmarkdbuser;
+
+INSERT INTO world (id, randomnumber)
+SELECT x.id, random() * 10000 + 1 FROM generate_series(1,10000) as x(id);
+
+DROP TABLE IF EXISTS fortune;
+CREATE TABLE fortune (
+  id integer NOT NULL default 0,
+  message varchar(2048) NOT NULL default '',
+  PRIMARY KEY  (id)
+);
+GRANT SELECT ON fortune to benchmarkdbuser;
+
+INSERT INTO fortune (id, message) VALUES (1, 'fortune: No such file or directory');
+INSERT INTO fortune (id, message) VALUES (2, 'A computer scientist is someone who fixes things that aren''t broken.');
+INSERT INTO fortune (id, message) VALUES (3, 'After enough decimal places, nobody gives a damn.');
+INSERT INTO fortune (id, message) VALUES (4, 'A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1');
+INSERT INTO fortune (id, message) VALUES (5, 'A computer program does what you tell it to do, not what you want it to do.');
+INSERT INTO fortune (id, message) VALUES (6, 'Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen');
+INSERT INTO fortune (id, message) VALUES (7, 'Any program that runs right is obsolete.');
+INSERT INTO fortune (id, message) VALUES (8, 'A list is only as strong as its weakest link. — Donald Knuth');
+INSERT INTO fortune (id, message) VALUES (9, 'Feature: A bug with seniority.');
+INSERT INTO fortune (id, message) VALUES (10, 'Computers make very fast, very accurate mistakes.');
+INSERT INTO fortune (id, message) VALUES (11, '<script>alert("This should not be displayed in a browser alert box.");</script>');
+INSERT INTO fortune (id, message) VALUES (12, 'フレームワークのベンチマーク');
+
+
+DROP TABLE IF EXISTS "world";
+CREATE TABLE  "world" (
+  id integer NOT NULL default 0,
+  randomNumber integer NOT NULL default 0,
+  PRIMARY KEY  (id)
+);
+GRANT SELECT, UPDATE ON "world" to benchmarkdbuser;
+
+INSERT INTO "world" (id, randomnumber)
+SELECT x.id, random() * 10000 + 1 FROM generate_series(1,10000) as x(id);
+
+DROP TABLE IF EXISTS "fortune";
+CREATE TABLE "fortune" (
+  id integer NOT NULL default 0,
+  message varchar(2048) NOT NULL default '',
+  PRIMARY KEY  (id)
+);
+GRANT SELECT ON "fortune" to benchmarkdbuser;
+
+INSERT INTO "fortune" (id, message) VALUES (1, 'fortune: No such file or directory');
+INSERT INTO "fortune" (id, message) VALUES (2, 'A computer scientist is someone who fixes things that aren''t broken.');
+INSERT INTO "fortune" (id, message) VALUES (3, 'After enough decimal places, nobody gives a damn.');
+INSERT INTO "fortune" (id, message) VALUES (4, 'A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1');
+INSERT INTO "fortune" (id, message) VALUES (5, 'A computer program does what you tell it to do, not what you want it to do.');
+INSERT INTO "fortune" (id, message) VALUES (6, 'Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen');
+INSERT INTO "fortune" (id, message) VALUES (7, 'Any program that runs right is obsolete.');
+INSERT INTO "fortune" (id, message) VALUES (8, 'A list is only as strong as its weakest link. — Donald Knuth');
+INSERT INTO "fortune" (id, message) VALUES (9, 'Feature: A bug with seniority.');
+INSERT INTO "fortune" (id, message) VALUES (10, 'Computers make very fast, very accurate mistakes.');
+INSERT INTO "fortune" (id, message) VALUES (11, '<script>alert("This should not be displayed in a browser alert box.");</script>');
+INSERT INTO "fortune" (id, message) VALUES (12, 'フレームワークのベンチマーク');

+ 18 - 0
frameworks/Java/redkale/src/main/templates/org/redkalex/benchmark/FortunesTemplate.rocker.html

@@ -0,0 +1,18 @@
+@import org.redkalex.benchmark.Fortune
+@import java.util.List
+@args(List<Fortune> fortunes)
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+  <tr>
+    <th>id</th>
+    <th>message</th>
+  </tr> @for (fortune : fortunes) {
+  <tr>
+    <td>@fortune.getId()</td>
+    <td>@fortune.getMessage()</td>
+  </tr> } </table>
+</body>
+</html>