Skip to content

Instantly share code, notes, and snippets.

@nosix
Last active February 22, 2023 01:04
Show Gist options
  • Save nosix/93c32d0fd849df9fad31736f2ea4775a to your computer and use it in GitHub Desktop.
Save nosix/93c32d0fd849df9fad31736f2ea4775a to your computer and use it in GitHub Desktop.
Google App Engine Standard Environment in Java8 with Kotlin and SpringBoot
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<!-- See: https://cloud.google.com/appengine/docs/standard/java/config/appref -->
<threadsafe>true</threadsafe>
<runtime>java8</runtime>
</appengine-web-app>
package com.example
import com.example.mapper.json.JsonObjectMapper
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.web.support.SpringBootServletInitializer
import org.springframework.context.annotation.Bean
import org.thymeleaf.spring4.SpringTemplateEngine
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver
import org.thymeleaf.spring4.view.ThymeleafViewResolver
@SpringBootApplication
class Application : SpringBootServletInitializer() {
/**
* JSON Object Mapper (for Request/Response)
*/
@Bean
fun jsonObjectMapper() = JsonObjectMapper()
// Thymeleaf
private val TEMPLATE_MODE = "LOOSE_HTML5"
@Bean
fun viewResolver() = ThymeleafViewResolver().apply {
templateEngine = templateEngine()
}
@Bean
fun templateEngine() = SpringTemplateEngine().apply {
templateResolvers = setOf(templateResolver())
}
@Bean
fun templateResolver() = SpringResourceTemplateResolver().apply {
templateMode = TEMPLATE_MODE
prefix = "classpath:/templates/"
suffix = ".html"
isCacheable = false // TODO: make it true before release
}
}
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
buildscript {
ext.kotlin_version = '1.1.3'
ext.appengine_api = '1.9.54'
ext.spring_version = '1.5.3.RELEASE'
ext.jackson_version = '2.8.4'
repositories {
mavenCentral()
}
dependencies {
// Kotlin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// Google App Engine + Spring Boot
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
classpath "com.google.cloud.tools:appengine-gradle-plugin:+"
}
}
// Kotlin
apply plugin: 'kotlin'
// Google App Engine + Spring Boot
apply plugin: 'kotlin-spring'
apply plugin: 'war'
apply plugin: 'com.google.cloud.tools.appengine'
sourceCompatibility = 1.8
targetCompatibility = 1.8
appengine { // App Engine tasks configuration
//noinspection GroovyAssignabilityCheck
run { // local (dev_appserver) configuration (standard environments only)
port = 8080 // default
}
deploy { // deploy configuration
stopPreviousVersion = true // default - stop the current version
promote = true // default - & make this the current version
}
}
repositories {
mavenCentral()
}
dependencies {
// Kotlin
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// Google App Engine + Spring Boot
compile "com.google.appengine:appengine-api-1.0-sdk:$appengine_api"
compile("org.springframework.boot:spring-boot-starter-web:$spring_version") {
exclude module: 'jul-to-slf4j'
exclude module: 'spring-boot-starter-tomcat'
}
// Template Engine (Thymeleaf)
compile("org.springframework.boot:spring-boot-starter-thymeleaf:$spring_version") {
exclude module: 'tomcat-embed-core'
exclude module: 'tomcat-embed-el'
exclude module: 'tomcat-embed-websocket'
exclude module: 'groovy'
exclude module: 'jul-to-slf4j'
exclude module: 'spring-boot-starter-tomcat'
}
compile 'net.sourceforge.nekohtml:nekohtml:1.9.15' // for LEGACY HTML5 template
// JSON Object Mapper (for JSON Request/Response)
compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version") {
exclude module: 'kotlin-reflect'
}
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
}
package com.example.mapper.json
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZonedDateTime
class JsonObjectMapper : ObjectMapper() {
init {
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
registerKotlinModule()
registerModule(JavaTimeModule().apply {
addSerializer(ZonedDateTime::class.java, ZonedDateTimeSerializer())
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
addSerializer(LocalDate::class.java, LocalDateSerializer())
addDeserializer(LocalDate::class.java, LocalDateDeserializer())
addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer())
addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer())
})
}
}
package com.example.mapper.json
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import java.time.LocalDate
class LocalDateDeserializer : JsonDeserializer<LocalDate>() {
private val deserializer = ZonedDateTimeDeserializer()
override fun deserialize(p: JsonParser, ctx: DeserializationContext): LocalDate {
val dateTime = deserializer.deserialize(p, ctx)
return dateTime.toLocalDate()
}
}
package com.example.mapper.json
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import java.time.LocalDate
import java.time.ZoneId
class LocalDateSerializer : JsonSerializer<LocalDate>() {
private val serializer = ZonedDateTimeSerializer()
override fun serialize(value: LocalDate, gen: JsonGenerator, serializers: SerializerProvider) {
val dateTime = value.atStartOfDay(ZoneId.systemDefault())
serializer.serialize(dateTime, gen, serializers)
}
}
package com.example.mapper.json
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import java.time.LocalDateTime
class LocalDateTimeDeserializer : JsonDeserializer<LocalDateTime>() {
private val deserializer = ZonedDateTimeDeserializer()
override fun deserialize(p: JsonParser, ctx: DeserializationContext): LocalDateTime {
return deserializer.deserialize(p, ctx).toLocalDateTime()
}
}
package com.example.mapper.json
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import java.time.LocalDateTime
import java.time.ZoneId
class LocalDateTimeSerializer : JsonSerializer<LocalDateTime>() {
private val serializer = ZonedDateTimeSerializer()
override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) {
serializer.serialize(value.atZone(ZoneId.systemDefault()), gen, serializers)
}
}
package com.example.mapper.json
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime>() {
override fun deserialize(p: JsonParser, ctx: DeserializationContext): ZonedDateTime {
val utc = ZonedDateTime.parse(p.text, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"))
return ZonedDateTime.ofInstant(utc.toInstant(), ZoneId.systemDefault())
}
}
package com.example.mapper.json
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
class ZonedDateTimeSerializer : JsonSerializer<ZonedDateTime>() {
override fun serialize(value: ZonedDateTime, gen: JsonGenerator, serializers: SerializerProvider) {
val utc = ZonedDateTime.ofInstant(value.toInstant(), ZoneId.of("UTC"))
gen.writeString(utc.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX")))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment