Javalin 是一个面向 Kotlin 和 Java 的极简 Web 框架,它没有繁琐的配置文件,也没有厚重的依赖注入体系,几行代码就能启动一个 HTTP 服务。Vue 3 则是目前前端领域的主流框架之一,组合式 API 和 Vite 构建工具让开发体验非常流畅。把两者放在一起,就能得到一套轻量、快速、易于维护的全栈方案,特别适合中小型项目、内部工具和原型验证。本文将从工程化角度出发,完整讲解如何把 Vue 3 构建产物接入 Javalin,并搭建前后端分离但最终一体化部署的项目结构。

一、为什么选择 Javalin 加 Vue 3 的组合
在传统的 Java Web 开发中,Spring Boot 几乎是默认选项。但 Spring Boot 的自动装配和庞大的依赖树对小型项目来说往往是杀鸡用牛刀。Javalin 的核心 jar 包只有几百 KB,内嵌 Jetty 服务器,启动时间通常在一秒以内,内存占用也远低于 Spring Boot 应用。如果你的后端只需要提供一组简单的 REST 接口,Javalin 的优势非常明显。
从前端角度看,Vue 3 配合 Vite 的开发体验已经非常成熟。热更新速度极快,组合式 API 让逻辑复用更加自然。在开发阶段,前端跑在 Vite 的开发服务器上,通过代理转发请求到 Javalin 后端;到了部署阶段,将 Vue 项目打包成静态文件,直接交给 Javalin 托管,一个 jar 包就能跑起整个应用。
这套组合的典型适用场景包括:内部管理后台、数据看板、小型 SaaS 服务的单体部署、以及需要快速验证想法的原型项目。如果项目涉及复杂的事务管理、消息队列、微服务治理,那还是应该考虑 Spring 全家桶。
二、搭建后端服务并暴露 REST 接口
首先创建一个 Gradle 项目,引入 Javalin 依赖。以 Kotlin 为例,build.gradle.kts 中的核心依赖如下:
dependencies {
implementation("io.javalin:javalin:6.1.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")
implementation("org.slf4j:slf4j-simple:2.0.13")
}
接着编写主类,启动服务并注册几个接口。Javalin 的路由注册采用链式 DSL 写法,一个 lambda 就是一个处理器,不需要定义 Controller 类:
import io.javalin.Javalin
fun main() {
val app = Javalin.create { config ->
config.showJavalinBanner = false
}.start(7070)
// 模拟数据
val users = mutableListOf(
mapOf("id" to 1, "name" to "张三"),
mapOf("id" to 2, "name" to "李四")
)
// 查询列表
app.get("/api/users") { ctx -> ctx.json(users) }
// 新增用户
app.post("/api/users") { ctx ->
val body = ctx.bodyAsClass(Map::class.java)
users.add(mapOf("id" to users.size + 1, "name" to body["name"]))
ctx.status(201).json(mapOf("success" to true))
}
}
运行这段代码,打开浏览器访问 localhost:7070/api/users 就能看到 JSON 返回。注意 Javalin 默认使用 Jackson 做 JSON 序列化,所以引入 jackson-module-kotlin 可以避免 Kotlin Map 序列化时的兼容问题。日志方面,Javalin 依赖 SLF4J,这里用 slf4j-simple 提供最简实现,生产环境可以换成 Logback。
三、Vue 3 前端工程与开发环境联调
前端部分用 Vite 创建 Vue 3 项目,执行 npm create vite@latest 命令后选择 Vue 模板即可。开发阶段的关键是配置代理,让前端的 API 请求转发到 Javalin 服务。在 vite.config.js 中添加如下配置:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:7070',
changeOrigin: true
}
}
}
})
这样配置之后,前端代码里所有以 /api 开头的请求都会被 Vite 转发到 7070 端口,避免了浏览器跨域问题。组件中通过 fetch 或 axios 调用接口即可,例如一个简单的用户列表页面:
<script setup>
import { ref, onMounted } from 'vue'
const users = ref([])
const newName = ref('')
async function loadUsers() {
const res = await fetch('/api/users')
users.value = await res.json()
}
async function addUser() {
await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName.value })
})
newName.value = ''
loadUsers()
}
onMounted(loadUsers)
</script>
<template>
<div>
<ul>
<li v-for="u in users" :key="u.id">{{ u.name }}</li>
</ul>
<input v-model="newName" placeholder="输入姓名" />
<button @click="addUser">添加</button>
</div>
</template>
这种开发模式下,前后端各自独立运行,前端改动即时热更新,后端改动重启 Javalin 也只需一秒钟,整体迭代效率非常高。如果前端不打算走代理,而是直连后端域名,就需要在 Javalin 一侧开启跨域支持,通过 config.plugins.enableCors { it.add { it.allowCredentials = true } } 这类配置来完成。
四、打包集成与生产环境部署
开发完成后,在前端目录执行 npm run build,Vite 会在 dist 目录生成全部静态资源。工程化的做法是让构建产物自动复制到后端的资源目录中,Gradle 里可以定义一个任务:
tasks.register<Copy>("copyFrontend") {
dependsOn(":frontend:build")
from("frontend/dist")
into("src/main/resources/public")
}
resources/public 是 Javalin 默认的静态资源目录,构建后的 index.html、js、css 文件放进去之后,Javalin 会自动托管它们。为了支持前端路由的 history 模式,还需要把未匹配的 GET 请求重定向到 index.html:
// 放在所有 API 路由注册之后
app.get("/{path}") { ctx ->
ctx.result(java.io.File("src/main/resources/public/index.html").readText())
.contentType("text/html")
}
更优雅的做法是使用 Javalin 提供的 VuePlugin(javalin-vue 模块),它原生支持从 classpath 加载 Vue 文件并处理路由,适合不想引入 Node 构建流程的团队。不过对于已经熟悉 Vite 工作流的项目,独立构建再复制的方式更加灵活,可以正常使用 npm 生态里的各种插件。
最终执行 gradle build 得到一个可执行 fat jar,用 java -jar app.jar 一条命令即可启动整个全栈应用,包含前端页面和后端接口。相比 Nginx 加 Java 服务的双进程部署,这种单体方式运维成本更低,也方便打包成 Docker 镜像分发。总体而言,Javalin 加 Vue 3 是一套小而美的全栈方案,关键在于把开发代理、静态资源托管和前端路由回退这三件事配置妥当,剩下的开发体验会非常顺滑。