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

Комментарии (24)


  1. ShadowsMind
    29.07.2015 14:53
    +3

    Ничего не имею против ваших трудов, но лучше юзать готовые решения от серьезных поставщиков, имхо. Как показывает практика свои подделки очень редко бывают лучше аналогов, а проблемы багов и скудность функционала будут сводить на нет продуктивность.


    1. igumnov Автор
      29.07.2015 15:15
      +4

      Согласен. Но прикольно же велосипед изобретать -) Особенно в учебных целях…


  1. arsen_gl
    29.07.2015 16:15
    +1

    я обычно в место main метода, использую:

    object Main extends App {
        print("Hello, world!!!")
    }
    


    1. igumnov Автор
      29.07.2015 19:01
      +1

      тема -) не революция, но все равно меньше букв


  1. aparamonov
    29.07.2015 17:23
    +1

    Неидиоматично. Модель должна быть иммутабельной. ОРМ железно работает на рефлекшне — совсем не очень, все в мире скалы стараются этого избегать. Конфигурация сервера задается статически… тестировать пробовали? А если и пробовали, то получилось с костылями, верно?

    Если реально хочется заняться велосипедостроением — скопируйте мега проект типа slick — заодно всю функциональщину по полочкам разберете.

    Идеальное для меня решение — найти работу на скале. Если не хочется/не получается — контрибьютте в опен соурс — там полно скала проектов и все будут счастливы, и Вам дадут подробный фидбек, и польза обществу.

    По мне так Ваши статьи меня бы жестко напрягли как потенциального работодателя — слишком велосипедисто и плохой код.


    1. igumnov Автор
      29.07.2015 17:42
      +1

      Да — я еще тот говнокодер, но я учусь и показываю результат… Я не ищу работодателя. Просто учусь.


    1. ShadowsMind
      29.07.2015 18:45

      Вот бы кто-нибудь из серьезных дядек взял на себя и довел до production ready Spring Scala… Эх мечты, мечты…


      1. solver
        29.07.2015 18:49

        Так его и не доводят, потому что он нафиг не нужен…


        1. igumnov Автор
          29.07.2015 18:59

          Нужен-нужен!


          1. solver
            29.07.2015 19:44

            А для чего нужен то?
            Есть хоть один вменяемый пример?
            Конечно же в идеологии Scala, а не просто «смотрите как я могу».


        1. ShadowsMind
          29.07.2015 19:22
          +1

          Просто слишком разные модели и подходы. Spring не далеко от JavaEE ушел, с километром атотаций и всех этих Java магий через рефлекшн, кодогенерацию и т.д. А в Scala как бы постоянно намекают, что анотации и рефлекшн это не ок.
          Суть в том, что для больших проектов альтернатив Spring'у на Java нет(всякие Gouce не предлагать — в них и 10% инфраструктуры спринга нет… ), а на Scala мне совершенно не нравятся фрэймворки, вот и приходится писать на привычном Spring'е, только с приятным синтаксисом Scala ). Может быть я просто еще не привык, еще даже пол года нет как на Scala стал писать, глядишь скоро наступит фп головного мозга и стану юзать всякие pray, spray etc.


          1. igumnov Автор
            29.07.2015 19:27

            Ну вот я тоже планирую в своем коммерческом проекте на Spring Boot, Spring Data, Spring MVC новые модули писать на Scala…


          1. solver
            29.07.2015 19:52
            +1

            Тут дело не в ФП, оно ортогонально спрингу.
            Достаточно сломать себя один раз, поломать это привычное и потому кажущееся удобным использование спринга. И попробовать scala-way. Не потому что он идеален или еще какие тупые фанатские приблуды.
            Просто посмотреть как можно делать приложения по другому, на тех же play, spray, akka, slick.
            Увидеть разницу. И спринг уже точно не особо захочется.
            Хотя, лично я, не вижу ничего такого в использовании связки spring+scala.
            Это всего лишь инструменты.
            Просто я уже побывал на той стороне… и знаю, что есть инструменты лучше.


  1. fogone
    29.07.2015 18:09

    Получилось что если у тебя обычный не Highload-проект, то тебе совсем не упали Play и Spray и альтернатив нет для реализации одного из преимуществ Scala, писать меньше букв чем в Java. Особенно смотришь в сторону Spring boot, Spring Data и тд. Там все мило, коротко и красиво.


    Если только в этом дело, то попробуйте kotlin. И писать меньше и оборачивать ничего не надо, можно использовать любимые фреймворки.


    1. igumnov Автор
      29.07.2015 18:13

      Интересно — буду изучать.


  1. btd
    29.07.2015 19:38
    +3

    Качество кода у вас конечно (если честно то для скалы это ж*па)…
    1. Берем

    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)
          }
    

    2. ???
    3. Profit
    val user = ORM.findOne[ExampleUser](name)
    user.map(u => new User(u.userName, u.userPassword, Array("user_role")))
    


    Про God объект, WebServer и говорить нечего.


    1. igumnov Автор
      29.07.2015 19:42

      те если там пусто map не сработает? удобно — сильно давно читал книгу — видать забыл что так можно…

      про WebServer что не так-то? расскажите — учимся же…


      1. btd
        29.07.2015 19:45

        Про WebServer.

        scala> Option(null)// можно сразу писать None
        res27: Option[Null] = None
        
        scala> Option(null).map(_ => 10)
        res28: Option[Int] = None
        
        scala> Option("abc").map(_ => 10)
        res29: Option[Int] = Some(10)
        


        1. igumnov Автор
          29.07.2015 20:06
          -2

          я почему-то cli от Scala не юзаю. Наверное надо ввести в практику что бы простые вещи смотреть и проверять.


          1. igumnov Автор
            30.07.2015 14:50

            В итоге начал читать вот эти два источника

            twitter.github.io/scala_school/ru

            twitter.github.io/effectivescala/index-ru.html

            они довольно сжато подают информацию и когда есть минимальный опыт кодинга и прочтенная книжка по Scala очень легко воспринимается сейчас информация

            в итоге вижу что я в своей либе писал на Scala но в стиле Java -)))


            1. igumnov Автор
              30.07.2015 15:56

              Кстати вот щас сам переделал

              Было

                  if (params.isDefined) {
                    val messageParameters: Array[AnyRef] = params.get.toArray
                    resolver.resolveMessage(null, messageKey, messageParameters).getResolvedMessage
                  } else {
                    resolver.resolveMessage(null, messageKey, null).getResolvedMessage
              
                  }
              


              стало

                  val messageParameters: Array[AnyRef] = params.getOrElse(List[String]()).toArray
                  resolver.resolveMessage(null, messageKey, messageParameters).getResolvedMessage
              


  1. some_x
    30.07.2015 08:57
    +1

    Я не специалисит по java, я специалист по C#. Но тем не менее заглавная картинка удивила.
    В яве же есть лямбды(стрелочные функции)? А extension методы?
    Что тогда мешает в яве реализовать аналогичный метод flatMap, для класса List или интерфейса который он реализует?


    1. igumnov Автор
      30.07.2015 10:53

      Да все верно — картинка древняя. Просто в свое время не было и Scala еще более привлекательно выглядела для Java разработчиков.


    1. Googolplex
      30.07.2015 11:23
      +4

      В джаве нет экстеншн-методов, но в восьмёрке добавили дефолтные методы для интерфейсов, через которые на существующих интерфейсах коллекций добавили операции для работы со стримами. Поэтому, начиная с восьмёрки, flatMap в джаве есть, он даже называется так же:

      orders.stream().flatMap(o -> o.getProducts().stream()).collect(Collectors.toList())
      

      Но это всё равно более громоздко, чем в скале. И вообще, библиотека стримов в джаве менее практичная, чем в скале — там даже нормального foldLeft нет.