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

Java 專案靜態頁面生成方案:URLConnection 抓取動態頁輸出 HTML 完整實作

     編輯:關於JSP

為什麼要生成靜態頁?

優勢說明
效能無資料庫查詢、無模板渲染、直接由 Web Server (Nginx/Apache) 靜態檔案服務,極致快速
SEO搜尋引擎爬蟲偏好靜態 URL,利於收錄與排名
安全隱藏後端技術棧、資料庫結構、參數邏輯,降低注入/探測風險
穩定資料庫掛掉不影響已生成頁面;靜態檔案伺服器極簡、極穩

兩大主流方案比較

方案原理優點缺點適用場景
模板引擎FreeMarker/Velocity/Thymeleaf 渲染模板產出 HTML資料與視圖分離、邏輯可控、支援複雜條件需維護模板、語法學習成本、大頁面模板編寫繁瑣結構固定、需精細控制輸出的頁面
URL 抓取HTTP 請求動態頁,將回應直接存檔零侵入、複用現有 Controller/View、無需寫模板需處理 Session/Cookie、動態參數、並發生成鎖現有系統改造、頁面邏輯複雜、快速上線

核心工具類:HTMLGenerator

package com.example.util;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.regex.*;

public class HTMLGenerator {

    private static final Pattern URL_PATTERN = Pattern.compile("^(https?://)[\\w\\-\\./:]+");

    /**
     * 抓取 URL 完整 HTML 內容
     * @param url 目標完整 URL (含協定、域名、路徑、查詢字串)
     * @return HTML 字串,失敗返回 null
     */
    public static String fetch(String url) {
        if (StringUtils.isBlank(url) || !URL_PATTERN.matcher(url).find()) {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        try {
            URL target = new URL(url);
            URLConnection conn = target.openConnection();
            // 模擬瀏覽器,避免 403
            conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; HTMLGenerator/1.0)");
            conn.setConnectTimeout(10000);
            conn.setReadTimeout(30000);

            try (BufferedReader in = new BufferedReader(
                    new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
                String line;
                while ((line = in.readLine()) != null) {
                    sb.append(line);
                }
            }
        } catch (MalformedURLException e) {
            log.error("URL 格式錯誤: {}", url, e);
        } catch (IOException e) {
            log.error("抓取失敗: {}", url, e);
        }
        return sb.length() > 0 ? sb.toString() : null;
    }

    /**
     * 依請求動態生成靜態檔案
     * @param request 當前請求 (用於取得 contextPath、realPath)
     * @param url 目標 URL,null 表示當前請求 URL
     * @param toWebRoot true=存至 WebRoot 同層級,false=存至 /files/history/日期/
     * @param encoding 輸出編碼
     * @throws IOException
     */
    public void genHtml(HttpServletRequest request, String url, boolean toWebRoot, String encoding) throws IOException {
        if (url == null) url = request.getRequestURL().toString();

        String contextPath = request.getContextPath();
        String realRoot = request.getServletContext().getRealPath("/");
        if (!realRoot.endsWith(File.separator)) realRoot += File.separator;

        // 目標相對路徑:去除 contextPath,副檔名統一 .html
        String relPath = StringUtils.substringAfter(url, contextPath);
        relPath = relPath.replaceAll("\\.(do|jsp|action|html?)$", ".html");

        String saveDir, fileName;
        if (toWebRoot) {
            saveDir = "";
            fileName = relPath;
        } else {
            // 歷史版本目錄:files/history/yyyy/MM/dd/
            saveDir = "files" + File.separator + "history" + File.separator
                    + new SimpleDateFormat("yyyy/MM/dd/").format(new Date());
            // 檔名加入時間戳後 6 碼避免衝突
            String seq = String.valueOf(System.currentTimeMillis()).substring(6);
            fileName = StringUtils.substringBeforeLast(relPath, ".html") + "-" + seq + ".html";
        }

        File outFile = new File(realRoot + saveDir + fileName);
        FileUtils.writeStringToFile(outFile, fetch(url), encoding, false);
    }
}

Controller 整合範例

@Controller
public class CmsController {

    @Autowired HTMLGenerator htmlGenerator;

    @GetMapping("/article/{id}")
    public String view(@PathVariable Long id, HttpServletRequest request, Model model) {
        Article article = articleService.get(id);
        model.addAttribute("article", article);

        // 異步生成靜態頁,不阻塞回應
        CompletableFuture.runAsync(() -> {
            try {
                String url = request.getScheme() + "://" + request.getServerName() 
                           + ":" + request.getServerPort() + request.getRequestURI();
                htmlGenerator.genHtml(request, url, true, "UTF-8");
            } catch (Exception e) {
                log.error("靜態頁生成失敗", e);
            }
        });

        return "article/view"; // 正常渲染動態頁
    }
}

進階議題與解決方案

1. 權限頁面/個人化內容

  • 靜態頁不適用於「每人不同」的頁面 (購物車、用戶中心)。
  • 方案:僅對公開、緩存友善頁面 (文章、列表、商品詳情) 生成靜態;其餘走動態。

2. 並發生成衝突

// 使用分散式鎖 (Redis SETNX) 或本地鎖
String lockKey = "htmlgen:lock:" + url.hashCode();
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (locked) {
    try { htmlGenerator.genHtml(...); }
    finally { redisTemplate.delete(lockKey); }
}

3. 靜態資源 (CSS/JS/圖片) 路徑失效

  • 生成頁面中的相對路徑在深層目錄下會斷裂。
  • 方案:頁面統一使用 絕對路徑 (/static/...) 或 <base href="...">。

4. 更新/失效機制

  • 主動重新生成:資料異動時 (發布、編輯、刪除) 觸發相關頁面重新生成。
  • 定時掃描:每日跑批次重新生成熱門頁面。
  • Nginx try_files:優先尋找靜態檔,不存在再反向代理動態。
    location /article/ {
        try_files /static$uri @backend;
    }
    location @backend { proxy_pass https://tomcat; }
    

5. 大規模站點架構建議

  • 生成服務獨立部署,透過消息佇列 (Kafka/RocketMQ) 接收「頁面失效」事件。
  • 靜態檔案存儲於對象存儲 (S3/OSS/MinIO) + CDN 分發,Web Server 僅做回源。
  • 引入 增量生成:僅重新生成受影響頁面,配合依賴圖 (文章→分類列表→首頁) 自動級聯。

總結

URLConnection 抓取法以「零侵入、復用現有邏輯」見長,適合存量系統快速靜態化。配合異步生成、分散式鎖、CDN 回源,可支撐百萬級靜態頁管理。核心原則:公開頁靜態化、私有頁動態化、路徑絕對化、更新事件驅動。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved