PostgreSQL服务端预编译调用带OUT参数存储过程元数据解析异常
问题背景
问题环境
- 数据库:PostgreSQL 14.3
- 应用框架:Spring Boot 2.6.1
- JDBC驱动:org.postgresql:postgresql:42.3.3
问题现象
调用遗留系统存储过程时触发异常,该存储过程包含大量输入参数,同时定义6个标量类型OUT参数,调用代码结构如下:
try (CallableStatement cStm = connection.prepareCall("{Call PERSIST(?,?,...?)}")) { cStm.setString(1, (persistOrderer ? "1" : "0")); cStm.setString(2, (persistOrdererAditionals ? "1" : "0")); cStm.setString(3, "1"); cStm.setString(4, (persistBeneficiary ? "1" : "0")); .... cStm.registerOutParameter(84, java.sql.Types.NUMERIC); cStm.registerOutParameter(85, java.sql.Types.NUMERIC); cStm.registerOutParameter(86, java.sql.Types.NUMERIC); cStm.registerOutParameter(87, java.sql.Types.VARCHAR); cStm.registerOutParameter(88, java.sql.Types.VARCHAR); cStm.registerOutParameter(89, java.sql.Types.DATE); cStm.execute();
当调用次数未达到prepareThreshold阈值、未触发服务端预编译时,存储过程可正常执行;一旦触发服务端预编译,就会抛出如下异常:
java.lang.IllegalArgumentException: Can't change resolved type for param: 84 from 2278 to 1700 at org.postgresql.core.v3.SimpleParameterList.setResolvedType(SimpleParameterList.java:348) at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2151) at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:355) at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:490) at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:408) at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:166) at org.postgresql.jdbc.PgCallableStatement.executeWithFlags(PgCallableStatement.java:83) at org.postgresql.jdbc.PgPreparedStatement.execute(PgPreparedStatement.java:155) at com.zaxxer.hikari.pool.ProxyPreparedStatement.execute(ProxyPreparedStatement.java:44) at com.zaxxer.hikari.pool.HikariProxyCallableStatement.execute(HikariProxyCallableStatement.java) .... at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:170) at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:130) at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:660) at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:524) at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:474) at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364) at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:476) at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:434) at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:408) at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:69) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:492) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161) at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364) at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:164) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:247) at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:249) at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:60) at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:55) at javax.servlet.http.HttpServlet.service(HttpServlet.java:750) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1722) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.base/java.lang.Thread.run(Thread.java:829)
已排查信息
- 触发服务端预编译时,OUT参数在预拉取的元数据中被识别为JavaObject类型,但代码中注册的参数类型、存储过程实际定义的类型均为NUMERIC、VARCHAR、DATE。
- 异常抛出后连接池会废弃异常连接新建连接,后续几次调用可正常执行,直到再次达到预编译阈值后问题复现。
- 全局设置JDBC参数
prepareThreshold=0关闭服务端预编译可解决问题,但会导致应用内所有SQL无法使用服务端预编译,单事务包含数百条SQL的场景下性能损耗过大,无法落地。
待解答问题
- 为何触发服务端预编译后会出现参数元数据解析变更的情况?
- 该元数据解析问题是否仅在NUMERIC、VARCHAR这类标量类型参数上出现?
- 是否存在可行方案,仅针对该存储过程调用关闭服务端预编译,不影响其他SQL语句的执行性能?
解答
问题1原因
这是PostgreSQL JDBC 42.3.x版本的已知bug,和预编译的执行逻辑直接相关:
- 未达到
prepareThreshold时,驱动走简单查询协议,直接拼接参数发送给服务端,不会提前拉取存储过程的参数元数据,代码中registerOutParameter声明的类型会被直接使用,不存在类型校验冲突。 - 达到阈值触发服务端预编译后,驱动切换到扩展查询协议,会先向服务端请求存储过程的参数签名元数据。42.3.x版本的元数据解析逻辑存在缺陷,无法正确识别无返回值存储过程的OUT参数位置,会将所有OUT参数的类型提前解析为OID 2278(即void类型)并锁定参数类型。等语句实际执行完成,驱动拿到结果中OUT参数的实际类型OID(比如报错里的1700对应NUMERIC类型)时,已经不允许修改已锁定的参数类型,直接抛出参数类型不允许变更的异常。
问题2结论
该问题不是标量类型独有。
bug的核心是预编译阶段OUT参数被错误标记为void类型,和参数实际类型无关:只要是未被驱动特殊处理的OUT参数,不管是NUMERIC、VARCHAR、DATE这类标量,还是数组、自定义复合类型,只要预存的void类型和实际返回类型不一致就会触发异常。只是标量类型在存储过程中使用频率最高,踩坑案例更多。REFCURSOR类型的OUT参数因为驱动有单独的解析逻辑,一般不会触发该问题。
问题3落地方案
按优先级从高到低可选:
- 升级驱动版本(最优方案)
该bug在42.3.8、42.4.2及以上版本的JDBC驱动中已经修复,升级后不需要修改任何业务代码,也不需要调整预编译配置,全局SQL的性能不受影响,优先选择该方案。 - 语句级关闭预编译(无升级条件时使用)
PostgreSQL JDBC支持单语句级别的prepareThreshold配置,不需要修改全局参数,仅针对该存储过程调用关闭服务端预编译即可,代码示例如下:
该配置仅对当前try (CallableStatement cStm = connection.prepareCall("{Call PERSIST(?,?,...?)}")) { // 仅当前语句关闭服务端预编译,不影响其他SQL if (cStm.isWrapperFor(PgStatement.class)) { cStm.unwrap(PgStatement.class).setPrepareThreshold(0); } // 原有参数设置、OUT参数注册、执行逻辑保持不变 cStm.setString(1, (persistOrderer ? "1" : "0")); // ... 其余输入参数赋值 cStm.registerOutParameter(84, java.sql.Types.NUMERIC); // ... 其余OUT参数注册 cStm.execute(); }CallableStatement对象生效,其余SQL仍然使用默认的预编译阈值逻辑,没有全局性能损耗。 - 调用语法规避(备选方案)
若既无法升级驱动,也不方便使用驱动原生类做语句级配置,可以将存储过程调用语法从{Call PERSIST(...)}改为SELECT * FROM PERSIST(...)的形式,驱动在处理SELECT形式的函数调用时,会通过结果集元数据解析返回值类型,不会提前锁定OUT参数类型,也能规避该bug。
内容的提问来源于stack exchange,提问作者Facundor
相关产品推荐
相关产品推荐

