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个实现。

本章节部分代码参考:smiley-http-proxy-servlet做代理application/x-www-form-urlencoded请求类型,报错failed to respond_upstream failed to respond-CSDN博客

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;
    }
}

更多推荐