Habla con el usuario
Bucle: menú → concepto → importe → «monitor, apunta esto» → ¿otra? No sabe nada de bloqueos.
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 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.
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 €
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.
Bucle: menú → concepto → importe → «monitor, apunta esto» → ¿otra? No sabe nada de bloqueos.
Coge el lock, añade la línea al final del fichero y lo suelta al salir del try.
Menú, concepto, importe y confirmaciones s/n. Un único Scanner compartido. No se toca.
Con lock: B espera en lock() hasta que A termina. Las dos líneas quedan enteras.
AccountMonitor.writeOperationpublic 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 }
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.
Da el canal del fichero. Es el objeto que sabe pedir bloqueos al sistema operativo.
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.
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 para que salgan bien Ò, Ç y €. Locale.ROOT para que el importe salga 1500.00 y no 1500,00.
BankProcesspublic 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."); }
Breakpoint en fos.write(...). Lanzas A y B con Debug As, uno después del otro.
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.
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
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 €).
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.
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)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))Los errores del hijo salen en nuestra consola. Gracias a esto vimos el «Could not find or load main class».
Arranca todos los procesos de la lista y conecta la salida de cada uno con la entrada del siguiente. Devuelve la List<Process>.
Un proceso que ya está corriendo. Del último de cada tubería sacamos el resultado del mes.
La salida del hijo. Para el padre es una entrada, porque el padre la lee. De ahí el nombre raro.
Copia todo lo que sale del proceso al FileOutputStream. Si el proceso aún no ha acabado, espera.
Abre filter.txt en modo append, para que cada mes se añada detrás del anterior.
BankFilterpublic 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.
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.
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.
FORMATTER y fileName en rojo en ProcessInputUtils.
writeOperation estaba pegado en la clase equivocada. Va en AccountMonitor.
El debug parece «colgado» en una línea.
Es un SCANNER.nextLine() esperando teclado. Escribe en la consola y pulsa Enter.
En rojo startPipeline y el try de BankFilter.
Faltaban import java.io.IOException y throws IOException en el main.
Could not find or load main class bank.filter.StatementFilter ×24.
pb.directory("bin") + -Duser.dir=bin = buscar en bin/bin. Quitamos pb.directory.
filter.txt crece cada vez que ejecutas.
Se escribe con append. Por eso borramos el fichero al empezar el main.