在不引入 zull 网关或历史遗留问题,前端所有请求必须经过某个项目转发时,使用spring controller 实现一个轻量的代理网关。


实现原理

HTTP 请求基本由几个部分组成:

  1. 请求URL
  2. 请求头
  3. 请求体

所以只要将这几个部分组装起来,就可以实现。在我的场景中。


实现思路

用固定前缀接收所有请求,在请求实际代理方法时,去掉前缀,使用 path 路径区分代理目标应用,controller 代码如下:

import org.snailgary.RestResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URISyntaxException;

/**
 * <p>
 * </p>
 *
 * @author 蜗牛格里
 * @since 2022/7/22
 */
@RestController
@RequestMapping(BuckyDropDelegateController.DELEGATE_PREFIX)
public class BuckyDropDelegateController {

    @Autowired
    private LoadBalancedRoutingDelegate loadBalancedRoutingDelegate;

    public static final String DELEGATE_PREFIX = "/gatewat/delegation";

    // controller 接收所有请求方法
    @RequestMapping(value = "/{appName}/**", method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE})
    public RestResponse<?> delegateRequest(@PathVariable("appName") String appName, HttpServletRequest request, HttpServletResponse response) throws URISyntaxException, IOException {
           // 去掉前缀,得到真实的URL
        String delegateUrl = request.getRequestURI().replace(DELEGATE_PREFIX, "");
        return loadBalancedRoutingDelegate.doDelegateRequest(appName, delegateUrl, request);
    }

}

注意不要在controller中消耗request 流。

代理逻辑实现类

import org.snailgary.RestResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestTemplate;
import reactor.core.support.Assert;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * </p>
 *
 * @author chandler
 * @since 2022/7/22
 */
@Slf4j
@Service
public class LoadBalancedRoutingDelegate {

    // 微服务场景下,使用 loadBalanced restTemplate
    @Autowired
    @Qualifier("loadBalancedRestTemplate")
    private RestTemplate loadBalancedRestTemplate;

    public static final String HEADER_CUSTOMER_CODE = "x-customer-code";
    public static final String HEADER_CUSTOMER_ACCOUNT = "x-customer-account";

    /**
    * 配置代理目标服务链接
    */
    private Map<String, String> appRoutingConfig = new HashMap<>();

    @PostConstruct
    public void before() {
        
        appRoutingConfig.put("orders", "http://SNAIL-GARY-ORDER");
        // appRoutingConfig.put("orders", "http://localhost:8080");
        appRoutingConfig.put("tickets", "http://SNAIL-GARY-GOODS");
    }

    public RestResponse<?> doDelegateRequest(String appName, String delegateUrl, HttpServletRequest request) throws URISyntaxException, IOException {
        String appUrl = appRoutingConfig.get(appName);
        Assert.isTrue(appUrl != null, "404 Not found!");
        String queryString = request.getQueryString();
        String url = appUrl + delegateUrl + (queryString != null ? "?" + queryString : "");
        RequestEntity requestEntity = createRequestEntity(request, url);

        ResponseEntity<RestResponse> responseEntity = loadBalancedRestTemplate.exchange(requestEntity, RestResponse.class);
        return responseEntity.getBody();
    }

    // 组装请求体
    private RequestEntity createRequestEntity(HttpServletRequest request, String url) throws URISyntaxException, IOException {
        String method = request.getMethod();
        HttpMethod httpMethod = HttpMethod.resolve(method);
        MultiValueMap<String, String> headers = parseRequestHeader(request);
        byte[] body = parseRequestBody(request);
 
        return new RequestEntity<>(body, headers, httpMethod, new URI(url));
    }

   // 获取请求体二进制流
    private byte[] parseRequestBody(HttpServletRequest request) throws IOException {
        InputStream inputStream = request.getInputStream();
        return StreamUtils.copyToByteArray(inputStream);
    }
// 组装请求头
    private MultiValueMap<String, String> parseRequestHeader(HttpServletRequest request) {
        HttpHeaders headers = new HttpHeaders();
        List<String> headerNames = Collections.list(request.getHeaderNames());
        for (String headerName : headerNames) {
            List<String> headerValues = Collections.list(request.getHeaders(headerName));
            for (String headerValue : headerValues) {
                headers.add(headerName, headerValue);
            }
        }
        CustomerAccountInfoRspDTO infoRspDTO = CustomerUtils.getLoginCustomerInfo();
        if (infoRspDTO != null) {
            headers.add(HEADER_CUSTOMER_CODE, infoRspDTO.getCustomerCode());
            headers.add(HEADER_CUSTOMER_ACCOUNT, infoRspDTO.getCustomerAccount());
        }
        return headers;
    }

}

主要步骤非常简单,获取代理目标URL,组装请求参数,执行HTTP请求,不是微服务场景,只需将链接换成IP+端口或者域名地址就可以,亲测 Get with query string、POST JSON 参数都可以,并且代理目标服务可以获取到认证头、cookie等信息。

参考:https://www.cnblogs.com/xiaoqi/p/spring-boot-route.html


2022-07-28 补充

测了其他一些场景,发现不支持post form ,所以构建请求entity做一下改造:

private RequestEntity createRequestEntity(HttpServletRequest request, String url) throws URISyntaxException, IOException {
        String method = request.getMethod();
        HttpMethod httpMethod = HttpMethod.resolve(method);
        MultiValueMap<String, String> headers = parseRequestHeader(request);
        String contentType = request.getContentType();
        if (contentType.contains("multipart/form-data") || contentType.contains("application/x-www-form-urlencoded")) {
            MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
            Enumeration<String> names = request.getParameterNames();
            while (names.hasMoreElements()){
                String name = names.nextElement();
                params.add(name, request.getParameter(name));
            }
            // String bodyStr = new String(body);
            return new RequestEntity<>(params, headers, httpMethod, new URI(url));
        }else {
            byte[] body = parseRequestBody(request);
            // String bodyStr = new String(body);
            return new RequestEntity<>(body, headers, httpMethod, new URI(url));
        }

    }