1010
1111import java .io .FileInputStream ;
1212import java .io .IOException ;
13+ import java .nio .file .Files ;
14+ import java .nio .file .Path ;
1315
1416/**
1517 * 构造 BetaAnalyticsDataClient 单例 Bean 并注入 Service Account 凭证。
1618 * 客户端基于 gRPC,线程安全,在应用启动时创建一次即可;作用域仅申请 analytics.readonly,不写任何数据。
19+ *
20+ * 容错策略:凭证文件不存在不阻塞容器启动,而是注册一个"调用即抛 Ga4UnavailableException"的 stub
21+ * client。这样 GA4 接口运行时返回 503,其他业务(auth / preferences / 埋点)仍可用。
22+ * 之前直接 throw IOException 让 Spring ApplicationContext 起不来 → 整个后端崩,影响面过大。
1723 */
1824@ Configuration
1925public class Ga4ClientConfig {
@@ -22,26 +28,58 @@ public class Ga4ClientConfig {
2228
2329 /**
2430 * 从本地 JSON 文件加载 Google Service Account 密钥构造 GA4 Data API 客户端。
25- * 启动失败(如凭证路径不存在或无访问权限)会让容器启动失败,及时暴露配置问题。
31+ * 文件不存在或读取失败时记录 warning 并抛 IOException 让 Spring 跳过 bean 注册前的提前 check —
32+ * 实际改为返回一个调用失败的 stub。
2633 */
2734 @ Bean
28- public BetaAnalyticsDataClient betaAnalyticsDataClient (Ga4Properties ga4Properties ) throws IOException {
35+ public BetaAnalyticsDataClient betaAnalyticsDataClient (Ga4Properties ga4Properties ) {
2936 String credPath = ga4Properties .getCredentialsPath ();
30- log .info ("初始化 GA4 客户端,凭证路径: {}" , credPath );
3137
32- // 只申请只读权限(最小权限原则);try-with-resources 显式关闭文件流,
33- // GoogleCredentials.fromStream 并不保证替调用方关闭 InputStream
34- GoogleCredentials credentials ;
38+ if (credPath == null || credPath .isBlank () || !Files .exists (Path .of (credPath ))) {
39+ log .warn ("GA4 凭证文件不存在或未配置 ({}), GA4 接口将返回 503,其余功能不受影响。" ,
40+ credPath );
41+ return brokenClient (credPath );
42+ }
43+
44+ log .info ("初始化 GA4 客户端,凭证路径: {}" , credPath );
3545 try (FileInputStream credentialsStream = new FileInputStream (credPath )) {
36- credentials = GoogleCredentials
46+ GoogleCredentials credentials = GoogleCredentials
3747 .fromStream (credentialsStream )
3848 .createScoped ("https://www.googleapis.com/auth/analytics.readonly" );
39- }
4049
41- BetaAnalyticsDataSettings settings = BetaAnalyticsDataSettings .newBuilder ()
42- .setCredentialsProvider (() -> credentials )
43- .build ();
50+ BetaAnalyticsDataSettings settings = BetaAnalyticsDataSettings .newBuilder ()
51+ .setCredentialsProvider (() -> credentials )
52+ .build ();
4453
45- return BetaAnalyticsDataClient .create (settings );
54+ return BetaAnalyticsDataClient .create (settings );
55+ } catch (IOException e ) {
56+ log .warn ("GA4 客户端初始化失败 ({}): {}, 降级到 stub" , credPath , e .getMessage ());
57+ return brokenClient (credPath );
58+ }
59+ }
60+
61+ /**
62+ * 构造一个"调用即失败"的 BetaAnalyticsDataClient,让 Ga4ReportService.fetchTopPaths
63+ * 走异常分支抛 Ga4UnavailableException → GlobalExceptionHandler 转 503。
64+ *
65+ * 用 BetaAnalyticsDataClient.create(settings) 配一个无效 endpoint 即可;
66+ * 实际方法调用会在网络层 / auth 层失败。
67+ */
68+ private BetaAnalyticsDataClient brokenClient (String credPath ) {
69+ try {
70+ BetaAnalyticsDataSettings settings = BetaAnalyticsDataSettings .newBuilder ()
71+ // 用一个明显无效的 endpoint,确保任何 RPC 立刻失败而不是真的去打 google
72+ .setEndpoint ("invalid.localhost:0" )
73+ .setCredentialsProvider (() -> {
74+ throw new IllegalStateException (
75+ "GA4 credentials file not configured: " + credPath );
76+ })
77+ .build ();
78+ return BetaAnalyticsDataClient .create (settings );
79+ } catch (IOException e ) {
80+ // 这里几乎不会发生(create 只是包对象不发请求),保险起见还是抛 RuntimeException
81+ throw new IllegalStateException (
82+ "Failed to construct stub GA4 client; both real and stub init failed" , e );
83+ }
4684 }
4785}
0 commit comments