Spring Cloud Stream与Cassandra集成测试出现空指针异常问题
你碰到的这个空指针问题,本质是测试用例执行的时序不匹配:cassandra-unit-spring默认会在每个测试用例运行前,删除并重建整个testkeyspace以及里面的UDT和表,但第二个测试用例的事件触发(stubTrigger.trigger("product_bought"))跑得太快,在Cassandra的client和product这两个UDT还没完全创建完成时,Spring Data Cassandra就尝试去解析这些类型,导致SimpleUserTypeResolver找不到对应类型,抛出NPE。你加Thread.sleep(1000)能解决,就是因为给了Cassandra足够的时间完成元数据创建。
我给你几个更可靠的解决思路,替代不稳定的sleep:
方案1:主动等待UDT创建完成(最可靠)
我们可以在每个测试用例执行前,主动检查Cassandra的元数据,确认需要的UDT已经存在,再继续执行测试。这样比固定sleep更灵活,也不会浪费不必要的时间。
在你的Spock测试类里添加以下代码:
@Autowired private CassandraAdminTemplate cassandraAdminTemplate def setup() { // 等待client和product两个UDT都创建完成 waitForUdtReady("client") waitForUdtReady("product") } private void waitForUdtReady(String udtName) { long timeoutMillis = 10000 // 最多等10秒,防止无限等待 long checkInterval = 500 // 每500毫秒检查一次 long startTime = System.currentTimeMillis() boolean udtExists = false while (!udtExists && (System.currentTimeMillis() - startTime) < timeoutMillis) { try { // 查询system_schema确认UDT存在 cassandraAdminTemplate.getCqlOperations() .execute("SELECT 1 FROM system_schema.types WHERE keyspace_name = 'testkeyspace' AND type_name = '$udtName'") udtExists = true } catch (Exception ignored) { // 忽略查询异常,继续等待 sleep(checkInterval) } } if (!udtExists) { throw new IllegalStateException("UDT '$udtName' 未能在超时时间内创建完成") } }
这个方法会在每个测试前确保UDT已经就绪,从根源上避免时序问题。
方案2:让Cassandra只初始化一次(最简便)
默认cassandra-unit会在每个测试用例前重建keyspace和数据集,我们可以改成整个测试类只初始化一次Cassandra,避免重复重建的开销和时序问题。
修改测试类的注解配置:
// 替换原来的CassandraUnitDependencyInjectionTestExecutionListener @TestExecutionListeners(listeners = [ CassandraUnitTestExecutionListener.class, DependencyInjectionTestExecutionListener.class ]) // 使用CREATE_ONLY策略,只在测试类启动时加载一次数据集 @CassandraDataSet(keyspace = "testkeyspace", value = "dataset.cql", loadStrategy = LoadStrategy.CREATE_ONLY) @EmbeddedCassandra(timeout = 60000L)
注意:这种方式下,测试用例之间会共享Cassandra数据,所以你需要在每个测试用例执行后清理数据(比如删除所有事件),或者给每个测试用例使用唯一的客户端ID,避免数据干扰。
方案3:手动刷新Spring Data Cassandra的元数据
Spring Data Cassandra会缓存UDT的元数据,当Cassandra重建keyspace后,缓存的元数据就失效了。我们可以在每个测试前手动刷新元数据:
@Autowired private CassandraMappingContext mappingContext def setup() { // 强制刷新UDT解析器的元数据 def userTypeResolver = (SimpleUserTypeResolver) mappingContext.getUserTypeResolver() userTypeResolver.refresh() // 或者重新加载整个映射上下文的元数据 mappingContext.afterPropertiesSet() }
这个方法会让Spring Data Cassandra重新从Cassandra获取最新的元数据,确保能识别到刚创建的UDT。
选择其中一种方案后,重新运行测试用例,第二个测试的空指针异常应该就会消失了。如果追求可靠性,优先选方案1;如果想减少测试执行时间,方案2是不错的选择。
内容的提问来源于stack exchange,提问作者Tarek

