Skip to content

Commit 82b50f0

Browse files
authored
build: enforce NullAway and tighten nullability contracts across modules (#19)
## Summary Enforced NullAway checks for production source sets and aligned nullability contracts across core/autoconfigure internals. Also applied module-level formatting updates for `jsonrpc-core`, `jsonrpc-spring-webmvc`, `jsonrpc-spring-boot-autoconfigure`, `jsonrpc-spring-boot-starter`, `samples`, and `docs`. Key changes: - Build quality gate: - Added Error Prone + NullAway in Gradle version catalog and root build. - Enabled NullAway for production Java compile tasks and excluded test source sets. - Core nullability alignment: - Kept defensive runtime guards on public entry points. - Removed impossible internal null branches where flow already guarantees non-null. - Added/updated tests for binder/dispatcher/exception resolver behavior. - Auto-configuration/support alignment: - Normalized nullable/non-null contracts in interceptor/metrics/observer/executor paths. - Added explicit non-null constructor guards for required collaborators. - Formatting-only normalization: - core, webmvc, autoconfigure, starter, samples, and docs. ## Related Issues - Closes #18 - Related #17 ## Change Type - [ ] Bug fix - [ ] Feature - [x] Refactor - [x] Documentation - [x] Test - [x] Build/CI ## JSON-RPC Impact Describe protocol-level impact, if any: - [x] No protocol behavior change - [ ] Request validation behavior changed - [ ] Error mapping behavior changed - [ ] Method registration/dispatch behavior changed Details: - This PR strengthens compile-time null-safety and code consistency; JSON-RPC protocol semantics remain unchanged. ## Validation - [x] `./gradlew test` - [x] `./gradlew check` - [x] Added/updated tests for new behavior ## Documentation - [ ] Updated README (if needed) - [ ] Added migration notes for breaking changes (if any)
1 parent d3e5c04 commit 82b50f0

110 files changed

Lines changed: 2477 additions & 2282 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Thanks for contributing to `jsonrpc-spring-boot-starter`.
55
## Scope
66

77
This project provides JSON-RPC 2.0 server components for Spring Boot:
8+
89
- core protocol/dispatch module
910
- Spring WebMVC transport module
1011
- Spring Boot auto-configuration and starter
@@ -20,6 +21,7 @@ Protocol behavior should stay aligned with the JSON-RPC 2.0 specification.
2021
## Development Setup
2122

2223
Requirements:
24+
2325
- JDK 17+
2426
- Gradle wrapper (`./gradlew`)
2527

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

37+
Null-safety gate:
38+
39+
- Production code (`compile*Java` except test source sets) is validated by NullAway during `check`.
40+
- Test source sets are intentionally excluded from NullAway to keep tests focused on behavior assertions.
41+
- If NullAway fails, fix nullable contracts (`@Nullable`, guard clauses, fallback defaults) before opening a PR.
42+
3543
## Coding Guidelines
3644

3745
- Follow existing module boundaries and abstraction style.
3846
- Preserve JSON-RPC 2.0 compliance.
3947
- Add or update tests for:
40-
- success paths
41-
- failure paths
42-
- exception/edge branches
48+
- success paths
49+
- failure paths
50+
- exception/edge branches
4351
- Keep public API behavior backward compatible unless a breaking change is intentional and documented.
4452

4553
## Issue Labels and Triage
4654

4755
This repository uses a two-axis label taxonomy:
56+
4857
- `type:*` labels classify issue category (`type: bug`, `type: feature`, etc.).
49-
- `status:*` labels represent workflow state (`status: blocked`, `status: declined`, `status: duplicate`, `status: waiting-for-feedback`).
58+
- `status:*` labels represent workflow state (`status: blocked`, `status: declined`, `status: duplicate`,
59+
`status: waiting-for-feedback`).
5060

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

5668
## Commit and PR Guidelines
5769

build.gradle

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import me.champeau.gradle.japicmp.JapicmpTask
2+
import net.ltgt.gradle.errorprone.CheckSeverity
23

34
plugins {
45
id 'base'
56
alias(libs.plugins.japicmp) apply false
7+
alias(libs.plugins.errorprone) apply false
68
}
79

810
allprojects {
@@ -15,6 +17,7 @@ subprojects {
1517
apply plugin: 'java-library'
1618
apply plugin: 'maven-publish'
1719
apply plugin: 'signing'
20+
apply plugin: 'net.ltgt.errorprone'
1821

1922
java {
2023
toolchain {
@@ -33,11 +36,23 @@ subprojects {
3336
testImplementation libs.junit.jupiter
3437
testRuntimeOnly libs.junit.platform.launcher
3538
compileOnly libs.jspecify
39+
errorprone libs.errorprone.core
40+
errorprone libs.nullaway
3641
}
3742

3843
tasks.withType(JavaCompile).configureEach {
3944
options.encoding = 'UTF-8'
4045
options.compilerArgs += ['-parameters']
46+
47+
def isTestCompile = name.toLowerCase().contains('test')
48+
options.errorprone.enabled = !isTestCompile
49+
options.errorprone.disableWarningsInGeneratedCode = true
50+
if (!isTestCompile) {
51+
options.errorprone.disableAllChecks = true
52+
options.errorprone.check('NullAway', CheckSeverity.ERROR)
53+
options.errorprone.option('NullAway:AnnotatedPackages', 'com.limehee.jsonrpc')
54+
options.errorprone.option('NullAway:JSpecifyMode', 'true')
55+
}
4156
}
4257

4358
tasks.withType(Test).configureEach {
@@ -162,10 +177,10 @@ subprojects {
162177
}
163178

164179
def publishedApiModules = [
165-
project(':jsonrpc-core'),
166-
project(':jsonrpc-spring-webmvc'),
167-
project(':jsonrpc-spring-boot-autoconfigure'),
168-
project(':jsonrpc-spring-boot-starter')
180+
project(':jsonrpc-core'),
181+
project(':jsonrpc-spring-webmvc'),
182+
project(':jsonrpc-spring-boot-autoconfigure'),
183+
project(':jsonrpc-spring-boot-starter')
169184
]
170185
def apiBaselineVersionProvider = providers.gradleProperty('apiBaselineVersion')
171186

docs/configuration-reference.md

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,35 @@ All properties are under `jsonrpc.*` and are bound to `JsonRpcProperties`.
44

55
## 1. Property Table
66

7-
| Key | Type | Default | Description |
8-
|--------------------------------------------------------|---------------------------------------|------------------|----------------------------------------------------------------------|
9-
| `jsonrpc.enabled` | `boolean` | `true` | Enable/disable WebMVC endpoint auto-configuration |
10-
| `jsonrpc.path` | `String` | `/jsonrpc` | JSON-RPC HTTP endpoint path |
11-
| `jsonrpc.max-batch-size` | `int` | `100` | Maximum number of entries allowed in one batch request |
12-
| `jsonrpc.max-request-bytes` | `int` | `1048576` | Raw HTTP request payload size limit in bytes |
13-
| `jsonrpc.scan-annotated-methods` | `boolean` | `true` | Scan Spring beans for `@JsonRpcMethod` |
14-
| `jsonrpc.include-error-data` | `boolean` | `false` | Include `JsonRpcException.data` in error responses |
15-
| `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 |
16-
| `jsonrpc.validation.response.require-json-rpc-version-20` | `boolean` | `true` | Require incoming response `jsonrpc` to equal `"2.0"` |
17-
| `jsonrpc.validation.response.require-response-id-member` | `boolean` | `true` | Require incoming responses to include an `id` member |
18-
| `jsonrpc.validation.response.allow-null-response-id` | `boolean` | `true` | Allow `id: null` in incoming responses |
19-
| `jsonrpc.validation.response.allow-string-response-id` | `boolean` | `true` | Allow string IDs in incoming responses |
20-
| `jsonrpc.validation.response.allow-numeric-response-id` | `boolean` | `true` | Allow numeric IDs in incoming responses |
21-
| `jsonrpc.validation.response.allow-fractional-response-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming responses |
22-
| `jsonrpc.validation.response.require-exclusive-result-or-error` | `boolean` | `true` | Require exactly one of `result` or `error` |
23-
| `jsonrpc.validation.response.require-error-object-when-present` | `boolean` | `true` | Require `error` to be an object when present |
24-
| `jsonrpc.validation.response.require-integer-error-code` | `boolean` | `true` | Require `error.code` to be an integer |
25-
| `jsonrpc.validation.response.require-string-error-message` | `boolean` | `true` | Require `error.message` to be a string |
26-
| `jsonrpc.validation.response.allow-request-fields-in-response` | `boolean` | `true` | Allow request-only fields (`method`/`params`) on responses |
27-
| `jsonrpc.method-registration-conflict-policy` | `REJECT` or `REPLACE` | `REJECT` | Duplicate method name registration policy |
28-
| `jsonrpc.method-allowlist` | `List<String>` | `[]` | Allowlist for method access filtering |
29-
| `jsonrpc.method-denylist` | `List<String>` | `[]` | Denylist for method access filtering (higher priority) |
30-
| `jsonrpc.metrics-enabled` | `boolean` | `true` | Enable Micrometer interceptor/observer when registry is present |
31-
| `jsonrpc.metrics-latency-histogram-enabled` | `boolean` | `false` | Publish latency histogram buckets |
32-
| `jsonrpc.metrics-latency-percentiles` | `List<Double>` | `[]` | Optional latency percentiles (`0.0 < p < 1.0`) |
33-
| `jsonrpc.metrics-max-method-tag-values` | `int` | `100` | Max distinct method tag values before fallback to `other` |
34-
| `jsonrpc.notification-executor-enabled` | `boolean` | `false` | Enable executor-backed notification dispatch |
35-
| `jsonrpc.notification-executor-bean-name` | `String` | `""` | Preferred executor bean name for notifications |
7+
| Key | Type | Default | Description |
8+
|-----------------------------------------------------------------|---------------------------------------|------------------|----------------------------------------------------------------------|
9+
| `jsonrpc.enabled` | `boolean` | `true` | Enable/disable WebMVC endpoint auto-configuration |
10+
| `jsonrpc.path` | `String` | `/jsonrpc` | JSON-RPC HTTP endpoint path |
11+
| `jsonrpc.max-batch-size` | `int` | `100` | Maximum number of entries allowed in one batch request |
12+
| `jsonrpc.max-request-bytes` | `int` | `1048576` | Raw HTTP request payload size limit in bytes |
13+
| `jsonrpc.scan-annotated-methods` | `boolean` | `true` | Scan Spring beans for `@JsonRpcMethod` |
14+
| `jsonrpc.include-error-data` | `boolean` | `false` | Include `JsonRpcException.data` in error responses |
15+
| `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 |
16+
| `jsonrpc.validation.response.require-json-rpc-version-20` | `boolean` | `true` | Require incoming response `jsonrpc` to equal `"2.0"` |
17+
| `jsonrpc.validation.response.require-response-id-member` | `boolean` | `true` | Require incoming responses to include an `id` member |
18+
| `jsonrpc.validation.response.allow-null-response-id` | `boolean` | `true` | Allow `id: null` in incoming responses |
19+
| `jsonrpc.validation.response.allow-string-response-id` | `boolean` | `true` | Allow string IDs in incoming responses |
20+
| `jsonrpc.validation.response.allow-numeric-response-id` | `boolean` | `true` | Allow numeric IDs in incoming responses |
21+
| `jsonrpc.validation.response.allow-fractional-response-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming responses |
22+
| `jsonrpc.validation.response.require-exclusive-result-or-error` | `boolean` | `true` | Require exactly one of `result` or `error` |
23+
| `jsonrpc.validation.response.require-error-object-when-present` | `boolean` | `true` | Require `error` to be an object when present |
24+
| `jsonrpc.validation.response.require-integer-error-code` | `boolean` | `true` | Require `error.code` to be an integer |
25+
| `jsonrpc.validation.response.require-string-error-message` | `boolean` | `true` | Require `error.message` to be a string |
26+
| `jsonrpc.validation.response.allow-request-fields-in-response` | `boolean` | `true` | Allow request-only fields (`method`/`params`) on responses |
27+
| `jsonrpc.method-registration-conflict-policy` | `REJECT` or `REPLACE` | `REJECT` | Duplicate method name registration policy |
28+
| `jsonrpc.method-allowlist` | `List<String>` | `[]` | Allowlist for method access filtering |
29+
| `jsonrpc.method-denylist` | `List<String>` | `[]` | Denylist for method access filtering (higher priority) |
30+
| `jsonrpc.metrics-enabled` | `boolean` | `true` | Enable Micrometer interceptor/observer when registry is present |
31+
| `jsonrpc.metrics-latency-histogram-enabled` | `boolean` | `false` | Publish latency histogram buckets |
32+
| `jsonrpc.metrics-latency-percentiles` | `List<Double>` | `[]` | Optional latency percentiles (`0.0 < p < 1.0`) |
33+
| `jsonrpc.metrics-max-method-tag-values` | `int` | `100` | Max distinct method tag values before fallback to `other` |
34+
| `jsonrpc.notification-executor-enabled` | `boolean` | `false` | Enable executor-backed notification dispatch |
35+
| `jsonrpc.notification-executor-bean-name` | `String` | `""` | Preferred executor bean name for notifications |
3636

3737
## 2. Validation Rules (Fail Fast)
3838

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

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

9293
## 4. Property Source Precedence (Spring Boot)
9394

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

103104
- `jsonrpc.max-request-bytes` -> `JSONRPC_MAX_REQUEST_BYTES`
104105
- `jsonrpc.method-registration-conflict-policy` -> `JSONRPC_METHOD_REGISTRATION_CONFLICT_POLICY`
105-
- `jsonrpc.validation.request.params-type-violation-code-policy` -> `JSONRPC_VALIDATION_REQUEST_PARAMS_TYPE_VIOLATION_CODE_POLICY`
106+
- `jsonrpc.validation.request.params-type-violation-code-policy` ->
107+
`JSONRPC_VALIDATION_REQUEST_PARAMS_TYPE_VIOLATION_CODE_POLICY`
106108

107109
## 5. Example Configurations
108110

@@ -131,16 +133,16 @@ jsonrpc:
131133
require-integer-error-code: true
132134
require-string-error-message: true
133135
allow-request-fields-in-response: true
134-
method-allowlist: []
135-
method-denylist: []
136+
method-allowlist: [ ]
137+
method-denylist: [ ]
136138
```
137139
138140
## 5.2 Strict access profile
139141
140142
```yaml
141143
jsonrpc:
142-
method-allowlist: [user.find, user.update]
143-
method-denylist: [user.delete]
144+
method-allowlist: [ user.find, user.update ]
145+
method-denylist: [ user.delete ]
144146
```
145147
146148
## 5.3 Async notification profile
@@ -157,7 +159,7 @@ jsonrpc:
157159
jsonrpc:
158160
metrics-enabled: true
159161
metrics-latency-histogram-enabled: true
160-
metrics-latency-percentiles: [0.9, 0.95, 0.99]
162+
metrics-latency-percentiles: [ 0.9, 0.95, 0.99 ]
161163
metrics-max-method-tag-values: 200
162164
```
163165

docs/extension-points.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,19 @@ When `MeterRegistry` is present and `jsonrpc.metrics-enabled=true`, a Micrometer
6969
Metrics:
7070

7171
- Counter: `jsonrpc.server.calls`
72-
- tags: `method`, `outcome`, `errorCode`
72+
- tags: `method`, `outcome`, `errorCode`
7373
- Timer: `jsonrpc.server.latency`
74-
- tags: `method`, `outcome`
74+
- tags: `method`, `outcome`
7575
- Counter: `jsonrpc.server.stage.events`
76-
- tags: `method`, `stage`
76+
- tags: `method`, `stage`
7777
- Counter: `jsonrpc.server.failures`
78-
- tags: `method`, `errorCode`, `source`
78+
- tags: `method`, `errorCode`, `source`
7979
- Counter: `jsonrpc.server.transport.errors`
80-
- tags: `reason` (`parse_error`, `request_too_large`)
80+
- tags: `reason` (`parse_error`, `request_too_large`)
8181
- Counter: `jsonrpc.server.batch.requests`
82-
- tags: `outcome` (`all_success`, `all_error`, `mixed`, `notification_only`)
82+
- tags: `outcome` (`all_success`, `all_error`, `mixed`, `notification_only`)
8383
- Counter: `jsonrpc.server.batch.entries`
84-
- tags: `outcome` (`success`, `error`, `notification`)
84+
- tags: `outcome` (`success`, `error`, `notification`)
8585
- Summary: `jsonrpc.server.batch.size`
8686
- Timer: `jsonrpc.server.notification.queue.delay`
8787
- Timer: `jsonrpc.server.notification.execution`
@@ -115,6 +115,7 @@ Default strategy returns `200` for protocol responses and `204` for notification
115115

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

120121
You can provide your own implementation for custom backpressure/isolation/retry behavior.

docs/performance.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ JMH benchmark exists in `jsonrpc-core`:
2727
./gradlew :jsonrpc-core:jmh
2828
```
2929

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

3233
Quick profile (short warmup/measurement):
3334

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

5657
## Performance Testing Guidance
5758

0 commit comments

Comments
 (0)