基于Scala宏为外部Java API自动生成流畅构建器可行吗?
This is a classic use case for Scala's metaprogramming tools—you don't need to modify the external Java POJOs at all. We can generate the fluent withXXXX builder methods at compile time using macros (for Scala 2) or Scala 3's built-in compile-time operations. Let's walk through the best approaches:
Scala 2: Macro Annotation + Implicit Value Class
Scala 2's macro annotations let us generate code during compilation. We'll create an annotation that scans the target Java POJO's void-returning setXXXX methods and generates corresponding withXXXX methods in an implicit value class (to avoid runtime overhead).
Step 1: Define the Macro Annotation
import scala.annotation.{StaticAnnotation, compileTimeOnly} import scala.language.experimental.macros import scala.reflect.macros.whitebox @compileTimeOnly("Enable macro paradise to expand this annotation") class WithBuilder[T] extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro WithBuilderMacro.impl } object WithBuilderMacro { def impl(c: whitebox.Context)(annottees: c.Tree*): c.Tree = { import c.universe._ annottees.head match { // Target an implicit value class wrapping our Java POJO case q"implicit class $className($param: $tpe) extends AnyVal { ..$body }" => val targetType = tpe match { case tq"$t" => t } // Extract all void-returning setXXXX methods from the POJO val setMethods = c.typecheck(tq"$targetType", mode = c.TYPEmode).tpe.decls .filter(_.isMethod) .map(_.asMethod) .filter(m => m.returnType =:= typeOf[Unit]) .filter(m => m.name.startsWith("set") && m.paramLists.nonEmpty) // Generate withXXXX methods for each set method val withMethods = setMethods.map { m => val withName = TermName(m.name.toString.replaceFirst("^set", "with")) val paramName = m.paramLists.head.head.name val paramType = m.paramLists.head.head.typeSignature q""" def $withName($paramName: $paramType): $targetType = { $param.${m.name}($paramName) $param } """ } // Assemble the final implicit class with all generated methods q""" implicit class $className($param: $targetType) extends AnyVal { ..$withMethods } """ case _ => c.abort(c.enclosingPosition, "Annotation only works with implicit value classes wrapping the target Java POJO") } } }
Step 2: Use the Annotation
For a Java POJO like this:
public class User { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } }
Apply the annotation to an empty implicit class, and you're ready to use fluent builders:
@WithBuilder[User] implicit class UserOps(user: User) extends AnyVal // Usage: val alice = new User().withName("Alice").withAge(30)
Scala 3: Inline Extensions + Compile-Time Reflection
Scala 3 simplifies metaprogramming with inline methods and compile-time APIs, no need for macro paradise. We can generate extension methods directly at compile time.
Implementation
import scala.quoted.* object BuilderExtensions { inline def generate[T]: Unit = ${ generateImpl[T] } private def generateImpl[T: Type](using Quotes): Expr[Unit] = { import quotes.reflect.* val targetType = TypeRepr.of[T] // Find all void-returning setXXXX methods val setMethods = targetType.declaredMethods .filter(m => m.returnType =:= TypeRepr.of[Unit]) .filter(m => m.name.startsWith("set") && m.paramSymss.nonEmpty) // Generate extension methods for each set method val extensions = setMethods.map { m => val withName = m.name.replaceFirst("^set", "with") val paramSym = m.paramSymss.head.head val paramType = paramSym.tree.tpe val paramName = paramSym.name '{ extension (obj: T) def ${Expr(TermName(withName))}(${Expr(Ident(paramName))}: ${Expr(paramType)}): T = { obj.${Expr(m.name)}(${Expr(Ident(paramName))}) obj } } } // Combine all extensions into a single expression Expr.ofTupleFromSeq(extensions).asExprOf[Unit] } }
Usage
// Generate extensions for the User class BuilderExtensions.generate[User] // Now you can use fluent builders directly: val bob = new User().withName("Bob").withAge(25)
Alternative: Runtime Dynamic Builder (No Macros)
If you don't want to use macros, you can use Scala's Dynamic trait for a runtime-based solution. Note that this lacks compile-time type safety:
class FluentBuilder[T](private val obj: T) extends Dynamic { def selectDynamic(methodName: String): Any = { if (methodName.startsWith("with")) { val setMethodName = "set" + methodName.substring(4) (param: Any) => { obj.getClass.getMethod(setMethodName, param.getClass).invoke(obj, param) this } } else throw new IllegalArgumentException(s"Unsupported method: $methodName") } def build: T = obj } // Usage: val charlie = new FluentBuilder(new User()).withName("Charlie").withAge(35).build
Final Recommendation
For type safety and no runtime overhead, go with the macro approach (Scala 2) or Scala 3's compile-time extensions. These solutions generate the withXXXX methods during compilation, so you get full IDE support and error checking just like with hand-written code.
内容的提问来源于stack exchange,提问作者pathikrit

