Tomcat/SQL Server/Spring - 当 setStructured 参数由 JNDI 配置引起时,SQLServerCallableStatement 抛出异常

Tomcat/SQL Server/Spring - Cast Exception with SQLServerCallableStatement when setStructured parameter caused by JNDI configuration

技术堆栈:

我正在尝试在我的 Java 网络应用程序中实现一个数据库连接池,虽然我已经实现了,但我在执行 SQL 语句时检测到一些功能缺失。

我尝试了两种不同的方法都没有成功,希望你能提供任何线索:

1° - Tomcat JNDI 资源

虽然我的连接池可以工作,但在 callableStatement 中使用结构作为参数时,我失去了功能,从 Proxy$14 抛出强制转换异常到 SQLServerCallableStatement,常规数据源不会出现这个问题。我注意到

jdbcInterceptors="ConnectionState;StatementFinalizer;SlowQueryReport(threshold=1500);" - 发生转换异常并且 JNDI 池连接工作正常。

jdbcInterceptors="ConnectionState;StatementFinalizer;" - 没有转换异常,但结构参数被忽略,JNDI 池连接工作正常。

jdbcInterceptors=" - 结构参数工作正常但 JNDI 池连接 运行 可用连接不足。

这是我的配置:

Tomcat server.xml

<Resource name="jdbc/TomcatDS"
    global="jdbc/TomcatDS"
    factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
    auth="Container"
    type="javax.sql.DataSource"
    username="user"
    password="pass"
    driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver"
    description="SQLServer DB DS"
    url="jdbc:sqlserver://<host>:1433;DatabaseName=<DB>;schema=dbo;encrypt=true;trustServerCertificate=true;"
    maxActive="50"
    maxTotal="50"
    maxIdle="50"
    minIdle="10"
    maxWait="15000"
    reomoveAbandoned="true"
    removeAbandonedTimeout="3000"
    defaultAutoCommit="true" jdbcInterceptors="ConnectionState;StatementFinalizer;SlowQueryReport(threshold=1500);"/>

web.xml

<resource-ref>
    <description>DB Connection</description>
    <res-ref-name>jdbc/TomcatDS</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
</resource-ref>

context.xml

<Context path="/">
    <ResourceLink name="jdbc/TomcatDS" global="jdbc/TomcatDS" type="javax.sql.DataSource"/>
</Context>

spring-db-config.xml

<bean id="sqlServerDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/TomcatDS"/>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" abstract="true">
    <property name="dataSource">
        <ref bean="sqlServerDataSource" />
    </property>
</bean>

2°- Spring 数据源使用 Tomcat 的 DBCP jar

通过这种方式,我的连接池连接工作很奇怪,虽然没有发生 Cast Exceptions,但我还没有解决的是连接没有释放到池中,而是在我的应用程序 运行 断线。 (禁用 tomcat JNDI)

我的配置是:

spring-db-config.xml

<bean id="sqlServerPoolDataSource" class="org.apache.tomcat.jdbc.pool.DataSource" destroy-method="close">
        <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
        <property name="username" value="user"/>
        <property name="password" value="pass"/>
        <property name="url" value="jdbc:sqlserver://<host>:1433;DatabaseName=<DB>;schema=dbo;encrypt=true;trustServerCertificate=true;"/>
        <property name="maxActive" value="50"/>
        <property name="maxIdle" value="50"/>
        <property name="minIdle" value="10"/>
        <property name="maxWait" value="15000"/>
        <property name="removeAbandoned" value="true"/>
        <property name="removeAbandonedTimeout" value="3000"/>
        <property name="defaultAutoCommit" value="true"/>
        <property name="jdbcInterceptors" value="ConnectionState;StatementFinalizer;"/>     
    </bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" abstract="true">
    <property name="dataSource">
        <ref bean="sqlServerPoolDataSource" />
    </property>
</bean>

DAO.java

此代码在每个 DB 调用的 finally try 块中执行。

public void terminateDBCall(ResultSet rs, CallableStatement cstmt) {
            if (cstmt != null) {
                try {                
                    if (cstmt.getConnection() != null && !cstmt.getConnection().isClosed()){
                        cstmt.getConnection().close();                  
                    }
                    cstmt.close();
                    cstmt = null;
                } catch (SQLException ex) {
                    logHandler.registerSystemEvent(this.getClass(), Constants.SystemLogEvent.ERROR, "La session de base de datos no pudo ser cerrada", ex);
                }
            } 
            if (rs != null) {
                try {
                    rs.close();
                    rs = null;
                } catch (SQLException ex) {
                    logHandler.registerSystemEvent(this.getClass(), Constants.SystemLogEvent.ERROR, "El ResultSet no pudo ser cerrado", ex);
                }
            }               
        }

抛出强制转换异常的代码

@Override
    public boolean updateUser(User userVO) {
        boolean resultFlag = false;
        CallableStatement cstmt = null;
        ResultSet rs = null;
        try {
            SQLServerDataTable sourceDataTable = new SQLServerDataTable(); 
            sourceDataTable.addColumnMetadata("plantaId", java.sql.Types.INTEGER);
            for (FactoryValueObject factoryVO : userVO.getFactoryList()) {
                sourceDataTable.addRow(factoryVO.getFactoryId());
            }
            
            cstmt = getCallableStatement("UPDATE_USER");
            cstmt.setInt("UsuarioId", userVO.getUserId());
            cstmt.setNString("Usuario", userVO.getUserName());
            cstmt.setNString("Password", userVO.getUserPassword());
            cstmt.setInt("RolId", userVO.getRoleId());
            cstmt.setNString("Email", userVO.getEmail());
            cstmt.setInt("Activo", (userVO.isActiveFlag() ? 1 : 0));
            cstmt.setNString("ModificadoPor", userVO.getUpdatedBy());
=====>          ((SQLServerCallableStatement) cstmt).setStructured("ListaPlantaIds", "dbo.ID_PLANTAS_ASIGNADAS", sourceDataTable);
            cstmt.execute();
            rs = cstmt.getResultSet();
            rs.next();
            resultFlag = rs.getInt("resultFlag") == 1;
            if(!resultFlag)
                logDataBaseError(rs);
            logHandler.registerSystemEvent(this.getClass(), Constants.SystemLogEvent.DEBUG, "The user ID: " + userVO.getUserId() + " has been updated!");
        } catch (Exception ex) {
            logHandler.registerSystemEvent(this.getClass(), Constants.SystemLogEvent.ERROR, ex.getMessage(), ex);
        } finally {
            terminateDBCall(rs, cstmt);
        }   
        return resultFlag;
    }

java.lang.ClassCastException: com.sun.proxy.$Proxy14 cannot be cast to com.microsoft.sqlserver.jdbc.SQLServerCallableStatement

您从池中获取的Connection对象不是连接到数据库的原始连接对象。它不会是通过调用 DriverManager.getConnection 生成的对象,并且子对象没有类型 SQLServerCallableStatement。相反,您将获得一个 wrapper 对象,允许池正确管理它。

很明显,在池连接上调用 close 几乎会破坏池,因此您调用 close 的任何对象都需要代替 return 到池的连接,不关闭底层连接。

为了获得对 "real" 连接对象的引用,您需要像这样打开连接:

if(cstmt.isWrapperFor(SQLServerCallableStatement.class)) {
    SQLServerCallableStatement raw = cstmt.unwrap(SQLServerCallableStatement.class);
    raw.setStructured("ListaPlantaIds", "dbo.ID_PLANTAS_ASIGNADAS", sourceDataTable);
} else {
    // Do whatever you need to ; maybe throw an exception?
}

我想知道您是否需要在这里获取对原始类型的引用。不能使用现有的 JDBC 调用来处理 "structured" 数据吗?我相信 java.sql.Ref 类型是您以与供应商无关的方式执行此操作的方式。使用 "plain" JDBC 将比使用底层驱动程序生成的数据类型更干净、更不脆弱。

在 JDBC 1.6 之前,程序员在尝试创建新的 Clob 对象时通常不得不这样做,并且会求助于向下转换他们的连接以获取供应商提供的方法来创建新的Clob 对象(而不是简单地实现一个 Clob 接口,这一直是可能的)。这些天,你不应该做这种事情,因为 API 应该允许你做任何事情而不用向下转换。