需求背景:
當(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ù)的后端地址。
主要流程:
前端的所有請求,都請求到轉(zhuǎn)發(fā)服務(wù)
轉(zhuǎn)發(fā)服務(wù)根據(jù)請求頭中的區(qū)域進行轉(zhuǎn)發(fā)
各區(qū)域提供服務(wù)的ip和端口通過zk中獲取,zk扮演注冊中心的角色
各區(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等。
待補充