From 2d0ff2bf84b9fb59bc79bc2c38a52957dbf3ebbe Mon Sep 17 00:00:00 2001 From: Rinjae Date: Thu, 27 Aug 2026 16:44:57 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=97=94?= =?UTF-8?q?=EC=A7=84=20=EC=83=81=ED=83=9C=20=EC=A0=90=EA=B2=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20Thymeleaf=20=EA=B5=AC=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apim/portal/apps/ReadinessController.java | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java b/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java index 125bb9f..230d425 100644 --- a/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java +++ b/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java @@ -10,14 +10,21 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import org.thymeleaf.spring5.SpringTemplateEngine; /** * Readiness probe. * *

Distinct from {@link HealthCheckController} (liveness — servlet alive?). * This endpoint validates both EMS and Gateway datasources via JDBC - * {@code Connection.isValid(timeout)} to confirm the app is ready to serve - * requests that depend on the database. + * {@code Connection.isValid(timeout)}, and the Thymeleaf {@link SpringTemplateEngine} + * configuration, to confirm the app is ready to serve real page requests. + * + *

The template-engine check exists because this controller bypasses view + * resolution entirely (plain {@code @RestController} JSON) — a broken + * {@code TemplateEngine.getConfiguration()} (e.g. classpath split causing + * {@link java.util.ServiceConfigurationError} during dialect/module discovery) + * previously left every real page returning 500 while this probe still reported 200. * *

HTTP 200 + JSON when all checks pass. *

HTTP 503 + JSON when any check fails — body still includes the per-component @@ -30,24 +37,29 @@ public class ReadinessController { private final DataSource portalDataSource; private final DataSource gatewayDataSource; + private final SpringTemplateEngine templateEngine; public ReadinessController( @Qualifier("portalDataSource") DataSource portalDataSource, - @Qualifier("gatewayDataSource") DataSource gatewayDataSource) { + @Qualifier("gatewayDataSource") DataSource gatewayDataSource, + SpringTemplateEngine templateEngine) { this.portalDataSource = portalDataSource; this.gatewayDataSource = gatewayDataSource; + this.templateEngine = templateEngine; } @GetMapping(value = "/health/ready", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity ready() { - String ems = check(portalDataSource); - String gw = check(gatewayDataSource); - boolean ok = "UP".equals(ems) && "UP".equals(gw); + String ems = check(portalDataSource); + String gw = check(gatewayDataSource); + String view = checkTemplateEngine(); + boolean ok = "UP".equals(ems) && "UP".equals(gw) && "UP".equals(view); String body = "{" - + "\"status\":\"" + (ok ? "UP" : "DOWN") + "\"," - + "\"ems\":\"" + ems + "\"," - + "\"gateway\":\""+ gw + "\"" + + "\"status\":\"" + (ok ? "UP" : "DOWN") + "\"," + + "\"ems\":\"" + ems + "\"," + + "\"gateway\":\"" + gw + "\"," + + "\"view\":\"" + view + "\"" + "}"; return ResponseEntity.status(ok ? 200 : 503) @@ -55,6 +67,20 @@ public class ReadinessController { .body(body); } + /** {@code getConfiguration()} lazily runs Thymeleaf's dialect/module init on first call + * and caches it — so this is cheap once healthy, and reproduces the exact failure path + * a real page render would hit. Catches {@link Throwable}: init failures here have + * surfaced as {@link Error} (ServiceConfigurationError), not just Exception. */ + private String checkTemplateEngine() { + try { + templateEngine.getConfiguration(); + return "UP"; + } catch (Throwable t) { + String msg = t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage(); + return "DOWN:" + msg.replace('"', '\'').replace('\n', ' ').replace('\r', ' '); + } + } + private static String check(DataSource ds) { if (ds == null) return "DOWN:NO_DATASOURCE"; try (Connection c = ds.getConnection()) {