mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-17 21:06:24 +00:00
- VERSION 5.09.14
``` feat: Implement hot-swap for DB config reload and JSON POST support **Cambios Principales:** 1. **Hot-Swap para recarga de configuraciones de DB sin reiniciar servidor** 2. **Migración a ReentrantLock para sincronización por incompatibilidad con Sync** 3. **Soporte para peticiones POST con Content-Type: application/json** 4. **Mejoras en inicialización del pool de conexiones y soporte multi-DB** **Problemas Resueltos:** - Falta de "Hot-Swap" en `reload`: El comando no permitía recarga dinámica de configuraciones sin reinicio - Ausencia de mecanismo de cierre de pools en RDCConnector para liberación ordenada de conexiones - Incompatibilidad con `Sync` en entorno B4X - Procesamiento incorrecto de peticiones POST con Content-Type: application/json - Inicialización incorrecta de pools C3P0 con TotalConnections: 0 - Configuración inconsistente de parámetros críticos de C3P0 - jdbcUrl truncada/vacía en logs por shadowing de variables **Cambios Implementados:** **Manager.bas:** - Reemplazo completo de lógica para comando "reload" - Creación de nuevos conectores antes de reemplazar los antiguos - Sincronización con ReentrantLock para acceso thread-safe - Patrón seguro de bloqueo sin `Finally` usando bandera booleana - Cierre explícito de oldConnectors después del reemplazo - Validación de inicialización y control de errores robusto - Registro detallado en log HTML del proceso **RDCConnector.bas:** - Implementación de método `Public Sub Close()` para liberar pools C3P0 - Corrección de shadowing de variable `config` en LoadConfigMap - Reordenamiento de Initialize - Configuración completa de C3P0 antes de adquirir conexiones - Forzar reportes de errores con acquireRetryAttempts y breakAfterAcquireFailure - Activación forzada del pool con conexión temporal **Main.bas:** - Declaración de `MainConnectorsLock As JavaObject` (ReentrantLock) - Inicialización del lock en AppStart - Declaración separada de conectores (con1, con2, con3, con4) **DBHandlerJSON.bas:** - Detección de peticiones POST con Content-Type: application/json - Lectura de JSON desde InputStream en lugar de parámetro URL - Cierre explícito del InputStream para liberación de recursos - Corrección de nombres de variables para evitar conflictos - Mensajes de error mejorados para ambos métodos (legacy y nuevo) **Beneficios:** - Recarga en caliente de configuraciones DB sin interrupción de servicio - Mayor disponibilidad y mantenibilidad del servidor - Prevención de fugas de recursos con cierre ordenado de pools - Compatibilidad con estándares APIs web (POST application/json) - Inicialización robusta y confiable de pools de conexiones - Mejor reporting de errores y diagnóstico de problemas - Soporte multi-DB más estable y confiable ```
This commit is contained in:
@@ -12,14 +12,13 @@ Sub Class_Globals
|
||||
End Sub
|
||||
|
||||
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
|
||||
' En este caso, no se necesita ninguna inicialización específica.
|
||||
Public Sub Initialize
|
||||
|
||||
End Sub
|
||||
|
||||
' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp).
|
||||
' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp).
|
||||
Sub Handle(req As ServletRequest, resp As ServletResponse)
|
||||
Log("============== DB1JsonHandler ==============")
|
||||
|
||||
' --- Headers CORS (Cross-Origin Resource Sharing) ---
|
||||
' Estos encabezados son necesarios para permitir que un cliente web (ej. una página con JavaScript)
|
||||
' que se encuentra en un dominio diferente pueda hacer peticiones a este servidor.
|
||||
@@ -34,37 +33,54 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
|
||||
|
||||
' Establece "DB1" como el nombre de la base de datos por defecto.
|
||||
Dim DB As String = "DB1"
|
||||
' Obtiene el objeto conector para la base de datos por defecto desde el objeto Main.
|
||||
|
||||
' Obtiene el objeto conector para la base de datos por defecto o especificada.
|
||||
Connector = Main.Connectors.Get(DB)
|
||||
|
||||
' Declara una variable para la conexión SQL.
|
||||
Dim con As SQL
|
||||
|
||||
' Inicia un bloque Try...Catch para manejar posibles errores durante la ejecución.
|
||||
Try
|
||||
' Obtiene el valor del parámetro 'j' de la petición. Se espera que contenga una cadena JSON.
|
||||
Dim jsonString As String = req.GetParameter("j")
|
||||
' Verifica si el parámetro 'j' es nulo o está vacío.
|
||||
Dim jsonString As String
|
||||
|
||||
' *** INICIO DE LA LÓGICA CORREGIDA PARA LEER JSON DEL CUERPO (POST) O DE PARÁMETROS (GET/POST Form-urlencoded) ***
|
||||
If req.Method = "POST" And req.ContentType.Contains("application/json") Then
|
||||
' Para peticiones POST con Content-Type: application/json, el JSON viene en el cuerpo (InputStream).
|
||||
Dim Is0 As InputStream = req.InputStream ' ¡CORREGIDO: Usamos Is0 en lugar de Is para evitar conflicto con palabra reservada!
|
||||
' Usamos Bit.InputStreamToBytes de la librería jcore para leer el stream completo a un array de bytes.
|
||||
Dim bytes() As Byte = Bit.InputStreamToBytes(Is0)
|
||||
jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convertimos los bytes a String UTF8.
|
||||
Is0.Close ' ¡Es CRÍTICO cerrar el InputStream para liberar los recursos del sistema!
|
||||
Else
|
||||
' Para peticiones GET o POST con Content-Type como 'application/x-www-form-urlencoded',
|
||||
' el JSON se espera en el parámetro 'j' de la URL.
|
||||
jsonString = req.GetParameter("j")
|
||||
End If
|
||||
' *** FIN DE LA LÓGICA CORREGIDA PARA LEER JSON ***
|
||||
|
||||
' Verifica si la cadena JSON obtenida es nula o está vacía.
|
||||
If jsonString = Null Or jsonString = "" Then
|
||||
' Si falta el parámetro, envía una respuesta de error 400 (Bad Request) y termina la ejecución.
|
||||
SendErrorResponse(resp, 400, "Falta el parametro 'j' en el URL")
|
||||
' Si falta el JSON, envía una respuesta de error 400 (Bad Request) y termina la ejecución.
|
||||
SendErrorResponse(resp, 400, "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición.")
|
||||
Return
|
||||
End If
|
||||
|
||||
' Crea un objeto JSONParser para analizar la cadena JSON.
|
||||
Dim parser As JSONParser
|
||||
parser.Initialize(jsonString)
|
||||
|
||||
' Convierte la cadena JSON en un objeto Map, que es como un diccionario (clave-valor).
|
||||
Dim RootMap As Map = parser.NextObject
|
||||
|
||||
' Extrae los datos necesarios del JSON.
|
||||
Dim execType As String = RootMap.GetDefault("exec", "") ' Tipo de ejecución: "executeQuery" o "executeCommand".
|
||||
Dim queryName As String = RootMap.Get("query") ' Nombre del comando SQL (definido en config.properties).
|
||||
|
||||
' Se obtiene "params" como una Lista en lugar de un Mapa.
|
||||
Dim execType As String = RootMap.GetDefault("exec", "") ' Tipo de ejecución: "executeQuery" (SELECT) o "executeCommand" (INSERT/UPDATE/DELETE).
|
||||
Dim queryName As String = RootMap.Get("query") ' Nombre del comando SQL (definido en config.properties).
|
||||
|
||||
' Se obtiene "params" como una Lista en lugar de un Mapa, para soportar el ordenamiento de B4A.
|
||||
Dim paramsList As List = RootMap.Get("params")
|
||||
|
||||
' Si la lista de parámetros es nula (no se proporcionó en el JSON),
|
||||
' la inicializamos como una lista vacía para evitar errores más adelante.
|
||||
' Si la lista de parámetros es nula (no se proporcionó en el JSON), la inicializamos como una lista vacía.
|
||||
If paramsList = Null Or paramsList.IsInitialized = False Then
|
||||
paramsList.Initialize
|
||||
End If
|
||||
@@ -74,101 +90,66 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
|
||||
|
||||
' Valida que el nombre de la base de datos (DB) exista en la lista de conexiones configuradas en Main.
|
||||
If Main.listaDeCP.IndexOf(DB) = -1 Then
|
||||
SendErrorResponse(resp, 400, "Parametro 'DB' invalido. El nombre '" & DB & "' no es válido.")
|
||||
' Se añade Return para detener la ejecución si la BD no es válida.
|
||||
SendErrorResponse(resp, 400, "Parámetro 'DB' inválido. El nombre '" & DB & "' no es válido.")
|
||||
Return
|
||||
End If
|
||||
|
||||
' Obtiene una conexión a la base de datos del pool de conexiones.
|
||||
' Obtiene una conexión a la base de datos del pool de conexiones para la DB seleccionada.
|
||||
con = Connector.GetConnection(DB)
|
||||
|
||||
' Obtiene la cadena SQL del archivo de configuración usando el nombre de la consulta (queryName).
|
||||
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
|
||||
|
||||
' <<< INICIO VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE >>>
|
||||
' Comprueba si el comando SQL (query) especificado en el JSON fue encontrado en el archivo de configuración.
|
||||
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
|
||||
' Si no se encontró el comando, crea un mensaje de error claro.
|
||||
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
|
||||
' Registra el error en el log del servidor para depuración.
|
||||
Log(errorMessage)
|
||||
' Envía una respuesta de error 400 (Bad Request) al cliente en formato JSON.
|
||||
SendErrorResponse(resp, 400, errorMessage)
|
||||
' Cierra la conexión a la base de datos antes de salir para evitar fugas de conexión.
|
||||
If con <> Null And con.IsInitialized Then con.Close
|
||||
' Detiene la ejecución del método Handle para esta petición.
|
||||
Return
|
||||
End If
|
||||
' <<< FIN VALIDACIÓN >>>
|
||||
|
||||
' Comprueba el tipo de ejecución solicitado ("executeQuery" o "executeCommand").
|
||||
If execType.ToLowerCase = "executequery" Then
|
||||
' Declara una variable para almacenar el resultado de la consulta.
|
||||
Dim rs As ResultSet
|
||||
|
||||
' Si el comando SQL contiene placeholders ('?'), significa que espera parámetros.
|
||||
' Se usa 'paramsList' directamente en lugar de 'orderedParams'.
|
||||
If sqlCommand.Contains("?") Or paramsList.Size > 0 Then
|
||||
' =================================================================
|
||||
' === VALIDACIÓN DE CONTEO DE PARÁMETROS ==========================
|
||||
' =================================================================
|
||||
' Calcula cuántos parámetros espera la consulta contando el número de '?'.
|
||||
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
|
||||
' Obtiene cuántos parámetros se recibieron de la lista.
|
||||
Dim receivedParams As Int = paramsList.Size
|
||||
|
||||
Log($"expectedParams: ${expectedParams}, receivedParams: ${receivedParams}"$)
|
||||
|
||||
If expectedParams <> receivedParams Then
|
||||
' Si no coinciden, envía un error 400 detallado.
|
||||
SendErrorResponse(resp, 400, $"Número de parametros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
|
||||
' Cierra la conexión antes de salir para evitar fugas.
|
||||
SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
|
||||
If con <> Null And con.IsInitialized Then con.Close
|
||||
' Detiene la ejecución para evitar un error en la base de datos.
|
||||
Return
|
||||
End If
|
||||
' =================================================================
|
||||
' Ejecuta la consulta pasando el comando SQL y la lista de parámetros.
|
||||
rs = con.ExecQuery2(sqlCommand, paramsList)
|
||||
Else
|
||||
' Si no hay '?', ejecuta la consulta directamente sin parámetros.
|
||||
rs = con.ExecQuery(sqlCommand)
|
||||
End If
|
||||
|
||||
' --- Procesamiento de resultados ---
|
||||
' Prepara una lista para almacenar todas las filas del resultado.
|
||||
Dim ResultList As List
|
||||
ResultList.Initialize
|
||||
' Usa un objeto JavaObject para acceder a los metadatos del resultado (info de columnas).
|
||||
Dim jrs As JavaObject = rs
|
||||
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
|
||||
' Obtiene el número de columnas en el resultado.
|
||||
Dim cols As Int = rsmd.RunMethod("getColumnCount", Null)
|
||||
|
||||
' Itera sobre cada fila del resultado (ResultSet).
|
||||
Do While rs.NextRow
|
||||
' Crea un mapa para almacenar los datos de la fila actual (columna -> valor).
|
||||
Dim RowMap As Map
|
||||
RowMap.Initialize
|
||||
' Itera sobre cada columna de la fila.
|
||||
For i = 1 To cols
|
||||
' Obtiene el nombre de la columna.
|
||||
Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i))
|
||||
' Obtiene el valor de la columna.
|
||||
Dim value As Object = jrs.RunMethod("getObject", Array(i))
|
||||
' Añade la pareja (nombre_columna, valor) al mapa de la fila.
|
||||
RowMap.Put(ColumnName, value)
|
||||
Next
|
||||
' Añade el mapa de la fila a la lista de resultados.
|
||||
ResultList.Add(RowMap)
|
||||
Loop
|
||||
' Cierra el ResultSet para liberar recursos de la base de datos.
|
||||
rs.Close
|
||||
|
||||
' Envía una respuesta de éxito con la lista de resultados en formato JSON.
|
||||
SendSuccessResponse(resp, CreateMap("result": ResultList))
|
||||
|
||||
Else If execType.ToLowerCase = "executecommand" Then
|
||||
' Si es un comando (INSERT, UPDATE, DELETE), también valida los parámetros.
|
||||
If sqlCommand.Contains("?") Then
|
||||
' =================================================================
|
||||
' === VALIDACIÓN DE CONTEO DE PARÁMETROS (para Comandos) ==========
|
||||
@@ -176,38 +157,23 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
|
||||
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
|
||||
Dim receivedParams As Int = paramsList.Size
|
||||
If expectedParams <> receivedParams Then
|
||||
SendErrorResponse(resp, 400, $"Número de parametros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
|
||||
' Cierra la conexión antes de salir.
|
||||
SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
|
||||
If con <> Null And con.IsInitialized Then con.Close
|
||||
' Detiene la ejecución.
|
||||
Return
|
||||
End If
|
||||
' =================================================================
|
||||
End If
|
||||
|
||||
' Ejecuta el comando que no devuelve resultados (NonQuery) con sus parámetros.
|
||||
con.ExecNonQuery2(sqlCommand, paramsList)
|
||||
' Envía una respuesta de éxito con un mensaje de confirmación.
|
||||
SendSuccessResponse(resp, CreateMap("message": "Command executed successfully"))
|
||||
|
||||
Else
|
||||
' Si el valor de 'exec' no es ni "executeQuery" ni "executeCommand", envía un error.
|
||||
SendErrorResponse(resp, 400, "Parametro 'exec' inválido. '" & execType & "' no es un valor permitido.")
|
||||
SendErrorResponse(resp, 400, "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido.")
|
||||
End If
|
||||
|
||||
Catch
|
||||
' Si ocurre cualquier error inesperado en el bloque Try...
|
||||
' Registra la excepción completa en el log del servidor para diagnóstico.
|
||||
Log(LastException)
|
||||
' Envía una respuesta de error 500 (Internal Server Error) con el mensaje de la excepción.
|
||||
SendErrorResponse(resp, 500, LastException.Message)
|
||||
End Try
|
||||
|
||||
' Este bloque se ejecuta siempre al final, haya habido error o no, *excepto si se usó Return antes*.
|
||||
' Comprueba si el objeto de conexión fue inicializado y sigue abierto.
|
||||
|
||||
If con <> Null And con.IsInitialized Then
|
||||
' Cierra la conexión para devolverla al pool y que pueda ser reutilizada.
|
||||
' Esto es fundamental para no agotar las conexiones a la base de datos.
|
||||
con.Close
|
||||
End If
|
||||
End Sub
|
||||
|
||||
Reference in New Issue
Block a user