Изобретаем велосипед на Scala — свой Framework ORM, WebServer (RESTful и MVC)
Эта статья логическое продолжение моего предыдущего поста Изобретаем велосипед на Java — пишем свой Framework (DI, ORM, MVC and etc). Прошло несколько месяцев как был опубликован мой первый Framework на Java. Мне повезло и я свою разработку применил в коммерческом проекте. На практике выяснилось, что мои многие предположения, как будет этим удобно пользоваться, оказались не верны. Но я не филонил и переписывал и дополнял библиотеку. Если вы сравните API в моей первой статье, с тем, что сейчас там есть в библиотеке, то увидите прогресс.
Но вернемся к Scala. Я смотрел как устроены Framework-и Play и Spray. Заметил такой тренд, что они все заточены на архитектуру в стиле Акторов (актеров) для обеспечения Highload. Это конечно все правильно и перспективно. Но почему-то погоня за этим сделала кодинг проектов несколько чуть более сложным. Получилось что если у тебя обычный не Highload-проект, то тебе совсем не упали Play и Spray и альтернатив нет для реализации одного из преимуществ Scala, писать меньше букв чем в Java. Особенно смотришь в сторону Spring boot, Spring Data и тд. Там все мило, коротко и красиво. А в Scala библиотеки в актор-стиле похожи на первые версии J2EE по параметру удобства использования.

Начал я изучение Scala с прочтения книги Хорстман К. — Scala для нетерпеливых (есть русское издание). Потом был перерыв в несколько месяцев во время которого я обтачивал свой Framework на Java и с сомнением вспоминал о плюшках из Scala. Но в итоге я все-таки решился и начал писать библиотеку на Scala. Я заметил два нюанса:
- Когда пишешь на Scala несколько дней (например весь Weekend), то возвращаясь в свой коммерческий проект на Java начинаешь выть, как много букв надо писать и как не удобно, а вот на Scala это куда проще.
- Вызывая API написанное в Java из Scala приходиться «приседать» и многие удобства Scala сводятся на нет (ну не все, но всё что нового с Java 6–7–8: лямды, мульти-параметры методов и тд). По этой причине и надо писать на Scala обвертки вокруг стандартной библиотеки или Framework-ов на Java, что бы ими было пользоваться комфортно и удобно
Сначала, я свою библиотеку на Java обвертывал на Scala (работа с Json). Потом где явно уперся в архитектурные особенности в глубине своей Java-реализации с Jetty, переписал этот участок с Java на Scala. И там и там я получил колоссальный опыт. Я лично убедился, что писать код на Scala действительно короче и быстрей (особенно когда запоминаешь синтаксис языка). И что можно без проблем весь наработанный багаж существующих Java библиотек и Framework-ов использовать в своем Scala-проекте. Я уж молчу о магии Scala, которая позволяет делать DSL (Предметно-ориентированный язык). Вспомним причину возникновения ООП (Объе́ктно-ориенти́рованное программи́рование), это желание сделать код читабельным и понимабельным на уровне человеко-понятных выражений. Scala это позволяет сделать еще на более высоком уровне. Например можно описывать свои управляющие структуры похожие на if, for, switch и тд (применил это в ORM для тарнзакций).
В итоге код фреймворка на гитхабе github.com/evgenyigumnov/scala-common
Пример веб-сервиса использующего этот фреймворк на гитхабе github.com/evgenyigumnov/example-scala
Структура примера:
./:
build.sbt
./javascript:
user.js
./pages:
index.html
layout.html
login.html
./sql:
1.sql
./locale:
messages_en.properties
./src/main/scala/com/igumnov/scala-2.11/example:
ExampleUser.scala
SiteServer.scala
build.sbt
name := "example-scala"
version := "1.0"
scalaVersion := "2.11.7"
libraryDependencies += "com.igumnov.scala" % "scala-common_2.11" % "0.5" // Подключаем наш фреймворк
libraryDependencies += "com.h2database" % "h2" % "1.4.187" // подключаем БД
// подключаем Bootstrap, AnglularJS и тд из webjars проекта
libraryDependencies += "org.webjars" % "angular-ui-bootstrap" % "0.12.0"
libraryDependencies += "org.webjars" % "angularjs" % "1.3.8"
libraryDependencies += "org.webjars" % "bootstrap" % "3.3.1"
SiteServer.scala
package com.igumnov.scala.example
import java.util.Calendar
import com.igumnov.scala._
import com.igumnov.scala.webserver.User
object SiteServer {
def main(args: Array[String]) {
// Создаем пул коннекций к БД (максимум 3 коннекта)
ORM.connectionPool("org.h2.Driver", "jdbc:h2:mem:test", "SA", "", 1, 3)
// Накатываем на базу объявления таблиц или оно это пропускает если уже делало
ORM.applyDDL("sql")
// Размер пула нитей для вебсервера
WebServer.setPoolSize(5,10)
// Задаем начальные параметры веб-сервера
WebServer.init("localhost", 8989)
// Определям откуда брать обьекты с пользователями
WebServer.loginService((name) => {
val user = ORM.findOne[ExampleUser](name)
if (user.isDefined) {
Option(new User(user.get.userName, user.get.userPassword, Array[String]("user_role")))
} else {
Option(null)
}
})
// Говорим что у нас включена безопасность которая должна работать по URL-ам
WebServer.securityPages("/login", "/login?error=1", "/logout")
// Ограничиваем доступ только для пользователям с ролью user_role
WebServer.addRestrictRule("/*", Array("user_role"))
// Даем доступ для всех к статическому контенту
WebServer.addAllowRule("/static/*")
// Указываем откуда брать этот статический контент из classpath от webjars
WebServer.addClassPathHandler("/static", "META-INF/resources/webjars")
// Даем доступ для всех к нашим Java Script-ам
WebServer.addAllowRule("/js/*")
// Указываем в какой папке на винте лежат наши Java Script
WebServer.addStaticContentHandler("/js", "javascript")
// Определяем каким образом серверу вычислять какой язык (в примере захардкожен всего один единственный язык)
// И указываем в каком файле для этого языка лежат ключи - значения
WebServer.locale(Map("en" -> "locale/messages_en.properties"), (rq,rs)=>{
"en"
})
// Указываем в какой папке на винте лежат шаблоны страниц
WebServer.templates("pages",0)
// Добавляем контроллер по урл "/", который добавляет в модель текущее время и говорит, что нужно отобразить index.html
WebServer.addController("/", (rq, rs,model) => {
model += "time" -> Calendar.getInstance.getTime
"index"
})
// Добавляем контроллер по урл "/login", который говорит, что нужно отобразить login.html
WebServer.addController("/login", (rq, rs,model) => {
"login"
})
// Добавляем REST-контроллер по урл "/rest/user" и указываем что могут методом POST/PUT прислать JSON-объект типа ExampleUser
WebServer.addRestController[ExampleUser]("/rest/user", (rq, rs, obj) => {
rq.getMethod match {
case "GET" => { // Прилетел GET запрос
ORM.findAll[ExampleUser]() // Извлекаем список пользователей
}
case "POST" => { // Прилетел POST запрос
val user = obj.get
user.userPassword = WebServer.crypthPassword(user.userName, user.userPassword)
ORM.insert(obj.get) // Вставляем его в БД
}
case "DELETE" => { // Прилетел DELETE запрос
val user = ORM.findOne[ExampleUser](rq.getParameter("userName"))
// Если юзер demo не даем удалять
if(user.get.userName == "demo") throw new Exception("You cant delete demo user")
ORM.delete(user.get)
user.get
}
}
})
// Для того что бы эксепшены в рест сервисе выдавались в виде JSON возвращаем ошибку в виде обьекта Error
WebServer.addRestErrorHandler((rq, rs, e) => {
object Error{
var message:String =_
}
Error.message = e.getMessage
Error
})
val users = ORM.findAll[ExampleUser] // Берем из БД всех пользователей
if(users.size==0) { // В таблице с пользователями пусто
val user = new ExampleUser
user.userName="demo"
user.userPassword=WebServer.crypthPassword(user.userName, "demo")
ORM.insert(user) // Добавляем demo/demo пользователя в БД
}
// Если до этого места кода дошло управление и ничего не вывалилось по Exception, то стартуем веб-сервер :)
WebServer.start
}
}
ExampleUser.scala
// Данный класс используется для JSON сериализации и десериализации и также для меппинга в БД
package com.igumnov.scala.example
import com.igumnov.scala.orm.Id
class ExampleUser {
@Id(autoIncremental = false)
var userName: String = _
var userPassword: String = _
}
1.sql
# Создаем таблицу в БД где будем хранить через ORM объекты типа ExampleUser.class
CREATE TABLE ExampleUser (userName VARCHAR(255) PRIMARY KEY, userPassword VARCHAR(255))
login.html
index.html
layout.html
Title
user.js
angular.module('com.igumnov.common.example', ['ui.bootstrap', 'ngResource'])
.factory('User', ['$resource', function ($resource) { // Объявляем REST-ресурс User
return $resource('/rest/user', {}, {
list: { // Список юзеров
method: 'GET',
cache: false,
isArray: true // Результат вызова массив
},
add: { // Добавляем юзера
method: 'POST',
cache: false,
isArray: false // Результат вызова один объект
},
delete: { // Удаляем юзера
method: 'DELETE',
cache: false,
isArray: false // Результат вызова один объект
}
});
}])
.controller('UserCtrl', function ($scope, User) { // Обьявляем наш контроллер UserCtrl
$scope.users = User.list({}); // Заполняем список пользователя при инициализации контроллера
$scope.addUser = function (user) { // Функция добавления пользователя
User.add({},user,function (data) { // Дергаем REST-интерфейс
$scope.users = User.list({}); // В случае успеха, перезаполняем список пользователей
}, function (err) {
alert(err.data.message); // В случае ошибки, выводим ошибку
});
}
$scope.deleteUser = function (user) { // Функция удаления пользователя
User.delete({"userName" : user.userName},user,function (data) { // Дергаем REST-интерфейс
$scope.users = User.list({}); // В случае успеха, перезаполняем список пользователей
}, function (err) {
alert(err.data.message); // В случае ошибки, выводим ошибку
});
}
});
messages_en.properties
login.title=Login
В заключении могу сказать, что Scala буду внедрять в коммерческих проектах написанных на Java. Не собираюсь там ничего переписывать из сделанного на Java, но новые модули, точно буду писать на Scala. Причина такого решения: на Scala быстрее, удобнее, чище код. В общем это эффективно.
PS Так как я новичок в Scala, с радостью готов выслушать критику по своему коду. Нужен фидбек, чтобы понять что делаю не так.
