🎮 Play 入门与学习(五) Dependency Injection

依赖注入是一种广泛使用的设计模式,有助于将组件的行为与依赖性解析分开。Play支持基于JSR 330(在本页中描述)的运行时依赖注入和在Scala中编译时依赖注入。
之所以调用运行时依赖项注入,是因为依赖关系图是在运行时创建,连接和验证的。 如果找不到特定组件的依赖项,则在运行应用程序之前不会出现错误。

Play支持Guice开箱即用,但可以插入其他JSR 330实现. Guice wiki 是一个很好的资源,可以更多地了解Guice和DI设计模式的功能。

注意:Guice是一个Java库,本文档中的示例使用Guice的内置Java API。 如果您更喜欢Scala DSL,您可能希望使用scala-guice或sse-guice库。

Motivation

依赖注入实现了几个目标:

  • 它允许您轻松绑定同一组件的不同实现。 这对于测试尤其有用,您可以使用模拟依赖项手动实例化组件或注入备用实现。
  • 它允许您避免全局静态。 虽然静态工厂可以实现第一个目标,但您必须小心确保正确设置状态。 特别是Play(现已弃用)的静态API需要运行的应用程序,这使得测试的灵活性降低。 并且一次有多个实例可以并行运行测试。

Guice wiki 有一些很好的例子可以更详细地解释这一点。

How it works

Play 提供了许多内置组件,并在诸如 BuiltinModule 之类的模块中声明它们。 这些绑定描述了创建 Application 实例所需的所有内容,默认情况下包括由 routes compiler 生成的 route,该 route 将控制器注入到构造函数中。 然后可以将这些绑定转换为在 Guice 和其他运行时DI框架中工作。

Play团队维护Guice模块,该模块提供 GuiceApplicationLoader。 这为 Guice 进行绑定转换,使用这些绑定创建Guice注入器,并从注入器请求Application实例。

There are also third-party loaders that do this for other frameworks, including Scaldi and Spring.

或者,Play提供了一个 BuiltInComponents 特性,允许您创建一个纯Scala实现,在编译时将您的应用程序连接在一起。

我们将在下面详细介绍如何自定义默认绑定和应用程序加载器。

Declaring runtime DI dependencies

声明运行时DI依赖项

如果您有一个组件(例如控制器),并且它需要一些其他组件作为依赖项,那么可以使用 @Inject 注解来声明它。 @Inject 注解可用于字段或构造函数。 我们建议您在构造函数上使用它,例如:

1
2
3
4
5
6
import javax.inject._
import play.api.libs.ws._

class MyComponent @Inject() (ws: WSClient) {
// ...
}

请注意,@Inject 注释必须位于类名之后但在构造函数参数之前,并且必须具有括号。

此外,Guice 确实提供了其他几种类型的注入方式,但构造函数注入通常是 Scala 中最清晰,简洁和可测试的,因此我们建议使用它。

Guice 能够在其构造函数上使用 @Inject 自动实例化任何类,而无需显式绑定它。此功能被称为 just in time bindingsGuice 文档中对此进行了更详细的描述。 如果您需要执行更复杂的操作,可以声明自定义绑定,如下所述。

Dependency injecting controllers

依赖注入控制器

There are two ways to make Play use dependency injected controllers.

有两种方法可以使 Play 使用依赖注入控制器。

Injected routes generator

默认情况下(从2.5.0开始),Play 将生成一个 router ,该 router 将声明它所路由的所有控制器作为依赖项,允许您的控制器自己依赖注入。

要专门启用注入的路由生成器,请将以下内容添加到 build.sbt 中的构建设置:

1
routesGenerator := InjectedRoutesGenerator

当使用 injected routes generator 时,为操作添加 @ 符号前缀具有特殊含义,这意味着不是直接注入控制器,而是注入控制器的提供者。 例如,这允许 prototype controllers,以及用于打破循环依赖性的选项。

Static routes generator

您可以将Play配置为使用 legacy(pre 2.5.0)静态路由生成器,该生成器假定所有操作都是静态方法。 要配置项目,请将以下内容添加到 build.sbt

1
routesGenerator := StaticRoutesGenerator

我们建议始终使用注入的路由生成器。 静态路由生成器主要作为辅助迁移的工具存在,因此现有项目不必一次使所有控制器不是静态的。

如果使用静态路由生成器,则可以通过在操作前加上@来指示操作具有注入的控制器,如下所示:

1
GET        /some/path           @controllers.Application.index

Component lifecycle

依赖注入系统管理注入组件的生命周期,根据需要创建它们并将它们注入其他组件。以下是组件生命周期的工作原理:

  • 每次需要组件时都会创建新实例。如果组件多次使用,则默认情况下将创建组件的多个实例。如果您只需要组件的单个实例,则需要将其标记为单个实例。
  • 实例在需要时会以懒加载的形式创建。如果组件从未被其他组件使用,则根本不会创建它。这通常是你想要的。对于大多数组件而言,在需要之前创建它们是没有意义的。但是,在某些情况下,您希望直接启动组件,或者即使它们未被其他组件使用也是如此。例如,您可能希望在应用程序启动时向远程系统发送消息或预热缓存。您可以使用急切绑定 (eager binding) 强制创建组件。
  • 除了正常的垃圾收集之外,实例不会自动清理。当组件不再被引用时,它们将被垃圾收集,但框架将不会执行任何特殊操作来关闭组件,例如调用 close 方法。但是,Play 提供了一种特殊类型的组件,称为ApplicationLifecycle,它允许您注册组件以在应用程序停止时关闭。

Singletons

有时,您可能拥有一个包含某些状态的组件,例如缓存,或者与外部资源的连接,或者创建组件可能很昂贵。 在这些情况下,该组件应该是单例的。 这可以使用 @Singleton 注解来实现:

1
2
3
4
5
6
7
8
9
import javax.inject._

@Singleton
class CurrentSharePrice {
@volatile private var price = 0

def set(p: Int) = price = p
def get = price
}

Stopping/cleaning up

Play 关闭时可能需要清理某些组件,例如,停止线程池。 Play提供了一个 ApplicationLifecycle 组件,可用于在 Play 关闭时注册挂钩以停止组件

1
2
3
4
5
6
7
8
9
10
11
12
13
import scala.concurrent.Future
import javax.inject._
import play.api.inject.ApplicationLifecycle

@Singleton
class MessageQueueConnection @Inject() (lifecycle: ApplicationLifecycle) {
val connection = connectToMessageQueue()
lifecycle.addStopHook { () =>
Future.successful(connection.stop())
}

//...
}

ApplicationLifecycle 将在创建时以相反的顺序停止所有组件。 这意味着您依赖的任何组件仍然可以安全地用在组件的停止钩子上。 因为您依赖它们,所以它们必须在组件之前创建,因此在组件停止之前不会停止。

注意:确保注册 stop hook 的所有组件都是单例非常重要。 注册 stop 钩子的任何非单例组件都可能是内存泄漏的来源,因为每次创建组件时都会注册一个新的钩子。

Providing custom bindings

提供自定义绑定

为组件定义 trait 被认为是一种好习惯,并且其他类依赖于该 trait,而不是组件的实现。 通过这样做,您可以注入不同的实现,例如,在测试应用程序时注入模拟实现。

在这种情况下,DI系统需要知道哪个实现应该绑定到该 trait。 我们建议您声明这一点的方式取决于您是将 Play 应用程序编写为 Play 的最终用户,还是编写其他 Play 应用程序将使用的库。

Play applications

我们建议 Play 应用程序使用应用程序正在使用的 DI 框架提供的任何机制。 尽管Play确实提供了绑定API,但此API有些限制,并且不允许您充分利用您正在使用的框架的强大功能。

由于Play为Guice提供了开箱即用的支持,下面的示例显示了如何为 Guice 提供绑定。

Binding annotations

将实现绑定到接口的最简单方法是使用 Guice @ImplementedBy 注释。 例如:

1
2
3
4
5
6
7
8
9
10
import com.google.inject.ImplementedBy

@ImplementedBy(classOf[EnglishHello])
trait Hello {
def sayHello(name: String): String
}

class EnglishHello extends Hello {
def sayHello(name: String) = "Hello " + name
}

Programmatic bindings

在一些更复杂的情况下,您可能希望提供更复杂的绑定,例如当您有一个 trait 的多个实现时,这些 trait@Named 注释限定。 在这些情况下,您可以实现自定义 Guice 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.google.inject.AbstractModule
import com.google.inject.name.Names

class Module extends AbstractModule {
def configure() = {

bind(classOf[Hello])
.annotatedWith(Names.named("en"))
.to(classOf[EnglishHello])

bind(classOf[Hello])
.annotatedWith(Names.named("de"))
.to(classOf[GermanHello])
}
}

如果您将此模块称为 Module 并将其放在根包中,它将自动注册到 Play。 或者,如果要为其指定不同的名称或将其放在不同的包中,可以通过将其全限定名附加到 application.conf 中的 play.modules.enabled 列表来将其注册到 Play

1
play.modules.enabled += "modules.HelloModule"

您还可以通过将以下模块添加到已禁用的模块来禁用根软件包中名为 Module 的模块的自动注册

1
play.modules.disabled += "Module"

Configurable bindings

可配置的绑定

有时您可能希望在配置 Guice 绑定时读取 Play Configuration 或使用 ClassLoader。 您可以通过将这些对象添加到模块的构造函数来访问这些对象。

在下面的示例中,从配置文件中读取每种语言的 Hello 绑定。 这允许通过在 application.conf 文件中添加新设置来添加新的 Hello 绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.google.inject.AbstractModule
import com.google.inject.name.Names
import play.api.{ Configuration, Environment }

class Module(environment: Environment,configuration: Configuration)
extends AbstractModule {
def configure() = {
// Expect configuration like:
// hello.en = "myapp.EnglishHello"
// hello.de = "myapp.GermanHello"
val helloConfiguration: Configuration =
configuration.getOptional[Configuration]("hello").getOrElse(Configuration.empty)
val languages: Set[String] = helloConfiguration.subKeys
// Iterate through all the languages and bind the
// class associated with that language. Use Play's
// ClassLoader to load the classes.
for (l <- languages) {
val bindingClassName: String = helloConfiguration.get[String](l)
val bindingClass: Class[_ <: Hello] =
environment.classLoader.loadClass(bindingClassName)
.asSubclass(classOf[Hello])
bind(classOf[Hello])
.annotatedWith(Names.named(l))
.to(bindingClass)
}
}
}

注意:在大多数情况下,如果在创建组件时需要访问 Configuration,则应将 Configuration 对象注入组件本身或组件的 Provider 中。 然后,您可以在创建组件时读取“配置”。 在为组件创建绑定时,通常不需要读取Configuration

Eager bindings

提前绑定

在上面的代码中,每次使用时都会创建新的 EnglishHelloGermanHello 对象。 如果您只想创建一次这些对象,可能因为创建它们很昂贵,那么您应该使用 @Singleton 注释。 如果你想创建它们一次并在应用程序启动时急切地创建它们,而不是在需要它们懒加载,那么你就可以使用 Guice’s eager singleton binding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.google.inject.AbstractModule
import com.google.inject.name.Names

// A Module is needed to register bindings
class Module extends AbstractModule {
override def configure() = {

// Bind the `Hello` interface to the `EnglishHello` implementation as eager singleton.
bind(classOf[Hello])
.annotatedWith(Names.named("en"))
.to(classOf[EnglishHello]).asEagerSingleton()

bind(classOf[Hello])
.annotatedWith(Names.named("de"))
.to(classOf[GermanHello]).asEagerSingleton()
}
}

当应用程序启动时,可以使用 Eager 单例来启动服务。 它们通常与关闭钩子组合在一起,以便服务可以在应用程序停止时清理其资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
import scala.concurrent.Future
import javax.inject._
import play.api.inject.ApplicationLifecycle

// This creates an `ApplicationStart` object once at start-up and registers hook for shut-down.
@Singleton
class ApplicationStart @Inject() (lifecycle: ApplicationLifecycle) {
// Shut-down hook
lifecycle.addStopHook { () =>
Future.successful(())
}
//...
}
1
2
3
4
5
6
7
import com.google.inject.AbstractModule

class StartModule extends AbstractModule {
override def configure() = {
bind(classOf[ApplicationStart]).asEagerSingleton()
}
}

Play libraries

如果您正在为Play实现一个库,那么您可能希望它与DI框架无关,这样无论在应用程序中使用哪个DI框架,您的库都可以开箱即用。 出于这个原因,Play提供了一个轻量级绑定API,用于以DI框架无关的方式提供绑定。

要提供绑定,请实现Module以返回要提供的绑定序列。 Module trait还提供用于构建绑定的DSL:

1
2
3
4
5
6
7
8
9
import play.api.inject._

class HelloModule extends Module {
def bindings(environment: Environment,
configuration: Configuration) = Seq(
bind[Hello].qualifiedWith("en").to[EnglishHello],
bind[Hello].qualifiedWith("de").to[GermanHello]
)
}

通过将该模块附加到reference.conf中的play.modules.enabled列表,可以自动注册该模块:

1
play.modules.enabled += "com.example.HelloModule"

Module bindings方法采用Play环境和配置。 如果要动态配置绑定,可以访问这些。
模块绑定支持急切绑定。 要声明一个急切绑定,请在绑定结束时添加.eagerly。

为了最大化跨框架兼容性,请记住以下事项:

  • 并非所有DI框架都只支持时间绑定。 确保明确绑定库提供的所有组件。
  • 尝试保持绑定键简单 - 不同的运行时DI框架对键是什么以及它应该如何唯一或不唯一有不同的看

Excluding modules

如果有一个您不想加载的模块,可以通过将其附加到application.conf中的play.modules.disabled属性来将其排除:

1
play.modules.disabled += "play.api.db.evolutions.EvolutionsModule"

Managing circular dependencies

当您的某个组件依赖于依赖于原始组件的另一个组件(直接或间接)时,就会发生循环依赖关系。 例如:

1
2
3
4
5
import javax.inject.Inject

class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Foo)

在这种情况下,Foo依赖于Bar,它取决于Baz,它依赖于Foo。 因此,您将无法实例化任何这些类。 您可以使用提供程序解决此问题:

1
2
3
4
5
import javax.inject.{ Inject, Provider }

class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Provider[Foo])

通常,可以通过以更原子的方式分解组件或查找要依赖的更具体的组件来解决循环依赖性。 常见问题是对Application的依赖。 当你的组件依赖于应用程序时,它说它需要一个完整的应用程序来完成它的工作; 通常情况并非如此。 您的依赖项应该位于具有您需要的特定功能的更具体的组件(例如,环境)上。 作为最后的手段,您可以通过注入Provider [Application]来解决问题。

Advanced: Extending the GuiceApplicationLoader

Play的运行时依赖注入由GuiceApplicationLoader类引导。 该类加载所有模块,将模块提供给Guice,然后使用Guice创建应用程序。 如果要控制Guice如何初始化应用程序,则可以扩展GuiceApplicationLoader类。

您可以覆盖几种方法,但通常需要覆盖构建器方法。 此方法读取ApplicationLoader.Context并创建GuiceApplicationBuilder。 您可以在下面看到构建器的标准实现,您可以按照自己喜欢的方式进行更改。 您可以在有关使用Guice进行测试的部分中找到如何使用GuiceApplicationBuilder。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import play.api.ApplicationLoader
import play.api.Configuration
import play.api.inject._
import play.api.inject.guice._

class CustomApplicationLoader extends GuiceApplicationLoader() {
override def builder(context: ApplicationLoader.Context): GuiceApplicationBuilder = {
val extra = Configuration("a" -> 1)
initialBuilder
.in(context.environment)
.loadConfig(extra ++ context.initialConfiguration)
.overrides(overrides(context): _*)
}
}

当您覆盖ApplicationLoader时,您需要告诉Play。 将以下设置添加到application.conf:

play.application.loader =“modules.CustomApplicationLoader”
您不仅限于使用Guice进行依赖注入。 通过重写ApplicationLoader,您可以控制应用程序的初始化方式。 在下一节中了解更多信息。

🎮 Play 入门与学习(三) Asynchronous results

本章我们将讲解使用 Play 进行异步非阻塞编程情况下,如何处理异步返回结果的问题。

Handling asynchronous results

从现在开始我们将进入异步 http 编程模块,本章将会介绍如何处理异步返回结果。

Make controllers asynchronous

使 controllers 变为异步的

Play Framework 内部,其机制是自底向上全异步编程模式的,Play 会以异步、非阻塞的方式处理每个请求。

默认配置(configuration) 针对异步控制器(asynchronous controllers) 进行了优化。换句话说,应用程序代码应该尽量避免在控制器中进行阻塞,这样会导致控制器一直等待那个阻塞的操作。此类阻塞操作的常见示例有JDBC调用、流API、HTTP请求和长计算等。

虽然可以增加默认 executionContext 中线程的数量,以允许阻塞控制器处理更多的并发请求,但是遵循建议的保持控制器异步的方法可以更容易地进行扩展,并在负载下保持系统响应。

Creating non-blocking actions

创建非阻塞的 actions

由于 Play 的工作方式,action 代码必须尽可能快,即非阻塞。那么,如果我们在还没有生成结果情形下,如何返回结果呢?答案是使用 Future

A Future[Result] will eventually be redeemed with a value of type Result.

By giving a Future[Result] instead of a normal Result, we are able to quickly generate the result without blocking.

Play will then serve the result as soon as the promise is redeemed.

The web client will be blocked while waiting for the response, but nothing will be blocked on the server, and server resources can be used to serve other clients.

Using a Future is only half of the picture though!

然而,使用Future只是这幅画的一半. (使用 Future 只是 Play 异步编程的一半)

If you are calling out to a blocking API such as JDBC,then you still will need to have your ExecutionStage(执行阶段) run with a different executor, to move it off Play’s rendering thread pool.

You can do this by creating a subclass of play.api.libs.concurrent.CustomExecutionContextwith a reference to the custom dispatcher.

这里意思是如果针对长时间阻塞的任务,比如JDBC,像上述方式操作,我们只不过是把当前任务的执行放到了另外一条线程中继续阻塞了而已,因此仍然是假异步。这时候,我们可以定义自定义的CustomExecutionContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import play.api.libs.concurrent.CustomExecutionContext

//请确保使用"Scala Dependency Injection"文档页面中列出的 custom binding 技术之一
//将 new context 绑定到当前 trait
trait MyExecutionContext extends ExecutionContext

class MyExecutionContextImpl @Inject()(system: ActorSystem)
extends CustomExecutionContext(system, "my.executor") with MyExecutionContext

class HomeController @Inject()(myExecutionContext: MyExecutionContext, val controllerComponents: ControllerComponents) extends BaseController {
def index = Action.async {
Future {
// Call some blocking API
Ok("result of blocking call")
}(myExecutionContext)
}
}

How to create a Future[Result]

要创建一个 Future[Result],我们首先需要另一个future, 这个 future 会给我们返回我们需要计算的实际的结果。

1
2
3
4
val futurePIValue: Future[Double] = computePIAsynchronously()
val futureResult: Future[Result] = futurePIValue.map { pi =>
Ok("PI value computed: " + pi)
}

All of Play’s asynchronous API calls give you a Future. This is the case whether you are calling an external web service using the play.api.libs.WS API, or using Akka to schedule asynchronous tasks or to communicate with actors using play.api.libs.Akka.

PlayFramework 所有异步的 API 返回的结果都是一个 Future,无论你是通过 play.api.libs.WS API 调用一个外部的 web 服务,或者使用Akka调度异步任务,或者使用 play. API .lib .Akkaactor 通信等等,返回都是 Future

下面是一个通过异步模式执行一段阻塞的代码,并返回一个 Future 简单的例子

1
2
3
val futureInt: Future[Int] = scala.concurrent.Future {
intensiveComputation()
}

注意点

It’s important to understand which thread code runs on with futures. In the two code blocks above, there is an import on Plays default execution context. This is an implicit parameter that gets passed to all methods on the future API that accept callbacks. The execution context will often be equivalent to a thread pool, though not necessarily.

理解哪些线程代码在 Future 上运行是很重要的。在上面的两个代码块中,在 Play 默认 executionContext 上有一个导入。这是一个隐式参数,传递给 future API上所有接受回调的方法。executionContext 通常等价于线程池,但也不一定。

You can’t magically turn synchronous IO into asynchronous by wrapping it in a Future. If you can’t change the application’s architecture to avoid blocking operations, at some point that operation will have to be executed, and that thread is going to block. So in addition to enclosing the operation in a Future, it’s necessary to configure it to run in a separate execution context that has been configured with enough threads to deal with the expected concurrency. See Understanding Play thread pools for more information, and download the play example templates that show database integration.

你不能通过将代码块包装在一个 Future下来神奇地把同步IO变成异步的。如果您不能更改应用程序的体系结构以避免阻塞操作,那么在某个时刻,该操作将不得不执行,而该线程将阻塞。

因此,除了将操作封装在 Future 中之外,还需要将其配置为在一个单独的 executionContext 中运行,该上下文中已经配置了足够的线程来处理预期的并发。有关更多信息,请参见了解Play线程池,并下载显示数据库集成的Play示例模板

It can also be helpful to use Actors for blocking operations.

Actors provide a clean model for handling timeouts and failures, setting up blocking execution contexts, and managing any state that may be associated with the service.

Also Actors provide patterns like ScatterGatherFirstCompletedRouter to address simultaneous cache and database requests and allow remote execution on a cluster of backend servers. But an Actor may be overkill depending on what you need.

对于阻塞操作场景,使用 Actors 模式是一个不错的选择。Actors 提供了一个简单一个简单的模型,来处理超时、故障、设置阻塞的 executionContext 以及与服务关联的任何状态。

Actors 还提供了像 “ScatterGatherFirstCompletedRouter” 这样的模式来处理同步缓存和数据库请求,并允许在后端服务器集群上远程执行。但是一个 actor 可能会因为你的需要而 overkill。

Returning futures

返回 futures

While we were using the Action.apply builder method to build actions until now, to send an asynchronous result we need to use the Action.async builder method:

当我们使用 Action.apply 时,将 builder 方法应用于 actions ,到目前为止,要发送异步结果,我们需要使用异步的 Actiton builder 方法

1
2
3
4
def index = Action.async {
val futureInt = scala.concurrent.Future { intensiveComputation() }
futureInt.map(i => Ok("Got result: " + i))
}

Actions are asynchronous by default

默认情况下,Actions 是异步的。例如,在下面的控制器代码中,代码的{Ok(…)} 部分不是控制器的方法体。它是一个匿名函数,被传递给 Action 对象的 apply方法,该方法创建一个 Action 类型的对象。在内部,您编写的匿名函数将被调用,其结果将返回在 Future中。

1
2
3
def echo = Action { request =>
Ok("Got request [" + request + "]")
}

Note: Both Action.apply and Action.async create Action objects that are handled internally in the same way. There is a single kind of Action, which is asynchronous, and not two kinds (a synchronous one and an asynchronous one). The .async builder is just a facility to simplify creating actions based on APIs that return a Future, which makes it easier to write non-blocking code.

注意

Action.applyAction.async 在内部都以相同的方式来处理 Action 对象。

有一种单独的 Action,它是异步的,但是不是a synchronous one and an asynchronous one 中的一种。

.async builder只是一个工具,用于在创建 actions 时基于返回 Future result 的 api 的操作进行简化,这使得编写非阻塞代码更加容易。

Handling time-outs

处理超时情况

正确处理超时,避免 web 浏览器阻塞并在出现问题时等待,这通常很有用。您可以使用 play.api.libs.concurrent.Futures 来将非阻塞的超时包装在一个 Futures

1
2
3
4
5
6
7
8
9
10
11
12
import scala.concurrent.duration._
import play.api.libs.concurrent.Futures._

def index = Action.async {
// 你可以隐式的提供一个超时参数,这题哦你歌唱可以通过controller的构造参数来达到。
intensiveComputation().withTimeout(1.seconds).map { i =>
Ok("Got result: " + i)
}.recover {
case e: scala.concurrent.TimeoutException =>
InternalServerError("timeout")
}
}

注意

超时(Timeout)与取消(cancellation) 是不同的。对于超时而言,即使出现了超时,给定的 Future 仍然会完成,即使未返回已完成的值。

🎮 Play 入门与学习(二) Action Composition

本章将讲解 Action 的组成和原理,并且介绍了几种定义通用 Action 的方法。

Action composition

Action 组成结构

Custom action builders

自定义 action builders
我们有多种方法来声明一个 Action,使用 request 参数,不使用 request 参数, 使用 body parser 等等。实际上,并不止这些,我们将在异步编程一节进行讲述。

这些用于构建 actions 的方法实际上都是由一个名为 ActionBuildertrait 定义的。而我们用来声明 ActionAction Object 只是这个 trait 的一个实例。通过实现自己的 ActionBuilder,您可以声明可重用的 action stack,然后可以使用它们来构建 actions

让我们从日志修饰符(logging decorator) 的简单示例开始,我们希望记录对 action 的每次调用日志。

第一种方法是在 invokeBlock 方法中实现这个功能,ActionBuilder 构建的每一个 action 都会调用这个方法。

1
2
3
4
5
6
7
8
9
import play.api.mvc._

class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) {

override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result])={
Logger.info("Calling action")
block(request)
}
}

现在,我们可以在 controllers 中使用依赖注入来获取 LoggingAction 的一个实例,并且以使用普通 Action 的方式来使用它。

1
2
3
4
5
6
7
class MyController @Inject()(loggingAction: LoggingAction,
cc:ControllerComponents)
extends AbstractController(cc) {
def index = loggingAction {
Ok("Hello World")
}
}

Since ActionBuilder provides all the different methods of building actions, this also works with, for example, declaring a custom body parser:

由于 ActionBuilder 提供了创建 actions 所有不同的方法,所以我们也可以在自定义的 Action 中使用 body parser 等普通 Action 的功能。

1
2
3
def submit = loggingAction(parse.text) { request =>
Ok("Got a body " + request.body.length + " bytes long")
}

组合 actions

Composing actions

在大多数应用程序中,我们希望有多个 action builders, 比如一些做不同类型的 authentication, 一些提供不同类型的通用功能组件,等等。

在这种情况下,我们不想为每一个 action builder 重写 loggingAction,我们需要定义一个可重用的方式来简化代码。可重用的操作代码可以通过包装操作(wrapping actions)来实现

1
2
3
4
5
6
7
8
9
10
11
12
import play.api.mvc._

case class Logging[A](action: Action[A]) extends Action[A] {

def apply(request: Request[A]): Future[Result] = {
Logger.info("Calling action")
action(request)
}

override def parser = action.parser
override def executionContext = action.executionContext
}

We can also use the Action action builder to build actions without defining our own action class:

我们也可以使用 Action action builder 来 创建 actions 而不需要定义我们自己的 action class

1
2
3
4
5
6
import play.api.mvc._

def logging[A](action: Action[A])= Action.async(action.parser) { request =>
Logger.info("Calling action")
action(request)
}

可以使用 composeAction 方法将 Action 混合到 action builders

1
2
3
4
5
6
class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
block(request)
}
override def composeAction[A](action: Action[A]) = new Logging(action)
}

通过这样 code 后使用,效果和之前的例子一样

1
2
3
def index = loggingAction {
Ok("Hello World")
}

我们也可以在不使用 action builder 的情况下将 action 混合到 wrapping actions

1
2
3
4
5
def index = Logging {
Action {
Ok("Hello World")
}
}

More complicated actions

更复杂的 Action

到目前为止,我们只展示了完全不会影响请求的 actions。当然,我们也可以对传入的请求对象进行读取和修改。

1
2
3
4
5
6
7
8
9
10
11
12
import play.api.mvc._
import play.api.mvc.request.RemoteConnection

def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
val newRequest = request.headers.get("X-Forwarded-For") match {
case None => request
case Some(xff) =>
val xffConnection = RemoteConnection(xff, request.connection.secure, None)
request.withConnection(xffConnection)
}
action(newRequest)
}

Note: Play already has built in support for X-Forwarded-For headers.

我们可以 block 请求

1
2
3
4
5
6
7
8
9
10
import play.api.mvc._
import play.api.mvc.Results._

def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
request.headers.get("X-Forwarded-Proto").collect {
case "https" => action(request)
} getOrElse {
Future.successful(Forbidden("Only HTTPS requests allowed"))
}
}

最后我们还能修改返回的结果

1
2
3
4
5
import play.api.mvc._

def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}

Different request types

不同的请求类型

虽然 action composition 允许您在 HTTP request 和 response 级别执行额外的处理,但是您通常希望构建数据转换管道( pipelines),以便向请求本身添加上下文(context) 或 执行验证(perfom validation)。

ActionFunction 可以看作是作用在 request 上的一个函数,在输入请求类型和传递到下一层的输出类型上都参数化

每个操作函数都可以表示模块处理,例如身份验证、对象的数据库查找、权限检查或希望跨操作组合和重用的其他操作。

一些实现了实现 ActionFunction 的预定义的 trait 对于不同类型的处理非常有用。

  • ActionTransformer
    • can change the request, for example by adding additional information.
    • 可以改变一个请求,例如为此请求添加额外信息等。
  • ActionFilter
    • can selectively intercept requests, for example to produce errors, without changing the request value.
    • 可以选择性的拦截请求,例如在不改变请求的前提下产生一个错误。
  • ActionRefiner
    • is the general case of both of the above.
    • 上面两个 trait 的通用父类(trait)。
  • ActionBuilder
    • is the special case of functions that take Request as input, and thus can build actions.
    • 以 “Request“ 作为输入的函数的一种特殊情况,并且是否可以构建 actions。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
trait ActionRefiner[-R[_], +P[_]] extends ActionFunction[R, P] {
/**
* 确定怎么处理一个请求,这是继承 ActionRefiner 后需要实现的主方法。
*
* 它可以决定立即拦截请求并返回结果(Left),或者继续处理类型为P的新参数(Right)。
*
* @return Either a result or a new parameter to pass to the Action block
*/
protected def refine[A](request: R[A]): Future[Either[Result, P[A]]]

final def invokeBlock[A](request: R[A], block: P[A] => Future[Result]) =
refine(request).flatMap(_.fold(Future.successful, block))(executionContext)
}


trait ActionFilter[R[_]] extends ActionRefiner[R, R] {
/**
* 确定是否处理请求。这是 ActionFilter 必须实现的主要方法
*
* 它可以决定立即拦截请求并返回结果(Some),或者继续处理(None)。
*
* @return An optional Result with which to abort the request
*/
protected def filter[A](request: R[A]): Future[Option[Result]]

final protected def refine[A](request: R[A]) =
filter(request).map(_.toLeft(request))(executionContext)
}

trait ActionTransformer[-R[_], +P[_]] extends ActionRefiner[R, P] {
/**
*扩展或转换现有请求。这是ActionTransformer必须实现的主要方法
* @return The new parameter to pass to the Action block
*/
protected def transform[A](request: R[A]): Future[P[A]]

final def refine[A](request: R[A]) =
transform(request).map(Right(_))(executionContext)
}

我们还可以通过实现 invokeBlock 方法定义自己的 ActionFunction。这样通常可以方便地创建请求的输入和输出类型实例(使用 WrappedRequest),但这并不是严格必需的。

Authentication

action functions 最常见的用例之一是身份验证。我们可以很容易地实现我们自己的身份验证操作转换器,它从原始请求确定用户并将其添加到新的 UserRequest。注意,这也是一个 ActionBuilder,因为它接受一个简单的请求作为输入

1
2
3
4
5
6
7
8
9
10
import play.api.mvc._

class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)

class UserAction @Inject()(val parser: BodyParsers.Default)(implicit val executionContext: ExecutionContext)
extends ActionBuilder[UserRequest, AnyContent] with ActionTransformer[Request, UserRequest] {
def transform[A](request: Request[A]) = Future.successful {
new UserRequest(request.session.get("username"), request)
}
}

内置的 authentication action builder 只是一个方便的帮助程序,它可以最小化实现简单情况下身份验证所需的代码,其实现与上面的示例非常相似。

由于编写自己的身份验证帮助程序很简单,所以如果内置的帮助程序不适合您的需要,我们建议这样做。

🎮 Play 入门与学习(一) Controller & Router & BodyParser

Controllers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Singleton
class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {

def index() = Action { implicit request: Request[AnyContent] =>
Ok(views.html.index())
}

def explore() = Action { implicit request: Request[AnyContent] =>
Ok(views.html.explore())
}

def tutorial() = Action { implicit request: Request[AnyContent] =>
Ok(views.html.tutorial())
}

}

conf 目录下配置 routes 文件,对请求url 进行映射到 controllers,和 springmvc@RequestMapping 很类似。

1
2
3
4
5
6
7
8
9
# Routes
# This file defines all application routes (Higher priority routes first)
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~

# An example controller showing a sample home page
GET / controllers.HomeController.index
GET /explore controllers.HomeController.explore
GET /tutorial controllers.HomeController.tutorial

通过上述配置之后,我们就可以通过 url 访问到具体的 请求方法了。

Action

每一个请求是被一个 Action 进行处理了,处理之后返回 Results

Results

常见的 results

1
Ok("Got request [" + request + "]")

重定向到另一个 url 对应的 Action

1
Redirect("/echo")

mark 方法还没有完成

1
def todo() = TODO

自定义 Result

1
2
3
4
Result(
header = ResponseHeader(200, Map.empty),
body = HttpEntity.Strict(ByteString("Hello world!"), Some("text/plain"))
)

Http Routing

所有路由信息将定义在 conf/routes 文件下,前文有提及到。router 是负责将每个传入 HTTP 请求转换为 Action 的组件。

每一个 Http 请求被 Play MVC Framework 认为是一个事件。每个请求包含两条主要信息:

  • 请求路径,restful 风格
  • http 请求方法,类似 Get、Post、Delete、Put 等

语法

conf/routesrouter 使用的配置文件。该文件列出了应用程序所需的所有 routes。每个路由由一个 HTTP 方法和 URI 模式组成,它们都与 Action 的调用相关联。

让我们看看路由定义是什么样的:

1
2
GET   /clients/:id          controllers.Clients.show(id: Long)
GET /clients/:id controllers.Clients.show(id: Long)

通过 -> 来使用不同的路由规则

1
->      /api                        api.MyRouter

当与字符串插值路由DSL(也称为SIRD路由)结合使用时,或者在处理使用多个路由文件路由的子项目时,这一点尤其有用。

通过 nocsrf 来禁用 CSRF filter

1
2
+ nocsrf
POST /api/new controllers.Api.newThing

URL规则:

1
2
3
4
5
6
7
8
9
10
#静态 path
GET /clients/all controllers.Clients.list()


# 动态 path
GET /clients/:id controllers.Clients.show(id: Long)

# 正则匹配模式

GET /files/*name controllers.Application.download(name)

逆向、反转 routing

1
Redirect(routes.HelloController.echo())

操作处理结果返回

结果内容类型自动从您指定作为响应体的Scala值推断出来。

通过 play.api.http.ContentTypeOf 来实现

1
2
3
4
5
6
7
8
9
10
//Will automatically set the Content-Type header to text/plain, while:
val textResult = Ok("Hello World!")

//will set the Content-Type header to application/xml.
val xmlResult = Ok(<message>Hello World!</message>)

//自己定义返回类型
val htmlResult = Ok(<h1>Hello World!</h1>).as("text/html")
val htmlResult2 = Ok(<h1>Hello World!</h1>).as(HTML)

Manipulating Http headers (操纵 Http 头)

1
2
//添加返回头信息
val result = Ok("Hello World!").withHeaders(CACHE_CONTROL -> "max-age=3600",ETAG -> "xx")

Session and Flash

存储在 session 中的数据在整个会话期间都是可用的,存储在 flash 作用域的数据只对下一个请求可用。

需要注意,session 和 flash 的数据不是由服务器存储的,而是使用 cookie 机制添加到每个后续 http 请求中的。这意味着数据大小将非常有限(up to 4kb),并且只能存储字符串值。 cookie 的默认名称是 PLAY_SESSION。这可以在 application.conf 通过配置 key play.http.session 来更改。

Session 存储

1
2
3
4
5
6
//这会将 session 完全替换掉
Ok("Welcome!").withSession("connected" -> "user@gmail.com")
//在现有session 基础上添加 element 内容即可
Ok("Hello World!").withSession(request.session + ("saidHello" -> "yes"))
//通过key remove 部分内容
Ok("Theme reset!").withSession(request.session - "theme")

读取 Session 内容

1
2
3
4
5
6
7
def index = Action { request =>
request.session.get("connected").map { user =>
Ok("Hello " + user)
}.getOrElse {
Unauthorized("Oops, you are not connected")
}
}

丢弃整个 Session 内容

1
Ok("Bye").withNewSession

Flash Scope (Flash作用域)

flash scope 和 session 作用很像,但是有两个区别

  • data are kept for only one request
  • the Flash cookie is not signed, making it possible for the user to modify it.

session 的 cookie 会进行加密,而 flash 的 cookie 不会进行加密。

Flash作用域 应该只用于在简单的 非ajax 应用程序上传输 成功/错误消息。由于数据只是为下一个请求保存的,而且在复杂的 Web 应用程序中不能保证请求顺序,所以 Flash作用域 受竞态条件的限制。(the Flash scope is subject to race conditions.)

code using flash scope

1
2
3
4
5
6
7
8
9
def index = Action { implicit request =>
Ok {
request.flash.get("success").getOrElse("Welcome!")
}
}

def save = Action {
Redirect("/home").flashing("success" -> "The item has been created")
}

To retrieve the Flash scope value in your view, add an implicit Flash parameter

要在视图中检索Flash作用域值,请添加一个隐式Flash参数:

1
2
3
4
@()(implicit flash: Flash)
...
@flash.get("success").getOrElse("Welcome!")
...

请求体解析 Body Parsers

什么是 body parsers

HTTP请求 是header 后面跟着 body。header 通常很小——它可以安全地缓冲在内存中,因此在 Play 中它是使用RequestHeader 类建模的。

然而,body 可能非常长,因此不在内存中缓冲,而是建模为流。然而,许多请求体有效负载 (payloads) 都很小,并且可以在内存中建模,因此为了将 body流 映射到内存中的对象,Play 提供了一个BodyParser 抽象。

由于 Play 是一个异步框架,传统的 InputStream 不能用于读取请求体——输入流阻塞了,当您调用 read 时,调用它的线程必须等待数据可用。

相反,Play 使用一个名为 Akka Streams 的异步流库。Akka StreamsReactive Streams 的一个实现。

允许许多异步流api 无缝地协同工作,所以尽管传统 InputStream 的基础技术不适合使用, 但是Akka StreamsReactive Streams 的整个生态系统的异步库将为你提供你需要的一切。

使用 Body Parsers

如果没有显式选择 body parserPlay 将使用的缺省的 body parser 将查看传入的 Content-Type,并相应地解析body。

例如,类型 application/json 的内容类型将被解析为 JsValue,而类型 application/x-www-form- urlencoding 的内容类型将被解析为 Map[String, Seq[String]]

默认的 Body Parser 生成 AnyContent 类型的 BodyAnyContent 支持的各种类型, 可以通过 as方法 访问,例如 asJson,它返回一个 Option[body类型]

1
2
3
4
5
6
7
8
9
10
11
def save = Action { request: Request[AnyContent] =>
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson

// Expecting json body
jsonBody.map { json =>
Ok("Got: " + (json \ "name").as[String])
}.getOrElse {
BadRequest("Expecting application/json request body")
}
}

下面是默认 body parser 支持的类型映射 (The following is a mapping of types supported by the default body parser)

  • text/plain: String, accessible via asText.
  • application/json: JsValue, accessible via asJson.
  • application/xml, text/xml or application/XXX+xml: scala.xml.NodeSeq, accessible via asXml.
  • application/x-www-form-urlencoded: Map[String, Seq[String]], accessible via asFormUrlEncoded.
  • multipart/form-data: MultipartFormData, accessible via asMultipartFormData.
  • Any other content type: RawBuffer, accessible via asRaw.

默认的 body parser 在解析之前会 try to determine 请求是否具有 body

根据 HTTP 规范(spec),内容长度(Content-Length) 或 传输编码标头(Transfer-Encoding) 的出现都表示主体的存在,因此解析器只在出现其中一个标头时进行解析,或者在显式设置非空主体时在 FakeRequest 上进行解析。

如果希望在所有情况下解析主体,可以使用下面描述的anyContent主体解析器。

显示的选择一个 Body Parser

如果希望显式地选择主体解析器,可以将 body parser 传递给 Action 的 apply或 async 方法。

Play 提供了许多开箱即用的 body parser (Play provides a number of body parsers out of the box),

这是通过 PlayBodyParsers trait 提供的,它可以注入到您的控制器中。

例子,如果要定义一个 request body 期望是 Json Body 的 Action

1
2
3
4
//如果不是 Json类型会返回 415 Unsupported Media Type , 类似 Springmvc 在参数前面加 @RequestBody
def save = Action(parse.json) { request: Request[JsValue] =>
Ok("Got: " + (request.body \ "name").as[String])
}

注意,上述 body 的类型为 JsValue, 它不是 Option 类型的了。原因是 json body parser 会验证请求是否具有 application/jsonContent-Type,如果不是,则会返回 415 的错误,即 415 Unsupported Media Type,这样我们就不用再次检查了。

这样依赖,这个方法将要对请求的type 有严格的限制了,客户端要清楚这一点。如果希望有更宽松的做法,即不是 ``application/json类型的Content-Type` 也能够进行解析,可以使用如下方法:

1
2
3
4
//不会返回 415,会尝试进行解析
def save = Action(parse.tolerantJson) { request: Request[JsValue] =>
Ok("Got: " + (request.body \ "name").as[String])
}

另一个例子,保存文件

1
2
3
def save = Action(parse.file(to = new File("/tmp/upload"))) { request: Request[File]  =>
Ok("Saved the request content to " + request.body)
}

组合 Body Parsers (Combining body parsers)

在上一个保存文件的示例中,所有请求 bodies 都存储在同一个文件中 (/tmp/upload),这是存在问题的。

我们可以编写另一个自定义主体解析器,它从请求会话中提取用户名,为每个用户提供一个惟一的文件

1
2
3
4
5
6
7
8
9
10
11
val storeInUserFile: BodyParser[File] = parse.using { request =>
request.session.get("username").map { user =>
parse.file(to = new File("/tmp/" + user + ".upload"))
}.getOrElse {
sys.error("You don't have the right to upload here")
}
}
//自定义 body parser
def save: Action[File] = Action(storeInUserFile) { request =>
Ok("Saved the request content to " + request.body)
}

上面我们并没有真正编写自己的 BodyParser,而只是组合现有的 BodyParser。这通常就足够了,应该涵盖大多数用例。高级主题部分将介绍从头编写 BodyParser

Max content length

基于文本的 body parsers (例如 text、json、xml 或 formurlencoding) 需要有一个最大内容长度。因为它们必须将所有内容加载到内存中。默认情况下,它们将解析的最大内容长度是 100KB。可以通过指定 play.http.parser 来覆盖它。可以通过在 application.conf 指定 maxMemoryBuffer属性来改变这个大小。

1
2
# application.conf
play.http.parser.maxMemoryBuffer=128K

对于将内容缓冲 (buffer) 到磁盘上的 body parser,例如 raw parsermultipart/form-data 等,它们的最大内容长度使用 play.http.parsermaxDiskBuffer 来进行指定。

maxDiskBuffer属性,默认值为10MBmultipart/form-data 解析器还强制为数据字段的聚合设置文本最大长度属性。

您还可以通过在 Action 中显示指定来覆盖默认的最大长度。

1
2
3
4
// Accept only 10KB of data.
def save = Action(parse.text(maxLength = 1024 * 10)) { request: Request[String] =>
Ok("Got: " + text)
}

You can also wrap any body parser with maxLength

1
2
3
4
// Accept only 10KB of data.
def save = Action(parse.maxLength(1024 * 10, storeInUserFile)) { request =>
Ok("Saved the request content to " + request.body)
}

Writing a custom body parser

我们可以实现 BodyParser 特质来自定义 body parser,这个 trait 是一个很简单的函数

1
trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])
  • 接收一个 RequestHeader,这样我们可以检查关于请求的信息,它通常用于获取 Content-Type,这样 body 就会被正确的解析。

  • 函数的返回类型是 Accumulator,它是一个累加器。Accumulator 是 围绕 Akka Streams Sink 的一层薄层。Accumulator 异步地将 streams of elements 加到结果中,它可以通过传入 Akka Streams Source 来运行。这将返回一个 Future,并且在累加器完成时,future得到完成。

  • Accumulator 本质上和 Sink[E,Future[A]] 一样,事实上,Accumulator 是 Sink 的一个包装器。但是最大的区别是 Accumulator 提供了方便的方法,比如 map、mapFuture、recover 等等。

  • Accumulator 用于将结果作为一个 promise 来处理,其中 Sink 要求将所有的操作包装在 mapMaterializedValue 的调用中。

  • Accumulator 的 apply 返回 ByteString 类型的元素,这些元素本质上是字节数组,但与 byte[] 不同的是,ByteString 是不可变的,它的许多操作(如 slicing 或者 appending) 都是在固定的时间内完成的。

Accumulator 的返回类型是 Either[Result, A],它要么返回一个 Result,要么返回一个类型 A。通常在发生错误的情况下返回 Result。例如,如果 body 解析失败,比如 Content-Type 与 Body Parser 接受的类型不匹配,或者内存缓冲区溢出了。当 body parser 返回一个 result 时,这将缩短 action 的处理时间,action 的结果将里脊返回,并且永远不会调用该操作。

将 Body 引向别处 Directing the body elsewhere

编写 body parser 的一个常见用例是,当您实际上不想解析主体时,而是想将它以流的形式引入到其他地方(stream it elsewhere)。为此,您可以定义一个自定义 body parser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.inject._
import play.api.mvc._
import play.api.libs.streams._
import play.api.libs.ws._
import scala.concurrent.ExecutionContext
import akka.util.ByteString

class MyController @Inject() (ws: WSClient, val controllerComponents: ControllerComponents)(implicit ec: ExecutionContext) extends BaseController {

def forward(request: WSRequest): BodyParser[WSResponse] = BodyParser { req =>
Accumulator.source[ByteString].mapFuture { source =>
request
.withBody(source)
.execute()
.map(Right.apply)
}
}

def myAction = Action(forward(ws.url("https://example.com"))) { req =>
Ok("Uploaded")
}
}

使用 Akka Streams 进行自定义的解析

Custom parsing using Akka Streams

在很少的情况下,可能需要使用 Akka Streams 编写自定义解析器。在大多数情况下,先用 ByteString 缓冲 body 就足够了,这通常提供一种简单得多的解析方法,因为您可以对主体使用强制方法和随机访问。

但是,当这不可行时,例如需要解析的主体太长而无法装入内存时,则可能需要编写自定义主体解析器。

关于如何使用 Akka Streams 的完整描述超出了本文档的范围——最好从阅读 Akka Streams 文档开始。但是,下面显示了一个 CSV 解析器,它基于Akka Streams 烹饪书中 ByteStrings 文档流的解析行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import play.api.mvc.BodyParser
import play.api.libs.streams._
import akka.util.ByteString
import akka.stream.scaladsl._

val csv: BodyParser[Seq[Seq[String]]] = BodyParser { req =>

// A flow that splits the stream into CSV lines
val sink: Sink[ByteString, Future[Seq[Seq[String]]]] = Flow[ByteString]
//我们按照 new line character 进行分割,每行最多允许 1000个字符。
.via(Framing.delimiter(ByteString("\n"), 1000, allowTruncation = true))
//把每一行变成一个String,用逗号分隔
.map(_.utf8String.trim.split(",").toSeq)
// 现在我们使用 fold 将其变为一个列表 List
.toMat(Sink.fold(Seq.empty[Seq[String]])(_ :+ _))(Keep.right)

// 将 body 转为为 Either right 返回
Accumulator(sink).map(Right.apply)
}

使用 Docker 容器方式搭建 Zookeeper 集群

1. Zookeeper是什么

Zookeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

Zookeeper 作为一个分布式的服务框架,主要用来解决分布式集群中应用系统的一致性问题,它能提供基于类似于文件系统的目录节点树方式的数据存储, Zookeeper 作用主要是用来维护和监控存储的数据的状态变化,通过监控这些数据状态的变化,从而达到基于数据的集群管理

简单的说,Zookeeper =文件系统+通知机制。

2. Zookeeper集群介绍

要搭建一个高可用的 ZooKeeper 集群,我们首先需要确定好集群的规模。为了使得 ZooKeeper 集群能够顺利地选举出 Leader,必须将 ZooKeeper 集群的服务器数部署成奇数。其实任意台 ZooKeeper 服务器都能部署且能正常运行。

一个 ZooKeeper 集群如果要对外提供可用的服务,那么集群中必须要有过半的机器正常工作并且彼此之间能够正常通信。

ZooKeeper 集群有一个特性:过半存活即可用

Zookeeper 的启动过程中 leader 选举是非常重要而且最复杂的一个环节。Zookeeper 的选举过程速度很慢,一旦出现网络隔离,Zookeeper 就要发起选举流程。Zookeeper 的选举流程通常耗时 30120 秒,期间 Zookeeper 由于没有 master,都是不可用的

3. 集群搭建

我们将介绍两种搭建方式,分别在一台机器上启动三个节点使用不同端口号完成伪分布式和在三台节点上完全搭建 Zookeeper 集群。

伪分布式

docker 容器中搭建 zookeeper 集群,可以用docker-compose.yml 文件 (本教程使用3个容器做集群)

伪分布式.png

执行 docker-compose up -d 启动容器,启动成功如下图所示:

image.png

查看 Zookeeper 节点状态:

docker exec -it zoo2 sh 进入容器 ,执行 zkServer.sh status

查看

完全分布式

利用 docker-compose 搭建 Zookeeper 集群,本教程使用3个节点,每个节点启动一个 docker 容器做集群,配置和单机节点没啥区别,注意配置

1
network_mode: "host"

image.png

4. 水平扩容

简单地讲,水平扩容就是向集群中添加更多的机器,以提高系统的服务质量。
很遗憾的是,ZooKeeper 在水平扩容扩容方面做得并不十分完美,需要进行整个集群的重启。

通常有两种重启方式,一种是集群整体重启,另外一种是逐台进行服务器的重启。

(1) 整体重启

所谓集群整体重启,就是先将整个集群停止,然后更新 ZooKeeper 的配置,然后再次启动。如果在你的系统中,ZooKeeper 并不是个非常核心的组件,并且能够允许短暂的服务停止(通常是几秒钟的时间间隔),那么不妨选择这种方式。在整体重启的过程中,所有该集群的客户端都无法连接上集群。等到集群再次启动,这些客户端就能够自动连接上——注意,整体启动前建立起的客户端会话,并不会因为此次整体重启而失效。也就是说,在整体重启期间花费的时间将不计入会话超时时间的计算中。

(2) 逐台重启

这种方式更适合绝大多数的实际场景。在这种方式中,每次仅仅重启集群中的一台机器,然后逐台对整个集群中的机器进行重启操作。这种方式可以在重启期间依然保证集群对外的正常服务。

5. 客户端使用配置

客户端使用 Zookeeper 集群,只需要修改之前的Zookeeper 集群的地址即可

1
zookeeper.host=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183

从示例逐渐理解Scala隐式转换

隐式转换和隐式参数是 Scala 的两个功能强大的工具。隐式转换可以丰富现有类的功能,隐式对象是如何被自动呼出以用于执行转换或其他任务的。利用这两点,我们可以提供优雅的类库。

本文将通过几个示例代码来整体学习一下 Scala 隐式转换的四个特性和运用。它们分别是 隐式函数运用、隐式类扩展运用、隐式参数、类型类(Type class)运用。

隐式转换

implicit conversion function:指的是那种以 implicit 关键字声明的带有单个参数的函数。这样的函数将被自动应用,将值从一种类型转换为另一种类型。

隐式函数 implicit def

定义一个 case classMultiply,并定义一个方法 multiply ,接收一个当前对象,并将值相乘后返回。
定义隐式转换函数 int2Multiply

1
2
3
4
5
6
7
8
9
10
case class Multiply(m: Int, n: Int) {

def multiply(other: Multiply): Multiply = {
Multiply(other.m * m, other.n * n)
}
}
object MultiplyImplicit {
//定义隐式转换函数,参数单个,将 int 隐式转换为 Multiply 对象
implicit def int2Multiply(n: Int): Multiply = Multiply(n, 2)
}

测试类 MultiplyMain

1
2
3
4
5
6
7
8
9
object MultiplyMain {
//导入隐式转换方法(局部引用可以避免不想要的隐式转换发生)
import com.maple.implic.one.MultiplyImplicit._

def main(args: Array[String]): Unit = {
val x: Multiply = 3.multiply(Multiply(2, 1))
println(x.toString)
}
}
  • 运行程序结果如下:
    1
    2
    3
    4
    5
    结果为:Multiply(6,2)
    //计算过程,3 隐式转换为 Multiply(3, 2)
    3 => Multiply(3, 2)
    //调用multiply计算
    Multiply(3, 2).multiply(Multiply(2, 1)) =Multiply( 3*2 , 2*1 )
    如果我们提供多个隐式转换函数
    1
    2
    3
    4
    5
    object MultiplyImplicit {
    implicit def int2Multiply(n: Int): Multiply = Multiply(n, 2)
    //提供第二个隐式转换函数
    implicit def int2Multiply2(n: Int): Multiply = Multiply(n, 3)
    }
    Main 中,我们可以通过两种方式进行指定具体使用哪个隐式转换函数。
    比如我们选择使用 int2Multiply的隐式转换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    object MultiplyMain {
    //方法1: 排除 int2Multiply2 方法,引入其余所有的方法
    import com.maple.implic.one.MultiplyImplicit.{int2Multiply2 ⇒ _, _}
    // 方法2: 精确引入
    import com.maple.implic.one.MultiplyImplicit.int2Multiply

    def main(args: Array[String]): Unit = {
    val x: Multiply = 3.multiply(Multiply(2, 1))
    println(x.toString)
    }
    }

隐式类,丰富现有类库功能

你是否希望某个类拥有新的方法,而此方法该类并没有提供,那么隐式转换可以丰富这个类,给它提供更多的方法

例如数据库连接类 Connection, 我们希望给它新增一个 executeUpdate 方法来对数据进行修改,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.maple.implic.two

import java.sql.Connection
import scala.language.implicitConversions
//隐式类,Rich表示对Connection的增强类
class RichConnection(conn: Connection) {
//定义的新方法 executeUpdate,对数据操作
def executeUpdate(sql: String): Int = {
conn.prepareStatement(sql).executeUpdate()
}
}
//提供隐式转换 func 来将原有类型转换为Rich 类型
object RichConnection {
implicit def executeUpdate(connection: Connection) = new RichConnection(connection)
}

测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object ConnectionMain {
//引入隐式转换 func
import com.maple.implic.two.RichConnection._

def main(args: Array[String]): Unit = {
//定义 dataSource
val ds: DataSource = {
val ds = new MysqlDataSource
ds.setURL("jdbc:mysql://127.0.0.1:3306/maple?useUnicode=true&characterEncoding=utf8")
ds.setUser("root")
ds.setPassword("123456")
ds
}
//获取 conn
val connection = ds.getConnection
//执行查询
connection.executeUpdate("UPDATE t_user SET name = 'maple' WHERE id = 1")
}
}

上面通过定义一个 RichConnection 我们可以增强现有类 Connection 的功能。这样一来,通过 connection 对数据库进行增删改查,可以简化大量代码。

隐式参数

函数或方法可以带有一个标记为 implicit 的参数列表。在这种情况下,编译器将会查找默认值,提供给本次函数调用。

利用隐式参数进行隐式转换

隐式的函数参数也可以被用作隐式转换。如果我们定义一个泛型函数

1
def larger[T](x: T, y: T) = if (x > y) x else y

ex.png

关于隐式参数的另一件事是,它们可能最常用于提供有关在早期参数列表中显式提到的类型的信息,类似于Haskell的类型类。作为示例,请考虑清单21.2中所示的maxListUpBound函数,该函数返回传递列表的最大元素

1
2
3
4
5
6
7
8
9
10
11
def maxListUpBound[T <: Ordered[T]](elements: List[T]): T = {
elements match {
case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x
case x :: rest =>
val maxRest = maxListUpBound(rest)
if (x > maxRest) x
else maxRest
}
}

maxListUpBound 表示传入一个 List,然后返回 list 中最大的一个元素。功能简单,但是用法十分限制,该List中的成员必须是 Ordered[T] 的子类,否则就会报错。比如我们运行如下例子

1
2
3
4
5
6
7
object ImplicitParameterMain {
import com.maple.implic.three.ImplicitParameter._
def main(args: Array[String]): Unit = {
val result = maxListUpBound(List[Integer](1, 2, 3))
println(result)
}
}

当我们运行 main 函数时,编译器会报如下错。意思是 Int 不是 Ordered[T] 子类,因此无法使用。

1
2
3
4
5
6
Error:(49, 18) inferred type arguments [Int] do not conform to method maxListUpBound's type parameter bounds [T <: Ordered[T]]
val result = maxListUpBound(List[Int](1, 2, 3))
Error:(49, 42) type mismatch;
found : List[Int]
required: List[T]
val result = maxListUpBound(List[Int](1, 2, 3))

使用隐式参数优化

如果让 maxListUpBound 更通用,我们需要分离这个函数,增加一个参数,来将 T 转换为 Ordered[T],使用隐式参数 implicit orders: T ⇒ Ordered[T]来做到这一点。

1
2
3
4
5
6
7
8
9
10
11
def maxListUpBound2[T](elements: List[T])(implicit orders: TOrdered[T]): T = {
elements match {
case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x
case x :: rest =>
val maxRest = maxListUpBound2(rest)
if (x > maxRest) x
else maxRest
}
}

测试程序:

1
2
3
4
5
6
import com.maple.implic.three.ImplicitParameter._

def main(args: Array[String]): Unit = {
val result = maxListUpBound2(List[Int](1, 2, 3))
println(result)
}

结果为3,并没有报错。这其中编译器是将 Int 转换为了 Ordered[Int]

类型类(type class)

让某个类拥有某个算法,我们无需修改这个类,提供一个隐式转换即可。这种做法相对于面向对象的继承扩展来的更灵活。

看下面两个例子,OrderingScala 提供的类型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object Implicits {

implicit class OrderedExt[T: Ordering](v: T) {
def between(min: T, max: T) = {
val ordering = implicitly[Ordering[T]]
ordering.lteq(min, v) && ordering.lteq(v, max)
}
}

implicit class OrderedExt2[T](v: T)(implicit ordering: Ordering[T]) {
def between2(min: T, max: T) = {
ordering.lteq(min, v) && ordering.lteq(v, max)
}
}
}

使用,上面两种写法都可以达到相同的功能。

1
2
3
4
5
6
7
import com.maple.implic.Implicits._
def main(args: Array[String]): Unit = {
val isBetween = 10.between(2, 20)
val isBetween2 = 30.between2(2, 20)
println(isBetween)
println(isBetween2)
}

Ordering 这样的特质(trait) 被称为类型类(type class,源自 Haskell) 。类型类定义了某种行为,任何类型都可以通过提供相应的行为来加入这个类。这个类是因为共用的目的而组合在一起的类型。

自定义类型类

Scala 标准类库提供了不少类型类。比如 EquivNumericFractionalHashingIsTraverableOneIsTraverableLike 等。我们通过自定义一个类型 CustomOperation 来更深入的学习。

定义特质 CustomOperation

1
2
3
4
5
6
trait CustomOperation[T] {
// 加操作
def plus(x: T, y: T): T
// 乘操作
def multiply(x: T, y: T): T
}

在伴生对象中给 String 类型扩展基于 CustomOperation 的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
object CustomOperation {

implicit object StringCustomOperation extends CustomOperation[String] {
override def plus(x: String, y: String): String = x + y

override def multiply(x: String, y: String): String = x + "*" + y
}

//定义隐式类,对 `String` 进行增强
implicit class CustomImplicitClass[T: CustomOperation](v: T) {

def multiply(x: T, y: T): T = {
//从冥界召唤的CustomOperation[T]隐式类型。
val custom = implicitly[CustomOperation[T]]
custom.multiply(v, x) + "+" + custom.multiply(v, y).toString
}
//另外一种写法
/* def multiply(x: T, y: T)(implicit custom: CustomOperation[T]): String = {
custom.multiply(v, x) + custom.multiply(v, y).toString
}*/


def plus(x: T, y: T): String = {
val custom = implicitly[CustomOperation[T]]
custom.plus(v, x) + custom.plus(v, y).toString
// custom.plus(x, y)
}
}
}

测试类 CustomOperationMain:

1
2
3
4
5
6
7
8
import com.maple.implic.typeclass.CustomOperation._
object CustomOperationMain {
//
def main(args: Array[String]): Unit = {
val str: String = "maple".plus("<", ">")
println(str)
}
}

结果如下:

1
2
3
4
maple<maple>
//隐式转换的运算过程为
custom.plus(v, x) + custom.plus(v, y).toString
override def plus(x: String, y: String): String = x + y

如果想要对 Double 支持上述操作,同样定义如下类型类扩展即可:

1
2
3
4
5
implicit object DoubleCustomOperation extends CustomOperation[Double] {
override def plus(x: Double, y: Double): Double = x + y

override def multiply(x: Double, y: Double): Double = x * y
}

测试用例:

1
2
3
4
5
import com.maple.implic.typeclass.CustomOperation._
def main(args: Array[String]): Unit = {
val doubleValue = 5.5.multiply(2.0, 3.0)
println(doubleValue)
}

结果为 11.0+16.5
计算过程:

1
2
3
4
custom.multiply(v, x) + "+" + custom.multiply(v, y).toString
override def multiply(x: Double, y: Double): Double = x * y
//相乘后字符串相加
5.5*2.0 + 5.5*3.0 ===> 11.0+16.5

Type Class 总结

TypeClass行为定义具有行为的对象 分离。有点类似于 AOP,但是比 AOP 简洁很多。同时, 在函数式编程中,通常将 数据行为 相分离,甚至是数据与行为按需绑定,已达到更为高级的组合特性。

隐式转换触发时机

Scala 会考虑如下的隐式转换函数:

  • 1.位于源或目标类型的伴生对象中的隐式函数或隐式类。
  • 2.位于当前作用域中可以以单个标识符指代的隐式函数或隐式类。
    隐式转换可以显示加上,来进行代码调试。

总结

本文主要介绍 Scala 隐式转换的几种用法,通过详细的例子加深读者对隐式转换的理解。关于隐式转换的触发时机以及编译器优化顺序等,将不在本篇文章详细介绍,可以关注笔者后续文章。

推荐

最后推荐一下本人微信公众号,欢迎大家关注。

image.png

从示例逐渐理解Scala尾递归

1.递归与尾递归

1.1 递归

1.1.1 递归定义

递归大家都不陌生,一个函数直接或间接的调用它自己本身,就是递归。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的代码就可以执行多次重复的计算。

1.1.2 递归的条件

一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

以递归方式实现阶乘函数的实现:

代码清单1-1

1
2
3
4
def factorial(n:Int): Long ={
if(n <= 0) 1
else n * factorial(n-1)
}

代码清单中,if(n <= 0) 1是递归返回段,else后面部分是递归前进段。

1.1.3 递归的缺点:
  • 需要保持调用堆栈,如代码清单1-1,每一次递归都要保存n*factorial(n-1)栈帧信息。如果调用次数太多,可能会导致栈溢出
  • 效率会比较低,递归就是不断的调用自己本身,如果方法本身比较复杂,每次调用自己效率会较低。

1.2 尾递归

1.2.1 定义

尾递归的定义比较简单,即函数在函数体最后调用它本身,就被称为尾递归

我们可以这样理解尾递归

  • 所有递归形式的调用都出现在函数的末尾
  • 递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分
1.2.2 例子程序

下面我们使用尾递归的模式实现上面的阶乘

代码清单1-2

1
2
3
4
5
6
7
8
9
def factorial(n:Int):Long = {
@tailrec
def factorial(main:Int,aggr:Int): Long ={
if(main <= 0) aggr
else factorial(main-1,main*aggr)
}

factorial(n,1)
}

我们可以比较代码清单1-1和1-2
1-1中,每次的递归调用都要本身时依赖n这个变量,所以,它只能是个不同的递归。

1-2中,函数factorial每次返回的都是它自己本身,没有依赖任何值。它做的是将main每次减1,将aggr每次乘main,然后将这两个结果作为下一次递归调用的参数,进行调用。

尾递归的核心思想是通过参数来传递每一次的调用结果,达到不压栈。它维护着一个迭代器和一个累加器。

1.3 循环

循环能够解决大多数的累计问题,循环可以完成累加和迭代,处理问题比较简单,思想也比较符合,容易理解

n的阶乘循环的写法

代码清单1-3

1
2
3
4
5
6
7
def fibfor(n:Int): Int ={
var m = 1
for (i <- 1 to n) {
m = m * i
}
m
}

循环版本,会有var的可变变量,我们知道,函数式编程就应该更函数范,我们尽可能的要用vals去替换可变的vars
所以我们可以使用递归的方式来消除掉vars


2.改写 (循环,递归 TO 尾递归)

事实上,scala都是将尾递归直接编译成循环模式的。所以我们可以大胆的说,所有的循环模式都能改写为尾递归的写法模式

尾递归会维护一个或多个累计值(aggregate)参数和一个迭代参数。我们具体分析

2.1迭代器和累计器

  • 累计值参数aggregate将每次循环产生的结果进行累计,然后传到下一次的调用中。

  • 迭代器,和普通递归或循环一样,每次递归或循环后,改变一次。(如for(i=0;i<1-;i++)里面的i)

2.2 普通递归转换为尾递归

并不是所有的递归都能改写为尾递归,那些比较复杂的递归调用时无法优化为尾递归的。但是大部分还是能够进行优化的。

代码清单1-1 和代码清单 1-2 是求n阶阶乘的普通递归与尾递归的写法,前者没有进行优化,每次调用都会压栈。
后者,通过定义一个aggregate(累计)参数,对每一次调用后的结果做一次累计,而另一个参数main称为迭代器,每一次调用都会-1,当达到符合返回的条件时,将累计值返回。

2.3 循环(while loop)转为尾递归(tail recursion)

正如上文循环例子所述,存在var,函数式编程就应该有函数范,我们尽量使用val来代替,所以接下来来看,怎么将循环转换为尾递归

2.3.1 循环和尾递归

正如上文所说的迭代器和累计器,循环和尾递归都有这两个概念

迭代器累计器

尾递归每一次的调用自身都会有一次累加(或者累积,累减等),然后会有一个迭代器进行迭代,一个累加器进行累加迭代的结果,然后作为参数,再去调用自身。

2.3.2 如上面求n阶乘的尾递归例子:
  • 1.循环的例子中存在一个var,它在每次循环中充当一个累加器的角色,累加每一次的迭代结果,而每次迭代过程就是m*i的一个过程。

  • 2.尾递归也是一样的思想,以main作为迭代器,每次递减1,类似循环里的i,以aggr作为累加器,每次累计迭代的结果,类似循环的m

  • 3.相对于普通的递归,这里尾递归多的一个参数就是累加器aggr,用于累计每一次递归迭代的结果。这样做的目的就是每一次调用的结果可以作为下一次函数调用的参数。

3.具体示例-加深理解

3.1 例子1 - 求斐波拉契数列

  • 普通递归写法(性能较低)

    1
    2
    3
    4
    5
    6
    def fibonacci(n:Int): Long ={
    n match {
    case 1 | 2 => 1
    case _ => fibonacci(n-1) + fibonacci(n-2)
    }
    }
  • 循环的写法(循环写法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def fibonacciFor(n:Int): Int = {
    var current = 1
    var next = 1
    if(n <=2) 1
    else {
    for(i <- 2 until n) {
    var aggr = current + next
    current = next
    next = aggr
    }
    next
    }

    }

    可以看到,aggr是累加器,然后将累加的值赋值给下一个next,而current等于next,每一次循环,都有给current和next赋予新值,当累加完成后,返回next的值。

  • 尾递归写法

    如何对其进行优化?

    仔细分析,上面的普通循环,每一轮两个值都在改变,然后又一个累加器aggr,对这两个值进行累加,并赋值给更大的next,然后进入下一次循环。

尾递归,我们也是同样的做法,定义两个接受值,当前的,和下一个,然后需要一个累加值。

这里普通方法的递归调用是两个原函数相加,涉及到的变量有 n , n-1 , n-2

因此,我们在考虑使用尾递归时,可能也需要使用到三个参数,初略涉及,尾递归函数需要使用三个参数,于是改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def fibonacci(n: Int): Long = {
@tailrec
def fibonacciTail(main: Int, current: Int, next: Int): Long = {
main match {
case 1 | 2 => next
case _ => fibonacciByTail(main - 1, next, current+next)
}
fibonacciTail(n, 1, 1)
}

fibonacciTail(n,1,1)

}

使用尾递归和模式匹配。每一次调用自身,将next赋值给current,然后累加current和next的值赋值给新的next值,call下一轮。思想上和上面循环很像。但是更函数范,消除掉了var。


3.2 例子2 - loadBalance算法

需求,设计一个程序,传入一个比例数组,比如Array(1,3,6,一直调用该函数,返回的3个节点的比例也应该如传入的1:3:6的比例一样。

  • 我最开始使用for循环return实现了这个需求,代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    def loadBalance(arr:Array[Int]): Int ={
    //根据传入的数组使用scan高级函数进行变化,具体算法例子:
    //eg (1,3,6) -> (1,4,10)
    //这样的目的是,随机出来的值为0-1时,选择第一个节点,为1-4时选择第二节点,依次类推
    val segment:Array[Int] = arr.scan(0)(_ + _).drop(1)
    //随机数的范围,根据传入的数组的数据之和来,例如上的便是 10 ,产生的随机数介于0 - 9 之间
    val weightSum:Int = arr.sum
    val random = new Random().nextInt(weightSum)

    for(i <- 0 until segment.size ){
    if(random < segment(i)){

    return i
    }

    }
    0
    }
    我通过测试程序调用1万次该方法,返回的随机节点的比例是符合传入的比例的。

思考

虽然这样可以达到目的,但是代码写的既不优雅,在scala函数式编程中最好是不能使用return来强行打断函数执行的,并且在最后,我还需要去写一个0来作为默认返回。

尾递归优化

大部分或者几乎所有的for循环都能使用尾递归进行优化,那上面这个代码如何进行优化呢?

思路:上文的for循环,每次增加的是segment的下标,每循环一次 +1,因此,我们在设计尾递归时,可以使用一个参数来实现相同的功能,而另一个参数应该就是产生的随机数。
ok,我们来进行实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def loadBalance(arr:Array[Int]): Int ={
//根据传入的数组使用scan高级函数进行变化,具体算法例子:
//eg (1,3,6) -> (1,4,10)
//这样的目的是,随机出来的值为0-1时,选择第一个节点,为1-4时选择第二节点,依次类推
val segment:Array[Int] = arr.scan(0)(_ + _).drop(1)
//随机数的范围,根据传入的数组的数据之和来,例如上的便是 10 ,产生的随机数介于0 - 9 之间
val weightSum:Int = arr.sum
val random = new Random().nextInt(weightSum)
//写一个内部方法
def loadUtil(rand:Int,index:Int) {
//assert,保证程序健壮
assert(index < arr.length && arr(index) >= 0)

if(rand < segment(index)) index
else loadUtil(rand,index+1)
}
loadUtil(random,0)
}

我们可以看到,使用尾递归的做法,代码会非常的优雅,现在写一个测试类进行测试!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def main(args: Array[String]): Unit = {
val arr = Array(1,2,7)
val countMap = collection.mutable.Map[Int,Int]()

for(_ <- 1 until 100000) {
val res = loadBalance(arr)

countMap.get(res) match {
case Some(x) => countMap += (res -> (x+1))
case None => countMap +=(res -> 1)
}
}

countMap.foreach(x => {
println(s"${x._1} 调用次数 ${x._2}")
})

}

//测试10000次,返回结果如下:

2 调用次数 69966
1 调用次数 20028
0 调用次数 10005

如上,测试是通过的!是不是很优雅,感受到了尾递归的魅力?


4. scala编译器对尾递归的优化

Scala 对形式上严格的尾递归进行了优化,对于严格的尾递归,不需要注解

@tailrec 可以让编译器去检查该函数到底是不是尾递归,如果不是会报错

具体以上面那个计算斐波拉契数列的例子进行性能分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def time[T](t: =>T): T  = {
val b = System.nanoTime()
val x = t
val e = System.nanoTime();
println("time: " + (e-b)/1000 + "us");
x

}

var count: Long = 0
// @tailrec
def fib2(n: Long): Long = {
count += 1
n match {
case 1 | 2 => 1
case _ =>
fib2(n-1) + fib2(n-2)
}
}

通过上面时间和调用次数的测试,可以得出尾递归的性能消耗很低,速度很快。

4.1 编译器对尾递归的优化

当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。

scala编译器会察觉到尾递归,并对其进行优化,将它编译成循环的模式。

4.2 Scala尾递归的限制

  • 尾递归有严格的要求,就是最后一个语句是递归调用,因此写法比较严格。

  • 尾递归最后调用的必须是它本身,间接的赋值它本身的函数也无法进行优化。

5. 总结

循环调用都是一个累计器和一个迭代器的作用,同理,尾递归也是如此,它也是通过累加和迭代将结果赋值给新一轮的调用,通过这个思路,我们可以轻松的将循环转换为尾递归的形式。

[本文完,欢迎转载,转载请注明出处]

从示例理解Scala隐式转换

隐式转换和隐式参数是 Scala 的两个功能强大的工具。隐式转换可以丰富现有类的功能,隐式对象是如何被自动呼出以用于执行转换或其他任务的。利用这两点,我们可以提供优雅的类库。

本文将通过几个示例代码来整体学习一下 Scala 隐式转换的四个特性和运用。它们分别是 隐式函数运用、隐式类扩展运用、隐式参数、类型类(Type class)运用。

程序员如何保持优秀

程序员如何保持优秀

本文是英文原文 How to be an Excellent Programmer for Many Years 的翻译, 摘抄自 oldrat lee 的博客

  • 1.小范围的选择一些有用技术,透彻的学习它们,拥抱它们。然后不断的扩展这个范围。

  • 2.理解各种数据结构的优点和缺点,包括它们在内存中和在硬盘上的各自表现。

  • 3.理解各种算法的优点和缺点。

  • 4.了解你的工作领域。关上电脑,去做你的用户们在做的事。

  • 5.有准备,有愿望,有能力在任何时候投入到多种技术层面中。你必须知道表象下的技术原理。在“各个技术层面的掌握程度”和“编程能力”上有着密切的联系。

  • 6.发挥你的想象力。永远都要问,“有更好的方法吗?”跳出常规思维约束。最好的解决方案也许还没有被发现。

  • 7.优秀程序员:我优化代码。更优秀程序员:我设计数据。最优秀程序员:他们的不同之处是什么?

  • 8.正确的构造你的数据。任何的缺陷都将造成你的代码里无尽的技术债务。

  • 9.正确的命名事物。使用“动词-形容词-名词”格式来命名程序和函数。变量名要足够长,尽量短,有意义。如果其他程序员不能够理解你的代码,说明你写的不够清楚。在大多数情况下,针对下一个程序员而编码要比针对环境而编码重要的多。

  • 10.把分析和编程分离开做。它们不是同类的事物,需要不同类型的劳力资源,需要在完全不同的时间和地点分开做。如果同时做它们,你一样都做不好。(我喜欢在一天的末尾做不涉及技术的分析,而在第二天早上进行编程。)

  • 11.永远不要图省事走近道。永远不要把相同的代码部署两次。永远不要把一个变量命名成另一个变量名的一部分。也许你不明白这些规则,也许你要辩解。但如果你是遵守着这样做的,这些规则就会约束你正确的构造你的程序。图省事的做法是让那些低等级的程序员永远停留在低等级的原因。

  • 12.学习如何测评程序性能。你会惊奇的发现从中能学到很多之外的知识。

  • 13.学会区别对待问题细节和问题后果。问题细节不会导致太大的差别,而问题后果能导致世界灭亡。只关注后果。

  • 14.密切关注你的用户/客户/管理人员。帮助他们认清楚他们的“what”,这比帮助他们明白他们的“how”要重要的多。

  • 15.写一个框架,不论你是否打算用它。你将从中学到从其它途径中学不到的东西。

  • 16.把你知道的东西教给他人——通过口口交流或通过写作。最终这将成为教育自己的机会。

  • 17.永远要对你的客户/用户说“Yes”,即使在你不确定的情况下。90%的情况下,你会最终找到方法实现它。10%的机会,你将会去向他们道歉。这是重要的个人成长中付出的一点小代价。

  • 18.寻找别人的做出神奇的事情但却一滩糊涂的代码。重构它。然后丢掉它,并发誓自己永远不要犯他们犯下的相同错误。(这样的程序你会发现很多。)

  • 19.数据永远 > 理论或观点。通过开发东西来学习数据。

  • 20.有可能的话,开创自己的业务(服务或产品)。你将从中学到很多你做雇员永远学不到的关于编程的知识。

  • 21.如果不喜爱你现在的工作,就换一个。

🍎 Dubbo SPI 之扩展点自动包装 Wrapper 类

之前的文章我们分析了 Dubbo 的扩展点自适应机制。Dubbo SPI 主要思想也是来自于 JDK 原生的 SPI 机制。框架定义好扩展点接口,服务提供者实现接口。框架可以通过SPI机制动态的将服务提供商切入到应用中。使我们的程序可以面向接口,对扩展开放。

Dubbo SPI 与 JDK SPI

JDK SPI 需要在 classpathMETA-INF/services 目录下创建以扩展点接口全限定名命名的文件,里面内容为实现类的名称(完整包名),多个实现类换行分隔。
文件名称:
spi.png

spi内容.png

JDK加载扩展点实现类的方式:

1
Iterator<Registry> registryImpls = ServiceLoader.load(Registry.class).iterator();

结论
JDK SPI 会加载 classpath 下的所有 META-INF/services 下的所有接口对应的实现类。如果该实现类在 classpath 存在,则会通过ServiceLoader加载出来。如果有多个实现类,多个实现类都会被加载出来,用户则会选择使用哪个。

Dubbo SPI 改进

  • 不一次性实例化扩展点所有实现,再用到的时候,再选择性加载,可以减少资源浪费。
  • 扩展点加载失败异常跑出更明确的信息
  • 提供扩展点实现类与实现类之间的Wrapper 操作,可以用来聚合公共部分逻辑。
  • 提供 IOCAOP 等功能。

Dubbo 扩展点包装 Wrapper 类

一个典型的Wrapper类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CommonRegistry implements Registry {
// 持有 扩展点接口
private Registry registry;
// 构造器注入
public CommonRegistry(Registry registry) {
this.registry = registry;
}

@Override
public String register(URL url, String msg) {
// doSomething
return registry.register(url,msg);
}

@Override
public String discovery(URL url, String content) {
// doSomething
return registry.register(url,msg);
}
}

分析上面的包装类,我们得出 Dubbo 认为的包装类需要满足的两个条件

  • 1.持有扩展点接口对象属性,并通过构造器方式初始化该属性
  • 2.这个类也要实现扩展点接口类,并在实现方法中进行增强操作

Wrapper 包装类实战

上一篇文章 [Dubbo SPI 之 Adaptive 自适应类] 我们介绍过两个注册中心的实现。这里我们有一个新的需求,需要对每一个注册中心的注册服务和发现服务统计一下耗时,增加一些共有逻辑。所以我们定义一个 Wrapper 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CommonRegistry implements Registry {
private Logger logger = LoggerFactory.getLogger(CommonRegistry.class);
// 持有 扩展点接口
private Registry registry;
// 构造器注入
public CommonRegistry(Registry registry) {
this.registry = registry;
}

@Override
public String register(URL url, String msg) {
long begin = System.currentTimeMillis();
String register = registry.register(url, msg);
long end = System.currentTimeMillis();
logger.info("register method 处理耗时 cost: {} ms", end - begin);
return register;
}

@Override
public String discovery(URL url, String content) {
//...实现同上
}
}

META-INF/dubbo/com.maple.spi.Registry里面增加 wrapper 类的信息:

1
common=com.maple.spi.impl.CommonRegistry

测试 Main

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
URL url = URL.valueOf("test://localhost/test")
.addParameter("service", "helloService")
.addParameter("registry","etcd");
//.addParameter("registry","zookeeper");

Registry registry = ExtensionLoader.getExtensionLoader(Registry.class)
.getAdaptiveExtension();

System.out.println(registry.register(url, "maple"););
}

分别尝试使用 etcdzookeeper,控制台打印如下:

1
2
3
4
5
6
7
8
//Etcd
09-26 00:55:16 707 main INFO - 服务: helloService 已注册到 Etcd 上,备注: maple
09-26 00:55:16 709 main INFO - register method 处理耗时 cost: 2 ms
Etcd register already!
// zookeeper
09-26 00:56:17 282 main INFO - 服务: helloService 已注册到zookeeper上,备注: maple
09-26 00:56:17 284 main INFO - register method 处理耗时 cost: 2 ms
Zookeeper register already!

我们看到了 register method 处理耗时 cost: 2 ms 这条日志,说明 CommonRegistry 的逻辑已经运行了。程序先构造 CommonRegistry,从它的构造器中传入的是扩展点实现类,程序会先调用wrapper类对应的方法, 然后在方法内部再调用扩展点实现类的对应方法。类似于装饰器模式,为扩展点实现类增强了功能。
通过这种设计模式,我们可以将多个扩展点实现共用的公共逻辑都移到此类中来。

Wrapper 类不属于候选的扩展点实现

Wrapper 类不属于扩展点实现,我们可以通过如下代码进行验证:

1
2
3
4
5
Set<String> extens = ExtensionLoader
.getExtensionLoader(Registry.class)
.getSupportedExtensions();
//结果
[etcd, zookeeper]

通过 getSupportedExtensions 可以获取扩展点接口 Registry 当前所有的服务扩展实现的 key 值。控制台的结果只有 etcdzookeeper。 因此,wrapper 不属于 扩展点实现,同理 上一篇文章介绍的自适应类 Adaptive, 也不属于扩展点实现。

总结

通过本文总结 JDK SPI 原理和使用方式,然后和 Dubbo SPI 进行对比。

Dubbo 扩展点自动包装Wrapper类,类似与AOP,为扩展点实现增加更多前置或者后置功能模块。实现原理采用装饰器设计模式,将真正的扩展点实现包装在Wrapper类中。扩展点的Wrapper可以有多个,可以根据需求新增。



  扫一扫,关注我的微信公众号 ![公众号.png](https://upload-images.jianshu.io/upload_images/6393906-457a99e16106e3a3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/600)