SpringCloud開發(fā)、本地聯(lián)調(diào)解決方案-代理中心實現(xiàn)

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. 解決方案

為了解決上述問題,我們需要一種代理機制。

  1. 該代理可以將某個客戶端ip發(fā)出的請求轉(zhuǎn)發(fā)到所配置的目標機器。
  2. 配置成功后,該服務(wù)的一切請求均將被代理,無論其處于鏈路的哪個階段。
  3. 點對點代理,不會影響他人訪問。

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)系:

image.png

簡單描述下上述流程:

  1. Spring啟動時,注冊Client,Client為Feign的具體調(diào)用類。由于我們使用的eureka,故會注冊LoadBalancerFeignClient。
  2. 當我們加入@EnableFeignClients注解后,Spring啟動時會執(zhí)行FeignClientsRegistrar,掃描包下的@FeignClient注解,創(chuàng)建FeignClientFactoryBean。FeignClientFactoryBean為Feign的工廠生成類,會生成多個Feign的實現(xiàn)類注入到Spring中。
  3. 由于我們使用Hystrix,故會生成HystrixFeign。該類引用并擴展了ReflectiveFeign,ReflectiveFeign為繼承Feign的具體實現(xiàn)類。
  4. HystrixFeign中創(chuàng)建HystrixInvocationHandler,HystrixInvocationHandler為代理方法,會在執(zhí)行真實方法前執(zhí)行所附加的方法,該類會附加hystrix的command策略。由于我們使用的sleuth,故默認注入的是SleuthHystrixConcurrencyStrategy策略,該策略包裝了請求,并傳遞了trace,以供整個鏈路使用。
  5. ReflectiveFeign同時會創(chuàng)建一個SynchronousMethodHandler代理類,該類會feign的攔截器(如果存在)。
  6. 最終當我們調(diào)用FeignClient所注解的接口中的方法時,會首先調(diào)用SynchronousMethodHandler執(zhí)行攔截器,接著調(diào)用HystrixInvocationHandler執(zhí)行包裝請求,最終通過LoadBalancerFeignClient發(fā)出請求。

3.2.1 如何擴展

當我們理解整個過程中,我們得到以下幾個信息:

  1. 在hystrix包裝過程中,會創(chuàng)建線程,從而與當前請求到controller所在的線程隔離,如果要傳輸當前請求線程中的數(shù)據(jù),需要在包裝類創(chuàng)建的線程中傳入,sleuth就是這樣做的。
  2. 攔截器是在hystrix包裝前之前執(zhí)行的,并且兩者在同一線程。
  3. 我們需要在發(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">&times;</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ù)。
代理添加成功后,本地客戶端正常請求接口即可。

添加.png

客戶端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 篩選代理

在頁面右上方可以進行篩選。


列表.png

3.4.2 清空本機代理

點擊按鈕可以清空本機ip所設(shè)置的全部代理,聯(lián)調(diào)結(jié)束后,請及時清空代理,以免為后續(xù)調(diào)試造成影響。

3.4.3 清空全部代理

點擊按鈕可以清空全部代理,請謹慎使用。

3.4.4 刪除單個代理

在表格后方操作欄中可刪除單個代理。


刪除.png

想不想看看墻外的世界
高質(zhì)量圖片壓縮工具

最后編輯于
?著作權(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ù)。

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