springcloud+nacos实现灰度发布示例详解

 更新时间:2023年08月16日 11:40:16   作者:weixin_42181142  
这篇文章主要介绍了springcloud+nacos实现灰度发布,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

灰度发布

什么是灰度发布呢?要想了解这个问题就要先明白什么是灰度。灰度从字面意思理解就是存在于黑与白之间的一个平滑过渡的区域,所以说对于互联网产品来说,上线和未上线就是黑与白之分,而实现未上线功能平稳过渡的一种方式就叫做灰度发布。

在一般情况下,升级服务器端应用,需要将应用源码或程序包上传到服务器,然后停止掉老版本服务,再启动新版本。但是这种简单的发布方式存在两个问题,一方面,在新版本升级过程中,服务是暂时中断的,另一方面,如果新版本有BUG,升级失败,回滚起来也非常麻烦,容易造成更长时间的服务不可用。

在了解了什么是灰度发布的定义以后,就可以来了解一下灰度发布的具体操作方法了。可以通过抽取一部分用户,比如说选择自己的测试用户,使这些用户的请求全部访问灰度发布的服务,正式用户请求走正常上线的服务,那么就能够将需要灰度发布的版本也连接到正常上线服务中进行测试,没有问题之后将其设置为正常服务即完成版本的上线测试和发布。

gateway网关实现灰度路由

灰度发布实体

package com.scm.boss.common.bean;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
 * 灰度发布实体
 */
@Data
@Accessors(chain = true)
public class GrayBean implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 版本
     */
    private String preVersion;
}

灰度发布上下文信息

package com.scm.boss.common.bean;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
 * 灰度发布实体
 */
@Data
@Accessors(chain = true)
public class GrayBean implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 版本
     */
    private String preVersion;
}

灰度过滤器设置灰度上下文信息

package com.scm.gateway.common.config;
import com.scm.boss.common.bean.GrayBean;
import com.scm.boss.common.constants.CommonConstants;
import com.scm.boss.common.utils.CurrentGrayUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
 * 灰度发布版本标识过滤器
 */
@Slf4j
public class GrayFilter implements GlobalFilter, Ordered {
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
		String grayVersion = httpHeaders.getFirst(CommonConstants.GRAY_VERSION);
		if (StringUtils.isNotBlank(grayVersion)) {
			GrayBean grayBean = new GrayBean();
			grayBean.setPreVersion(grayVersion);
			CurrentGrayUtils.setGray(grayBean);
			//请求头添加灰度版本号,用于灰度请求
			exchange.getRequest().mutate()
					.header(CommonConstants.GRAY_VERSION, grayVersion)
					.build();
		}
		return chain.filter(exchange);
	}
	@Override
	public int getOrder() {
		return Integer.MIN_VALUE;
	}
}

灰度路由规则

package com.scm.gateway.common.config;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.google.common.base.Optional;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.scm.boss.common.bean.GrayBean;
import com.scm.boss.common.constants.CommonConstants;
import com.scm.boss.common.exception.ApiException;
import com.scm.boss.common.utils.CurrentGrayUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Component
public class GateWayGrayRouteRule extends ZoneAvoidanceRule {
    @Override
    public Server choose(Object key) {
        Optional<Server> server;
        try {
            // 根据灰度路由规则,过滤出符合规则的服务 this.getServers()
            // 再根据负载均衡策略,过滤掉不可用和性能差的服务,然后在剩下的服务中进行轮询  getPredicate().chooseRoundRobinAfterFiltering()
            server = getPredicate()
                    .chooseRoundRobinAfterFiltering(this.getServers(), key);
            //获取请求头中的版本号
            GrayBean grayBean = CurrentGrayUtils.getGray();
            if (null != grayBean && !StringUtils.isEmpty(grayBean.getPreVersion())) {
                log.info("灰度路由规则过滤后的服务实例:{}", server.isPresent() ? server.get().getHostPort() : null);
            }
        } finally {
            CurrentGrayUtils.clear();
        }
        return server.isPresent() ? server.get() : null;
    }
    /**
     * 灰度路由过滤服务实例
     *
     * 如果设置了期望版本, 则过滤出所有的期望版本 ,然后再走默认的轮询 如果没有一个期望的版本实例,则不过滤,降级为原有的规则,进行所有的服务轮询。(灰度路由失效) 如果没有设置期望版本
     * 则不走灰度路由,按原有轮询机制轮询所有
     */
    protected List<Server> getServers() {
        // 获取spring cloud默认负载均衡器
        // 获取所有待选的服务
        List<Server> allServers = getLoadBalancer().getReachableServers();
        if (CollectionUtils.isEmpty(allServers)) {
            log.error("没有可用的服务实例");
            throw new ApiException("没有可用的服务实例");
        }
        //获取请求头中的版本号
        GrayBean grayBean = CurrentGrayUtils.getGray();
        // 如果没有设置要访问的版本,则不过滤,返回所有,走原有默认的轮询机制
        if (null == grayBean || StringUtils.isEmpty(grayBean.getPreVersion())) {
            //这里需要过滤掉灰度服务实例
            List<Server> list = allServers.stream().filter(f -> {
                // 获取服务实例在注册中心上的元数据
                Map<String, String> metadata = ((NacosServer) f).getMetadata();
                // 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
                if ((null != metadata && StringUtils.isNotBlank(metadata.get(CommonConstants.GRAY_VERSION)))
                        || CommonConstants.GRAY_VERSION_VALUE.equals(metadata.get(CommonConstants.GRAY_VERSION))) {
                    return false;
                }
                return true;
            }).collect(Collectors.toList());
            return list;
        }
        // 开始灰度规则匹配过滤
        List<Server> filterServer = new ArrayList<>();
        for (Server server : allServers) {
            // 获取服务实例在注册中心上的元数据
            Map<String, String> metadata = ((NacosServer) server).getMetadata();
            // 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
            if (null != metadata && grayBean.getPreVersion().equals(metadata.get(CommonConstants.GRAY_VERSION))) {
                filterServer.add(server);
            }
        }
        // 如果没有匹配到期望的版本实例服务,为了保证服务可用性,让灰度规则失效,走原有的轮询所有可用服务的机制
        if (CollectionUtils.isEmpty(filterServer)) {
            log.error("灰度路由规则失效,没有找到期望的版本实例");
            throw new ApiException("没有匹配的灰度服务实例");
        }
        return filterServer;
    }
}

gateway网关需要引入的pom

 <dependencies>
        <!-- Nacos注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- Nacos配置中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!-- gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.scm</groupId>
            <artifactId>scm-common-boss</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-netflix-ribbon</artifactId>
        </dependency>
    </dependencies>

常量

package com.scm.boss.common.constants;
public interface CommonConstants {
	 /**
	     * 灰度请求头参数
	     */
	    String GRAY_VERSION = "grayVersion";
	    /**
	     * 灰度版本值
	     */
	    String GRAY_VERSION_VALUE = "V1";
    }

微服务feign调用灰度

服务路由规则

package com.scm.cloud.config;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.google.common.base.Optional;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.scm.boss.common.bean.GrayBean;
import com.scm.boss.common.constants.CommonConstants;
import com.scm.boss.common.exception.ApiException;
import com.scm.boss.common.utils.CurrentGrayUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
public class GrayRouteRule extends ZoneAvoidanceRule {
    @Override
    public Server choose(Object key) {
        // 根据灰度路由规则,过滤出符合规则的服务 this.getServers()
        // 再根据负载均衡策略,过滤掉不可用和性能差的服务,然后在剩下的服务中进行轮询  getPredicate().chooseRoundRobinAfterFiltering()
        Optional<Server> server = getPredicate()
                .chooseRoundRobinAfterFiltering(this.getServers(), key);
        //获取请求头中的版本号
        GrayBean grayBean = CurrentGrayUtils.getGray();
        if (null != grayBean && !StringUtils.isEmpty(grayBean.getPreVersion())) {
            log.info("灰度路由规则过滤后的服务实例:{}", server.isPresent() ? server.get().getHostPort() : null);
        }
        return server.isPresent() ? server.get() : null;
    }
    /**
     * 灰度路由过滤服务实例
     *
     * 如果设置了期望版本, 则过滤出所有的期望版本 ,然后再走默认的轮询 如果没有一个期望的版本实例,则不过滤,降级为原有的规则,进行所有的服务轮询。(灰度路由失效) 如果没有设置期望版本
     * 则不走灰度路由,按原有轮询机制轮询所有
     */
    protected List<Server> getServers() {
        // 获取spring cloud默认负载均衡器
        // 获取所有待选的服务
        List<Server> allServers = getLoadBalancer().getReachableServers();
        if (CollectionUtils.isEmpty(allServers)) {
            log.error("没有可用的服务实例");
            throw new ApiException("没有可用的服务实例");
        }
        //获取请求头中的版本号
        GrayBean grayBean = CurrentGrayUtils.getGray();
        // 如果没有设置要访问的版本,则不过滤,返回所有,走原有默认的轮询机制
        if (null == grayBean || StringUtils.isEmpty(grayBean.getPreVersion())) {
            //这里需要过滤掉灰度服务实例
            List<Server> list = allServers.stream().filter(f -> {
                // 获取服务实例在注册中心上的元数据
                Map<String, String> metadata = ((NacosServer) f).getMetadata();
                // 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
                if ((null != metadata && StringUtils.isNotBlank(metadata.get(CommonConstants.GRAY_VERSION)))
                        || CommonConstants.GRAY_VERSION_VALUE.equals(metadata.get(CommonConstants.GRAY_VERSION))) {
                    return false;
                }
                return true;
            }).collect(Collectors.toList());
            return list;
        }
        // 开始灰度规则匹配过滤
        List<Server> filterServer = new ArrayList<>();
        for (Server server : allServers) {
            // 获取服务实例在注册中心上的元数据
            Map<String, String> metadata = ((NacosServer) server).getMetadata();
            // 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
            if (null != metadata && grayBean.getPreVersion().equals(metadata.get(CommonConstants.GRAY_VERSION))) {
                filterServer.add(server);
            }
        }
        // 如果没有匹配到期望的版本实例服务,为了保证服务可用性,让灰度规则失效,走原有的轮询所有可用服务的机制
        if (CollectionUtils.isEmpty(filterServer)) {
            log.error("灰度路由规则失效,没有找到期望的版本实例,version={}", grayBean.getPreVersion());
            throw new ApiException("灰度路由规则失效,没有找到期望的版本实例");
        }
        return filterServer;
    }
}

需要传递灰度版本号,所以需要把灰度版本请求参数传递下去,以及解决Hystrix的线程切换导致参数无法传递下的问题

使用TransmittableThreadLocal可以跨线程传递

package com.scm.cloud.config;
import com.scm.cloud.security.DefaultSecurityInterceptor;
import com.scm.cloud.security.SecurityInterceptor;
import com.scm.cloud.webmvc.WebMvcCommonConfigurer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.PostConstruct;
/**
 * 配置
 * @date 2023/7/13 18:12
 * @author luohao
 */
@Configuration
@Slf4j
public class CommonConfiguration {
    /**
     * 低优先级
     */
    private final static int LOWER_PRECEDENCE = 10000;
    /**
     * 使用TransmittableThreadLocal可以跨线程传递
     */
    @PostConstruct
    public void init(){
        new GlobalHystrixConcurrencyStrategy();
    }
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcCommonConfigurer();
    }
    /**
     * 优先级
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    @Order(value = LOWER_PRECEDENCE)
    public SecurityInterceptor securityInterceptor(){
        return new DefaultSecurityInterceptor();
    }
}

bean重复则覆盖

package com.scm.cloud.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
/**
 * @author xiewu
 * @date 2022/12/29 10:41
 */
public class EnvironmentPostProcessorConfig implements EnvironmentPostProcessor {
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        application.setAllowBeanDefinitionOverriding(true);
    }
}

feign调用拦截器

package com.scm.cloud.config;
import com.scm.boss.common.bean.CurrentUserBean;
import com.scm.boss.common.bean.DealerApiDetailBean;
import com.scm.boss.common.bean.GrayBean;
import com.scm.boss.common.constants.CommonConstants;
import com.scm.boss.common.utils.CurrentGrayUtils;
import com.scm.boss.common.utils.CurrentUserUtils;
import com.scm.boss.common.utils.CurrentDealerApiDetailUtils;
import feign.Feign;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@Slf4j
@Configuration
public class FeignConfig {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                GrayBean grayBean = CurrentGrayUtils.getGray();
                if (null != grayBean) {
                    requestTemplate.header(CommonConstants.GRAY_VERSION, grayBean.getPreVersion());
                }
                DealerApiDetailBean dealerApiDetailBean = CurrentDealerApiDetailUtils.getDealerApiConditionNull();
                if (dealerApiDetailBean != null){
                    requestTemplate.header(CommonConstants.DEALER_ID, dealerApiDetailBean.getDealerId());
                    requestTemplate.header(CommonConstants.DEALER_PROJECT_ID, dealerApiDetailBean.getDealerProjectId());
                }
                CurrentUserBean currentUser = CurrentUserUtils.getCurrentUserConditionNull();
                if (currentUser == null){
                    return;
                }
                requestTemplate.header(CommonConstants.SUPPLIER_ID, currentUser.getSupplierId() == null ? null : currentUser.getId().toString());
                requestTemplate.header(CommonConstants.ACCOUNT_NO, currentUser.getAccountNo());
                requestTemplate.header(CommonConstants.REQUEST_SOURCE, currentUser.getType());
                requestTemplate.header(CommonConstants.ID, currentUser.getId() == null ? null : currentUser.getId().toString());
            }
        };
    }
    /**
     * Feign 客户端的日志记录,默认级别为NONE
     * Logger.Level 的具体级别如下:
     * NONE:不记录任何信息
     * BASIC:仅记录请求方法、URL以及响应状态码和执行时间
     * HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
     * FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
    /**
     * Feign支持文件上传
     *
     * @param messageConverters
     * @return
     */
    @Bean
    @Primary
    @Scope("prototype")
    public Encoder multipartFormEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }
}

Hystrix并发策略

package com.scm.cloud.config;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
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.scm.boss.common.bean.CurrentUserBean;
import com.scm.boss.common.bean.DealerApiDetailBean;
import com.scm.boss.common.bean.GrayBean;
import com.scm.boss.common.utils.CurrentGrayUtils;
import com.scm.boss.common.utils.CurrentUserUtils;
import com.scm.boss.common.utils.CurrentDealerApiDetailUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
@Slf4j
public class GlobalHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
    private HystrixConcurrencyStrategy delegate;
    public GlobalHystrixConcurrencyStrategy() {
        this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
        if (this.delegate instanceof GlobalHystrixConcurrencyStrategy) {
            return;
        }
        HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
        HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy();
        HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance().getCommandExecutionHook();
        HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
        HystrixPlugins.reset();
        HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
        // Registers existing plugins except the new MicroMeter Strategy plugin.
        HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
        HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
        HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
        HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
        log.info("Construct HystrixConcurrencyStrategy:[{}] for application,",GlobalHystrixConcurrencyStrategy.class.getName());
    }
    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        final CurrentUserBean user = CurrentUserUtils.getCurrentUserConditionNull();
        final DealerApiDetailBean dealerApiDetailBean = CurrentDealerApiDetailUtils.getDealerApiConditionNull();
        final GrayBean grayBean = CurrentGrayUtils.getGray();
        if (callable instanceof HeaderCallable) {
            return callable;
        }
        Callable<T> wrappedCallable = this.delegate != null
                ? this.delegate.wrapCallable(callable) : callable;
        if (wrappedCallable instanceof HeaderCallable) {
            return wrappedCallable;
        }
        return new HeaderCallable<T>(wrappedCallable,user,dealerApiDetailBean, grayBean);
    }
}

Hystrix并发参数线程中传递参数

package com.scm.cloud.config;
import com.scm.boss.common.bean.CurrentUserBean;
import com.scm.boss.common.bean.DealerApiDetailBean;
import com.scm.boss.common.bean.GrayBean;
import com.scm.boss.common.utils.CurrentGrayUtils;
import com.scm.boss.common.utils.CurrentUserUtils;
import com.scm.boss.common.utils.CurrentDealerApiDetailUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
@Slf4j
public class HeaderCallable<V> implements Callable<V> {
    private final Callable<V> delegate;
    private final CurrentUserBean currentUserBean;
    private final DealerApiDetailBean dealerApiDetailBean;
    private final GrayBean grayBean;
    public HeaderCallable(Callable<V> delegate, CurrentUserBean currentUserBean, DealerApiDetailBean dealerApiDetailBean, GrayBean grayBean) {
        this.delegate = delegate;
        this.currentUserBean = currentUserBean;
        this.dealerApiDetailBean = dealerApiDetailBean;
        this.grayBean = grayBean;
    }
    @Override
    public V call() throws Exception {
        try {
            CurrentUserUtils.setCurrentUser(currentUserBean);
            CurrentDealerApiDetailUtils.setDealerApi(dealerApiDetailBean);
            CurrentGrayUtils.setGray(grayBean);
            return this.delegate.call();
        } catch (Exception e) {
            //这里无法抓取到delegate.call()方法的异常,因为是线程池异步请求的
            throw e;
        } finally {
            CurrentUserUtils.clear();
            CurrentGrayUtils.clear();
            CurrentDealerApiDetailUtils.clear();
        }
    }
}

LoadBalancerFeignClient

package com.scm.cloud.config;
import feign.Client;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PersonBeanConfiguration {
    /**
     * 创建FeignClient
     */
    @Bean
    @ConditionalOnMissingBean
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                              SpringClientFactory clientFactory) {
        return new LoadBalancerFeignClient(new Client.Default(null, null),
                cachingFactory, clientFactory);
    }
}

拦截器HandlerInterceptor

package com.scm.cloud.webmvc;
import com.alibaba.fastjson.JSONArray;
import com.scm.boss.common.bean.CurrentUserBean;
import com.scm.boss.common.bean.DealerApiDetailBean;
import com.scm.boss.common.bean.GrayBean;
import com.scm.boss.common.bean.RouteAttrPermVO;
import com.scm.boss.common.constants.CommonConstants;
import com.scm.boss.common.constants.PlatformTypeEnum;
import com.scm.boss.common.constants.UserTypeEnum;
import com.scm.boss.common.utils.CurrentDealerApiDetailUtils;
import com.scm.boss.common.utils.CurrentGrayUtils;
import com.scm.boss.common.utils.CurrentUserUtils;
import com.scm.boss.common.utils.FieldListUtils;
import com.scm.redis.template.RedisRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
 * 拦截器
 * @date 2023/7/13 18:09
 * @author luohao
 */
@Slf4j
public class GlobalHandlerInterceptor implements HandlerInterceptor {
    private RedisRepository redisRepository;
    public GlobalHandlerInterceptor(RedisRepository redisRepository) {
        this.redisRepository = redisRepository;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        extractedHeadersGre(request);
        extractedHeaders(request);
        extractedHeadersApi(request);
        extractedPermissionFields(request);
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
    /**
     * 灰度发布
     * @param request
     */
    private void extractedHeadersGre(HttpServletRequest request) {
        String grayVersion = request.getHeader(CommonConstants.GRAY_VERSION);
        if (StringUtils.isNotBlank(grayVersion)) {
            GrayBean grayBean = new GrayBean();
            grayBean.setPreVersion(grayVersion);
            CurrentGrayUtils.setGray(grayBean);
        }
    }
    /**
     * 第三方经销商调用
     * @param request
     */
    private void extractedHeadersApi(HttpServletRequest request) {
        DealerApiDetailBean dealerApiDetailBean = new DealerApiDetailBean();
        dealerApiDetailBean.setDealerId(request.getHeader(CommonConstants.DEALER_ID))
                .setDealerProjectId(request.getHeader(CommonConstants.DEALER_PROJECT_ID));
        CurrentDealerApiDetailUtils.setDealerApi(dealerApiDetailBean);
    }
    private void extractedHeaders(HttpServletRequest request) {
        CurrentUserBean currentUserBean = new CurrentUserBean();
        currentUserBean.setAccountNo(request.getHeader(CommonConstants.ACCOUNT_NO));
        currentUserBean.setType(request.getHeader(CommonConstants.REQUEST_SOURCE));
        currentUserBean.setStatus(request.getHeader(CommonConstants.STATUS) == null ? null : Integer.valueOf(request.getHeader(CommonConstants.STATUS)));
        currentUserBean.setId(request.getHeader(CommonConstants.ID) == null ? null : Integer.valueOf(request.getHeader(CommonConstants.ID)));
        if (UserTypeEnum.SUPPLIER_USER.getCode().equals(currentUserBean.getType())) {
            currentUserBean.setSupplierId(request.getHeader(CommonConstants.SUPPLIER_ID) == null ? null : Integer.valueOf(request.getHeader(CommonConstants.SUPPLIER_ID)));
        }
        CurrentUserUtils.setCurrentUser(currentUserBean);
    }
    /**
     * 获取接口无权限字段
     * @date 2023/7/13 16:41
     * @author luohao
     */
    private void extractedPermissionFields(HttpServletRequest request){
        String requestMapping = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).toString();
        CurrentUserBean currentUser = CurrentUserUtils.getCurrentUser();
        if(Objects.isNull(currentUser) || Objects.isNull(currentUser.getAccountNo())){
            return;
        }
        String key;
        if(currentUser.getType().equals(PlatformTypeEnum.APPLY_CHAIN.getCode().toString())){
            key = CommonConstants.SUPPLY_CHAIN_ATTR;
        }else if(currentUser.getType().equals(PlatformTypeEnum.DEALER.getCode().toString())){
            key = CommonConstants.DEALER_ATTR;
        }else{
            return;
        }
        String redisKey = new StringBuilder(key).append(currentUser.getAccountNo()).toString();
        List<RouteAttrPermVO> spuEditDTO = JSONArray.parseArray(redisRepository.get(redisKey), RouteAttrPermVO.class);
        if(CollectionUtils.isEmpty(spuEditDTO)){
            return;
        }
        List<String> nonPermAttrs = spuEditDTO.stream().filter(i -> i.getUrl().equals(requestMapping)).map(RouteAttrPermVO::getAttrName).collect(Collectors.toList());
        FieldListUtils.setFieldList(nonPermAttrs);
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        CurrentUserUtils.clear();
        FieldListUtils.clear();
    }
}

WebMvcConfigurer

package com.scm.cloud.webmvc;
import com.scm.redis.template.RedisRepository;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
 * WebMvc
 * @date 2023/7/13 18:11
 * @author luohao
 */
public class WebMvcCommonConfigurer implements WebMvcConfigurer {
    @Resource
    private RedisRepository redisRepository;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new GlobalHandlerInterceptor(redisRepository)).addPathPatterns("/**").excludePathPatterns("/info","/actuator/**");
    }
}

特殊数据权限过滤

package com.scm.cloud.webmvc;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.JSONSerializer;
import com.alibaba.fastjson.serializer.ObjectSerializer;
import com.alibaba.fastjson.serializer.SerializeConfig;
import com.alibaba.fastjson.serializer.SerializeWriter;
import com.scm.boss.common.utils.FieldListUtils;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.io.IOException;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 特殊数据权限过滤
 * @date 2023/7/12 14:54
 * @author luohao
 */
@Component
@RestControllerAdvice
public class BaseGlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }
    @Override
    public Object beforeBodyWrite(final Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if(ObjectUtils.isEmpty(body)){
            return body;
        }
        List<String> fieldList = FieldListUtils.getFieldList();
        if(CollectionUtils.isEmpty(fieldList)){
            return body;
        }
        SerializeConfig config = new SerializeConfig();
        config.put( Date.class, new DateJsonSerializer());
        return objectEval(JSONObject.parseObject(JSON.toJSONString(body,config)), fieldList);
    }
    /**
     * 权限数据处理
     * @param body
     * @param nonPermAttrs
     * @return
     */
    public Object objectEval(Object body, List<String> nonPermAttrs) {
        if (Objects.nonNull(body) && body instanceof Map) {
            Map<String, Object> map = (Map<String, Object>) body;
            map.keySet().forEach(key -> {
                Object o = map.get(key);
                if (Objects.nonNull(o) && o instanceof Map) {
                    map.put(key, objectEval(o, nonPermAttrs));
                } else if (Objects.nonNull(o) && o instanceof List){
                    map.put(key, objectEval(o, nonPermAttrs));
                }else {
                    List<String> collect = nonPermAttrs.stream().filter(i -> i.equals(key)).collect(Collectors.toList());
                    if (CollectionUtils.isNotEmpty(collect)){
                        map.put(key, null);
                    }
                }
            });
        } else if (Objects.nonNull(body) && body instanceof List) {
            final List<Object> dataList = (List<Object>) body;
            dataList.forEach(i -> objectEval(i,nonPermAttrs));
        }
        return body;
    }
}
class DateJsonSerializer implements ObjectSerializer {
    @Override
    public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
        SerializeWriter out = serializer.getWriter();
        if (object == null) {
            serializer.getWriter().writeNull();
            return;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone( TimeZone.getTimeZone("Etc/GMT-8"));
        out.write("\"" + sdf.format( (Date) object ) + "\"");
    }
}

微服务的spring.factories配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.scm.cloud.config.FeignConfig,\
com.scm.cloud.config.PersonBeanConfiguration,\
com.scm.cloud.webmvc.BaseGlobalResponseBodyAdvice,\
com.scm.cloud.config.CommonConfiguration,\
com.scm.cloud.config.GrayRouteRule
org.springframework.boot.env.EnvironmentPostProcessor = com.scm.cloud.config.EnvironmentPostProcessorConfig

微服务的pom文件

<dependencies>
        <!-- Nacos注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- Nacos配置中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!-- feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.scm</groupId>
            <artifactId>scm-starter-redis</artifactId>
            <version>${project.version}</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

到此这篇关于springcloud+nacos实现灰度发布的文章就介绍到这了,更多相关springcloud灰度发布内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java框架Struts2实现图片上传功能

    Java框架Struts2实现图片上传功能

    这篇文章主要为大家详细介绍了Java框架Struts2实现图片上传功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08
  • 解决mybatisPlus 中的field-strategy配置失效问题

    解决mybatisPlus 中的field-strategy配置失效问题

    这篇文章主要介绍了解决mybatisPlus 中的field-strategy配置失效问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • Java使用Scala实现尾递归优化来解决爆栈问题

    Java使用Scala实现尾递归优化来解决爆栈问题

    Scala 作为一种多范式的编程语言,结合了面向对象和函数式编程的特性,在 Scala 中,尾递归 是通过编译器优化来防止栈溢出问题的,尾递归优化是一种特殊的优化方式,可以让递归调用不使用新的栈帧,所以本文介绍了在Java项目中如何使用Scala实现尾递归优化来解决爆栈问题
    2024-10-10
  • Spring的CorsFilter会失效的原因及解决方法

    Spring的CorsFilter会失效的原因及解决方法

    众所周知CorsFilter是Spring提供的跨域过滤器,我们可能会做以下的配置,基本上就是允许任何跨域请求,我利用Spring的CorsFilter做跨域操作但是出现报错,接下来小编就给大家介绍一Spring的CorsFilter会失效的原因及解决方法,需要的朋友可以参考下
    2023-09-09
  • Java Thread多线程全面解析

    Java Thread多线程全面解析

    这篇文章主要介绍了Java Thread多线程全面解析的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-06-06
  • springcloud干货之服务注册与发现(Eureka)

    springcloud干货之服务注册与发现(Eureka)

    这篇文章主要介绍了springcloud干货之服务注册与发现(Eureka) ,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • java远程调用接口、URL的方式代码

    java远程调用接口、URL的方式代码

    我们都知道接口有自己本地的,也有远程别人写好的,而调用远程接口的就需要使用远程调用啦,这篇文章主要给大家介绍了关于java远程调用接口、URL的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-11-11
  • jackson 实体转json 为NULL或者为空不参加序列化(实例讲解)

    jackson 实体转json 为NULL或者为空不参加序列化(实例讲解)

    下面小编就为大家带来一篇jackson 实体转json 为NULL或者为空不参加序列化(实例讲解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • SpringBoot应用启动慢的原因分析及优化方法

    SpringBoot应用启动慢的原因分析及优化方法

    在使用Spring Boot进行开发时,快速启动应用程序是一个非常重要的需求,然而,在某些情况下,我们会遇到Spring Boot应用启动缓慢的问题,本文将分析Spring Boot应用启动慢的常见原因,并提供一些优化方法,需要的朋友可以参考下
    2024-08-08
  • Window搭建部署RocketMQ步骤详解

    Window搭建部署RocketMQ步骤详解

    这篇文章主要介绍了Window搭建部署RocketMQ步骤详解,RocketMq是一个由阿里巴巴开源的消息中间件,脱胎去阿里每部使用的MetaQ,在设计上借鉴了Kafka。,需要的朋友可以参考下
    2019-06-06

最新评论