You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Scala如何自动生成移除首参数的派生Client trait及实现?

Autogenerating Authenticated Client Traits in Scala

Absolutely, there are several idiomatic Scala tools and libraries that can help you auto-generate these authenticated client traits and implementations without manual duplication. Here are the most practical approaches tailored to your use case:

1. Compile-Time Code Generation with Scala Meta

Scala Meta is a powerful metaprogramming library that lets you parse, manipulate, and generate Scala code at compile time. It’s ideal for this scenario because it ensures type safety and produces clean, maintainable generated code.

Step 1: Define a Marker Annotation

First, create an annotation to mark client traits that need an authenticated variant:

import scala.annotation.StaticAnnotation

// Marker annotation to trigger generation of authenticated client
class GenerateAuthenticatedClient extends StaticAnnotation

Step 2: Annotate Your Original Client

Mark your FooClient trait with the annotation:

@GenerateAuthenticatedClient
trait FooClient {
  def doSomething(authToken: AuthNToken, request: MyBarRequest): Future[Unit]
  def getSomething(authToken: AuthNToken, id: MyId): Future[MyResource]
}

Step 3: Write the Annotation Processor

Use Scala Meta to write an annotation processor that scans for @GenerateAuthenticatedClient, parses the trait’s methods, and generates the authenticated trait + implementation. Here’s a simplified sketch of the logic:

import scala.meta._

// Processor logic (would be packaged as an sbt plugin or macro)
def generateAuthenticatedClient(traitDef: Def.Trait): List[Source] = {
  // Extract trait name (e.g., FooClient → AuthenticatedFooClient)
  val traitName = traitDef.name.value
  val authenticatedTraitName = Type.Name(s"Authenticated$traitName")
  val implClassName = Type.Name(s"Authenticated${traitName}Impl")

  // Transform each method: remove first parameter (authToken)
  val transformedMethods = traitDef.stats.collect {
    defn: Def.Def =>
      val newParams = defn.paramss.head.tail // Remove first parameter list's first arg
      defn.copy(paramss = List(newParams))
  }

  // Generate the authenticated trait
  val authenticatedTrait =
    s"""
       |trait $authenticatedTraitName {
       |  ${transformedMethods.map(_.syntax).mkString("\n  ")}
       |}
       |""".stripMargin.parse[Source].get

  // Generate the implementation class
  val implClass =
    s"""
       |class $implClassName(authToken: AuthNToken, delegate: $traitName) extends $authenticatedTraitName {
       |  ${transformedMethods.map { m =>
         val methodName = m.name.value
         val args = m.paramss.head.map(_.name.value).mkString(", ")
         s"override def $methodName($args): ${m.returnType.syntax} = delegate.$methodName(authToken, $args)"
       }.mkString("\n  ")}
       |}
       |""".stripMargin.parse[Source].get

  List(authenticatedTrait, implClass)
}

This processor will generate exactly the AuthenticatedFooClient trait and AuthenticatedFooClientImpl class you described, with full compile-time type checking.

2. Lightweight sbt Code Generation

If you don’t want to dive into metaprogramming, a custom sbt code generation task is a straightforward alternative. It reads your source files, parses method signatures, and writes the generated code to managed sources.

Configure the sbt Task

Add this to your build.sbt:

lazy val generateAuthenticatedClients = taskKey[Seq[File]]("Generate authenticated client variants")

generateAuthenticatedClients := {
  val sourceDir = (sourceDirectory in Compile).value / "scala" / "com" / "yourcompany" / "clients"
  val outputDir = (sourceManaged in Compile).value / "scala" / "com" / "yourcompany" / "clients"
  IO.createDirectory(outputDir)

  // Scan for client traits (adjust the filter as needed)
  val clientFiles = sourceDir.listFiles().filter(f => f.getName.endsWith("Client.scala") && !f.getName.startsWith("Authenticated"))

  clientFiles.flatMap { file =>
    val source = IO.read(file)
    val traitName = file.getName.replace(".scala", "")
    val authenticatedTraitName = s"Authenticated$traitName"
    val implClassName = s"Authenticated${traitName}Impl"

    // Extract method signatures (simplified regex-based parsing; use Scala Meta for robustness)
    val methodRegex = """def (\w+)\(authToken: AuthNToken, (.+)\): (.+)""".r
    val methods = methodRegex.findAllMatchIn(source).map { m =>
      val name = m.group(1)
      val params = m.group(2)
      val returnType = m.group(3)
      (name, params, returnType)
    }.toList

    // Generate trait code
    val traitCode =
      s"""package com.yourcompany.clients
         |
         |trait $authenticatedTraitName {
         |  ${methods.map { case (name, params, rt) => s"def $name($params): $rt" }.mkString("\n  ")}
         |}
         |""".stripMargin

    // Generate implementation code
    val implCode =
      s"""package com.yourcompany.clients
         |
         |class $implClassName(authToken: AuthNToken, delegate: $traitName) extends $authenticatedTraitName {
         |  ${methods.map { case (name, params, rt) => s"override def $name($params): $rt = delegate.$name(authToken, $params)" }.mkString("\n  ")}
         |}
         |""".stripMargin

    // Write files
    val traitFile = outputDir / s"$authenticatedTraitName.scala"
    val implFile = outputDir / s"$implClassName.scala"
    IO.write(traitFile, traitCode)
    IO.write(implFile, implCode)

    Seq(traitFile, implFile)
  }
}

// Ensure generation runs before compilation
compile in Compile := (compile in Compile).dependsOn(generateAuthenticatedClients).value

This approach is easy to set up and debug, though regex-based parsing is less robust than AST-based parsing (swap in Scala Meta for parsing if you need better reliability).

3. Runtime Dynamic Proxy (No Code Generation)

If you don’t need physical generated files, you can use dynamic proxies to wrap your clients at runtime, automatically injecting the authToken into every method call. ByteBuddy is a great library for this.

Add ByteBuddy Dependency

libraryDependencies += "net.bytebuddy" % "byte-buddy" % "1.14.11"

Create a Proxy Factory

import net.bytebuddy.ByteBuddy
import net.bytebuddy.implementation.MethodDelegation
import net.bytebuddy.matcher.ElementMatchers._
import scala.reflect.ClassTag

class AuthTokenInterceptor[T](authToken: AuthNToken, delegate: T) {
  // Intercept all method calls and prepend the authToken
  @SuppressWarnings(Array("unchecked"))
  def intercept(@Origin method: java.lang.reflect.Method, @AllArguments args: Array[AnyRef]): AnyRef = {
    val newArgs = Array(authToken.asInstanceOf[AnyRef]) ++ args
    method.invoke(delegate, newArgs: _*)
  }
}

object AuthenticatedClientFactory {
  def create[T: ClassTag](authToken: AuthNToken, delegate: T): T = {
    val clazz = implicitly[ClassTag[T]].runtimeClass
    new ByteBuddy()
      .subclass(clazz)
      .method(isDeclaredBy(clazz))
      .intercept(MethodDelegation.to(new AuthTokenInterceptor(authToken, delegate)))
      .make()
      .load(clazz.getClassLoader)
      .getLoaded
      .newInstance()
      .asInstanceOf[T]
  }
}

Usage Example

val fooClient: FooClient = // your original implementation
val authToken: AuthNToken = // get token from context
val authenticatedFooClient: FooClient = AuthenticatedClientFactory.create(authToken, fooClient)

// Now you can call methods without passing authToken
authenticatedFooClient.doSomething(MyBarRequest(...))
authenticatedFooClient.getSomething(MyId(...))

This approach is fast to implement but lacks compile-time type safety (e.g., if a method doesn’t have AuthNToken as the first parameter, it’ll fail at runtime). It’s best for simple, controlled environments.

Recommendation

  • For full type safety and maintainability: Use Scala Meta (compile-time generation).
  • For simplicity without metaprogramming: Use the sbt code generation task.
  • For quick prototyping or runtime flexibility: Use ByteBuddy dynamic proxies.

内容的提问来源于stack exchange,提问作者Giulio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:15:13