You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的场景下性能损耗过大,无法落地。

待解答问题

  1. 为何触发服务端预编译后会出现参数元数据解析变更的情况?
  2. 该元数据解析问题是否仅在NUMERIC、VARCHAR这类标量类型参数上出现?
  3. 是否存在可行方案,仅针对该存储过程调用关闭服务端预编译,不影响其他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落地方案

按优先级从高到低可选:

  1. 升级驱动版本(最优方案)
    该bug在42.3.8、42.4.2及以上版本的JDBC驱动中已经修复,升级后不需要修改任何业务代码,也不需要调整预编译配置,全局SQL的性能不受影响,优先选择该方案。
  2. 语句级关闭预编译(无升级条件时使用)
    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仍然使用默认的预编译阈值逻辑,没有全局性能损耗。
  3. 调用语法规避(备选方案)
    若既无法升级驱动,也不方便使用驱动原生类做语句级配置,可以将存储过程调用语法从{Call PERSIST(...)}改为SELECT * FROM PERSIST(...)的形式,驱动在处理SELECT形式的函数调用时,会通过结果集元数据解析返回值类型,不会提前锁定OUT参数类型,也能规避该bug。

内容的提问来源于stack exchange,提问作者Facundor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 00:21:39