diff --git a/src/main/java/dev/surl/surl/SurlApplication.kt b/src/main/java/dev/surl/surl/SurlApplication.kt index 95c0aa5..bc255fe 100644 --- a/src/main/java/dev/surl/surl/SurlApplication.kt +++ b/src/main/java/dev/surl/surl/SurlApplication.kt @@ -1,10 +1,13 @@ package dev.surl.surl +import dev.surl.surl.cfg.BaseConfig import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication import org.springframework.context.ConfigurableApplicationContext @SpringBootApplication +@EnableConfigurationProperties(BaseConfig::class) open class SurlApplication { companion object { lateinit var context: ConfigurableApplicationContext diff --git a/src/main/java/dev/surl/surl/cfg/BaseConfig.kt b/src/main/java/dev/surl/surl/cfg/BaseConfig.kt index 3e82050..4706425 100644 --- a/src/main/java/dev/surl/surl/cfg/BaseConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/BaseConfig.kt @@ -3,19 +3,13 @@ package dev.surl.surl.cfg import dev.surl.surl.util.numberToKey import io.jsonwebtoken.security.Keys import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.stereotype.Component import java.util.Date import javax.crypto.SecretKey -@Component @ConfigurationProperties(prefix = "base.configs") class BaseConfig( - /** - * 站点域名 - */ - var site: String = "https://surl.org", - var expire: Long = 24 * 60 * 60 * 1000, // token expire time - private var secret: String = numberToKey(Date().time), - var secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray()), - var tokenHead: String = "token" + val site: String = "https://surl.org", + val expire: Long = 3600000, // token expire time + private val secret: String = numberToKey(Date().time).repeat(5), + val secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray()) ) \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/Logging.kt b/src/main/java/dev/surl/surl/cfg/Logging.kt index acc319b..ede4c9d 100644 --- a/src/main/java/dev/surl/surl/cfg/Logging.kt +++ b/src/main/java/dev/surl/surl/cfg/Logging.kt @@ -8,4 +8,10 @@ import kotlin.reflect.KClass @Component open class Logging { fun logger(clazz: KClass): Logger = LoggerFactory.getLogger(clazz::class.java) -} \ No newline at end of file +} + +@Suppress("UNUSED") +fun T.logger(): Logger = LoggerFactory.getLogger(this::class.java) + +@Suppress("UNUSED") +fun logger(name: String): Logger = LoggerFactory.getLogger(name) \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/PatternConfig.kt b/src/main/java/dev/surl/surl/cfg/PatternConfig.kt index e92767f..4a10bf1 100644 --- a/src/main/java/dev/surl/surl/cfg/PatternConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/PatternConfig.kt @@ -3,6 +3,7 @@ package dev.surl.surl.cfg import org.springframework.context.annotation.Configuration @Configuration +@Suppress("UNUSED") open class PatternConfig { val usernamePattern = Regex("""\w{6,20}""") val passwordPattern = Regex("""^((?=\S*?[A-Z])(?=\S*?[a-z])(?=\S*?[0-9])(?=\S*?)).{10,}\S$""") diff --git a/src/main/java/dev/surl/surl/cfg/RedisConfig.kt b/src/main/java/dev/surl/surl/cfg/RedisConfig.kt new file mode 100644 index 0000000..b9c496b --- /dev/null +++ b/src/main/java/dev/surl/surl/cfg/RedisConfig.kt @@ -0,0 +1,15 @@ +package dev.surl.surl.cfg + +import org.springframework.context.annotation.Bean +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.stereotype.Component + +@Component +class RedisConfig { + + @Bean + fun baseRedis(factory: RedisConnectionFactory): StringRedisTemplate { + return StringRedisTemplate(factory) + } +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/controller/DefaultFcontroller.kt b/src/main/java/dev/surl/surl/controller/DefaultFcontroller.kt deleted file mode 100644 index f67c085..0000000 --- a/src/main/java/dev/surl/surl/controller/DefaultFcontroller.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.surl.surl.controller - -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -@RestController(value = "/") -class DefaultFcontroller { - @RequestMapping - fun defaultMessage() = 123 -} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/controller/RedirectController.kt b/src/main/java/dev/surl/surl/controller/RedirectController.kt index 83b655f..eff00a9 100644 --- a/src/main/java/dev/surl/surl/controller/RedirectController.kt +++ b/src/main/java/dev/surl/surl/controller/RedirectController.kt @@ -1,10 +1,11 @@ package dev.surl.surl.controller -import dev.surl.surl.cfg.BaseConfig +import dev.surl.surl.common.Msg import dev.surl.surl.service.SurlService import jakarta.validation.Valid +import jakarta.validation.constraints.Pattern import org.hibernate.validator.constraints.Length -import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -12,16 +13,18 @@ import org.springframework.web.bind.annotation.RestController import java.net.URI @RestController -class RedirectController { +class RedirectController(private val service: SurlService) { @GetMapping("/{key}") fun redirect( - @PathVariable @Valid @Length(min = 1, max = 11, message = "Key length is not valid") key: String, - @Autowired service: SurlService, - @Autowired cfg: BaseConfig + @PathVariable + @Valid + @Length(min = 1, max = 11, message = "Key length is not valid") + @Pattern(regexp = "[\\w!*().\\-_~]+", message = "Key format is not valid") + key: String ): ResponseEntity { val redirectUrl = service.getUrlByKey(key) return if(redirectUrl.isBlank()) { - ResponseEntity.status(302).location(URI.create(cfg.site)).build() + ResponseEntity(Msg(code = -1, msg = "key `$key` not found"), HttpStatus.NOT_FOUND) } else { ResponseEntity.status(302).location(URI.create(redirectUrl)).build() } diff --git a/src/main/java/dev/surl/surl/controller/SurlAddController.kt b/src/main/java/dev/surl/surl/controller/SurlAddController.kt index 544249c..46bed1b 100644 --- a/src/main/java/dev/surl/surl/controller/SurlAddController.kt +++ b/src/main/java/dev/surl/surl/controller/SurlAddController.kt @@ -10,14 +10,14 @@ import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @EnableWebSecurity class SurlAddController { - @RequestMapping("/surl/add") + @PostMapping("/surl/add") fun addSurl( @Valid @RequestBody body: SurlDto, @Autowired service: SurlService, @Autowired cfg: BaseConfig ): Any { diff --git a/src/main/java/dev/surl/surl/controller/UserController.kt b/src/main/java/dev/surl/surl/controller/UserController.kt index 6c060c9..c65926a 100644 --- a/src/main/java/dev/surl/surl/controller/UserController.kt +++ b/src/main/java/dev/surl/surl/controller/UserController.kt @@ -1,6 +1,5 @@ package dev.surl.surl.controller -import dev.surl.surl.common.Msg import dev.surl.surl.dto.UserDto import dev.surl.surl.service.UserService import jakarta.validation.Valid diff --git a/src/main/java/dev/surl/surl/dao/Surl.kt b/src/main/java/dev/surl/surl/dao/Surl.kt index 5d93fb8..3d52dc9 100644 --- a/src/main/java/dev/surl/surl/dao/Surl.kt +++ b/src/main/java/dev/surl/surl/dao/Surl.kt @@ -5,6 +5,7 @@ import org.jetbrains.exposed.dao.Entity import org.jetbrains.exposed.dao.EntityClass import org.jetbrains.exposed.dao.id.EntityID +@Suppress("UNUSED") class Surl(id: EntityID): Entity(id) { companion object: EntityClass(Surls) var url by Surls.url diff --git a/src/main/java/dev/surl/surl/dao/UserAccess.kt b/src/main/java/dev/surl/surl/dao/UserAccess.kt index e146c04..0476650 100644 --- a/src/main/java/dev/surl/surl/dao/UserAccess.kt +++ b/src/main/java/dev/surl/surl/dao/UserAccess.kt @@ -1,7 +1,6 @@ package dev.surl.surl.dao import dev.surl.surl.common.Access -import dev.surl.surl.dao.Surl.Companion.referrersOn import dev.surl.surl.dsl.UserAccesses import org.jetbrains.exposed.dao.LongEntity import org.jetbrains.exposed.dao.LongEntityClass diff --git a/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt b/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt index 1c9e8e3..82844e0 100644 --- a/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt +++ b/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt @@ -12,6 +12,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.context.request.WebRequest +import org.springframework.web.method.annotation.HandlerMethodValidationException import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler @ControllerAdvice @@ -22,6 +23,20 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() { return ResponseEntity(Msg(code = -1, msg = ex.allValidationResults.joinToString(";")), status) } + override fun handleHandlerMethodValidationException( + ex: HandlerMethodValidationException, + headers: HttpHeaders, + status: HttpStatusCode, + request: WebRequest + ): ResponseEntity { + return ResponseEntity(Msg(code = -1, msg = ex.allValidationResults.joinToString { + it.resolvableErrors.joinToString(";") { + err -> + err.defaultMessage ?: "unknown validation error" + } + }), status) + } + override fun handleMethodArgumentNotValid( ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest ): ResponseEntity { diff --git a/src/main/java/dev/surl/surl/service/SurlService.kt b/src/main/java/dev/surl/surl/service/SurlService.kt index 682a66b..bcd46d8 100644 --- a/src/main/java/dev/surl/surl/service/SurlService.kt +++ b/src/main/java/dev/surl/surl/service/SurlService.kt @@ -4,6 +4,7 @@ import dev.surl.surl.dao.Surl import dev.surl.surl.dsl.Surls import dev.surl.surl.util.* import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.stereotype.Service @@ -19,6 +20,13 @@ class SurlService { numberToKey(id) } + fun batchAddSurl(baseurls: List) = transaction { + Surls.batchInsert(baseurls, shouldReturnGeneratedValues = false) { + this[Surls.url] = it + this[Surls.id] = runBlocking { genSnowflakeUID() } + } + } + fun getUrlByKey(key: String): String { return transaction { Surls.select(Surls.url).where { diff --git a/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt b/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt index c7429fb..79b9c74 100644 --- a/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt +++ b/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt @@ -1,16 +1,28 @@ package dev.surl.surl.util.redis +import dev.surl.surl.cfg.BaseConfig import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit - -class RedisUtil { - private val template = StringRedisTemplate() - fun get(key: String): String? { - return template.opsForValue().get(key) +@Suppress("UNUSED") +@Component +class RedisUtil(private val template: StringRedisTemplate, private val cfg: BaseConfig) { + private val ops = template.opsForValue() + fun getString(key: String): String? { + return ops.get(key) + } + fun setString(key: String, value: String, expire: Long = cfg.expire, unit: TimeUnit = TimeUnit.MILLISECONDS) { + ops.set(key, value, expire, unit) } - fun batchSet(map: Map) { - template.executePipelined { + fun delKey(key: String) { + ops.operations.delete(key) + } + + fun flushdb() { + template.execute { + it.serverCommands().flushDb() } } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e11f5c3..1c35a36 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,7 +1,5 @@ server: port: 18888 - servlet: - context-path: /api/v1 spring: profiles: active: default @@ -26,13 +24,25 @@ spring: time-zone: Asia/Shanghai serialization: indent-output: true + data: + redis: + host: localhost + database: 0 + jedis: + pool: + enabled: true + max-active: 8 + max-idle: 8 + min-idle: 1 + max-wait: 100ms exposed: show-sql: false generate-ddl: true logging: level: root: info - Exposed: debug + Exposed: info base: configs: - expire: 1000 \ No newline at end of file + site: http://127.0.0.1:18888 + expire: 3600000 \ No newline at end of file diff --git a/src/test/java/dev/surl/surl/Benchmark.kt b/src/test/java/dev/surl/surl/Benchmark.kt index fe956d5..6f3fd30 100644 --- a/src/test/java/dev/surl/surl/Benchmark.kt +++ b/src/test/java/dev/surl/surl/Benchmark.kt @@ -23,6 +23,19 @@ class Benchmark { } } val timeEllapsed = System.currentTimeMillis() - now - println("TPS: ${128 * 100.0 * 1000 / timeEllapsed}") + println("one by one TPS: ${128 * 100.0 * 1000 / timeEllapsed}") + } + @Test + fun `test batch insert`(@Autowired service: SurlService) { + runBlocking { + val urls = mutableListOf() + for(i in 1..10000) { + urls.add("https://surl.org/${genSnowflakeUID()}") + } + val now = System.currentTimeMillis() + service.batchAddSurl(urls) + val timeEllapsed = System.currentTimeMillis() - now + println("batch TPS: ${10000 * 1000 / timeEllapsed}") + } } } \ No newline at end of file diff --git a/src/test/java/dev/surl/surl/RedisTest.kt b/src/test/java/dev/surl/surl/RedisTest.kt new file mode 100644 index 0000000..957350c --- /dev/null +++ b/src/test/java/dev/surl/surl/RedisTest.kt @@ -0,0 +1,22 @@ +package dev.surl.surl + +import dev.surl.surl.util.numberToKey +import dev.surl.surl.util.redis.RedisUtil +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.AutoConfiguration +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +@AutoConfiguration +open class RedisTest(@Autowired private val redisUtil: RedisUtil) { + @Test + fun `test set value`() { + redisUtil.setString("test", numberToKey(1145141919810L)) + } + @Test + fun `test get value`() { + val value = redisUtil.getString("test") + println(value) + } +} \ No newline at end of file