首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> JSP 程式設計 >> 關於JSP

JSP 結合 JavaBean 生成靜態頁面:模板引擎模式實現詳解

     編輯:關於JSP

核心思想

將動態 JSP 頁面渲染結果捕獲並寫入靜態 HTML 文件,後續請求直接由 Web 服務器 (Nginx/Apache) 靜態服務,消除 JSP 編譯執行開銷、數據庫查詢壓力,極大提升高並發讀場景性能。

實現架構

[用戶請求] → [Controller/Servlet] → [數據準備] → [模板引擎渲染] → [寫入靜態文件] → [返回/重定向靜態頁]
                                      ↑
                              [定時任務/事件觸發重新生成]

方案一:基於 JSP include 指令的靜態化 (簡易版)

缺點

  • 仍需 JSP 容器運行
  • 無法完全卸載動態邏輯
  • 難以實現增量更新

不推薦生產使用,僅作理解原理。

方案二:JavaBean + 模板引擎 (推薦標準)

使用成熟模板引擎 替代 JSP,支持獨立運行、語法豐富、緩存編譯。

1. 依賴 (Maven)


<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version><!-- 或 2.3.33 -->
</dependency>
<!-- 或 Thymeleaf -->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.1.2.RELEASE</version>
</dependency>

2. FreeMarker 模板文件 (templates/article.ftl)


<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>${article.title?html} - ${siteName}</title>
    <meta name="description" content="${article.summary?html}">
    <link rel="stylesheet" href="/static/css/article.css">
</head>
<body>
    <header><h1>${siteName}</h1></header>
    <article>
        <h2>${article.title?html}</h2>
        <div class="meta">
            作者: ${article.author?html} | 
            發布: ${article.publishTime?string("yyyy-MM-dd HH:mm")} | 
            分類: ${article.category?html}
        </div>
        <div class="content">
            ${article.content} <#-- 富文本內容已含 HTML,信任來源時不轉義 -->
        </div>
    </article>
    <footer>&copy; ${.now?string("yyyy")} ${siteName}</footer>
</body>
</html>

3. JavaBean 數據模型


package com.example.model;

import java.time.LocalDateTime;

public class Article {
    private Long id;
    private String title;
    private String summary;
    private String content;      // HTML 富文本
    private String author;
    private String category;
    private LocalDateTime publishTime;
    // Getter/Setter/Builder 省略
}

4. 靜態頁生成服務


package com.example.service;

import com.example.model.Article;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@Service
public class StaticPageGenerator {

    private final Configuration cfg;
    private final String outputBaseDir;

    public StaticPageGenerator(Configuration cfg, @Value("${static.output.dir:/var/www/html}") String outputBaseDir) {
        this.cfg = cfg;
        this.outputBaseDir = outputBaseDir;
        initConfig();
    }

    private void initConfig() {
        cfg.setClassForTemplateLoading(this.getClass(), "/templates");
        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        cfg.setLogTemplateExceptions(false);
        cfg.setWrapUncheckedExceptions(true);
        cfg.setApiBuiltinEnabled(true); // 允許 ?api 內置
    }

    /**
     * 生成文章靜態頁
     * @param article 文章數據
     * @return 生成的文件相對路徑 (如 /article/123.html)
     */
    public String generateArticlePage(Article article) {
        try {
            Template template = cfg.getTemplate("article.ftl");

            // 數據模型
            Map model = new HashMap<>();
            model.put("article", article);
            model.put("siteName", "技術博客");

            // 輸出文件路徑: /article/{id}.html
            String relPath = "/article/" + article.getId() + ".html";
            File outFile = new File(outputBaseDir, relPath);
            outFile.getParentFile().mkdirs(); // 確保目錄存在

            // 渲染寫入
            try (Writer writer = new FileWriter(outFile, StandardCharsets.UTF_8)) {
                template.process(model, writer);
            }

            return relPath;
        } catch (Exception e) {
            throw new RuntimeException("生成靜態頁失敗: " + article.getId(), e);
        }
    }

    /**
     * 批量生成 (如全站靜態化)
     */
    public void generateAllArticles(Iterable
articles) { for (Article article : articles) { try { generateArticlePage(article); } catch (Exception e) { log.error("生成失敗: {}", article.getId(), e); } } } }

5. Controller 觸發生成


@RestController
@RequestMapping("/admin/article")
public class ArticleAdminController {

    @Autowired private ArticleService articleService;
    @Autowired private StaticPageGenerator staticGenerator;

    @PostMapping("/{id}/publish")
    public Result publish(@PathVariable Long id) {
        Article article = articleService.getById(id);
        article.setStatus(ArticleStatus.PUBLISHED);
        articleService.update(article);

        // 異步生成靜態頁 (不阻塞響應)
        CompletableFuture.runAsync(() -> staticGenerator.generateArticlePage(article));
        
        return Result.success();
    }

    @PostMapping("/{id}/unpublish")
    public Result unpublish(@PathVariable Long id) {
        Article article = articleService.getById(id);
        article.setStatus(ArticleStatus.DRAFT);
        articleService.update(article);

        // 刪除靜態文件
        CompletableFuture.runAsync(() -> deleteStaticFile(article.getId()));
        return Result.success();
    }
}

方案三:JSP 捕獲輸出流生成靜態頁 (遷移過渡方案)

若現有項目全是 JSP,短期無法引入模板引擎,可用此法過渡:


public String generateFromJsp(HttpServletRequest req, HttpServletResponse resp, String jspPath, Map model) 
        throws ServletException, IOException {
    
    // 1. 將模型數據放入 request
    model.forEach(req::setAttribute);

    // 2. 捕獲 JSP 輸出
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ServletOutputStream servletOut = new ServletOutputStream() {
        @Override public void write(int b) { baos.write(b); }
        @Override public void write(byte[] b, int off, int len) { baos.write(b, off, len); }
        @Override public boolean isReady() { return true; }
        @Override public void setWriteListener(WriteListener l) {}
    };
    
    // 3. 包裝 response
    HttpServletResponse wrappedResp = new HttpServletResponseWrapper(resp) {
        @Override public ServletOutputStream getOutputStream() { return servletOut; }
        @Override public PrintWriter getWriter() { return new PrintWriter(servletOut, true); }
    };

    // 4. 請求分派器包含 JSP (執行 JSP 並寫入 baos)
    req.getRequestDispatcher(jspPath).include(req, wrappedResp);

    // 5. 獲取生成的 HTML
    String html = baos.toString(StandardCharsets.UTF_8);
    
    // 6. 寫入文件...
    return html;
}

靜態文件服務最佳實踐 (Nginx)


# nginx.conf
server {
    listen 80;
    server_name blog.example.com;
    root /var/www/html;  # 靜態文件輸出目錄

    # 文章靜態頁直接服務
    location /article/ {
        try_files $uri $uri/ =404;
        expires 1h;
        add_header Cache-Control "public";
    }

    # 動態接口轉發到後端
    location /api/ {
        proxy_pass https://backend:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # 管理後台等需認證頁面
    location /admin/ {
        proxy_pass https://backend:8080;
        # auth_basic ...
    }
}

緩存失效與更新策略

策略觸發時機實現方式優/缺點
發布/更新時重新生成文章發布、編輯保存Controller 調用生成服務強一致、實時
定時全量重新生成每日凌晨/每小時Quartz/XXL-Job 定時任務遍歷數據庫兜底、修復異常文件
增量生成 (消息驅動)數據變更事件RabbitMQ/Kafka 發送 ArticleChangedEvent,消費者生成高性能、解耦
CDN 緩存刷新靜態文件更新後調用 CDN API (阿里云/騰訊云/Cloudflare) 刷新 URL邊緣節點同步

文件命名與組織規範


/var/www/html/
├── article/
│   ├── 1001.html
│   ├── 1002.html
│   └── index.html          # 列表頁靜態化
├── category/
│   ├── java.html
│   └── python.html
├── tag/
│   ├── spring.html
│   └── jvm.html
├── sitemap.xml
├── robots.txt
└── static/
    ├── css/
    ├── js/
    └── images/
  • ID 基於路徑:/article/{id}.html 簡單、唯一、利於 SEO
  • 目錄分層:單目錄文件數 < 10000,避免文件系統性能下降
  • 版本化靜態資源**:app.v20240115.css 長期緩存

總結對比

方案適用場景性能維護成本推薦度
JSP include 靜態化極簡頁面、無模板引擎經驗低 (仍走 Servlet 容器)高 (邏輯耦合在 JSP)⭐
FreeMarker/Thymeleaf + JavaBean內容發布系統、電商商品頁、部落格、文檔站極高 (純靜態文件 + Nginx)低 (模板分離、語法強大)⭐⭐⭐⭐⭐
JSP 輸出捕獲存量 JSP 專案過渡期中 (仍需容器渲染)中 (復用現有 JSP)⭐⭐ (過渡用)
現代 SSG (Next.js/Astro/VitePress)新項目、前端分離、組件化極高 (構建時生成、邊緣部署)低 (前端生態)⭐⭐⭐⭐ (新架構)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved