Category

工程实践

我为什么从 WordPress 换到了 Astro:从家宽非标端口到 Cloudflare Pages

工程实践

非标端口不得上桌

之前的博客部署在自己家里的自建服务器上,从 9999 的 http 到 3443 的 https,家宽总是与标准端口无缘,毕竟现在没有哪个运营商还开放 80 和 443 给家宽用。

看到前公司同事创业,发在公众号的文章,注意到自己做的东西应该让别人看到,这些也是个人竞争力的一部分,然而自己之前一直闭门造车,希望用做的好的产品(软件)而不是个人来吸引到注意力。看到同事的经历发现和他的想法和我的非常类似,虽然现在不想把个人推到台前做什么自媒体博主,但是提前做准备肯定是没错的,看起来中年危机离得很远,实际上不早早准备,机会来了也未必能抓住。

同事的经历给了我很好的借鉴方向,提前建立一些个人的技术资产,平时就持续积累,比等到要用的时候再到处找强太多。但是一个跑在家宽非标端口上的自建 WordPress 实在是上不了台面,所以开始找更好的方案

技术选型

搞一个有标准端口的博客方案很多,找个 VPS 扔上去,或者放 GitHub Pages 之类的,或者干脆直接套 Cloudflare 的代理,转发一下,但是我希望博客在国内也能正常快速访问,延迟低,也不用单独管一台服务器。

本来想要一个功能完整的博客,带一套后端可以有评论/访问量统计之类的功能,但是这基本上就意味着要管一个专门的 VPS 托管网站,这就有点太麻烦了,之前家里云的服务器跑了太多服务,虱子多了不痒,债多了不愁,多一个博客不多,少一个博客不少,就直接扔上面了,但是要单独搞一个 VPS ,专门上去维护,就很麻烦了。

但是如果只是想要评论和统计的话,用 GitHub Pages 之类的静态博客托管,也是有办法支持的。调研了一下发现有Giscus,或者可以直接用 Cloudflare Pages Functions 做轻量级后端也可以实现。

这个时候正好在和一些朋友聊天,于是讨论了一下,发现评论之类的功能实际上并不重要,评论之类的动态功能对博客来说只是锦上添花,博客最核心的点就是看博客,对于真正希望看到博客内容的人来说,有没有评论都不重要,之前开着评论也没有人发表什么很有价值的评论,于是决定干脆就先不做这些,先把标准端口的博客弄出来再说。

既然不要动态功能,那就太简单了,直接用 Hexo 快快的搭一个出来部署到 Cloudflare Pages 上,搭完之后配置的时候发现 Hexo 很麻烦,之前博客用的主题有一个 Hexo 版本 的也没维护了,而且如果要修改会更麻烦,很多想改的地方都要通过主题和插件,改起来很绕。

反正现在 vibe coding 这么方便,我直接 vibe 一个得了。

于是按照 Astro + Cloudflare Pages 的方案快速vibe了一个出来,从 WordPress 把博客导了出来做成了现在的博客。

最终效果

Cloudflare Pages 的 GitHub 仓库对接体验还是很好的,不需要配 CI 也可以直接在推送时自动构建。而且延迟其实并不高,首次加载在 2s 左右就加载完了,对于一个博客来说完全可以接受。

未来如果需要评论,大概会考虑 Pages Functions + D1/KV,看起来免费额度拿来做这些功能是足够的。

优化超大单体Spring Boot项目开发环境启动速度

工程实践

之前维护的一个项目,项目启动需要12分钟,开发环境需要频繁启停,极大影响开发效率

分析

使用IDEA profiler收集启动阶段的数据,并用idea打开火焰图分析

发现启动时间大部分都消耗在注入Autowired资源以及创建Aop切面

同时整理了项目的模块发现开发环境下有一部分是不常用的

优化方案

首先是修改pom使开发环境不加载不常用的模块,启动速度快了60秒左右

然后编写了脚本静态分析源码,收集了所有没有使用的@Autowired和@Resource,并手动处理(项目代码规范不是很好, 不确定是否有特殊的引用, 虽然最后没发现这样的引用),启动速度快了30秒左右

java
import java.io.File

val dir = File("/path/to/a/project")
val unused = mutableListOf<Pair<String, String>>()
val autowiredPattern = Regex("(private|public|protected)\\s+\\w+\\s+(?<varName>\\w+)\\s*;")

fun collect(file: File) {
    if (file.isDirectory) {
        // 忽略编译产物
        file.name == "target" && file.parentFile.resolve("pom.xml").exists() && return
        file.listFiles()?.forEach { collect(it) }
        return
    }
    if (!file.name.endsWith(".java")) {
        return
    }
    // println("scan ${file.name}")
    // 找到所有autowired
    val autowiredList = mutableListOf<String>()
    var nextIsAutowired = false
    val lines = file.readLines().filter { it.startsWith("//") }
    for (line in lines) {
        if (line.trim().run{ startsWith("@Autowired") || startsWith("@Resource") }) {
            nextIsAutowired = true
        }
        if (nextIsAutowired) {
            val result = autowiredPattern.find(line)
            if (result != null) autowiredList.add(result.groups["varName"]!!.value)
            nextIsAutowired = false
            continue
        }
    }
    if (autowiredList.isEmpty()) return
    // 查询没有使用的autowired
    val filePath = file.absolutePath
        .replace("\\", "/")
        .substringAfter("src/main/java/")
        .replace("/", ".")
        .removeSuffix(".java")
    autowiredList.filter { varName ->
        lines.all { line -> varName !in line || line.trim().matches(autowiredPattern) }
    }.let {
        unused += it.map { v -> filePath to v }
    }
}

collect(dir)
File("unused-autowired-${dir.name}.txt").writeText(unused.joinToString("\n") { (k, v) -> "$k: $v" })

此时启动速度依然很慢,于是使用 lazy-initialization 的方案,环境变量中添加 spring.main.lazy-initialization=true

但是所有bean都懒加载会导致一部分模块出错最终导致启动失败,于是添加filter

纯文本
@Bean
public LazyInitializationExcludeFilter filter() {
    return (beanName, beanDefinition, beanType) -> {
        String className = beanType.getName();
        return className.startsWith("com.example.module.plugin.important")
                || className.startsWith("com.example.module.common")
                ;
    };
}

最终启动速度从12分钟优化到最快4分钟(不启用可选的模块)

总结

  • 模块拆分解耦是好文明,在这种场景下直接禁用不用的模块也不影响项目启动
  • 在bean初始化里写逻辑是坏文明,遇到一个上游依赖的组件,自己封装了一层XxlJobSpringExecutor,还把class设置成package-private,导致不加载该模块就无法正常初始化xxljob

kotlin script

工程实践

kotlin是我最喜欢用的语言,语法简洁功能丰富,但是项目管理略嫌麻烦,那有什么办法可以跳过麻烦的项目管理,同时又直接使用jvm庞大生态的依赖呢

答案就是 kotlin script

使用

首先需要一个最新的idea,旧版本的idea对这类新特性的支持并不太好

kotlin脚本有好几种,临时文件里创建的scratch,gradle项目管理的build.gradle.kts,以及本文的主题 main.kts

用idea在任意位置创建一个 test.main.kts ,随便写一段代码

纯文本
println("hello kotlin script")

虽然文件行号上没有任何标记,但是我们可以用 ctrl + shilt + f10 运行该文件

当然也可以直接新建一个运行项

引入依赖

引入依赖的方式如下 @file:DependsOn("com.google.code.gson:gson:2.11.0")

也可以用 @file:Repository("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") 指定maven仓库

修改代码如下,然后点一下代码编辑器右上角的加载脚本依赖项按钮

kotlin
@file:DependsOn("com.google.code.gson:gson:2.11.0")

import com.google.gson.JsonElement
import com.google.gson.JsonParser

val json: JsonElement = JsonParser.parseString("""{ "name": "kotlin script", "age": 1 }""")

println(json)

此时就已经可以使用gson的类了

当然在没有科学上网的情况下有时候是无法正常下载依赖的,对于这一点我修改了maven镜像源,以便在不开tun的情况下正常下载依赖

序列化

虽然我一直都很喜欢kotlinx.serialization的序列化,但是该序列化依赖gradle插件,我还没有研究明白怎么在 kts 脚本里使用这种插件

所以为了在脚本里使用序列化,我会选择gson作为序列化实现

提供服务

我有一些简单的http服务需要实现,这个时候我会选择ktor,他和kotlin相性极佳(毕竟都是一家的项目)

kotlin
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2")

@file:DependsOn("io.ktor:ktor-server-content-negotiation:2.3.13")
@file:DependsOn("io.ktor:ktor-server-compression-jvm:2.3.13")
@file:DependsOn("io.ktor:ktor-serialization-kotlinx-json:2.3.13")
@file:DependsOn("io.ktor:ktor-server-core-jvm:2.3.13")
@file:DependsOn("io.ktor:ktor-server-netty-jvm:2.3.13")
@file:DependsOn("io.ktor:ktor-server-call-logging-jvm:2.3.13")

@file:DependsOn("com.google.code.gson:gson:2.11.0")

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import kotlin.system.exitProcess

val logger: Logger = LoggerFactory.getLogger("App")
val scope = CoroutineScope(Dispatchers.IO) + CoroutineExceptionHandler { coroutineContext, throwable ->
    logger.warn("", throwable)
}
val gson = Gson()
val json = """
    |{
    |  "kotlin": 1,
    |  "java": 2
    |}
""".trimMargin()

// 信号处理
run {
    Runtime.getRuntime().addShutdownHook(Thread {
        logger.info("exit...")
        scope.cancel()
        server.stop(0, 0)
    })
}

fun Route.configureRouting() {
    get("/{id}/status") {
        val id = call.parameters["id"]!!
        val status = gson.fromJson<Map<String, Int>>(json, object : TypeToken<Map<String, Int>>() {}.type)[id]
        call.respondText(status.toString())
    }
}

// http服务
val server = embeddedServer(Netty, 5753) {
    install(CallLogging) {
        level = Level.INFO
    }
    install(Compression) {
        gzip {
            priority = 1.0
            matchContentType(ContentType.Text.Any)
        }
        deflate {
            priority = 10.0
            minimumSize(1024)
        }
    }
    routing {
        configureRouting()
    }
}
server.start(true)
exitProcess(0)

部署

在idea里面我们已经可以运行main.kts脚本了,但是如果要部署,那怎么办呢

这儿需要到 kotlinrelease 下载 kotlin-compiler

纯文本
#!/bin/bash

kotlinc/bin/kotlin -Dfile.encoding=utf8 your_file.main.kts

然而添加jvm参数的部分在win上用不了,虽然我提了issue但是并没有解决

优势

很多时候我会写很多测试代码,调研某个库的使用方法和功能实现,但是这些代码如果集中在一个项目,会让项目变得很大,加载很慢,如果分开会不方便统一管理

这个时候使用kotlin script就可以很方便的管理

小问题

当我有如下代码

kotlin
@file:DependsOn("com.google.code.gson:gson:2.11.0")

import com.google.gson.JsonElement
import com.google.gson.JsonParser

val json: JsonElement = JsonParser.parseString("""{ "name": "kotlin script", "age": 1 }""")

object MyService {
    fun someFunction(): String {
        return json.toString()
    }
}

println(MyService.someFunction())

那么运行之后会出现

虽然这是合理的是可能的,但是这是合理的是不太可能的

另外idea在重载kts脚本依赖的时候会阻塞ui线程导致idea无响应

再另外,对于低版本的idea使用kts脚本引用外部依赖,会在打开反编译代码的时候放一个索引按钮,但是点了压根没用(新版本修了

总结

对于一些简单项目,使用 kotlin script非常爽,不管是修改还是部署,都很方便

总之好用爱用多用😋

kotlin notebook 使用体验

工程实践

最近遇到一些数据分析的需求,正好之前看到又新又好的[kotlin notebook](https://book.kotlincn.net/text/d-notebook-get-started.html),就拿来玩了一下

前置条件

首先按照教程,需要安装插件,此处由于公司电脑的idea版本较旧(不是不想更,是更新了jrebel启动慢一倍),所以一开始新建了文件也没有代码高亮,最终装个两个版本的idea,旧版本idea专门跑jrebel项目

然后按照教程需要一个项目,此处测试了项目类型,intellij和gradle都是可以的

使用

在项目文件夹内任意位置新建一个kotlin notebook文件,即可使用

引入依赖

纯文本
// 使用最新版本的依赖
%useLatestDescriptors

// 引入 Kotlin DataFrame 依赖
%use dataframe

// 引入 Gson 依赖 (引入方式和 main.kts 一样)
@file:DependsOn("com.google.code.gson:gson:2.11.0")

高亮

在运行notebook中的代码之后,关闭项目并重新打开,会发现代码高亮没了,此时需要重新完整运行notebook的代码,才能重置高亮(另外代码块不支持折叠很坏

小问题

可以在块1定义class块2引用,但是直接在块2引用块1定义的变量,代码高亮会出现错误

总结

虽然体验上任有改进空间,但是实际使用是没有大问题的,好用爱用多用😋