java反向代理解决方案(不依赖springcloud网关)
1、需求实现描述
在不使用网关的前提下,实现前端请求 springboot 项目,然后项目在把请求转发到外部http系统中去。让单体springboot项目实现类似springcloud-gateway的目的,目前主流的有两种方案,第一种是smiley-http-proxy-servlet 第二种是jetty,如果使用jetty的话,需要将springboot默认的tomcat替换为jetty,如果你不想换servlet容器可以考虑smiley-http-proxy-servlet,支持springboot2和springboot3.
2、smiley-http-proxy-servlet
此项目是一个老外的开源项目,github地址为:https://github.com/mitre/HTTP-Proxy-Servlet
这个开源项目的优势是国内资料相对多一些,不需要换servlet容器,缺点是个人项目。如果你不想换servlet容器可以用这个。以下提供了普通http转发和servlet转发2个实现。
2.1、依赖
因为我用的是springboot3.x,所以我依赖2.0的maven依赖,坐标如下:
<dependency>
<groupId>org.mitre.dsmiley.httpproxy</groupId>
<artifactId>smiley-http-proxy-servlet</artifactId>
<version>2.0</version>
</dependency>
如果你是springboot2.x,请添加以下依赖:
<dependency>
<groupId>org.mitre.dsmiley.httpproxy</groupId>
<artifactId>smiley-http-proxy-servlet</artifactId>
<version>1.12.1</version>
</dependency>
如果碰到http-client冲突请去作者仓库看解决方案。
2.2、springboot配置类
用于从yml文件读取配置,供config类使用。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
/**
* dataease 反向代理配置文件
* @date 2025/06/24 17:29
**/
@Configuration
@ConfigurationProperties(prefix = "dataease.proxy")
public class ProxyMappingConfig {
private Map<String, String> httpMapping;
private Map<String, String> websocketMapping;
public Map<String, String> getHttpMapping() {
return httpMapping;
}
public void setHttpMapping(Map<String, String> httpMapping) {
this.httpMapping = httpMapping;
}
public Map<String, String> getWebsocketMapping() {
return websocketMapping;
}
public void setWebsocketMapping(Map<String, String> websocketMapping) {
this.websocketMapping = websocketMapping;
}
}
2.3、http转发自定义实现
以下类继承了ProxyServlet,解决了urlencode传参的时候无法转发的问题。
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.springframework.http.MediaType;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
/**
* dataease http反向代理
* @date 2025/06/24 17:29
**/
@Slf4j
public class HttpProxyServlet extends ProxyServlet {
@Override
public void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException {
if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) {
servletRequest.setAttribute(ATTR_TARGET_URI, this.targetUri);
}
if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) {
servletRequest.setAttribute(ATTR_TARGET_HOST, this.targetHost);
}
String method = servletRequest.getMethod();
String proxyRequestUri = this.rewriteUrlFromRequest(servletRequest);
Object proxyRequest;
if (servletRequest.getHeader("Content-Length") == null && servletRequest.getHeader("Transfer-Encoding") == null) {
proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
} else {
proxyRequest = this.newProxyRequestWithEntity(method, proxyRequestUri, servletRequest);
}
this.copyRequestHeaders(servletRequest, (HttpRequest) proxyRequest);
this.setXForwardedForHeader(servletRequest, (HttpRequest) proxyRequest);
HttpResponse proxyResponse = null;
try {
if (this.doLog) {
log.info("proxyRequestUri:"+ proxyRequestUri);
log.info("Content-Type:"+ servletRequest.getContentType());
// log.info("params:"+getRequestBodyByInputStream(((HttpEntityEnclosingRequest)proxyRequest).getEntity().getContent()));
}
proxyResponse = this.doExecute(servletRequest, servletResponse, (HttpRequest) proxyRequest);
int statusCode = proxyResponse.getStatusLine().getStatusCode();
servletResponse.setStatus(statusCode);
this.copyResponseHeaders(proxyResponse, servletRequest, servletResponse);
if (statusCode == 304) {
servletResponse.setIntHeader("Content-Length", 0);
} else {
this.copyResponseEntity(proxyResponse, servletResponse, (HttpRequest) proxyRequest, servletRequest);
}
if (this.doLog) {
log.info("respStatusCode:"+ statusCode);
}
} catch (Exception var11) {
this.handleRequestException((HttpRequest) proxyRequest, proxyResponse, var11);
} finally {
if (proxyResponse != null) {
EntityUtils.consumeQuietly(proxyResponse.getEntity());
}
}
}
/**
* 获取http中的请求报文
*
* @return
*/
private String getRequestBodyByInputStream(InputStream inputStream) {
try {
// 获取ServletInputStream
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder requestBody = new StringBuilder();
char[] buffer = new char[1024];
int read;
while ((read = reader.read(buffer)) != -1) {
requestBody.append(buffer, 0, read);
}
// 现在requestBody包含了请求体的字符串内容
return requestBody.toString();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
@Override
protected HttpRequest newProxyRequestWithEntity(String method, String proxyRequestUri, HttpServletRequest servletRequest) throws IOException {
HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, proxyRequestUri);
if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(servletRequest.getContentType())
|| servletRequest.getContentType().contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
// 添加参数
Map<String, String[]> mapdata = servletRequest.getParameterMap();
List<NameValuePair> nameValuePairs = new ArrayList<>();
if (mapdata.size() != 0) {
// 将mapdata中的key存在set集合中,通过迭代器取出所有的key,再获取每一个键对应的值
Set keySet = mapdata.keySet();
Iterator it = keySet.iterator();
while (it.hasNext()) {
String k = it.next().toString();
String[] v = mapdata.get(k);
nameValuePairs.add(new BasicNameValuePair(k, v[0]));
}
}
eProxyRequest.setEntity(new UrlEncodedFormEntity(nameValuePairs, servletRequest.getCharacterEncoding()));
} else {
eProxyRequest.setEntity(new InputStreamEntity(servletRequest.getInputStream(), this.getContentLength(servletRequest)));
}
return eProxyRequest;
}
private long getContentLength(HttpServletRequest request) {
String contentLengthHeader = request.getHeader("Content-Length");
return contentLengthHeader != null ? Long.parseLong(contentLengthHeader) : -1L;
}
private void setXForwardedForHeader(HttpServletRequest servletRequest, HttpRequest proxyRequest) {
if (this.doForwardIP) {
String forHeaderName = "X-Forwarded-For";
String forHeader = servletRequest.getRemoteAddr();
String existingForHeader = servletRequest.getHeader(forHeaderName);
if (existingForHeader != null) {
forHeader = existingForHeader + ", " + forHeader;
}
proxyRequest.setHeader(forHeaderName, forHeader);
String protoHeaderName = "X-Forwarded-Proto";
String protoHeader = servletRequest.getScheme();
proxyRequest.setHeader(protoHeaderName, protoHeader);
}
}
}
2.4、websocket转发自定义实现
websocket部分建议用 9.0.54以上的tomcat版本,低版本tomcat可能存在bug。
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpUpgradeHandler;
import jakarta.servlet.http.WebConnection;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpRequest;
import org.mitre.dsmiley.httpproxy.ProxyServlet;
/**
* dataease websocket反向代理
* @date 2025/06/24 17:29
**/
@Slf4j
public class WebsocketProxyServlet extends ProxyServlet {
private static final long serialVersionUID = -2566573965489129976L;
protected ExecutorService exec;
@Override
public void init() throws ServletException {
super.init();
exec = Executors.newCachedThreadPool();
}
@Override
public void destroy() {
super.destroy();
exec.shutdown();
}
@Override
protected void copyRequestHeaders(HttpServletRequest servletRequest, HttpRequest proxyRequest) {
super.copyRequestHeaders(servletRequest, proxyRequest);
String userId = (String)servletRequest.getAttribute("UserID");
if (userId != null) {
proxyRequest.addHeader("UserID", userId);
}
}
@Override
protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws ServletException, IOException {
var wsKey = servletRequest.getHeader("Sec-WebSocket-Key");
if (wsKey != null) {
//initialize request attributes from caches if unset by a subclass by this point
if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) {
servletRequest.setAttribute(ATTR_TARGET_URI, targetUri);
}
if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) {
servletRequest.setAttribute(ATTR_TARGET_HOST, targetHost);
}
String proxyRequestUri = rewriteUrlFromRequest(servletRequest);
URL u = new URL(proxyRequestUri);
Socket sock = new Socket(u.getHost(), u.getPort());
boolean closeSocket = false;
try {
var sockIn = sock.getInputStream();
var sockOut = sock.getOutputStream();
StringBuilder req = new StringBuilder(512);
req.append("GET " + u.getFile()).append(" HTTP/1.1");
log.debug(" > WS|" + req);
req.append("\r\n");
var en = servletRequest.getHeaderNames();
while (en.hasMoreElements()) {
var n = en.nextElement();
String header = servletRequest.getHeader(n);
log.debug(" > WS| " + n + ": " + header);
req.append(n + ": " + header + "\r\n");
}
req.append("\r\n");
sockOut.write(req.toString().getBytes(StandardCharsets.UTF_8));
sockOut.flush();
StringBuilder responseBytes = new StringBuilder(512);
int b = 0;
while (b != -1) {
b = sockIn.read();
if (b != -1) {
responseBytes.append((char)b);
var len = responseBytes.length();
if (len >= 4
&& responseBytes.charAt(len - 4) == '\r'
&& responseBytes.charAt(len - 3) == '\n'
&& responseBytes.charAt(len - 2) == '\r'
&& responseBytes.charAt(len - 1) == '\n'
) {
break;
}
}
}
String[] rows = responseBytes.toString().split("\r\n");
String response = rows[0];
log.debug(" < WS|" + response);
int idx1 = response.indexOf(' ');
int idx2 = response.indexOf(' ', idx1 + 1);
for (int i = 1; i < rows.length; i++) {
String line = rows[i];
int idx3 = line.indexOf(":");
var k = line.substring(0, idx3);
var headerField = line.substring(idx3 + 2);
log.debug(" < WS| " + k + ": " + headerField);
servletResponse.setHeader(k, headerField);
}
int respCode = Integer.parseInt(response.substring(idx1 + 1, idx2));
if (respCode != 101) {
servletResponse.setStatus(respCode);
servletResponse.flushBuffer();
log.debug(" < WS| Flush");
closeSocket = true;
} else {
var uh = servletRequest.upgrade(WsUpgradeHandler.class);
uh.preInit(exec, sockIn, sockOut, sock);
}
} finally {
if (closeSocket) {
sock.close();
}
}
} else {
super.service(servletRequest, servletResponse);
}
}
public static class WsUpgradeHandler implements HttpUpgradeHandler {
ExecutorService exec;
InputStream sockIn;
OutputStream sockOut;
Socket sock;
Future<?> f2;
public WsUpgradeHandler() { }
public void preInit(ExecutorService exec, InputStream sockIn, OutputStream sockOut, Socket sock) {
this.exec = exec;
this.sockIn = sockIn;
this.sockOut = sockOut;
this.sock = sock;
}
@Override
public void init(WebConnection wc) {
log.debug(" * WS| Upgrade begin");
try {
var servletIn = wc.getInputStream();
var servletOut = wc.getOutputStream();
f2 = exec.submit(() -> {
log.debug(" > WS| Backend -> Client");
var c = 0;
try {
var bs = 0;
while ((bs = sockIn.read()) != -1) {
servletOut.write(bs);
servletOut.flush();
c++;
}
} catch (SocketException | EOFException exc) {
// this is fine
} catch (Exception exc) {
exc.printStackTrace();
} finally {
servletOut.close();
}
log.debug(" < WS| Done: " + c);
return null;
});
log.debug(" > WS| Client -> Backend");
var c = 0;
var bs = 0;
try {
while ((bs = servletIn.read()) != -1) {
sockOut.write(bs);
c++;
}
} catch (SocketException | EOFException exc) {
// this is fine
} catch (Exception exc) {
exc.printStackTrace();
} finally {
sockOut.close();
}
log.debug(" > WS| Done: " + c);
f2.get();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (f2 != null) {
f2.cancel(true);
}
}
}
@Override
public void destroy() {
log.debug(" * WS| Upgrade closing");
if (f2 != null) {
f2.cancel(true);
}
try {
sock.close();
} catch (IOException ex) {
}
log.debug(" * WS| Upgrade close");
}
}
}
2.5、添加springboot配置类
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import lombok.extern.slf4j.Slf4j;
import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Configuration;
import vip.xiaonuo.dataease.core.proxy.HttpProxyServlet;
import vip.xiaonuo.dataease.core.proxy.WebsocketProxyServlet;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* dataease 反向代理配置
* @date 2025/06/24 17:29
**/
@Slf4j
@Configuration
public class DataeaseProxyConfig implements ServletContextInitializer {
//这里加个自动注入,不然空指针。。。看谁复制代码都不看注释
private ProxyMappingConfig proxyMappingConfig;
//项目启动时,会读取yml配置里所有的路径映射,每个映射注册一个ProxyServlet
@Override
public void onStartup(ServletContext servletContext) {
Map<String, String> httpMapping = proxyMappingConfig.getHttpMapping();
if (null != httpMapping && httpMapping.size() > 0) {
//计数
AtomicInteger count = new AtomicInteger(0);
httpMapping.forEach((serverUrl, targetUrl) -> {
log.info("【请求代理注册,serverUrl={},targetUrl={}】", serverUrl, targetUrl);
ProxyServlet servlet = new HttpProxyServlet();
//每个servlet名字不能重复
String servletName = "servlet" + count.get();
ServletRegistration.Dynamic dynamic = servletContext.addServlet(servletName, servlet);
Map<String, String> paramMap = new HashMap<>();
paramMap.put(ProxyServlet.P_TARGET_URI, targetUrl);
paramMap.put(ProxyServlet.P_LOG, "true");
dynamic.setInitParameters(paramMap);
dynamic.addMapping(serverUrl);
count.incrementAndGet();
});
}
Map<String, String> websocketMapping = proxyMappingConfig.getWebsocketMapping();
if (null != websocketMapping && websocketMapping.size() > 0) {
//计数
AtomicInteger count = new AtomicInteger(0);
websocketMapping.forEach((serverUrl, targetUrl) -> {
log.info("【请求代理注册,serverUrl={},targetUrl={}】", serverUrl, targetUrl);
ProxyServlet servlet = new WebsocketProxyServlet();
//每个servlet名字不能重复
String servletName = "websocketServlet" + count.get();
ServletRegistration.Dynamic dynamic = servletContext.addServlet(servletName, servlet);
Map<String, String> paramMap = new HashMap<>();
paramMap.put(ProxyServlet.P_TARGET_URI, targetUrl);
paramMap.put(ProxyServlet.P_LOG, "true");
dynamic.setInitParameters(paramMap);
dynamic.addMapping(serverUrl);
count.incrementAndGet();
});
}
}
}
3、jetty实现
jetty的好处是大厂出品,但是关于代理这块依旧资料很少,以下代码在springboot3.x 搭配jetty12.0.5版本测试成功,其他版本请自行测试。
3.1、依赖配置
<!-- 排除Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入Jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<!-- 引入Jetty代理,记得改版本和jetty一致 -->
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-proxy</artifactId>
<version>12.0.5</version>
</dependency>
3.2、springboot配置
以下代码http转发已经经过测试,websocket理论上可以,但是实际上没经过测试,请自行测试。
import jakarta.servlet.Servlet; import org.eclipse.jetty.ee10.proxy.ProxyServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /* * 用于创建代理 * @author 王磊 * @date 2025/05/15 17:29 **/ @Configuration public class DataeaseJettyProxyConfig { private static final String TARGET_URL = "http://192.168.0.3:8101/"; private static final String PROXY_PREFIX = "/dataease"; @Bean public ServletRegistrationBean<Servlet> proxyServletRegistration() { // 创建Jetty 12的透明代理Servlet ProxyServlet.Transparent proxyServlet = new ProxyServlet.Transparent(); // 配置初始化参数 Map<String, String> initParams = new HashMap<>(); initParams.put("proxyTo", TARGET_URL); // 目标服务器地址 initParams.put("prefix", PROXY_PREFIX); // 路径前缀处理 initParams.put("viaHost", "jetty-proxy-12"); // 代理标识头 initParams.put("maxThreads", "200"); // 最大线程数 initParams.put("timeout", "30000"); // 超时时间(ms) initParams.put("idleTimeout", "30000"); // 空闲连接超时 // WebSocket特殊配置 initParams.put("wsUpgrade", "true"); // 启用WebSocket升级 initParams.put("maxWebSocketMessageSize", "1048576"); // 1MB消息限制 // 注册Servlet并映射路径 ServletRegistrationBean<Servlet> registration = new ServletRegistrationBean<>(); registration.setServlet(proxyServlet); registration.addUrlMappings(PROXY_PREFIX + "/*"); registration.setInitParameters(initParams); registration.setAsyncSupported(true); // 启用异步支持 return registration; } }
更多推荐


所有评论(0)