Skip to content

Java / Spring Boot

In this chapter, you’ll integrate Oicana into a Java web service using Spring Boot. Spring Boot is a popular Java framework for building production-ready web services. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.

Let’s start with a fresh Spring Boot project. Create a new directory and initialize it with Gradle:

Terminal window
mkdir my-pdf-service
cd my-pdf-service
gradle init --type basic --dsl kotlin

Replace the generated build.gradle.kts with:

build.gradle.kts
plugins {
java
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "1.0.0"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.oicana:oicana:0.8.0")
// The following are all the native implementations that Oicana for Java has.
// To save on bandwidth and project size, remove the platforms you won't run this project on.
runtimeOnly("com.oicana:oicana-linux-x86_64:0.8.0")
runtimeOnly("com.oicana:oicana-linux-aarch64:0.8.0")
runtimeOnly("com.oicana:oicana-macos-x86_64:0.8.0")
runtimeOnly("com.oicana:oicana-macos-aarch64:0.8.0")
runtimeOnly("com.oicana:oicana-windows-x86_64:0.8.0")
}

Create the main application class:

src/main/java/com/example/Application.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

Start the service with ./gradlew bootRun. It has no endpoints yet, so http://localhost:8080 answers with Spring’s Whitelabel error page and a 404 - that is the expected result at this point and means the application is up.

We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.

  1. Create the directory src/main/resources/templates in the project and copy example-0.1.0.zip into it. Everything under src/main/resources is packed into the jar, so the template travels with the application.

  2. Create a service to load and compile the template:

    src/main/java/com/example/TemplateService.java
    package com.example;
    import com.oicana.CompilationMode;
    import com.oicana.ExportFormat;
    import com.oicana.Template;
    import jakarta.annotation.PostConstruct;
    import jakarta.annotation.PreDestroy;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.stereotype.Service;
    import java.io.IOException;
    import java.util.Map;
    @Service
    public class TemplateService {
    private Template template;
    @PostConstruct
    public void init() throws IOException {
    var resource = new ClassPathResource("templates/example-0.1.0.zip");
    try (var stream = resource.getInputStream()) {
    template = new Template(stream.readAllBytes());
    }
    }
    public byte[] compile() {
    return template.export(
    ExportFormat.pdf(),
    CompilationMode.DEVELOPMENT
    );
    }
    @PreDestroy
    public void cleanup() {
    template.close();
    }
    }

    The Template constructor loads the template once. The compile method compiles it without inputs and CompilationMode.DEVELOPMENT, so the template uses the development value you defined for the info input ({ "name": "Chuck Norris" }). In a follow-up step, we will set an input value instead. The @PreDestroy cleanup releases native resources.

    Template takes bytes, so if you would rather ship templates separately from the application, read them from a file, an object store, or a database instead.

  3. Create a controller with a compile endpoint:

    src/main/java/com/example/CompileController.java
    package com.example;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    @RestController
    public class CompileController {
    private final TemplateService templateService;
    public CompileController(TemplateService templateService) {
    this.templateService = templateService;
    }
    @PostMapping("/compile")
    public ResponseEntity<byte[]> compile() {
    byte[] pdf = templateService.compile();
    return ResponseEntity.ok()
    .header(HttpHeaders.CONTENT_DISPOSITION,
    "attachment; filename=\"example.pdf\"")
    .contentType(MediaType.APPLICATION_PDF)
    .body(pdf);
    }
    }

    This code defines a new POST endpoint at /compile. For every request, it compiles the template and returns the PDF file.

After restarting the service, you can test the endpoint with curl:

Terminal window
curl -X POST http://localhost:8080/compile --output example.pdf

The generated example.pdf file should contain your template with the development value.

Oicana for Java loads a native library, and JDK 24 and newer restrict that. Running on such a JDK you get a warning like this at startup:

WARNING: java.lang.System::load has been called by com.oicana.NativeLoader in an unnamed module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Grant it in whichever way fits how you start the application:

  • Executable jar: add Enable-Native-Access: ALL-UNNAMED to the jar manifest. This travels with the artifact and needs no launcher change, but only applies to java -jar.

    Part of build.gradle.kts
    tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
    manifest { attributes("Enable-Native-Access" to "ALL-UNNAMED") }
    }
  • Any other launch, including ./gradlew bootRun and containers: pass --enable-native-access=ALL-UNNAMED as a JVM argument.

    Part of build.gradle.kts
    tasks.named<org.springframework.boot.gradle.tasks.run.BootRun>("bootRun") {
    jvmArgs("--enable-native-access=ALL-UNNAMED")
    }
  • On the module path: the API jar is the named module com.oicana, so you can narrow the grant to --enable-native-access=com.oicana instead of opening up the whole classpath.

The PDF generation should not take longer than a couple of milliseconds. The Template instance is thread-safe and can be shared across requests - Spring Boot’s singleton service scope handles this naturally.

Repeated compilations are fast because Typst memoizes its work in a global cache. The cache management guide explains how that cache is evicted and when it pays off to tune it.

Our compile method is currently calling template.export without inputs in development mode. Now we’ll provide an explicit input value and switch to production mode:

Part of TemplateService.java
public byte[] compile() {
return template.export(
Map.of("info", "{\"name\": \"Baby Yoda\"}"),
Map.of()
);
}

Your explicit input value takes precedence over the development value in either mode, so it is what changes the output here. We now pass JSON inputs and an empty blob inputs map. The export(Map, Map) overload defaults to CompilationMode.PRODUCTION and PDF output. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values for inputs. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles none values for that input.

Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the open source Spring Boot example project on GitHub for a more complete showcase of the Oicana Java integration.

For inputs other than JSON, see Template inputs, which documents blob inputs with examples for every integration.

A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s example template declares no JSON schema, so only the first case can happen here. export throws an OicanaException that we can catch:

Part of CompileController.java
import com.oicana.OicanaException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger log =
LoggerFactory.getLogger(CompileController.class);
@PostMapping("/compile")
public ResponseEntity<byte[]> compile() {
byte[] pdf;
try {
pdf = templateService.compile();
} catch (OicanaException exception) {
log.error("Failed to compile template", exception);
return ResponseEntity.internalServerError()
.contentType(MediaType.TEXT_PLAIN)
.body("Failed to generate the document".getBytes());
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
Complete code at the end of this chapter
build.gradle.kts
plugins {
java
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "1.0.0"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.oicana:oicana:0.8.0")
// Since this is an example project, we add all native implementations.
// In your project, only add what you need.
runtimeOnly("com.oicana:oicana-linux-x86_64:0.8.0")
runtimeOnly("com.oicana:oicana-linux-aarch64:0.8.0")
runtimeOnly("com.oicana:oicana-macos-x86_64:0.8.0")
runtimeOnly("com.oicana:oicana-macos-aarch64:0.8.0")
runtimeOnly("com.oicana:oicana-windows-x86_64:0.8.0")
}
settings.gradle.kts
rootProject.name = "my-pdf-service"
src/main/java/com/example/Application.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
src/main/java/com/example/TemplateService.java
package com.example;
import com.oicana.Template;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
@Service
public class TemplateService {
private Template template;
@PostConstruct
public void init() throws IOException {
var resource = new ClassPathResource("templates/example-0.1.0.zip");
try (var stream = resource.getInputStream()) {
template = new Template(stream.readAllBytes());
}
}
public byte[] compile() {
return template.export(
Map.of("info", "{\"name\": \"Baby Yoda\"}"),
Map.of()
);
}
@PreDestroy
public void cleanup() {
template.close();
}
}
src/main/java/com/example/CompileController.java
package com.example;
import com.oicana.OicanaException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CompileController {
private static final Logger log =
LoggerFactory.getLogger(CompileController.class);
private final TemplateService templateService;
public CompileController(TemplateService templateService) {
this.templateService = templateService;
}
@PostMapping("/compile")
public ResponseEntity<byte[]> compile() {
byte[] pdf;
try {
pdf = templateService.compile();
} catch (OicanaException exception) {
log.error("Failed to compile template", exception);
return ResponseEntity.internalServerError()
.contentType(MediaType.TEXT_PLAIN)
.body("Failed to generate the document".getBytes());
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
}