大きな移行







まえがき



Hi%username%! 今年は多くの興味深い新製品と良いニュースをもたらしました。 リアクティブカーネルと組み込みのKotlinサポートを備えた待望のSpring 5リリースが発表されましたが、これにまだ多くの興味深いことがあります。 セバスチャンは、 Kotlinで新しい機能的なSpring構成アプローチを導入しましたJUnit 5を起動しました。 Kotlin 1.2は、マルチプラットフォームアプリケーションのサポートが改善されたリリースに近づいています。 そして今年は重要なイベント開催されました ! Kotlinは、GradleのGroovy Dslでのビルドから、Kotlin Dslを使用したビルドに移行しました。







通常、新しいスタックからすぐに開始する方が簡単ですが、古いアプローチの実装方法については常に疑問が生じます。 したがって、Javaで記述されたアプリケーションの例、GradleでLombokとGroovy Dslを使用したSpring Boot 1.5(Spring 4 +)、Spring boot 2(Spring 5)、JUnit 5、Kotlinへのステップバイステップの切り替えを見て、機能的なスタイルでプロジェクトを実装してみましょうspring-boot



ないspring-webflux



。 Groovy DslからKotlin Dslにアップグレードする方法と同様に。 投稿では、主な焦点は移行にあるので、既にSpring、Spring Boot、Gradleに精通していると便利です。







読むのが面倒な人のために、 githubで他の皆のためのサンプルコードを見ることができます-私は猫の下で尋ねます:







1.基本アプリケーションから始めましょう



例として、Spring Boot 1.5.8およびSpring 4.3.12に基づいたシンプルなユーザー管理アプリケーションを取り上げましょう。







構成を読み取る例については、 src/main/resources/application.ym



にファイルを作成します。このファイルには、アプリケーションの起動ポートと、アプリケーションで使用するdb



セクションを指定します。







 server: port: 8080 db: url: localhost:8080 user: vasia password: vasiaPasswordSecret
      
      





ロンボクの接続:







 compileOnly("org.projectlombok:lombok:1.16.18")
      
      





アノテーション@ConfigurationProperties



および@Configuration



とともにDBConfigurationクラスを使用して、構成ファイルからセクションを読み取ります。 同じ構成ファイルで、データベース接続設定を使用してDbConfig



Beanを作成します。







 @Configuration @ConfigurationProperties @Getter @Setter public class DBConfiguration { private DbConfig db; @Bean public DbConfig configureDb() { return new DbConfig(db.getUrl(), db.getUser(), unSecure(db.getPassword())); } private String unSecure(String password) { int secretIndex = password.indexOf("Secret"); return password.substring(0, secretIndex); } @Data @AllArgsConstructor @NoArgsConstructor public static class DbConfig { private String url; private String user; private String password; } }
      
      





複雑にならないように、ユーザーをメモリに保存します。 これを行うには、 UserRepository



リポジトリを追加します。 接続設定を使用して通常Bean



を作成したことを確認するために、 DbConfig



をコンソールにDbConfig



ます。







UserRepository
 @Repository public class UserRepository { private DBConfiguration.DbConfig dbConfig; public UserRepository(DBConfiguration.DbConfig dbConfig) { this.dbConfig = dbConfig; System.out.println(dbConfig); } private Long index = 3L; private List<User> users = Arrays.asList( new User(1L, "Oleg", "BigMan", 21), new User(2L, "Lesia", "Listova", 25), new User(3L, "Bin", "Bigbanovich", 30) ); public List<User> findAllUsers() { return new ArrayList<>(users); } public synchronized Optional<Long> addUser(User newUser) { Long newIndex = nextIndex(); boolean addStatus = users.add(newUser.copy(newIndex)); if (addStatus) { return Optional.of(newIndex); } else { return Optional.empty(); } } public Optional<User> findUser(Long id) { return users.stream() .filter(user -> user.getId().equals(id)) .findFirst(); } public synchronized boolean deleteUser(Long id) { Optional<User> findUser = users.stream() .filter(user -> user.getId().equals(id)) .findFirst(); Boolean status = false; if (findUser.isPresent()) { users.remove(findUser.get()); status = true; } return status; } private Long nextIndex() { return index++; } }
      
      





いくつかのコントローラーを追加します。









統計コントローラ
 RestController("stats") public class StatsController { private StatsService statsService; public StatsController(StatsService statsService) { this.statsService = statsService; } @GetMapping public StatsResponse stats() { Stats stats = statsService.getStats(); return new StatsResponse(true, "user stats", stats); } }
      
      







ユーザーコントローラー
 @RestController public class UserController { private UserRepository userRepository; public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @GetMapping("users") public UserResponse users() { List<User> users = userRepository.findAllUsers(); return new UserResponse(true, "return users", users); } @GetMapping("user/{id}") public UserResponse users(@PathVariable("id") Long userId) { Optional<User> user = userRepository.findUser(userId); return user .map(findUser -> new UserResponse(true, "find user with requested id", Collections.singletonList(findUser))) .orElseGet(() -> new UserResponse(false, "user not found", Collections.emptyList())); } @PutMapping(value = "user") public Response addUser(@RequestBody User user) { Optional<Long> addIndex = userRepository.addUser(user); return addIndex .map(index -> new UserAddResponse(true, "user add successfully", index)) .orElseGet(() -> new UserAddResponse(false, "user not added", -1L)); } @DeleteMapping("user/{id}") public Response deleteUser(@PathVariable("id") Long id) { boolean status = userRepository.deleteUser(id); if (status) { return new Response(true, "user has been deleted"); } else { return new Response(false, "user not been deleted"); } } }
      
      





そして、統計用のデータを準備する「ビジネスロジック」を備えた統計コントローラー用の小さなサービスを追加します。







スタットサービス
 @Service public class StatsService { private UserRepository userRepository; public StatsService(UserRepository userRepository) { this.userRepository = userRepository; } public Stats getStats() { List<User> allUsers = userRepository.findAllUsers(); User oldestUser = allUsers.stream() .max(Comparator.comparingInt(User::getAge)) .get(); User youngestUser = allUsers.stream() .min(Comparator.comparingInt(User::getAge)) .get(); return new Stats( allUsers.size(), oldestUser, youngestUser ); } }
      
      





アプリケーションの動作をテストするには、コントローラーごとに@SpringRunner



を使用して起動したテストを追加します。 ランダムなポートでアプリケーションを起動すると、Springコンテキスト全体がその中で発生します。 以下は、 StatsControllerTest



コントローラーのテストコードです。 その中で、サービスのインスタンスとして、@ MockBeanを使用してmock



を作成します。 Spring spring-boot-starter-test



と一緒にすぐに使用できるTestRestTemplateを使用して、コントローラーにリクエストを送信します。







StatsControllerTest
 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class StatsControllerTest { @Autowired private TestRestTemplate restTemplate; @MockBean private StatsService statsServiceMock; @Test public void statsControllerShouldReturnValidResult() { Stats expectedStats = new Stats( 2, new User(1L, "name1", "surname1", 25), new User(2L, "name2", "surname2", 30) ); when(this.statsServiceMock.getStats()).thenReturn(expectedStats); StatsResponse expectedResponse = new StatsResponse(true, "user stats", expectedStats); StatsResponse actualResponse = restTemplate.getForObject("/stats", StatsResponse.class); assertEquals("invalid stats response", expectedResponse, actualResponse); } }
      
      





また、StatsServiceサービスの単純なmockitoベースのテストを追加します。







StatsServiceTest
 public class StatsServiceTest { @Test public void statsServiceShouldReturnRightData() { UserRepository userRepositoryMock = mock(UserRepository.class); User youngestUser = new User(1L, "UserName1", "Sr1", 21); User someOtherUser = new User(2L, "UserName2", "Sr2", 25); User oldestUser = new User(3L, "UserName3", "Sr3", 30); when(userRepositoryMock.findAllUsers()).thenReturn(Arrays.asList( youngestUser, someOtherUser, oldestUser )); StatsService statsService = new StatsService(userRepositoryMock); Stats actualStats = statsService.getStats(); Stats expectedStats = new Stats( 3, oldestUser, youngestUser ); Assert.assertEquals("invalid stats", expectedStats, actualStats); } }
      
      





最終的なビルドスクリプトは次のようになります。







 group 'evgzakharov' version '1.0-SNAPSHOT' buildscript { ext { springBootVersion = '1.5.8.RELEASE' } repositories { jcenter() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: "java" apply plugin: "org.springframework.boot" sourceCompatibility = 1.8 dependencies { compile("org.springframework.boot:spring-boot-starter-web") compileOnly("org.projectlombok:lombok:1.16.18") testCompile("org.springframework.boot:spring-boot-starter-test") }
      
      





どうやら、ここではエキゾチックなものは使用していません。 すべては誰もが普通に行うことです。

他のクラスと完全なコード例はここにあります

実行して、すべてが機能することを確認します







 cd step1_start_project gradle build && gradle build && java -jar build/libs/step1_start_project-1.0-SNAPSHOT.jar
      
      





その後、アプリケーションはポート8080で起動するはずです。エンドポイント「/ stats」によって正しい答えが返されることを確認します。 これを行うには、ターミナルでコマンドを実行します。







 curl -XGET "http://localhost:8080/stats" --silent | jq
      
      





答えは次のとおりです。







 { "success": true, "description": "user stats", "stats": { "userCount": 3, "oldestUser": { "id": 3, "name": "Bin", "surname": "Bigbanovich", "age": 30 }, "youngestUser": { "id": 1, "name": "Oleg", "surname": "BigMan", "age": 21 } } }
      
      





動作中のアプリケーションの準備ができました。 それでは、コードの更新と書き換えを始めましょう。







2. Spring Boot 2(Spring 5)およびJUnit 5に渡します



最も簡単な移行から始めましょう。 まず、Spring Bootバージョンを2.0.0.M5にアップグレードします。 残念ながら、執筆時点では、リリースバージョンはまだリリースされていないため、次のリポジトリをビルドスクリプトに追加します。







 maven { url = "http://repo.spring.io/milestone" }
      
      





スタジオでプロジェクトを更新し、 spring-starter-*



依存関係がなくなったエラーをキャッチしようとしています。 これは、依存関係バージョンの自動構成が別のプラグインに移動したという事実によるものです。 ビルドスクリプトに追加します。







  apply plugin: "io.spring.dependency-management"
      
      





アプリケーションを更新していますが、すべてが揃っています。 このような単純なプロジェクトの場合、これがspring-boot



新しいバージョンにアップグレードするために必要なことのすべてですが、実際のプロジェクトでは、もちろん他の問題が発生する可能性があります。







それでは、JUnit 5に進みましょう。







フレームワークの新しいバージョンには多くの興味深いことがあります 。少なくとも新しいドキュメントを見るだけで十分です。

現在、JUnit 5は3つのメインサブプロジェクトで構成されています。







 JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
      
      





どこで

JUnit Platform



は、JVMでテストを実行するための基盤です。 また、テストフレームワークを実行するためのTestEngine API



も提供しTestEngine API





JUnit Jupiter



TestEngine



新しいプログラミングモデルと、JUnit 5のテストおよび拡張機能を記述するための拡張モデルを組み合わせて構成されます。また、Jupiterプラットフォームで記述されたテストを実行するTestEngine



を含むサブプロジェクトが含まれます。

JUnit Vintage



-JUnit 3およびJUnit 4で記述されたテストを実行するTestEngine



を提供します。







gradleから新しいテストを実行するには、新しいプラグインを接続する必要があります。







 buildscript { ... dependencies { …. classpath("org.junit.platform:junit-platform-gradle-plugin:1.0.1") } } apply plugin: "org.junit.platform.gradle.plugin"
      
      





TestEngine



現在のバージョンでサポートされているテストを実行できます。 さらに、新しいテストに完全に切り替えることを前提に、次の依存関係を追加します。







 testCompile("org.junit.jupiter:junit-jupiter-api:$junitVersion") testRuntime("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
      
      





これで、すべてが新しいテストを実行する準備が整いました。古いテストを書き換えるだけです。 少し変更するのがより正確です。 JUnit 5への移行は非常に簡単です。 主なことは、JUnitアノテーションのインポートを変更することです。







 //old import org.junit.Test; //new import org.junit.jupiter.api.Test;
      
      





そして、 @DisplayName



アノテーションを使用してテストの名前を指定する新しい機能など、新しい機能を追加できます。 以前は、通常、メソッドの名前に、テスト対象の完全な説明を含める必要がありました。 これで、注釈に説明を入力し、メソッドの名前を短くすることができます。







したがって、更新されたStatsServiceTest



テストは次のようになります。







 @DisplayName("Service test with mockito") public class StatsServiceTest { @Test @DisplayName("stats service should return right data") public void test() { // ... } }
      
      





Intellij IdeaはすでにJUnit 5をサポートしているため、その中で直接テストを実行できます。









ただし、まだJUnit5をサポートしていない別のスタジオを使用している場合でも、クラスに@RunWith(JUnitPlatform.class)



アノテーション@RunWith(JUnitPlatform.class)



を追加することにより、JUnit 4の機能を使用してテストを実行できます。







また、バージョン5以降、Jupiterテストのサポートが登場しました。 このため、 SpringExtension



クラスが追加され、 SpringRunner



代わりに使用されるようにSpringRunner



StatsControllerTest



は次のようになります。







 @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @DisplayName("StatsController test") public class StatsControllerTest { // .. @Test @DisplayName("stats controller should return valid result") public void test() { // ... } }
      
      





テストを実行し、すべてが機能することを確認します。









これらはすべて、必要な変更です。 すべてが機能することを収集して検証します







 cd step2_migration_to_spring5_junit5 gradle build && gradle build && java -jar build/libs/step2_migration_to_spring5_junit5-1.0-SNAPSHOT.jar
      
      





アプリケーションを起動する前に、テストに関する情報を表示するための更新された形式があります。







 Test run finished after 3535 ms [ 4 containers found ] [ 0 containers skipped ] [ 4 containers started ] [ 0 containers aborted ] [ 4 containers successful ] [ 0 containers failed ] [ 6 tests found ] [ 0 tests skipped ] [ 6 tests started ] [ 0 tests aborted ] [ 6 tests successful ] [ 0 tests failed ]
      
      





アプリケーションの起動を待っており、セクション1のようにcurl



を使用して、すべてが機能することを確認します。 このJUnit 5およびSpring 5への移行は完了したと見なすことができます。







3.コトリンに渡す



Kotlinでの理由の問題については触れたくありません(それでも、これはかなり全体論的なトピックです)。 要するに、私の主観的な意見は、現時点ではJVMから遠く離れることなく美しい簡潔なコードを書くことができる唯一の静的型付け言語であり、非常に重要なことには、既存のJavaライブラリとの非常にスムーズでシームレスな統合、特にKotlinはコレクションを持ち込まず、Javaの標準コレクションを使用します。 さらに、マルチプラットフォームアプリケーションを完全にKotlinで作成することを強く期待しています。







Kotlinを接続します。 これを行うには、ビルドスクリプトにkotlin



プラグインを追加し、 java



プラグインを削除します。







 buildscript { ext { ... kotlinVersion = "1.1.51" } dependencies { … classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } ... apply plugin: 'kotlin'
      
      





また、Spring 4には十分なkolin-stdlib



を追加する必要がありますが、Kotlinの組み込みサポートがあり、一部の場所ではリフレクションを使用しているため、Spring 5にはkotlin-reflect



を接続する必要があります。







 compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlinVersion") compile("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
      
      





Javaコードの変換を始めましょう。 ここでは、最初の段階で、Intellij Ideaスタジオが役立ちます。 Javaファイルを開き、Shiftキーを2回押します。検索フィールドに「javaファイルをkotlinに変換」と入力します。 同様に、他のすべての* .javaファイルに対してアクションを繰り返します。









組み込みのコンバーターで十分ですが、その後、手でコードをわずかに修正する必要があります。 これは主に、どのタイプをnullable



することができ、どのタイプをnullable



not-nullable



するかの決定に関係します。 そして多くの点で、変換は一対一です。 つまり、出力は本質的に同じJavaコードであり、Kotlinでのみ記述されています(ただし、定型コードの大部分はありません)







DBConfiguration



例を使用して変換を見てみましょう。 スタジオによる変換後、コードは次のようになります。







 @Configuration @ConfigurationProperties @Getter @Setter class DBConfiguration { var db: DbConfig? = null set(db) { field = this.db } @Bean fun configureDb(): DbConfig { return DbConfig(this.db!!.url, this.db!!.user, unSecure(this.db!!.password)) } private fun unSecure(password: String?): String { val secretIndex = password!!.indexOf("Secret") return password.substring(0, secretIndex) } @Data @AllArgsConstructor @NoArgsConstructor class DbConfig { var url: String? = null set(url) { field = this.url } var user: String? = null set(user) { field = this.user } var password: String? = null set(password) { field = this.password } } }
      
      





まだあまり美しくないので、次のことを行います。









改善後、次のものが得られます。







 @Configuration @ConfigurationProperties open class DBConfiguration { var db: DbConfig = DbConfig() @Bean open fun configureDb(): DbConfig { return DbConfig(db.url, db.user, unSecure(db.password)) } private fun unSecure(password: String): String { return password.substringBefore("Secret") } data class DbConfig( var url: String = "", var user: String = "", var password: String = "" ) }
      
      





同様に、 StatsService



サービスを変換および簡素化します。 次のものが得られます。







 @Service open class StatsService(private val userRepository: UserRepository) { open fun getStats(): Stats { val allUsers = userRepository.findAllUsers() if (allUsers.isEmpty()) throw RuntimeException("not find any user") val oldestUser = allUsers.maxBy { it.age } val youngestUser = allUsers.minBy { it.age } return Stats( allUsers.size, oldestUser!!, youngestUser!! ) } }
      
      





Javaでは、コントローラー応答の各バリアントのクラスごとに個別のファイルを作成する必要がありました。 その結果、4つのクラスがあり、それぞれが個別のファイルにありました。







 //src/main/java/migration/simple/responses/Response.java @AllArgsConstructor @NoArgsConstructor @Getter @Setter @EqualsAndHashCode public class Response { private Boolean success; private String description; } //src/main/java/migration/simple/responses/StatsResponse.java @Getter @Setter @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class StatsResponse extends Response { private Stats stats; public StatsResponse(Boolean success, String description, Stats stats) { super(success, description); this.stats = stats; } } //src/main/java/migration/simple/responses/UserAddResponse.java @Getter @Setter @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class UserAddResponse extends Response { private Long userId; public UserAddResponse(Boolean success, String description, Long userId) { super(success, description); this.userId = userId; } } //src/main/java/migration/simple/responses/UserResponse.java @Getter @Setter @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class UserResponse extends Response { private List<User> users; public UserResponse(Boolean success, String description, List<User> users) { super(success, description); this.users = users; } }
      
      





Kotlinの登場により、かなり簡潔な記録ですべてを1つのファイルに収めることができるようになりました。







 interface Response { val success: Boolean val description: String } data class DeleteResponse( override val success: Boolean, override val description: String ) : Response data class StatsResponse( override val success: Boolean, override val description: String, val stats: Stats ) : Response data class UserAddResponse( override val success: Boolean, override val description: String, val userId: Long ) : Response data class UserResponse( override val success: Boolean, override val description: String, val users: List<User> ) : Response
      
      





他のすべてのクラスを同じ方法で編集して、テストに進みましょう。 ここでは、Kocklinが追加のライブラリを接続する必要があるmockitoを積極的に使用しています。







 testCompile("com.nhaarman:mockito-kotlin-kt1.1:1.5.0")
      
      





また、すべてのテストで、古いmockitoのインポートを削除し、com.nhaarman.mockito_kotlinからのインポートに変更する必要があります。







 //old import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; //new import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.whenever
      
      





when



in Kotlinはキーワードであるため、代わりに`when`



を残さないテストでは、ライブラリの`when`



を使用します。 そして、単純なmock<T>()



構造を使用してmokaを作成できるようになりました。また、宣言時に既知である場合、タイプはオプションになります。







また、宣言されたfield



に注意を払う価値がありfield



。このfield



の値は、テストの開始時にテストフレームワークによって初期化されました。 すべての宣言のKotlinでは、値をすぐに初期化する必要があります。そのため、そのようなfield



nullable



型のみを指定し、初期値としてnullを指定するか、 lateinit



使用できlateinit



(この場合は望ましい)。 lateinit



のlateinitは次のように機能します:宣言時に変数の値を初期化することはできませんが、値を取得しようとすると、初期化されていることが確認され、初期化されていない場合は例外がスローされます。 したがって、この機会を慎重に使用する価値があります。 このような可能性は、既存のJavaフレームワークとの便利な対話のために本質的に現れました。







field



JavaからKotlinへの移行の例は、宣言時に初期化されません。







 //Java @Autowired private TestRestTemplate restTemplate;
      
      





 //Kotlin @Autowired private lateinit var restTemplate: TestRestTemplate
      
      





Kotlinの登場による素晴らしい追加は、 @DisplayName



アノテーションが不要になったことです。 長いメソッド名をスペースを含むスペースに書き換えることができます。 また、スタジオは以下のヘルプも提供します。









Kotlinの主な利点の1つは、nullablityを型システムに統合することです。この利点を最大限に活用するために、コンパイルフラグ「-Xjsr305 = strict」を追加できます。 これを行うには、ビルドスクリプトに追加します。







 compileKotlin { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = ["-Xjsr305=strict"] } }
      
      





このオプションを使用すると、Kotlinはnull 5型に関する注釈をSpring 5で検討し、NPEを取得する可能性が大幅に減少します。







さらに、ターゲットjvm 8を示します(Kotlinをjvm 8バイトコードにコンパイルしたい場合、これまではjvm 6バイトコードにコンパイルする可能性があります)。







この変換で完了することができます。 アプリケーションを収集して起動します。







 cd step3_migration_to_kotlin gradle build && java -jar build/libs/step3_migration_to_kotlin-1.0-SNAPSHOT.jar
      
      





そして、私たちは何も壊さず、すべてがうまくいくと確信しています。 その場合、次に進みます。







4. spring-webfluxと機能的なKotlinに渡します



Springのブログ投稿を見ときに、この移行に触発されまし 。 その中で、SébastienDeleuzeは、spring-webfluxに基づいており、spring-bootを使用しない機能的アプローチでSpringアプリケーションを初期化する例を示しています。







Spring 5の登場により、さまざまなWebサーバーとさまざまなアプローチでアプリケーションを可変的に初期化することが可能になりました。











彼の例では、SébastienはNettyでアプリケーションを実行します。例はここにあります 。 変更のために、Undertowでアプリケーションを起動します。







スプリングブートなしでSpringを起動することから始めましょう。 GenericApplicationContext



, web- . , GenericApplicationContext



WebHttpHandlerBuilder



, HttpHandler



, , , web- .

Spring :







 // Tomcat and Jetty (also see notes below) HttpServlet servlet = new ServletHttpHandlerAdapter(handler); ... // Reactor Netty ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); HttpServer.create(host, port).newHandler(adapter).block(); // RxNetty RxNettyHttpHandlerAdapter adapter = new RxNettyHttpHandlerAdapter(handler); HttpServer server = HttpServer.newServer(new InetSocketAddress(host, port)); server.startAndAwait(adapter); // Undertow UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler); Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build(); server.start();
      
      





, . :







 class Application(port: Int? = null, beanConfig: BeanDefinitionDsl = beansConfiguration()) { private val server: Undertow init { val context = GenericApplicationContext().apply { beanConfig.initialize(this) loadConfig() refresh() } val build = WebHttpHandlerBuilder.applicationContext(context).build() val adapter = build .run { UndertowHttpHandlerAdapter(this) } val startupPort = port ?: context.environment.getProperty("server.port")?.toInt() ?: DEFAULT_PORT server = Undertow.builder() .addHttpListener(startupPort, "localhost") .setHandler(adapter) .build() } fun start() { server.start() } fun stop() { server.stop() } private fun GenericApplicationContext.loadConfig() { val resource = ClassPathResource("/application.yml") val sourceLoader = YamlPropertySourceLoader() val properties = sourceLoader.load("main config", resource, null) environment.propertySources.addFirst(properties) } companion object { private val DEFAULT_PORT = 8080 } } fun main(args: Array<String>) { Application().start() }
      
      





, spring-boot



. application.yml, @ConfigurationProperties



spring-boot



, yml ( snakeyaml) spring-boot-starter



. spring-boot-starter



, spring-boot



.







beansConfiguration



, . , , , , . :







 fun beansConfiguration(beanConfig: BeanDefinitionDsl.() -> Unit = {}): BeanDefinitionDsl = beans { bean<DBConfiguration>() //controllers bean<StatsController>() bean<UserController>() //repository bean<UserRepository>() //services bean<StatsService>() //routes bean<Routes>() bean("webHandler") { RouterFunctions.toWebHandler(ref<Routes>().router(), HandlerStrategies.builder().viewResolver(ref()).build()) } //view resolver bean { val prefix = "classpath:/templates/" val suffix = ".mustache" val loader = MustacheResourceTemplateLoader(prefix, suffix) MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply { setPrefix(prefix) setSuffix(suffix) } } //processors bean<CommonAnnotationBeanPostProcessor>() bean<ConfigurationClassPostProcessor>() bean<ConfigurationPropertiesBindingPostProcessor>() beanConfig() }
      
      





beans



, Spring 5 spring-context



. Kotlin . bean



, . , , , , ViewResolver



.







Spring , @Bean



, @Configuration



, @ConfigurationProperties



, @PostConstruct



, , , BeanPostProcessor



.







, , .







Routes



. “webHandler” RouterFunctions.toWebHandler(ref<Routes>().router(), …)



.







ref<Routes>()



, spring-context



, :







 inline fun <reified T : Any> ref(name: String? = null) : T = when (name) { null -> context.getBean(T::class.java) else -> context.getBean(name, T::class.java) }
      
      





, Routes



:







 open class Routes( private val userController: UserController, private val statsController: StatsController ) { fun router() = router { accept(APPLICATION_JSON).nest(userController.nest()) accept(APPLICATION_JSON).nest(statsController.nest()) GET("/") { ok().render("index") } } }
      
      





, , , router



spring-context



. Sébastien :







 accept(TEXT_HTML).nest { GET("/") { ok().render("index") } GET("/sse") { ok().render("sse") } GET("/users", userHandler::findAllView) } "/api".nest { accept(APPLICATION_JSON).nest { GET("/users", userHandler::findAll) } accept(TEXT_EVENT_STREAM).nest { GET("/users", userHandler::stream) } }
      
      





, . , , .







, nest



. :







 interface Controller { fun nest(): RouterFunctionDsl.() -> Unit }
      
      





StatsController



:







 open class StatsController(private val statsService: StatsService) : Controller { override fun nest(): RouterFunctionDsl.() -> Unit = { GET("/stats") { ok().body(stats()) } } open fun stats(): Mono<StatsResponse> { val stats = statsService.getStats() return Mono.just(StatsResponse(true, "user stats", stats)) } }
      
      





“/stats”, GET stats



. Flux



Mono



, spring-webflux



. UserController



:







 open class UserController(private val userRepository: UserRepository) { fun nest(): RouterFunctionDsl.() -> Unit = { GET("/users") { ok().body(users()) } GET("/user/{id}") { ok().body(user(it.pathVariable("id").toLong())) } PUT("/user") { ok().body(addUser(it.bodyToMono(User::class.java))) } DELETE("/user/{id}") { ok().body(deleteUser(it.pathVariable("id").toLong())) } } open fun users(): Mono<UserResponse> { // …. } open fun user(userId: Long): Mono<UserResponse> { // …. } open fun addUser(user: Mono<User>): Mono<UserAddResponse> = user.map { // …. } open fun deleteUser(id: Long): Mono<DeleteResponse> { // …. } }
      
      





, . , , , , .







. StatsServiceTest



, . , StatsControllerTest



:







 @DisplayName("StatsController test") open class StatsControllerTest { private val statsServiceMock = mock<StatsService>() private val port = 8181 private val configuration = beansConfiguration { bean { statsServiceMock } } private val application = Application(port, configuration) @BeforeEach fun before() { reset(statsServiceMock) application.start() } @AfterEach fun after() { application.stop() } @Test fun `stats controller should return valid result`() { val expectedStats = Stats( 2, User(1L, "name1", "surname1", 25), User(2L, "name2", "surname2", 30) ) whenever(statsServiceMock.getStats()).thenReturn(expectedStats) val expectedResponse = StatsResponse(true, "user stats", expectedStats) val response: StatsResponse = "http://localhost:$port/stats".GET() assertEquals(expectedResponse, response, "invalid response") } }
      
      





. Spring, . . .







restTemplate. : "http://localhost:$port/stats".GET()



. GET , . OkHttp3:







 var client = OkHttpClient() val JSON = MediaType.parse("application/json; charset=utf-8") val mapper: ObjectMapper = ObjectMapper() .registerKotlinModule() inline fun <reified T> String.GET(): T { val request = Request.Builder() .url(this) .build() return client.newCall(request).executeAndGet(T::class.java) } inline fun <reified T> String.PUT(data: Any): T { val body = RequestBody.create(JSON, mapper.writeValueAsString(data)) val request = Request.Builder() .url(this) .put(body) .build() return client.newCall(request).executeAndGet(T::class.java) } inline fun <reified T> String.POST(data: Any): T { val body = RequestBody.create(JSON, mapper.writeValueAsString(data)) val request = Request.Builder() .url(this) .post(body) .build() return client.newCall(request).executeAndGet(T::class.java) } inline fun <reified T> String.DELETE(): T { val request = Request.Builder() .url(this) .delete() .build() return client.newCall(request).executeAndGet(T::class.java) } fun <T> Call.executeAndGet(clazz: Class<T>): T { execute().use { response -> return mapper.readValue(response.body()!!.string(), clazz) } }
      
      





UserControllerTest



.







UserControllerTest
 @DisplayName("UserController test") class UserControllerTest { private var port = 8181 private lateinit var userRepositoryMock: UserRepository private lateinit var configuration: BeanDefinitionDsl private lateinit var application: Application @BeforeEach fun before() { userRepositoryMock = mock() configuration = beansConfiguration { bean { userRepositoryMock } } application = Application(port, configuration) application.start() } @AfterEach fun after() { application.stop() } @Test fun `all users should be return correctly`() { val users = listOf( User(1L, "name1", "surname1", 25), User(2L, "name2", "surname2", 30) ) whenever(userRepositoryMock.findAllUsers()).thenReturn(users) val expectedResponse = UserResponse(true, "return users", users) val response: UserResponse = "http://localhost:$port/users".GET() assertEquals(expectedResponse, response, "invalid response") } @Test fun `user should be return correctly`() { val user = User(1L, "name1", "surname1", 25) whenever(userRepositoryMock.findUser(1L)).thenReturn(user) whenever(userRepositoryMock.findUser(2L)).thenReturn(null) val expectedResponse = UserResponse(true, "find user with requested id", listOf(user)) val response: UserResponse = "http://localhost:$port/user/1".GET() assertEquals(expectedResponse, response, "not find exists user") val expectedMissedResponse = UserResponse(false, "user not found", emptyList()) val missingResponse: UserResponse = "http://localhost:$port/user/2".GET() assertEquals(expectedMissedResponse, missingResponse, "invalid user response") } @Test fun `user should be added correctly`() { val newUser1 = User(null, "name", "surname", 15) val newUser2 = User(null, "name2", "surname2", 18) whenever(userRepositoryMock.addUser(newUser1)).thenReturn(15L) whenever(userRepositoryMock.addUser(newUser2)).thenReturn(null) val expectedResponse = UserAddResponse(true, "user add successfully", 15L) val response: UserAddResponse = "http://localhost:$port/user".PUT(newUser1) assertEquals(expectedResponse, response, "invalid add response") val expectedErrorResponse = UserAddResponse(false, "user not added", -1L) val errorResponse: UserAddResponse = "http://localhost:$port/user".PUT(newUser2) assertEquals(expectedErrorResponse, errorResponse, "invalid add response") } @Test fun `user should be deleted correctly`() { whenever(userRepositoryMock.deleteUser(1L)).thenReturn(true) whenever(userRepositoryMock.deleteUser(2L)).thenReturn(false) val expectedResponse = DeleteResponse(true, "user has been deleted") val response: DeleteResponse = "http://localhost:$port/user/1".DELETE() assertEquals(expectedResponse, response, "invalid response") val expectedErrorResponse = DeleteResponse(false, "user not been deleted") val errorResponse: DeleteResponse = "http://localhost:$port/user/2".DELETE() assertEquals(expectedErrorResponse, errorResponse, "invalid response") } }
      
      





:







 group 'evgzakharov' version '1.0-SNAPSHOT' buildscript { ext { springBootVersion = "2.0.0.M5" junitVersion = "5.0.1" kotlinVersion = "1.1.51" } repositories { jcenter() maven { url = "http://repo.spring.io/milestone" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("org.junit.platform:junit-platform-gradle-plugin:1.0.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } apply plugin: "org.springframework.boot" apply plugin: "org.junit.platform.gradle.plugin" apply plugin: 'kotlin' apply plugin: "io.spring.dependency-management" sourceCompatibility = 1.8 repositories { jcenter() maven { url = "http://repo.spring.io/milestone" } } dependencies { compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlinVersion") compile("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") compile("org.springframework.boot:spring-boot-starter") compile("org.springframework:spring-webflux") compile("io.undertow:undertow-core") compile("com.samskivert:jmustache") compile("com.fasterxml.jackson.module:jackson-module-kotlin") testCompile("com.nhaarman:mockito-kotlin-kt1.1:1.5.0") testCompile("com.squareup.okhttp3:okhttp:3.9.0") testCompile("org.junit.jupiter:junit-jupiter-api:$junitVersion") testRuntime("org.junit.jupiter:junit-jupiter-engine:$junitVersion") } compileKotlin { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = ["-Xjsr305=strict"] } } compileTestKotlin { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = ["-Xjsr305=strict"] } }
      
      





, .







 cd step4_migration_to_webflux gradle build && java -jar build/libs/step4_migration_to_webflux-1.0-SNAPSHOT.jar
      
      





curl









5. Kotlin Dsl



Kotlin Dsl ( 0.12.1 ), .







, Groovy Dsl, IDE, . Gradle Kotlin Dsl 3.0 ( 4.2.1), Intellij Idea “ ” Kotlin.







. , . , , . :









 artifactory { setContextUrl("${project.findProperty("artifactory_contextUrl")}") publish(delegateClosureOf<PublisherConfig> { repository(delegateClosureOf<GroovyObject> { setProperty("repoKey", "ecomm") setProperty("username", project.findProperty("artifactory_user")) setProperty("password", project.findProperty("artifactory_password")) setProperty("mavenCompatible", true) defaults(delegateClosureOf<GroovyObject> { invokeMethod("publishConfigs", "wgReports") }) }) }) }
      
      





Groovy :







 artifactory { contextUrl = "${artifactory_contextUrl}" publish { repository { repoKey = 'ecomm' username = "${artifactory_user}" password = "${artifactory_password}" mavenCompatible = true } defaults { publishConfigs('wgReports') } } }
      
      





, Gradle API, Closure, Kotlin Dsl.







. “.kts” build.gradle :










All Articles