Local debugging guide — Java
This guide covers everything specific to debugging an Extend Event Handler app written in Java. For general debugging concepts — environment setup, VS Code debug workflow, log reading, and common issues — see:
- Extend local debugging guide — common concepts for all Extend app types
- Java language setup guide — Java prerequisites, JVM remote debug, jq, and Java-specific troubleshooting
File paths, class names, and configuration files in this guide refer to the AccelByte Extend Event Handler Java template. If your project uses different names, the concepts still apply — adjust paths to match your layout.
Prerequisites
See the Java language setup guide for installation requirements. In addition, grpcurl is required for simulating incoming events during local development — see Triggering events for testing in the main guide for installation and usage.
Project structure
| File / Class | What it does |
|---|---|
src/main/java/net/accelbyte/Application.java | Entry point — bootstraps the Spring Boot application. |
src/main/java/net/accelbyte/config/AppConfig.java | Spring configuration — validates ITEM_ID_TO_GRANT and creates the AccelByteSDK bean (calls loginClient() on startup). |
src/main/java/net/accelbyte/service/LoginHandler.java | Your business logic for userLoggedIn events — extends the generated gRPC stub and calls grantEntitlement. |
src/main/java/net/accelbyte/service/ThirdPartyLoginHandler.java | Your business logic for userThirdPartyLoggedIn events — same pattern as LoginHandler. |
src/main/java/net/accelbyte/service/EntitlementService.java | Shared Spring service that calls the AGS Fulfillment API to grant an item to a user. |
src/main/java/net/accelbyte/grpc/DebugLoggerServerInterceptor.java | Optional gRPC interceptor that logs every request/response when plugin.grpc.server.interceptor.debug-logger.enabled=true. |
src/main/java/net/accelbyte/grpc/ExceptionServerInterceptor.java | gRPC interceptor that converts unhandled exceptions to proper gRPC status codes. |
src/main/resources/application.yml | Main Spring Boot configuration — maps environment variables to application properties. |
src/main/resources/application-local.yml | Local-only profile overrides — enables the debug logger interceptor and uses logback-spring-local.xml. |
src/main/proto/ | .proto files describing AGS event message shapes. |
build.gradle | Build file — defines dependencies, the bootRun task (with OTEL agent), and the bootJar task. |
Port numbers (configured in application.yml and Spring Boot defaults):
| Port | Purpose |
|---|---|
6565 | gRPC server — receives events from Kafka Connect and simulated grpcurl calls |
8080 | Prometheus metrics endpoint (/metrics) |
Running the service locally
From the terminal
# Export all variables from your .env file
export $(grep -v '^#' .env | xargs)
./gradlew bootRun
The bootRun task (defined in build.gradle) automatically attaches the AWS OpenTelemetry
agent and activates the local Spring profile, which enables the debug logger interceptor.
Confirming the service is up
You should see log output similar to this (formatted by Logstash JSON encoder):
{"@timestamp":"...","level":"INFO","message":"Starting Application using Java 17..."}
{"@timestamp":"...","level":"INFO","message":"LoginHandler initialized"}
{"@timestamp":"...","level":"INFO","message":"ThirdPartyLoginHandler initialized"}
{"@timestamp":"...","level":"INFO","message":"DebugLoggerServerInterceptor initialized"}
{"@timestamp":"...","level":"INFO","message":"Started Application in 3.x seconds"}
Confirm gRPC reflection is working:
grpcurl -plaintext localhost:6565 list
Expected output — a list of registered services:
accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService
accelbyte.iam.account.v1.UserAuthenticationUserThirdPartyLoggedInService
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
Attaching the debugger
VS Code (recommended)
Because the repository does not yet ship a .vscode/launch.json, create one now:
mkdir -p .vscode
Then create .vscode/launch.json with the following content:
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Debug: Service",
"request": "attach",
"hostName": "localhost",
"port": 5005
}
]
}
Steps:
Follow the attaching the debugger steps in the generic guide. For Java, the debugger attaches over JDWP rather than launching directly. Start the service with the JDWP debug agent first:
export $(grep -v '^#' .env | xargs)
./gradlew bootRun --jvmArgs="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
Wait until you see Started Application in in the output, open the Run and Debug panel,
select "Debug: Service", and press F5 to attach.
The Java debugger connects over JDWP. Once attached, breakpoints set in any .java file
will pause execution.
Other IDEs — JDWP attach mode
Start the service with the JDWP agent as shown above, then configure your IDE to attach to
localhost:5005 using a Remote JVM Debug configuration (IntelliJ IDEA) or equivalent.
Suspend on startup (breakpoints in @PostConstruct or static initializers)
If you need to pause before the application initializes (e.g., to debug AppConfig), use
suspend=y instead of suspend=n. The JVM will freeze until the debugger connects:
./gradlew bootRun --jvmArgs="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005"
Where to put breakpoints
Most of the interesting logic is in the service classes and the config. Start there.
| What you want to investigate | File and location |
|---|---|
| Any login event arriving | LoginHandler.java — top of onMessage |
| Third-party login event arriving | ThirdPartyLoginHandler.java — top of onMessage |
| Entitlement grant logic | EntitlementService.java — inside grantEntitlement |
| SDK login / credential validation | AppConfig.java — inside provideAccelbyteSdk, after loginClient() |
ITEM_ID_TO_GRANT validation | AppConfig.java — inside validateItemIdToGrant |
| Inspect every raw gRPC request | DebugLoggerServerInterceptor.java — inside onMessage of the ForwardingServerCallListener |
Inspecting the incoming event payload
When the debugger pauses inside onMessage, the request parameter contains the full
event. Expand it in the Variables panel to see all fields — userId, namespace,
platformId, and others depending on the event type.
Conditional breakpoint syntax
Right-click a breakpoint → Edit Breakpoint → enter a Java expression, for example:
request.getUserId().equals("test-user-001")
For more conditional breakpoint syntax examples, see the Java language guide.
Triggering events for testing
For event simulation options and grpcurl commands, see
Triggering events for testing in the main guide.
Describe a service method
When you need to know the exact field names for a proto method, use grpcurl describe:
grpcurl -plaintext localhost:6565 \
describe accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService.OnMessage
This prints the full proto definition of the method — useful when you need to know exactly
what fields to include in the -d payload.
Reading logs
For jq log filtering, see the
Java language guide.
Trace IDs in logs
The application.yml configures the log pattern to include OpenTelemetry trace context:
trace_id=%mdc{trace_id} span_id=%mdc{span_id} trace_flags=%mdc{trace_flags} %5p
After sending a grpcurl request, grep for the trace ID to see all log lines from that
one event:
./gradlew bootRun 2>&1 | jq 'select(.trace_id != null and .trace_id != "") | {trace_id, level, message}'
Enabling the debug logger interceptor
The DebugLoggerServerInterceptor logs every raw gRPC request and response but is disabled
by default. To enable it, set the property in application-local.yml (already there) or
pass it on the command line:
./gradlew bootRun -Dplugin.grpc.server.interceptor.debug-logger.enabled=true
When enabled, each call logs:
{"level":"INFO","message":"Request path: accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService/OnMessage"}
{"level":"INFO","message":"Request headers: ..."}
{"level":"INFO","message":"Request message: userId: \"test-user-001\" namespace: \"mygame\""}
Java-specific troubleshooting
Service fails to start: ITEM_ID_TO_GRANT is not configured
Symptom:
Caused by: java.lang.IllegalArgumentException: Required envar ITEM_ID_TO_GRANT is not configured
Cause: The ITEM_ID_TO_GRANT environment variable is empty or not set.
Fix: Set ITEM_ID_TO_GRANT in your .env file to a valid item ID from a published store
in your namespace, then re-export and restart.
Service fails to start: failed to sdk.loginClient()
Symptom:
Caused by: org.springframework.web.server.ServerErrorException: failed to sdk.loginClient()
Cause: AppConfig.provideAccelbyteSdk() could not authenticate with AGS. This happens
when AB_BASE_URL, AB_CLIENT_ID, or AB_CLIENT_SECRET is wrong or missing.
Fix:
- Verify
AB_BASE_URLis reachable:curl "$AB_BASE_URL/iam/v3/public/config". - Confirm
AB_CLIENT_IDandAB_CLIENT_SECRETare set and match an existing OAuth client. - Set a breakpoint in
AppConfig.provideAccelbyteSdk()to inspect the SDK state beforeloginClient()is called.
Debugger never pauses at a breakpoint
Symptom: The attach succeeds but breakpoints are never hit.
Causes and fixes:
- Stale
.classfiles — the class the debugger loaded does not match the source. Run./gradlew clean bootRun --jvmArgs="..."to force a full recompile. - Wrong port — confirm the service is listening on 5005:
ss -tlnp | grep 5005. - Hot-swap mismatch — the Java debugger supports limited hot-swap. If you edited source after attaching, restart the service and reattach.
Checking for port conflicts
If you see Address already in use:
ss -tlnp | grep -E '6565|8080|5005'
Kill the stale process and start again.
grpcurl returns "Failed to dial"
Symptom:
Failed to dial target host "localhost:6565": ...
Cause: The gRPC server has not started yet, crashed on startup, or is listening on a different port.
Fix:
- Check the service logs for
ERRORlines — look for Spring startup failures. - Confirm port 6565 is open:
ss -tlnp | grep 6565. - Rerun
grpcurl -plaintext localhost:6565 list— if it succeeds, the server is up.
Handler returns INTERNAL but no ERROR log appears
Symptom: grpcurl reports Code: Internal but no ERROR line appears in the logs.
Cause: The exception is caught by ExceptionServerInterceptor before it reaches the
structured logger, or the log level filters it out.
Fix:
- Set a breakpoint in
EntitlementService.grantEntitlementon thecatch (Exception e)block to inspect the exception before it propagates. - Temporarily lower the log level: add
logging.level.net.accelbyte=DEBUGtoapplication-local.ymland restart.
Proto changes have no effect
Symptom: You edited a file in src/main/proto/ but the change has no effect at runtime.
Cause: The generated Java stubs have not been regenerated.
Fix: Run a clean build to trigger proto compilation:
./gradlew clean build
Then restart the service.
AI assistance
The app template ships with a Claude agent skill at
.claude/skills/debugging-guide/SKILL.md.
Copy the full skill file into your own repository and activate it in your AI assistant.
For the full skill content and prompting tips, see the AI assistance section in the main guide.