倧きな移行


たえがき


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 :




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


All Articles