Статьи

Продвинутая маршрутизация в Play Framework

Мы часто получаем вопросы о том, как удовлетворить все виды потребностей маршрутизации в Play Framework. Хотя встроенного маршрутизатора достаточно для большинства пользователей, иногда вам могут встретиться случаи, когда этого недостаточно. Или, может быть, вы хотите более удобный способ реализации некоторого шаблона маршрутизации. Что бы это ни было, Play позволит вам делать практически все, что угодно. Этот пост в блоге будет описывать некоторые распространенные случаи использования.

Подключение к механизму маршрутизации Plays

Если по какой-то причине вам не нравится роутер Plays или вы хотите использовать модифицированный роутер, то Play позволит вам сделать это легко. Global.onRouteRequest — это метод, который

вызывается, чтобы сделать маршрутизацию. По умолчанию это делегирует маршрутизатору Play, но вы можете переопределить его, чтобы сделать все, что вы хотите. Например:

1
2
3
4
5
6
7
override def onRouteRequest(req: RequestHeader): Option[Handler] = {
  (req.method, req.path) match {
    case ("GET", "/") => Some(controllers.Application.index)
    case ("POST", "/submit") => Some(controllers.Application.submit)
    case _ => None
  }
}

Как вы можете видеть, я практически реализовал свой собственный маленький DSL маршрутизации. Я также мог бы делегировать обратно маршрутизатору по умолчанию, вызвав super.onRouteRequest(req) . Интересная вещь, которую также можно сделать, — делегировать разные маршрутизаторы на основе чего-то в запросе. Маршрутизатор воспроизведения компилируется в экземпляр Router.Routes , и это будет сам объект с именем Routes . По умолчанию любой файл с расширением .routes в каталоге conf будет скомпилирован и .routes в пакет с тем же именем, что и имя файла, за исключением .routes . Поэтому, если бы у меня было два маршрутизатора, foo.routes и bar.routes , я мог бы реализовать грубую форму виртуального хостинга следующим образом:

1
2
3
4
5
6
7
8
9
override def onRouteRequest(req: RequestHeader): Option[Handler] = {
  if (req.host == "foo.example.com") {
    foo.Routes.routes.lift(req)
  } else if (req.host == "bar.example.com") {
    bar.Routes.routes.lift(req)
  } else {
    super.onRouteRequest(req)
  }
}

Итак, вот несколько вариантов использования, для onRouteRequest может быть полезно переопределение onRouteRequest :

  • Изменение запроса каким-либо образом перед выполнением маршрутизации
  • Подключите совершенно другой маршрутизатор (например, Jaxrs)
  • Делегирование в разные файлы маршрутов в зависимости от некоторых аспектов запроса

Реализация собственного роутера

В предыдущем примере мы видели, как использовать интерфейс Plays Router.Routes , другой вариант — реализовать его. Теперь нет никакой реальной причины для его реализации, если вы просто собираетесь делегировать ему напрямую из onRouteRequest . Тем не менее, реализовав этот интерфейс, вы можете включить его в другой файл маршрутов, используя синтаксис для дополнительных маршрутов, который в случае, если вы раньше не сталкивались с этим, обычно выглядит следующим образом:

1
->    /foo         foo.Routes

Теперь то, за что люди часто критикуют Play, это то, что он не поддерживает маршрутизацию ресурсов в стиле rails, где используется соглашение для маршрутизации обычно необходимых конечных точек REST к определенным методам на контроллере. Хотя в Play нет ничего стандартного, что делает это, сегодня нетрудно реализовать это для своего проекта, в Play 2.1 есть все, что вам нужно для его поддержки, с использованием синтаксиса маршрутов и реализации собственного маршрутизатора. И у меня также есть хорошие новости, мы скоро представим такую ​​функцию в Play. Но до тех пор, а также, если у вас есть собственные соглашения, которые вы хотите реализовать, вы, вероятно, найдете эти инструкции очень полезными.
Итак, давайте начнем с интерфейса, который могут реализовать наши контроллеры:

1
2
3
4
5
6
7
8
9
trait ResourceController[T] extends Controller {
  def index: EssentialAction
  def newScreen: EssentialAction
  def create: EssentialAction
  def show(id: T): EssentialAction
  def edit(id: T): EssentialAction
  def update(id: T): EssentialAction
  def destroy(id: T): EssentialAction
}

Я мог бы предоставить реализации по умолчанию, которые возвращают не реализованные, но затем для его реализации потребуется использовать ключевые слова override . Я думаю, что это вопрос предпочтений здесь.
Теперь я собираюсь написать роутер. Интерфейс маршрутизатора выглядит следующим образом:

1
2
3
4
5
6
trait Routes {
  def routes: PartialFunction[RequestHeader, Handler]
  def documentation: Seq[(String, String, String)]
  def setPrefix(prefix: String)
  def prefix: String
}

Метод routes довольно понятен, это функция, которая ищет обработчик для запроса. documentation используется для документирования маршрутизатора, она не является обязательной, но она используется по крайней мере одним инструментом документирования REST API, чтобы узнать, какие маршруты доступны и как они выглядят. Для краткости в этом посте мы не будем беспокоиться о его реализации. Методы prefix и setPrefix используются Play для ввода пути к маршрутизатору. В маршрутах есть синтаксис, который я показал выше, вы можете видеть, что мы объявили, что маршрутизатор находится на пути /foo . Этот путь вводится с использованием этого механизма. Поэтому мы напишем абстрактный класс, который реализует интерфейс маршрутов и интерфейс ResourceController :

01
02
03
04
05
06
07
08
09
10
abstract class ResourceRouter[T](implicit idBindable: PathBindable[T])
    extends Router.Routes with ResourceController[T] {
  private var path: String = ""
  def setPrefix(prefix: String) {
    path = prefix
  }
  def prefix = path
  def documentation = Nil
  def routes = ...
}

Я дал ему PathBindable , так что у нас есть способ преобразовать id из String извлеченной из пути, в тип, принятый методами. PathBindable — это тот же интерфейс, который используется под обложками для преобразования типов в обычный файл маршрутов.
Теперь о реализации routes . Сначала я собираюсь создать несколько регулярных выражений для сопоставления разных путей:

1
2
3
4
private val MaybeSlash = "/?".r
  private val NewScreen = "/new/?".r
  private val Id = "/([^/]+)/?".r
  private val Edit = "/([^/]+)/edit/?".r

Я также собираюсь создать вспомогательную функцию для маршрутов, которые требуют привязки идентификатора:

1
2
def withId(id: String, action: T => EssentialAction) =
  idBindable.bind("id", id).fold(badRequest, action)

badRequest самом деле badRequest — это метод в Router.Routes который принимает сообщение об ошибке и превращает его в действие, которое возвращает его в результате. Теперь я готов реализовать частичную функцию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
def routes = new AbstractPartialFunction[RequestHeader, Handler] {
  override def applyOrElse[A <: RequestHeader, B >: Handler](rh: A, default: A => B) = {
    if (rh.path.startsWith(path)) {
      (rh.method, rh.path.drop(path.length)) match {
        case ("GET", MaybeSlash()) => index
        case ("GET", NewScreen()) => newScreen
        case ("POST", MaybeSlash()) => create
        case ("GET", Id(id)) => withId(id, show)
        case ("GET", Edit(id)) => withId(id, edit)
        case ("PUT", Id(id)) => withId(id, update)
        case ("DELETE", Id(id)) => withId(id, destroy)
        case _ => default(rh)
      }
    } else {
      default(rh)
    }
  }
 
  def isDefinedAt(rh: RequestHeader) = ...
}

Я реализовал AbstractPartialFunction , и основной метод для реализации — applyOrElse . Оператор match не очень похож на мини-DSL, который я показал в первом примере кода. Я использую регулярные выражения в качестве объектов экстрактора, чтобы извлечь идентификаторы из пути. Обратите внимание, что я не показал реализацию isDefinedAt . Play на самом деле не будет вызывать это, но в любом случае это хорошо для реализации, в основном это та же реализация, что и applyOrElse , за исключением того, что вместо вызова соответствующих методов он возвращает true или, если ничего не совпадает, он возвращает false . И теперь мы закончили. Так как же это выглядит? Мой контроллер выглядит так:

1
2
3
4
5
6
7
8
package controllers
 
object MyResource extends ResourceRouter[Long] {
  def index = Action {...}
  def create(id: Long) = Action {...}
  ...
  def custom(id: Long) = Action {...}
}

И в моем файле маршрутов у меня есть это:

1
2
->     /myresource              controllers.MyResource
POST   /myresource/:id/custom   controllers.MyResource.custom(id: Long)

Вы можете видеть, что я также показал пример добавления настраиваемого действия к контроллеру, очевидно, что стандартных действий crud не будет достаточно, и приятно то, что вы можете добавить столько произвольных маршрутов, сколько захотите.
Но что, если мы хотим иметь управляемый контроллер, то есть тот, экземпляром которого управляет структура DI? Хорошо, давайте создадим еще один маршрутизатор, который делает это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class ManagedResourceRouter[T, R >: ResourceController[T]]
    (implicit idBindable: PathBindable[T], ct: ClassTag[R])
    extends ResourceRouter[T] {
 
  private def invoke(action: R => EssentialAction) = {
    Play.maybeApplication.map { app =>
      action(app.global.getControllerInstance(ct.runtimeClass.asInstanceOf[Class[R]]))
    } getOrElse {
      Action(Results.InternalServerError("No application"))
    }
  }
 
  def index = invoke(_.index)
  def newScreen = invoke(_.newScreen)
  def create = invoke(_.create)
  def show(id: T) = invoke(_.show(id))
  def edit(id: T) = invoke(_.edit(id))
  def update(id: T) = invoke(_.update(id))
  def destroy(id: T) = invoke(_.destroy(id))
}

При этом используется тот же метод Global.getControllerInstance который используется управляемыми контроллерами при обычном использовании маршрутизатора. Теперь использовать это очень просто:

1
2
3
4
5
6
7
8
9
package controllers
 
class MyResource(dbService: DbService) extends ResourceController[Long] {
  def index = Action {...}
  def create(id: Long) = Action {...}
  ...
  def custom(id: Long) = Action {...}
}
object MyResource extends ManagedResourceRouter[Long, MyResource]

И в файле маршрутов:

1
2
->     /myresource              controllers.MyResource
POST   /myresource/:id/custom   @controllers.MyResource.custom(id: Long)

Последнее, что нам нужно рассмотреть, — это обратная маршрутизация и маршрутизатор Javascript. Опять же, это очень просто, но я не буду вдаваться в подробности. Вместо этого вы можете ознакомиться с конечным продуктом, который имеет несколько дополнительных функций, здесь .