Skip to content

Java Quickstart — from Install to a First Request

What you get from this chapter

You can install the packages and run a minimal project where two processes call each other.

The project lives at framework/languages/java/quickstart/java/. The code blocks below are read from those files when the site is built. The Java and Kotlin quickstarts share one Gradle build. Without a location store, two processes name each other's endpoint directly and exchange one request/reply.

0. Clone the examples repository

This chapter's project is quickstart/ in the zlink-java-examples repository; tutorial/, the program read by the feature guides, and samples/ live beside it.

git clone https://github.com/zlink-systems/zlink-java-examples.git
cd zlink-java-examples/quickstart

main is the latest release plus the fixes merged since, with package versions pinned to that release. An older release is the tag vA.B.C (git checkout vA.B.C). Send issues and PRs to zlink-systems/zlink.

1. Installation

  • JDK 25 or later. The published zlink-framework-core 0.16.0 has class file major 69 (Java 25)
  • Access to Maven Central

Get it from Maven Central. The minimal combination needed to build one server is the following.

// The contract and runtime
implementation("systems.zlink:zlink-framework-core")
// DI/lifecycle registration
implementation("systems.zlink:zlink-framework-spring-boot-starter")

Do not list systems.zlink:zlink (the binding) yourself. zlink-framework-core declares the version it depends on.

gradle/libs.versions.toml
[versions]
# 세 zlink-framework-* artifact를 한 번에 고정한다.
zlinkFramework = "0.30.0"
# 아래 셋은 zlink-framework POM이 선언한 값과 맞춘 것이다.
# 충돌 해석에 맡기지 않는다.
springBoot = "3.5.14"
kotlin = "2.3.21"
jackson = "2.17.2"
# systems.zlink:zlink(binding)는 적지 않는다. zlink-framework-core의 POM이 정한다.

[libraries]
zlink-framework-core = { module = "systems.zlink:zlink-framework-core", version.ref = "zlinkFramework" }
zlink-framework-spring-boot-starter = { module = "systems.zlink:zlink-framework-spring-boot-starter", version.ref = "zlinkFramework" }
zlink-framework-kotlin = { module = "systems.zlink:zlink-framework-kotlin", version.ref = "zlinkFramework" }
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springBoot" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springBoot" }
# Kotlin data class를 기본 JSON codec이 다루려면 필요하다. Kotlin Server는 web starter가
# 없어 전이로 들어오지 않는다.
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
# Spring이 @Configuration·@RestController를 CGLIB로 감싸는데 Kotlin class는 기본이 final이다.
# 이 플러그인이 해당 class를 열어 준다.
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }

Artifacts to add when you need them:

Artifact When to add it
zlink-framework-locations-redis When using the Redis location store for auto-connect (Location)
zlink-framework-codec-protobuf · -codec-msgpack To use instead of the default JSON codec (Handlers and Message Processing)
zlink-stream-connector When building an external client (a game client, mobile) (STREAM)
zlink-http-client When the server calls out over HTTP (HTTP Client guide)

The license differs by layer — core/binding is MPL-2.0, framework is FSL-1.1-ALv2, and zlink-http-client is Apache-2.0. There is no cost to building and selling a service.

2. Shared contract

Shared
package systems.zlink.quickstart.shared;

// Request/reply contract shared by the server and client processes.
public record Hello(String name) {}

3. The handling side

addHandlersFromPackageOf only discovers handler types. Which channel exposes one is a separate registration on channelName(...).server().

This process serves no HTTP, so SpringApplication.setKeepAlive(true) is required; without it the JVM exits right after the context refreshes.

Server
package systems.zlink.quickstart.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import systems.zlink.framework.ZLinkMessageContext;
import systems.zlink.framework.channels.ZLinkRequestHandler;
import systems.zlink.framework.configuration.ZLinkMeshNodeBuilder;
import systems.zlink.framework.spring.EnableZLinkFramework;
import systems.zlink.framework.spring.ZLinkFrameworkConfigurer;
import systems.zlink.quickstart.shared.Greeting;
import systems.zlink.quickstart.shared.Hello;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

@EnableZLinkFramework
@SpringBootApplication
public class ServerApplication {

    public static void main(String[] args) {
        // This process serves no HTTP; without setKeepAlive the JVM exits as
        // soon as the context finishes refreshing.
        SpringApplication app = new SpringApplication(ServerApplication.class);
        app.setKeepAlive(true);
        app.run(args);
    }

    @Bean
    ZLinkFrameworkConfigurer zlink() {
        return options -> {
            // Discovers handler types.
            options.addHandlersFromPackageOf(ServerApplication.class);

            // Names the mesh.
            ZLinkMeshNodeBuilder mesh =
                    options.addRouteMesh("services")
                            // This process's own endpoint, for peers to connect to.
                            .listen("tcp://127.0.0.1:7101");
            // This process handles the "greeting" channel.
            mesh.channelName("greeting")
                    .server()
                    .addRequestHandler(HelloHandler.class, Hello.class, Greeting.class);
        };
    }
}

// Handles one request on the "greeting" channel.
final class HelloHandler implements ZLinkRequestHandler<Hello, Greeting> {

    @Override
    public CompletionStage<Greeting> handle(Hello request, ZLinkMessageContext context) {
        return CompletableFuture.completedFuture(new Greeting("hello, " + request.name()));
    }
}

4. The calling side

Client
package systems.zlink.quickstart.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import systems.zlink.framework.channels.ZLinkRouteClient;
import systems.zlink.framework.configuration.ZLinkMeshNodeBuilder;
import systems.zlink.framework.spring.EnableZLinkFramework;
import systems.zlink.framework.spring.ZLinkFrameworkConfigurer;
import systems.zlink.quickstart.shared.Greeting;
import systems.zlink.quickstart.shared.Hello;

@EnableZLinkFramework
@SpringBootApplication
public class ClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }

    @Bean
    ZLinkFrameworkConfigurer zlink() {
        return options -> {
            // This process also needs its own endpoint.
            ZLinkMeshNodeBuilder mesh =
                    options.addRouteMesh("services").listen("tcp://127.0.0.1:7102");
            // This side only calls; it does not handle "greeting".
            mesh.channelName("greeting").client();
            // Manual connection -- the server's endpoint is given directly.
            mesh.peerConnections().connect("tcp://127.0.0.1:7101");
        };
    }
}

@RestController
class HelloController {
    private final ZLinkRouteClient route;

    HelloController(ZLinkRouteClient route) {
        this.route = route;
    }

    @GetMapping("/hello/{name}")
    String hello(@PathVariable String name) {
        // The target is a single ChannelName; which node handles it is not specified.
        // submit_sync blocks this application thread until the reply arrives. A request
        // handler runs on an application thread, not a runtime execution context, so
        // blocking here is allowed.
        return route.requestToChannel("greeting", new Hello(name))
                .submit_sync(Greeting.class)
                .text();
    }
}

5. Run

cd zlink-java-examples/quickstart
./gradlew :java:Server:installDist :java:Client:installDist

# Two terminals. Start the server first.
./java/Server/build/install/Server/bin/Server
./java/Client/build/install/Client/bin/Client

curl http://127.0.0.1:5080/hello/world

The response is hello, world with status 200.

6. What to check when the first run fails

Symptom What to check
An artifact is not found Check that the artifact names in section 1 were copied exactly. The binding version is not pinned separately
Startup fails Check that both processes name the same mesh, and that the listen endpoint does not collide with another process
The server exits immediately Check that SpringApplication.setKeepAlive(true) is called
A call ends with no target Check that the receiving side registered that channel name in the server role, and that the two processes are connected as peers
No answer arrives Check that the caller used request. A send receives no answer

7. What to carry over

File Content
gradle/libs.versions.toml One zlinkFramework entry pins all three artifacts. The binding stays transitive
Shared The message types both processes share
Server addRouteMesh → listen → channelName(...).server().addRequestHandler(...), plus setKeepAlive(true)
Client channelName(...).client(), peerConnections().connect(...), requestToChannel(...)

Further build-setup requirements are listed in the project's README.md.

These two processes connect by writing each other's endpoint directly. Keeping the calling code unchanged while servers are added or restarted at another address needs automatic connection, and that is covered by Location.