Lagom中的依赖注入
当用Lagom构建服务时,你的代码将依赖于Lagom api和其他需要在运行时通过具体实现来满足的服务。通常,特定的实现在开发、测试和生产环境之间会有所不同,所以不要将代码与具体的实现类紧密耦合是很重要的。一个类可以用依赖项的抽象类型来声明构造函数参数——通常用Scala中的traits来表示——允许在构造类时提供具体的实现。这种模式被称为“依赖注入”,是Lagom应用程序组装方式的基础。阅读“Scala中使用MacWire的依赖注入”来了解更多关于这种模式及其好处的背景知识。
服务的依赖项将依次有它们自己对其他API的依赖项。所有这些都形成了一个依赖关系图,在应用程序启动时必须构造它。这个过程被称为“wiring”应用程序,通过创建包含初始化代码的LagomApplication的子类来执行。
相互依赖的类集群共同组成一个更大的逻辑组件是很常见的。以反映这些分组的方式对连接代码进行模块化是很有用的。在Scala中,你可以定义一个“组件”特征,其中包含一系列惰性val声明,这些声明实例化了组件提供的接口的具体实现。它们的一些构造函数参数可以声明为抽象方法,指示必须由另一个组件或应用程序本身提供的依赖关系。然后,您的应用程序可以扩展多个组件,将它们混合到一个完全组装的依赖关系图中。如果您没有满足服务中包含的组件声明的所有要求,那么它将无法编译。
在《Scala依赖注入指南》中,这种将组件混合在一起形成应用程序的方法被称为“薄蛋糕模式”。该指南还介绍了我们将在接下来的步骤中使用的 Macwire 。
组装Lagom应用
在服务描述符和实现服务一节中,我们创建了一个服务及其实现,现在我们想要绑定它们并使用它们创建一个Lagom服务。Lagom的Scala API构建在Play框架之上,并使用Play的编译时依赖注入支持来组装一个Lagom应用程序。Play的编译时依赖注入支持是基于我们刚才描述的“薄蛋糕”模式的。
创建应用程序后,您将使用 Lagom 提供的组件并从中获得依赖项(例如,一个 Akka Actor System 或一个 CassandraSession 用于读端处理器访问数据库)。虽然严格来说并不必要,但是我们建议您使用 Macwire 来帮助将依赖关系连接到代码中。Macwire 提供了一些非常轻量级的宏,它们可以定位您希望创建组件的依赖项,这样您就不必自己手动将它们连接在一起。可以通过添加以下依赖项来使 Macwire 添加到您的服务中:
libraryDependencies += "com.softwaremill.macwire" %% "macros" % "2.2.5" % "provided"
构建应用并将代码连接到其中的最简单方法是创建一个扩展LagomApplication
的抽象类:
import com.lightbend.lagom.scaladsl.server._
import play.api.libs.ws.ahc.AhcWSComponents
import com.softwaremill.macwire._
abstract class HelloApplication(context: LagomApplicationContext)
extends LagomApplication(context)
with AhcWSComponents {
override lazy val lagomServer = serverFor[HelloService](wire[HelloServiceImpl])
}
在这个示例中,HelloApplication
使用蛋糕模式混合了 AhcWSComponents,并使用 Macwire 实现了lazy val lagomServer
。这里重要方法是 lagomServer
方法。Lagom 将使用它来发现您的服务绑定,并创建一个 Play 路由来处理您的服务调用。您可以看到,我们已经将一个服务描述符 HelloService
绑定到了 HelloServiceImpl
实现。您绑定的服务描述符的名称将用作服务名称,该名称在跨服务通信中用于标识发出请求的客户机。
我们使用 Macwire 的 wire 宏将依赖关系连接到 HelloServiceImpl
中——目前我们的服务实际上没有依赖关系,因此我们可以自己手动构建它,但是真正的服务实现不太可能没有依赖关系。HelloApplication
是一个抽象类,原因是仍然有一个方法尚未实现,那就是 serviceLocator
方法。HelloApplication
扩展了 LagomApplication ,它需要一个serviceLocator: serviceLocator
、一个 lagomServer: lagomServer
和一个 wcClient: WSClient
。我们在 AhcWSComponents
中提供 wcClient: WSClient
混合,并以编程方式提供 lagomServer: lagomServer
。一个典型的应用程序将在不同的环境中使用不同的服务定位器,在开发过程中,它将使用 Lagom 开发环境提供的服务定位器,而在生产环境中,它将使用适合您的生产环境的任何东西,例如 Akka Discovery Service Locator 提供的服务定位器实现。因此,我们的主要应用程序蛋糕保留了这个方法的抽象,这样它就可以根据应用程序加载时它处于哪种模式来混合使用正确的方法。
创建了应用程序之后,现在可以编写应用程序的加载程序了。Play 加载应用程序的机制是为应用程序提供一个应用程序加载程序。Play 将向此加载程序传递一些上下文信息,例如类加载程序、运行模式和任何额外的配置,以便应用程序可以自启动。Lagom 提供了一个方便的实现机制,LagomApplicationLoader:
import com.lightbend.lagom.scaladsl.server._
import com.lightbend.lagom.scaladsl.api.ServiceLocator
import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents
class HelloApplicationLoader extends LagomApplicationLoader {
override def loadDevMode(context: LagomApplicationContext) =
new HelloApplication(context) with LagomDevModeComponents
override def load(context: LagomApplicationContext) =
new HelloApplication(context) {
override def serviceLocator = ServiceLocator.NoServiceLocator
}
override def describeService = Some(readDescriptor[HelloService])
}
加载程序有两个必须实现的方法: load
和 loadDevMode
。你可以看到,我们为每个方法混合了不同的服务定位器,我们混合了 LagomDevModeComponents,它提供了开发模式服务定位器,并在dev模式下注册服务,现在在生产模式下,我们现在简单地提供了 NoServiceLocator
作为服务定位器——这是一个对于每次查找都不返回任何内容的服务定位器。我们将在部署到生产环境的文档中看到如何为生产选择正确的服务定位器。
第三个方法是 describeService
,它是可选的,但是工具可以使用它来发现由该服务提供的服务api。从这里读取的元数据可以反过来用于配置服务网关和其他组件。
最后,我们需要告诉Play关于应用程序加载器的信息。我们可以通过在 application.conf
中添加以下配置来实现:
play.application.loader = com.example.HelloApplicationLoader
自定义组件
创建一个复杂的应用程序(一个使用持久性、集群、代理API等的应用程序),需要混合许多组件。创建小的自定义特性,混合在其中的一些组件中,并通过混合这些自定义特性来构建应用程序,这是一个很好的选择。这将允许您单独测试完整应用程序的各个部分。
假设您的服务使用来自Broker主题的消息,该主题通知Orders
。然后您的服务将这些信息存储到数据库中,并进行一些处理,最后一步是调用第三方端点。如果您只想测试消息的消费和适当的存储,您可以创建一个OrderConsumingComponent
特质,并混合使用LagomServiceClientComponents
和CassandraPersistenceComponents
,这样您就可以消费消息并存储它们。在测试中,您可以使用TestTopicComponents
扩展OrderConsumingComponent
,它提供一个模拟代理,这样您就不需要启动代理来运行测试。最后,在你的应用程序中,你可以混合使用测试过的OrderConsumingComponent
和LagomKafkaClientComponents
。
Lagom 提供了一些组件。在这个参考指南中,我们按照特性对组件进行了分组: