Scala如何自动生成移除首参数的派生Client trait及实现?
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

