Ultra-fast REST API framework combining Rust Hyper HTTP server with Java handlers.
This release candidate clarifies the framework's direction: keep application code simple in Java, while Rust handles the low-level HTTP runtime, native memory boundaries, large response delivery, WebSocket transport, and selected hot serialization paths.
In practice, the biggest gains appear when endpoints are small, read-heavy, precomputed, raw JSON, or file/export oriented. Regular dynamic DTO endpoints still work the familiar Java way; they benefit from the lighter runtime, but their final performance also depends on how much object creation and business logic each request performs.
The normal application model is unchanged: your controllers/handlers, services, components, and business logic still live in Java. The new features are opt-in tools for lower latency, lower RSS, and safer production behavior.
What it does: adds the Java framework and the matching Rust native runtime with one Maven dependency.
For normal usage you do not copy DLL/SO files manually. The JAR already contains:
native/windows-x64/rust_hyper.dll and native/linux-x64/librust_hyper.so.
How to use it:
<dependency>
<groupId>com.reactor</groupId>
<artifactId>rust-java-rest</artifactId>
<version>3.1.0-rc1</version>
</dependency>Only use a manual native path when you intentionally want to test or deploy a custom native build:
java -Drust.lib.path=/opt/native/librust_hyper.so -jar app.jarIf Java loads an old native library, startup fails early with an ABI mismatch instead of failing later under traffic.
What it does: returns a payload directly without creating a response DTO and serializing it again.
Use it when the response is precomputed, cached, simple, or already serialized. Do not use it for every dynamic endpoint by default; normal DTO responses are still fine for regular business APIs.
Example:
@GetMapping(value = "/health/raw", requestType = Void.class, responseType = RawResponse.class)
public RawResponse health() {
return RawResponse.text("{\"status\":\"ok\"}", "application/json; charset=utf-8");
}For read-heavy payloads that repeat often, register once in Rust and return the native id:
private static final RawResponse CACHED_CONFIG =
RawResponse.registeredJson("""
{"feature":"enabled","version":"3.1.0-rc1"}
""".getBytes(StandardCharsets.UTF_8));
@GetMapping(value = "/config", requestType = Void.class, responseType = RawResponse.class)
public RawResponse config() {
return RawResponse.nativeJson(CACHED_CONFIG.getNativeId());
}What it does: Java returns the file path and headers; Rust streams the file to the socket. The file body does not move through Java heap or a JNI response frame.
Use it for large downloads, generated exports, reports, static files, and any response where loading the
whole file into a Java byte[] would hurt memory.
Example:
@GetMapping(value = "/reports/daily", requestType = Void.class, responseType = FileResponse.class)
public FileResponse dailyReport() {
Path file = Path.of("/data/reports/daily.csv");
return FileResponse.download(file, "daily.csv", "text/csv")
.header("Cache-Control", "no-store");
}What it does: keeps default memory limits tight while allowing specific routes to accept or return larger bodies.
Use it when one endpoint legitimately needs bigger request or response bodies. Avoid raising global limits just because one route needs them.
Example:
@PostMapping(value = "/upload", requestType = UploadRequest.class, responseType = UploadResult.class)
@MaxRequestBodySize(8 * 1024 * 1024)
@MaxResponseSize(2 * 1024 * 1024)
public UploadResult upload(@RequestBody UploadRequest request) {
return uploadService.save(request);
}What it does: lets a handler write JSON directly into the native response buffer. This avoids building a
large Java DTO graph and avoids an extra serializer-owned byte[] for selected hot paths.
Use it for endpoints that are called very frequently or produce heavy JSON in a predictable shape. Keep normal DTO handlers for ordinary business endpoints where maintainability is more important than the last allocation.
Example:
@RustRoute(
method = "GET",
path = "/api/v1/heavy",
requestType = Void.class,
responseType = HeavyResponse.class
)
@DirectQueryInt(value = "items", defaultValue = 100, min = 1, max = 1000)
public int heavy(ByteBuffer out, int offset, int items) {
JsonBufferWriter json = JsonBufferWriter.wrap(out, offset);
json.beginObject()
.fieldInt("items", items)
.comma()
.fieldString("status", "ok")
.endObject();
return json.result();
}@DirectQueryInt means Rust parses ?items=100 and passes a primitive int to Java. That avoids
allocating and parsing query strings on this hot route.
What it does: WebSocketSession.sendText(...), sendBinary(...), and close(...) now use a Rust-side
session registry with bounded outbound queues.
Use it the same way as before, but expect slow consumers to be rejected/closed instead of allowing unbounded memory growth.
Example:
@OnMessage
public void onMessage(WebSocketSession session, String message) {
String roomId = session.getPathParams().get("roomId");
WebSocketBroadcaster.getInstance().broadcastToRoom(roomId, message, session);
}Tune the queue and frame limits when you know your WebSocket traffic profile:
reactor.rust.websocket.max-frame-bytes=1048576
reactor.rust.websocket.outbound-queue-capacity=1024
reactor.rust.websocket.send-timeout-ms=5000What it does: exposes the metrics needed to understand latency, memory, backpressure, and native runtime behavior.
Use it during benchmark runs, container tuning, and production readiness checks.
curl http://localhost:8080/metrics
curl http://localhost:8080/metrics/summary
curl http://localhost:8080/diagnostics/memory
curl http://localhost:8080/diagnostics/native/trim/metrics/reset is useful for controlled benchmark runs, but should be protected or disabled in a real
production deployment.
What it does: lets you choose between low RSS, throughput, and stricter overload behavior without changing Java business code.
Start conservative for production-like services:
reactor.rust.http.max-request-body-bytes=1048576
reactor.rust.http.max-response-body-bytes=8388608
reactor.rust.http.max-inflight-response-bytes=67108864
reactor.rust.http.max-connections=1024
reactor.rust.jni.queue-capacity=1024
reactor.rust.log.level=error
reactor.rust.java.log.level=warnIf you increase per-request limits, also cap total in-flight bytes. Raising only
max-response-body-bytes can improve one endpoint but damage RSS under concurrency.
Profile: low-rss, CPU limit 2, Rust-Java memory limit 128m, Spring Boot memory limit 512m,
OpenJ9/Semeru 21, concurrency 1000, duration 20s.
| Endpoint | Rust-Java RPS | Spring Boot RPS | Ratio | Rust P99 | Spring P99 | Rust Max Mem | Spring Max Mem |
|---|---|---|---|---|---|---|---|
| candidates | 10,510 | 4,665 | 2.25x | 303ms | 639ms | 113 MiB | 281 MiB |
| echo | 11,171 | 4,128 | 2.71x | 289ms | 640ms | 117 MiB | 299 MiB |
| heavy100 raw | 10,106 | 4,281 | 2.36x | 290ms | 513ms | 98 MiB | 254 MiB |
| heavy100 dynamic | 2,469 | 1,791 | 1.38x | 552ms | 2.43s | 105 MiB | 280 MiB |
Benchmark run id: container_20260425_204114. The RC release notes include the c=1000 summary table.
| Optimization | Impact |
|---|---|
| Bounded JNI worker queue | Predictable overload behavior instead of unbounded blocking/allocation |
| Direct response buffer writers | Avoid DTO graph and serializer-owned byte[] for selected hot endpoints |
| Primitive direct route API | Query ints can be parsed in Rust and passed as primitives through JNI |
| Generated-style JSON parser/writer prototype | Echo path avoids generic reflection/map-style parsing |
| Response pool and native memory diagnostics | Lower RSS retention risk and measurable native memory behavior |
| Raw/File/native response paths | Large/static/read-heavy responses avoid carrying Java body bytes per request |
| Timeout/keep-alive/header/body limits | Production safety knobs for slow clients and bounded resource usage |
| Low-RSS / throughput / micro-RSS profiles | Runtime can be tuned by workload instead of one-size-fits-all config |
Release notes: docs/release-notes/v3.1.0-rc1.md.
Full WebSocket support with annotation-based handlers:
@Component
@WebSocket("/ws/chat/{roomId}")
public class ChatWebSocketHandler {
@OnOpen
public void onOpen(WebSocketSession session) {
String roomId = session.getPathParams().get("roomId");
session.sendText("{\"type\":\"connected\",\"roomId\":\"" + roomId + "\"}");
}
@OnMessage
public void onMessage(WebSocketSession session, String message) {
// Broadcast to all sessions in room
WebSocketBroadcaster.getInstance()
.broadcastToRoom("room1", message);
}
@OnClose
public void onClose(WebSocketSession session) {
System.out.println("Session closed: " + session.getId());
}
@OnError
public void onError(WebSocketSession session, String error) {
System.err.println("Error: " + error);
}
}Features:
- Path parameters (
/ws/chat/{roomId}) - Room management and broadcasting
- Binary and text messages
- Session lifecycle callbacks
Non-blocking handlers with virtual threads (Java 21+):
@PostMapping(value = "/order/create", requestType = OrderRequest.class)
public CompletableFuture<ResponseEntity<OrderResponse>> createAsync(
@RequestBody OrderRequest request) {
return orderService.createOrderAsync(request)
.thenApply(order -> ResponseEntity.ok(
new OrderResponse(order.getId(), "Created")
))
.exceptionally(ex -> ResponseEntity.status(500).body(
new OrderResponse(-1, "Error: " + ex.getMessage())
));
}
// Combine multiple async calls
@GetMapping(value = "/order/{id}/full")
public CompletableFuture<ResponseEntity<FullOrderResponse>> getFullOrder(
@PathVariable("id") String orderId) {
CompletableFuture<Order> orderFuture = orderService.getOrderAsync(orderId);
CompletableFuture<List<Payment>> paymentsFuture = paymentService.getPaymentsAsync(orderId);
return CompletableFuture.allOf(orderFuture, paymentsFuture)
.thenApply(v -> new FullOrderResponse(orderFuture.join(), paymentsFuture.join()));
}Production-ready static file serving with caching:
@Component
@StaticFiles(
path = "/static",
location = "static",
cacheMaxAge = 3600,
indexFile = "index.html"
)
public class StaticFileConfig {}GET /static/css/style.css β classpath:/static/css/style.css
GET /static/js/app.js β classpath:/static/js/app.js
GET /static/ β classpath:/static/index.html
Features:
- 20+ MIME types supported
- Automatic file caching (< 1MB files)
- Multiple static locations
- Cache-Control headers
| Optimization | Before | After | Improvement |
|---|---|---|---|
| Annotation lookup | ~200ns | ~5ns | 40x faster |
| Parameter map lookup | O(n) | O(1) | Robin-Hood hashing |
| Header encoding | String allocation | Zero-copy | No GC pressure |
| Error responses | allocation | pre-allocated | Zero allocation |
All v2.0.0 features are included:
- Zero-overhead Dependency Injection (@Service, @Autowired, @PostConstruct)
- Spring Boot-like annotations (@GetMapping, @PostMapping, etc.)
- ResponseEntity return type support
- Automatic parameter resolution (@PathVariable, @RequestParam, @HeaderParam, @RequestBody)
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/esasmer-dou/rust-java-rest</url>
</repository>
</repositories><dependency>
<groupId>com.reactor</groupId>
<artifactId>rust-java-rest</artifactId>
<version>3.1.0-rc1</version>
</dependency>Note: GitHub Packages requires authentication. Add your GitHub token to
~/.m2/settings.xml:<settings> <servers> <server> <id>github</id> <username>GITHUB_USERNAME</username> <password>GITHUB_TOKEN</password> </server> </servers> </settings>To create a token: GitHub β Settings β Developer settings β Personal access tokens β Generate new token (classic) Required scope:
read:packages
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<release>21</release>
<annotationProcessorPaths>
<path>
<groupId>com.dslplatform</groupId>
<artifactId>dsl-json</artifactId>
<version>2.0.2</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>import com.reactor.rust.annotations.Request;
import com.dslplatform.json.CompiledJson;
@Request
@CompiledJson
public record OrderRequest(
String orderId,
double amount
) {}import com.reactor.rust.annotations.Response;
import com.dslplatform.json.CompiledJson;
@Response
@CompiledJson
public record OrderResponse(
int status,
String message
) {}import com.reactor.rust.annotations.*;
import com.reactor.rust.http.ResponseEntity;
import com.reactor.rust.http.HttpStatus;
import com.reactor.rust.http.MediaType;
@RequestMapping("/order")
public class OrderHandler {
@PostMapping(value = "/create", requestType = OrderRequest.class, responseType = OrderResponse.class)
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<OrderResponse> create(
@RequestBody OrderRequest request,
@HeaderParam("X-Request-ID") String requestId) {
// Business logic
System.out.println("Order: " + request.orderId());
System.out.println("Request ID: " + requestId);
// Return ResponseEntity
OrderResponse response = new OrderResponse(1, "Success");
return ResponseEntity.ok(response);
}
@GetMapping(value = "/{id}", responseType = OrderResponse.class)
public ResponseEntity<OrderResponse> getById(@PathVariable("id") String id) {
OrderResponse response = new OrderResponse(1, "Found: " + id);
return ResponseEntity.ok(response);
}
@GetMapping(value = "/search", responseType = OrderResponse.class)
public ResponseEntity<OrderResponse> search(@RequestParam("status") String status) {
OrderResponse response = new OrderResponse(1, "Status: " + status);
return ResponseEntity.ok(response);
}
}import com.reactor.rust.annotations.RustRoute;
import com.reactor.rust.json.DslJsonService;
import java.nio.ByteBuffer;
public class OrderHandler {
@RustRoute(
method = "POST",
path = "/order/create",
requestType = OrderRequest.class,
responseType = OrderResponse.class
)
public int create(ByteBuffer out, int offset, byte[] body) {
// Parse JSON to object
OrderRequest request = DslJsonService.parse(body, OrderRequest.class);
// Business logic
System.out.println("Order: " + request.orderId());
// Create response
OrderResponse response = new OrderResponse(1, "Success");
// Serialize to buffer
return DslJsonService.writeToBuffer(response, out, offset);
}
}import com.reactor.rust.bridge.HandlerRegistry;
import com.reactor.rust.bridge.NativeBridge;
import com.reactor.rust.bridge.RouteScanner;
public class Application {
public static void main(String[] args) throws InterruptedException {
// Register handlers
HandlerRegistry registry = HandlerRegistry.getInstance();
registry.registerBean(new OrderHandler());
// Scan routes
RouteScanner.scanAndRegister();
// Start server
NativeBridge.startHttpServer(8080);
System.out.println("Server running: http://localhost:8080");
// Keep JVM alive
Thread.sleep(Long.MAX_VALUE);
}
}mvn clean package -DskipTests
java -cp target/rust-java-rest-2.0.0.jar:target/lib/* Applicationcurl -X POST http://localhost:8080/order/create \
-H "Content-Type: application/json" \
-H "X-Request-ID: REQ-001" \
-d '{"orderId":"ORD-001", "amount":150.50}'The framework supports Spring Boot-like HTTP method annotations:
@GetMapping(value = "/product/{id}", responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> getById(@PathVariable("id") String id) {
return ResponseEntity.ok(productService.find(id));
}@PostMapping(value = "/product/add", requestType = ProductRequest.class, responseType = ProductResponse.class)
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<ProductResponse> add(@RequestBody ProductRequest request) {
return ResponseEntity.created(productService.save(request));
}@PutMapping(value = "/product/update", requestType = ProductRequest.class, responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> update(@RequestBody ProductRequest request) {
return ResponseEntity.ok(productService.update(request));
}@PatchMapping(value = "/product/price", requestType = PriceUpdateRequest.class, responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> updatePrice(@RequestBody PriceUpdateRequest request) {
return ResponseEntity.ok(productService.updatePrice(request));
}@DeleteMapping(value = "/product/{id}", responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> delete(@PathVariable("id") String id) {
productService.delete(id);
return ResponseEntity.ok(new ProductResponse(1, "Deleted"));
}@RequestMapping("/api/v1")
public class ApiHandler {
@GetMapping(value = "/products", responseType = ProductListResponse.class)
public ResponseEntity<ProductListResponse> getAllProducts() {
// GET /api/v1/products
return ResponseEntity.ok(productService.getAll());
}
}@GetMapping(value = "/order/{id}", responseType = OrderResponse.class)
public ResponseEntity<OrderResponse> getById(@PathVariable("id") String orderId) {
return ResponseEntity.ok(orderService.find(orderId));
}
@GetMapping(value = "/order/{id}/item/{itemId}", responseType = ItemResponse.class)
public ResponseEntity<ItemResponse> getItem(
@PathVariable("id") String orderId,
@PathVariable("itemId") String itemId) {
// GET /order/ORD-001/item/ITEM-123
return ResponseEntity.ok(orderService.findItem(orderId, itemId));
}@GetMapping(value = "/order/search", responseType = OrderListResponse.class)
public ResponseEntity<OrderListResponse> search(
@RequestParam("status") String status,
@RequestParam(value = "page", defaultValue = "1") int page) {
// GET /order/search?status=pending&page=2
return ResponseEntity.ok(orderService.search(status, page));
}
@GetMapping(value = "/product/list", responseType = ProductListResponse.class)
public ResponseEntity<ProductListResponse> list(
@RequestParam(value = "sort", required = false) String sort,
@RequestParam(value = "limit", defaultValue = "10") int limit) {
// GET /product/list?sort=price&limit=20
return ResponseEntity.ok(productService.list(sort, limit));
}@PostMapping(value = "/order/create", requestType = OrderRequest.class, responseType = OrderResponse.class)
public ResponseEntity<OrderResponse> create(
@RequestBody OrderRequest request,
@HeaderParam("X-Request-ID") String requestId,
@HeaderParam("Authorization") String token) {
// Get X-Request-ID and Authorization headers
return ResponseEntity.ok(orderService.create(request, requestId, token));
}@PostMapping(value = "/product/add", requestType = ProductRequest.class, responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> add(@RequestBody ProductRequest request) {
// Body is automatically deserialized to ProductRequest
return ResponseEntity.ok(productService.save(request));
}@GetMapping(value = "/user/info", responseType = UserResponse.class)
public ResponseEntity<UserResponse> getInfo(@CookieValue("sessionId") String sessionId) {
// Get sessionId from cookie
return ResponseEntity.ok(userService.findBySession(sessionId));
}ResponseEntity provides a type-safe wrapper for HTTP responses:
import com.reactor.rust.http.ResponseEntity;
import com.reactor.rust.http.HttpStatus;
// 200 OK
@GetMapping(value = "/product/{id}", responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> getById(@PathVariable("id") String id) {
return ResponseEntity.ok(productService.find(id));
}
// 201 Created
@PostMapping(value = "/product/add", requestType = ProductRequest.class, responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> add(@RequestBody ProductRequest request) {
return ResponseEntity.created(productService.save(request));
}
// 404 Not Found
@GetMapping(value = "/product/{id}", responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> getById(@PathVariable("id") String id) {
ProductResponse product = productService.find(id);
if (product == null) {
return ResponseEntity.notFound();
}
return ResponseEntity.ok(product);
}
// 400 Bad Request
@PostMapping(value = "/product/add", requestType = ProductRequest.class, responseType = ProductResponse.class)
public ResponseEntity<ProductResponse> add(@RequestBody ProductRequest request) {
if (request.name() == null || request.name().isEmpty()) {
return ResponseEntity.badRequest();
}
return ResponseEntity.ok(productService.save(request));
}
// Custom Status
@DeleteMapping(value = "/product/{id}", responseType = Void.class)
public ResponseEntity<Void> delete(@PathVariable("id") String id) {
productService.delete(id);
return ResponseEntity.status(HttpStatus.NO_CONTENT);
}Used to specify HTTP status code for handler methods:
import com.reactor.rust.annotations.ResponseStatus;
import com.reactor.rust.http.HttpStatus;
@PostMapping(value = "/order/create", requestType = OrderRequest.class, responseType = OrderResponse.class)
@ResponseStatus(201) // or HttpStatus.CREATED = 201
public ResponseEntity<OrderResponse> create(@RequestBody OrderRequest request) {
return ResponseEntity.ok(orderService.create(request));
}
@DeleteMapping(value = "/order/{id}", responseType = Void.class)
@ResponseStatus(204) // or HttpStatus.NO_CONTENT = 204
public ResponseEntity<Void> delete(@PathVariable("id") String id) {
orderService.delete(id);
return null;
}Enum for common HTTP status codes:
import com.reactor.rust.http.HttpStatus;
// Usage
HttpStatus.OK // 200
HttpStatus.CREATED // 201
HttpStatus.NO_CONTENT // 204
HttpStatus.BAD_REQUEST // 400
HttpStatus.UNAUTHORIZED // 401
HttpStatus.FORBIDDEN // 403
HttpStatus.NOT_FOUND // 404
HttpStatus.INTERNAL_SERVER_ERROR // 500
// With ResponseEntity
return ResponseEntity.status(HttpStatus.CREATED);Constants for content types:
import com.reactor.rust.http.MediaType;
MediaType.APPLICATION_JSON // "application/json"
MediaType.TEXT_PLAIN // "text/plain"
MediaType.TEXT_HTML // "text/html"
MediaType.APPLICATION_XML // "application/xml"
MediaType.TEXT_CSV // "text/csv"
MediaType.APPLICATION_OCTET_STREAM // "application/octet-stream"
// Usage
@PostMapping(value = "/order/create", requestType = OrderRequest.class, responseType = OrderResponse.class)
public ResponseEntity<OrderResponse> create(
@RequestBody OrderRequest request,
@HeaderParam("Content-Type") String contentType) {
if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON)) {
return ResponseEntity.badRequest();
}
return ResponseEntity.ok(orderService.create(request));
}If you don't want annotation-based parameters, you can continue using the old V4 signature:
@RustRoute(
method = "POST",
path = "/order/create",
requestType = OrderRequest.class,
responseType = OrderResponse.class
)
public int create(ByteBuffer out, int offset, byte[] body) {
OrderRequest request = DslJsonService.parse(body, OrderRequest.class);
OrderResponse response = new OrderResponse(1, "Success");
return DslJsonService.writeToBuffer(response, out, offset);
}@RustRoute(
method = "GET",
path = "/order/{id}",
requestType = Void.class,
responseType = OrderResponse.class
)
public int getById(ByteBuffer out, int offset, byte[] body, String pathParams) {
// pathParams = "id=ORD-001"
String id = getParam(pathParams, "id");
OrderResponse response = new OrderResponse(1, "Found: " + id);
return DslJsonService.writeToBuffer(response, out, offset);
}
// Helper method
private String getParam(String params, String key) {
if (params == null) return null;
for (String pair : params.split("&")) {
String[] kv = pair.split("=", 2);
if (kv[0].equals(key)) return kv[1];
}
return null;
}@RustRoute(
method = "GET",
path = "/order/search",
requestType = Void.class,
responseType = OrderResponse.class
)
public int search(ByteBuffer out, int offset, byte[] body,
String pathParams, String queryString) {
// queryString = "status=pending&page=1"
String status = getParam(queryString, "status");
OrderResponse response = new OrderResponse(1, "Status: " + status);
return DslJsonService.writeToBuffer(response, out, offset);
}@RustRoute(
method = "POST",
path = "/order/create",
requestType = OrderRequest.class,
responseType = OrderResponse.class
)
public int create(ByteBuffer out, int offset, byte[] body,
String pathParams, String queryString, String headers) {
// headers = "Content-Type=application/json&X-Request-ID=REQ-001"
String requestId = getParam(headers, "X-Request-ID");
OrderRequest request = DslJsonService.parse(body, OrderRequest.class);
OrderResponse response = orderService.create(request, requestId);
return DslJsonService.writeToBuffer(response, out, offset);
}| Signature | Description |
|---|---|
ResponseEntity<T> method(@PathVariable String id) |
Path parameter |
ResponseEntity<T> method(@RequestParam String q) |
Query parameter |
ResponseEntity<T> method(@RequestBody Request req) |
Request body |
ResponseEntity<T> method(@HeaderParam String h) |
Header |
T method(...) |
Automatically serialized |
| Need | Signature |
|---|---|
| Body only | int method(ByteBuffer out, int offset, byte[] body) |
| Path parameter | int method(ByteBuffer out, int offset, byte[] body, String pathParams) |
| Path + Query | int method(ByteBuffer out, int offset, byte[] body, String pathParams, String queryString) |
| Full signature | int method(ByteBuffer out, int offset, byte[] body, String pathParams, String queryString, String headers) |
| Platform | Native Library | Status |
|---|---|---|
| Linux x64 | librust_hyper.so |
Supported |
| Windows x64 | rust_hyper.dll |
Supported |
| macOS x64 | librust_hyper.dylib |
Coming Soon |
| macOS ARM64 | librust_hyper.dylib |
Coming Soon |
The framework requires a native library for the Rust Hyper HTTP server. This library is automatically embedded in the JAR and loaded at runtime.
// Native library is loaded automatically - no action required
NativeBridge.startHttpServer(8080);# Specify custom library path
java -Drust.lib.path=/path/to/rust_hyper.dll -jar myapp.jar
# Or use java.library.path
java -Djava.library.path=/path/to/native/dir -jar myapp.jar| Platform | File | Location (in JAR) |
|---|---|---|
| Windows x64 | rust_hyper.dll |
native/windows-x64/ |
| Linux x64 | librust_hyper.so |
native/linux-x64/ |
The framework provides ultra-minimal Docker images optimized for production.
| Image | Size | Base | Runtime Memory | Description |
|---|---|---|---|---|
rust-java-rest:ultra |
149MB | Debian slim | 28 MB | Ultra-low memory (v3.0.0) |
ghcr.io/esasmer-dou/rust-java-rest:3.1.0-rc1 |
Debian slim | low-rss profile | RC / performance preview | |
rust-java-rest:minimal |
74MB | Distroless | ~35 MB | Minimal (v2.0.0) |
rust-java-rest:optimized |
136MB | Debian slim | ~35 MB | With curl |
# Ultra-low memory image (v3.0.0) - RECOMMENDED
docker pull ghcr.io/esasmer-dou/rust-java-rest:3.1.0-rc1
docker run -p 8080:8080 --memory=128m ghcr.io/esasmer-dou/rust-java-rest:3.1.0-rc1
# Legacy minimal image (v2.0.0)
docker pull ghcr.io/esasmer-dou/rust-java-rest:2.0.0
docker run -p 8080:8080 --memory=40m ghcr.io/esasmer-dou/rust-java-rest:2.0.0Option 1: Ultra-Low Memory (v3.0.0) - 149MB image, 28MB runtime
docker build -t rust-java-rest:ultra -f src/main/resources/container/Dockerfile.ultra .
docker run -d -p 8080:8080 --memory=50m --name rust-java rust-java-rest:ultraOption 2: Minimal (Distroless + jlink) - 74MB
docker build -t rust-java-rest:minimal -f src/main/resources/container/Dockerfile.minimal .Option 3: Standard (Debian + jlink) - 136MB
docker build -t rust-java-rest:optimized -f src/main/resources/container/Dockerfile.optimized .# With 50MB memory limit (v3.0.0 - recommended)
docker run -d -p 8080:8080 --memory=50m --name rust-java-app rust-java-rest:ultra
# With 40MB memory limit (v2.0.0)
docker run -d -p 8080:8080 --memory=40m --name rust-java-app rust-java-rest:minimal| Feature | Ultra (v3.0.0) | Minimal | Optimized |
|---|---|---|---|
| Base Image | Debian slim | Distroless | Debian slim |
| Image Size | 149MB | 74MB | 136MB |
| Runtime Memory | 28 MB | ~35 MB | ~35 MB |
| jlink JRE | ~25MB | 35MB | 35MB |
| Health Check | curl | External | curl |
| Memory Limit | 50MB | 40MB | 40MB |
| Non-root User | Yes | Yes | Yes |
| Multi-stage Build | Yes (4 stages) | Yes | Yes |
# v3.0.0 Ultra-low memory settings
-Xms4m # Minimum heap (4MB)
-Xmx24m # Maximum heap (24MB)
-XX:+UseSerialGC # Lowest memory GC
-XX:MaxMetaspaceSize=20m # Metaspace limit (reduced)
-XX:ReservedCodeCacheSize=8m # Code cache limit
-XX:+TieredCompilation # Fast startup
-XX:TieredStopAtLevel=1 # C1 compiler only
-XX:CICompilerCount=1 # Single compiler thread
-XX:+UseCompressedOops # Memory optimization
-XX:+UseCompressedClassPointers # Memory optimization
-XX:+UseStringDeduplication # String deduplication
-Xss256k # Thread stack sizeversion: '3.8'
services:
rust-java-rest:
image: rust-java-rest:2.0.0
ports:
- "8080:8080"
deploy:
resources:
limits:
memory: 40M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 3s
retries: 3- Java 21+
- Maven 3.8+
com.myapp/
βββ Application.java # Main class
βββ dto/
β βββ OrderRequest.java
β βββ OrderResponse.java
βββ handler/
βββ OrderHandler.java
| Annotation | Description |
|---|---|
@RequestMapping |
Class-level base path |
@GetMapping |
GET request handler |
@PostMapping |
POST request handler |
@PutMapping |
PUT request handler |
@PatchMapping |
PATCH request handler |
@DeleteMapping |
DELETE request handler |
@PathVariable |
Get path parameter |
@RequestParam |
Get query parameter |
@HeaderParam |
Get header value |
@RequestBody |
Deserialize request body |
@CookieValue |
Get cookie value |
@ResponseStatus |
Specify HTTP status code |
@RustRoute |
Legacy annotation (V4 signature) |
@Request |
Mark Request DTO |
@Response |
Mark Response DTO |
| Annotation | Description |
|---|---|
@Component |
Mark general component |
@Service |
Business logic service |
@Repository |
Data access layer |
@Configuration |
Configuration class |
@Bean |
Bean-producing method |
@Autowired |
Dependency injection |
@PostConstruct |
Initialization callback |
@PreDestroy |
Cleanup callback |
@Primary |
Primary bean |
@Qualifier |
Bean selection |
| Annotation | Description |
|---|---|
@WebSocket |
Mark WebSocket handler class |
@OnOpen |
Connection opened callback |
@OnMessage |
Message received handler |
@OnClose |
Connection closed callback |
@OnError |
Error handler |
| Annotation | Description |
|---|---|
@StaticFiles |
Configure static file serving |
The framework provides zero-overhead Dependency Injection similar to Spring Boot. All dependencies are resolved at startup, NO runtime reflection.
| Annotation | Description |
|---|---|
@Component |
General component |
@Service |
Business logic service |
@Repository |
Data access layer |
@Configuration |
Configuration class |
@Bean |
Bean-producing method |
@Autowired |
Dependency injection |
@PostConstruct |
Initialization callback |
@PreDestroy |
Cleanup callback |
@Primary |
Primary bean |
@Qualifier |
Bean selection |
import com.reactor.rust.di.annotation.Service;
import com.reactor.rust.di.annotation.Autowired;
import com.reactor.rust.di.annotation.PostConstruct;
@Service
public class OrderService {
@Autowired(required = false)
private NotificationService notificationService;
private final Map<String, Order> orders = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
System.out.println("[OrderService] Initialized");
}
public Order createOrder(OrderRequest request) {
Order order = new Order(generateId(), request);
orders.put(order.id(), order);
if (notificationService != null) {
notificationService.notify("Order created: " + order.id());
}
return order;
}
}import com.reactor.rust.di.annotation.Configuration;
import com.reactor.rust.di.annotation.Bean;
@Configuration
public class AppConfiguration {
@Bean
public ExecutorService taskExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
@Bean("appMetadata")
public AppMetadata appMetadata() {
return new AppMetadata("my-app", "1.0.0");
}
public record AppMetadata(String name, String version) {}
}import com.reactor.rust.di.annotation.Autowired;
@RequestMapping("/order")
public class OrderHandler {
@Autowired
private OrderService orderService; // Automatically injected
@PostMapping(value = "/create", requestType = OrderRequest.class, responseType = OrderResponse.class)
public ResponseEntity<OrderResponse> create(@RequestBody OrderRequest request) {
// orderService is automatically injected
Order order = orderService.createOrder(request);
return ResponseEntity.ok(new OrderResponse(1, "OK"));
}
}import com.reactor.rust.di.BeanContainer;
import com.reactor.rust.bridge.HandlerRegistry;
import com.reactor.rust.bridge.RouteScanner;
import com.reactor.rust.bridge.NativeBridge;
public class Application {
public static void main(String[] args) throws InterruptedException {
// 1. Initialize DI Container
BeanContainer container = BeanContainer.getInstance();
// 2. Component scanning
container.scan("com.myapp");
// 3. Start container (all dependencies resolved)
container.start();
// 4. Scan routes
RouteScanner.scanAndRegister();
// 5. Register handlers
HandlerRegistry registry = HandlerRegistry.getInstance();
registry.registerBean(new OrderHandler());
// 6. Start server
NativeBridge.startHttpServer(8080);
System.out.println("Server running: http://localhost:8080");
Thread.sleep(Long.MAX_VALUE);
}
}import com.reactor.rust.di.annotation.Service;
import com.reactor.rust.di.annotation.PostConstruct;
import com.reactor.rust.di.annotation.PreDestroy;
@Service
public class NotificationService {
private ExecutorService executor;
@PostConstruct
public void init() {
// Initialization
executor = Executors.newSingleThreadExecutor();
System.out.println("[NotificationService] Ready");
}
@PreDestroy
public void cleanup() {
// Cleanup
executor.shutdown();
System.out.println("[NotificationService] Shutdown");
}
public void notify(String message) {
executor.submit(() -> sendNotification(message));
}
}When multiple beans implement the same interface, use @Primary to mark the default and @Qualifier to select a specific implementation.
// Payment interface
public interface PaymentService {
String processPayment(String orderId, double amount);
String getPaymentMethod();
}
// Primary implementation (default)
@Service
@Primary // <-- Makes this the default when multiple candidates exist
public class CreditCardPaymentService implements PaymentService {
@Override
public String processPayment(String orderId, double amount) {
return "CC-" + System.currentTimeMillis();
}
@Override
public String getPaymentMethod() {
return "CREDIT_CARD";
}
}
// Alternative implementation
@Service
public class PayPalPaymentService implements PaymentService {
@Override
public String processPayment(String orderId, double amount) {
return "PP-" + System.currentTimeMillis();
}
@Override
public String getPaymentMethod() {
return "PAYPAL";
}
}
// Another alternative
@Service
public class BankTransferPaymentService implements PaymentService {
@Override
public String processPayment(String orderId, double amount) {
return "BT-" + System.currentTimeMillis();
}
@Override
public String getPaymentMethod() {
return "BANK_TRANSFER";
}
}@Component
public class PaymentHandler {
// @Primary injection - gets CreditCardPaymentService by default
@Autowired
private PaymentService paymentService;
// @Qualifier injection - gets specific implementation
@Autowired
@Qualifier("payPalPaymentService")
private PaymentService payPalService;
@Autowired
@Qualifier("bankTransferPaymentService")
private PaymentService bankService;
@PostMapping(value = "/payment/process", requestType = PaymentRequest.class, responseType = PaymentResponse.class)
public ResponseEntity<PaymentResponse> processPayment(@RequestBody PaymentRequest request) {
// Uses @Primary (CreditCardPaymentService)
String txId = paymentService.processPayment(request.orderId(), request.amount());
return ResponseEntity.ok(new PaymentResponse(txId, paymentService.getPaymentMethod(), "SUCCESS"));
}
@PostMapping(value = "/payment/paypal", requestType = PaymentRequest.class, responseType = PaymentResponse.class)
public ResponseEntity<PaymentResponse> processPayPal(@RequestBody PaymentRequest request) {
// Uses @Qualifier("payPalPaymentService")
String txId = payPalService.processPayment(request.orderId(), request.amount());
return ResponseEntity.ok(new PaymentResponse(txId, payPalService.getPaymentMethod(), "SUCCESS"));
}
@GetMapping(value = "/payment/methods", responseType = PaymentMethodsResponse.class)
public ResponseEntity<PaymentMethodsResponse> getPaymentMethods() {
// Access all implementations
return ResponseEntity.ok(new PaymentMethodsResponse(List.of(
new PaymentMethodInfo("credit-card", paymentService.getPaymentMethod(), true),
new PaymentMethodInfo("paypal", payPalService.getPaymentMethod(), false),
new PaymentMethodInfo("bank-transfer", bankService.getPaymentMethod(), false)
)));
}
}Bean names default to camelCase class name:
CreditCardPaymentService->creditCardPaymentServicePayPalPaymentService->payPalPaymentServiceBankTransferPaymentService->bankTransferPaymentService
You can also specify a custom name with @Service("customName").
| Metric | Value |
|---|---|
| Bean Lookup | O(1) ConcurrentHashMap |
| Lookup Time | ~0.4 microseconds |
| Memory Overhead | ~50-100 bytes/bean |
| Runtime Reflection | NONE |
| Feature | Rust-Java REST | Spring Boot |
|---|---|---|
| Startup Time | ~100ms | ~2-5s |
| Memory Overhead | ~1-2 MB | ~30-50 MB |
| Bean Lookup | O(1) direct | O(1) + proxy |
| Runtime Reflection | No | Yes |
| AOP Support | No | Yes |
| Proxy Overhead | No | Yes |
Full WebSocket support with annotation-based handlers.
import com.reactor.rust.websocket.annotation.WebSocket;
import com.reactor.rust.websocket.annotation.OnOpen;
import com.reactor.rust.websocket.annotation.OnMessage;
import com.reactor.rust.websocket.annotation.OnClose;
import com.reactor.rust.websocket.annotation.OnError;
import com.reactor.rust.websocket.WebSocketSession;
@Component
@WebSocket("/ws/echo")
public class EchoWebSocketHandler {
@OnOpen
public void onOpen(WebSocketSession session) {
System.out.println("Session opened: " + session.getId());
session.sendText("{\"type\":\"connected\",\"sessionId\":\"" + session.getId() + "\"}");
}
@OnMessage
public void onMessage(WebSocketSession session, String message) {
session.sendText("{\"type\":\"echo\",\"message\":\"" + escapeJson(message) + "\"}");
}
@OnClose
public void onClose(WebSocketSession session) {
System.out.println("Session closed: " + session.getId());
}
@OnError
public void onError(WebSocketSession session, String error) {
System.err.println("Error: " + error);
}
}@Component
@WebSocket("/ws/chat/{roomId}")
public class ChatWebSocketHandler {
private final ConcurrentHashMap<String, Set<WebSocketSession>> rooms = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(WebSocketSession session) {
String roomId = session.getPathParams().get("roomId");
rooms.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(session);
broadcast(roomId, "{\"type\":\"join\",\"sessionId\":\"" + session.getId() + "\"}");
}
@OnMessage
public void onMessage(WebSocketSession session, String message) {
String roomId = session.getPathParams().get("roomId");
broadcast(roomId, "{\"type\":\"message\",\"text\":\"" + escapeJson(message) + "\"}");
}
private void broadcast(String roomId, String message) {
for (WebSocketSession s : rooms.get(roomId)) {
s.sendText(message);
}
}
}import com.reactor.rust.websocket.WebSocketBroadcaster;
WebSocketBroadcaster broadcaster = WebSocketBroadcaster.getInstance();
// Broadcast to all sessions
broadcaster.broadcast("{\"type\":\"notification\",\"text\":\"Hello all!\"}");
// Broadcast to specific room
broadcaster.broadcastToRoom("room1", "{\"type\":\"message\",\"text\":\"Hello room1!\"}");
// Broadcast excluding sender
broadcaster.broadcast(message, excludeSessionId);
// Broadcast binary data
broadcaster.broadcastBinary(data);
broadcaster.broadcastBinaryToRoom("room1", data);
// Room management
broadcaster.joinRoom(sessionId, "room1");
broadcaster.leaveRoom(sessionId, "room1");
broadcaster.getSessionsInRoom("room1");// Echo
const ws = new WebSocket('ws://localhost:8080/ws/echo');
ws.onopen = () => ws.send('Hello!');
ws.onmessage = (e) => console.log(e.data);
// Chat room
const chat = new WebSocket('ws://localhost:8080/ws/chat/room1');
chat.onopen = () => chat.send('Hi everyone!');
chat.onmessage = (e) => console.log(e.data);Support for non-blocking async handlers with virtual threads (Java 21+).
import java.util.concurrent.CompletableFuture;
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
public CompletableFuture<Order> createOrderAsync(OrderRequest request) {
return CompletableFuture.supplyAsync(() -> {
// This runs on a virtual thread (Java 21+)
Order order = new Order(generateId(), request);
// Process payment (blocking call)
paymentService.process(order);
return order;
});
}
}@RequestMapping("/order")
public class OrderHandler {
@Autowired
private OrderService orderService;
@PostMapping(value = "/create", requestType = OrderRequest.class, responseType = OrderResponse.class)
public CompletableFuture<ResponseEntity<OrderResponse>> createAsync(
@RequestBody OrderRequest request) {
return orderService.createOrderAsync(request)
.thenApply(order -> ResponseEntity.ok(
new OrderResponse(order.getId(), "Created", order.getAmount())
))
.exceptionally(ex -> ResponseEntity.status(500).body(
new OrderResponse(-1, "Error: " + ex.getMessage(), 0)
));
}
// Multiple async calls combined
@GetMapping(value = "/{id}/full", responseType = FullOrderResponse.class)
public CompletableFuture<ResponseEntity<FullOrderResponse>> getFullOrder(
@PathVariable("id") String orderId) {
CompletableFuture<Order> orderFuture = orderService.getOrderAsync(orderId);
CompletableFuture<List<Payment>> paymentsFuture = paymentService.getPaymentsAsync(orderId);
return CompletableFuture.allOf(orderFuture, paymentsFuture)
.thenApply(v -> ResponseEntity.ok(new FullOrderResponse(
orderFuture.join(),
paymentsFuture.join()
)));
}
}import com.reactor.rust.async.AsyncHandlerExecutor;
AsyncHandlerExecutor executor = AsyncHandlerExecutor.getInstance();
// Submit async task
CompletableFuture<Order> future = executor.submit(() -> {
return db.query("SELECT * FROM orders WHERE id = ?", id);
});
// Submit with timeout (5 seconds)
CompletableFuture<Order> future = executor.submit(() -> {
return externalApi.call();
}, 5000);Production-ready static file serving with caching and MIME type detection.
import com.reactor.rust.annotations.StaticFiles;
@Component
@StaticFiles(path = "/static", location = "static")
public class StaticFileConfig {}This serves files from classpath:/static/ at /static/*:
GET /static/css/style.css β classpath:/static/css/style.css
GET /static/js/app.js β classpath:/static/js/app.js
GET /static/ β classpath:/static/index.html (default)
GET /static/images/logo.png β classpath:/static/images/logo.png
@Component
@StaticFiles(
path = "/public",
location = "public",
directoryListing = false, // Enable directory listing (default: false)
cacheMaxAge = 3600, // Cache max-age in seconds (default: 3600)
indexFile = "index.html" // Index file for directories (default: "index.html")
)
public class PublicStaticFiles {}@Component
@StaticFiles(path = "/assets", location = "assets")
public class AssetsConfig {}
@Component
@StaticFiles(path = "/uploads", location = "uploads", cacheMaxAge = 0)
public class UploadsConfig {}
@Component
@StaticFiles(path = "/", location = "public", indexFile = "index.html")
public class RootStaticFiles {}| Extension | MIME Type |
|---|---|
| .html, .htm | text/html |
| .css | text/css |
| .js | application/javascript |
| .json | application/json |
| .png | image/png |
| .jpg, .jpeg | image/jpeg |
| .gif | image/gif |
| .svg | image/svg+xml |
| .ico | image/x-icon |
| .webp | image/webp |
| .woff, .woff2 | font/woff, font/woff2 |
| .ttf | font/ttf |
| .mp4 | video/mp4 |
| .webm | video/webm |
| .mp3 | audio/mpeg |
| application/pdf | |
| .xml | application/xml |
| .txt | text/plain |
Small files (< 1MB) are automatically cached in memory.
// Clear cache programmatically (development mode)
StaticFileRegistry.getInstance().clearCache();Comprehensive load testing comparing Rust-Java REST Framework vs Spring Boot.
| Configuration | Value |
|---|---|
| Platform | Windows 10 x64 |
| JDK | OpenJDK 21 |
| Memory Limit | 40 MB (framework), 200 MB (Spring Boot) |
| Endpoint | /api/v1/candidates (JSON response with 19 nested objects) |
| Warmup | 500 requests |
Higher is better.
| Concurrency | Rust-Java REST | Spring Boot | Improvement |
|---|---|---|---|
| 10 | 2,937 RPS | ~1,150 RPS | 155% faster |
| 50 | 2,299 RPS | ~980 RPS | 135% faster |
| 100 | 3,626 RPS | ~850 RPS | 326% faster |
| 1000 | 2,738 RPS | ~400 RPS | 585% faster |
Lower is better.
| Concurrency | Rust-Java (avg) | Rust-Java (P99) | Spring Boot (avg) | Spring Boot (P99) |
|---|---|---|---|---|
| 10 | 3.34 ms | 16.61 ms | ~15 ms | ~50 ms |
| 50 | 21.49 ms | 342.63 ms | ~75 ms | ~200 ms |
| 100 | 26.81 ms | 223.46 ms | ~120 ms | ~350 ms |
| 1000 | 285.51 ms | 1650.88 ms | ~800 ms | ~2500 ms |
Lower is better.
| Metric | Rust-Java REST | Spring Boot | Improvement |
|---|---|---|---|
| Docker Image | 74 MB | ~300 MB | 75% smaller |
| Heap at Startup | ~4 MB | ~50 MB | 92% less |
| Heap under Load | ~27 MB | ~94 MB | 71% less |
| Max Memory Config | 40 MB | 200 MB | 80% less |
Lower is better.
| Metric | Rust-Java REST | Spring Boot |
|---|---|---|
| GC Algorithm | Serial GC | G1GC |
| GC Pauses/sec | ~0.1 | ~2-5 |
| Pause Duration | <1ms | 10-50ms |
| Object Allocation | Minimal (ThreadLocal reuse) | High (wrappers, proxies) |
| Concurrency | Rust-Java REST | Spring Boot |
|---|---|---|
| 10 | 100% | 100% |
| 50 | 100% | ~99.8% |
| 100 | 100% | ~99.5% |
| 1000 | 100% | ~95% |
Running with strict 40MB memory limit:
# Rust-Java REST - Works perfectly
docker run --memory=40m -p 8080:8080 ghcr.io/esasmer-dou/rust-java-rest:2.0.0
# Spring Boot - OOM Killed
docker run --memory=40m -p 8081:8080 spring-boot-app
# Error: java.lang.OutOfMemoryError: Java heap space// Using Java 21 HttpClient with concurrent requests
ExecutorService executor = Executors.newFixedThreadPool(concurrency);
for (int i = 0; i < requests; i++) {
executor.submit(() -> {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
});
}- Rust Hyper HTTP Server: Zero-copy network I/O, async Tokio runtime
- DSL-JSON Serialization: Compile-time code generation, no runtime reflection
- ThreadLocal Buffer Pools: Eliminates per-request allocations
- Minimal GC Pressure: Object reuse patterns, primitive types where possible
- Direct ByteBuffer: Zero-copy JNI boundary crossing
MIT