Dockerized Bootiful Kotlin

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 Trail

Yarış öncesi her ne kadar tatsız ve yaşanmasını arzu etmediğimiz şeyler gerçekleşmiş olsa da Erciyes Ultra Sky Trail, yarışı gerçekten güzel ve heyecan dolu bir deneyim oldu benim için.

Yarış Başlangıcı

Yarış Hakkında;

Yarış Erciyes Dağı/Tekir yaylasından başlayıp Tekir Göleti’nde son bulan yaklaşık 64 km ve 3000m+ irtifa kazanımı içeren bir dağ patika koşusu olmakla birlikte dağı çevreleyen derin vadiler, inişli çıkışlı karstik kayalıklar, tarlalar, köyler size eşsiz bir yarış deneyimi sunuyor.

Yarış günü öncesinde 15 Temmuz 2016 tarihinde ülkemiz açısından çok talihsiz bir olay vuku bulmuş, bir çok yurttaşımız yaralanmış ve bir çoğu da hayatını maalesef kayıp etmişti. Bu karanlık ve kötü havanın elbette beni ve koşucu arkdaşlarımı derinden üzmesine rağmen, hiç bir zorbalığa ve baskıya boyun eğmeyeceğimizi göstermek için koşmaktan ve spor yapmaktan vazgeçmedik ve başlangıç noktasında ki yerlerimizi aldık.

Yarış Başlangıcı

Yarış boyunca bir çok koşucu ile tanışma sohbet etme fırsatı buldum, itiraf etmeliyim hiç bu kadar güzel, samimi bir ortam da bulunmamıştım, yapmacılıktan uzak birbiri ile iletişim kurmaktan çekinmeyen, egolarından arınmış ve yardım sever insanlar. Öyle ki yarış sırasında tanıştığım @Sevil ile yarışı birlikte tamamladık, onun enerjisi ve neşesi yarış boyunca beni iten güç oldu. Yarışın kazananı ben olamadım ama çok güzel arkadaşlıklar edindim.

Ultra Maraton yarışlarında başınıza neler gelebileceğini kestirmek çok güçtür, bu yüzden etkinlikte bizim sportif aktivitemizin sağlıklı bir şekilde tamamlanması için can hıraş çalışan gönüllü arkadaşlarımızın önemini anlatmaya kelimeler yetersiz kalır. Onlara sarf etmiş oldukları emekler için bir kere daha teşekkür etmek istiyorum.

Herkes’in ilham aldığı ve güç bulduğu efsaneleri vardır. Benimkisi de Aykut Çelikbaş’tı1. Yarışmaya gelmeden önce Onunla tanışacağım için gerçekten çok heyecanlıydım, tanıştıktan sonra mütavazılığı, yardımseverliği ve sportmenliği karşısında daha da mutlu oldum. Ülkemizin böyle insanlara sahip olması gurur verici bir şey. İnsana hâlâ ülkede umut varmış dedirtiyor.

Yarış Başlangıcı

Yarış sırasında çeşitli olumsuzluklarla karşılaşabileceğinize yukarıda değinmiştim bu tip şeylerle karşılaşmadan önce önlem almak yarış sırasında hayatınızı kolaylaştırabilir belki de hayatınızı kurtarabilir. Bu önerilerimi sıralayacak olursam;

  • Ayakkabı; Evet ayağınıza her oturan ayakkabı size uygun olmayabilir, o yüzden trail ayakkabısı dahi almış olsanız ayak yapınıza uymuyorsa yarış sonunda ayak tırnaklarınızdan olabilirsiniz ve itiraf etmeliyim çok acı veriyor. :)
  • Çorap; Çorapları çok üzdüğümüzü onları önemsemediğimizi Sky Erciyes boyunca sıkça hatırladım; eğer sert bir zeminde iseniz ve koştuğunuz yerler sık dikenli olacaksa sağlam ve pamuklu bir çorap giymek ayağınıza alacağınız darbeleri azaltacaktır. O yüzden artık çoraplarınızı sevin ve onlara karşı daha duyarlı olun. :)
  • GPS Saati; Yarış boyunca sizin en büyük yardımcınız, nerede olduğunuzu, hızınızı, yüksekliği ve bir çok yeteneğe sahip olan bu cihazlarınızı yarış öncesi şarj ettiğinizden lütfen emin olun. Yoksa fazladan 4 km koşmak zorunda kalabilirsiniz.
  • Su; Herkes yeterince altını çiziyor ama Suyun bu spor dalı için hayati öneme sahip olduğunun altını çizmek istiyorum. Kilometre başına ne çok fazla ne de çok az su içmeye özen gösterin. Aman diyeyim bir kez daha :)
  • Kendini tanımak, Ultra maraton yarışları insanın fiziksel limitlerinin zorlandığı ve hatta aşıldığı sporlardır, o yüzden düzenli antremenlar yapmak, iyi beslenmek, mental olarak koşuya kendinizi hazırlamak, uzun yol koşularında size fayda sağlayacaktır.

Elbette yazacaklarım bununla sınırlı değil, #SkyErciyes de bir çok şeyi deneyim etme fırsatım oldu, ve daha öğreneceğim bir çok şey de olacak, bunları sizler bu sorunlarla karşılaşmadan önce aktarmaktan mutluluk duyacağımı ayrıca belirtmek istiyorum. :) Tırnaklar kırılmasın değil mi?.

Ve Yarışın tamamlanması, verilen mücadelenin zaferle sonuçlanması kadar güzel bir şey yoktur dünya da. Yarış sonrası içilen su bile boğazınızdan geçerken anlam kazanır.


Yarış detaylarıma Garmin’in raporundan göz atabilirsiniz, yarış sırasında saatimin şarjı bittiği için sadece 42 kmlik bölümü göreceksiniz. :)

Ayrıca beni Garmin Connect’ten eklemek ve yarışmak isteyenler buradan ekleyebilir

Peki sırada hangi yarış ve yarışlar var?

  • Ilgaz Dağ Maratonu 27-29 Ağustos 20162
  • Kaçkar Ultra Maratonu 24-25 Eylül 20163
  • Salamon Kapadokya Ultra Maratonu 22-23 Ekim 20164

Yukarıda görmüş olduğunuz bu etkinliklere katılmayı arzu ediyorum, birlikte koşalım ya da sana sponsor olalım diyorsanız :) benimle iletişime geçebilirsiniz.

Bu kadar konuştuktan sonra biraz müzik iyi gelecektir kanısındayım, o halde “Run boy run!!” :))

Sağlıcakla kalın, iyi dinlemeler..