Quarkus热重载后InfluxDB客户端反射获取POJO字段失败
问题场景
在Quarkus服务中使用InfluxDB Java Client管理指标,自行开发了提供InfluxDB 2开发容器的扩展。首次运行测试完全正常,但触发热重载(保存任意文件)后,发送指标到数据库的测试失败。测试重新运行会触发异常,停止并重启测试后首次运行恢复正常,直到再次触发热重载;移除Metric中的duration字段,报错会转移到job字段。
环境:Java 21、Quarkus 3.15、InfluxDBClient 7.2.0
测试代码及Metric模型
package mypackage.models.measurements; import static mypackage.config.InfluxDBConstants.DefaultConstants.BUCKET; import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Duration; import java.time.Instant; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; import com.influxdb.annotations.Column; import com.influxdb.annotations.Measurement; import com.influxdb.client.InfluxDBClient; import com.influxdb.client.InfluxDBClientFactory; import com.influxdb.client.domain.WritePrecision; import myextension.quarkus.devservices.influxdb.runtime.InfluxDBConfig; import mypackage.config.InfluxDBConstants.JobMetricConstants.Fields; import mypackage.config.InfluxDBConstants.JobMetricConstants.Tags; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; /** * JobMetricTest */ @QuarkusTest @TestInstance(Lifecycle.PER_CLASS) public class JobMetricTest { @Inject InfluxDBConfig config; public InfluxDBClient getClient() { InfluxDBClient client = InfluxDBClientFactory.create( config.url(), config.token().toCharArray(), config.organization().get(), config.bucket().get()); assertTrue(client.ping()); return client; } @Test public void testInitialization() throws InterruptedException { Instant testTime = Instant.now(); JobMetric metric = new JobMetric("testJob"); assertTrue(metric.duration == -1); TimeUnit.MILLISECONDS.sleep(1); metric.finish(); assertTrue(metric.duration > 0); assertTrue(metric.job.equals("testJob")); assertTrue(metric.start.getEpochSecond() >= testTime.getEpochSecond()); } @Test public void testInfluxReflection() throws InterruptedException { InfluxDBClient client = getClient(); Metric metric = new Metric(); metric.job = "job"; metric.hasError = false; metric.duration = Double.valueOf(-1); metric.start = Instant.now(); Instant stop = Instant.now(); Duration d = Duration.between(metric.start, stop); metric.duration = (double) d.getSeconds(); metric.duration += ((double) d.toMillisPart()) / 1000; client.getWriteApiBlocking() .writeMeasurement( // Line 72: Fails here! BUCKET, config.organization().get(), WritePrecision.NS, metric ); } } @Measurement(name = "metric") class Metric { @Column(timestamp = true) public Instant start; @Column(tag = true, name = Tags.JOB) public String job; @Column(name = Fields.HAS_ERROR, tag = false) public Boolean hasError; @Column(name = Fields.DURATION, tag = false) public Double duration; }
异常信息
2024-11-17 21:42:17,616 ERROR [io.qua.test] (Test runner thread) ==================== TEST REPORT #2 ==================== [Error Occurred After Shutdown] 2024-11-17 21:42:17,616 ERROR [io.qua.test] (Test runner thread) Test JobMetricTest#testInfluxReflextion() failed [Error Occurred After Shutdown]: java.lang.IllegalArgumentException: Can not get java.lang.Double field mypackage.models.measurements.Metric.duration on mypackage.models.measurements.Metric at java.base/jdk.internal.reflect.MethodHandleFieldAccessorImpl.newGetIllegalArgumentException(MethodHandleFieldAccessorImpl.java:86) at java.base/jdk.internal.reflect.MethodHandleObjectFieldAccessorImpl.get(MethodHandleObjectFieldAccessorImpl.java:61) at java.base/java.lang.reflect.Field.get(Field.java:444) at com.influxdb.client.internal.MeasurementMapper.getObject(MeasurementMapper.java:130) at com.influxdb.client.internal.MeasurementMapper.toPoint(MeasurementMapper.java:68) at com.influxdb.client.internal.AbstractWriteClient$BatchWriteDataMeasurement.toLineProtocol(AbstractWriteClient.java:386) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:212) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:212) at java.base/java.util.Collections$2.tryAdvance(Collections.java:5074) at java.base/java.util.Collections$2.forEachRemaining(Collections.java:5082) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:556) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:546) at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:702) at com.influxdb.client.internal.AbstractWriteBlockingClient.write(AbstractWriteClient.java:69) at com.influxdb.client.internal.WriteApiBlockingImpl.writeMeasurements(WriteApiBlockingImpl.java:253) at com.influxdb.client.internal.WriteApiBlockingImpl.writeMeasurement(WriteApiBlockingImpl.java:220) at com.influxdb.client.internal.WriteApiBlockingImpl.writeMeasurement(WriteApiBlockingImpl.java:207) at mypackage.models.measurements.JobMetricTest.testInfluxReflextion(JobMetricTest.java:72)
解决方案
这是Quarkus热重载的类加载器隔离机制导致的问题:热重载后,新修改的Metric类由新的类加载器加载,而之前手动创建的InfluxDBClient实例绑定的是旧类加载器的元数据,反射时无法识别新类的字段。
修复步骤
将InfluxDBClient注册为CDI Bean
不要在测试中每次新建客户端,而是在扩展中把InfluxDBClient注册为Quarkus管理的CDI Bean,确保它和应用类使用同一个类加载器:// 在你的InfluxDB扩展的Producer类中添加 @Produces @ApplicationScoped public InfluxDBClient produceInfluxDBClient(InfluxDBConfig config) { return InfluxDBClientFactory.create( config.url(), config.token().toCharArray(), config.organization().get(), config.bucket().get()); }修改测试类,直接注入InfluxDBClient
移除测试中的getClient()方法,直接注入客户端实例:@QuarkusTest @TestInstance(Lifecycle.PER_CLASS) public class JobMetricTest { @Inject InfluxDBConfig config; @Inject InfluxDBClient client; // 直接注入管理好的客户端 @Test public void testInitialization() throws InterruptedException { // 原代码保持不变 } @Test public void testInfluxReflection() throws InterruptedException { Metric metric = new Metric(); metric.job = "job"; metric.hasError = false; metric.duration = Double.valueOf(-1); metric.start = Instant.now(); Instant stop = Instant.now(); Duration d = Duration.between(metric.start, stop); metric.duration = (double) d.getSeconds(); metric.duration += ((double) d.toMillisPart()) / 1000; client.getWriteApiBlocking() .writeMeasurement( BUCKET, config.organization().get(), WritePrecision.NS, metric ); } }可选:为Metric类添加反射注册注解
如果热重载后Metric类的反射元数据仍有问题,添加@RegisterForReflection注解告诉Quarkus保留该类的反射信息:@Measurement(name = "metric") @RegisterForReflection class Metric { // 原字段保持不变 }
原理
Quarkus热重载会创建新的类加载器加载更新后的类,将InfluxDBClient转为CDI Bean后,它会被Quarkus的类加载器管理,热重载时会重新绑定到新的类元数据,反射访问字段时就能匹配新加载的Metric类。
内容的提问来源于stack exchange,提问作者JWo

