Synopsis

I was experimenting with the new Kubernetes probes support in Spring Actuator. The official documentation suggests that we can listen to changes in readiness or liveness by registering a bean like the following:

@Component
public class ReadinessStateExporter {

    @EventListener
    public void onStateChange(AvailabilityChangeEvent<ReadinessState> event) {
        switch (event.getState()) {
        case ACCEPTING_TRAFFIC:
            // create file /tmp/healthy
        break;
        case REFUSING_TRAFFIC:
            // remove file /tmp/healthy
        break;
        }
    }
}

However, running an application with a bean like this fails with:

java.lang.ClassCastException: org.springframework.boot.availability.LivenessState cannot be cast to org.springframework.boot.availability.ReadinessState

Details

This failure makes sense because during the application startup, Spring Boot fires both AvailabilityChangeEvent<ReadinessState> and AvailabilityChangeEvent<LivenessState> events. Due to erasure, both of those events would e handled by the ReadinessStateExporter. Quite reasonably, the LivenessState change event should fail because we can't cast LivenessState to ReadinessState. Hence the error:

java.lang.ClassCastException: org.springframework.boot.availability.LivenessState cannot be cast to org.springframework.boot.availability.ReadinessState
    at ReadinessChangedListener.onStateChange(ReadinessChangedListener.java:13) ~[classes/:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
    at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:305) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.context.event.ApplicationListenerMethodAdapter.processEvent(ApplicationListenerMethodAdapter.java:190) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.context.event.ApplicationListenerMethodAdapter.onApplicationEvent(ApplicationListenerMethodAdapter.java:153) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:403) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.boot.availability.AvailabilityChangeEvent.publish(AvailabilityChangeEvent.java:81) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
    at org.springframework.boot.availability.AvailabilityChangeEvent.publish(AvailabilityChangeEvent.java:67) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
    at org.springframework.boot.context.event.EventPublishingRunListener.started(EventPublishingRunListener.java:103) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
    at org.springframework.boot.SpringApplicationRunListeners.started(SpringApplicationRunListeners.java:71) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:321) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
    at DemoApplication.main(DemoApplication.java:10) [classes/:na]

I guess we probably should change the documentation as the sample code makes the Spring App to fail at startup. Also, we could listen to AvailabilityState:

@Component
public class ReadinessChangedListener {

    @EventListener
    public void onStateChange(AvailabilityChangeEvent<AvailabilityState> event) {
        // check if it's liveness or readiness or anything else
    }

}

Please kindly let me know what you think of this. Cheers!

Comment From: philwebb

I was under the impression that ApplicationListenerMethodAdapter would deal with generics correctly. Do you by any chance have a sample application that reproduces the problem?

Comment From: alimate

Sure. This a simple sample application https://github.com/alimate/boot-probes

Comment From: philwebb

Thanks for the detailed report and the sample app. I've tracked this down and things should work as expected with the next release. In the meantime, I'm afraid you'll have to use your workaround.

Comment From: lifejwang11

Sure. This a simple sample application https://github.com/alimate/boot-probes

hello,i see this problem to understand the source code,try this without changing the source code,I hope that will be helpful.

     @EventListener
    public void onStateChange(AvailabilityChangeEvent<AvailabilityState> event) {
        if (event.getState() instanceof LivenessState){
            switch ((LivenessState)event.getState()){
                case  CORRECT:
                    System.out.println("CORRECT");
                    break;
                case BROKEN:
                    System.out.println("BROKEN");
                    break;
                default:

            }
        }else if(event.getState() instanceof ReadinessState){
            switch ((ReadinessState)event.getState()){
                case  ACCEPTING_TRAFFIC:
                    System.out.println("ACCEPTING_TRAFFIC");
                    break;
                case REFUSING_TRAFFIC:
                    System.out.println("REFUSING_TRAFFIC");
                    break;
                default:

            }
        }
    }

Comment From: alimate

@lifejwang11 Definitely helpful. Thanks. Moreover, this bug apparently is going to be fixed by 2.4.0 release.

Comment From: bclozel

@alimate the fix is scheduled for 2.3.2