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フレヌムワヌク 、 SBT 、 Slick 、 Liftを芋る䟡倀がありたす。 しかし、すでに取り組んできたものから始めたす。 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、最小化およびその他の䟿利なもののアセンブリを䜿甚しお、最前線を䜜成したす。 Yeoman 、 Bower 、 Gruntず友達になり、 Vagrantを通じおプログラマヌ向けの環境を展開したす。

これらはすべお、Bitbucket https://bitbucket.org/andy-inc/scala-habr-templateで衚瀺できたす 。

タむプミスや間違いを芋぀けた堎合は、PMに連絡しおください。 あらかじめご了承ください。

ご静聎ありがずうございたした。意芋を共有しおください。

Source: https://habr.com/ru/post/J230451/


All Articles