Hi% username%! This year brought a lot of interesting new products and good news. The long-awaited release of Spring 5 has come out , with a reactive core and integrated Kotlin support , for which there will still be a lot of interesting things. Sébastien presented a new functional approach to the Spring configuration at Kotlin. Outdated JUnit 5 . The release of Kotlin 1.2 with improved support for multi-platform applications is coming. And this year there was a significant event ! Now Kotlin has moved from assembly to Groovy Dsl in Gradle to build with Kotlin Dsl.
As a rule, it is easier to start immediately with a new stack, but there are always questions about how to implement the old approaches. Therefore, consider the example of an application written in Java, Spring Boot 1.5 (Spring 4+) using Lombok and Groovy Dsl in Gradle, gradually switch to Spring boot 2 (Spring 5), JUnit 5, Kotlin, and try to implement the project in a functional style on spring-webflux
without spring-boot
. And also how to switch from Groovy Dsl to Kotlin Dsl. The post will focus on the transition, so it would be nice if you are already familiar with Spring, Spring Boot and Gradle.
For those who are too lazy to read, you can see a sample code on github , for all the others, I ask for the cat:
As an example, take a simple user management application based on Spring Boot 1.5.8 and Spring 4.3.12
For an example of reading the configuration, create a file in src/main/resources/application.ym
, in which we specify the application launch port, and the db
section, which we will use in the application.
server: port: 8080 db: url: localhost:8080 user: vasia password: vasiaPasswordSecret
Connect Lombok:
compileOnly("org.projectlombok:lombok:1.16.18")
The section from the configuration file will be read using the DBConfiguration class with annotations @ConfigurationProperties
and @Configuration
. In the same configuration file, we will create DbConfig
bean with the settings for connecting to the database.
@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; } }
In order not to complicate, users will be stored in memory. To do this, add the UserRepository
repository. And to check that we have created a Bean
with the connection settings, we will DbConfig
to the console.
@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++; } }
Add a few controllers:
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"); } } }
And we will add a small service for the statistics controller with “business logic”, which will prepare the data for statistics:
@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 ); } }
To test the application, we @SpringRunner
add a test for each controller that is run using @SpringRunner
. It will bring up the entire Spring context with running the application on a random port. Below is the test code for the StatsControllerTest
controller. In it, as an instance of the service, we will create a mock
using @MockBean. We will send requests to the controller using TestRestTemplate, which is out of the box along with spring-boot-starter-test
.
@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); } }
Also add a simple mockito based test for StatsService
:
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); } }
The final build script will look like this:
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") }
As you can see, here we do not use anything exotic. Everything is as usual.
The rest of the classes and the full code sample can be found here .
Run and check that everything works.
cd step1_start_project gradle build && gradle build && java -jar build/libs/step1_start_project-1.0-SNAPSHOT.jar
After that, the application should start on port 8080. We check that the endpoint’s “/ stats” returns the correct answer. To do this, in the terminal run the command:
curl -XGET "http://localhost:8080/stats" --silent | jq
The answer should be as follows:
{ "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 } } }
The working application is ready. Now we are going to update and rewrite the code.
Let's start with the easiest transition. First, we’ll update Spring Boot version to 2.0.0.M5. Unfortunately, at the time of writing the post, the release version has not yet been released, so we are adding the following repository to the build script:
maven { url = "http://repo.spring.io/milestone" }
We are trying to update the project in the studio and catch the mistakes that we now have no spring-starter-*
dependencies. This is due to the fact that now the automatic configuration of dependency versions has moved to another plugin . Add it to the build script:
apply plugin: "io.spring.dependency-management"
We update the application and now we have everything. For such a simple project, this is all you need to do to upgrade to the new version of spring-boot
, but in real projects, of course, other difficulties may arise.
We now turn to the transition to JUnit 5.
In the new version of the framework, a lot of interesting things have appeared, it is enough to at least look at the new documentation .
JUnit 5 now consists of three main subprojects:
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
Where,
JUnit Platform
- the basis for running tests on the JVM. It also provides the TestEngine API
for running test frameworks.
JUnit Jupiter
- consists of combining a new software model and an extension model for writing tests and extensions for JUnit 5. Also contains a subproject containing TestEngine
for running tests written on the Jupiter platform.
JUnit Vintage
- provides TestEngine
for running tests written in JUnit 3 and JUnit 4.
To run new tests from gradle we need to connect their new plugin:
buildscript { ... dependencies { …. classpath("org.junit.platform:junit-platform-gradle-plugin:1.0.1") } } apply plugin: "org.junit.platform.gradle.plugin"
It allows you to run any tests that are supported by the current version of TestEngine
. In addition, given that we are completely switching to new tests, we add the following dependencies:
testCompile("org.junit.jupiter:junit-jupiter-api:$junitVersion") testRuntime("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
Now everything is ready to launch new tests, it remains only to rewrite the old ones. Although it would be more correct to change a little. The transition to JUnit 5 is fairly painless. The main thing you need to do is to change the import annotation JUnit:
//old import org.junit.Test; //new import org.junit.jupiter.api.Test;
And then we can add new functionality, such as, for example, the new ability to specify the name of the test using the annotation @DisplayName
. Previously, as a rule, we had to fill in the name of the method with a complete description of what we are testing. Now, you can make a description in the abstract and leave the name of the method short.
So, now the updated test StatsServiceTest
will look like:
@DisplayName("Service test with mockito") public class StatsServiceTest { @Test @DisplayName("stats service should return right data") public void test() { // ... } }
Intellij Idea already supports JUnit 5, so we can run the test right in it:
But even if you use another studio that does not yet support JUnit5, you can use the features from JUnit 4 to run the tests by adding the annotation @RunWith(JUnitPlatform.class)
to the class.
In Spring, also since version 5, support for Jupiter tests has appeared. For this, the SpringExtension
class has been added, which is now used instead of SpringRunner
. StatsControllerTest
will now look like this:
@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() { // ... } }
Run the test and check that everything works:
These are all the changes that needed to be made. We collect and check that everything works:
cd step2_migration_to_spring5_junit5 gradle build && gradle build && java -jar build/libs/step2_migration_to_spring5_junit5-1.0-SNAPSHOT.jar
Before launching the application, there will be an updated test information display format:
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 ]
We are waiting for the launch of the application and also, as in section 1, with the help of curl
we are convinced that everything works. At this transition to JUnit 5 and Spring 5 can be considered complete.
I would not like to touch on the question of why it is on Kotlin (this is still a rather holivar topic). But briefly, my subjective opinion is that at the moment it is the only statically typed language that allows you to write beautiful concise code without going far from the JVM, while possessing, which is very important, fairly smooth and seamless integration with existing Java libraries, especially considering that Kotlin does not bring its collections but uses standard ones from Java. In addition, I have high hopes for writing multi-platform applications entirely on Kotlin.
Connect Kotlin. To do this, in the build script, add the kotlin
plugin and remove the java
plugin.
buildscript { ext { ... kotlinVersion = "1.1.51" } dependencies { … classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } ... apply plugin: 'kotlin'
And we also need to add a small kolin-stdlib
, which would be enough for Spring 4, but Spring 5 still needs to be connected with kotlin-reflect
, thanks to the fact that it has built-in support for Kotlin and uses its reflection in places.
compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlinVersion") compile("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
Let's start converting Java code. Here, at the first stage, the studio Intellij Idea will help us. Open any Java file and press 2 times shift, where in the search field we write: “convert java file to kotlin”. Similarly, we repeat the action for all other * .java files.
The built-in converter is quite good, but after it you need to tweak the code a little with your hands. This mainly concerns the definition of which types can be nullable
and which not-nullable
. And in many ways, conversion is one-to-one. In other words, the output is, in fact, the same Java code, only written in Kotlin (but without a significant part of the template code)
Let's look at the conversion using the example of DBConfiguration
. After the conversion by the studio, the code will look like this:
@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 } } }
While not very beautiful, so we do the following:
set
properties. There is no need for them here either, since the @ConfigurationProperties
annotation @ConfigurationProperties
required only for a field
with public getter
and setter
. And go to the non-nullable types, in which, as the default value, we specify empty lines. Although here you can leave the nullable
types, but then you will have to put up with the code further (and as practice has shown, it is better to do non-nullable values ​​for the data from the configuration). For the DbConfig
class, you can add a data
modifier here, although for all its properties you have to specify a default value so that the class has a constructor without arguments that is required to initialize the value from the configuration in the spring.open
for the DBConfiguration
class. This is necessary in order for the class redefinition mechanism built into Spring to work properly. There is another option not to add open, but to plug in the gradle
kotlin-spring
plugin in kotlin-spring
, which itself at the compilation stage will open for redefining the necessary classes (and methods), but I prefer the explicit approach.configureDb
method with the @Bean
annotation, all for the same reason, since the default methods are final
.unSecure
method using the substringBefore
extension
methodAfter improvements we get the following:
@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 = "" ) }
Similarly, we convert and simplify the StatsService
service. We get the following:
@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!! ) } }
In Java, for each variant of the controller's response, we had to create a separate file for each class. As a result, there were 4 classes and each in a separate file:
//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; } }
With the advent of Kotlin, you can now fit everything in one file with a fairly concise record:
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
We rule in a similar way all the other classes and proceed to the tests. Here we are actively using mockito, for which to work in Kotlin you need to connect an additional library:
testCompile("com.nhaarman:mockito-kotlin-kt1.1:1.5.0")
And in all tests, you need to remove the imports for the old mockito and change them for imports from 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 is a keyword, and in the tests not to leave `when`
instead of it, whenever
use whenever
from the library. And now we can create mocks using a simple mock<T>()
construct, and the type can be optional if known at the time of the announcement.
It is also worth paying attention to the declared field
, the values ​​of which were initialized by the test framework at the start of the test. Kotlin for all declarations requires that the value be immediately initialized, so we can only specify either the nullable
type for such nullable
and specify null as the initialization value or use lateinit
(which is preferable in this case). lateinit
in Kotin works as follows: it is allowed not to initialize the value of a variable when declaring, but when you try to get a value, it is checked that it is initialized, and if it is not, then an exception is thrown. Therefore it is worth using this opportunity with caution. Such an opportunity in essence appeared for convenient interaction with existing Java frameworks.
An example of a transition from Java to Kotlin for a field
that is not initialized at the time of the declaration:
//Java @Autowired private TestRestTemplate restTemplate;
//Kotlin @Autowired private lateinit var restTemplate: TestRestTemplate
A pleasant addition with the arrival of Kotlin is that we no longer need the @DisplayName
annotation. We can rewrite long method names with spaces. And even the studio itself offers assistance in this:
One of the key advantages of Kotlin is embedding nullablity in the type system, and to unleash this advantage to the full, we can add the compilation flag “-Xjsr305 = strict”. To do this, add to the build script:
compileKotlin { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = ["-Xjsr305=strict"] } }
With this option, Kotlin will take annotations in Spring 5 into account for the nullability of types, and the probability of getting NPE is significantly reduced.
We also specify that target jvm 8 (if we want Kotlin to compile to jvm 8 bytecode, but so far there is a possibility to compile to jvm 6 bytecode).
At this conversion can be completed. Build and run the application:
cd step3_migration_to_kotlin gradle build && java -jar build/libs/step3_migration_to_kotlin-1.0-SNAPSHOT.jar
And we are convinced that we have not broken anything and that everything works. If so, then go ahead.
I was inspired for this transition by seeing the Spring blog post. In it, Sébastien Deleuze shows an example of initializing a Spring application in a functional approach based on spring-webflux and without a spring-boot.
With the advent of Spring 5, there was an opportunity for a variety of application initialization on various web servers and various approaches:
In his example, Sébastien runs the application on Netty, an example can be found here . For a change, run the application on Undertow.
Let's start by running Spring without spring-boot. To do this, we need to configure GenericApplicationContext
, which is bound to the web server to be launched using adapters. , 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
.
@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
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 :
compile
dependencies
, Kotlin Gradle:
plugins { id("org.jetbrains.kotlin.jvm") version "1.1.51" }
group 'evgzakharov'
, , Kotlin . group = "evgzakharov"
ext.springBootVersion = "2.0.0.M5"
: extra[“springBootVersion”] = "2.0.0.M5"
, buildscript
val springBootVersion by extra { "2.0.0.M5" }
dependencies
, :
val springBootVersion: String by project.extra val junitVersion: String by project.extra val kotlinVersion: String by project.extra
, Kotlin, .
compileKotlin
. :
tasks.withType<KotlinCompile> { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = listOf("-Xjsr305=strict") } }
Kotlin Dsl:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile group = "evgzakharov" version = "1.0-SNAPSHOT" buildscript { val springBootVersion by extra { "2.0.0.M5" } extra["junitVersion"] = "5.0.1" extra["kotlinVersion"] = "1.1.51" repositories { jcenter() maven { setUrl("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:1.1.51") } } plugins { id("org.jetbrains.kotlin.jvm") version "1.1.51" } apply { plugin("org.springframework.boot") plugin("org.junit.platform.gradle.plugin") plugin("io.spring.dependency-management") } repositories { jcenter() maven { setUrl("http://repo.spring.io/milestone") } } val junitVersion: String by project.extra val kotlinVersion: String by project.extra 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") } tasks.withType<KotlinCompile> { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = listOf("-Xjsr305=strict") } }
:
cd step5_migration_kotlin_dsl gradle build && java -jar build/libs/step5_migration_kotlin_dsl-1.0-SNAPSHOT.jar
:)
. Kotlin, Spring JUnit, Kotlin Dsl, Spring Spring-Boot ()
, , spring-webflux. , , . , Spring . , , , , Spring . .
Spring Boot 2 JUnit 5 , . ? , , .
… Kotlin! — . . stackoverflow , , Mockito, , . , , , , Java Kotlin ( )
! :)
Source: https://habr.com/ru/post/340942/