プロジェクトのある時点で、操作の進行状況を追跡し、それに関する情報を取得または保存するという疑問が生じます。 このために、操作コンテキスト、たとえばクライアントセッションのコンテキストが可能な限り機能します。 これを比較的簡単に行う方法に興味がある場合は、猫をお願いします。
Javaの世界では、多くの場合(常にではありませんが)、各操作は独自のスレッドで実行されます。 そして、ここではすべてが非常に単純であることが
わかります
。ThreadLocalオブジェクトを使用して、操作中にいつでも取得できます。
class Context { public static final ThreadLocal<Context> global = new ThreadLocal<Context>; }
scalaでは、それほど単純ではないことが多く、操作中、非常に非同期なアプリケーションなどでフローが繰り返し変化する場合があります。 また、
ThreadLocalを使用したメソッド
は適切で
はなくなりました(Javaのスレッド切り替えの場合はもちろん)。
頭に浮かぶかもしれない最初のものは、暗黙の関数引数を通してコンテキストを渡すことです。
def foo(bar: Bar)(implicit context: Context)
しかし、これはサービスプロトコルを混乱させます。 少し頭を痛めた、かなり単純なアイデアが浮上しました。サービスオブジェクトにコンテキストをアタッチし、関数が呼び出されたときに内部サービスにそれを配布するというものです。
コンテキストが次のようになっているとしましょう。
コンテキスト依存オブジェクトをマークするトレイトを作成しましょう:
trait ContextualObject { protected def context: Option[Context] }
次に、サービスと実装を発表します。
そして、最初のサービスが使用する2番目のサービス:
trait ServiceB extends ChangeableContextualObject[ServiceB] { def someOperationWithoutServiceA: Int def someOperationWithServiceA(implicit executionContext: ExecutionContext): Future[Boolean] } trait ServiceBImpl extends ServiceB { self => protected def serviceA: ServiceA override def someOperationWithoutServiceA: Int = 1 override def someOperationWithServiceA(implicit executionContext: ExecutionContext): Future[Boolean] = { serviceA.someLongOperation.map { case res if res % 2 == 0 => context.foreach(_.data.put("ServiceB.res", "even")) true case res => context.foreach(_.data.put("ServiceB.res", "odd")) false } } override def withContext(ctx: Option[Context]): ServiceB = new ServiceBImpl { ctx.foreach(_.data.put("ServiceB.withContext", "true")) override protected val context: Option[Context] = ctx
その結果、呼び出しの場所で次のコードを取得します。
val context = new Context("opId") val serviceBWithContext = serviceB.withContext(Some(context)) serviceBWithContext.someOperationWithoutServiceA context.data.get("ServiceB.withContext")
すべてが非常に単純です-したがって、操作のコースは同じコンテキストを持ちます。 ただし、このための実際のアプリケーションを見つける必要があります。 たとえば、運用中に重要な情報を記録しましたが、この情報を誓約したいと考えています。 最も簡単なオプションは、コンテキストごとにロガーを作成し、メッセージにログを書き込む際に、この情報をロガーに割り当てることでした。 ただし、コードの外部(サードパーティライブラリなど)で発生するログの問題があります。
コンテキストをコード外で使用するには、コンテキストで
ThreadLocalを作成します。
object Context { val global: ThreadLocal[Option[Context]] = ThreadLocal.withInitial[Option[Context]](() => None)
たとえば、ロギングに
logback-classicライブラリを使用する場合、これらのパラメーターを記録するレイアウトを作成できます。
可能な実装 class OperationContextLayout extends LayoutBase[ILoggingEvent] { private val separator: String = System.getProperty("line.separator") override def doLayout(event: ILoggingEvent): String = { val sb = new StringBuilder(256) sb.append(event.getFormattedMessage) .append(separator) appendContextParams(sb) appendStack(event, sb) sb.toString() } private def appendContextParams(sb: StringBuilder): Unit = { Context.global.get().foreach { ctx => sb.append("operationId=") .append(ctx.operationId) ctx.data.readOnlySnapshot().foreach { case (key, value) => sb.append(" ").append(key).append("=").append(value) } sb.append(separator) } } private def appendStack(event: ILoggingEvent, sb: StringBuilder): Unit = { if (event.getThrowableProxy != null) { val converter = new ThrowableProxyConverter converter.setOptionList(List("full").asJava) converter.start() sb.append() } } }
可能な構成 <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="operation.context.logging.OperationContextLayout" /> </encoder> </appender> <root level="debug"> <appender-ref ref="STDOUT" /> </root> </configuration>
そして、何かを誓うようにしてください:
def runWithoutA(): Unit = { val context = Some(createContext()) val res = serviceB.withContext(context).someOperationWithoutServiceA Context.runWith(context) {
def runWithA(): Future[_] = { val context = Some(createContext()) serviceB.withContext(context).someOperationWithServiceA.andThen { case _ => Context.runWith(context) {
そして疑問が残りました:
ExecutionContextで実行される外部コードはどうですか? しかし、誰も彼のためにラッパーを書くことを気にしません:
ラッパーの可能な実装 class ContextualExecutionContext(context: Option[Context], executor: ExecutionContext) extends ExecutionContext { override def execute(runnable: Runnable): Unit = executor.execute(() => { Context.runWith(context)(runnable.run()) }) override def reportFailure(cause: Throwable): Unit = { Context.runWith(context)(executor.reportFailure(cause)) } } object ContextualExecutionContext { implicit class ContextualExecutionContextOps(val executor: ExecutionContext) extends AnyVal { def withContext(context: Option[Context]): ContextualExecutionContext = new ContextualExecutionContext(context, executor) } }
外部システムの可能な実装 class SomeExternalObject { val logger: Logger = LoggerFactory.getLogger(classOf[SomeExternalObject]) def externalCall(implicit executionContext: ExecutionContext): Future[Int] = { Future(1).andThen { case Success(res) => logger.debug(s"external res $res") } } }
ExecutionContextで呼び出しを試行してみましょう。
def runExternal(): Future[_] = { val context = Some(createContext()) implicit val executor = global.withContext(context)
それが全体のアイデアです。 実際、コンテキストの使用はロギングに限定されません。 このコンテキストには何でも保存できます。 たとえば、操作中にすべてのサービスが同じデータを処理する場合、いくつかの状態のキャスト。 などなど。
アクターを通信するときにコンテキストの監視を実装する必要がある場合は、コメントを書いて、記事を補足します。 他の実装についてのアイデアがあり、コメントも書いてください。読むのは面白いでしょう。
PS記事
github.com/eld0727/scala-operation-contextで使用されているプロジェクトのソースコード。
PPSこのアプローチは、匿名クラスを作成できる他の言語にも適用できると確信しており、これはscalaでの可能な実装にすぎません。