Java静态网站生成实战:FreeMarker模板引擎与Jetty打包JAR全解析
2026/9/21 0:36:36 网站建设 项目流程

简介:沁竹音乐网 v3.0 是一套面向个人站长与ASP开发者的音乐网站源码,核心卖点在于全站静态生成,有利于搜索引擎收录,同时降低服务器资源消耗,适合需要快速搭建轻量音乐站点的场景。压缩包采用rar格式,整体约3.34MB,包含ASP后台程序、静态页面模板及fso目录下可供修改的页面,便于二次开发。后台入口为admin/login.asp,默认账号admin/admin888,用户可直接登录管理,省去环境配置后的初始摸索。现有57人学习下载,资源带有清晰的静态化实现思路和后台管理逻辑,读者可从中了解音乐站点的栏目组织、页面静态生成方式及后台操作流程,也可在此基础上扩展功能、更换界面;尤其值得留意的是,页面修改入口集中在fso目录,方便针对不同页面做局部调整。整体而言,这是一套实用性强、体量精简的ASP项目,适合入门至中级开发者参考。

1. 整体方案设计:为什么非要做成静态生成版

沁竹音乐网从最早的单体应用改到 v3.0 静态生成版,这一步走了不少弯路,今天把整个改造过程整理出来,给同样要做内容站、导航站、作品集、小型媒体站点的朋友做个参考。

先说结论:静态生成并不是倒退,恰恰相反,对音乐网站这种“读多写少”的内容型站点,它是低成本、高回报的最优解之一。音乐网站的核心数据是歌手、专辑、歌曲列表、榜单页,这些内容更新频率不高,但访问频率很高。用传统动态框架每次请求都查数据库、渲染模板,纯属浪费资源。v3.0 版本把整个站点的所有 HTML 页面在构建期一次生成完毕,运行时不再依赖数据库和模板引擎,访问就是一个纯静态文件的读取,响应速度和稳定性都上了一个台阶。

我做的“静态生成版”技术路线概括起来是三句话:

  1. 用数据文件(JSON / YAML)维护全站内容数据,比如歌手、专辑、歌曲信息;
  2. 写一个独立的生成器程序,读取数据后套用模板,产出完整的静态 HTML 页面;
  3. 再把生成好的静态站点连同启动脚本一起打成 JAR 包,一条命令即可启动并访问。

这个方案解决了三个实际问题:服务器成本降到最低(一个几百 MB 的小机器就能跑)、开发调试效率提高(不需要启动数据库和中间件)、部署交付变得极其简单(不用在服务器上装环境,直接跑 JAR)。

提醒一点:静态生成并不是万能药,如果你的站点有大量用户交互、实时评论、个性化推荐,那还是踏踏实实用动态方案。静态生成适合的是“内容生产——发布——消费”这种单向流动的站点形态,这个界限要先想清楚再动手。

2. 内容建模与目录架构

做静态生成最容易犯的错误是一上来就写代码,结果生成器写得越来越复杂,数据格式反复改,最后变成一个大泥球。我在 v2.0 改 v3.0 时踩过这个坑,v3.0 先花了整整一天时间梳理内容和目录结构,事实证明花得值。

2.1 音乐站点的数据模型怎么设计

音乐网站的内容层级一般是:歌手(Artist)— 专辑(Album)— 歌曲(Song),外加一些聚合页面(榜单、分类、专题)。我最终用 JSON 文件做数据源,原因有三:JSON 零依赖,Java / Python / Node 都能直接读;结构清晰,嵌套关系天然贴合“歌手-专辑-歌曲”这种层级;编辑门槛低,不懂代码的人也能照着格式填数据。

一个简单的歌曲数据条目长这样:

{ "id": "song_001", "title": "夜风", "artist_id": "artist_001", "album_id": "album_001", "duration": "04:32", "lyric_url": "/lyrics/song_001.html", "audio_url": "/audio/song_001.mp3", "cover_url": "/covers/album_001.jpg", "release_date": "2024-03-15", "tags": ["民谣", "治愈"] }

歌手、专辑、歌曲各建一个 JSON 文件,用 ID 互相引用,这个设计参考了关系型数据库的外键思路,但去掉了数据库的运行时依赖。生成器读取数据后,在内存中建立索引,根据页面类型分别生成列表页、详情页。

2.2 目录结构与页面输出规划

静态站点的目录结构直接决定了 URL 的美观程度和后续扩展是否方便。我最终定的输出目录如下:

site/ ├── index.html // 首页:热门歌曲、推荐专辑 ├── artists/ │ ├── index.html // 歌手列表页 │ └── artist_001.html // 歌手详情页 ├── albums/ │ ├── index.html // 专辑列表页 │ └── album_001.html // 专辑详情页(含歌曲列表) ├── songs/ │ └── song_001.html // 歌曲详情页(含播放器嵌入) ├── charts/ │ └── hot.html // 热门榜单页 ├── assets/ │ ├── css/ │ ├── js/ │ └── images/

每个页面都是独立、完整的 HTML 文件,不依赖任何后端 API,直接把浏览器地址指到对应路径就能打开。这里有个细节:URL 后缀尽量用.html,而不是把页面做成/artists/artist_001这种伪静态路径。因为打成 JAR 包后用内嵌服务器访问时,伪静态路径需要额外的路径映射配置,而真实.html文件不需要任何特殊处理,文件名就是 URL,简单可靠。

2.3 模板拆分的粒度怎么把握

模板设计遵循一个原则:共用部分抽成组件,差异部分独立成页。头部导航、底部版权信息、播放器横条、侧边栏推荐位都抽成公共模板片段,页面级模板只关心自己特有的内容区域。

我用的是 FreeMarker 模板引擎,它的<#include>指令可以把公共片段嵌入到任何页面,还支持宏(macro)来实现类似函数的功能。比如生成歌曲列表时,用宏接收一个歌曲数组,循环输出列表项,这样在专辑页、热门榜、歌手页多处复用同一套渲染逻辑,改样式只需要动一处。

这里有一个实战建议:模板越多维护成本越高,v3.0 最终只拆了 8 个模板文件,却能覆盖全站所有页面类型。如果你的页面类型超过 15 种,就要重新审视一下是不是拆得太细了,因为静态生成的核心优势之一是简单,别把简单做复杂。

3. 生成器实现与静态化核心流程

这一部分是整个项目的心脏,也是从“能跑”到“好维护”的关键跳跃。先从热词里那个“生成显示 helloworld 的静态 html 页面”说起,很多人的困惑其实是从最小可运行例子到完整工程之间的鸿沟。

3.1 最小实现:从 Hello World 生成器开始

我最早的原型就是一个极简的生成器,核心逻辑只有三件事:读取模板文件、传入数据、输出 HTML。当时用 Java 实现了第一个版本,代码核心只有十几行:

Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); cfg.setDirectoryForTemplateLoading(new File("templates")); cfg.setDefaultEncoding("UTF-8"); Template template = cfg.getTemplate("index.ftl"); Map<String, Object> data = new HashMap<>(); data.put("siteName", "沁竹音乐网"); data.put("welcome", "Hello World"); try (FileWriter out = new FileWriter("site/index.html")) { template.process(data, out); }

这段代码做的事情非常朴素:从templates目录加载index.ftl模板,传入一个包含siteNamewelcome两个变量的数据模型,输出到site/index.html。运行完用浏览器打开site/index.html,就能看到带 Hello World 的页面。

别看它简单,它就是完整生成器的骨架。之后的整个 v3.0 生成器不过是在这个骨架上不断增加数据源、增加页面类型、增加公共组件而已。第一步先把“模板 + 数据 + 输出”这个闭环跑通,后面的路就好走了。

3.2 完整生成器的数据流设计

升级到完整版后,生成器的核心流程演变成四个阶段:加载数据、构建模型、渲染页面、拷贝资源。

加载数据阶段,把所有 JSON 文件读入内存,并建立好互相之间的关联索引。比如拿到song_001,通过artist_id找到对应的歌手对象,通过album_id找到对应的专辑对象,这样在渲染一首歌的详情页时,就能同时展示歌手名、专辑封面、专辑内其他歌曲等关联信息。

构建模型阶段,针对每个页面生成独立的渲染上下文。比如渲染专辑详情页时,上下文里包括专辑基本信息、歌曲列表、歌手信息、同风格推荐专辑等。这个上下文是纯粹的 Java Map 或 POJO,与模板引擎解耦。

渲染页面阶段,遍历所有歌手、专辑、歌曲、榜单配置,逐页生成 HTML。这里有一个优化要点:生成顺序要讲究,先渲染详情页,再渲染列表页,因为列表页可能会引用详情页的一些摘要信息,比如最新歌曲的封面图和时长。

拷贝资源阶段,把 CSS、JS、图片、音频文件从源码目录复制到输出目录。这一步用 Apache Commons IO 的FileUtils.copyDirectory就能搞定,几行代码解决。

3.3 页面生成性能实测

我最初担心全站上百个页面生成会不会慢,实际测下来完全多虑了。v3.0 全站包含 50 来位歌手、120 多张专辑、800 多首歌曲,加上列表页、榜单页、专题页,总计约 1100 个 HTML 页面。在我的老笔记本(i5-8250U,16GB 内存)上跑完整生成流程,耗时在 3 秒以内。为什么这么快?因为模板渲染是纯字符串替换,数据全部在内存中,没有 IO 等待、没有数据库查询,CPU 全速跑完也就是一瞬间的事。

实战心得:如果你要实现增量生成(只重新生成内容有变化的页面),就需要在数据中维护页面与数据之间的依赖关系,复杂度会成倍增加。我的建议是 v1 版本直接全量生成,1000 个页面 3 秒这个量级完全可以接受,别把方案搞复杂了。

4. 打包成 JAR:静态站点的交付新姿势

静态站做完,常规做法是丢到 Nginx 里完事。但沁竹音乐网 v3.0 选择了打成 JAR 包,这个决策是基于实际运维场景做的:用户拿到的交付物是一个可执行文件,双击就能跑,不要求服务器装有 Nginx、不要求配置虚拟主机、不要求懂 Linux,这对非专业运维的使用者来说极其友好。

4.1 用嵌入式服务器托管静态资源

JAR 包方案的核心是嵌入式服务器,我用的是 Jetty,因为它够轻量,集成方式简单。核心代码是用DefaultServlet把静态资源目录映射到根路径:

public class MusicSiteServer { public static void main(String[] args) throws Exception { Server server = new Server(8080); URL webRoot = MusicSiteServer.class.getClassLoader() .getResource("site"); WebAppContext context = new WebAppContext(); context.setResourceBase(webRoot.toURI().toString()); context.setContextPath("/"); context.setWelcomeFiles(new String[]{"index.html"}); context.addServlet(DefaultServlet.class, "/"); server.setHandler(context); server.start(); server.join(); } }

这段代码的关键是URLClassLoader.getResource("site"):站点文件放在资源目录下,打包进 JAR 后,运行时通过类加载器就能定位到这些资源。Jetty 的DefaultServlet会把site目录下的所有文件按路径映射出来访问,/index.html/songs/song_001.html等全部自动生效,不需要写任何额外的路由代码。

4.2 端口与访问路径的灵活处理

端口不能写死,尤其当用户机器上有其他程序占用 8080 时,服务直接启动失败。我做了一个简单的参数化处理:启动时支持通过--port参数指定端口,没指定时默认使用 8080,如果 8080 被占用则自动尝试 8081、8082,最多尝试 10 个端口。

int port = 8080; for (int i = 0; i < args.length - 1; i++) { if ("--port".equals(args[i])) { port = Integer.parseInt(args[i + 1]); } }

同时,启动后会在控制台打印访问地址,并且尝试调用系统默认浏览器打开首页。这个小细节很提升体验,用户拿到包后什么都不用管,双击 JAR,浏览器自动弹出音乐网首页。

4.3 打包配置的完整解析

打包使用 Maven Shade Plugin,把依赖、资源文件、启动类全部打进一个可执行 JAR。关键配置如下:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.0</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <finalName>qinzhumusic</finalName> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.qinzhu.bootstrap.MusicSiteServer</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin>

三四两个配置点值得展开讲。ManifestResourceTransformer的作用是让java -jar qinzhumusic.jar能直接找到入口类,不加这个配置会报“no main manifest attribute”错误。filter 里排除META-INF/*.SF*.DSA*.RSA这几个签名文件,是因为 Shade 打包依赖时会混入依赖包的签名信息,不加排除会有SecurityException: Invalid signature file digest的报错,这是新手最容易踩的坑之一。

生成器输出的site目录要作为资源打进 JAR,在pom.xml的 build 配置里加:

<resources> <resource> <directory>site</directory> <targetPath>site</targetPath> </resource> <resource> <directory>templates</directory> <targetPath>templates</targetPath> </resource> </resources>

site目录映射到 JAR 内的/site路径,运行时通过类加载器定位。templates目录也可以一并打包进去,方便后续做二次生成。完整的构建流程是:先运行生成器产出site目录,再执行mvn package打包 JAR,一条命令搞定。

5. 常见问题与排查技巧实录

从 v3.0 开发到交付,我实际踩了不少坑,记录在这里,供遇到类似问题的人参考。

5.1 资源路径 404:相对路径与绝对路径的坑

最早生成的页面里,CSS 和图片用的是相对路径,比如css/style.css。这种方式在目录层级浅的时候没问题,但一旦访问/songs/song_001.html这个深层页面,浏览器解析相对路径会变成/songs/css/style.css,资源自然 404。

排查方法:打开浏览器开发者工具(F12),切换到 Console 和 Network 面板,看具体的 404 请求路径是什么,判断是相对路径解析错误还是文件确实没打包进去。

解决方案:模板中所有资源引用全部使用绝对路径,以/开头,例如/assets/css/style.css。因为内嵌服务器的 contextPath 固定为/,使用绝对路径后无论页面在哪个层级,资源都能正确加载。

5.2 UTF-8 乱码:文件编码统一问题

生成出来的页面中文显示乱码,是编码不一致导致的。模板文件是 UTF-8 编码,但 FreeMarker 默认编码可能不是 UTF-8;同时输出的 HTML 文件头里没有声明的 charset 也可能导致乱码。

解决方案有两层:模板加载时指定编码为 UTF-8(cfg.setDefaultEncoding("UTF-8"));输出 HTML 的<head>里加上<meta charset="UTF-8">。另外确保编辑模板的 IDE 文件编码默认是 UTF-8,否则文件存盘时就已经是乱码了。

5.3 更新内容后页面没变化:缓存惹的祸

改完数据重新生成,浏览器打开看到的还是旧页面。这个大概率是浏览器缓存或 CDN 缓存。开发调试时按Ctrl+F5强制刷新可以验证;给 CSS / JS 文件加上版本号参数(如style.css?v=20240315)可以从根源上解决。

生成器侧也可以做一些预防工作:输出时把所有资源文件的时间戳设为当前时间,有些服务器或浏览器会基于 Last-Modified 做缓存校验,文件时间变了就会重新请求。

5.4 常见问题排查速查表

问题可能原因解决方案
启动 JAR 报主类找不到没配置 ManifestResourceTransformer检查 pom.xml 中 mainClass 配置
启动报签名文件错误依赖包签名冲突在 Shade 插件的 filters 中排除 META-INF 下的 .SF/.DSA/.RSA 文件
页面样式全丢资源路径用了相对路径 / CSS 文件没打包改用绝对路径,检查 maven-resources 配置
中文乱码模板编码或输出编码不一致统一 UTF-8,HTML 头部声明 charset
8080 端口被占用其他程序占用端口实现端口参数配置和自动尝试机制
音频文件无法播放浏览器限制媒体自动播放页面中播放器加controls属性,响应用户点击后再播放

5.5 打 JAR 后临时文件处理

JAR 包内的资源是只读的,如果生成器运行时想往templates目录写文件,直接写会报错。建议在代码中把模板和站点资源读取后都视为只读输入,所有需要写的临时文件放到系统临时目录。我在做图片缩略时遇到过一次这个问题,生成器在 JAR 内运行时想输出缩略图到资源目录,结果一直报FileNotFoundException,最后改成输出到项目外部的data/generated路径,问题解决。

6. 从 v3.0 延伸到更远的场景

静态生成版完成之后,日常维护几乎变成了“改数据、跑生成、重启”三步曲,把服务器维护成本压到了接近零。现在服务器上只需要一个 Java 运行环境,没有数据库、没有中间件、没有进程守护脚本,JAR 包启动后就是一个完整的站点。

这套方案对我的实际意义还在于:它可以很方便地嵌入到自动化流程中。比如说,数据更新后触发生成器,再自动重启 JAR,整个过程不需要人工干预。虽然 v3.0 还没接这种方式,但生成器的独立设计已经为它留好了接口。我的一个实际感受是:静态生成并不是把问题变简单了,而是把复杂的问题移到构建期一次解决,运行时只保留最简单、最稳定的一条路。选择技术方案的时候,与其追逐最新的框架,不如把“生成流程清晰、部署成本低、扩展路径明确”这三件事想明白。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询