1. 背景
在SpringCloud架構(gòu)下,開發(fā)過程中的聯(lián)調(diào)成為痛點。
一般情況下,開發(fā)人員需要運行多個或全部服務(wù),或干擾其他環(huán)境才可正常進行聯(lián)調(diào),成本極高。
我們希望找到一種簡單、快速的方式來解決以下問題。
1.1 僅希望啟動正在開發(fā)的服務(wù)
開發(fā)人員通常只希望啟動當前正在開發(fā)的服務(wù),但微服務(wù)存在眾多鏈路,僅啟動下游服務(wù)是無法通過從上游服務(wù)直接訪問的。
通常我們需要啟動一個上游服務(wù)指向本地的下游服務(wù)或通過線上負載不斷請求,通過一定頻率落在本地服務(wù)。
所以我們需要一個能夠在上游服務(wù)直接請求到本地服務(wù)的方法,減少不必要的等待。
1.2 希望在聯(lián)調(diào)階段斷點
當開發(fā)進入前后端聯(lián)調(diào)時,如開發(fā)環(huán)境存在問題,開發(fā)人員通常希望與前端進行斷點調(diào)試以快速找到問題。
但我們的服務(wù)不僅僅是針對某一人提供的,此時如果他人請求我們所斷點的服務(wù),即會出現(xiàn)問題。
所以我們需要一個能夠針對某個人所單獨提供特定服務(wù)(即本地服務(wù))的方法,以便進行點對點調(diào)試。
2. 解決方案
為了解決上述問題,我們需要一種代理機制。
- 該代理可以將某個客戶端ip發(fā)出的請求轉(zhuǎn)發(fā)到所配置的目標機器。
- 配置成功后,該服務(wù)的一切請求均將被代理,無論其處于鏈路的哪個階段。
- 點對點代理,不會影響他人訪問。
3. 實現(xiàn)思路及源碼
基本思想:對于代理而言,基本思路就是攔截請求方所要請求的地址,轉(zhuǎn)發(fā)到我們希望其到達的目的地。
對于本服務(wù)而言,我們將客戶端所配置的代理均存放在redis中,并且在請求發(fā)出前,比對當前請求是否存在于redis內(nèi),如果存在,重寫其地址為redis中所配置的地址。
基于目前的框架,代理轉(zhuǎn)發(fā)服務(wù)需要代理的組件有兩種。
一種是spring cloud gateway所配置的上游服務(wù)的路由。
另外一種是服務(wù)間的通訊OpenFeign服務(wù)。
我們基于spring boot 2.1.5版本,分別對用于實現(xiàn)代理的兩種組件的原理進行介紹。
3.1 SpringCloudGateway
對于Gateway而言,我們只需要為其添加一個全局過濾器,攔截請求后修改url地址即可。
由于我們的gateway是注冊進eureka的,所以默認情況下,會存在一個LoadBalancerClientFilter。
LoadBalancerClientFilter從名字就可以看出,它的作用是針對客戶端的請求進行負載,它會將以"lb:"開頭的請求(由路由配置的)從eureka中通過策略拿出一個可用的遠端服務(wù),并且轉(zhuǎn)發(fā)請求至改服務(wù)。這與我們要做的代理轉(zhuǎn)發(fā)相同。
而我們只需要在其過濾器執(zhí)行前,將url地址改變,從而不經(jīng)過其過濾器,轉(zhuǎn)而從正常的http請求即可。
全部代碼如下:
3.1.1 NetworkUtil
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Objects;
/**
* @author zhangbowen
*/
public class NetworkUtil {
public static final String HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
public static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
public static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
public static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";
public static final String HTTP_X_FORWARD_IP = "HTTP_X_FORWARDED_FOR";
/**
* 獲取請求主機IP地址,如果通過代理進來,則透過防火墻獲取真實IP地址;
*
* @param request 請求
* @return ip
*/
public static String getIp(ServerHttpRequest request) {
try {
HttpHeaders httpHeaders = request.getHeaders();
// 獲取請求主機IP地址,如果通過代理進來,則透過防火墻獲取真實IP地址
String ip = httpHeaders.getFirst(HEADER_X_FORWARDED_FOR);
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = httpHeaders.getFirst(PROXY_CLIENT_IP);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = httpHeaders.getFirst(WL_PROXY_CLIENT_IP);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = httpHeaders.getFirst(HTTP_CLIENT_IP);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = httpHeaders.getFirst(HTTP_X_FORWARD_IP);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
InetSocketAddress remoteAddress = request.getRemoteAddress();
ip = Objects.requireNonNull(remoteAddress).getAddress().getHostAddress();
}
} else if (ip.length() > 15) {
String[] ips = ip.split(",");
for (String ip1 : ips) {
if (!("unknown".equalsIgnoreCase(ip1))) {
ip = ip1;
break;
}
}
}
if (ip.equals("0:0:0:0:0:0:0:1")) {
ip = InetAddress.getLocalHost().getHostAddress();
}
return ip;
} catch (Exception e) {
return "";
}
}
}
3.1.2 FeignClientProxyForward
import lombok.Data;
/**
* @author zhangbowen
* @since 2019-06-20
*/
@Data
public class FeignClientProxyForward {
/**
* 客戶端ip
*/
private String clientIp;
/**
* 代理服務(wù)
*/
private String serviceName;
/**
* 目標ip
*/
private String targetIp;
/**
* 目標端口
*/
private String targetPort;
}
3.1.3 RedisHelper
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Type;
/**
* @author zhangbowen
* @since 2019/5/27
*/
public class RedisHelper {
private static RedisConnectionFactory factory;
private static ObjectMapper objectMapper;
public RedisHelper(RedisConnectionFactory factory, ObjectMapper objectMapper) {
RedisHelper.factory = factory;
RedisHelper.objectMapper = objectMapper;
}
/**
* 創(chuàng)建泛型template
*
* @param clazz 泛型類
* @param <T> 定義泛型
* @return RedisTemplate
*/
public static <T> RedisTemplate<String, T> template(Class<T> clazz) {
RedisTemplate<String, T> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(clazz);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 創(chuàng)建泛型template
*
* @param <T> 定義泛型
* @return RedisTemplate
*/
public static <T> RedisTemplate<String, T> template() {
RedisTemplate<String, T> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 創(chuàng)建泛型template
*
* @param type 泛型
* @param <T> 定義泛型
* @return RedisTemplate
*/
public static <T> RedisTemplate<String, T> template(Type type) {
RedisTemplate<String, T> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(TypeFactory.defaultInstance().constructType(type));
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
3.1.4 ProxyForwardRequestFilter
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Map;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*;
/**
* @author zhangbowen
* @since 2019-06-21
*/
@Slf4j
public class ProxyForwardRequestFilter implements GlobalFilter, Ordered {
public static final String CLIENT_PROXY_FORWARD_KEY = "feign-client-proxy-forward";
public static final int PROXY_FORWARD_REQUEST_FILTER = 10099;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}
//保留原始url
addOriginalRequestUrl(exchange, url);
String clientIp = NetworkUtil.getIp(exchange.getRequest());
//要請求的服務(wù)名稱
String clientName = url.getHost();
//獲取代理
FeignClientProxyForward proxyForward = getProxyForward(clientIp, clientName);
//如果目標請求未代理,走原請求
if (proxyForward == null) {
//不做任何操作
return chain.filter(exchange);
}
//如果被代理,修改代理的ip,重新構(gòu)建URI
URI newUrl = replaceHostName(url.toString(), clientName, proxyForward.getTargetIp() + ":" + proxyForward.getTargetPort());
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newUrl);
return chain.filter(exchange);
}
private URI replaceHostName(String originalUrl, String host, String newHost) {
String newUrl = originalUrl;
if (originalUrl.startsWith("lb://")) {
newUrl = originalUrl.substring(0, 5)
+ newHost
+ originalUrl.substring(5 + host.length());
}
StringBuffer buffer = new StringBuffer(newUrl);
if ((newUrl.startsWith("lb://") && newUrl.length() == 5)) {
buffer.append("/");
}
return URI.create("http" + buffer.substring(2));
}
/**
* 根據(jù)請求獲取該請求的代理,如果為null,請求源路徑
*
* @return
*/
private FeignClientProxyForward getProxyForward(String requestIp, String clientName) {
try {
BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CLIENT_PROXY_FORWARD_KEY);
//獲取代理json列表
String clientProxyTableJson = boundHashOperations.get(requestIp);
//代理列表為空
if (StringUtils.isEmpty(clientProxyTableJson)) {
//走原方法
return null;
}
Map<String, FeignClientProxyForward> clientProxyTable = JSON.parseObject(clientProxyTableJson, new TypeReference<Map<String, FeignClientProxyForward>>() {
});
if (CollectionUtils.isEmpty(clientProxyTable)) {
//走原方法
return null;
}
return clientProxyTable.get(clientName.toLowerCase());
} catch (Exception e) {
return null;
}
}
@Override
public int getOrder() {
return PROXY_FORWARD_REQUEST_FILTER;
}
}
3.1.5 RequestProxyForwardAutoConfiguration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author zhangbowen
* @since 2019-06-20
*/
@Configuration
public class RequestProxyForwardAutoConfiguration {
@Bean
public ProxyForwardRequestFilter proxyForwardRequestFilter() {
return new ProxyForwardRequestFilter();
}
@Bean
public RedisHelper redisHelper(RedisConnectionFactory factory, ObjectMapper objectMapper) {
return new RedisHelper(factory, objectMapper);
}
}
3.2 OpenFeign
針對于OpenFeign的攔截會復(fù)雜一些,接下來的所涉及到的內(nèi)容會冗長且繁雜,希望大家可以多一些耐心。
先來看一張OpenFeign在SpringCloud使用過程中基本的類圖關(guān)系:

簡單描述下上述流程:
- Spring啟動時,注冊Client,Client為Feign的具體調(diào)用類。由于我們使用的eureka,故會注冊LoadBalancerFeignClient。
- 當我們加入@EnableFeignClients注解后,Spring啟動時會執(zhí)行FeignClientsRegistrar,掃描包下的@FeignClient注解,創(chuàng)建FeignClientFactoryBean。FeignClientFactoryBean為Feign的工廠生成類,會生成多個Feign的實現(xiàn)類注入到Spring中。
- 由于我們使用Hystrix,故會生成HystrixFeign。該類引用并擴展了ReflectiveFeign,ReflectiveFeign為繼承Feign的具體實現(xiàn)類。
- HystrixFeign中創(chuàng)建HystrixInvocationHandler,HystrixInvocationHandler為代理方法,會在執(zhí)行真實方法前執(zhí)行所附加的方法,該類會附加hystrix的command策略。由于我們使用的sleuth,故默認注入的是SleuthHystrixConcurrencyStrategy策略,該策略包裝了請求,并傳遞了trace,以供整個鏈路使用。
- ReflectiveFeign同時會創(chuàng)建一個SynchronousMethodHandler代理類,該類會feign的攔截器(如果存在)。
- 最終當我們調(diào)用FeignClient所注解的接口中的方法時,會首先調(diào)用SynchronousMethodHandler執(zhí)行攔截器,接著調(diào)用HystrixInvocationHandler執(zhí)行包裝請求,最終通過LoadBalancerFeignClient發(fā)出請求。
3.2.1 如何擴展
當我們理解整個過程中,我們得到以下幾個信息:
- 在hystrix包裝過程中,會創(chuàng)建線程,從而與當前請求到controller所在的線程隔離,如果要傳輸當前請求線程中的數(shù)據(jù),需要在包裝類創(chuàng)建的線程中傳入,sleuth就是這樣做的。
- 攔截器是在hystrix包裝前之前執(zhí)行的,并且兩者在同一線程。
- 我們需要在發(fā)出請求前改變url,也就是LoadBalancerFeignClient內(nèi)。
3.2.2 FeignClientProxyForward
@Data
public class FeignClientProxyForward {
/**
* 客戶端ip
*/
private String clientIp;
/**
* 代理服務(wù)
*/
private String serviceName;
/**
* 目標ip
*/
private String targetIp;
/**
* 目標端口
*/
private String targetPort;
}
3.2.3 傳遞請求方ip
HystrixConcurrencyStrategy為我們提供了擴展口,它允許我們自己實現(xiàn)一個策略,以允許在傳輸過程中傳遞參數(shù),sleuth已經(jīng)為我們實現(xiàn)了,因此我們可以仿照其代碼進行修改:
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle;
import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier;
import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook;
import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher;
import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy;
import com.netflix.hystrix.strategy.properties.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author zhangbowen
* @since 2019-06-20
*/
@Slf4j
public class TransferHeaderHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
private HystrixConcurrencyStrategy delegate;
public TransferHeaderHystrixConcurrencyStrategy() {
try {
this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
if (this.delegate instanceof TransferHeaderHystrixConcurrencyStrategy) {
return;
}
HystrixCommandExecutionHook commandExecutionHook =
HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy =
HystrixPlugins.getInstance().getPropertiesStrategy();
this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy);
HystrixPlugins.reset();
HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
} catch (Exception e) {
log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e);
}
}
private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier,
HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) {
if (log.isDebugEnabled()) {
log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy ["
+ this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher ["
+ metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]");
log.debug("Registering Sleuth Hystrix Concurrency Strategy.");
}
}
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return delegate != null
? delegate.getBlockingQueue(maxQueueSize)
: super.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(
HystrixRequestVariableLifecycle<T> rv) {
return delegate != null
? delegate.getRequestVariable(rv)
: super.getRequestVariable(rv);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize,
HystrixProperty<Integer> keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
return delegate != null
? delegate.getThreadPool(threadPoolKey, corePoolSize,
maximumPoolSize, keepAliveTime, unit, workQueue)
: super.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize,
keepAliveTime, unit, workQueue);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixThreadPoolProperties threadPoolProperties) {
return delegate != null
? delegate.getThreadPool(threadPoolKey,
threadPoolProperties)
: super.getThreadPool(threadPoolKey, threadPoolProperties);
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
if (callable instanceof TransferCallable) {
return callable;
}
Callable<T> wrappedCallable = this.delegate != null
? this.delegate.wrapCallable(callable) : callable;
if (wrappedCallable instanceof TransferCallable) {
return wrappedCallable;
}
//將當前請求信息拿到,放入包裝類所在線程中
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return new TransferCallable<>(wrappedCallable, requestAttributes);
}
static class TransferCallable<T> implements Callable<T> {
private final Callable<T> delegate;
private final RequestAttributes requestAttributes;
public TransferCallable(Callable<T> delegate, RequestAttributes requestAttributes) {
this.requestAttributes = requestAttributes;
this.delegate = delegate;
}
@Override
public T call() throws Exception {
try {
//將當前請求信息拿到,放入包裝類所在線程中
RequestContextHolder.setRequestAttributes(requestAttributes);
return delegate.call();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
}
}
3.2.4 NetWorkUtils
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
/**
* @author zhangbowen
*/
public class NetworkUtil {
public static final String HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
public static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
public static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
public static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";
public static final String HTTP_X_FORWARD_IP = "HTTP_X_FORWARDED_FOR";
/**
* 獲取請求主機IP地址,如果通過代理進來,則透過防火墻獲取真實IP地址;
*
* @param request 請求
* @return ip
*/
public static String getIp(HttpServletRequest request) {
try {
// 獲取請求主機IP地址,如果通過代理進來,則透過防火墻獲取真實IP地址
String ip = request.getHeader(HEADER_X_FORWARDED_FOR);
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader(PROXY_CLIENT_IP);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader(WL_PROXY_CLIENT_IP);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader(HTTP_CLIENT_IP);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader(HTTP_X_FORWARD_IP);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} else if (ip.length() > 15) {
String[] ips = ip.split(",");
for (String ip1 : ips) {
if (!("unknown".equalsIgnoreCase(ip1))) {
ip = ip1;
break;
}
}
}
if (ip.equals("0:0:0:0:0:0:0:1")) {
ip = InetAddress.getLocalHost().getHostAddress();
}
return ip;
} catch (Exception e) {
return "";
}
}
}
3.2.5 在攔截器的header中放入真實ip
實現(xiàn)攔截器,將包裝類所在線程中的信息,拿出放入header中傳遞。
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @author zhangbowen
* @since 2019-06-20
* 轉(zhuǎn)發(fā)ip
*/
public class TransferClientIpRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (attributes == null) {
return;
}
HttpServletRequest request = attributes.getRequest();
//放入ip
template.header(NetworkUtil.HEADER_X_FORWARDED_FOR, NetworkUtil.getIp(request));
}
}
3.2.6 實現(xiàn)client,重寫url
我們需要實現(xiàn)自己的client,以便由redis中讀取所配置的代理信息,并且重寫url。
請注意,由于某些方法需要在feign包下才可使用,故我們建立與feign包一致的包名。
package org.springframework.cloud.openfeign.ribbon;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.netflix.client.ClientException;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.client.config.IClientConfig;
import feign.Client;
import feign.Request;
import feign.Response;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
/**
* @author zhangbowen
* 設(shè)置開啟該配置的開關(guān),以便在生產(chǎn)環(huán)境不開啟代理。
* 否則,注入原有客戶端,不進行覆蓋。
*/
public class ProxyForwardLoadBalancerFeignClient implements Client {
public static final String CLIENT_PROXY_FORWARD_KEY = "feign-client-proxy-forward";
static final Request.Options DEFAULT_OPTIONS = new Request.Options();
private final Client delegate;
private CachingSpringLoadBalancerFactory lbClientFactory;
private SpringClientFactory clientFactory;
public ProxyForwardLoadBalancerFeignClient(Client delegate,
CachingSpringLoadBalancerFactory lbClientFactory,
SpringClientFactory clientFactory) {
this.delegate = delegate;
this.lbClientFactory = lbClientFactory;
this.clientFactory = clientFactory;
}
static URI cleanUrl(String originalUrl, String host) {
String newUrl = originalUrl;
if (originalUrl.startsWith("https://")) {
newUrl = originalUrl.substring(0, 8)
+ originalUrl.substring(8 + host.length());
} else if (originalUrl.startsWith("http")) {
newUrl = originalUrl.substring(0, 7)
+ originalUrl.substring(7 + host.length());
}
StringBuffer buffer = new StringBuffer(newUrl);
if ((newUrl.startsWith("https://") && newUrl.length() == 8)
|| (newUrl.startsWith("http://") && newUrl.length() == 7)) {
buffer.append("/");
}
return URI.create(buffer.toString());
}
static URI replaceHostName(String originalUrl, String host, String newHost) {
String newUrl = originalUrl;
if (originalUrl.startsWith("https://")) {
newUrl = originalUrl.substring(0, 8)
+ newHost
+ originalUrl.substring(8 + host.length());
} else if (originalUrl.startsWith("http")) {
newUrl = originalUrl.substring(0, 7)
+ newHost
+ originalUrl.substring(7 + host.length());
}
StringBuffer buffer = new StringBuffer(newUrl);
if ((newUrl.startsWith("https://") && newUrl.length() == 8)
|| (newUrl.startsWith("http://") && newUrl.length() == 7)) {
buffer.append("/");
}
return URI.create(buffer.toString());
}
/**
* 根據(jù)請求獲取該請求的代理,如果為null,請求源路徑
*
* @param request
* @return
*/
private FeignClientProxyForward getProxyForward(Request request, String clientName) {
try {
String requestIp = request.headers().getOrDefault(NetworkUtil.HEADER_X_FORWARDED_FOR, Collections.emptyList()).iterator().next();
BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CLIENT_PROXY_FORWARD_KEY);
//獲取代理json列表
String clientProxyTableJson = boundHashOperations.get(requestIp);
//代理列表為空
if (StringUtils.isEmpty(clientProxyTableJson)) {
//走原方法
return null;
}
Map<String, FeignClientProxyForward> clientProxyTable = JSON.parseObject(clientProxyTableJson, new TypeReference<Map<String, FeignClientProxyForward>>() {
});
if (CollectionUtils.isEmpty(clientProxyTable)) {
//走原方法
return null;
}
return clientProxyTable.get(clientName.toLowerCase());
} catch (Exception e) {
return null;
}
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
//獲取當前請求方的ip。
//根據(jù)ip獲取該ip設(shè)置的代理列表,判斷是否存在當前clientName。
//如果存在,重寫url,轉(zhuǎn)向訪問代理。
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
//判斷該次請求的目標服務(wù)是否代理
FeignClientProxyForward proxyForward = getProxyForward(request, clientName);
//如果目標請求未代理,走原請求
if (proxyForward == null) {
//走原方法
return original(request, options);
}
//如果被代理,修改代理的ip,重新構(gòu)建URI
URI uriWithoutHost = replaceHostName(request.url(), clientName, proxyForward.getTargetIp() + ":" + proxyForward.getTargetPort());
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
} catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
public Response original(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
} catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
} else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
protected IOException findIOException(Throwable t) {
if (t == null) {
return null;
}
if (t instanceof IOException) {
return (IOException) t;
}
return findIOException(t.getCause());
}
public Client getDelegate() {
return this.delegate;
}
private FeignLoadBalancer lbClient(String clientName) {
return this.lbClientFactory.create(clientName);
}
static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
FeignOptionsClientConfig(Request.Options options) {
setProperty(CommonClientConfigKey.ConnectTimeout,
options.connectTimeoutMillis());
setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
}
@Override
public void loadProperties(String clientName) {
}
@Override
public void loadDefaultValues() {
}
}
}
3.2.7 RequestProxyForwardAutoConfiguration
import feign.Client;
import feign.httpclient.ApacheHttpClient;
import org.apache.http.client.HttpClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.ProxyForwardLoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author zhangbowen
* @since 2019-06-20
*/
@Configuration
public class RequestProxyForwardAutoConfiguration {
@Bean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory, HttpClient httpClient) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new ProxyForwardLoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
@Bean
public TransferHeaderHystrixConcurrencyStrategy transferHeaderHystrixConcurrencyStrategy() {
return new TransferHeaderHystrixConcurrencyStrategy();
}
@Bean
public TransferClientIpRequestInterceptor transferHeaderRequestInterceptor() {
return new TransferClientIpRequestInterceptor();
}
}
3.3 管理端
基礎(chǔ)設(shè)施搭建完以后,我們需要有一個簡單的管理端來管理我們的代理服務(wù)。
這里我們使用bootstrap簡單搭建。
3.3.1 增刪改查接口
import com.whdx.framework.web.bean.MessageBody;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author zhangbowen
* @since 2019-06-20
*/
@RequestMapping("/proxy")
@RestController
public class RequestProxyForwardController {
@Autowired
private RequestProxyForwardService proxyForwardService;
/**
* 列表數(shù)據(jù)
*/
@GetMapping("/list")
public MessageBody list() {
return proxyForwardService.list();
}
/**
* 設(shè)置數(shù)據(jù)
*/
@PostMapping("/put")
public MessageBody put(FeignClientProxyForward feignClientProxyForward) {
return proxyForwardService.put(feignClientProxyForward);
}
/**
* 清空某個ip數(shù)據(jù)
*/
@DeleteMapping("/clearByIp")
public MessageBody clearByIp() {
return proxyForwardService.clearByIp();
}
/**
* 根據(jù)服務(wù)名與ip刪除
*/
@DeleteMapping("/deleteWithTargetNameAndIp")
public MessageBody deleteWithTargetNameAndIp(FeignClientProxyForward feignClientProxyForward) {
return proxyForwardService.deleteWithTargetNameAndIp(feignClientProxyForward);
}
/**
* 清空全部數(shù)據(jù)
*/
@DeleteMapping("/clearAll")
public MessageBody clearAll() {
return proxyForwardService.clearAll();
}
}
3.3.2 RequestProxyForwardService
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author zhangbowen
* @since 2019-06-20
*/
@Service
public class RequestProxyForwardService {
public static final String CACHE_KEY = "feign-client-proxy-forward";
/**
* 全部數(shù)據(jù)
*
* @return
*/
public MessageBody list() {
BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CACHE_KEY);
MessageBody<List<FeignClientProxyForward>> messageBody = new MessageBody<>();
List<String> list = boundHashOperations.values();
if (CollectionUtils.isEmpty(list)) {
messageBody.setRetData(new ArrayList<>());
return messageBody;
}
List<FeignClientProxyForward> result = list.stream().flatMap(item -> JSON.parseObject(item, new TypeReference<Map<String, FeignClientProxyForward>>() {
}).values().stream()).collect(Collectors.toList());
messageBody.setRetData(result);
return messageBody;
}
/**
* 設(shè)置數(shù)據(jù)
* redis map 各key含義: CACHE_KEY,clientIp,serviceName
* @return
*/
public MessageBody put(FeignClientProxyForward feignClientProxyForward) {
feignClientProxyForward.setServiceName(feignClientProxyForward.getServiceName().toLowerCase());
BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CACHE_KEY);
String clientProxyMapJson = boundHashOperations.get(feignClientProxyForward.getClientIp());
Map<String, FeignClientProxyForward> clientProxyMap;
if (clientProxyMapJson == null) {
clientProxyMap = new HashMap<>();
} else {
clientProxyMap = JSON.parseObject(clientProxyMapJson, new TypeReference<Map<String, FeignClientProxyForward>>() {
});
}
clientProxyMap.put(feignClientProxyForward.getServiceName(), feignClientProxyForward);
boundHashOperations.put(feignClientProxyForward.getClientIp(), JSON.toJSONString(clientProxyMap));
return new MessageBody();
}
/**
* 清空某個ip下的代理
*
* @return
*/
public MessageBody clearByIp() {
BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CACHE_KEY);
boundHashOperations.delete(WebContextFacade.getRequestContext().getIp());
return new MessageBody();
}
/**
* 根據(jù)ip+serviceName刪除
*
* @return
*/
public MessageBody deleteWithTargetNameAndIp(FeignClientProxyForward feignClientProxyForward) {
BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CACHE_KEY);
String clientProxyJson = boundHashOperations.get(feignClientProxyForward.getClientIp());
Map<String, FeignClientProxyForward> clientProxyMap;
if (clientProxyJson == null) {
return new MessageBody();
} else {
clientProxyMap = JSON.parseObject(clientProxyJson, new TypeReference<Map<String, FeignClientProxyForward>>() {
});
}
String key = feignClientProxyForward.getServiceName();
if (clientProxyMap == null || !clientProxyMap.containsKey(key)) {
return new MessageBody();
}
clientProxyMap.remove(key);
boundHashOperations.put(feignClientProxyForward.getClientIp(), JSON.toJSONString(clientProxyMap));
return new MessageBody();
}
/**
* 清空全部
*
* @return
*/
public MessageBody clearAll() {
RedisHelper.template().delete(CACHE_KEY);
return new MessageBody();
}
}
3.3.3 FeignClientProxyForward
import lombok.Data;
/**
* @author zhangbowen
* @since 2019-06-20
*/
@Data
public class FeignClientProxyForward {
/**
* 客戶端ip
*/
private String clientIp;
/**
* 代理服務(wù)
*/
private String serviceName;
/**
* 目標ip
*/
private String targetIp;
/**
* 目標端口
*/
private String targetPort;
}
3.3.4 RequestProxyForwardViewController
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author zhangbowen
* @since 2019-06-20
*/
@RequestMapping("/proxy")
@Controller
public class RequestProxyForwardViewController {
/**
* 主頁
*
* @return
*/
@GetMapping
public String index(Model model) {
model.addAttribute("clientIp", WebContextFacade.getRequestContext().getIp());
return "proxyIndex";
}
}
3.3.5 proxyIndex.ftl
頁面中引用的js和css大家自行尋找吧。
bootstrap4.x
<!DOCTYPE html>
<html>
<head>
<link href="/static/bootstrap-table.min.css" rel="stylesheet">
<link href="/static/bootstrap.min.css" rel="stylesheet">
<script src="/static/jquery-3.4.1.min.js"></script>
<script src="/static/bootstrap.min.js"></script>
<script src="/static/bootstrap-table.min.js"></script>
<script src="/static/bootstrap-table-zh-CN.min.js"></script>
<script src="/static/layer.js"></script>
<style>
body {
margin: 20px;
}
.title {
text-align: center;
}
.op {
margin-top: 20px;
}
.op button {
margin-right: 10px;
}
.content {
margin-top: 20px;
}
.layui-layer-btn0 {
color: white !important;
}
</style>
</head>
<div class="title">
<h1>微服務(wù)代理配置中心</h1>
</div>
<div class="op">
<button type="button" data-toggle="modal" onclick="openAddProxyModal()" class="btn btn-outline-primary">添加代理
</button>
<button type="button" data-toggle="modal" onclick="clearProxyIp()" class="btn btn-outline-danger">清空本機ip代理
</button>
<button type="button" data-toggle="modal" onclick="clearAllProxy()" class="btn btn-outline-danger">清空全部代理
</button>
</div>
<div class="content">
<table
id="table"
data-toggle="table"
data-search="true"
data-locale="zh-CN"
>
<thead>
<tr>
<th data-field="clientIp">客戶端ip</th>
<th data-field="serviceName">代理服務(wù)</th>
<th data-field="targetIp">目標ip</th>
<th data-field="targetPort">目標端口</th>
<th data-field="operate" data-formatter="operateFormatter" data-events="operateEvents">操作</th>
</tr>
</thead>
</table>
</div>
<div>
<div class="modal fade" id="addProxy" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加代理</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group row">
<label for="clientIp" class="col-md-3 col-form-label">客戶端ip</label>
<div class="col-md-6">
<input type="text" class="form-control" id="clientIp">
</div>
</div>
<div class="form-group row">
<label for="targetIp" class="col-md-3 col-form-label">目標ip</label>
<div class="col-md-6">
<input type="text" class="form-control" id="targetIp">
</div>
</div>
<div class="form-group row">
<label for="targetPort" class="col-md-3 col-form-label">目標端口</label>
<div class="col-md-6">
<input type="text" class="form-control" id="targetPort">
</div>
</div>
<div class="form-group row">
<label for="serviceName" class="col-md-3 col-form-label">代理服務(wù)</label>
<div class="col-md-6">
<input type="text" class="form-control" id="serviceName">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">關(guān)閉</button>
<button type="button" class="btn btn-primary" id="saveProxy">確定</button>
</div>
</div>
</div>
</div>
</div>
<script>
window.operateEvents = {
'click .deleteOne': function (e, value, row, index) {
layer.confirm('確定刪除本條代理?', function (index) {
$.ajax({
type: "DELETE",
url: "/proxy/deleteWithTargetNameAndIp",
data:{
clientIp: row.clientIp,
serviceName: row.serviceName
},
dataType: "json",
success: function () {
layer.close(index);
layer.msg("刪除成功");
setTimeout(function(){loadData();},100);
}
});
});
}
}
var $table = $('#table');
function operateFormatter(value, row, index) {
return [
'<button type="button" data-toggle="modal" class="deleteOne btn btn-outline-danger">刪除</button>'
].join('')
}
function clearProxyIp() {
layer.confirm('確定清空本機全部代理?', function (index) {
$.ajax({
type: "DELETE",
url: "/proxy/clearByIp",
dataType: "json",
success: function () {
layer.close(index);
layer.msg("清理成功");
setTimeout(function(){loadData();},100);
}
});
});
}
function clearAllProxy() {
layer.confirm('是否清空全部代理?(不可還原)', function (index) {
$.ajax({
type: "DELETE",
url: "/proxy/clearAll",
dataType: "json",
success: function () {
layer.close(index);
layer.msg("清理成功");
setTimeout(function(){loadData();},100);
}
});
});
}
function openAddProxyModal() {
$("#clientIp").val("${clientIp}");
$("#targetIp").val("");
$("#targetName").val("");
$("#targetPort").val("");
$('#addProxy').modal('show')
}
function loadData() {
$.get("/proxy/list", function (res) {
var list = res.retData;
$table.bootstrapTable('load', list)
})
}
$(function () {
loadData();
$("#saveProxy").on("click", function () {
var clientIp = $("#clientIp").val();
var targetIp = $("#targetIp").val();
var targetPort = $("#targetPort").val();
var serviceName = $("#serviceName").val();
if (!(clientIp && targetIp && serviceName && targetPort)) {
layer.msg("請完善信息");
return false;
}
$.post("/proxy/put", {
"clientIp": clientIp,
"targetIp": targetIp,
"targetPort": targetPort,
"serviceName": serviceName,
}, function (res) {
layer.msg("保存成功");
$('#addProxy').modal('hide');
loadData();
});
});
})
</script>
</html>
3.4 快速使用
進入頁面后,點擊左上方添加代理。在彈出的頁面中錄入相關(guān)數(shù)據(jù)。
代理添加成功后,本地客戶端正常請求接口即可。

客戶端ip:這里的客戶端指的是最初的訪問者,即打開瀏覽器或打開某個應(yīng)用的用戶所使用的終端ip。
目標ip:要訪問的機器的ip地址,即某個后端開發(fā)人員的機器ip。
目標端口:要訪問的機器端口,即某個后端開發(fā)人員所開啟的應(yīng)用端口。端口號為數(shù)字,如:8080
代理服務(wù):要代理的開發(fā)環(huán)境服務(wù)名稱,如:paper、common、user。具體某個服務(wù)經(jīng)過鏈路最終所到達的服務(wù)名稱需詢問相關(guān)后端人員。
全部服務(wù)名可以從此地址查看:eureka。進入后,application即服務(wù)名
添加相同ip、服務(wù)名的代理,即會覆蓋已經(jīng)存在的代理。
3.4.1 篩選代理
在頁面右上方可以進行篩選。

3.4.2 清空本機代理
點擊按鈕可以清空本機ip所設(shè)置的全部代理,聯(lián)調(diào)結(jié)束后,請及時清空代理,以免為后續(xù)調(diào)試造成影響。
3.4.3 清空全部代理
點擊按鈕可以清空全部代理,請謹慎使用。
3.4.4 刪除單個代理
在表格后方操作欄中可刪除單個代理。
