JavaをScalaに変更します。 基本アプリケーション

こんにちは、Habr。



夏が近づいてきて、休暇が近づいており、私の成果、JavaプラットフォームでWebアプリケーションを作成した経験を共有するための空き時間が現れました。 メイン言語として、Scalaを使用します。 これは小さなガイドのようなもので、Javaの経験を持つ人が徐々にScalaを使い始め、既存の成果を放棄しない方法です。



これは、アプリケーションの基本構造に注意を払う一連の記事の最初の部分です。 これは、Spring、Hibernate、JPA、JSP、およびその他の3〜4文字の略語で作業しているJavaを知っている人々を対象としています。 プロジェクトでScalaを可能な限り迅速かつ簡単に使用し始め、新しいアプリケーションを別の方法で設計する方法を説明します。 これらはすべてプロジェクトの周りで行われ、多くの要件を満たす必要があります。

1.アプリケーションは完全に閉じられ、承認後にのみ動作します

2.便利なAPIの存在(RESTは忘れてしまいます(履歴は既にあります)。SQLのようなクエリを使用して、Google AdWords APIのようなものを記述します)

3.アプリケーションサーバーなしで実行する機能

4.i18n

5.データベースの移行

6. Vagrantを介して開発環境を展開する必要があります

7.そして、ささいなことに、ロギング、デプロイメント...



これはすべて、アプリケーションの付随と開発が非常に簡単になるように行う必要があります。そのため、新しいディレクトリを追加するときにプログラマが2日間これを評価するような状況はありません。 私があなたに興味があるなら、猫をお願いします。







始めるには



たとえば、 ホルストマンの著書Scala for the Impatientめくるなど、Scalaの構文に慣れる必要があります。 言語がどのように機能するかを大まかに想像し、その中に何があるかを知るため。 ジャングルに直行せず、シンプルに始めて、複雑で面白いデザインを見た場所を覚えておくようにアドバイスします。 しばらくしてから、それらに戻って実装方法を確認し、そのようなことを試してください。 言語が大きく、すぐにすべての機能を使用すると問題が発生する可能性があります。



使用するもの



Scalaには、たとえばPlayフレームワークSBTSlickLiftを見る価値があります。 しかし、すでに取り組んできたものから始めます。 Mavenを介してアセンブリを行います。 Spring、Spring MVC、Spring Securityを基礎としてください。 データベースについては、 Squerylを使用してみましょう(その重さ、特定の機能、常に問題のあるLazyのせいでHibernateは好きではありません)。 私たちの前線は完全にAngularになり、スタイルについてはSASSを使用し、JSではなくCoffeeScriptを使用します(使用方法を示しますが、同じようにCoffeeを拒否できます)。 もちろん、 ScalaTestで統合とモジュールの両方のテストを作成します。 これは独自の特性を備えた別個のボリューム会話であるため、前面のテストは省略します。 APIは私たちにとって興味深いものになります。 サービスの概念があり、サービスにはメソッドがあり、クエリクエリのようなSQLもサポートします。 例:

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
  <properties> <scala-version>2.10.4</scala-version> </properties> <dependencies> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala-version}</version> </dependency> </dependencies> <build> <pluginManagement> <plugins> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>3.1.6</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> </plugin> </plugins> </pluginManagement> <plugins> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <executions> <execution> <id>scala-compile-first</id> <phase>process-resources</phase> <goals> <goal>add-source</goal> <goal>compile</goal> </goals> </execution> <execution> <id>scala-test-compile</id> <phase>process-test-resources</phase> <goals> <goal>testCompile</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <executions> <execution> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
      
      







すべてのソースはsrc / main / scalaディレクトリにあります。また、Javaで記述してsrc / main / javaに置くこともできます。 実際、そのような機会が必要な場合、ScalaクラスはJavaクラスで使用でき、その逆も可能です。 また、Spring、Spring MVC、Spring Security、Spring OAuthも必要になります。これらすべてを接続するのは難しくないと思うので、説明しません。 ニュアンスのうち、Jettyも必要になります(開発中にアプリケーションを実行します)。 その他のScala Config、ScalaTest。 テストをMavenで実行するには、Maven Surefireプラグインをオフにして、Scalatest Mavenプラグインを使用する必要があります

pom.xml
  <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.7</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <groupId>org.scalatest</groupId> <artifactId>scalatest-maven-plugin</artifactId> <version>1.0</version> <configuration> <reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory> <junitxml>.</junitxml> <filereports>WDF TestSuite.txt</filereports> </configuration> <executions> <execution> <id>test</id> <goals> <goal>test</goal> </goals> </execution> </executions> </plugin>
      
      







各クラスでロガーの初期化を記述しないために、 特性LazyLoggingを提供するライブラリを接続します。

 <dependency> <groupId>com.typesafe.scala-logging</groupId> <artifactId>scala-logging-slf4j_2.10</artifactId> <version>2.1.2</version> </dependency>
      
      







データベースの移行


次に、データベースについて考えます。 移行にはLiquibaseを使用します 。 最初に、すべての変更セットへのリンクを記述するファイルを作成します。

リソース/ changelog / db.changelog-master.xml
 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <include file="classpath:changelog/db.changelog-0.1.xml"/> </databaseChangeLog>
      
      







そして、最初の変更セットについて説明します。ここには、承認とOAuthのすべてのテーブルがあります

db.changelog-0.1.xml
 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <changeSet id="0.1-auth" author="andy.sumskoy@gmail.com"> <createTable tableName="users"> <column name="user_id" type="int" autoIncrement="true"> <constraints primaryKey="true" nullable="false"/> </column> <column name="username" type="varchar(255)"> <constraints unique="true" nullable="false"/> </column> <column name="password" type="varchar(255)"> <constraints nullable="false"/> </column> <column name="enabled" type="boolean" defaultValueBoolean="true"> <constraints nullable="false"/> </column> </createTable> <createTable tableName="authority"> <column name="authority_id" type="int" autoIncrement="true"> <constraints primaryKey="true" nullable="false"/> </column> <column name="name" type="varchar(255)"> <constraints unique="true" nullable="false"/> </column> </createTable> <createTable tableName="user_authorities"> <column name="user_id" type="int"> <constraints foreignKeyName="fk_user_authorities_users" referencedTableName="users" referencedColumnNames="user_id"/> </column> <column name="authority_id" type="int"> <constraints foreignKeyName="fk_user_authorities_authority" referencedTableName="authority" referencedColumnNames="authority_id"/> </column> </createTable> <addPrimaryKey columnNames="user_id, authority_id" constraintName="pk_user_authorities" tableName="user_authorities"/> <insert tableName="authority"> <column name="authority_id">1</column> <column name="name">ROLE_ADMIN</column> </insert> <insert tableName="authority"> <column name="authority_id">2</column> <column name="name">ROLE_USER</column> </insert> <insert tableName="authority"> <column name="authority_id">3</column> <column name="name">ROLE_POWER_USER</column> </insert> <createTable tableName="persistent_logins"> <column name="username" type="varchar(64)"> <constraints nullable="false"/> </column> <column name="series" type="varchar(64)"> <constraints nullable="false" primaryKey="true"/> </column> <column name="token" type="varchar(64)"> <constraints nullable="false"/> </column> <column name="last_used" type="timestamp"> <constraints nullable="false"/> </column> </createTable> <createTable tableName="oauth_client_details"> <column name="client_id" type="varchar(256)"> <constraints primaryKey="true" nullable="false"/> </column> <column name="resource_ids" type="varchar(256)"/> <column name="client_secret" type="varchar(256)"/> <column name="scope" type="varchar(256)"/> <column name="authorized_grant_types" type="varchar(256)"/> <column name="web_server_redirect_uri" type="varchar(256)"/> <column name="authorities" type="varchar(256)"/> <column name="access_token_validity" type="int"/> <column name="refresh_token_validity" type="int"/> <column name="additional_information" type="text"/> <column name="autoapprove" type="varchar(256)"/> </createTable> <createTable tableName="oauth_access_token"> <column name="token_id" type="varchar(256)"/> <column name="token" type="blob"/> <column name="authentication_id" type="varchar(256)"/> <column name="user_name" type="varchar(256)"/> <column name="client_id" type="varchar(256)"/> <column name="authentication" type="blob"/> <column name="refresh_token" type="varchar(256)"/> </createTable> <createTable tableName="oauth_refresh_token"> <column name="token_id" type="varchar(256)"/> <column name="token" type="blob"/> <column name="authentication" type="blob"/> </createTable> </changeSet> <changeSet id="0.1-auth-data" author="andy.sumskoy@gmail.com" context="test"> <insert tableName="users"> <column name="user_id">1</column> <column name="username">admin</column> <column name="password">dd28a28446b96db4c2207c3488a8f93fbb843af1eeb7db5d2044e64581145341c4f1f25de48be21b </column> <column name="enabled">true</column> </insert> <insert tableName="user_authorities"> <column name="user_id">1</column> <column name="authority_id">1</column> </insert> <insert tableName="user_authorities"> <column name="user_id">1</column> <column name="authority_id">2</column> </insert> <insert tableName="user_authorities"> <column name="user_id">1</column> <column name="authority_id">3</column> </insert> <insert tableName="oauth_client_details"> <column name="client_id">simple-client</column> <column name="client_secret">simple-client-secret-key</column> <column name="authorized_grant_types">password</column> </insert> </changeSet> </databaseChangeLog>
      
      







ここで、アプリケーションがテスト環境で起動された場合、管理ユーザーはすべての可能な権限を持つ管理パスワードでシステムに登録され、OAuth用のクライアントが作成されるという事実に注意する価値があります。 また、1つのDBMSのみを使用する場合は、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 = { } }
      
      





ここでは、いくつかのセクションを作成します。デフォルトでは、すべてのデフォルト設定が設定されています。devでは、テストは環境に応じて固有です。 また、アプリケーションの構成を担当する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 、またはENVをエクスポートできます。 configとmerjimの必要なブランチをデフォルト設定でロードします。 データベース接続プールを作成します。 ここでは、設定でプールサイズを作成できます。たとえば、すべてがオプションです。 さて、特定のランタイムでデータベース内の構造全体を完全に削除することをサポートするliquibaseを作成します。たとえば、アプリケーションでCIを使用する場合、すべてを削除すると便利です。 SpringでDataSourceとLiquibaseをBeanとして登録できるようになりました

root.xml
 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <bean id="config" class="com.sumskoy.habr.template.AppConfig"/> <bean id="dataSource" factory-bean="config" factory-method="dataSource"/> <bean id="liquibase" factory-bean="config" factory-method="liquibase"> <constructor-arg ref="dataSource"/> </bean> </beans>
      
      









Jettyの下から実行する


私は常に開発にJettyを使用します。アプリケーションサーバーで起動するたびに長い待ち時間がなくなります。また、大量のリソースがある場合、このプロセスには最大30秒かかり、非常に面倒です。 アプリケーションへのエントリポイントを作成します。

メイン
 object Main extends App { val server = new Server(8080) 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ブランチに配置するということだけを説明します。

単純なユーザーモデルを作成し、そのリポジトリを作成します。ここでは、1つのメソッドがあり、ユーザーを名前で返す必要があります。 現在のユーザーの名前を返すコントローラーを作成しましょう:

ユーザーエンティティ
 case class User(username: String, password: String, enabled: Boolean, @Column("user_id") override val id: Int) extends BaseEntity { def this() = this("", "", false, 0) }
      
      







モデルを回路に追加します。

コアスキーマ
 object CoreSchema extends Schema { val users = table[User]("users") on(users)(user => declare( user.id is autoIncremented, user.username is unique )) }
      
      







そして、簡単なリポジトリを作成します。 ほとんどの場合、これを必要とせず、コードを再度混乱させるだけなので、実装とのインターフェースは行いません。すぐに実装を記述します。 実装を突然変更したり、AOPを使用したりする必要がある場合、クラスからインターフェイスを選択することは難しくありませんが、現在は必要ありません。そのような必要性は近い将来にはありません。 私たちの生活を複雑にしないようにしましょう。

ユーザーリポジトリ
 @Repository class UserRepository { def findOne(username: String) = inTransaction { CoreSchema.users.where(_.username === username).singleOption } }
      
      









さて、シンプルなコントローラー

認証コントローラー
 @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 }
      
      





 <beans:bean id="converter" factory-bean="config" factory-method="converter"/> <mvc:annotation-driven> <message-converters register-defaults="true"> <beans:ref bean="converter"/> </message-converters> </mvc:annotation-driven>
      
      







テスト


ここで、テストを通じて承認動作を修正する必要があります。 クライアントがログインフォームとOAuthを介してログインできることを保証します。 このためのテストをいくつか書いてみましょう。

まず、Spring MVCを使用してすべてのテストの基本クラスを作成しましょう

IntegrationTestSpec
 @ContextConfiguration(value = Array("classpath:context/root.xml", "classpath:context/mvc.xml")) @WebAppConfiguration abstract class IntegrationTestSpec extends FlatSpec with ShouldMatchers with ScalaFutures { @Resource private val springSecurityFilterChain: java.util.List[FilterChainProxy] = new util.ArrayList[FilterChainProxy]() @Autowired private val wac: WebApplicationContext = null 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、最小化およびその他の便利なもののアセンブリを使用して、最前線を作成します。 YeomanBowerGruntと友達になり、 Vagrantを通じてプログラマー向けの環境を展開します。



これらはすべて、Bitbucket https://bitbucket.org/andy-inc/scala-habr-templateで表示できます



タイプミスや間違いを見つけた場合は、PMに連絡してください。 あらかじめご了承ください。



ご静聴ありがとうございました。意見を共有してください。



All Articles