Изобретаем велосипед на 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 по параметру удобства использования.

image

Начал я изучение Scala с прочтения книги Хорстман К. — Scala для нетерпеливых (есть русское издание). Потом был перерыв в несколько месяцев во время который я обтачивал свой Framework на Java и с сомнением вспоминал о плюшках из Scala. Но в итоге я все-таки решился и начал писать библиотеку на Scala. Я заметил два нюанса:

  • Когда пишешь на Scala несколько дней (например весь Weekend), то возвращаясь в свой коммерческий проект на Java начинаешь выть, как много букв надо писать и как не удобно, а вот на Scala это куда проще.
  • Вызывая API написанное в Java из Scala приходиться «приседать» и многие удобства Scala сводятся на нет (ну не все, но всё что нового с Java 6-7: лямды, мульти-параметры методов и тд). По этой причине и надо писать на 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

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd"> <!-- Указываем что нужно использовать декоратор layout из layout.html --> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"       layout:decorator="layout"> <body> <!-- Объявляем наш контент блок который будет подставлен в layout.html --> <div layout:fragment="content">     <form name="form" action="/j_security_check" method="POST">         <div class="modal-header">             <h3 class="modal-title" th:text="#{login.title}"></h3> <!-- Берем название окна из ленг файла -->         </div>         <div class="modal-body">             <div class="form-group">                 <input type="text" name="j_username" class="form-control" value="" placeholder="Login"/>             </div>             <div class="form-group">                 <input type="password" name="j_password" class="form-control" placeholder="Password"/>             </div>             <div class="form-group">                 <button type="submit" id="login" class="btn btn-primary">OK</button>             </div>         </div>     </form>  </div> </body> </html> 

index.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd"> <!-- Указываем что нужно использовать декоратор layout из layout.html --> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"       layout:decorator="layout"> <body> <!-- Объявляем наш контент блок который будет подставлен в layout.html --> <div layout:fragment="content">     <!-- Подключаем наш контроллер на AngularJS-->     <script src="/js/user.js"></script>     <h1 th:text="${time}"></h1> // Выводим текущее время переданное в модель     <!-- Обозначаем область действия нашего контроллера UserCtrl -->     <div ng-controller="UserCtrl">         <table class="table">             <thead>             <tr>                 <th>Name</th>                 <th>Password</th>                 <th></th>             </tr>             </thead>             <tbody>            <!-- В цикле заполняем таблицу пользователями -->             <tr ng-repeat="user in users">                 <td>{{user.userName}}</td>                 <td>{{user.userPassword}}</td>                <!-- По клику на крестик вызываем функцию на контроллере для удаления пользователя -->                 <td><a href="#"><span class="glyphicon glyphicon-remove" tooltip="Delete" ng-click="deleteUser(user)"/></a></td>             </tr>             </tbody>         </table>         <div ng-model="user">         <!-- Форма добавления пользователя -->             <div class="form-group">                 <input type="text" class="form-control" ng-model="user.userName" placeholder="Login"/>             </div>             <div class="form-group">                 <input type="password" class="form-control" ng-model="user.userPassword" placeholder="Password"/>             </div>             <div class="form-group">                 <!-- По клику на кнопке вызываем функцию в контроллере добавляющую пользователя -->                 <button class="btn btn-primary" ng-click="addUser(user)">Add</button>             </div>         </div>     </div> </div> </body> </html> 

layout.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd"> <!-- Область действия нашего приложения на AngularJS --> <html ng-app="com.igumnov.common.example"> <head>     <title>Title</title>     <link rel="stylesheet" href="/static/bootstrap/3.3.1/css/bootstrap.min.css" />     <meta charset="utf-8" />     <meta http-equiv="X-UA-Compatible" content="IE=edge" />     <meta name="viewport" content="width=device-width, initial-scale=1" /> </head> <body> <script src="/static/angularjs/1.3.8/angular.min.js"></script> <script src="/static/angularjs/1.3.8/angular-resource.min.js"></script> <script src="/static/angular-ui-bootstrap/0.12.0/ui-bootstrap-tpls.min.js"></script> <div class="container"> <!-- Сюда будет вставляться контентный блок -->     <div layout:fragment="content"></div> </div> </body> </html> 

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, с радостью готовы выслушать критику по своему коду. Нужен фидбек что бы понять что делаю не так.

ссылка на оригинал статьи http://habrahabr.ru/post/263749/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *