本地开发
如果是SpringCloud项目一般开发本地会启动很多服务.像注册/配置中心.网关.各个服务模块. 开发效率极其低下. 思考这些公用的模块都可以搞到开发环境中去. 在网关和服务调用时做下调用隔离即可. 这样本地开发时只需要关注自己开发的一个模块即可.
流程分析.
- 建立公用的gateway应用.部署到开发环境.开发人员本地开发请求流量都经过gateway
- 在网关处使用拦截器将开发者的ip记录到ThreadLocal中和请求头中
- 在自定义的
LoadBalancer
组件中.对比注册的服务ip是否为开发者的ip.并进行进行路由 - 请求到服务层是使用拦截器获取到请求头中的ip,再次放入ThreadLocal中和请求头中
- 当服务间调用使用拦截器透传客户端ip到服务间请求的request header中
- 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
这样就可以愉快的玩耍了.