Меняем Java на Scala. Базовое приложение
Здравствуй, Хабр.Лето на дворе, скоро отпуск и появилось немного свободного времени поделиться наработками, каким-то опытом по написанию Web приложений на Java платформе. Как основной язык я буду использовать Scala. Это будет похоже на небольшой гайд, как человеку с опытом Java постепенно начать использовать Scala и не отказываться от уже имеющихся у него наработок.
Это первая часть из серии статей, в которой мы уделим внимание базовой структуре приложения. Ориентирована на людей знающих Java, работавших со Spring, Hibernate, JPA, JSP и другими 3–4ех буквенными сокращениями. Я попытаюсь рассказать как максимально быстро и безболезненно начать использовать Scala в ваших проектах и по-другому проектировать ваше новое приложение. Все это будет вокруг проекта, который должен выполнять ряд требований:1. Приложение полностью закрыто, работаем только после авторизации2. Наличие удобного API (REST мы забудем (он уже история) и напишем что-то вроде Google AdWords API, со своим SQL like запросником)3. Возможность запуска на сервере приложений так и без него4. i18n5. Миграция БД6. Среда для разработки должна разворачиваться через Vagrant7. И, по мелочи, логирование, развертывание…
Все это нужно сделать так, чтобы сопровождать и развивать наше приложение было очень легко, чтобы не возникло такой ситуации, когда при добавление нового справочника программист оценивает это сроком в 2 дня. Если я вас заинтересовал, прошу под кат.
Для началаСтоит ознакомиться с синтаксисом Scala, например пролистав книгу Хорстмана «Scala для нетерпеливых». Чтобы примерно представлять, как устроен язык и знать что в нем есть. Я советую не лезть сразу в дебри, начинать с простого и запоминать где вы видели какие-нибудь сложные и интересные конструкции. Через время возвращаться к ним и смотреть как они реализованы, пробовать сделать подобные вещи. Язык большой и сразу использовать все возможности может быть проблематично.Что будем использовать
Для Scala есть много заточенных вещей, например стоит посмотреть на Play Framework, SBT, Slick, Lift. Но мы начнем с тех вещей с которыми уже работали. Сборку будем делать через Maven. Возьмем Spring, Spring MVC, Spring Security за основу. Для БД возьмем Squeryl (мне не нравится Hibernate из-за своих тяжеловесности, специфических особенностей, и вечно проблемным Lazy). Фронт у нас будет полностью на Angular, для стилей будет SASS, вместо JS возьмем CoffeeScript (я покажу как его использовать, но вы с той же легкостью можете и отказаться от Coffee). Ну и, само собой, мы будем писать тесты, как интеграционные, так и модульные, на ScalaTest. Тестирование фронта мы опустим, так как это отдельный со своими особенностями объемный разговор. API у нас будет интересное. В нем будет понятие сервиса, у сервиса будут методы, также мы будем поддерживать SQL like запросы на выборку. Например:
select id, name, bank from Organization where bank.id = : id // => [{id: 1, name: 'name', bank: {id: 1, name: 'bankname', node: 'Note'}}]
select name, bank.id, bank.name from Organization order by bank.name // => [{name: 'name', bank: {id: 1, name: 'bankname'}}]
К делу
Структура и зависимости
Первым делом создаем Maven проект и сразу подключим плагин для компиляции Scala.pom.xml
Тут стоит обратить внимание на то, что если наше приложение будет запущено в тестовой среде, то в системе будет зарегистрирован пользователь admin с паролем admin, имеющий все возможные права и будет создан клиент для OAuth. Также стоит отметить, что если вы собираетесь использовать только одну СУБД, то я бы рекомендовал писать changeset на SQL (об этом можно почитать в документации к liquibase).Теперь нужно добиться того, чтобы при старте приложения liquibase доводил нашу БД «до кондиции», но об этом чуточку позже.
Настройки приложения Для начала нам нужно создать resources/application.conf habr.template = { default = { db.url = «jdbc: postgresql://localhost/habr» db.user = «habr» db.password = «habr» }
test = { db.url = «jdbc: postgresql://localhost/test-habr» }
dev = {
} } Тут мы создаем несколько секций, в default выставлены все настройки по умолчанию, в dev, test специфичные в зависимости от окружения. Создадим также класс AppConfig, который будет отвечать за настройку нашего приложенияAppConfig class AppConfig {
val env = scala.util.Properties.propOrElse («spring.profiles.active», scala.util.Properties.envOrElse («ENV», «test»)) val conf = ConfigFactory.load () val default = conf.getConfig («habr.template.default») val config = conf.getConfig («habr.template.» + env).withFallback (default)
def dataSource = { val ds = new BasicDataSource ds.setDriverClassName («org.postgresql.Driver») ds.setUsername (config.getString («db.user»)) ds.setPassword (config.getString («db.password»)) ds.setMaxActive (20) ds.setMaxIdle (10) ds.setInitialSize (10) ds.setUrl (config.getString («db.url»)) ds }
def liquibase (dataSource: DataSource) = { val liquibase = new LiquibaseDropAllSupport () liquibase.setDataSource (dataSource) liquibase.setChangeLog («classpath: changelog/db.changelog-master.xml») liquibase.setContexts (env) liquibase.setShouldRun (true) liquibase.dropAllContexts += «test» liquibase }
}
Мы определяем среду, в которой запущено приложение, это может быть — -Dspring.profiles.active, либо export ENV. Загружаем нужную ветку конфига и мерджим с настройками по умолчанию. Создаем пулл соединений с БД. Тут еще можно вынести в настройки размер пула, к примеру, все по желанию. Ну и создаем liquibase, который поддерживает полное удаление всей структуры в БД для определенных сред выполнения, к примеру, удаление всего может пригодиться если вы используете CI для вашего приложения. Теперь можно зарегистрировать DataSource и Liquibase как Bean в Springroot.xml
val webAppContext = new WebAppContext () webAppContext.setResourceBase («src/main/webapp») webAppContext.setContextPath (»/») webAppContext.setParentLoaderPriority (true) webAppContext.setConfigurations (Array ( new WebXmlConfiguration () ))
server.setHandler (webAppContext) server.start () server.join () } Безопасность Я не буду расписывать как настроить Spring Security, единственное оговорю, что для авторизации мы будем использовать /login.html, как освной url — /index.html, все API у нас будет в ветке /api.Давайте сделаем простую модель User, сделаем к ней Repository, в которой пока будет один метод, он должен будет возвращать пользователя по имени. Сделаем контроллер, который возвращает имя текущего пользователя: User Entity case class User (username: String, password: String, enabled: Boolean, @Column («user_id») override val id: Int) extends BaseEntity { def this () = this (»,», false, 0) } Добавим модель в схемуCore Schema object CoreSchema extends Schema { val users = table[User](«users»)
on (users)(user => declare ( user.id is autoIncremented, user.username is unique )) } И напишем простой Repository. Я не буду делать интерфейс с реализацией, сразу напишу реализацию, так как в большинстве случаев в этом нет нужды, только лишний раз захламляет код. Если вдруг понадобиться менять реализацию или использовать AOP, то выделить интерфейс из класса не составит труда, но сейчас нам это не нужно и такая необходимость не предвидится в ближайшем будущем. Не будем усложнять себе жизнь.User Repository @Repository class UserRepository {
def findOne (username: String) = inTransaction { CoreSchema.users.where (_.username === username).singleOption }
} Ну и простой контроллер
AuthController @Controller @RequestMapping (Array («api/auth»)) class AuthController @Autowired ()(private val userRepository: UserRepository) {
@RequestMapping (Array («check»)) @ResponseBody def checkTokenValid (principal: Principal): Map[String, Any] = {
userRepository.findOne (principal.getName) match { case Some (user) => Map[String, Any](«username» → user.username, «enabled» → user.enabled) case _ => throw new ObjectNotFound () }
}
} Тут стоит упомянуть что для сериализации в JSON мы используем Jackson. К нему есть библиотека, которая позволяет работать в классами и коллекциями Scala, для этого определим правильный маппер для Spring def converter () = { val messageConverter = new MappingJackson2HttpMessageConverter ()
val objectMapper = new ObjectMapper () with ScalaObjectMapper objectMapper.registerModule (DefaultScalaModule) messageConverter.setObjectMapper (objectMapper)
messageConverter
}
new TestContextManager (this.getClass).prepareTestInstance (this)
var builder = MockMvcBuilders.webAppContextSetup (this.wac) for (filter <- springSecurityFilterChain.asScala) builder = builder.addFilters(filter)
val mockMvc = builder.build () val md = MediaType.parseMediaType («application/json; charset=UTF-8»)
val objectMapper = new ObjectMapper () with ScalaObjectMapper objectMapper.registerModule (DefaultScalaModule) } И напишем наш первый тест для авторизации it should «Login as admin through oauth with default password» in { val resultActions = mockMvc.perform ( get (»/oauth/token»). accept (md). param («grant_type», «password»). param («client_id», «simple-client»). param («client_secret», «simple-client-secret-key»). param («username», «admin»). param («password», «admin»)). andExpect (status.isOk). andExpect (content.contentType (md)). andExpect (jsonPath (»$.access_token»).exists). andExpect (jsonPath (»$.token_type»).exists). andExpect (jsonPath (»$.expires_in»).exists)
val contentAsString = resultActions.andReturn.getResponse.getContentAsString
val map: Map[String, String] = objectMapper.readValue (contentAsString, new TypeReference[Map[String, String]] {}) val access_token = map.get («access_token»).get val token_type = map.get («token_type»).get
mockMvc.perform ( get (»/api/auth/check»). accept (md). header («Authorization», token_type + » » + access_token)). andExpect (status.isOk). andExpect (content.contentType (md)). andExpect (jsonPath (»$.username»).value («admin»)). andExpect (jsonPath (»$.enabled»).value (true)) } И тест для авторизации через форму it should «Login as admin through user form with default password» in { mockMvc.perform ( post (»/auth/j_spring_security_check»). contentType (MediaType.APPLICATION_FORM_URLENCODED). param («j_username», «admin»). param («j_password», «admin»)). andExpect (status.is3xxRedirection ()). andExpect (header ().string («location»,»/index.html»)) } На этом мы пока остановимся. В следующей статье мы сделаем фронт, со сборкой SASS, CoffeeScript, минимизацией и прочими удобными штуками. Подружимся с Yeoman, Bower, Grunt, а также сделаем развертывание среды для программиста через Vagrant.
Все это можно посмотреть на Bitbucket https://bitbucket.org/andy-inc/scala-habr-template.
Если нашли опечатку или ошибку, пишите в ЛС. Заранее благодарю за понимание.
Спасибо за ваше внимание, делитесь мнениями и не минусуйте молча.
