Skip to content

基于域名的静态资源隔离 ​

在多站点部署或不同环境隔离的场景下,我们可以通过基于域名的配置实现资源隔离。不同的域名对应不同的配置文件,进而映射到服务器文件系统中的不同目录。这样可以在一台服务器上为多个站点提供服务,每个站点的静态资源都存放在独立的目录中。在 tio-boot 中可以使用自定义 StaticResourceHandler 实现

数据库表定义 ​

下列 SQL 脚本用于创建保存各个域名对应静态资源目录信息的数据表 website_page_folder。其中主要字段包括 domain 表示域名,path 表示与该域名对应的静态资源根目录。

sql
drop table if exists website_page_folder;
CREATE TABLE website_page_folder (
  id BIGINT primary key,
  name varchar(256),
  code varchar(256),
  path varchar(256),
  domain varchar(256),
  status int,
  "creator" VARCHAR(64) DEFAULT '',
  "create_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "updater" VARCHAR(64) DEFAULT '',
  "update_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "deleted" SMALLINT DEFAULT 0,
  "tenant_id" BIGINT NOT NULL DEFAULT 0
);

在初始化时,插入一条记录,用于 localhost 域名,其静态资源目录为 pages/channel_001。

sql
INSERT INTO "website_page_folder"("id", "name", "code", "path", "domain", "status", "creator", "create_time", "updater", "update_time", "deleted", "tenant_id")
VALUES (2, 'localhost', 'localhost', 'pages/channel_001', 'localhost', NULL, '', '2025-03-13 09:14:53.140402+08', '', '2025-03-13 09:14:53.140402+08', 0, 0);

代码实现 ​

系统中主要有两个模块实现资源隔离的逻辑:

1. PageFolderService ​

该服务负责根据请求的域名从数据库中获取对应的资源根目录。

java
package nexus.io.website.service;

import nexus.io.db.activerecord.Db;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class PageFolderService {

  public String getByDomain(String domain) {
    log.info("doamin:{}",domain);
    String sql = "select path from website_page_folder where domain=?";
    return Db.queryStr(sql, domain);
  }
}

在 getByDomain 方法中,根据传入的域名参数查询数据库,返回相应的资源目录。例如,当请求的域名为 localhost 时,将返回 pages/channel_001。

2. MyStaticResourceHandler ​

java
package nexus.io.website.handler;

import java.io.File;

import nexus.io.constants.ServerConfigKeys;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.boot.http.handler.internal.StaticResourceHandler;
import nexus.io.tio.boot.utils.HttpResourceUtils;
import nexus.io.tio.http.common.HeaderName;
import nexus.io.tio.http.common.HeaderValue;
import nexus.io.tio.http.common.HttpConfig;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResource;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.common.HttpResponseStatus;
import nexus.io.tio.http.server.handler.FileCache;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.cache.AbsCache;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.hutool.StrUtil;
import nexus.io.website.service.PageDomainService;
import nexus.io.website.service.PageFolderService;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyStaticResourceHandler implements StaticResourceHandler {
  private static final long MAX_CACHE_FILE_SIZE = 5 * 1024 * 1024;
  private static final HeaderName TIO_FROM_CACHE = HeaderName.tio_from_cache;
  private static final HeaderValue TRUE_CACHE = HeaderValue.Tio_From_Cache.TRUE;
  private PageFolderService pageFolderService = Aop.get(PageFolderService.class);
  private PageDomainService pageDomainService = Aop.get(PageDomainService.class);

  private static final long FILE_SIZE_THRESHOLD = 10 * 1024 * 1024; // 10MB分界点

  @Override
  public HttpResponse handle(String path, HttpRequest request, HttpConfig httpConfig, AbsCache staticResCache) {
    try {
      String cacheKey = generateCacheKey(request);
      HttpResponse cachedResponse = tryServeFromCache(cacheKey, request, staticResCache);
      if (cachedResponse != null) {
        return cachedResponse;
      }

      HttpResource httpResource = locateResource(path, request, httpConfig);
      if (httpResource == null || !httpResource.getFile().exists()) {
        HttpResponse response = TioRequestContext.getResponse();
        response.setStatus(HttpResponseStatus.C404);
        return Resps.html(response, "Resource not found");
      }

      HttpResponse response = TioRequestContext.getResponse();
      HttpResourceUtils.buildFileResponse(response, httpResource);
      cacheResponseIfNeeded(cacheKey, httpResource.getFile(), response, staticResCache);
      return response;

    } catch (Exception e) {
      log.error("Error handling static resource: {}", path, e);
      HttpResponse response = TioRequestContext.getResponse();
      response.setStatus(HttpResponseStatus.C500);
      return Resps.html(response, "Internal server error");
    }
  }

  private String generateCacheKey(HttpRequest request) {
    return request.getHost().replace(':', '_') + request.getRequestURI();
  }

  private HttpResponse tryServeFromCache(String cacheKey, HttpRequest request, AbsCache staticResCache) {
    if (!isCacheEnabled() || staticResCache == null)
      return null;

    FileCache fileCache = (FileCache) staticResCache.get(cacheKey);
    if (fileCache == null)
      return null;

    HttpResponse notModifiedResponse = checkNotModified(request, fileCache.getLastModified());
    if (notModifiedResponse != null)
      return notModifiedResponse;

    return buildCachedResponse(request, fileCache);
  }

  private boolean isCacheEnabled() {
    return EnvUtils.getBoolean(ServerConfigKeys.SERVER_RESOURCES_STATIC_FILE_CACHE_ENABLE, false);
  }

  private HttpResponse checkNotModified(HttpRequest request, long lastModified) {
    return Resps.try304(request, lastModified);
  }

  private HttpResponse buildCachedResponse(HttpRequest request, FileCache fileCache) {
    HttpResponse response = new HttpResponse(request);
    response.setBody(fileCache.getContent());
    response.setLastModified(HeaderValue.from(String.valueOf(fileCache.getLastModified())));
    response.setSkipGzipped(fileCache.isHasGzipped());
    response.addHeader(TIO_FROM_CACHE, TRUE_CACHE);

    if (fileCache.getContentType() != null) {
      response.addHeader(HeaderName.Content_Type, fileCache.getContentType());
    }
    if (fileCache.getContentEncoding() != null) {
      response.addHeader(HeaderName.Content_Encoding, fileCache.getContentEncoding());
    }
    return response;
  }

  private HttpResource locateResource(String path, HttpRequest request, HttpConfig httpConfig) throws Exception {
    String domain = request.getDomain();
    String pageRoot = pageFolderService.getPathByDomain(domain);
    if (StrUtil.isEmpty(pageRoot)) {
      pageRoot = pageDomainService.getPathByDomain(domain);
      if (StrUtil.isEmpty(pageRoot)) {
        pageRoot = httpConfig.getPageRoot();
      }
    }

    return HttpResourceUtils.getResource(pageRoot, path);
  }

  private void cacheResponseIfNeeded(String cacheKey, File file, HttpResponse response, AbsCache staticResCache) {
    // 大文件不进行缓存
    if (file.length() > FILE_SIZE_THRESHOLD) {
      log.debug("Skipping cache for large file: {}", cacheKey);
      return;
    }

    if (!shouldCache(response))
      return;

    byte[] content = response.getBody();
    if (content.length > MAX_CACHE_FILE_SIZE) {
      log.info("File too large for caching: {} ({} bytes)", cacheKey, content.length);
      return;
    }

    FileCache fileCache = createFileCache(file, response, content);
    staticResCache.put(cacheKey, fileCache);
    log.info("Cached resource: {} ({} bytes)", cacheKey, content.length);
  }

  private boolean shouldCache(HttpResponse response) {
    return isCacheEnabled() && response.isStaticRes() && response.getStatus() == HttpResponseStatus.C200 && response.getBody() != null;
  }

  private FileCache createFileCache(File file, HttpResponse response, byte[] content) {
    return new FileCache(content, file.lastModified(), response.getHeader(HeaderName.Content_Type), response.getHeader(HeaderName.Content_Encoding), response.hasGzipped());
  }
}

该处理器负责接收静态资源请求,并定位实际文件。关键步骤如下:

  1. 生成缓存键
    利用请求的 host 和 URI 生成唯一的缓存键。

  2. 从缓存中读取
    如果缓存中有对应的响应,则直接返回缓存内容。

  3. 定位资源
    根据请求中获取的域名,通过 PageFolderService 获取资源根目录,然后将请求路径与该根目录拼接生成完整的文件路径。例如:

    • 请求路径:/images/439681354499067904.png
    • 返回的资源根目录:pages/channel_001
    • 拼接后完整的文件路径:pages/channel_001/images/439681354499067904.png
    java
    private HttpResource locateResource(String path, HttpRequest request, HttpConfig httpConfig) throws Exception {
      String domain = request.getDomain();
      String pageRoot = pageFolderService.getByDomain(domain);
      return getResource(pageRoot, path);
    }
    
    public HttpResource getResource(String pageRoot, String path) throws Exception {
      HttpResource httpResource = null;
      if (pageRoot != null) {
        if (StrUtil.endWith(path, "/")) {
          path = path + "index.html";
        }
        String complatePath = pageRoot + path;
        File file = new File(complatePath);
        if (file.exists()) {
          httpResource = new HttpResource(path, null, file);
        }
      }
      return httpResource;
    }
  4. 构建响应
    如果文件存在,读取文件内容后构建 HTTP 响应,同时支持静态文件缓存,进一步提升性能。

URL 访问与文件路径映射说明 ​

当用户访问 URL:

http://localhost:8123/images/439681354499067904.png

系统内部的处理过程如下:

  1. 提取域名
    请求中包含的域名为 localhost。

  2. 查询对应的资源根目录
    通过 PageFolderService.getByDomain("localhost") 方法查询数据库,返回值为 pages/channel_001。

  3. 拼接路径
    将返回的资源根目录 pages/channel_001 与请求路径 /images/439681354499067904.png 进行拼接,最终定位到文件系统中的路径为:

    pages/channel_001/images/439681354499067904.png

这种设计实现了基于域名的资源隔离。尽管用户访问的是 /images/439681354499067904.png,但是后端会根据域名确定使用哪个资源根目录。这样可以在同一服务器上支持多个不同站点的静态资源,同时保证每个站点的资源不会混淆。


总结来说,这套系统通过在数据库中配置域名与对应资源目录的映射关系,实现了基于域名的资源隔离。请求 URL 中的路径经过域名解析后,会映射到对应站点的资源目录下,从而读取正确的静态文件。这不仅使得资源管理更加清晰,也为不同站点提供了灵活的配置能力。

清空静态文件缓存 ​

在 tio-boot 框架中,我们可以通过编写一个处理器来清空静态文件缓存。示例代码中,处理器类 StaticResCacheHandler 定义了一个 clear 方法,该方法首先调用 TioBootServer.me().getHttpRequestDispatcher().clearStaticResCache() 来清空静态资源缓存,然后利用 TioRequestContext.getResponse() 获取当前请求的响应对象,并将响应内容设置为 "OK",以此返回一个简单的成功标识。这种方式非常适用于在前端文件更新后希望立即刷新缓存、加载最新资源的场景。

java
package nexus.io.website.handler;

import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;

public class StaticResCacheHandler {

  public HttpResponse clear(HttpRequest request) {
    TioBootServer.me().getHttpRequestDispatcher().clearStaticResCache();
    HttpResponse response = TioRequestContext.getResponse();
    response.setString("OK");
    return response;
  }
}

MIT Licensed