DAM · Programación de servicios y procesos

FILS Bank Process

Resumen de todo lo que hemos hecho en la práctica: varios procesos escribiendo en el mismo extracto con FileLock, y 12 tuberías de procesos filtrando extractos en paralelo con ProcessBuilder.

El punto de partida

La plantilla del profe: dos programas en uno

El proyecto tiene dos paquetes que no se llaman entre sí. Cada uno es un ejercicio distinto sobre procesos. Lo único que comparten es el formato de las líneas del extracto.

bank/ ← proyecto de Eclipse (se ejecuta desde aquí) ├── src/ │ ├── bank/ │ │ ├── AccountMonitor.java ● la hemos hecho: escribe con FileLock │ │ ├── BankProcess.java ● la hemos hecho: el main con el menú │ │ └── ProcessInputUtils.java ✓ venía hecha: lee del teclado │ └── bank/filter/ │ ├── BankFilter.java ● la hemos hecho: monta las 12 tuberías │ └── StatementFilter.java ✓ venía hecha: el filtro tipo grep ├── bin/ ← los .class compilados (Eclipse la oculta) └── bank/ └── statement-01..12.txt ← extractos de cada mes

Cada línea del extracto tiene 4 columnas separadas por |. StatementFilter depende de ese orden: 0 fecha, 1 operación, 2 concepto, 3 importe.

2025-01-04 11:32:15 | WITHDRAW  | SUPERMERCAT          |    -120.00 €
Parte 1 · paquete bank

Varios procesos, un solo fichero

Cada entidad (el titular, la empresa que paga la nómina…) ejecuta su propio BankProcess. Si lanzas el programa 3 veces, tienes 3 procesos independientes, cada uno con su memoria y su consola. Lo único que comparten es statement.txt en el disco, y por eso hay que coordinar quién escribe.

statement.txt
Un lavabo. Solo cabe una persona.
BankProcess
Cada persona que quiere entrar. Lanzar el programa otra vez es otra persona.
FileLock
La única llave. Quien la tiene entra; los demás esperan en la puerta.
AccountMonitor
El que gestiona la llave. Es el único sitio del código que toca el fichero.

Quién hace qué

BankProcess

Habla con el usuario

Bucle: menú → concepto → importe → «monitor, apunta esto» → ¿otra? No sabe nada de bloqueos.

AccountMonitor

Escribe con la llave

Coge el lock, añade la línea al final del fichero y lo suelta al salir del try.

ProcessInputUtils

Lee el teclado

Menú, concepto, importe y confirmaciones s/n. Un único Scanner compartido. No se toca.

Proceso ABank Office 4117 DEPOSIT · NÒMINA · 1500 terminado ✓
statement.txt🔓 libre
Proceso BBank Office 4122 WITHDRAW · COMPRES · 60 terminado ✓

Con lock: B espera en lock() hasta que A termina. Las dos líneas quedan enteras.

El método bueno: AccountMonitor.writeOperation

AccountMonitor.javala línea marcada es la única diferencia con la versión mala
public void writeOperation(Operation operation, String concept, double amount) {
    String fecha     = LocalDateTime.now().format(FORMATTER);
    String operacion = operation.toString();
    String signo     = operation.sign();
    String importe   = String.format(Locale.ROOT, "%.2f", amount);

    String linea = fecha + " | " + operacion + " | " + concept + " | " + signo + importe + " €\n";

    try (FileOutputStream fos = new FileOutputStream(fileName, true);   // true = añadir al final
         FileLock lock = fos.getChannel().lock()) {               // 🔒 si otro lo tiene, espera aquí
        fos.write(linea.getBytes(StandardCharsets.UTF_8));
    } catch (IOException e) {
        System.err.println("Error: " + e.getMessage());
    }
    // 🔓 al salir del try se libera el lock solo
}
FileOutputStream(f, true)

Abre el fichero para escribir. El true es el modo append: cada línea se añade al final en vez de borrar lo que había.

getChannel()

Da el canal del fichero. Es el objeto que sabe pedir bloqueos al sistema operativo.

lock()

Pide el bloqueo exclusivo. Si otro proceso lo tiene, este se queda parado en esa línea hasta que lo suelten. Funciona entre procesos distintos, que es justo lo que pide el enunciado.

try-with-resources

Cierra todo al salir, en orden inverso: primero libera el lock y luego cierra el fichero. Así se suelta aunque haya una excepción.

UTF-8 y Locale.ROOT

UTF-8 para que salgan bien Ò, Ç y €. Locale.ROOT para que el importe salga 1500.00 y no 1500,00.

El main de BankProcess

public static void main(String[] args) {
    AccountMonitor monitor = new AccountMonitor(STATEMENT_FILE);
    long pid = ProcessHandle.current().pid();          // cada proceso tiene su PID
    System.out.println("Bienvenido a la oficina " + pid);

    boolean seguir = true;
    while (seguir) {
        Operation operation = ProcessInputUtils.readOperationMenu();
        if (operation == null) break;                // ha elegido 0 y confirmado

        String concept = ProcessInputUtils.readConcept();
        double amount  = ProcessInputUtils.readAmount();

        monitor.writeOperation(operation, concept, amount);   // aquí entra en juego el lock

        seguir = ProcessInputUtils.confirmAnotherOperation();
    }
    System.out.println("Bank process finished.");
}

Lo que verás en el debugger

Breakpoint en fos.write(...). Lanzas A y B con Debug As, uno después del otro.

Proceso A
Proceso B
Deposit · NÒMINA · 1500
lock() → coge la llave 🔒
⏸ parado en el breakpoint
Withdraw · COMPRES · 60
lock() → ⏳ espera (sale como Running)
F8 → escribe, suelta la llave 🔓
terminado
lock() → coge la llave 🔒 y para en el breakpoint
F8 → escribe y termina

Con writeOperationSinLock los dos se paran a la vez en el breakpoint, porque nadie bloquea a nadie. Alternando F8 entre A y B ves las letras mezclarse en el fichero.

Parte 2 · paquete bank.filter

12 tuberías de procesos en paralelo

BankFilter hace desde Java lo mismo que esta línea de terminal, pero para los 12 meses a la vez:

cat statement-01.txt | java StatementFilter amount -gt 100 | java StatementFilter amount -lt 200 >> filter.txt
statement-01.txt→ filtro 1 · importe > 100→ filtro 2 · importe < 200→ salida del mes

Cada filtro es un proceso nuevo (un java que ejecuta StatementFilter). 2 filtros × 12 meses = 24 procesos funcionando a la vez. Entre 100 y 200 € solo quedan los movimientos de SUPERMERCAT (120 €).

Ejecución terminada · 52 líneas en filter.txt
mesfichero → filtro 1 → filtro 2estado
filter.txtappend = true

Fase 1: el bucle arranca las 12 tuberías sin esperar. Fase 2: fuera del bucle se leen las salidas en orden. Si un mes aún no ha acabado, transferTo espera solo a ese mes.

Las piezas, una a una

ProcessBuilder

La «receta» de un proceso: qué comando ejecutar y cómo conectar su entrada, salida y errores. Crearlo no arranca nada todavía.

new ProcessBuilder(command)
redirectInput(File)

Cambia la entrada del proceso: en vez del teclado, lee un fichero. Se lo ponemos solo al primer filtro, porque el segundo lee de la salida del primero.

.get(0).redirectInput(new File("bank", fileName))
redirectError(INHERIT)

Los errores del hijo salen en nuestra consola. Gracias a esto vimos el «Could not find or load main class».

startPipeline(lista)

Arranca todos los procesos de la lista y conecta la salida de cada uno con la entrada del siguiente. Devuelve la List<Process>.

Process

Un proceso que ya está corriendo. Del último de cada tubería sacamos el resultado del mes.

getInputStream()

La salida del hijo. Para el padre es una entrada, porque el padre la lee. De ahí el nombre raro.

transferTo(fos)

Copia todo lo que sale del proceso al FileOutputStream. Si el proceso aún no ha acabado, espera.

FileOutputStream(…, true)

Abre filter.txt en modo append, para que cada mes se añada detrás del anterior.

El código final de BankFilter

public static void main(String[] args) throws IOException {
    new File(OUTPUT_FILE).delete();                      // 0. empezar con filter.txt limpio
    List<Process> lastPipelineProcesses = new ArrayList<>();

    for (int month = 1; month <= MONTHS; month++) {    // 1. un pipeline por mes
        String fileName = String.format("%s%02d%s", STATEMENT_PREFIX, month, STATEMENT_SUFFIX);

        List<ProcessBuilder> pipelineProcessBuilders = new ArrayList<>();
        for (String[] command : PIPE_LINE) {             // 2. un builder por filtro
            ProcessBuilder pb = new ProcessBuilder(command);
            pb.redirectError(ProcessBuilder.Redirect.INHERIT);
            pipelineProcessBuilders.add(pb);
        }

        pipelineProcessBuilders.get(0).redirectInput(new File("bank", fileName));   // 3. el primero lee el mes

        List<Process> pipelineProcesses = ProcessBuilder.startPipeline(pipelineProcessBuilders);   // 4. arrancar, SIN esperar

        Process lastPipelineProcess = pipelineProcesses.get(pipelineProcesses.size() - 1);       // 5. guardar el último
        lastPipelineProcesses.add(lastPipelineProcess);
    }

    try (FileOutputStream fos = new FileOutputStream(OUTPUT_FILE, true)) {   // 6. recoger en orden
        for (Process p : lastPipelineProcesses) {
            p.getInputStream().transferTo(fos);
        }
    }
    System.exit(0);
}

La regla del paralelismo: dentro del bucle solo se arranca. Ni waitFor() ni lecturas ahí. Si leyeras dentro, febrero no empezaría hasta que acabara enero.

El error que más nos costó

Por qué sobraba pb.directory(new File("bin"))

Hay dos «carpetas actuales» distintas. La real, donde arranca el proceso (la decide pb.directory). Y user.dir, la que Java cree que es la suya, desde donde busca las clases. El profe ya puso -Duser.dir=bin en los comandos, y es relativo a la carpeta real.

con pb.directory("bin")
arranca en …/bank/bin user.dir=bin …/bank/bin/bin busca …/bank/bin/bin/bank/filter/ StatementFilter.class
✗ bin dos veces · ClassNotFoundException ×24
sin pb.directory
arranca en …/bank user.dir=bin …/bank/bin busca …/bank/bin/bank/filter/ StatementFilter.class
✓ bin una vez · funciona

El redirectInput no se ve afectado: esa ruta la resuelve el proceso padre, que se ejecuta en la raíz del proyecto. Por eso new File("bank", fileName) apunta a …/bank/bank/statement-01.txt, que es la ruta que viste en Properties → Location.

Por el camino

Errores que hemos resuelto

Síntoma

FORMATTER y fileName en rojo en ProcessInputUtils.

Causa y arreglo

writeOperation estaba pegado en la clase equivocada. Va en AccountMonitor.

Síntoma

El debug parece «colgado» en una línea.

Causa y arreglo

Es un SCANNER.nextLine() esperando teclado. Escribe en la consola y pulsa Enter.

Síntoma

En rojo startPipeline y el try de BankFilter.

Causa y arreglo

Faltaban import java.io.IOException y throws IOException en el main.

Síntoma

Could not find or load main class bank.filter.StatementFilter ×24.

Causa y arreglo

pb.directory("bin") + -Duser.dir=bin = buscar en bin/bin. Quitamos pb.directory.

Síntoma

filter.txt crece cada vez que ejecutas.

Causa y arreglo

Se escribe con append. Por eso borramos el fichero al empezar el main.

Para practicar

Quiz