Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Microservices with Spring Boot and Kotlin

Microservices with Spring Boot and Kotlin

Christoph Leiter

April 07, 2017

More Decks by Christoph Leiter

Other Decks in Programming


  1. Introduction Enthusiastic Kotlin user since beginning of 2016 Spring Boot

    simplifies many things and reduces boilerplate Perfect match for Kotlin The Spring people like Kotlin as well and will add more support in Spring 5.0 and in Spring Boot 2.0 My latest project combines Kotlin and Spring Boot in a Microservices architecture 3
  2. starjack II Customers sign up and order their personal keycard

    Tickets of various lift operators can be booked and are available within seconds No more standing in line for a ticket 6
  3. starjack III Talks to multiple services: Two different backend systems

    for getting and ordering tickets (SOAP) Payment Service Provider (SOAP) Service to produce keycards (REST) External e-mail provider (REST) External billing service to create invoices (REST) Google Maps (REST) Weather service (REST) We’re using microservices to isolate services from each other and to handle complexity. 7
  4. starjack IV Backend is 100% written in Kotlin Everything is

    automated Complete infrastructure setup is done with Terraform EC2 instance and database setup is done with Ansible Services are packaged as docker containers, deployments are triggered via Ansible, scheduled by Nomad, and discovered with Consul EC2 instances are stateless, state is only in the DB and in S3 Each service is deployed once per availability zone, everything is redundant Service-to-Service communication via REST or asynchronously via SQS 8
  5. Starting Your Application The main method must be static: @SpringBootApplication

    open class BootApplication { companion object { @JvmStatic fun main(args: Array <String >) { SpringApplication .run( BootApplication :: class.java , *args ) } } } 14
  6. Defining Beans You can define a bean with some properties

    like in Java: @SpringBootApplication open class BootApplication { @Bean open fun myBean (): MyBean { val myBean = MyBean () myBean.x = "foo" myBean.y = 12 return myBean } } Do we need an instance variable? 16
  7. Defining Beans II Let’s use apply: @SpringBootApplication open class BootApplication

    { @Bean open fun myBean (): MyBean { return MyBean (). apply { x = "foo" y = 12 } } } Now that it’s just one expression... 17
  8. Defining Beans III Just use a single expression function: @SpringBootApplication

    open class BootApplication { @Bean open fun myBean () = MyBean (). apply { x = "foo" y = 12 } } All these open keywords are a bit annoying though... 18
  9. Kotlin Compiler Plugins Starting with 1.0.6 Kotlin has compiler plugins

    which help with framework integration kotlin-allopen Makes classes and all methods automatically open if marked with an annotation. Great for Spring. kotlin-noarg Creates a synthetic default constructor for classes if marked with an annotation. Great for JPA. Annotations are customizable, even supporting meta annotations. Very easy support for Spring and JPA with spring and jpa plugins. 19
  10. Kotlin Compiler Plugins II <plugin > <artifactId >kotlin -maven -plugin

    </artifactId > <groupId >org.jetbrains.kotlin </groupId > <version >${kotlin.version}</version > <configuration > <compilerPlugins > <plugin >jpa</plugin > <plugin >spring </plugin > </ compilerPlugins > </ configuration > 20
  11. Kotlin Compiler Plugins III <dependencies > <dependency > <groupId >org.jetbrains.kotlin

    </groupId > <artifactId >kotlin -maven -allopen </artifactId > <version >${kotlin.version}</version > </dependency > <dependency > <groupId >org.jetbrains.kotlin </groupId > <artifactId >kotlin -maven -noarg </artifactId > <version >${kotlin.version}</version > </dependency > </ dependencies > </plugin > 21
  12. Defining Beans IV No more open! @SpringBootApplication class BootApplication {

    @Bean fun myBean () = MyBean (). apply { x = "foo" y = 12 } } 22
  13. Reading Properties Configuration is often externalized to make it customizable:

    service.url=http :// example.com/endpoint Injecting configuration properties works just like in Java: class Service( @Value("\${service.url}") val url: String ) Note that you have to escape the $ because it’s used by Kotlin for string interpolation. The preferred way now is to use @ConfigurationProperties. 23
  14. Reading Properties II Using @ConfigurationProperties: @Component @ConfigurationProperties ("service") class ServiceProperties

    { lateinit var url: String } or with the Jackson Kotlin module: @Component @ConfigurationProperties ("service") data class ServiceProperties ( val url: String ) You can even use the standard Java validation annotations. 24
  15. JPA

  16. JPA My recommendations: Use spring-data-jpa, reduces tons of boilerplate Use

    flyway or liquibase to define your schema Set jpa.hibernate.ddl-auto to validate to ensure your database schema matches your entities Don’t create a complex completely navigable domain model, use aggregates (Domain Driven Design) instead 26
  17. Defining Entities Defining a bean: @Entity class Customer { @Id

    val id: UUID = UUID.randomUUID () lateinit var firstname: String lateinit var lastname: String } How can we handle properties better? It’s not a good option to make everything nullable and it’s not even possible for basic types. Every lateinit is a NullPointerException waiting to happen. kotlin-noarg makes it easy to use data classes instead. 27
  18. Defining Entities II Now with using a data class: @Entity

    data class Customer( @Id val id: UUID = UUID.randomUUID (), var firstname: String , var lastname: String ) You can specify sensible default values for your properties. Hibernate will invoke the synthetic default (no-arg) constructor and set the values. Automatic generation of toString, equals, and hashCode plus copy function. 28
  19. spring-data-jpa Repositories Instead of implementing all the basic CRUD operations

    yourself you can just define a single interface: interface CustomerRepository : CrudRepository <Customer , UUID > You automatically get methods like findOne(id), save(entity), delete(entity), etc. Code is created dynamically by spring-data-jpa. Tip: Consider defining your own KCrudRepository interface to add nullability information. 29
  20. Custom JPA Queries You can use @Query to define custom

    queries in spring-data-jpa Repositories interface CustomerRepository : CrudRepository <Customer , UUID > { @Query("SELECT c FROM " + "Customer c " + "WHERE LOWER(c.firstname) LIKE '%' ||" + "|| LOWER (: search) || '%'") fun search( @Param("search") search: String ): List <Customer > } But that’s how it’s done in Java... 30
  21. Custom JPA Queries II We can use Kotlin multi line

    strings to improve readability: @Query(""" SELECT c FROM Customer c WHERE LOWER(c.firstname) LIKE '%' || LOWER (: search) || '%' """) fun search( @Param("search") search: String ): List <Customer > Now that’s much better. 31
  22. Defining DTOs Jackson needs to be able to instantiate your

    DTOs. By default it would just use the default constructor and you need to use lateinit. class CreateUserCommand { lateinit var email: String lateinit var firstname: String lateinit var lastname: String lateinit var signupCode: String? = null } Fortunately there’s a better option. 33
  23. Kotlin Module for Jackson Introducing jackson-module-kotlin: Allows to use primary

    constructor to create instances Can also use secondary constructors with @JsonCreator Automatically understands nullable and non-nullable types Supports Kotlin data types like Pair, Triple, IntRange, etc. 34
  24. Kotlin Module for Jackson II Register Kotlin module for ObjectMapper

    used by Spring: @Bean fun kotlinModule () = KotlinModule () Add it to instances of your ObjectMapper: val om = ObjectMapper () . registerModule (KotlinModule ()) Tip: Subclass ObjectMapper and configure it like you need it in your project. 35
  25. Defining DTOs II Jackson will call the constructor automatically data

    class CreateUserCommand ( val email: String , val firstname: String val lastname: String , val signupCode: String? } Now DTOs can even be immutable! Let’s add some validations. 36
  26. Defining DTOs III Annotations need to be prefixed with get:

    so they are put on the getters data class CreateUserCommand ( @get:Email val email: String , @get:Length(max = 32) val firstname: String , @get:Length(max = 32) val lastname: String , val signupCode: String? } No need for @NotNull, it’s already in the type system. 37
  27. Setting Basic Auth There’s no easy way to set the

    authorization header for HTTP basic auth. Let’s add an extension function: fun HttpHeaders.setBasicAuth ( username: String , password: String ) { val auth = String( Base64.getEncoder (). encode( "$username:$password".toByteArray () ) ) set(AUTHORIZATION , "Basic $auth") } val headers = HttpHeaders (). apply { setBasicAuth("user", "password") } 38
  28. Authorized requests In a microservices environment you often have to

    pass user authorization from service to service. val rest = RestTemplate () val headers = HttpHeaders (). apply { set(AUTHORIZATION , auth) } val entity = HttpEntity(body , headers) val response = rest. postForObject ( url , entity , Response :: class.java) This gets boring very soon. 39
  29. Authorized requests II Let’s create an extension function which helps

    fun <T> RestTemplate .withUser( auth: String , block: RestTemplate .() -> T ): T { val ic = ClientHttpRequestInterceptor { request , body , execution -> request.headers.set( AUTHORIZATION , auth) execution.execute(request , body) } interceptors.add(ic) try { return block () } finally { interceptors.remove(ic) } } 40
  30. Authorized requests III Usage: val rest = RestTemplate () val

    response = rest.withUser(auth) { postForObject( url , entity , Response :: class.java) } Make sure not to share instances of such a RestTemplate or store the authorization in a ThreadLocal! 41
  31. SOAP Requests SOAP requests can be complex and often consist

    of deeply nested structures Let’s create a request to create a contact: Main request object is a CreateContactRequest Consists of the property contact which holds a B2CContact instance Contact has a list of address ids 42
  32. SOAP Requests II val request = CreateContactRequest () val contact

    = B2CContact () contact.firstName = cmd.firstname contact.lastName = cmd.lastname contact.title = cmd.title contact.birthDate = cmd.birthday.toCalendar () contact.gender = cmd.gender.name.toGender () contact.email = cmd.email val addressIds = AddressIds () addressIds.addressId += cmd.addressId request.contact = contact The linear structure is not easy to grasp. Easy to forget to link an item. Can you spot the mistake? And that’s a trivial example... Can we do better? You guessed right: Let’s use apply! 43
  33. SOAP Requests III val request = CreateContactRequest (). apply {

    contact = B2CContact (). apply { firstName = cmd.firstname lastName = cmd.lastname title = cmd.title birthDate = cmd.birthday.toCalendar () gender = cmd.gender.name.toGender () email = cmd.email addressIds = AddressIds (). apply { addressId += cmd.addressId } } } Code structure exactly reflects XML structure. Beautiful. 44
  34. Test Method Names TestMethodsTendToGetLongAndTheyAreHardToRead: @Test fun emailAdressesCanNotBeUsedTwice () {} This

    was my workaround in Java: @Test fun email_adresses_can_not_be_used_twice () {} But Kotlin just allows methods with spaces in the name: @Test fun `email adresses can not be used twice `() {} Please, don’t use this in production code. ; ) 46
  35. JSON in tests As for @Query JSON/XML strings in tests

    are good use case for multi line strings: val userJson = """ { "firstname ": "Max", "lastname ": "Mustermann", "roles ": [ "customer" ] } """ val user = ObjectMapper () .readValue(userJson , User :: class.java) Tip: You can even tell IntelliJ it’s a JSON string by prepending it with // language=JSON. This enables syntax checking and highlighting. 47
  36. Factory Methods for Tests fun testUser( fname: String , lname:

    String , email: String ) = User(fname , lname , email) fun testUser () = testUser("max", "muster", "[email protected]") fun testUser(fname: String) = testUser(fname , "muster", "[email protected]") fun testUser(fname: String , lname: String) = testUser(fname , lname , "[email protected]") Often seen in Java codebases. The amount of methods explodes really fast and it’s hard to maintain. Get’s even more fun when argument types collide, how do you define a method to just set the lastname? 48
  37. Factory Methods for Tests II In Kotlin we can use

    default argument values and named arguments instead: fun testUser( firstname: String = "Max", lastname: String = "Mustermann" email: String = "max. mustermann@example .com" ) = User(firstname , lastname , email) @Test fun `my test `() { val user = testUser( lastname = "Muster", email = " maxmuster@example .com" ) } Uses default values if not explicitly defined. No more typing boilerplate and easy to extend. 49
  38. Mockito Mockito has a very unfortunate method name conflict with

    when: Mockito.`when `(service.foo ()). thenReturn("bar") Escaping of method names isn’t pretty. 50
  39. Mockito II The mockito-kotlin module has some neat methods: whenever(service.foo

    ()). thenReturn("bar") Also has alternative methods for e.g. anyObject() which don’t cause problems in Kotlin (those methods return null and Mockito fails). 51
  40. Mockito III Instead of the default mocking syntax, which is

    a little bit verbose: val service = Mockito.mock(Service :: class.java) Allows some more compact syntax leveraging reified generics val service = mock <Service >() or val service: Service = mock () Allows easier definition of method stubs within mock, check their wiki. 52
  41. Using JUnit Rules JUnit rules work just as in Java

    but JUnit won’t see the annotation on the property. Hence you have to add get: to specify the use-site target: @get:Rule val myRule = MyRule () The alternative is to use the @JvmField annotation. 53
  42. Injecting Spring Services For integration tests it’s handy to inject

    Spring services: @SpringBootTest @RunWith(SpringRunner :: class) class MyIntegrationTest { @Autowired lateinit var myService: MyService } Spring will process those annotations and set your services. Use lateinit only for fields set by a framework. 54
  43. Use dcevm Dynamic Code Evolution VM Allows to make structural

    changes (e.g. adding methods) to classes and dynamically reloads them Patches your JVM installation - make sure to use a dedicated installation Specify patched JVM in IntelliJ, run your project in debug mode, press Ctrl+F9 and changes will reload Not perfect but saves a lot of time 56
  44. finally We’re looking for more developers to build awesome stuff

    :) Part or full time From Student to Senior Developer Java knowledge, willing to learn Kotlin ;) Experience with frontend development is welcome Talk to me or contact me at [email protected] 58
  45. References Introducing Kotlin Support in Spring Framework 5.0: https://spring.io/blog/2017/01/04/ introducing-kotlin-support-in-spring-framework-5-0

    Improve Kotlin Support in Spring Boot https://github.com/spring-projects/spring-boot/issues/5537 starjack: https://starjack.at Terraform: https://terraform.io Ansible: https://ansible.com Nomad: https://nomadproject.io Consul: https://consul.io Kotlin 1.0.6 is here: https: //blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here/ Mockito-Kotlin: https://github.com/nhaarman/mockito-kotlin jackson-module-kotlin: https://github.com/FasterXML/jackson-module-kotlin dcevm: https://dcevm.github.io/ 59