Dockerized Bootiful Kotlin

Reading time ~12 minutes

Part I.

Bu yazım da Kotlin nedir? ne iş yapar gibi basit, herkesin küçük bir ‘google’ aramasıyla ulaşabileceği şeylerden ziya de ‘Kotlin’ ‘i gerçek dünya uygulamaların da nasıl kullanabiliriz, nasıl enterprise uygulama,’lar geliştirebiliriz, ona odaklanıp, dili bu bakış açısıyla inceleyeceğiz. Yazı dizimiz üç bölümden oluşacak, birinci bölümde web servis alt yapısını, ikinci bölümde, hazırlamış olduğumuz bu servisi tüketecek istemci tarafını ve son bölümde ise docker teknolojisini kullanarak hazırlamış olduğumuz uygulamayı deploy işlemini gerçekleşitreceğiz.

Kotlin; Groovy ve Scala gibi bir JVM programlama dilidir. Nesne yönelimlidir ve Java ile tam uyumlu (interoperability) bir şekilde çalışır. Yani ihtiyacınız olduğunda, daha önceden hazırlamış olduğunuz Java kütüphanelerinizi kotlin içerisinden çağırıp kullanabilirsiniz. Bunun dışında Kotlin muadillerinin aksine tam bir geliştirici dostu olup, Productive Oriented Programming Language ‘dir. Uygulama içerisinde adım adım ilerlerken kotlin’in sahip olduğu güzel özellikleri kullanmaya ve açıklamaya çalışacağım.

Uygulamamızı geliştirirken, standart monolitik yapıyı benimsemektense microservis yapısını benimseyeceğiz. Bu sayede bakımı daha kolay ve modern bir web alt yapısı hazırlamış olacağız.

Hangi teknolojileri kullanacağız? Spring Boot v2.0, Kotlin v1.1.3 ve veritabanı olarak da H2 Db kullanıp, projeyi Maven kullanarak derleyeceğiz.

├── main
│   ├── kotlin
│   │   └── com
│   │       └── aytacozkan
│   │           ├── BlogApplication.kt
│   │           ├── client
│   │           │   ├── ProfileClient.kt
│   │           │   ├── TagClient.kt
│   │           │   └── UserClient.kt
│   │           ├── exception
│   │           │   ├── ForbiddenRequestException.kt
│   │           │   ├── InvalidException.kt
│   │           │   ├── InvalidLoginException.kt
│   │           │   ├── NotFoundException.kt
│   │           │   ├── UnauthorizedException.kt
│   │           │   └── UserExistException.kt
│   │           ├── jwt
│   │           │   ├── ApiKeySecured.kt
│   │           │   ├── ApiKeySecuredAspect.kt
│   │           │   └── ExposeResponseInterceptor.kt
│   │           ├── model
│   │           │   ├── Article.kt
│   │           │   ├── Comment.kt
│   │           │   ├── Tag.kt
│   │           │   ├── User.kt
│   │           │   └── inout
│   │           │       ├── Article.kt
│   │           │       ├── Comment.kt
│   │           │       ├── Login.kt
│   │           │       ├── NewArticle.kt
│   │           │       ├── NewComment.kt
│   │           │       ├── Profile.kt
│   │           │       ├── Register.kt
│   │           │       ├── UpdateArticle.kt
│   │           │       └── UpdateUser.kt
│   │           ├── repository
│   │           │   ├── ArticleRepository.kt
│   │           │   ├── CommentRepository.kt
│   │           │   ├── TagRepository.kt
│   │           │   ├── UserRepository.kt
│   │           │   └── specification
│   │           │       └── ArticlesSpecifications.kt
│   │           ├── response
│   │           │   ├── InLogin.kt
│   │           │   ├── InRegister.kt
│   │           │   ├── OutProfile.kt
│   │           │   ├── OutTag.kt
│   │           │   └── OutUser.kt
│   │           ├── service
│   │           │   └── UserService.kt
│   │           └── web
│   │               ├── ArticleHandler.kt
│   │               ├── InvalidRequestHandler.kt
│   │               ├── ProfileHandler.kt
│   │               ├── TagHandler.kt
│   │               └── UserHandler.kt
│   └── resources
│       └── application.properties
└── test
    └── kotlin
        └── com
            └── aytacozkan
                └── BlogApplicationTests.kt
 

Data class

User’ı bir data class yaparak;

  • Standart kütüphaneden bazı koleksiyon tarafından kullanılan hashcode yöntemini doğru bir şekilde uygulanmasını sağlar.
  • Doğru eşitleme yöntemini uygulayarak, aynı ayar değerlerine sahip iki nesneyi eşit kılar.
  • Hashcode yerine özellik değerlerini yazdıran yeni bir toString yöntemi sağlar.
  • copy methodu.
package com.aytacozkan.model
 import com.fasterxml.jackson.annotation.JsonIgnore
 import com.fasterxml.jackson.annotation.JsonRootName
 import javax.persistence.*

 /**
  * Created by aytacozkan on 10/07/2017.
  */
 @Entity
 @JsonRootName("user")
 data class User(val email : String = "",
                @JsonIgnore
                var password: String = "",
                var token : String = "",
                var username :  String = "",
                var bio : String = "",
                var image :String = "",
                @ManyToMany
                @JsonIgnore
                var follows : MutableList<User> = mutableListOf(),
                @Id @GeneratedValue(strategy = GenerationType.AUTO)
                var id : Long = 0){
    override fun toString(): String = "User($email, $username)"
  

User Repository

 package com.aytacozkan.repository
 import com.aytacozkan.model.User
 import org.springframework.data.repository.CrudRepository
 import org.springframework.stereotype.Repository

 /**
  * Created by aytacozkan on 11/07/2017.
  */
 @Repository
 interface UserRepository : CrudRepository<User, Long>{
     fun existsByEmail(email:String) : Boolean
     fun existsByUsername(username:String) : Boolean
     fun findBEmail(email: String):User?
     fun findByToken(token:String) : User?
     fun findByEnailAndPassword(email: String, pasword:String) :User?
     fun findByUsername(username: String):User?
 }

User Service

 package com.aytacozkan.service

 import com.aytacozkan.exception.InvalidLoginException
 import com.aytacozkan.model.User
 import com.aytacozkan.model.inout.Login
 import com.aytacozkan.repository.UserRepository
 import io.jsonwebtoken.Jwts
 import io.jsonwebtoken.SignatureAlgorithm
 import org.mindrot.jbcrypt.BCrypt
 import org.springframework.beans.factory.annotation.Value
 import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties
 import org.springframework.stereotype.Service
 import java.util.*
 import javax.persistence.Cacheable

 /**
  * Created by aytacozkan on 11/07/2017.
  */
 @Service
 class UserService(val userRepository: UserRepository,
                  @Value("\${jwt.secret}") val jwtSecret:String,
                  @Value("\${jwt.issuer}") val jwtIssuer :String
 ){
    val currentUser = ThreadLocal<User>()

    fun findByToken(token:String) : User? = userRepository.findByToken(token)

    fun findByUsername(username:String):User? = userRepository.findByUsername(username)

    fun clearCurrentUser(): Unit = currentUser.remove()

    fun setCurrentUser(user:User):User{
        currentUser.set(user)
        return user
    }
    fun currentUser():User = currentUser.get()
    fun newToken(user:User) :String{
        return Jwts.builder()
                .setIssuedAt(Date())
                .setSubject(user.email)
                .setIssuer(jwtIssuer)
                .setExpiration(Date(System.currentTimeMillis() + 10 * 24 * 60 * 60 * 1000))
                .signWith(SignatureAlgorithm.HS256, jwtSecret).compact()
    }
    fun validToken(token:String, user:User):Boolean{
        val claims = Jwts.parser().setSigningKey(jwtSecret)
                .parseClaimsJws(token).body
        return  claims.subject == user.email && claims.issuer == jwtIssuer && Date().before(claims.expiration)
    }
    fun updateToken(user:User):User{
        user.token = newToken(user)
        return userRepository.save(user)
    }
    fun login(login:Login) :User? {
        userRepository.findBEmail(login.email!!)?.let {
            if (BCrypt.checkpw(login.password!!, it.password)){
                return  updateToken(it)
            }
            throw InvalidLoginException("password", "invalid password")
        }
        throw InvalidLoginException("email", "unknown email")
    }
 }
 

User Handler

 package com.aytacozkan.web

 import com.aytacozkan.exception.ForbiddenRequestException
 import com.aytacozkan.exception.InvalidException
 import com.aytacozkan.exception.InvalidLoginException
 import com.aytacozkan.exception.InvalidRequest
 import com.aytacozkan.jwt.ApiKeySecured
 import com.aytacozkan.model.User
 import com.aytacozkan.model.inout.Login
 import com.aytacozkan.model.inout.Register
 import com.aytacozkan.model.inout.UpdateUser
 import com.aytacozkan.repository.UserRepository
 import com.aytacozkan.service.UserService
 import org.mindrot.jbcrypt.BCrypt
 import org.springframework.validation.BindException
 import org.springframework.validation.Errors
 import org.springframework.validation.FieldError
 import org.springframework.web.bind.annotation.*
 import javax.validation.Valid

 /**
 * Created by aytacozkan on 11/07/2017.
 */
 @RestController
 class UserHandler(val userRepository: UserRepository,
                  val service : UserService) {
    @PostMapping("/api/users/login")
    fun login(@Valid @RequestBody login : Login, errors:Errors): Any{
        InvalidRequest.check(errors)
        try {
            service.login(login)?.let {
                return view(service.updateToken(it))
            }
            return ForbiddenRequestException()
        } catch (e: InvalidLoginException){
            val errors = org.springframework.validation.BindException(this,"")
            errors.addError(FieldError("",e.field,e.error))
            throw InvalidException(errors)
        }
    }
    @PostMapping("/api/users")
    fun register(@Valid @RequestBody register: Register, errors: Errors) : Any{
        InvalidRequest.check(errors)

        //check for duplicate user
        val errors  = BindException(this,"")
        checkUserAvailability(errors,register.email,register.username)
        InvalidRequest.check(errors)

        val user = User(username = register.username!!, email = register.email!!, password = BCrypt.hashpw(register.password,BCrypt.gensalt()))
        user.token = service.newToken(user)

        return view(userRepository.save(user))
    }
    @ApiKeySecured
    @GetMapping("/api/user")
    fun currentUser() = view(service.currentUser())
    @ApiKeySecured
    @PutMapping("/api/user")
    fun updateUser(@Valid @RequestBody user: UpdateUser, errors: Errors) : Any{
        InvalidRequest.check(errors)
        val currentUser = service.currentUser()
        //check for errors
        val errors = BindException(this, "")
        if(currentUser.email != user.email && user.email != null){
            if (userRepository.existsByEmail(user.email!!)){
                errors.addError(FieldError("","email","already taken"))
            }
        }
        if (currentUser.username != user.username && user.username != null){
            if (userRepository.existsByUsername(user.username!!)){
                errors.addError(FieldError("","username","already taken"))
            }
        }
        if(user.password == ""){
            errors.addError(FieldError("","password","can't be empty"))
        }
        InvalidRequest.check(errors)
        val u = currentUser.copy(email = user.email ?: currentUser.email,
                username = user.username ?: currentUser.username,
                password = BCrypt.hashpw(user.password, BCrypt.gensalt()), image =  user.image ?: currentUser.image,
                bio = user.bio ?: currentUser.bio)
        //if update only if email changed.
        if (currentUser.email  != u.email){
            u.token = service.newToken(u)
        }
        return view(userRepository.save(u))
    }
    private fun checkUserAvailability(errors: BindException, email : String?, username:String?){
        email?.let {
            email ->
            if (userRepository.existsByEmail(email)){
                errors.addError(FieldError("","email","already taken"))
            }
        }
        username?.let { username ->
            if (userRepository.existsByUsername(username)){
                errors.addError(FieldError("","username","already taken"))
            }
        }
    }
    fun view(user:User) = mapOf("user" to user)
 }
 

Spring Application Properties

spring.datasource.url=jdbc:h2:tcp://localhost:1521/default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true

jwt.secret=
jwt.issuer=Kotlin&Spring

logging.level.org.springframework.web=DEBUG
server.port=8181

BlogApplication

package com.aytacozkan

 import com.aytacozkan.jwt.ExposeResponseInterceptor
 import org.springframework.boot.SpringApplication
 import org.springframework.boot.autoconfigure.SpringBootApplication
 import org.springframework.context.annotation.Bean
 import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
 import org.springframework.validation.beanvalidation.MethodValidationPostProcessor
 import org.springframework.web.servlet.config.annotation.CorsRegistry
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter

 @SpringBootApplication
 class BlogApplication : WebMvcConfigurerAdapter(){

    override fun addInterceptors(registry: InterceptorRegistry?) {
        registry!!.addInterceptor(exposeResponseInterceptor())
    }

    override fun addCorsMappings(registry: CorsRegistry?) {
        registry!!.addMapping("/api/**")
                .allowedOrigins("*")
                .allowedMethods("*")
                .allowedHeaders( "*")
                .allowCredentials(false)
                .maxAge(3600)
        super.addCorsMappings(registry)
    }
    @Bean
    fun exposeResponseInterceptor() = ExposeResponseInterceptor()

    @Bean
    fun methodValidationPostProcessor() : MethodValidationPostProcessor{
        val mv = MethodValidationPostProcessor()
        mv.setValidator(validator())
        return mv
    }
    @Bean
    fun validator()  = LocalValidatorFactoryBean()
}

fun main(args: Array<String>) {
    SpringApplication.run(BlogApplication::class.java, *args)
}

Uygulama kodlarına adım adım göz attıktan sonra, kodları kendi yerelinize indirip derleyebilirsiniz, ikinci bölümde görüşmek üzere.

Sky Erciyes Ultra Trail Adventure

Sky Erciyes Ultra TrailYarış öncesi her ne kadar tatsız ve yaşanmasını arzu etmediğimiz şeyler gerçekleşmiş olsa daErciyes Ultra Sky Trai...… Continue reading