Этот вопрос беспокоит меня уже некоторое время (надеюсь, я не единственный).Я хочу взять типичное 3-уровневое приложение Java EE и посмотреть, как оно может выглядеть реализованным с актерами.Я хотел бы выяснить, действительно ли имеет смысл делать такой переход и как я могу извлечь из этого выгоду, если это имеет смысл (может быть, производительность, лучшая архитектура, расширяемость, ремонтопригодность и т. Д.).
Вот типичные контроллеры (презентация), сервис (бизнес-логика), DAO (данные):
trait UserDao {
def getUsers(): List[User]
def getUser(id: Int): User
def addUser(user: User)
}
trait UserService {
def getUsers(): List[User]
def getUser(id: Int): User
def addUser(user: User): Unit
@Transactional
def makeSomethingWithUsers(): Unit
}
@Controller
class UserController {
@Get
def getUsers(): NodeSeq = ...
@Get
def getUser(id: Int): NodeSeq = ...
@Post
def addUser(user: User): Unit = { ... }
}
Вы можете найти что-то подобное во многих весенних приложениях.Мы можем взять простую реализацию, которая не имеет общего состояния и потому что не имеет синхронизированных блоков ... поэтому все состояние находится в базе данных, и приложение зависит от транзакций.Сервис, контроллер и дао имеют только один экземпляр.Таким образом, для каждого запроса сервер приложений будет использовать отдельный поток, но потоки не будут блокировать друг друга (но будут блокироваться вводом-выводом БД).
Предположим, мы пытаемся реализовать аналогичную функциональность с актерами.Это может выглядеть так:
sealed trait UserActions
case class GetUsers extends UserActions
case class GetUser(id: Int) extends UserActions
case class AddUser(user: User) extends UserActions
case class MakeSomethingWithUsers extends UserActions
val dao = actor {
case GetUsers() => ...
case GetUser(userId) => ...
case AddUser(user) => ...
}
val service = actor {
case GetUsers() => ...
case GetUser(userId) => ...
case AddUser(user) => ...
case MakeSomethingWithUsers() => ...
}
val controller = actor {
case Get("/users") => ...
case Get("/user", userId) => ...
case Post("/add-user", user) => ...
}
Я думаю, что здесь не очень важно, как реализованы экстракторы Get () и Post ().Предположим, я пишу основу для реализации этого.Я могу отправить сообщение контроллеру следующим образом:
controller !! Get("/users")
То же самое будет сделано контроллером и службой.В этом случае весь рабочий процесс будет синхронным.Еще хуже - я могу обрабатывать только один запрос за раз (в то же время все остальные запросы будут попадать в почтовый ящик контроллера).Поэтому мне нужно сделать все это асинхронным.
Есть ли какой-нибудь элегантный способ асинхронного выполнения каждого шага обработки в этой настройке?
Насколько я понимаю, каждый уровень долженкаким-то образом сохранить контекст полученного сообщения, а затем отправить сообщение на уровень ниже.Когда уровень ниже отвечает с некоторым сообщением результата, я должен быть в состоянии восстановить начальный контекст и ответить с этим результатом исходному отправителю.Это правильно?
Более того, на данный момент у меня есть только один экземпляр актера для каждого уровня.Даже если они будут работать асинхронно, я все равно смогу параллельно обрабатывать только один контроллер, сервис и дао-сообщение.Это означает, что мне нужно больше актеров того же типа.Что приводит меня к LoadBalancer для каждого уровня.Это также означает, что если у меня есть UserService и ItemService, я должен загрузить их оба по отдельности.
У меня есть ощущение, что я что-то не так понимаю.Все необходимые настройки кажутся слишком сложными.Что вы думаете об этом?
(PS: Было бы также очень интересно узнать, как транзакции БД вписываются в эту картину, но я думаю, что это излишне для этой темы)