在Play框架中用Guice注入Builder特质所有实现并动态选择
在Play框架中用Guice注入泛型Builder并实现动态选择
我来一步步帮你解决这个问题,核心要处理两个关键点:Guice如何注入所有Builder实现,以及绕过Scala泛型擦除实现动态类型匹配。
1. 改造Builder特质,添加类型判断能力
因为Scala的泛型在运行时会被擦除,直接通过match判断类型没法关联到对应的Builder。我们需要给每个Builder加上运行时的类型标识,这里用ClassTag最方便:
import scala.reflect.ClassTag trait Builder[T, K] { // 每个实现类需要提供对应的ClassTag,用来获取T的运行时类 implicit protected val classTag: ClassTag[T] def build(brick: T): K // 辅助方法:判断当前Builder是否能处理传入的item def canHandle(item: Any): Boolean = classTag.runtimeClass.isInstance(item) }
然后每个Builder实现类要实现classTag:
// 示例:StringBuilder实现 class StringBuilder extends Builder[Foo, String] { override protected val classTag: ClassTag[Foo] = ClassTag(classOf[Foo]) override def build(brick: Foo): String = s"Generated string for Foo: ${brick.toString}" } // HouseBuilder实现 class HouseBuilder extends Builder[Baa, House] { override protected val classTag: ClassTag[Baa] = ClassTag(classOf[Baa]) override def build(brick: Baa): House = House(brick.size) } // BaaBuilder实现 class BaaBuilder extends Builder[Baz, Int] { override protected val classTag: ClassTag[Baz] = ClassTag(classOf[Baz]) override def build(brick: Baz): Int = baz.value }
2. 用Guice注入所有Builder实现
Play框架默认用Guice做依赖注入,有两种方式注入同一特质的所有实现:
方式一:自动扫描(推荐)
只要你的Builder类是无参构造或者已经被Guice管理(比如加了@Singleton),Guice会自动收集所有Builder[_, _]的实现,直接在BuildingComponent里注入Set[Builder[_, _]]即可:
import javax.inject.Inject import scala.util.Try class BuildingComponent @Inject()(private val builders: Set[Builder[_, _]]) { def doIt(item: Any): Try[Any] = { // 找到第一个能处理该item的Builder builders.find(_.canHandle(item)) match { case Some(builder) => // 用@unchecked避免类型检查警告(我们已经通过canHandle确认类型匹配) Try(builder.asInstanceOf[Builder[Any, Any]].build(item)) case None => Try(throw new IllegalArgumentException(s"No builder available for item type: ${item.getClass.getName}")) } } }
方式二:显式绑定(Multibinder)
如果有些Builder没有被自动扫描到,或者你想更明确地控制绑定,可以用Guice的Multibinder在自定义Module里显式绑定所有实现:
首先创建一个Module类:
import com.google.inject.AbstractModule import com.google.inject.multibindings.Multibinder class AppModule extends AbstractModule { override def configure(): Unit = { // 创建Builder集合的绑定器 val builderBinder = Multibinder.newSetBinder(binder(), classOf[Builder[_, _]]) // 添加每个Builder的绑定 builderBinder.addBinding().to(classOf[StringBuilder]) builderBinder.addBinding().to(classOf[HouseBuilder]) builderBinder.addBinding().to(classOf[BaaBuilder]) } }
然后在conf/application.conf里启用这个Module:
play.modules.enabled += "your.package.path.AppModule"
3. 调用示例
你可以在Controller或者其他服务中注入BuildingComponent,然后调用doIt方法:
import javax.inject.Inject import play.mvc.Controller class BuildController @Inject()(buildingComponent: BuildingComponent) extends Controller { def buildItem() = Action { implicit request => val foo = Foo("test") val result = buildingComponent.doIt(foo) Ok(result.getOrElse("Build failed")) } }
注意事项
- 如果你需要更严格的类型安全,可以考虑用
Either或者自定义的结果类型替代Try[Any],避免返回Any类型。 - 如果有多个Builder能处理同一类型(虽然你的场景里应该不会),
find会返回第一个匹配的,你可以根据需求调整逻辑(比如抛出异常或者返回所有匹配结果)。
内容的提问来源于stack exchange,提问作者callidusTaurus
相关产品推荐
相关产品推荐

