【SpringBoot】2022-03-26【自定義請求轉(zhuǎn)發(fā)、分發(fā)】

需求背景:

當(dāng)后端需要部署多區(qū)域或者多實例,而前端界面是一個,往往通過前端的區(qū)域篩選器來切換訪問對應(yīng)區(qū)域的后端實例時。可以通過前端直接訪問不同區(qū)域的Ip,但這樣新增區(qū)域或者后端變化時不夠靈活;另外,也可以通過訪問形如注冊中心的轉(zhuǎn)發(fā)服務(wù),轉(zhuǎn)發(fā)服務(wù)根據(jù)請求中的區(qū)域字段獲取對應(yīng)后端的地址進而轉(zhuǎn)發(fā)到對應(yīng)區(qū)域的后端,拿到接口返回數(shù)據(jù)后返回給前端。這樣前端只需要配置一個訪問地址,即轉(zhuǎn)發(fā)服務(wù)的地址。

各區(qū)域的后端實例可以在啟動時,將本機服務(wù)信息注冊到zk中,這樣轉(zhuǎn)發(fā)服務(wù)就可以從zk中獲取提供服務(wù)的后端地址。

主要流程:

  1. 前端的所有請求,都請求到轉(zhuǎn)發(fā)服務(wù)

  2. 轉(zhuǎn)發(fā)服務(wù)根據(jù)請求頭中的區(qū)域進行轉(zhuǎn)發(fā)

  3. 各區(qū)域提供服務(wù)的ip和端口通過zk中獲取,zk扮演注冊中心的角色

  4. 各區(qū)域后端啟動的時候往zk中注冊服務(wù)

主要代碼:

對request對象進行包裝:CustomHttpServletRequestWrapper.class

package com.tiger.web.common;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 包裝request避免request中的輸入流被多次讀時報錯
 *
 * @author tiger 2022/3/18
 */
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;
    public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream reader = request.getInputStream();
        try (ByteArrayOutputStream writer = new ByteArrayOutputStream()) {
            int read;
            byte[] buf = new byte[Integer.parseInt(request.getHeader("content-length"))];
            while ((read = reader.read(buf)) != -1) {
                writer.write(buf, 0, read);
            }
            this.body = writer.toByteArray();
        }
    }

    @Override
    public ServletInputStream getInputStream() {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() {
                return byteArrayInputStream.read();
            }
            @Override
            public void setReadListener(ReadListener listener) {
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public boolean isFinished() {
                return false;
            }
        };
    }
}

過濾器,攔截所有前端請求并進行轉(zhuǎn)發(fā),DispatcherFilter.class

package com.tiger.web.common;

import com.alibaba.fastjson.JSON;
import com.tiger.common.constant.HttpStatus;
import com.tiger.common.domain.OResult;
import com.tiger.web.service.DispatcherService;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.I0Itec.zkclient.ZkClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
 * @author tiger 2022/3/26
 */
@Component
@WebFilter(urlPatterns = {"/*"}, filterName = "authFilter")
@NoArgsConstructor
@Slf4j
public class DispatcherFilter implements Filter {
    // default項目后端注冊的zk路徑
    private static final String REGISTRY_SERVER_PATH = GlobalConstants.ZK_NODE_SEPARATOR + GlobalConstants.DEFAULT_PROJECT + "/server";
    // other項目后端注冊的zk路徑
    private static final String OTHER_REGISTRY_SERVER_PATH = GlobalConstants.ZK_NODE_SEPARATOR + GlobalConstants.OTHER_PROJECT + "/server";

    // 特殊的請求路徑轉(zhuǎn)發(fā)到指定的后端實例
    @Value("${dispatcher.special.url}")
    List<String> dispatcherSpecialUrlList;
    @Value("${dispatcher.special.zoneCode}")
    String specialZoneCode;
    @Resource
    ZkClient zkClient;
    @Resource
    DispatcherService dispatcherService;

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        CustomHttpServletRequestWrapper requestWrapper = new CustomHttpServletRequestWrapper((HttpServletRequest) req);
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE,PUT");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");

        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json;charset=UTF-8");

        // user check
        if (authCheck(requestWrapper, response)) {
            return;
        }

        // 禁止favicon.ico圖標請求
        String iconRequest = "/favicon.ico";
        if (iconRequest.equals(requestWrapper.getRequestURI())) {
            return;
        }

        Dispatcher dispatcher = getDispatcherUrl(requestWrapper, response);
        if (dispatcher.getIsReturn()) {
            return;
        }

        String forward = "http://" + dispatcher.getTarget() + requestWrapper.getRequestURI();
        log.info("========== accept request {} {}", requestWrapper.getMethod(), forward);
        dispatcherService.requestService(requestWrapper, response, HttpMethod.valueOf(requestWrapper.getMethod()), forward);
        log.info("========== finished request {} {}", requestWrapper.getMethod(), forward);
    }

    /**
     * 獲取轉(zhuǎn)發(fā)地址
     *
     * @param myRequestWrapper
     * @param response
     * @return
     */
    private Dispatcher getDispatcherUrl(HttpServletRequest myRequestWrapper, HttpServletResponse response) throws IOException {
        Dispatcher result = new Dispatcher();
        result.setIsReturn(true);
        // 獲取注冊中心數(shù)據(jù)
        String zkChroot = myRequestWrapper.getRequestURI().contains("/other") ? OTHER_REGISTRY_SERVER_PATH : REGISTRY_SERVER_PATH;
        List<String> supportZoneList = zkClient.getChildren(zkChroot);
        log.info("獲取注冊中心支持的機房: {}", JSON.toJSONString(supportZoneList));

        String zoneCode = myRequestWrapper.getHeader("zoneCode");
        // 特殊處理的區(qū)域碼
        if ("all".equals(zoneCode)) {
            zoneCode = specialZoneCode;
        }

        if (!supportZoneList.contains(zoneCode)) {
            response.setStatus(HttpStatus.FORBIDDEN);
            response.getWriter().write(JSON.toJSONString(OResult.fail(HttpStatus.UNAUTHORIZED, String.format("不支持該機房%s", zoneCode))));
            return result;
        }

        // 隨機取
        List<String> nodes = zkClient.getChildren(zkChroot + GlobalConstants.ZK_NODE_SEPARATOR + zoneCode);
        if (nodes.size() == 0) {
            response.setStatus(HttpStatus.FORBIDDEN);
            response.getWriter().write(JSON.toJSONString(OResult.fail(HttpStatus.UNAUTHORIZED, String.format("當(dāng)前機房(%s)無可用服務(wù)", zoneCode))));
            return result;
        }
        String target = nodes.get((int) (Math.random() * nodes.size()));

        if (dispatcherSpecialUrlList.contains(myRequestWrapper.getRequestURI())) {
            log.info("Special url");
            List<String> spNodes = zkClient.getChildren(zkChroot + GlobalConstants.ZK_NODE_SEPARATOR + specialZoneCode);
            if (spNodes.size() == 0) {
                response.setStatus(HttpStatus.FORBIDDEN);
                response.getWriter().write(JSON.toJSONString(OResult.fail(HttpStatus.UNAUTHORIZED, String.format("當(dāng)前機房(%s)無可用服務(wù)", zoneCode))));
                return result;
            }
            // 隨機獲取
            int spIndex = (int) (Math.random() * spNodes.size());
            target = spNodes.get(spIndex);
        }
        result.setIsReturn(false);
        result.setTarget(target);
        return result;
    }

    /**
     * 用戶校驗等
     *
     * @param myRequestWrapper
     * @param response
     * @return 是否結(jié)束方法
     */
    private Boolean authCheck(HttpServletRequest myRequestWrapper, HttpServletResponse response) throws IOException {
        return true;
    }

    @Data
    static class Dispatcher {
        private Boolean isReturn;
        private String target;
    }

    @Override
    public void destroy() {
    }
}

具體的轉(zhuǎn)發(fā)實現(xiàn),DispatcherService.class

package com.tiger.web.service;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.tiger.web.common.CustomHttpServletRequestWrapper;
import com.tiger.common.constant.HttpStatus;
import com.tiger.common.domain.OResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.FileItem;
import org.apache.tomcat.util.http.fileupload.RequestContext;
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.List;

/**
 * @author tiger 2022/3/18
 */
@Component
@Slf4j
public class DispatcherService {
    @Resource
    RestTemplate restTemplate;
    private static final long MAX_SIZE = 10 * 1024 * 1024 * 1024L;

    public void requestService(HttpServletRequest request, HttpServletResponse response, HttpMethod method, String uri) throws IOException {
        if (ServletFileUpload.isMultipartContent(request)) {
            // 關(guān)鍵點:文件上傳的轉(zhuǎn)發(fā)
            doFileUploadDispatch(request, response, method, uri);
        } else {
            // 其他轉(zhuǎn)發(fā)
            doDispatch(request, response, method, uri);
        }
    }

    /**
     * 文件上傳
     */
    private void doFileUploadDispatch(HttpServletRequest request, HttpServletResponse response, HttpMethod method, String uri) throws IOException {
        log.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
        CustomHttpServletRequestWrapper requestWrapper = (CustomHttpServletRequestWrapper) request;
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(4096);
        factory.setRepository(new File("./uploadFileTemp"));
        ServletFileUpload fileUpload = new ServletFileUpload(factory);
        fileUpload.setHeaderEncoding("utf-8");
        fileUpload.setSizeMax(MAX_SIZE);

        List<FileItem> fileItemList;
        try {
            RequestContext requestContext = new ServletRequestContext(requestWrapper);
            fileItemList = fileUpload.parseRequest(requestContext);
        } catch (Exception exception) {
            exception.printStackTrace();

            response.setStatus(HttpStatus.ERROR);
            response.getWriter().write(JSON.toJSONString(OResult.fail(HttpStatus.ERROR, exception.getMessage())));
            return;
        }

        if (fileItemList == null || fileItemList.size() == 0) {
            response.setStatus(HttpStatus.ERROR);
            response.getWriter().write(JSON.toJSONString(OResult.fail(HttpStatus.ERROR, "沒有文件")));
            return;
        }
        List<Object> fileList = Lists.newArrayList();
        for (final FileItem fileItem : fileItemList) {
            ByteArrayResource byteArr = new ByteArrayResource(fileItem.get()) {
                @Override
                public String getFilename() throws IllegalStateException {
                    return fileItem.getName();
                }
            };
            fileList.add(byteArr);
        }

        // 進行轉(zhuǎn)發(fā)
        MultiValueMap<String, Object> from = new LinkedMultiValueMap<>();
        // 添加上傳的文件
        for (FileItem fileItem : fileItemList) {
            if (fileItem.getContentType() == null) {
                // 普通參數(shù)
                from.add(fileItem.getFieldName(), fileItem.getString());
            } else {
                // 文件
                from.addAll(fileItem.getFieldName(), fileList);
            }
        }

        // 請求URL
        if (!StringUtils.isEmpty(request.getQueryString())) {
            uri = String.format("%s?%s", uri, URLDecoder.decode(request.getQueryString(), "utf-8"));
        }
        // 請求頭
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            headers.add(name, request.getHeader(name));
        }
        HttpEntity<MultiValueMap<String, Object>> files = new HttpEntity<>(from, headers);

        try {
            ResponseEntity<String> responseEntity = restTemplate.exchange(uri, method, files, String.class);
            if (responseEntity.hasBody()) {
                // 設(shè)置響應(yīng)信息
                response.setStatus(responseEntity.getStatusCodeValue());
                response.getWriter().write(JSON.toJSONString(responseEntity.getBody()));
            }
        } catch (HttpClientErrorException httpClientErrorException) {
            httpClientErrorException.printStackTrace();

            response.setStatus(httpClientErrorException.getRawStatusCode());
            response.getWriter().write(JSON.toJSONString(OResult.fail(httpClientErrorException.getRawStatusCode(), httpClientErrorException.getMessage())));
        } catch (Exception exception) {
            exception.printStackTrace();

            response.setStatus(HttpStatus.ERROR);
            response.getWriter().write(JSON.toJSONString(OResult.fail(HttpStatus.ERROR, "轉(zhuǎn)發(fā)請求異常了" + exception.getMessage())));
        }
    }

    /**
     * 非文件請求
     */
    private void doDispatch(HttpServletRequest request, HttpServletResponse response, HttpMethod method, String uri) throws IOException {
        String requestBody = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
        // 請求體
        Object body = null;
        if (!StringUtils.isEmpty(requestBody)) {
            body = JSON.parse(requestBody);
        }
        // 請求頭
        HttpHeaders headers = new HttpHeaders();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            headers.add(name, request.getHeader(name));
        }
        // 請求URL
        if (!StringUtils.isEmpty(request.getQueryString())) {
            uri = String.format("%s?%s", uri, URLDecoder.decode(request.getQueryString(), "utf-8"));
        }
        HttpEntity<Object> httpEntity = new HttpEntity<>(body, headers);
        ResponseEntity<OResult> exchange;
        PrintWriter writer = response.getWriter();
        try {
            // 發(fā)送請求
            exchange = restTemplate.exchange(uri, method, httpEntity, OResult.class);
            // 設(shè)置響應(yīng)信息
            response.setStatus(exchange.getStatusCodeValue());

            writer.write(JSON.toJSONString(exchange.getBody()));
        } catch (HttpClientErrorException httpClientErrorException) {
            httpClientErrorException.printStackTrace();

            response.setStatus(httpClientErrorException.getRawStatusCode());
            writer.write(JSON.toJSONString(OResult.fail(httpClientErrorException.getRawStatusCode(), httpClientErrorException.getMessage())));
        } catch (Exception exception) {
            exception.printStackTrace();

            response.setStatus(HttpStatus.ERROR);
            writer.write(JSON.toJSONString(OResult.fail(HttpStatus.ERROR, "轉(zhuǎn)發(fā)請求異常了" + exception.getMessage())));
        }
    }
}

項目中定義的返回實體,OResult.class

public class OResult<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer status;
    private String msg;
    private T data;
    private String[] stack;

    public OResult() {
    }

    public static <T> OResult<T> success() {
        OResult<T> result = new OResult();
        result.setMsg("SUCCESS");
        result.setStatus(200);
        result.setData((Object)null);
        return result;
    }

    public static <T> OResult<T> fail() {
        OResult<T> result = new OResult();
        result.setMsg("請求處理出錯");
        return result;
    }

    public static <T> OResult<T> success(T t) {
        OResult<T> result = new OResult();
        result.setMsg("SUCCESS");
        result.setStatus(200);
        result.setData(t);
        return result;
    }
    public static <T> OResult<T> fail(String msg) {
        OResult<T> result = new OResult();
        result.setMsg(msg);
        return result;
    }

    public static <T> OResult<T> fail(int status, String msg) {
        OResult<T> result = new OResult();
        result.setStatus(status);
        result.setMsg(msg);
        return result;
    }

    public static <T> OResult<T> fail(Throwable e) {
        OResult<T> result = new OResult();
        result.setMsg(StringUtils.defaultString(e.getMessage(), e.toString()));
        result.setStack(ExceptionUtils.getStackFrames(e));
        return result;
    }

附后端啟動時將服務(wù)信息注冊到zk的實現(xiàn),ServerRegistry.class

package com.tiger.web.common;

import com.tiger.common.constant.GlobalConstants;
import lombok.extern.slf4j.Slf4j;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * 往zk中注冊服務(wù)
 *
 * @author tiger 2022/2/19
 */
@Component
@Slf4j
public class ServerRegistry implements ApplicationRunner {
    @Resource
    ZkClient zkClient;
    @Value("${zoneCode}")
    private String zoneCode;
    @Value("${server.port}")
    private String port;

    private static final String REGISTRY_SERVER_PATH = GlobalConstants.ZK_NODE_SEPARATOR + GlobalConstants.DEFAULT_PROJECT + "/server";
    private static final long REGISTRY_TIME_OUT_MS = 30000;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // TODO:臨時解決window啟動不注冊,單側(cè)跑不出來問題
        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.startsWith("win")) {
            log.info("Win system skip registry.");
            return;
        }
        // 創(chuàng)建根節(jié)點
        String zkNode = getIp() + ":" + port;
        log.info("Start registry server {}.", zkNode);
        if (!zkClient.exists(REGISTRY_SERVER_PATH)) {
            zkClient.createPersistent(REGISTRY_SERVER_PATH, true);
        }
        if (!zkClient.exists(REGISTRY_SERVER_PATH + GlobalConstants.ZK_NODE_SEPARATOR + zoneCode)) {
            zkClient.createPersistent(REGISTRY_SERVER_PATH + GlobalConstants.ZK_NODE_SEPARATOR + zoneCode);
        }
        // 注冊服務(wù),創(chuàng)建臨時節(jié)點
        long nextTime = System.currentTimeMillis() + REGISTRY_TIME_OUT_MS;
        boolean registry = false;
        while (nextTime - System.currentTimeMillis() > 0 && !registry) {
            try {
                zkClient.createEphemeral(REGISTRY_SERVER_PATH + GlobalConstants.ZK_NODE_SEPARATOR + zoneCode + GlobalConstants.ZK_NODE_SEPARATOR + zkNode);
                registry = true;
                log.info("Registry success.");
                break;
            } catch (ZkNodeExistsException zkNodeExistsException) {
                log.error("zkNode exist cause registry server failure.");
                Thread.sleep(3000);
            } catch (Exception exception) {
                exception.printStackTrace();
                log.error("registry server failure.", exception);
                Thread.sleep(3000);
            }
        }
        if (!registry) {
            // 若注冊不成功,直接退出服務(wù)
            log.error("Fatal error: registry server {} failure and exit.", zkNode);
            System.exit(1);
        }
    }

    private String getIp() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("Fatal error: get ip error and exit.");
            System.exit(1);
            e.printStackTrace();
        }
        return null;
    }
}

主要依賴:

<!--zk client-->
<dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
</dependency>

參考文獻:

本文有很多處可以以更好的方式來實現(xiàn),比如:zk可以用watch等。
待補充

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容