基于域名的静态资源隔离
在多站点部署或不同环境隔离的场景下,我们可以通过基于域名的配置实现资源隔离。不同的域名对应不同的配置文件,进而映射到服务器文件系统中的不同目录。这样可以在一台服务器上为多个站点提供服务,每个站点的静态资源都存放在独立的目录中。在 tio-boot 中可以使用自定义 StaticResourceHandler 实现
数据库表定义
下列 SQL 脚本用于创建保存各个域名对应静态资源目录信息的数据表 website_page_folder
。其中主要字段包括 domain
表示域名,path
表示与该域名对应的静态资源根目录。
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
。
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
该服务负责根据请求的域名从数据库中获取对应的资源根目录。
package com.litongjava.website.service;
import com.litongjava.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
package com.litongjava.website.handler;
import java.io.File;
import com.litongjava.constants.ServerConfigKeys;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.boot.http.handler.internal.StaticResourceHandler;
import com.litongjava.tio.boot.utils.HttpResourceUtils;
import com.litongjava.tio.http.common.HeaderName;
import com.litongjava.tio.http.common.HeaderValue;
import com.litongjava.tio.http.common.HttpConfig;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResource;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.HttpResponseStatus;
import com.litongjava.tio.http.server.handler.FileCache;
import com.litongjava.tio.http.server.util.Resps;
import com.litongjava.tio.utils.cache.AbsCache;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.hutool.StrUtil;
import com.litongjava.website.service.PageDomainService;
import com.litongjava.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.setHasGzipped(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());
}
}
该处理器负责接收静态资源请求,并定位实际文件。关键步骤如下:
生成缓存键
利用请求的 host 和 URI 生成唯一的缓存键。从缓存中读取
如果缓存中有对应的响应,则直接返回缓存内容。定位资源
根据请求中获取的域名,通过PageFolderService
获取资源根目录,然后将请求路径与该根目录拼接生成完整的文件路径。例如:- 请求路径:
/images/439681354499067904.png
- 返回的资源根目录:
pages/channel_001
- 拼接后完整的文件路径:
pages/channel_001/images/439681354499067904.png
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; }
- 请求路径:
构建响应
如果文件存在,读取文件内容后构建 HTTP 响应,同时支持静态文件缓存,进一步提升性能。
URL 访问与文件路径映射说明
当用户访问 URL:
http://localhost:8123/images/439681354499067904.png
系统内部的处理过程如下:
提取域名
请求中包含的域名为localhost
。查询对应的资源根目录
通过PageFolderService.getByDomain("localhost")
方法查询数据库,返回值为pages/channel_001
。拼接路径
将返回的资源根目录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",以此返回一个简单的成功标识。这种方式非常适用于在前端文件更新后希望立即刷新缓存、加载最新资源的场景。
package com.litongjava.website.handler;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.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;
}
}