优化SpringCloud本地开发方式

本地开发

如果是SpringCloud项目一般开发本地会启动很多服务.像注册/配置中心.网关.各个服务模块. 开发效率极其低下. 思考这些公用的模块都可以搞到开发环境中去. 在网关和服务调用时做下调用隔离即可. 这样本地开发时只需要关注自己开发的一个模块即可.

流程分析.

  1. 建立公用的gateway应用.部署到开发环境.开发人员本地开发请求流量都经过gateway
  2. 在网关处使用拦截器将开发者的ip记录到ThreadLocal中和请求头中
  3. 在自定义的LoadBalancer组件中.对比注册的服务ip是否为开发者的ip.并进行进行路由
  4. 请求到服务层是使用拦截器获取到请求头中的ip,再次放入ThreadLocal中和请求头中
  5. 当服务间调用使用拦截器透传客户端ip到服务间请求的request header中
  6. SpringCloud版本2020.0.1

具体实现

  • 网关filter
import cn.nvr.commons.tool.utils.StringPool;
import cn.nvr.springcloud.starter.develop.loadbalancer.DevelopContextHolder;
import cn.nvr.springcloud.starter.develop.loadbalancer.DevelopLoadBalancer;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Objects;

/**
 * @author xielongwang
 * @create 2021/3/8 2:57 下午
 * @description 将当前请求者的ip放入 DevelopContextHolder 用于用户服务调用. 仅在dev环境生效
 */
@Slf4j
@Profile("dev")
@AllArgsConstructor
public class ReactiveClientIpFilter implements GlobalFilter , Ordered {

    private static final String UNKNOWN = "unknown";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取客户端请求者的ip
        String requestIp = getServerHttpRequestIpAddress(exchange.getRequest());
        DevelopContextHolder.getCurrentContext().add(DevelopLoadBalancer.CLIENT_IP, requestIp);
        exchange.getRequest().mutate().header(DevelopLoadBalancer.CLIENT_IP, requestIp).build();
        return chain.filter(exchange);
    }

    public static String getServerHttpRequestIpAddress(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
            if (ip.contains(StringPool.COMMA)) {
                ip = ip.split(StringPool.COMMA)[0];
            }
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress();
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }

    @Override
    public int getOrder() {
        return -10002;
    }
}
  • 对webclient拦截
import cn.nvr.springcloud.starter.develop.loadbalancer.DevelopContextHolder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.cloud.client.loadbalancer.reactive.WebClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.web.reactive.function.client.ClientRequest;
import static cn.nvr.springcloud.starter.develop.loadbalancer.DevelopLoadBalancer.CLIENT_IP;

/**
 * @author xielongwang
 * @create 2021/3/10 9:47 上午
 * @description
 */
@Profile("dev")
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@Import(ReactiveClientIpFilter.class)
public class ReactiveClientIpFilterAutoConfiguration {

    @Bean
    public WebClientCustomizer webClientCustomizer() {
        return webClientBuilder -> webClientBuilder.filter((request, next) -> {
            final String developer = DevelopContextHolder.getCurrentContext().get(CLIENT_IP);
            if (developer != null && developer.length() > 0) {
                return next.exchange(ClientRequest.from(request).headers(httpHeaders -> {
                    httpHeaders.add(CLIENT_IP, developer);
                }).build());
            }
            return next.exchange(request);
        });
    }
}
  • 自定义的客户端负载均衡器
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.commons.util.InetUtilsProperties;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * @author xielongwang
 * @create 2021/3/8 9:40 上午
 * @description 根据ip实现调用开发者自己的注册的服务. 如果没有找到.那么call公用的服务
 */
public class DevelopLoadBalancer extends RoundRobinLoadBalancer {

    public static final String CLIENT_IP ="client_ip";

    private static final Logger log = LoggerFactory.getLogger(DevelopLoadBalancer.class);

    ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    final String serviceId;

    final InetUtils.HostInfo hostInfo;

    public DevelopLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, InetUtils inetUtils) {
        super(serviceInstanceListSupplierProvider, serviceId);
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.serviceId = serviceId;
        if (inetUtils==null){
            inetUtils=new InetUtils(new InetUtilsProperties());
        }
        this.hostInfo = inetUtils.findFirstNonLoopbackHostInfo();
    }

    @SuppressWarnings("rawtypes")
    @Override
    // see original
    // https://github.com/Netflix/ocelli/blob/master/ocelli-core/
    // src/main/java/netflix/ocelli/loadbalancer/RoundRobinLoadBalancer.java
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
                .getIfAvailable(NoopServiceInstanceListSupplier::new);
        //获取当前请求的 developer 的请求头.首先根据 developer 进行服务调用.
        return supplier.get(request).next()
                .map(serviceInstances -> processInstanceResponse(supplier, request, serviceInstances));
    }

    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, Request request,
                                                              List<ServiceInstance> serviceInstances) {
        Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances, request);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
        }
        return serviceInstanceResponse;
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + serviceId);
            }
            return new EmptyResponse();
        }

        DefaultRequestContext requestContext = (DefaultRequestContext) request.getContext();
        RequestData clientRequest = (RequestData) requestContext.getClientRequest();
        HttpHeaders headers = clientRequest.getHeaders();
        //获取到请求者的ip
        String developHost = headers.getFirst(CLIENT_IP);
        if (isBlank(developHost)){
            developHost=DevelopContextHolder.getCurrentContext().get(CLIENT_IP);
        }

        if ("127.0.0.1".equals(developHost)) {
            developHost = hostInfo.getIpAddress();
        }

        // 根据ip匹配到的instance
        final List<ServiceInstance> matchedMetaServers = new ArrayList<>();

        // 带有公共服务标签的服务.
        final List<ServiceInstance> commonServers = new ArrayList<>();

        for (ServiceInstance instance : instances) {
            String instanceHost = instance.getHost();
            if (instanceHost.equals(developHost)) {
                matchedMetaServers.add(instance);
            }

            boolean commonService = Boolean.parseBoolean(instance.getMetadata().get("develop.common.service"));
            if (commonService) {
                commonServers.add(instance);
            }
        }

        //如果 matchedMetaServers 不为空则从 matchedMetaServers 选择
        if (isNotEmpty(matchedMetaServers)) {
            return getDefaultInstanceResponse(matchedMetaServers);
        }

        // matchedMetaServers 为空 noMetaServers 不为空. 则从 noMetaServers 选择.
        return getDefaultInstanceResponse(commonServers);
    }


    private Response<ServiceInstance> getDefaultInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + serviceId);
            }
            return new EmptyResponse();
        }

        int index = ThreadLocalRandom.current().nextInt(instances.size());

        ServiceInstance instance = instances.get(index);

        return new DefaultResponse(instance);
    }


    /**
     * Check if the String has any non-whitespace character.
     *
     * @param string String to check
     * @return {@code true} if the String has any non-whitespace character
     */
    public static boolean isNotBlank(String string) {
        return !isBlank(string);
    }


    /**
     * Check if the String is null or has only whitespaces.
     * <p>
     * Modified from {org.apache.commons.lang.StringUtils#isBlank(String)}.
     *
     * @param string String to check
     * @return {@code true} if the String is null or has only whitespaces
     */
    public static boolean isBlank(String string) {
        if (isEmpty(string)) {
            return true;
        }
        for (int i = 0; i < string.length(); i++) {
            if (!Character.isWhitespace(string.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Check if the String is null or empty.
     *
     * @param string String to check
     * @return {@code true} if the String is null or empty
     */
    public static boolean isEmpty(String string) {
        return string == null || string.isEmpty();
    }

    /**
     * 集合是否为空
     *
     * @param collection 集合
     * @return 是否为空
     */
    public static boolean isEmpty(Collection<?> collection) {
        return collection == null || collection.isEmpty();
    }

    /**
     * 集合是否为非空
     *
     * @param collection 集合
     * @return 是否为非空
     */
    public static boolean isNotEmpty(Collection<?> collection) {
        return !isEmpty(collection);
    }
}
  • 使用自定义的负载均衡器
import cn.nvr.springcloud.starter.develop.loadbalancer.DevelopLoadBalancer;
import cn.nvr.springcloud.starter.develop.loadbalancer.reactive.ReactiveClientIpFilterAutoConfiguration;
import cn.nvr.springcloud.starter.develop.loadbalancer.web.WebClientIpFilterConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;

/**
 * @author xielongwang
 * @create 2021/2/25 9:54 上午
 * @description Develop 组件. 只在dev的情况下启用.
 */
@Profile("dev")
@Configuration
@ConditionalOnDiscoveryEnabled
//为所有的client注册自定义的负载均衡配置
@LoadBalancerClients(defaultConfiguration = DevelopAutoConfiguration.class)
@Import({WebClientIpFilterConfiguration.class, ReactiveClientIpFilterAutoConfiguration.class})
public class DevelopAutoConfiguration {


    @Bean
    public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
                                                                                   LoadBalancerClientFactory loadBalancerClientFactory,
                                                                                   ObjectProvider<InetUtils> inetUtils) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new DevelopLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, inetUtils.getIfAvailable());
    }

}
  • 服务层filter

import cn.nvr.springcloud.starter.develop.loadbalancer.DevelopContextHolder;
import cn.nvr.springcloud.starter.develop.loadbalancer.DevelopLoadBalancer;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author xielongwang
 * @create 2021/3/10 9:42 上午
 * @description DevelopContextHolder 放入客户端ip
 */
public class WebClientIpFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String header = request.getHeader(DevelopLoadBalancer.CLIENT_IP);
        try {
            DevelopContextHolder.getCurrentContext().add(DevelopLoadBalancer.CLIENT_IP, header);
            chain.doFilter(request, response);
        } finally {
            DevelopContextHolder.clearCurrentContext();
        }
    }
}
  • 配置feign,restTemplate拦截器
import cn.nvr.springcloud.starter.develop.loadbalancer.DevelopContextHolder;
import feign.RequestInterceptor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cloud.client.loadbalancer.RestTemplateCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.client.ClientHttpRequestInterceptor;

import java.util.ArrayList;
import java.util.List;

/**
 * @author xielongwang
 * @create 2021/3/10 9:13 上午
 * @description ClientIp 自动配置
 */
@Profile("dev")
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class WebClientIpFilterConfiguration {

    private static final String CLIENT_IP = "client_ip";

    @Bean
    public FilterRegistrationBean<WebClientIpFilter> routeProxyFilter() {
        FilterRegistrationBean<WebClientIpFilter> filterRegistrationBean = new FilterRegistrationBean<WebClientIpFilter>();
        filterRegistrationBean.setFilter(new WebClientIpFilter());
        filterRegistrationBean.setOrder(99);
        filterRegistrationBean.setEnabled(true);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

    /**
     * feign support
     *
     * @return RequestInterceptor
     */
    @Bean
    public RequestInterceptor headerInterceptor() {
        return requestTemplate -> {
            final String developer = DevelopContextHolder.getCurrentContext().get(CLIENT_IP);
            if (StringUtils.isNotBlank(developer)) {
                requestTemplate.header(CLIENT_IP, developer);
            }
        };
    }

    @Bean
    public RestTemplateCustomizer restTemplateCustomizer() {
        return restTemplate -> {
            List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
            list.add((request, body, execution) -> {
                final String clientIp = DevelopContextHolder.getCurrentContext().get(CLIENT_IP);
                if (StringUtils.isNotBlank(clientIp)) {
                    request.getHeaders().add(CLIENT_IP, clientIp);
                }
                return execution.execute(request, body);
            });
            restTemplate.setInterceptors(list);
        };
    }
}
  • 其他的类

public class DevelopContextHolder {

    private static final ThreadLocal<DevelopFilterContext> CONTEXT_HOLDER = new InheritableThreadLocal<DevelopFilterContext>() {
        @Override
        protected DevelopFilterContext initialValue() {
            return new DevelopFilterContext();
        }
    };

    public static DevelopFilterContext getCurrentContext() {
        return CONTEXT_HOLDER.get();
    }

    public static void setCurrentContext(DevelopFilterContext context) {
        CONTEXT_HOLDER.set(context);
    }

    public static void clearCurrentContext() {
        CONTEXT_HOLDER.remove();
    }

}



@Setter
@Getter
public class DevelopFilterContext {

    private final Map<String, String> attributes = new HashMap<>();

    public DevelopFilterContext add(String key, String value) {
        attributes.put(key, value);
        return this;
    }

    public String get(String key) {
        return attributes.get(key);
    }

    public DevelopFilterContext remove(String key) {
        attributes.remove(key);
        return this;
    }

    public Map<String, String> getAttributes() {
        return Collections.unmodifiableMap(attributes);
    }

}
  • spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.nvr.springcloud.starter.develop.DevelopAutoConfiguration

这样就可以愉快的玩耍了.

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值