Перейти к основному содержимому

Play Framework — первая программа

Разработчику Архитектору

Дальше: Akka — основы · Apache Spark на Scala · Scala — о разделе


Play Framework — первая программа

Play Framework — веб-фреймворк для JVM (Scala и Java). Он строится на реактивной модели: асинхронные запросы, маршрутизация через конфигурацию, встроенный HTTP-сервер Netty. Для Scala это один из стандартных путей от консольных утилит к production REST и HTML-приложениям.

Практикум идёт по шагам — проверка JDK и sbt, генератор проекта, маршруты и контроллер, HTML через Twirl, затем учебное REST API "Заметки". Близкий стек на BEAM — Phoenix; акторная модель JVM — Akka.

ШагТемаЗачем
1JDK, sbtУбедиться, что среда готова
2sbt newСоздать каркас проекта
3conf/routesСвязать URL с action
4КонтроллерОбработать HTTP-запрос
5Twirl-шаблонОтдать HTML
6JSON APIREST без фронтенда
7Тесты и productionЗакрепить и подготовить деплой
МатериалЗачем
Первая программа на ScalaСинтаксис и запуск
Основы языкаcase class, implicits
sbt-проектСборка и зависимости
HTTP как основа веб-интеграцийМетоды, статусы, заголовки
Scala — о разделеМаршрут по всему разделу

Навигация по экосистеме Scala

Редактор и терминал

Команды удобно выполнять во вкладке Терминал VS Code (Ctrl+`). Отладка JVM — статья про отладку.


Термины перед стартом

ТерминКратко
sbtSimple Build Tool — сборщик Scala/Java: зависимости, компиляция, запуск, тесты
ActionОбработчик одного HTTP-запроса в Play; возвращает Result (статус + тело)
TwirlШаблонизатор Play: .scala.html компилируется в типобезопасные Scala-функции
NettyАсинхронный сетевой фреймворк; Play слушает порт через него
DI (Guice)Внедрение зависимостей: @Inject() в контроллерах

Шаг 1 — проверка установки

КомпонентВерсияПроверка
JDK17+java -version
sbt1.9+sbt --version
Scala3.x (в проекте)см. build.sbt после генерации
java -version
sbt --version

Разбор:

  • Play 3.x требует JDK 17 или новее; на Windows установите Temurin или Oracle JDK и проверьте JAVA_HOME.
  • sbt скачивает нужную версию Scala при первом запуске проекта — глобально достаточно свежего sbt.
  • Официальная документация: playframework.com.
Windows и кодировка

Сохраняйте исходники в UTF-8. В PowerShell для curl иногда удобнее Git Bash или Windows Terminal.


Шаг 2 — создание проекта

Play поставляет генератор через sbt:

sbt new playframework/play-scala-seed.g8

На вопросы шаблона ответьте, например:

  • namehello-play
  • organizationcom.example
  • scala_version → оставьте значение по умолчанию (3.x)

Структура минимального проекта:

hello-play/
├── app/
│ ├── controllers/
│ │ └── HomeController.scala
│ └── views/
│ └── index.scala.html
├── conf/
│ ├── routes
│ └── application.conf
├── build.sbt
└── project/
└── plugins.sbt

Запуск dev-сервера:

cd hello-play
sbt run

При первом запуске sbt скачает зависимости — это может занять несколько минут. Когда в консоли появится (Ok) и строка про порта 9000, откройте http://localhost:9000 — должна появиться стартовая страница шаблона.

Остановка: Ctrl+C в терминале sbt.

Что лежит в build.sbt

name := "hello-play"
organization := "com.example"
version := "1.0-SNAPSHOT"
scalaVersion := "3.3.5"

libraryDependencies += guice

sbt читает этот файл как Scala-DSL: здесь имя артеfact, версия Scala и плагины Play.


Шаг 3 — маршруты (conf/routes)

Play связывает URL с action контроллера декларативно:

# conf/routes
GET / controllers.HomeController.index()
GET /hello/:name controllers.HomeController.hello(name: String)
GET /api/health controllers.HomeController.health()

Правила:

  • строка = метод HTTP + путь + вызов Controller.action(params);
  • :name — path-параметр, тип указывается явно;
  • порядок маршрутов важен — более специфичные выше общих;
  • после изменения routes Play в dev-режиме перекомпилирует проект автоматически.

Шаг 4 — контроллер

// app/controllers/HomeController.scala
package controllers

import javax.inject._
import play.api.mvc._
import play.api.libs.json._

@Singleton
class HomeController @Inject() (val controllerComponents: ControllerComponents)
extends BaseController {

def index(): Action[AnyContent] = Action {
Ok(views.html.index("Play + Scala"))
}

def hello(name: String): Action[AnyContent] = Action {
Ok(s"Hello, $name!")
}

def health(): Action[AnyContent] = Action {
Ok(Json.obj("status" -> "ok", "service" -> "hello-play"))
}
}

Разбор:

  • @Singleton + @Inject() — контроллер создаёт Guice один раз на приложение.
  • Action { ... } — синхронный обработчик; для I/O позже используют Action.async.
  • Ok(...) — ответ HTTP 200.
  • views.html.index(...) — Twirl-шаблон из app/views/.
  • Json.obj — встроенный Play JSON.

Шаг 5 — представление (Twirl)

@* app/views/index.scala.html *@
@(message: String)

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>@message</title>
</head>
<body>
<h1>@message</h1>
<p><a href="/hello/World">Проверить маршрут с параметром</a></p>
<p><a href="/api/health">JSON health</a></p>
</body>
</html>

Twirl компилируется в Scala-функции — опечатка в имени шаблона ловится на этапе сборки, а не в runtime. Файл index.scala.html → вызов views.html.index.


Шаг 6 — REST API "Заметки"

Соберём учебное API по тому же сценарию, что в Phoenix и Node.js: заметки хранятся в памяти (после перезапуска список пустой).

Модель и JSON

// app/models/Note.scala
package models

import play.api.libs.json._

case class Note(id: Long, text: String)

object Note {
implicit val format: OFormat[Note] = Json.format[Note]
}

Контроллер заметок

// app/controllers/NotesController.scala
package controllers

import javax.inject._
import models.Note
import play.api.libs.json._
import play.api.mvc._
import scala.collection.mutable

@Singleton
class NotesController @Inject() (val controllerComponents: ControllerComponents)
extends BaseController {

private val store = mutable.ArrayBuffer.empty[Note]
private var nextId = 1L

def list(): Action[AnyContent] = Action {
Ok(Json.toJson(store.toSeq))
}

def create(): Action[JsValue] = Action(parse.json) { request =>
val text = (request.body \ "text").asOpt[String].map(_.trim).filter(_.nonEmpty)
text match {
case None =>
BadRequest(Json.obj("error" -> "text is required"))
case Some(value) =>
val note = Note(nextId, value)
nextId += 1
store += note
Created(Json.toJson(note))
}
}

def delete(id: Long): Action[AnyContent] = Action {
val idx = store.indexWhere(_.id == id)
if (idx < 0) NotFound(Json.obj("error" -> "not found"))
else {
store.remove(idx)
NoContent
}
}
}

Маршруты API

# conf/routes — добавить
GET /api/notes controllers.NotesController.list()
POST /api/notes controllers.NotesController.create()
DELETE /api/notes/:id controllers.NotesController.delete(id: Long)

Проверка через curl

curl http://localhost:9000/api/health
curl http://localhost:9000/api/notes
curl -X POST http://localhost:9000/api/notes \
-H "Content-Type: application/json" \
-d "{\"text\": \"Изучить Play\"}"
curl http://localhost:9000/api/notes
curl -X DELETE http://localhost:9000/api/notes/1
МетодПутьУспехОшибка
GET/api/notes200 + массив
POST/api/notes201 + объект400 при пустом text
DELETE/api/notes/:id204404 если id не найден

Шаг 7 — конфигурация и production

conf/application.conf

play.server.http.port = 9000
play.filters.enabled += "play.filters.cors.CORSFilter"

play.filters.cors {
allowedOrigins = ["http://localhost:5173"]
allowedHttpMethods = ["GET", "POST", "DELETE", "OPTIONS"]
}

Для production секреты и строки БД выносят в переменные окружения — см. конфигурации и данные.

Сборка и Docker

sbt stage
# или
sbt docker:publishLocal
Production

In-memory store теряется при перезапуске — подключите PostgreSQL через JDBC, Slick или Doobie. HTTPS — за Nginx или Caddy. Секреты — только из окружения, никогда в git.


Архитектура запроса в Play

  • Endpoint — цепочка фильтров (CSRF, CORS, логирование).
  • Router — таблица из conf/routes.
  • Controller — бизнес-логика; тяжёлые задачи лучше выносить в сервис или Akka.

Тесты маршрутов

// test/controllers/HomeControllerSpec.scala
package controllers

import org.scalatestplus.play._
import play.api.test._
import play.api.test.Helpers._

class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest {

"HomeController GET /" should {
"return OK" in {
val controller = app.injector.instanceOf[HomeController]
val result = controller.index().apply(FakeRequest(GET, "/"))
status(result) mustBe OK
}
}

"NotesController POST /api/notes" should {
"reject empty text" in {
val controller = app.injector.instanceOf[NotesController]
val request = FakeRequest(POST, "/api/notes")
.withJsonBody(play.api.libs.json.Json.obj("text" -> ""))
val result = controller.create().apply(request)
status(result) mustBe BAD_REQUEST
}
}
}
sbt test

Частые ошибки

СимптомПричинаЧто сделать
Compilation error в routesНеверное имя action или тип параметраСверить с сигнатурой метода контроллера
Порт 9000 занятДругой экземпляр Playsbt -Dhttp.port=9001 run
Шаблон не найденФайл вне app/views/index.scala.htmlviews.html.index
Медленный cold startJVM + sbtНормально для dev; в prod — sbt stage
req.body пустой на POSTНет parse.jsonAction(parse.json) вместо Action
CORS в браузереФронт на другом портуНастроить play.filters.cors

Сравнение с соседними стеками

ФреймворкПлатформаКогда выбирают
PlayJVM, Scala/JavaREST + SSR, reactive I/O
Spring BootJVM, JavaEnterprise, огромная экосистема
PhoenixBEAM, ElixirWebSocket, LiveView — статья
RailsRubyБыстрый CRUD — Ruby

Оба JVM- и BEAM-стека опираются на сообщения и изоляцию — у Scala это Akka, у Elixir — процессы OTP.


Упражнения

  1. Добавьте GET /api/notes/:id — одна заметка или 404.
  2. Подключите форму POST (Play Forms) для HTML-создания заметки.
  3. Напишите тест на успешный POST с телом {"text": "test"}.
  4. Вынесите store в отдельный класс NoteRepository и внедрите через Guice.
  5. Сравните свой JSON API с Phoenix REST — какие статусы совпадают?

FAQ

Play только для Scala? Нет, есть шаблон play-java-seed, но типобезопасные Twirl и JSON удобнее в Scala.

Нужен ли Tomcat? Play встраивает Netty — отдельный servlet-контейнер для dev/prod не обязателен.

Play и Akka — одно и то же? Play — HTTP-слой; Akka — акторы и стримы. В Play 2.x была тесная связь; в Play 3 опираются на Pekko/Akka как на библиотеку по необходимости.

Когда Spark вместо Play? Spark — batch/stream big data на кластере, это другой класс задач.


План эволюции учебного API

  1. In-memory store для отладки маршрутов.
  2. PostgreSQL + Evolutions или Flyway.
  3. Единый формат ошибок JSON.
  4. Action.async + thread pool для БД.
  5. Логи, метрики, health-check для оркестратора.

Middleware и фильтры

Play обрабатывает запрос цепочкой фильтров до контроллера. В build.sbt уже подключены CSRF, security headers и gzip.

Логирование запросов

// app/filters/LoggingFilter.scala
package filters

import org.apache.pekko.stream.Materializer
import play.api.mvc._
import javax.inject._
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class LoggingFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext)
extends Filter {

def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
val start = System.currentTimeMillis()
nextFilter(requestHeader).map { result =>
val ms = System.currentTimeMillis() - start
println(s"${requestHeader.method} ${requestHeader.uri} -> ${result.header.status} (${ms}ms)")
result
}
}
}

Регистрация в conf/application.conf:

play.http.filters = "filters.LoggingFilter"

Разбор:

  • Filter — аналог middleware в Express.
  • nextFilter — следующий элемент цепочки; без вызова контроллер не выполнится.
  • Фильтр возвращает Future[Result] — Play реактивен end-to-end.

Форма POST и валидация

HTML-форма для создания заметки без JavaScript:

// app/controllers/NoteFormController.scala
package controllers

import javax.inject._
import play.api.data.Form
import play.api.data.Forms._
import play.api.mvc._

case class NoteFormData(text: String)

@Singleton
class NoteFormController @Inject() (val controllerComponents: ControllerComponents)
extends BaseController {

val form = Form(
mapping("text" -> nonEmptyText(maxLength = 500))(NoteFormData.apply)(NoteFormData.unapply)
)

def showForm() = Action { implicit request =>
Ok(views.html.noteForm(form))
}

def submit() = Action { implicit request =>
form.bindFromRequest().fold(
formWithErrors => BadRequest(views.html.noteForm(formWithErrors)),
data => Redirect(routes.NoteFormController.showForm()).flashing("success" -> s"Сохранено: ${data.text}")
)
}
}
@* app/views/noteForm.scala.html *@
@(noteForm: Form[controllers.NoteFormData])(implicit request: RequestHeader)

@helper.form(routes.NoteFormController.submit()) {
@helper.CSRF.formField
@helper.inputText(noteForm("text"), Symbol("_label") -> "Текст заметки")
<button type="submit">Отправить</button>
}

Маршруты:

GET /notes/new controllers.NoteFormController.showForm()
POST /notes/new controllers.NoteFormController.submit()
  • helper.CSRF.formField — токен для :browser pipeline.
  • bindFromRequest — парсинг application/x-www-form-urlencoded.
  • Ошибки валидации возвращают ту же форму с сообщениями.

Единый обработчик ошибок JSON

// app/controllers/JsonErrorHandler.scala
package controllers

import play.api.http.HttpErrorHandler
import play.api.libs.json.Json
import play.api.mvc._
import javax.inject._
import scala.concurrent._

@Singleton
class JsonErrorHandler @Inject() () extends HttpErrorHandler {

def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] =
Future.successful(
Results.Status(statusCode)(Json.obj("error" -> message, "status" -> statusCode))
)

def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = {
exception.printStackTrace()
Future.successful(
Results.InternalServerError(Json.obj("error" -> "internal server error"))
)
}
}
# conf/application.conf
play.http.errorHandler = "controllers.JsonErrorHandler"

Так клиент API всегда получает JSON, даже при 404/500 — удобно для SPA и mobile.


Подключение PostgreSQL (обзор)

После in-memory store следующий шаг — Evolutions + JDBC:

# conf/application.conf
db.default.driver = org.postgresql.Driver
db.default.url = "jdbc:postgresql://localhost:5430/notes"
db.default.username = "notes"
db.default.password = ${?DB_PASSWORD}
-- conf/evolutions/default/1.sql
# --- !Ups
CREATE TABLE note (
id BIGSERIAL PRIMARY KEY,
text TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);

# --- !Downs
DROP TABLE note;

Play применит миграции при sbt run. Детали SQL — первый проект SQL. ORM-слой — Slick или Doobie поверх того же JDBC.


Деплой — чек-лист

ШагКоманда / действие
1sbt clean stage
2Проверить target/universal/stage/bin/hello-play
3Передать APPLICATION_SECRET, DB_PASSWORD в env
4За reverse proxy — только HTTP внутри, HTTPS снаружи
5Health-check на /api/health для load balancer
target/universal/stage/bin/hello-play \
-Dhttp.port=9000 \
-Dconfig.resource=prod.conf

На Kubernetes — liveness probe на /api/health, readiness после прогрева JVM.


Расширенный FAQ

Можно ли смешивать Scala 2 и 3 в одном Play-проекте? Play 3.x ориентирован на Scala 3; для legacy смотрите Play 2.8 + Scala 2.13.

Как отдать static files? Каталог public/ — файлы доступны по корню URL; для CDN в prod копируйте в object storage.

Play и GraalVM native image? Экспериментально; типичный prod — JVM + -XX:+UseContainerSupport.

Где concurrency после Play? Долгие фоновые задачи — Akka actors или отдельный worker; Play Action должен быстро отдавать Future.


Связанные материалы

ТемаМатериал
Акторы на JVMAkka — основы
Big dataApache Spark на Scala
Phoenix на BEAMPhoenix — первая программа
Раздел ScalaScala — о разделе
HTTP теорияHTTP как основа веб-интеграций
Следующий шаг

После REST изучите Akka — concurrency через сообщения. Для аналитики больших объёмов — Spark.


Практикум — шаг 8: асинхронные Action

Синхронный Action блокирует поток Netty на время I/O. Для БД и внешних HTTP используйте Action.async:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def slowHealth(): Action[AnyContent] = Action.async {
Future {
Ok(Json.obj("status" -> "ok"))
}
}
ШагДействиеПроверка
1Вынести store в NoteServicesbt compile
2listAsync через Futurecurl /api/notes
3Тест NotesRouteSpec с route()sbt test

Сервисный слой

trait NoteService {
def list(): Future[Seq[Note]]
def create(text: String): Future[Either[String, Note]]
}

Контроллер только мапит Either в HTTP — как Context в Phoenix.


Практикум — шаг 9: единый JSON ошибок

case class ApiError(error: String, code: String)
object ApiError {
implicit val format: OFormat[ApiError] = Json.format[ApiError]
def badRequest(msg: String) = ApiError(msg, "bad_request")
}
HTTPcodeСлучай
400bad_requestПустой text
404not_foundDELETE без id
500internalException
Один формат
Все endpoints API должны возвращать ошибки в одной JSON-схеме — иначе клиент пишет несколько парсеров.

Практикум — шаг 10: интеграционные тесты

class NotesRouteSpec extends PlaySpec with GuiceOneAppPerSuite {
"POST /api/notes" should {
"create note" in {
val req = FakeRequest(POST, "/api/notes")
.withJsonBody(Json.obj("text" -> "test"))
val res = route(app, req).get
status(res) mustBe CREATED
}
}
}
sbt "testOnly controllers.NotesRouteSpec"

Практикум — шаг 11: Docker и health-check

FROM eclipse-temurin:17-jre
COPY target/universal/stage /app
EXPOSE 9000
CMD ["bin/hello-play", "-Dhttp.port=9000"]
livenessProbe:
httpGet:
path: /api/health
port: 9000
initialDelaySeconds: 60

JVM прогревается дольше Node — не занижайте initialDelaySeconds.


Сквозной воркшоп за 60 минут

МинЗадача
0–10sbt new playframework/play-scala-seed.g8
10–20Health + routes
20–35NotesController in-memory
35–45curl POST/GET/DELETE
45–55NotesRouteSpec
55–60CORS для :5173

Критерии: POST 201, пустой text 400, DELETE 404, тест зелёный.


Отладка — сценарии

СимптомШаг 1Шаг 2Шаг 3
404 routeconf/routesHTTP-методsbt compile
Пустой JSON POSTparse.jsonContent-Typeтело curl
Порт занятnetstat-Dhttp.port=9001kill процесс
Twirl errorapp/views/сигнатура @()имя views.html.*
CORS в браузереplay.filters.corsallowedOriginsOPTIONS

PowerShell и curl

$body = '{"text":"test"}'
Invoke-RestMethod -Uri http://localhost:9000/api/notes -Method POST -Body $body -ContentType "application/json"

Или сохраните тело в body.json и curl -d @body.json.


Дополнительные упражнения

  1. GET /api/notes/:id — 200 или 404.
  2. PATCH /api/notes/:id — обновление text.
  3. GET /api/notes?limit=10&offset=0 — пагинация.
  4. LoggingFilter — время запроса в мс.
  5. Таблица Play / Phoenix — маршруты, store, тесты.
Подсказка: PATCH
def update(id: Long): Action[JsValue] = Action(parse.json) { request =>
val text = (request.body \ "text").asOpt[String].map(_.trim).filter(_.nonEmpty)
// найти в store, заменить text или NotFound
}

Расширенный FAQ

Query-параметры?def search(q: Option[String]) в action.

Версия API /v1/? — префикс в routes или отдельный модуль.

Play 3 и Scala 3? — см. migration guide на playframework.com.

Статика? — каталог public/.

Swagger?play-swagger или openapi.yaml.

Сессия? — cookie; JWT — фильтр на Authorization.

Логировать body? — только dev, без PII.

После Play?Akka, Spark.

Тесты без сервера?route(app, FakeRequest) достаточно для routes.

Guice модуль?class Module extends AbstractModule для NoteService.

Эволюции БД?conf/evolutions + JDBC.

Reverse proxy? — Nginx/Caddy перед Play, HTTPS снаружи.

Метрики? — Micrometer, Prometheus endpoint.

WebSocket? — отдельный API; сравните Phoenix Channels.


Глоссарий (расширенный)

ТерминОпределение
ActionОбработчик HTTP-запроса
BodyParserРазбор тела: json, form, raw
ResultСтатус + заголовки + тело
StageStandalone после sbt stage
EvolutionsSQL-миграции Play
FilterMiddleware до контроллера
PekkoFork Akka в Play 3

Маршрут обучения

Smoke-тест
Сохраните curl-скрипт проверки API — пригодится при переходе на БД и async.


Учебный проект: NoteRepository и Guice

Интерфейс

trait NoteRepository {
def all(): Future[Seq[Note]]
def insert(text: String): Future[Note]
def delete(id: Long): Future[Boolean]
}

In-memory реализация

@Singleton
class InMemoryNoteRepository @Inject() (implicit ec: ExecutionContext)
extends NoteRepository {
private var data = Vector.empty[Note]
private var nextId = 1L
def all() = Future.successful(data)
def insert(text: String) = Future {
val n = Note(nextId, text.trim); nextId += 1; data :+= n; n
}
def delete(id: Long) = Future {
val b = data.size; data = data.filterNot(_.id == id); data.size < b
}
}

Module

class Module extends AbstractModule {
override def configure(): Unit =
bind(classOf[NoteRepository]).to(classOf[InMemoryNoteRepository])
}
play.modules.enabled += "Module"

JDBC в Future (обзор)

Future {
blocking {
// JDBC — только здесь
}
}

Для production: Slick, Doobie. Фоновые задачи — Akka.


Чек-лист code review

  • Секреты не в git
  • JSON ошибок единообразен
  • Route-тесты
  • CORS явный
  • Health для LB


Дополнительные практические сценарии (Play)

Сценарий A: первый запуск на Windows

ШагКомандаОжидаемый вывод
1java -version17+
2sbt --version1.9+
3sbt new playframework/play-scala-seed.g8шаблон создан
4cd hello-play && sbt run(Ok) порт 9000
5Браузер localhost:9000стартовая страница
Windows
При ошибке кодировки в PowerShell сохраняйте файлы UTF-8 и используйте Windows Terminal.

Сценарий B: отладка 404 на API

  1. Проверьте conf/routes — строка GET /api/notes.
  2. sbt compile — нет ошибок в routes.
  3. curl -v http://localhost:9000/api/notes — смотрите статус.
  4. Лог Play: уровень DEBUG для router при необходимости.

Сценарий C: smoke-тест скрипт

#!/bin/sh
set -e
BASE=http://localhost:9000
curl -sf "$BASE/api/health"
curl -sf "$BASE/api/notes" | grep '\[\]'
curl -sf -X POST "$BASE/api/notes" -H 'Content-Type: application/json' -d '{"text":"smoke"}'
curl -sf "$BASE/api/notes" | grep smoke
echo OK

Сценарий D: переход на PostgreSQL

ШагФайлДействие
11.sql evolutionsCREATE TABLE note
2application.confdb.default.url
3RepositoryJDBC в Future+blocking
4ТестTestcontainers Postgres

Сценарий E: сравнение с Phoenix

Заполните после прохождения Phoenix REST:

ЭлементPlayPhoenix
Маршрутыconf/routesrouter.ex
StoreArrayBuffer / RepoAgent / Ecto
Тестroute(app, req)ConnCase

FAQ — эксплуатация Play

Как уменьшить cold start JVM? Используйте sbt stage, CDS (Class Data Sharing), достаточный heap в контейнере.

Как логировать request id? Filter с X-Request-Id в MDC (Logback).

Как ограничить rate API? Filter или API gateway перед Play.

Как отдать OpenAPI? play-swagger или статический openapi.yaml в public.


Упражнения — контрольная точка

  1. Реализуйте NoteRepository с JDBC и Evolutions.
  2. Напишите smoke-скрипт из сценария C.
  3. Добавьте PATCH обновление текста заметки.
  4. Покройте PATCH route-тестом.
  5. Задокументируйте коды ошибок ApiError в README.
Критерии 16–20
  • После перезапуска данные в Postgres сохраняются
  • smoke.sh exit 0 на чистой БД
  • PATCH 200 с новым text, 404 для несуществующего id
  • README содержит таблицу HTTP кодов
Содержание