通过 Handler 转发请求
在一些情况下,我们可能需要查看客户端的请求转发到另个一个服端,包括请求头、请求体等。可以参考本文章实现
打印请求信息
拦截所有请求
配置一个 HttpRequestHanlderConfig 拦截所有请求
import com.litongjava.jfinal.aop.annotation.AConfiguration;
import com.litongjava.jfinal.aop.annotation.AInitialization;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.RequestRoute;
@AConfiguration
public class HttpRequestHanlderConfig {
@Initialization
public void config() {
// 获取router
RequestRoute r = TioBootServer.me().getRequestRoute();
// 创建handler
IndexRequestHandler indexRequestHandler = new IndexRequestHandler();
// 添加action
r.add("/*", indexRequestHandler::index);
}
}
示例代码
import java.util.Map;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpMethod;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.RequestLine;
public class IndexRequestHandler {
public HttpResponse index(HttpRequest request) {
StringBuffer printRequest = getRequest(request);
return TioRequestContext.getResponse().setString(printRequest.toString());
}
public static StringBuffer getRequest(HttpRequest httpRequest) {
RequestLine requestLine = httpRequest.getRequestLine();
HttpMethod requestMethod = requestLine.getMethod();
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(requestLine.toString()).append("\r\n");
// 打印请求头信息
Map<String, String> requestHeaders = httpRequest.getHeaders();
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
stringBuffer.append(e.getKey()).append(":").append(e.getValue()).append("\r\n");
}
stringBuffer.append("\r\n");
// 设置请求体
if (HttpMethod.POST.equals(requestMethod)) {
String contentType = httpRequest.getContentType();
if (contentType != null) {
if (contentType.startsWith("application/json")) {
stringBuffer.append(httpRequest.getBodyString());
} else if (contentType.startsWith("application/x-www-form-urlencoded")) {
Map<String, Object[]> params = httpRequest.getParams();
for (Map.Entry<String, Object[]> e : params.entrySet()) {
// 添加参数
stringBuffer.append(e.getKey()).append(":").append(e.getValue()[0]);
}
} else if (contentType.startsWith("multipart/form-data")) {
Map<String, Object[]> params = httpRequest.getParams();
for (Map.Entry<String, Object[]> e : params.entrySet()) {
Object value = e.getValue()[0];
// 添加参数
if (value instanceof String) {
stringBuffer.append(e.getKey()).append(":").append(e.getValue()[0]).append("\r\n");
} else {
stringBuffer.append(e.getKey()).append(":").append("binary").append("\r\n");
}
}
} else {
stringBuffer.append(httpRequest.getBodyString());
}
} else {
stringBuffer.append(httpRequest.getBodyString());
}
} else if (HttpMethod.PUT.equals(requestMethod)) {
stringBuffer.append(httpRequest.getBodyString());
}
return stringBuffer;
}
}
输出信息示例
示例 1:JSON 格式的请求体
请求信息:
POST /hi?text=how%20are%20you? HTTP/1.1
content-length:23
host:localhost
content-type:application/json
connection:keep-alive
accept-encoding:gzip, deflate, br
accept:*/*
{
"key":"value"
}
示例 2:表单格式的请求体
请求信息:
POST /hi?text=how%20are%20you? HTTP/1.1
content-length:9
host:localhost
connection:keep-alive
content-type:application/x-www-form-urlencoded
accept-encoding:gzip, deflate, br
accept:*/*
text:how are you?
key:value
通过上述代码和示例,我们可以轻松地在控制台输出客户端请求的详细信息,便于调试和分析。
代理请求
使用 okHttp 将请求转发到远程系统
示例代码
import java.io.IOException;
import java.util.Map;
import com.litongjava.open.chat.constants.OpenAiConstatns;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.RequestLine;
import com.litongjava.tio.http.server.util.HttpServerRequestUtils;
import com.litongjava.tio.http.server.util.HttpServerResponseUtils;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.http.OkHttpClientPool;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
//@Slf4j
public class IndexRequestHandler {
public HttpResponse index(HttpRequest httpRequest) {
HttpResponse httpResponse = TioRequestContext.getResponse();
printRequest(httpRequest);
// 修改授权信息
String authorization = EnvUtils.get("OPENAI_API_KEY");
httpRequest.addHeader("authorization", "Bearer "+authorization);
// response.setBody(requestString.toString().getBytes());
Request okHttpReqeust = HttpServerRequestUtils.toOkHttp(OpenAiConstatns.server_url, httpRequest);
OkHttpClient httpClient = OkHttpClientPool.getHttpClient();
try (Response okHttpResponse = httpClient.newCall(okHttpReqeust).execute()) {
HttpServerResponseUtils.fromOkHttp(okHttpResponse, httpResponse);
printResponse(okHttpResponse);
httpResponse.setHasGzipped(true);
// httpResponse.removeHeaders("Set-Cookie");
httpResponse.removeHeaders("Transfer-Encoding");
httpResponse.removeHeaders("Server");
httpResponse.removeHeaders("Date");
httpResponse.setHeader("Connection", "close");
} catch (IOException e) {
e.printStackTrace();
}
return httpResponse;
}
private void printResponse(Response okHttpResponse) {
if (okHttpResponse.isSuccessful()) {
try {
System.out.println("response:\n" + okHttpResponse.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}
private StringBuffer printRequest(HttpRequest httpRequest) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("request:\n");
RequestLine requestLine = httpRequest.getRequestLine();
stringBuffer.append(requestLine.toString()).append("\n");
Map<String, String> headers = httpRequest.getHeaders();
for (Map.Entry<String, String> e : headers.entrySet()) {
stringBuffer.append(e.getKey() + ":" + e.getValue()).append("\n");
}
// 请求体
String contentType = httpRequest.getContentType();
if (contentType != null) {
if (contentType.startsWith("application/json")) {
stringBuffer.append(httpRequest.getBodyString());
} else if (contentType.startsWith("application/x-www-form-urlencoded")) {
Map<String, Object[]> params = httpRequest.getParams();
for (Map.Entry<String, Object[]> e : params.entrySet()) {
stringBuffer.append(e.getKey() + ": " + e.getValue()[0]).append("\n");
}
} else if (contentType.startsWith("multipart/form-data")) {
Map<String, Object[]> params = httpRequest.getParams();
for (Map.Entry<String, Object[]> e : params.entrySet()) {
Object value = e.getValue()[0];
// 添加参数
if (value instanceof String) {
stringBuffer.append(e.getKey()).append(":").append(e.getValue()[0]).append("\n");
} else {
stringBuffer.append(e.getKey()).append(":").append("binary \n");
}
}
}
}
System.out.println(stringBuffer.toString());
return stringBuffer;
}
}
转发请求并记录到数据库
创建一张数据表
CREATE TABLE sys_http_forward_statistics (
id BIGINT NOT NULL,
ip VARCHAR,
ip_region VARCHAR,
method VARCHAR(10),
uri VARCHAR(256),
elapsed BIGINT,
request_header json,
request_body text,
response_status int,
response_body text,
remark VARCHAR (256),
creator VARCHAR (64) DEFAULT '',
create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR (64) DEFAULT '',
update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted SMALLINT NOT NULL DEFAULT 0,
tenant_id BIGINT NOT NULL DEFAULT 0
);
这张表 sys_http_forward_statistics
用于记录 HTTP 请求的相关数据,包括请求 IP 地址、请求头、请求体、响应状态码、响应体、耗时等。create_time
和 update_time
字段记录了数据的创建和更新时间,deleted
用于软删除,tenant_id
用于多租户支持。
添加一个 Handler,拦截所有请求
package com.litongjava.maxkb.config;
import com.litongjava.jfinal.aop.annotation.AConfiguration;
import com.litongjava.jfinal.aop.annotation.AInitialization;
import com.litongjava.maxkb.httphandler.AppRequestForwardHandler;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.RequestRoute;
@AConfiguration
public class HttpRequestHanlderConfig {
@Initialization
public void config() {
// 获取路由
RequestRoute r = TioBootServer.me().getRequestRoute();
// 创建处理器
AppRequestForwardHandler indexRequestHandler = new AppRequestForwardHandler();
// 添加处理动作,拦截所有请求
r.add("/*", indexRequestHandler::index);
}
}
HttpRequestHanlderConfig
类通过 TioBootServer
获取路由,并为所有请求路径(/*
)添加 AppRequestForwardHandler
进行拦截。这个配置类将拦截所有进入的 HTTP 请求,并交给自定义处理器处理。
在 AppRequestForwardHandler
中调用 TioHttpProxy
package com.litongjava.maxkb.httphandler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.maxkb.service.AppForwardRequestService;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.boot.http.forward.TioHttpProxy;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
public class AppRequestForwardHandler {
private String remoteServerUrl = "http://192.168.1.2:7006";
public HttpResponse index(HttpRequest httpRequest) {
HttpResponse httpResponse = TioRequestContext.getResponse();
AppForwardRequestService forwardRequestService = Aop.get(AppForwardRequestService.class);
// 通过 TioHttpProxy 进行请求转发
TioHttpProxy.reverseProxy(remoteServerUrl, httpRequest, httpResponse, true, forwardRequestService);
return httpResponse;
}
}
在 AppRequestForwardHandler
中,TioHttpProxy
被用于实现请求转发功能。reverseProxy
方法接收远程服务器地址、当前的 HttpRequest
和 HttpResponse
对象,并调用服务类 AppForwardRequestService
记录请求和响应数据。
ForwardRequestService
负责解析数据并入库
import java.util.Map;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Record;
import com.litongjava.tio.boot.http.forward.RequestProxyCallback;
import com.litongjava.tio.http.common.HeaderName;
import com.litongjava.tio.http.common.HeaderValue;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.RequestLine;
import com.litongjava.tio.utils.hutool.ZipUtil;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ProxyRequestService implements RequestProxyCallback {
public void saveRequest(long id, String ip, HttpRequest httpRequest) {
StringBuffer stringBuffer = new StringBuffer();
RequestLine requestLine = httpRequest.getRequestLine();
stringBuffer.append(requestLine.toString()).append("\n");
Map<String, String> headers = httpRequest.getHeaders();
// 处理请求体
String contentType = httpRequest.getContentType();
if (contentType != null) {
if (contentType.startsWith("application/json")) {
stringBuffer.append(httpRequest.getBodyString());
} else if (contentType.startsWith("application/x-www-form-urlencoded")) {
Map<String, Object[]> params = httpRequest.getParams();
if (params != null) {
for (Map.Entry<String, Object[]> e : params.entrySet()) {
stringBuffer.append(e.getKey() + ": " + e.getValue()[0]).append("\n");
}
}
} else if (contentType.startsWith("application/from-data")) {
Map<String, Object[]> params = httpRequest.getParams();
for (Map.Entry<String, Object[]> e : params.entrySet()) {
Object value = e.getValue()[0];
if (value instanceof String) {
stringBuffer.append(e.getKey()).append(":").append(e.getValue()[0]).append("\n");
} else {
stringBuffer.append(e.getKey()).append(":").append("binary \n");
}
}
}
}
String method = requestLine.getMethod().toString();
String path = requestLine.getPath();
Record record = Record.by("id", id).set("ip", ip).set("ip_region", Ip2RegionUtils.searchIp(ip))
.set("method", method).set("uri", path).set("request_header", headers).set("request_body", stringBuffer.toString());
String[] jsonFields = { "request_header" };
boolean saveResult = Db.save("sys_http_forward_statistics", record, jsonFields);
if (!saveResult) {
log.error("保存数据库失败:{}", "sys_http_forward_statistics");
}
}
@Override
public void saveResponse(long id, long elapsed, int statusCode, Map<HeaderName, HeaderValue> headers,
HeaderValue contentEncoding, byte[] body) {
Record record = Record.by("id", id).set("elapsed", elapsed).set("response_status", statusCode);
if (body != null && body.length > 0) {
if (contentEncoding != null && HeaderValue.Content_Encoding.gzip.equals(contentEncoding)) {
String value = new String(ZipUtil.unGzip(body));
log.info("响应内容:{},{}", id, value);
record.set("response_body", value);
} else {
record.set("response_body", new String(body));
}
}
String tableName = "sys_http_forward_statistics";
try {
boolean update = Db.update(tableName, record);
if (!update) {
log.error("更新表失败:{},{}", tableName, id);
}
} catch (Exception e) {
log.error("更新表出错:{},{},{}", tableName, id, e.getMessage());
}
}
}
ProxyRequestService
实现了 RequestProxyCallback
接口,并处理转发请求的日志记录。saveRequest
方法负责保存请求数据,包括请求头和请求体。saveResponse
方法则负责保存响应数据,包括响应状态码和响应体。
总结
通过上述步骤,我们实现了一个基于 Tio-Boot 的 HTTP 请求转发与日志记录系统。该系统能够将所有经过的 HTTP 请求进行拦截,转发到指定的服务器,并将请求与响应的详细信息记录到数据库中,方便后续分析与调试。