Skip to content

Skip gRPC port initializer when spring-grpc is absent - #51174

Open
r0h1tb wants to merge 1 commit into
spring-projects:mainfrom
r0h1tb:fix/grpc-port-initializer-guard
Open

Skip gRPC port initializer when spring-grpc is absent#51174
r0h1tb wants to merge 1 commit into
spring-projects:mainfrom
r0h1tb:fix/grpc-port-initializer-guard

Conversation

@r0h1tb

@r0h1tb r0h1tb commented Aug 1, 2026

Copy link
Copy Markdown

See gh-50825

Problem

GrpcPortInfoApplicationContextInitializer is registered unconditionally via META-INF/spring.factories, and its initialize() unconditionally adds a listener declared as ApplicationListener<GrpcServerStartedEvent>.

With spring-boot-grpc-test on the classpath but spring-grpc absent, Spring resolves that listener's generic interface to decide whether to deliver an event, and Class.getGenericInterfaces() throws — which is the stack trace in the report:

java.lang.TypeNotPresentException: Type org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent not present
	at java.base/java.lang.Class.getGenericInterfaces(Class.java:1278)
	at org.springframework.core.ResolvableType.getInterfaces(ResolvableType.java:544)

Fix

Guard registration on the event type being present:

private static final String GRPC_SERVER_STARTED_EVENT =
    "org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent";

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
    if (!ClassUtils.isPresent(GRPC_SERVER_STARTED_EVENT, applicationContext.getClassLoader())) {
        return;
    }
    applicationContext.addApplicationListener(new Listener(applicationContext));
}

ClassUtils.isPresent rather than a try/catch because that's what LogbackLoggingSystem.isBridgeHandlerAvailable() already does for the SLF4J bridge handler — same situation, optional class reached through a factories-registered component.

The classloader comes from the context rather than the class, so the guard sees the same classpath the listener resolution will.

Scope

Deliberately narrow: only the initializer is guarded. I did not touch the spring.factories registration itself, since the initializer being constructed is harmless — it's only the listener whose generic type can't resolve. Happy to take a different approach if you'd rather condition the registration.

Tests

Two added to GrpcPortInfoApplicationContextInitializerTests, using the existing FilteredClassLoader:

  • whenGrpcServerStartedEventIsNotOnTheClasspathRegistersNoListener — hides GrpcServerStartedEvent, asserts no listener is registered
  • whenGrpcServerStartedEventIsOnTheClasspathRegistersListener — control, so the guard can't silently disable the feature

With the source change stashed but both tests kept, the first fails:

GrpcPortInfoApplicationContextInitializerTests > whenGrpcServerStartedEventIsNotOnTheClasspathRegistersNoListener() FAILED

java.lang.AssertionError:
Expected size: 0 but was: 1 in:
[org.springframework.boot.grpc.test.autoconfigure.GrpcPortInfoApplicationContextInitializer$Listener@2407a36c]

Verification

./gradlew :module:spring-boot-grpc-test:test

tests failures errors
baseline (main, unmodified) 16 0 0
this branch 18 0 0
tests kept, source change stashed 18 1 0

The third row is what shows the tests pin the reported bug rather than restating the fix.

I ran the affected module's suite rather than the full build. Happy to run anything wider you'd like to see.

GrpcPortInfoApplicationContextInitializer is registered unconditionally via
META-INF/spring.factories, and its initialize() unconditionally adds a listener
declared as ApplicationListener<GrpcServerStartedEvent>.

When spring-boot-grpc-test is on the classpath but spring-grpc is not, Spring
resolves that listener's generic interface to decide whether to deliver an
event, and Class.getGenericInterfaces() throws:

  java.lang.TypeNotPresentException: Type
  org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent not present
    at java.base/java.lang.Class.getGenericInterfaces(Class.java:1278)
    at org.springframework.core.ResolvableType.getInterfaces(ResolvableType.java:544)

Guard registration on the event type being present, using ClassUtils.isPresent
as LogbackLoggingSystem already does for the SLF4J bridge handler.

See spring-projectsgh-50825
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants