001package com.streamconverter.context; 002 003import java.time.Instant; 004import java.util.Collections; 005import java.util.HashMap; 006import java.util.Map; 007import java.util.Objects; 008import java.util.UUID; 009import java.util.concurrent.ConcurrentHashMap; 010import java.util.concurrent.atomic.AtomicBoolean; 011import java.util.concurrent.atomic.AtomicInteger; 012import org.slf4j.MDC; 013 014/** 015 * 実行コンテキストを管理するクラス 016 * 017 * <p>StreamConverterの実行全体を通じて一意の識別子とコンテキスト情報を提供し、 マルチスレッド環境でのMDC伝播を支援します。 018 */ 019public class ExecutionContext { 020 021 private final String executionId; 022 private final Instant startTime; 023 private final AtomicInteger commandSequence; 024 private final Map<String, String> globalContext; 025 private final Map<String, String> userContext; 026 private final ConcurrentHashMap<String, String> sharedContext; 027 private final AtomicBoolean sharedContextDirty; 028 029 // 標準的なコンテキストキー 030 /** 実行ID用のMDCキー */ 031 public static final String EXECUTION_ID_KEY = "executionId"; 032 033 /** 開始時刻用のMDCキー */ 034 public static final String START_TIME_KEY = "startTime"; 035 036 /** コマンドシーケンス用のMDCキー */ 037 public static final String COMMAND_SEQUENCE_KEY = "commandSequence"; 038 039 /** スレッド名用のMDCキー */ 040 public static final String THREAD_NAME_KEY = "threadName"; 041 042 /** ステージ用のMDCキー */ 043 public static final String STAGE_KEY = "stage"; 044 045 private ExecutionContext(Builder builder) { 046 this.executionId = builder.executionId; 047 this.startTime = builder.startTime; 048 this.commandSequence = new AtomicInteger(0); 049 this.globalContext = Collections.unmodifiableMap(new HashMap<>(builder.globalContext)); 050 this.userContext = new HashMap<>(builder.userContext); 051 this.sharedContext = new ConcurrentHashMap<>(); 052 this.sharedContextDirty = new AtomicBoolean(false); 053 } 054 055 /** 056 * 新しい実行コンテキストを生成 057 * 058 * @return 新しいExecutionContextインスタンス 059 */ 060 public static ExecutionContext create() { 061 return new Builder().build(); 062 } 063 064 /** 065 * カスタム設定でExecutionContextを作成するためのビルダーを取得 066 * 067 * @return Builderインスタンス 068 */ 069 public static Builder builder() { 070 return new Builder(); 071 } 072 073 /** 074 * 実行ID(一意識別子)を取得 075 * 076 * @return 実行ID 077 */ 078 public String getExecutionId() { 079 return executionId; 080 } 081 082 /** 083 * 実行開始時刻を取得 084 * 085 * @return 開始時刻 086 */ 087 public Instant getStartTime() { 088 return startTime; 089 } 090 091 /** 092 * 次のコマンドシーケンス番号を取得(自動インクリメント) 093 * 094 * @return シーケンス番号 095 */ 096 public int getNextCommandSequence() { 097 return commandSequence.incrementAndGet(); 098 } 099 100 /** 101 * 現在のコマンドシーケンス番号を取得 102 * 103 * @return 現在のシーケンス番号 104 */ 105 public int getCurrentCommandSequence() { 106 return commandSequence.get(); 107 } 108 109 /** 110 * グローバルコンテキスト値を取得 111 * 112 * @param key キー 113 * @return 値(存在しない場合はnull) 114 */ 115 public String getGlobalContext(String key) { 116 return globalContext.get(key); 117 } 118 119 /** 120 * 全てのグローバルコンテキストを取得 121 * 122 * @return 読み取り専用のコンテキストマップ 123 */ 124 public Map<String, String> getAllGlobalContext() { 125 return globalContext; 126 } 127 128 /** 129 * ユーザーコンテキスト値を取得 130 * 131 * @param key キー 132 * @return 値(存在しない場合はnull) 133 */ 134 public String getUserContext(String key) { 135 return userContext.get(key); 136 } 137 138 /** 139 * ユーザーコンテキスト値を設定 140 * 141 * @param key キー 142 * @param value 値 143 */ 144 public void setUserContext(String key, String value) { 145 if (value == null) { 146 userContext.remove(key); 147 } else { 148 userContext.put(key, value); 149 } 150 } 151 152 /** 153 * 全てのユーザーコンテキストを取得 154 * 155 * @return ユーザーコンテキストマップ 156 */ 157 public Map<String, String> getAllUserContext() { 158 return new HashMap<>(userContext); 159 } 160 161 /** 162 * 共有コンテキスト値を取得 163 * 164 * <p>共有コンテキストはスレッド間で共有されるコンテキスト情報です。 ConcurrentHashMapで実装されているため、スレッドセーフです。 165 * 166 * @param key キー 167 * @return 値(存在しない場合はnull) 168 */ 169 public String getSharedContext(String key) { 170 return sharedContext.get(key); 171 } 172 173 /** 174 * 共有コンテキスト値を設定 175 * 176 * <p>共有コンテキストはスレッド間で共有されるコンテキスト情報です。 実行中のスレッドからいつでも設定・取得が可能で、他のスレッドから即座にアクセスできます。 177 * 178 * @param key キー 179 * @param value 値(nullの場合は削除) 180 */ 181 public void setSharedContext(String key, String value) { 182 if (value == null) { 183 sharedContext.remove(key); 184 } else { 185 sharedContext.put(key, value); 186 } 187 sharedContextDirty.set(true); 188 } 189 190 /** 191 * 全ての共有コンテキストを取得 192 * 193 * @return 共有コンテキストマップのコピー 194 */ 195 public Map<String, String> getAllSharedContext() { 196 return new HashMap<>(sharedContext); 197 } 198 199 /** 200 * 現在のコンテキストをMDCに設定 201 * 202 * <p>このメソッドを呼び出すことで、実行コンテキストの情報が 現在のスレッドのMDCに設定されます。 共有コンテキストも含めて全てのコンテキスト情報がMDCに反映されます。 203 * 204 * <p>共有コンテキストはDirty Flag最適化により、変更があった場合のみMDCに同期されます。 これにより、頻繁なapplyToMDC()呼び出しでもパフォーマンスが維持されます。 205 */ 206 public void applyToMDC() { 207 // 基本的なコンテキスト情報をMDCに設定 208 MDC.put(EXECUTION_ID_KEY, executionId); 209 MDC.put(START_TIME_KEY, startTime.toString()); 210 MDC.put(COMMAND_SEQUENCE_KEY, String.valueOf(getCurrentCommandSequence())); 211 MDC.put(THREAD_NAME_KEY, Thread.currentThread().getName()); 212 213 // グローバルコンテキストをMDCに設定 214 globalContext.forEach(MDC::put); 215 216 // ユーザーコンテキストをMDCに設定 217 userContext.forEach(MDC::put); 218 219 // 共有コンテキストをMDCに設定 220 // マルチスレッド環境では各スレッドが独自のMDCを持つため、常に同期が必要 221 sharedContext.forEach(MDC::put); 222 } 223 224 /** 225 * 特定のステージでのMDC設定 226 * 227 * @param stageName ステージ名 228 */ 229 public void applyToMDCWithStage(String stageName) { 230 applyToMDC(); 231 MDC.put(STAGE_KEY, stageName); 232 } 233 234 /** 235 * コンテキストのコピーを作成 ユーザーコンテキストは複製されますが、グローバルコンテキストは共有されます。 コピー時はコマンドシーケンスは0にリセットされます。 236 * 237 * @return コンテキストのコピー 238 */ 239 public ExecutionContext copy() { 240 return new Builder() 241 .executionId(this.executionId) 242 .startTime(this.startTime) 243 .globalContext(this.globalContext) 244 .userContext(this.userContext) 245 .build(); 246 // コマンドシーケンスは新しいコピーとして0から開始 247 } 248 249 /** ExecutionContext作成用のBuilderクラス */ 250 public static class Builder { 251 private String executionId = generateExecutionId(); 252 private Instant startTime = Instant.now(); 253 private Map<String, String> globalContext = new HashMap<>(); 254 private Map<String, String> userContext = new HashMap<>(); 255 256 /** Creates a new builder. */ 257 public Builder() {} 258 259 /** 260 * 実行IDを設定 261 * 262 * @param executionId 実行ID 263 * @return Builderインスタンス 264 */ 265 public Builder executionId(String executionId) { 266 this.executionId = Objects.requireNonNull(executionId, "executionId cannot be null"); 267 return this; 268 } 269 270 /** 271 * 開始時刻を設定 272 * 273 * @param startTime 開始時刻 274 * @return Builderインスタンス 275 */ 276 public Builder startTime(Instant startTime) { 277 this.startTime = Objects.requireNonNull(startTime, "startTime cannot be null"); 278 return this; 279 } 280 281 /** 282 * グローバルコンテキスト値を設定 283 * 284 * @param key キー 285 * @param value 値 286 * @return Builderインスタンス 287 */ 288 public Builder globalContext(String key, String value) { 289 Objects.requireNonNull(key, "key cannot be null"); 290 if (value == null) { 291 this.globalContext.remove(key); 292 } else { 293 this.globalContext.put(key, value); 294 } 295 return this; 296 } 297 298 /** 299 * グローバルコンテキストを一括設定 300 * 301 * @param context コンテキストマップ 302 * @return Builderインスタンス 303 */ 304 public Builder globalContext(Map<String, String> context) { 305 if (context != null) { 306 this.globalContext.putAll(context); 307 } 308 return this; 309 } 310 311 /** 312 * ユーザーコンテキスト値を設定 313 * 314 * @param key キー 315 * @param value 値 316 * @return Builderインスタンス 317 */ 318 public Builder userContext(String key, String value) { 319 Objects.requireNonNull(key, "key cannot be null"); 320 if (value == null) { 321 this.userContext.remove(key); 322 } else { 323 this.userContext.put(key, value); 324 } 325 return this; 326 } 327 328 /** 329 * ユーザーコンテキストを一括設定 330 * 331 * @param context コンテキストマップ 332 * @return Builderインスタンス 333 */ 334 public Builder userContext(Map<String, String> context) { 335 if (context != null) { 336 this.userContext.putAll(context); 337 } 338 return this; 339 } 340 341 /** 342 * ExecutionContextインスタンスを生成 343 * 344 * @return ExecutionContextインスタンス 345 */ 346 public ExecutionContext build() { 347 return new ExecutionContext(this); 348 } 349 350 private static String generateExecutionId() { 351 return "EXEC-" 352 + UUID.randomUUID().toString().substring(0, 8) 353 + "-" 354 + System.currentTimeMillis(); 355 } 356 } 357 358 @Override 359 public String toString() { 360 return String.format( 361 "ExecutionContext{executionId='%s', startTime=%s, commandSequence=%d}", 362 executionId, startTime, getCurrentCommandSequence()); 363 } 364 365 @Override 366 public boolean equals(Object obj) { 367 if (this == obj) return true; 368 if (obj == null || getClass() != obj.getClass()) return false; 369 ExecutionContext that = (ExecutionContext) obj; 370 return Objects.equals(executionId, that.executionId); 371 } 372 373 @Override 374 public int hashCode() { 375 return Objects.hash(executionId); 376 } 377}