一个简单的Http根据规则自动路由

在日常项目中,有时候会根据一些规则/标识进行Http路由匹配,下面我实现一个简单的Http根据systemCode自动路由;

Controller

java 复制代码
import com.example.demo.service.GatewayRoutingService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/gateway-routing")
public class GatewayRoutingController {
    private Logger log = LoggerFactory.getLogger(GatewayRoutingController.class);

    @Autowired
    private GatewayRoutingService service;

    /**
     * 根据systemCode,自行匹配路由到systemCode对应的域名上
     *
     * @param request
     * @param response
     */
    @RequestMapping("/invoke/**")
    public void invoke(HttpServletRequest request, HttpServletResponse response) {
        String systemCode = request.getHeader("system-code");
        if (StringUtils.isEmpty(systemCode)) {
            // return 404
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        try {

            // 路由调用
            service.invoke(systemCode, request, response);
        } catch (Exception e) {
            log.info("genericInvoke>>>exception", e);
            throw new RuntimeException("系统错误,请联系管理");
        }
    }
}

Service

java 复制代码
import com.example.demo.config.RequestInvokeProperties;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

@Service
public class GatewayRoutingService {
    private Logger log = LoggerFactory.getLogger(GatewayRoutingService.class);

    /**
     * 系统域名映射 key :系统唯一标识, value 系统
     */
    @Autowired
    private RequestInvokeProperties properties;

    /**
     * 路由调用
     */
    public void invoke(String systemCode, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 解析获取真实请求路径
        final URI realUrl = this.resolveRealUrl(systemCode, request);

        final HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
        if (httpMethod == null || realUrl == null) {
            throw new RuntimeException("当前请求不合法, 无法转发");
        }
        log.info("routing and forwarding>>>originalUrl = {}, realUrl = {}", request.getRequestURI(), realUrl);
        ClientHttpRequest delegateRequest = new SimpleClientHttpRequestFactory().createRequest(realUrl, httpMethod);
        // set header
        setRequestHeader(request, delegateRequest);
        // set body
        setRequestBody(request, delegateRequest);

        // http调用
        try (ClientHttpResponse clientHttpResponse = delegateRequest.execute();
             ServletOutputStream responseOutputStream = response.getOutputStream()) {
            response.setStatus(clientHttpResponse.getStatusCode().value());
            clientHttpResponse.getHeaders().forEach((key, values) -> {
                // 处理响应头重复情况,在请求返回时该响应头会出现重复,postman 调用成功,但是实际前端调用会出错
                if (!"Transfer-Encoding".equalsIgnoreCase(key)) {
                    values.forEach(value -> response.setHeader(key, value));
                }
            });
            // 拷贝响应流
            ioCopy(clientHttpResponse.getBody(), responseOutputStream, 2024);
        }
    }

    /**
     * 拷贝响应流
     *
     * @param in
     * @param out
     */
    private long ioCopy(InputStream in, OutputStream out, int bufferSize) throws IOException {
        Assert.notNull(in, "InputStream is null !");
        Assert.notNull(out, "OutputStream is null !");

        long size = 0L;
        try {
            if (bufferSize <= 0) {
                bufferSize = 2048;
            }
            byte[] buffer = new byte[bufferSize];
            int readSize;
            while ((readSize = in.read(buffer)) != -1) {
                out.write(buffer, 0, readSize);
                size += (long) readSize;
                out.flush();
            }
        } finally {
            in.close();
            out.close();
        }
        return size;
    }

    /**
     * 解析获取真实请求路径
     *
     * @param system
     * @param request
     * @return
     */
    private URI resolveRealUrl(String system, HttpServletRequest request) throws URISyntaxException, UnsupportedEncodingException {
        if (properties.getSystemMap() == null || properties.getSystemMap().isEmpty()) {
            return null;
        }
        // 获取真实的请求url:去除前缀 /invoke/
        StringBuilder requestUrl = new StringBuilder(StringUtils.substringAfter(request.getRequestURI(), "/invoke/"));
        // 根据systemCode获取对应的域名
        String domain = properties.getSystemMap().get(system);
        if (StringUtils.isNoneBlank(requestUrl.toString(), domain)) {
            final Enumeration<String> parameterNames = request.getParameterNames();
            StringBuilder uriVariables = new StringBuilder("?");
            while (parameterNames.hasMoreElements()) {
                // 转义部分特殊字符,如果请求参数里带有 +、空格等不转义请求路由过去会出问题
                final String parameterName = URLEncoder.encode(parameterNames.nextElement(), "UTF-8");
                final String parameter = URLEncoder.encode(request.getParameter(parameterName), "UTF-8");
                uriVariables.append(parameterName).append("=").append(parameter).append("&");
            }
            domain = domain.endsWith("/") ? domain : domain + "/";
            return new URI(domain + requestUrl + (uriVariables.length() == 1 ? "" : uriVariables.substring(0, uriVariables.length() - 1)));
        }
        return null;
    }

    /**
     * 设置请求体
     *
     * @throws IOException
     */
    private void setRequestBody(HttpServletRequest request, ClientHttpRequest delegateRequest) throws IOException {
        StreamUtils.copy(request.getInputStream(), delegateRequest.getBody());
    }

    /**
     * 设置请求头
     */
    private void setRequestHeader(HttpServletRequest request, ClientHttpRequest delegateRequest) {
        Enumeration<String> headerNames = request.getHeaderNames();
        // 设置请求头
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            Enumeration<String> header = request.getHeaders(headerName);
            List<String> arr = new ArrayList<>();
            while (header.hasMoreElements()) {
                arr.add(header.nextElement());
            }
            delegateRequest.getHeaders().addAll(headerName, arr);
        }
        if (!delegateRequest.getHeaders().containsKey("Content-Type")) {
            delegateRequest.getHeaders().add("Content-Type", "application/json; charset=utf-8");
        }
    }
}

Properties

Properties类

java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "request.invoke")
public class RequestInvokeProperties {

    private Map<String, String> systemMap;
}

Yaml

yaml 复制代码
request:
  invoke:
    systemMap:
      baidu: http://www.baidu.com/

测试

路由到baidu

请求:

shell 复制代码
curl --location --request GET 'http://localhost:8080/gateway-routing/invoke/s?wd=spring' \
--header 'system-code: baidu' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: localhost:8080' \
--header 'Connection: keep-alive' \
--data-raw ''

实际请求:

http://www.baidu.com/s?wd=spring

相关推荐
黑客学长-刘备4 分钟前
终于有人把网络安全就业方向一口气讲清了(非常详细)零基础入门到精通,收藏这一篇就够了
java·运维·服务器·网络·python·安全·web安全
夏天vs不热5 分钟前
Kubernetes中的网络模型:Service、Ingress、Pod通信详解
网络·容器·kubernetes
clear code24 分钟前
【modbus协议】Modbus-TCP消息帧格式
服务器·网络协议·tcp/ip
笨笨聊运维1 小时前
linux离线安装Ollama并完成大模型配置(无网络)
linux·网络·人工智能·php
吸油泼面1 小时前
一年期免费HTTPS证书:网络安全新选择
服务器·网络·网络协议·https·ssl
变形金刚卖人寿保险还是汽车保险2 小时前
vue路由配置
网络
苏湘涵2 小时前
HTTP的初步了解
网络·网络协议·http
夏子曦2 小时前
WebSocket与Socket
网络·websocket·网络协议