Repository navigation
Expand file tree
/
Copy pathExpSpringAsync.java
More file actions
219 lines (183 loc) · 10.9 KB
/
Copy pathExpSpringAsync.java
File metadata and controls
219 lines (183 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
// Simula Spring @Async sin dependencias de Spring.
//
// Cómo funciona @Async en Spring:
// 1. @EnableAsync activa un BeanPostProcessor que crea un proxy CGLIB/JDK alrededor
// del bean anotado con @Async.
// 2. Cuando se llama al método, el proxy intercepta la llamada y la delega a un
// ThreadPoolTaskExecutor en lugar de ejecutarla en el hilo del llamador.
// 3. El hilo del llamador recibe inmediatamente un Future/CompletableFuture o void.
// 4. Si el método lanza una excepción y devuelve void, el AsyncUncaughtExceptionHandler
// la recibe — sin él la excepción se pierde silenciosamente.
//
// Trampa clásica: llamar a un @Async desde el mismo bean no pasa por el proxy
// → la llamada es directa y el método se ejecuta sincrónicamente.
public class ExpSpringAsync {
// ── ThreadPoolTaskExecutor simulado ───────────────────────────────────────
// En Spring: @Bean ThreadPoolTaskExecutor taskExecutor() { ... }
// Parámetros clave:
// corePoolSize → hilos siempre vivos (incluso sin tareas)
// maxPoolSize → máximo de hilos antes de rechazar o encolar
// queueCapacity → tamaño de la cola; si se llena y hay maxPoolSize hilos → RejectedExecutionException
// keepAliveSeconds → tiempo que vive un hilo por encima de corePoolSize sin trabajo
static class ThreadPoolTaskExecutor {
private final ThreadPoolExecutor executor;
private final String name;
ThreadPoolTaskExecutor(String name, int core, int max, int queue) {
this.name = name;
// LinkedBlockingQueue con capacidad finita — evita memoria ilimitada
this.executor = new ThreadPoolExecutor(core, max, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queue),
r -> {
Thread t = new Thread(r, name + "-" + threadCounter.incrementAndGet());
t.setDaemon(true); // hilo daemon — no impide que la JVM termine
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy()); // cuando la cola está llena, el llamador ejecuta
}
private static final AtomicInteger threadCounter = new AtomicInteger(0);
ExecutorService executor() { return executor; }
void shutdown() {
executor.shutdown();
try { executor.awaitTermination(10, TimeUnit.SECONDS); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
void printStatus() {
System.out.printf(" [%s] active=%d pool=%d queue=%d completed=%d%n",
name, executor.getActiveCount(), executor.getPoolSize(),
executor.getQueue().size(), executor.getCompletedTaskCount());
}
}
// ── AsyncUncaughtExceptionHandler ─────────────────────────────────────────
// En Spring: implementar AsyncUncaughtExceptionHandler en @Configuration + @EnableAsync
// Captura excepciones de métodos @Async que devuelven void — no hay Future para propagarlas.
// Si devuelve Future/CompletableFuture, la excepción queda envuelta en el Future.
interface AsyncUncaughtExceptionHandler {
void handleUncaughtException(Throwable ex, String methodName, Object... params);
}
static class LoggingAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, String methodName, Object... params) {
System.out.printf(" [AsyncExceptionHandler] Excepción en '%s'(%s): %s%n",
methodName, java.util.Arrays.toString(params), ex.getMessage());
}
}
// ── Proxy async simulado ──────────────────────────────────────────────────
// El proxy intercepta la llamada y la despacha al executor.
// En Spring: el proxy es generado en runtime por CGLIB o JDK Dynamic Proxy.
static class AsyncProxy {
private final ExecutorService executor;
private final AsyncUncaughtExceptionHandler exceptionHandler;
AsyncProxy(ExecutorService executor, AsyncUncaughtExceptionHandler handler) {
this.executor = executor;
this.exceptionHandler = handler;
}
// Simula @Async en método void — despacha sin esperar resultado
void invokeAsync(String methodName, Runnable task, Object... params) {
executor.submit(() -> {
try {
task.run();
} catch (Exception e) {
// Sin Future que capture la excepción, va al handler
exceptionHandler.handleUncaughtException(e, methodName, params);
}
});
}
// Simula @Async en método CompletableFuture<T>
<T> CompletableFuture<T> invokeAsyncFuture(Callable<T> task) {
// supplyAsync delega al executor; la excepción queda dentro del Future
return CompletableFuture.supplyAsync(() -> {
try { return task.call(); }
catch (Exception e) { throw new CompletionException(e); }
}, executor);
}
}
// ── Servicio de negocio ───────────────────────────────────────────────────
static class NotificationService {
private final AsyncProxy proxy;
NotificationService(AsyncProxy proxy) { this.proxy = proxy; }
// @Async — void: el llamador no espera, las excepciones van al handler
void sendEmail(String to, String subject) {
proxy.invokeAsync("sendEmail", () -> {
busyWaitMs(80);
System.out.printf(" [Email] Enviado a %s: \"%s\"%n", to, subject);
}, to, subject);
}
// @Async — void que falla: la excepción va al AsyncUncaughtExceptionHandler
void sendPushNotification(String userId) {
proxy.invokeAsync("sendPushNotification", () -> {
busyWaitMs(30);
throw new RuntimeException("Dispositivo no registrado: " + userId);
}, userId);
}
// @Async — CompletableFuture<String>: el llamador puede esperar si quiere
CompletableFuture<String> generateReport(String reportId) {
return proxy.invokeAsyncFuture(() -> {
busyWaitMs(120);
return "report-" + reportId + "-" + Thread.currentThread().getName();
});
}
}
static void busyWaitMs(long ms) {
long end = System.currentTimeMillis() + ms;
while (System.currentTimeMillis() < end) { /* spin */ }
}
// ── Main ──────────────────────────────────────────────────────────────────
public static void main(String[] args) throws Exception {
System.out.println("═".repeat(60));
System.out.println(" SPRING @ASYNC — ThreadPool + Future + ExceptionHandler");
System.out.println("═".repeat(60));
// Construir executor equivalente al @Bean de Spring
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(
"async-executor", 2, 4, 10);
AsyncProxy proxy = new AsyncProxy(
taskExecutor.executor(),
new LoggingAsyncExceptionHandler());
NotificationService service = new NotificationService(proxy);
// ── Demo 1: void async (fire-and-forget) ────────────────────────────
System.out.println("\n=== @Async void — fire-and-forget ===");
long start = System.currentTimeMillis();
service.sendEmail("jorge@example.com", "Bienvenido");
service.sendEmail("ana@example.com", "Factura disponible");
System.out.printf(" Hilo principal continúa inmediatamente (%.0fms)%n",
(double)(System.currentTimeMillis() - start));
// ── Demo 2: excepción en @Async void → AsyncUncaughtExceptionHandler ─
System.out.println("\n=== Excepción en @Async void → handler ===");
service.sendPushNotification("user-999");
busyWaitMs(150); // esperar que el async termine antes de imprimir estado
// ── Demo 3: @Async CompletableFuture — el llamador puede unirse ──────
System.out.println("\n=== @Async CompletableFuture — esperar resultado ===");
List<CompletableFuture<String>> futures = List.of(
service.generateReport("R1"),
service.generateReport("R2"),
service.generateReport("R3"));
// Esperar todos en paralelo — sin bloquear en cada uno individualmente
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
futures.forEach(f -> {
try { System.out.println(" Report: " + f.get()); }
catch (Exception e) { System.out.println(" Error: " + e.getMessage()); }
});
// ── Demo 4: excepción en CompletableFuture ───────────────────────────
System.out.println("\n=== Excepción en @Async CompletableFuture ===");
CompletableFuture<String> failedFuture = proxy.invokeAsyncFuture(() -> {
busyWaitMs(20);
throw new RuntimeException("Servicio de reportes caído");
});
// exceptionally: gestión inline de errores sin try/catch
String fallback = failedFuture
.exceptionally(ex -> "fallback-report (error: " + ex.getCause().getMessage() + ")")
.get();
System.out.println(" Resultado: " + fallback);
taskExecutor.printStatus();
System.out.println("\n── Resumen ─────────────────────────────────────────────");
System.out.println(" @Async void → fire-and-forget, errores al handler");
System.out.println(" @Async Future<T> → bloquea en .get() si se necesita");
System.out.println(" @Async CompletableFuture<T> → pipeline non-blocking");
System.out.println(" AsyncUncaughtExceptionHandler → solo para métodos void");
System.out.println(" Self-invocation → no pasa por proxy → sync (trampa)");
System.out.println(" @EnableAsync → activa el BeanPostProcessor que crea el proxy");
taskExecutor.shutdown();
}
}