Unreal MMO Development with Yaz

4. Java and Micronaut – The easy way to get started

Make a game server with Java Micronaut, Jooq + Posgtgres + Flyway

In the previous post, we had our java app store characters movements and send them to UE client.

We will be using some of these tools here:

In this Youtube we briefly go over what each library is and its configuration:

Going over components and their configurations

Note the code will be available on Github with link at the end of this post. Read on to understand what it entails.

Now we need to start introducing some persistence into the models. Before we start saving the characters information, we need to sort out user accounts. To save account data, we will want to use a SQL database, in this case a PostgreSQL database. For the time being, we’ll only use the PostgreSQL database for accounts, and not for instance character information. The reason is that it doesn’t scale well, there’s other databases such as MongoDB that will be much more suited for that.

We need the SQL database for account generation because it’s “Structured Query Language”. We can make rules for columns, such as length and uniqueness. We can also make rules in SQL statement saying to create an account if there’s not one with same name. It can be possible with other NoSQL databases, but there’s more edge cases.

Let’s get started, this is a config heavy post.

Pre-requisite: You will need to install docker on your machine for this as we will have postgres run on that.

Build.gradle

First, we will look at your build.gradle file.

Here’s how it will look at the end:

buildscript {
    apply from: 'docker.gradle'
}

plugins {
    id("com.github.johnrengelman.shadow") version "6.1.0"
    id("io.micronaut.application") version "1.4.2"
    id 'nu.studer.jooq' version '5.2'
    id "org.flywaydb.flyway" version "7.7.2"
    id 'com.avast.gradle.docker-compose' version '0.12.1'
}

version = "0.1"
group = "mmo_server"

def dbDriver = System.getenv('DATABASE_DRIVER') ?: 'org.postgresql.Driver'
def dbUser = System.getenv('DATABASE_USER') ?: 'mmo_server'
def dbPassword = System.getenv('DATABASE_PASSWORD') ?: 'password123'
def dbHost = System.getenv('DATABASE_HOST') ?: '127.0.0.1'
def dbPort = System.getenv('DATABASE_PORT') ?: 5432
def dbName = System.getenv('DATABASE_NAME') ?: 'mmo_server'
def dbUrl = System.getenv('DATABASE_URL') ?: "jdbc:postgresql://$dbHost:$dbPort/$dbName"

jooq {
    configurations {
        main {
            generationTool {
                logging = org.jooq.meta.jaxb.Logging.WARN
                jdbc {
                    driver = dbDriver
                    url = dbUrl
                    user = dbUser
                    password = dbPassword
                    properties {
                        // SSL disabled at the moment (only running locally)
                        property {
                            key = 'ssl'
                            value = 'false'
                        }
                    }
                }
                generator {
                    name = 'org.jooq.codegen.DefaultGenerator'
                    database {
                        name = 'org.jooq.meta.postgres.PostgresDatabase'
                        inputSchema = 'public'
                        forcedTypes {
                            forcedType {
                                name = 'varchar'
                                includeExpression = '.*'
                                includeTypes = 'JSONB?'
                            }
                            forcedType {
                                name = 'varchar'
                                includeExpression = '.*'
                                includeTypes = 'INET'
                            }
                        }
                    }
                    generate {
                        // JPA annotations required for micronaut to annotate the generated classes correctly
                        fluentSetters = true
                        validationAnnotations = true
                        jpaAnnotations = true
                        pojos = true
                        daos = true

                    }
                    target {
                        packageName = 'com.org.mmo_server.repository.model'
                    }
                    strategy.name = 'org.jooq.codegen.DefaultGeneratorStrategy'
                }
            }
        }
    }
}

task migratePrimaryDB(type: org.flywaydb.gradle.task.FlywayMigrateTask) {
    locations = ['filesystem:src/main/resources/db/postgres/']
    url = dbUrl
    user = dbUser
    password = dbPassword
    baselineOnMigrate = true
    validateOnMigrate = false
}

// make sure Jooq can only run after we've finished creating db and migrating it
tasks.generateJooq.mustRunAfter(tasks.migratePrimaryDB)
tasks.compileJava.dependsOn(tasks.migratePrimaryDB)
tasks.migratePrimaryDB.dependsOn(tasks.composeUp)

repositories {
    mavenCentral()
}

micronaut {
    runtime("netty")
    testRuntime("junit5")
    processing {
        incremental(true)
        annotations("mmo_server.*")
    }
}

dependencies {
    implementation("io.micronaut:micronaut-http-client")
    implementation("io.micronaut:micronaut-runtime")
    implementation("io.micronaut:micronaut-validation")
    runtimeOnly("ch.qos.logback:logback-classic")
    annotationProcessor("io.micronaut.data:micronaut-data-processor:2.3.1")

    // Lombok:
    compileOnly 'org.projectlombok:lombok:1.18.16'
    annotationProcessor 'org.projectlombok:lombok:1.18.16'

    // Micronaut inject
    annotationProcessor(platform("io.micronaut:micronaut-bom:2.0.2"))
    annotationProcessor("io.micronaut:micronaut-inject-java")
    implementation(platform("io.micronaut:micronaut-bom:2.0.2"))
    implementation("io.micronaut:micronaut-inject")

    // database configs
    implementation("io.micronaut.flyway:micronaut-flyway")
    implementation("io.micronaut.sql:micronaut-jdbc-hikari")
    implementation("io.micronaut.sql:micronaut-jooq")
    jooqGenerator 'org.postgresql:postgresql:42.2.14'
    implementation 'org.postgresql:postgresql:42.2.14'
}

application {
    mainClass.set("mmo_server.Application")
}

java {
    sourceCompatibility = JavaVersion.toVersion("1.8")
    targetCompatibility = JavaVersion.toVersion("1.8")
}

dockerCompose {
    useComposeFiles = ['./docker-compose.yml']
    stopContainers = true
    // do not wait for debug port
    tcpPortsToIgnoreWhenWaiting = [5005]
}

Build.gradle explained

Docker for Postgres

The first thing we’ll look at is the docker configuration in build.gradle.

We add a plugin:

id 'com.avast.gradle.docker-compose' version '0.12.1'

This allows us to use docker compose, which is defined close to end of file:

dockerCompose {
    useComposeFiles = ['./docker-compose.yml']
    stopContainers = true
    // do not wait for debug port
    tcpPortsToIgnoreWhenWaiting = [5005]
}

This indicates that we’re using a docker-compose.yml file which has the following entry:

version: '2'
services:
  mmo_server:
    image: postgres:9.6
    environment:
      - POSTGRES_DB=mmo_server
      - POSTGRES_USER=mmo_server
      - POSTGRES_PASSWORD=password123
      - PGDATA=/var/lib/pgsql_docker/data
    ports:
      - "5432:5432"

Here, put all the database settings that you need. We don’t add a Dockerfile as we’re using the out-of-the-box image of postgres:9.6.

To make sure that we run this docker-compose, I add this following line:

tasks.migratePrimaryDB.dependsOn(tasks.composeUp)

Which means that if we want to run our flyway migrate script (will cover soon after), gradle will need to first run compose up (essentially starting docker and postgres database).

Note that if you have local version of psql running, or another dockerised postgres running, you should probably stop them as they will have port conflicts – it will just silently fail.

to do this just check running docker containers with docker ps and docker kill <container> any container you want to shut down.

and check if your local psql is running by typing psql and if it comes up you can do something like

which psql                                                                                                                          
/usr/local/opt/postgresql@11/bin/psql
brew services stop postgresql@11

if you’re running on Mac on this example.

Flyway

Flyway is used to migrate our database (add new tables, columns etc), we add it with a plugin:

    id "org.flywaydb.flyway" version "7.7.2"

We then configure a couple variables – make sure they match those in docker.gradle – these are to configure connections to db for tools like flyway/jooq.

def dbDriver = System.getenv('DATABASE_DRIVER') ?: 'org.postgresql.Driver'
def dbUser = System.getenv('DATABASE_USER') ?: 'mmo_server'
def dbPassword = System.getenv('DATABASE_PASSWORD') ?: 'password123'
def dbHost = System.getenv('DATABASE_HOST') ?: '127.0.0.1'
def dbPort = System.getenv('DATABASE_PORT') ?: 5432
def dbName = System.getenv('DATABASE_NAME') ?: 'mmo_server'
def dbUrl = System.getenv('DATABASE_URL') ?: "jdbc:postgresql://$dbHost:$dbPort/$dbName"

...

task migratePrimaryDB(type: org.flywaydb.gradle.task.FlywayMigrateTask) {
    locations = ['filesystem:src/main/resources/db/postgres/']
    url = dbUrl
    user = dbUser
    password = dbPassword
    baselineOnMigrate = true
    validateOnMigrate = false
}

Take note of the variables, they are also used for Jooq too, which we cover soon after.

Important part is that the task name is migratePrimaryDB and that the location it looks to find is in:

locations = ['filesystem:src/main/resources/db/postgres/']

We mentioned that this task must run after postgres database is initialised. We also created a migration file: resources/db/postgres/V1__create_user_table.sql

CREATE TABLE IF NOT EXISTS users
(
    username                  varchar(50)   not null,
    email                     varchar(100)  not null,
    password                  varchar(100)  not null,
    enabled                   boolean       not null default false,
    created_at                timestamp     not null,
    updated_at                timestamp     not null,
    last_logged_in_at         timestamp     not null,
    primary key(username)
);

CREATE UNIQUE INDEX idx_users_username ON users(username);
CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username_password ON users(username, password);

CREATE TABLE IF NOT EXISTS user_roles
(
    user_role_id              SERIAL PRIMARY KEY,
    username                  varchar(50)   not null,
    role                      varchar(20)   not null,
    unique(username, role),
    foreign key (username) references users (username)
);

insert into users(username, email, password, enabled, created_at, updated_at, last_logged_in_at) values ('test', 'test1', 'test3', true, NOW(), NOW(), NOW());
insert into user_roles(username, role) values ('test', 'ROLE_USER');

this creates a users and user_roles table. Feel free to modify them as you wish – or create a table that you need entirely. In the near future I am planning to cover JWT authentication so I would like to enorporate user roles as well.

I also included 2 insert statements, one for test user and one for a role for that user. You don’t need to do this, this is only for test purposes.

Jooq

Jooq is one of the key components of this post, it is kind of like an ORM here (Object Relational Mapping).

From Jooq, we want to create the database POJOs, DAOs and DSLContext in order to read or write the data from database.

The jooq configuration is fairly log, but I will highlight the important bits:

property {
    key = 'ssl'
    value = 'false'
}

Since this is using the dockerised default database, it will not have ssl by default so you will need this set to false.

generate {
    fluentSetters = true
    validationAnnotations = true
    jpaAnnotations = true
    pojos = true
    daos = true
}
target {
    packageName = 'com.org.mmo_server.repository.model'
}

Here we specify what we want jooq to generate.

For micronaut its required to have: jpaAnnotations = true. Set the things you require, here we will demo the daos and pojos. Update packageName to what you like, you will just need to reference this for java component scan.

To make Jooq work with Micronaut, we will need a couple additions in resources/application.yml

micronaut:
  application:
    name: mmo_server
  server:
    port: 8081

jooq:
  datasources:
    default:
      sql-dialect: 'POSTGRES'
jpa:
  default:
    entity-scan:
      classpath: true
      packages:
        - 'com.org.mmo_server.repository.model'

datasources:
  default:
    url: jdbc:postgresql://127.0.0.1/mmo_server
    username: mmo_server
    password: password123
    driverClassName: org.postgresql.Driver
    hikari:
      pool-name: ca-pool
      minimum-idle: 2
      maximum-pool-size: 5
      data-source-properties:
        cachePrepStmts: true
        prepStmtCacheSize: 250
        prepStmtCacheSqlLimit: 2048
        useServerPrepStmts: true
        useLocalSessionState: true
        rewriteBatchedStatements: true
        cacheResultSetMetadata: true
        cacheServerConfiguration: true
        elideSetAutoCommits: true
        maintainTimeStats: false
      leak-detection-threshold: 60000
      connection-test-query: "SELECT 1"
      validation-timeout: 50000

You can see here the jpa entity scan is set to the same package name that we referenced in the jooq target.

Note the datasources credentials are the same as the ones from build.gradle.

Here we used the hikari jdbc driver, which is considered performant, so note to use it we also have to add addition to build.gradle:

    implementation("io.micronaut.sql:micronaut-jdbc-hikari")

Implementation

Let’s combine the micronaut controller with persistence via Jooq. For this we need to create a controller for test: java/server/account/controller/AccountController.java

package server.account.controller;

import io.micronaut.context.annotation.Parameter;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import server.account.dto.Account;
import server.account.service.AccountService;

import javax.inject.Inject;

@Controller("/account")
public class AccountController {

    @Inject
    AccountService accountService;

    @Get("/get-user")
    public Account getAccountByUsername(@Parameter String username) {
        return accountService.fetchAccount(username);
    }
}

We’re going to make the AccountService that we reference: java/server/account/service/AccountService.java

package server.account.service;

import com.org.mmo_server.repository.model.tables.pojos.Users;
import server.account.dto.Account;
import server.account.repository.AccountRepository;

import javax.inject.Inject;
import javax.inject.Singleton;

@Singleton
public class AccountService {

    @Inject
    AccountRepository accountRepository;

    public Account fetchAccount(String username) {
        Users user = accountRepository.fetchByUsername(username);

        return new Account(user.getUsername(), user.getEmail());
    }
}

And finally, we’re creating the AccountRepository: java/server/account/repository/AccountRepository.java

package server.account.repository;

import com.org.mmo_server.repository.model.tables.daos.UsersDao;
import com.org.mmo_server.repository.model.tables.pojos.Users;

import javax.inject.Singleton;
import org.jooq.Configuration;

@Singleton
public class AccountRepository {
    // Use DSL context for higher performance results
    // @Inject
    // DSLContext dslContext;

    UsersDao usersDao;

    AccountRepository(Configuration configuration) {
        this.usersDao = new UsersDao(configuration);
    }

    public Users fetchByUsername(String username) {
        return usersDao.fetchOneByUsername(username);
    }
}

This file references the DAO, make sure you can find it like so:

Note that the DAOs are a lot less efficient than using DSLContext directly, so only use it when performance is not required.

Note that the service translates the database object (with all fields) to our own object (Account). This is because often we don’t want to surface certain information, for instance user password.

Here’s a basic DTO (Data Transfer Object) that we created for this purpose: java/server/account/dto/Account.java

package server.account.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Account {

    String username;
    String email;
}

Therefore when we return account information, we just return username and email – note this is only for template purposes at this time, fill this based on your requirements.

Let’s get testing

First of all, let’s build the project and make sure it completes succesfully, it should look similar to this:

Now you should be able to run/debug your application.java

Just make sure the application indicates its started and tells you where:

Now we will use Postman to test the endpoints. We combine the url with the controller function call:

http://localhost:8081/account/get-user?username=test

Postman request to the get user endpoint

Note that we’ve added a URL parameter of username with value test.

Let’s cross reference this with our controller code

@Controller("/account")
public class AccountController {

The @Controller("/account") adds /account to our base url

The @Get("/get-user") adds to the /account.

So basically we have <base_url>/account/get-user and we specified it as a GET request.

The function call specifies: public Account getAccountByUsername(@Parameter String username)

This means that it takes a URL parameter, which is a string type and is used as username.

Note in our migration file, we added this line:

insert into users(username, email, password, enabled, created_at, updated_at, last_logged_in_at) values ('test', 'test1', 'test3', true, NOW(), NOW(), NOW());

This added a simple row into users table with username test and email of test1.

So in our API call when we get user by username, we expect to get this user entry back.

Get user by username returns expected data

Voila, we get exactly what we want.

Git repository

As I think this post had a lot of information and most of it is playing with configuration and settings, I think it’s best to put this in a git repository.

I will separate out the details from first post (player controller) so just copy the packages manually if you’re deciding to clone this repository.

This repository will be a clean template of a micronaut + jooq + flyway + postgres project.

https://github.com/yazoo321/micronaut_template

Exit mobile version