Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Thanks for contributing to `jsonrpc-spring-boot-starter`.
## Scope

This project provides JSON-RPC 2.0 server components for Spring Boot:

- core protocol/dispatch module
- Spring WebMVC transport module
- Spring Boot auto-configuration and starter
Expand All @@ -20,6 +21,7 @@ Protocol behavior should stay aligned with the JSON-RPC 2.0 specification.
## Development Setup

Requirements:

- JDK 17+
- Gradle wrapper (`./gradlew`)

Expand All @@ -32,26 +34,36 @@ Common commands:
./gradlew -p samples/spring-boot-demo classes
```

Null-safety gate:

- Production code (`compile*Java` except test source sets) is validated by NullAway during `check`.
- Test source sets are intentionally excluded from NullAway to keep tests focused on behavior assertions.
- If NullAway fails, fix nullable contracts (`@Nullable`, guard clauses, fallback defaults) before opening a PR.

## Coding Guidelines

- Follow existing module boundaries and abstraction style.
- Preserve JSON-RPC 2.0 compliance.
- Add or update tests for:
- success paths
- failure paths
- exception/edge branches
- success paths
- failure paths
- exception/edge branches
- Keep public API behavior backward compatible unless a breaking change is intentional and documented.

## Issue Labels and Triage

This repository uses a two-axis label taxonomy:

- `type:*` labels classify issue category (`type: bug`, `type: feature`, etc.).
- `status:*` labels represent workflow state (`status: blocked`, `status: declined`, `status: duplicate`, `status: waiting-for-feedback`).
- `status:*` labels represent workflow state (`status: blocked`, `status: declined`, `status: duplicate`,
`status: waiting-for-feedback`).

Rules:

1. Every issue template must define exactly one `type:*` label and exactly one `status:*` label.
2. Only one `status:*` label should be present on an issue at a time.
3. Automated triage keeps status labels normalized on open/reopen/label events and can remove `status: waiting-for-feedback` when the issue author replies.
3. Automated triage keeps status labels normalized on open/reopen/label events and can remove
`status: waiting-for-feedback` when the issue author replies.

## Commit and PR Guidelines

Expand Down
23 changes: 19 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import me.champeau.gradle.japicmp.JapicmpTask
import net.ltgt.gradle.errorprone.CheckSeverity

plugins {
id 'base'
alias(libs.plugins.japicmp) apply false
alias(libs.plugins.errorprone) apply false
}

allprojects {
Expand All @@ -15,6 +17,7 @@ subprojects {
apply plugin: 'java-library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'net.ltgt.errorprone'

java {
toolchain {
Expand All @@ -33,11 +36,23 @@ subprojects {
testImplementation libs.junit.jupiter
testRuntimeOnly libs.junit.platform.launcher
compileOnly libs.jspecify
errorprone libs.errorprone.core
errorprone libs.nullaway
}

tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.compilerArgs += ['-parameters']

def isTestCompile = name.toLowerCase().contains('test')
options.errorprone.enabled = !isTestCompile
options.errorprone.disableWarningsInGeneratedCode = true
if (!isTestCompile) {
options.errorprone.disableAllChecks = true
options.errorprone.check('NullAway', CheckSeverity.ERROR)
options.errorprone.option('NullAway:AnnotatedPackages', 'com.limehee.jsonrpc')
options.errorprone.option('NullAway:JSpecifyMode', 'true')
}
}

tasks.withType(Test).configureEach {
Expand Down Expand Up @@ -162,10 +177,10 @@ subprojects {
}

def publishedApiModules = [
project(':jsonrpc-core'),
project(':jsonrpc-spring-webmvc'),
project(':jsonrpc-spring-boot-autoconfigure'),
project(':jsonrpc-spring-boot-starter')
project(':jsonrpc-core'),
project(':jsonrpc-spring-webmvc'),
project(':jsonrpc-spring-boot-autoconfigure'),
project(':jsonrpc-spring-boot-starter')
]
def apiBaselineVersionProvider = providers.gradleProperty('apiBaselineVersion')

Expand Down
74 changes: 38 additions & 36 deletions docs/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,35 @@ All properties are under `jsonrpc.*` and are bound to `JsonRpcProperties`.

## 1. Property Table

| Key | Type | Default | Description |
|--------------------------------------------------------|---------------------------------------|------------------|----------------------------------------------------------------------|
| `jsonrpc.enabled` | `boolean` | `true` | Enable/disable WebMVC endpoint auto-configuration |
| `jsonrpc.path` | `String` | `/jsonrpc` | JSON-RPC HTTP endpoint path |
| `jsonrpc.max-batch-size` | `int` | `100` | Maximum number of entries allowed in one batch request |
| `jsonrpc.max-request-bytes` | `int` | `1048576` | Raw HTTP request payload size limit in bytes |
| `jsonrpc.scan-annotated-methods` | `boolean` | `true` | Scan Spring beans for `@JsonRpcMethod` |
| `jsonrpc.include-error-data` | `boolean` | `false` | Include `JsonRpcException.data` in error responses |
| `jsonrpc.validation.request.params-type-violation-code-policy` | `INVALID_PARAMS` or `INVALID_REQUEST` | `INVALID_PARAMS` | Error code used when `params` exists but is neither object nor array |
| `jsonrpc.validation.response.require-json-rpc-version-20` | `boolean` | `true` | Require incoming response `jsonrpc` to equal `"2.0"` |
| `jsonrpc.validation.response.require-response-id-member` | `boolean` | `true` | Require incoming responses to include an `id` member |
| `jsonrpc.validation.response.allow-null-response-id` | `boolean` | `true` | Allow `id: null` in incoming responses |
| `jsonrpc.validation.response.allow-string-response-id` | `boolean` | `true` | Allow string IDs in incoming responses |
| `jsonrpc.validation.response.allow-numeric-response-id` | `boolean` | `true` | Allow numeric IDs in incoming responses |
| `jsonrpc.validation.response.allow-fractional-response-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming responses |
| `jsonrpc.validation.response.require-exclusive-result-or-error` | `boolean` | `true` | Require exactly one of `result` or `error` |
| `jsonrpc.validation.response.require-error-object-when-present` | `boolean` | `true` | Require `error` to be an object when present |
| `jsonrpc.validation.response.require-integer-error-code` | `boolean` | `true` | Require `error.code` to be an integer |
| `jsonrpc.validation.response.require-string-error-message` | `boolean` | `true` | Require `error.message` to be a string |
| `jsonrpc.validation.response.allow-request-fields-in-response` | `boolean` | `true` | Allow request-only fields (`method`/`params`) on responses |
| `jsonrpc.method-registration-conflict-policy` | `REJECT` or `REPLACE` | `REJECT` | Duplicate method name registration policy |
| `jsonrpc.method-allowlist` | `List<String>` | `[]` | Allowlist for method access filtering |
| `jsonrpc.method-denylist` | `List<String>` | `[]` | Denylist for method access filtering (higher priority) |
| `jsonrpc.metrics-enabled` | `boolean` | `true` | Enable Micrometer interceptor/observer when registry is present |
| `jsonrpc.metrics-latency-histogram-enabled` | `boolean` | `false` | Publish latency histogram buckets |
| `jsonrpc.metrics-latency-percentiles` | `List<Double>` | `[]` | Optional latency percentiles (`0.0 < p < 1.0`) |
| `jsonrpc.metrics-max-method-tag-values` | `int` | `100` | Max distinct method tag values before fallback to `other` |
| `jsonrpc.notification-executor-enabled` | `boolean` | `false` | Enable executor-backed notification dispatch |
| `jsonrpc.notification-executor-bean-name` | `String` | `""` | Preferred executor bean name for notifications |
| Key | Type | Default | Description |
|-----------------------------------------------------------------|---------------------------------------|------------------|----------------------------------------------------------------------|
| `jsonrpc.enabled` | `boolean` | `true` | Enable/disable WebMVC endpoint auto-configuration |
| `jsonrpc.path` | `String` | `/jsonrpc` | JSON-RPC HTTP endpoint path |
| `jsonrpc.max-batch-size` | `int` | `100` | Maximum number of entries allowed in one batch request |
| `jsonrpc.max-request-bytes` | `int` | `1048576` | Raw HTTP request payload size limit in bytes |
| `jsonrpc.scan-annotated-methods` | `boolean` | `true` | Scan Spring beans for `@JsonRpcMethod` |
| `jsonrpc.include-error-data` | `boolean` | `false` | Include `JsonRpcException.data` in error responses |
| `jsonrpc.validation.request.params-type-violation-code-policy` | `INVALID_PARAMS` or `INVALID_REQUEST` | `INVALID_PARAMS` | Error code used when `params` exists but is neither object nor array |
| `jsonrpc.validation.response.require-json-rpc-version-20` | `boolean` | `true` | Require incoming response `jsonrpc` to equal `"2.0"` |
| `jsonrpc.validation.response.require-response-id-member` | `boolean` | `true` | Require incoming responses to include an `id` member |
| `jsonrpc.validation.response.allow-null-response-id` | `boolean` | `true` | Allow `id: null` in incoming responses |
| `jsonrpc.validation.response.allow-string-response-id` | `boolean` | `true` | Allow string IDs in incoming responses |
| `jsonrpc.validation.response.allow-numeric-response-id` | `boolean` | `true` | Allow numeric IDs in incoming responses |
| `jsonrpc.validation.response.allow-fractional-response-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming responses |
| `jsonrpc.validation.response.require-exclusive-result-or-error` | `boolean` | `true` | Require exactly one of `result` or `error` |
| `jsonrpc.validation.response.require-error-object-when-present` | `boolean` | `true` | Require `error` to be an object when present |
| `jsonrpc.validation.response.require-integer-error-code` | `boolean` | `true` | Require `error.code` to be an integer |
| `jsonrpc.validation.response.require-string-error-message` | `boolean` | `true` | Require `error.message` to be a string |
| `jsonrpc.validation.response.allow-request-fields-in-response` | `boolean` | `true` | Allow request-only fields (`method`/`params`) on responses |
| `jsonrpc.method-registration-conflict-policy` | `REJECT` or `REPLACE` | `REJECT` | Duplicate method name registration policy |
| `jsonrpc.method-allowlist` | `List<String>` | `[]` | Allowlist for method access filtering |
| `jsonrpc.method-denylist` | `List<String>` | `[]` | Denylist for method access filtering (higher priority) |
| `jsonrpc.metrics-enabled` | `boolean` | `true` | Enable Micrometer interceptor/observer when registry is present |
| `jsonrpc.metrics-latency-histogram-enabled` | `boolean` | `false` | Publish latency histogram buckets |
| `jsonrpc.metrics-latency-percentiles` | `List<Double>` | `[]` | Optional latency percentiles (`0.0 < p < 1.0`) |
| `jsonrpc.metrics-max-method-tag-values` | `int` | `100` | Max distinct method tag values before fallback to `other` |
| `jsonrpc.notification-executor-enabled` | `boolean` | `false` | Enable executor-backed notification dispatch |
| `jsonrpc.notification-executor-bean-name` | `String` | `""` | Preferred executor bean name for notifications |

## 2. Validation Rules (Fail Fast)

Expand Down Expand Up @@ -87,7 +87,8 @@ If a configured bean name is missing, startup fails.
- `REJECT`: first duplicate fails registration.
- `REPLACE`: later registration wins.

In auto-configuration, annotation scanning runs after manual registrations, so annotation handlers can replace manual handlers under `REPLACE`.
In auto-configuration, annotation scanning runs after manual registrations, so annotation handlers can replace manual
handlers under `REPLACE`.

## 4. Property Source Precedence (Spring Boot)

Expand All @@ -102,7 +103,8 @@ Example environment variable mapping:

- `jsonrpc.max-request-bytes` -> `JSONRPC_MAX_REQUEST_BYTES`
- `jsonrpc.method-registration-conflict-policy` -> `JSONRPC_METHOD_REGISTRATION_CONFLICT_POLICY`
- `jsonrpc.validation.request.params-type-violation-code-policy` -> `JSONRPC_VALIDATION_REQUEST_PARAMS_TYPE_VIOLATION_CODE_POLICY`
- `jsonrpc.validation.request.params-type-violation-code-policy` ->
`JSONRPC_VALIDATION_REQUEST_PARAMS_TYPE_VIOLATION_CODE_POLICY`

## 5. Example Configurations

Expand Down Expand Up @@ -131,16 +133,16 @@ jsonrpc:
require-integer-error-code: true
require-string-error-message: true
allow-request-fields-in-response: true
method-allowlist: []
method-denylist: []
method-allowlist: [ ]
method-denylist: [ ]
```

## 5.2 Strict access profile

```yaml
jsonrpc:
method-allowlist: [user.find, user.update]
method-denylist: [user.delete]
method-allowlist: [ user.find, user.update ]
method-denylist: [ user.delete ]
```

## 5.3 Async notification profile
Expand All @@ -157,7 +159,7 @@ jsonrpc:
jsonrpc:
metrics-enabled: true
metrics-latency-histogram-enabled: true
metrics-latency-percentiles: [0.9, 0.95, 0.99]
metrics-latency-percentiles: [ 0.9, 0.95, 0.99 ]
metrics-max-method-tag-values: 200
```

Expand Down
17 changes: 9 additions & 8 deletions docs/extension-points.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ When `MeterRegistry` is present and `jsonrpc.metrics-enabled=true`, a Micrometer
Metrics:

- Counter: `jsonrpc.server.calls`
- tags: `method`, `outcome`, `errorCode`
- tags: `method`, `outcome`, `errorCode`
- Timer: `jsonrpc.server.latency`
- tags: `method`, `outcome`
- tags: `method`, `outcome`
- Counter: `jsonrpc.server.stage.events`
- tags: `method`, `stage`
- tags: `method`, `stage`
- Counter: `jsonrpc.server.failures`
- tags: `method`, `errorCode`, `source`
- tags: `method`, `errorCode`, `source`
- Counter: `jsonrpc.server.transport.errors`
- tags: `reason` (`parse_error`, `request_too_large`)
- tags: `reason` (`parse_error`, `request_too_large`)
- Counter: `jsonrpc.server.batch.requests`
- tags: `outcome` (`all_success`, `all_error`, `mixed`, `notification_only`)
- tags: `outcome` (`all_success`, `all_error`, `mixed`, `notification_only`)
- Counter: `jsonrpc.server.batch.entries`
- tags: `outcome` (`success`, `error`, `notification`)
- tags: `outcome` (`success`, `error`, `notification`)
- Summary: `jsonrpc.server.batch.size`
- Timer: `jsonrpc.server.notification.queue.delay`
- Timer: `jsonrpc.server.notification.execution`
Expand Down Expand Up @@ -115,6 +115,7 @@ Default strategy returns `200` for protocol responses and `204` for notification

- `DirectJsonRpcNotificationExecutor`: same thread
- `ExecutorJsonRpcNotificationExecutor`: delegated to Java `Executor`
- `InstrumentedJsonRpcNotificationExecutor`: wraps notification execution for queue/latency/failure metrics when metrics are enabled
- `InstrumentedJsonRpcNotificationExecutor`: wraps notification execution for queue/latency/failure metrics when metrics
are enabled

You can provide your own implementation for custom backpressure/isolation/retry behavior.
7 changes: 4 additions & 3 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ JMH benchmark exists in `jsonrpc-core`:
./gradlew :jsonrpc-core:jmh
```

It includes dispatcher scenarios for single success/error/invalid cases and large batch profiles (all-success, all-error, mixed, notification-only).
It includes dispatcher scenarios for single success/error/invalid cases and large batch profiles (all-success,
all-error, mixed, notification-only).

Quick profile (short warmup/measurement):

Expand All @@ -50,8 +51,8 @@ Run quick profile for a specific benchmark include pattern:
- Use allowlist/denylist to reduce exposed method surface area.
- Set `jsonrpc.metrics-max-method-tag-values` to bound method tag cardinality.
- Enable histogram/percentiles only when needed:
- `jsonrpc.metrics-latency-histogram-enabled`
- `jsonrpc.metrics-latency-percentiles`
- `jsonrpc.metrics-latency-histogram-enabled`
- `jsonrpc.metrics-latency-percentiles`

## Performance Testing Guidance

Expand Down
Loading