springcloud+nacos實現(xiàn)灰度發(fā)布示例詳解
灰度發(fā)布
什么是灰度發(fā)布呢?要想了解這個問題就要先明白什么是灰度?;叶葟淖置嬉馑祭斫饩褪谴嬖谟诤谂c白之間的一個平滑過渡的區(qū)域,所以說對于互聯(lián)網(wǎng)產(chǎn)品來說,上線和未上線就是黑與白之分,而實現(xiàn)未上線功能平穩(wěn)過渡的一種方式就叫做灰度發(fā)布。
在一般情況下,升級服務(wù)器端應(yīng)用,需要將應(yīng)用源碼或程序包上傳到服務(wù)器,然后停止掉老版本服務(wù),再啟動新版本。但是這種簡單的發(fā)布方式存在兩個問題,一方面,在新版本升級過程中,服務(wù)是暫時中斷的,另一方面,如果新版本有BUG,升級失敗,回滾起來也非常麻煩,容易造成更長時間的服務(wù)不可用。
在了解了什么是灰度發(fā)布的定義以后,就可以來了解一下灰度發(fā)布的具體操作方法了。可以通過抽取一部分用戶,比如說選擇自己的測試用戶,使這些用戶的請求全部訪問灰度發(fā)布的服務(wù),正式用戶請求走正常上線的服務(wù),那么就能夠?qū)⑿枰叶劝l(fā)布的版本也連接到正常上線服務(wù)中進行測試,沒有問題之后將其設(shè)置為正常服務(wù)即完成版本的上線測試和發(fā)布。
gateway網(wǎng)關(guān)實現(xiàn)灰度路由
灰度發(fā)布實體
package com.scm.boss.common.bean; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; /** * 灰度發(fā)布實體 */ @Data @Accessors(chain = true) public class GrayBean implements Serializable { private static final long serialVersionUID = 1L; /** * 版本 */ private String preVersion; }
灰度發(fā)布上下文信息
package com.scm.boss.common.bean; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; /** * 灰度發(fā)布實體 */ @Data @Accessors(chain = true) public class GrayBean implements Serializable { private static final long serialVersionUID = 1L; /** * 版本 */ private String preVersion; }
灰度過濾器設(shè)置灰度上下文信息
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; /** * 灰度發(fā)布版本標(biāo)識過濾器 */ @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; } }
灰度路由規(guī)則
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 { // 根據(jù)灰度路由規(guī)則,過濾出符合規(guī)則的服務(wù) this.getServers() // 再根據(jù)負(fù)載均衡策略,過濾掉不可用和性能差的服務(wù),然后在剩下的服務(wù)中進行輪詢 getPredicate().chooseRoundRobinAfterFiltering() server = getPredicate() .chooseRoundRobinAfterFiltering(this.getServers(), key); //獲取請求頭中的版本號 GrayBean grayBean = CurrentGrayUtils.getGray(); if (null != grayBean && !StringUtils.isEmpty(grayBean.getPreVersion())) { log.info("灰度路由規(guī)則過濾后的服務(wù)實例:{}", server.isPresent() ? server.get().getHostPort() : null); } } finally { CurrentGrayUtils.clear(); } return server.isPresent() ? server.get() : null; } /** * 灰度路由過濾服務(wù)實例 * * 如果設(shè)置了期望版本, 則過濾出所有的期望版本 ,然后再走默認(rèn)的輪詢 如果沒有一個期望的版本實例,則不過濾,降級為原有的規(guī)則,進行所有的服務(wù)輪詢。(灰度路由失效) 如果沒有設(shè)置期望版本 * 則不走灰度路由,按原有輪詢機制輪詢所有 */ protected List<Server> getServers() { // 獲取spring cloud默認(rèn)負(fù)載均衡器 // 獲取所有待選的服務(wù) List<Server> allServers = getLoadBalancer().getReachableServers(); if (CollectionUtils.isEmpty(allServers)) { log.error("沒有可用的服務(wù)實例"); throw new ApiException("沒有可用的服務(wù)實例"); } //獲取請求頭中的版本號 GrayBean grayBean = CurrentGrayUtils.getGray(); // 如果沒有設(shè)置要訪問的版本,則不過濾,返回所有,走原有默認(rèn)的輪詢機制 if (null == grayBean || StringUtils.isEmpty(grayBean.getPreVersion())) { //這里需要過濾掉灰度服務(wù)實例 List<Server> list = allServers.stream().filter(f -> { // 獲取服務(wù)實例在注冊中心上的元數(shù)據(jù) Map<String, String> metadata = ((NacosServer) f).getMetadata(); // 如果注冊中心上服務(wù)的版本標(biāo)簽和期望訪問的版本一致,則灰度路由匹配成功 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; } // 開始灰度規(guī)則匹配過濾 List<Server> filterServer = new ArrayList<>(); for (Server server : allServers) { // 獲取服務(wù)實例在注冊中心上的元數(shù)據(jù) Map<String, String> metadata = ((NacosServer) server).getMetadata(); // 如果注冊中心上服務(wù)的版本標(biāo)簽和期望訪問的版本一致,則灰度路由匹配成功 if (null != metadata && grayBean.getPreVersion().equals(metadata.get(CommonConstants.GRAY_VERSION))) { filterServer.add(server); } } // 如果沒有匹配到期望的版本實例服務(wù),為了保證服務(wù)可用性,讓灰度規(guī)則失效,走原有的輪詢所有可用服務(wù)的機制 if (CollectionUtils.isEmpty(filterServer)) { log.error("灰度路由規(guī)則失效,沒有找到期望的版本實例"); throw new ApiException("沒有匹配的灰度服務(wù)實例"); } return filterServer; } }
gateway網(wǎng)關(guān)需要引入的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 { /** * 灰度請求頭參數(shù) */ String GRAY_VERSION = "grayVersion"; /** * 灰度版本值 */ String GRAY_VERSION_VALUE = "V1"; }
微服務(wù)feign調(diào)用灰度
服務(wù)路由規(guī)則
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) { // 根據(jù)灰度路由規(guī)則,過濾出符合規(guī)則的服務(wù) this.getServers() // 再根據(jù)負(fù)載均衡策略,過濾掉不可用和性能差的服務(wù),然后在剩下的服務(wù)中進行輪詢 getPredicate().chooseRoundRobinAfterFiltering() Optional<Server> server = getPredicate() .chooseRoundRobinAfterFiltering(this.getServers(), key); //獲取請求頭中的版本號 GrayBean grayBean = CurrentGrayUtils.getGray(); if (null != grayBean && !StringUtils.isEmpty(grayBean.getPreVersion())) { log.info("灰度路由規(guī)則過濾后的服務(wù)實例:{}", server.isPresent() ? server.get().getHostPort() : null); } return server.isPresent() ? server.get() : null; } /** * 灰度路由過濾服務(wù)實例 * * 如果設(shè)置了期望版本, 則過濾出所有的期望版本 ,然后再走默認(rèn)的輪詢 如果沒有一個期望的版本實例,則不過濾,降級為原有的規(guī)則,進行所有的服務(wù)輪詢。(灰度路由失效) 如果沒有設(shè)置期望版本 * 則不走灰度路由,按原有輪詢機制輪詢所有 */ protected List<Server> getServers() { // 獲取spring cloud默認(rèn)負(fù)載均衡器 // 獲取所有待選的服務(wù) List<Server> allServers = getLoadBalancer().getReachableServers(); if (CollectionUtils.isEmpty(allServers)) { log.error("沒有可用的服務(wù)實例"); throw new ApiException("沒有可用的服務(wù)實例"); } //獲取請求頭中的版本號 GrayBean grayBean = CurrentGrayUtils.getGray(); // 如果沒有設(shè)置要訪問的版本,則不過濾,返回所有,走原有默認(rèn)的輪詢機制 if (null == grayBean || StringUtils.isEmpty(grayBean.getPreVersion())) { //這里需要過濾掉灰度服務(wù)實例 List<Server> list = allServers.stream().filter(f -> { // 獲取服務(wù)實例在注冊中心上的元數(shù)據(jù) Map<String, String> metadata = ((NacosServer) f).getMetadata(); // 如果注冊中心上服務(wù)的版本標(biāo)簽和期望訪問的版本一致,則灰度路由匹配成功 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; } // 開始灰度規(guī)則匹配過濾 List<Server> filterServer = new ArrayList<>(); for (Server server : allServers) { // 獲取服務(wù)實例在注冊中心上的元數(shù)據(jù) Map<String, String> metadata = ((NacosServer) server).getMetadata(); // 如果注冊中心上服務(wù)的版本標(biāo)簽和期望訪問的版本一致,則灰度路由匹配成功 if (null != metadata && grayBean.getPreVersion().equals(metadata.get(CommonConstants.GRAY_VERSION))) { filterServer.add(server); } } // 如果沒有匹配到期望的版本實例服務(wù),為了保證服務(wù)可用性,讓灰度規(guī)則失效,走原有的輪詢所有可用服務(wù)的機制 if (CollectionUtils.isEmpty(filterServer)) { log.error("灰度路由規(guī)則失效,沒有找到期望的版本實例,version={}", grayBean.getPreVersion()); throw new ApiException("灰度路由規(guī)則失效,沒有找到期望的版本實例"); } return filterServer; } }
需要傳遞灰度版本號,所以需要把灰度版本請求參數(shù)傳遞下去,以及解決Hystrix的線程切換導(dǎo)致參數(shù)無法傳遞下的問題
使用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 { /** * 低優(yōu)先級 */ private final static int LOWER_PRECEDENCE = 10000; /** * 使用TransmittableThreadLocal可以跨線程傳遞 */ @PostConstruct public void init(){ new GlobalHystrixConcurrencyStrategy(); } @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcCommonConfigurer(); } /** * 優(yōu)先級 * @return */ @Bean @ConditionalOnMissingBean @Order(value = LOWER_PRECEDENCE) public SecurityInterceptor securityInterceptor(){ return new DefaultSecurityInterceptor(); } }
bean重復(fù)則覆蓋
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調(diào)用攔截器
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 客戶端的日志記錄,默認(rèn)級別為NONE * Logger.Level 的具體級別如下: * NONE:不記錄任何信息 * BASIC:僅記錄請求方法、URL以及響應(yīng)狀態(tài)碼和執(zhí)行時間 * HEADERS:除了記錄 BASIC級別的信息外,還會記錄請求和響應(yīng)的頭信息 * FULL:記錄所有請求與響應(yīng)的明細(xì),包括頭信息、請求體、元數(shù)據(jù) */ @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并發(fā)策略
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并發(fā)參數(shù)線程中傳遞參數(shù)
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 { /** * 創(chuàng)建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); } /** * 灰度發(fā)布 * @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); } } /** * 第三方經(jīng)銷商調(diào)用 * @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); } /** * 獲取接口無權(quán)限字段 * @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/**"); } }
特殊數(shù)據(jù)權(quán)限過濾
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; /** * 特殊數(shù)據(jù)權(quán)限過濾 * @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); } /** * 權(quán)限數(shù)據(jù)處理 * @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 ) + "\""); } }
微服務(wù)的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
微服務(wù)的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>
到此這篇關(guān)于springcloud+nacos實現(xiàn)灰度發(fā)布的文章就介紹到這了,更多相關(guān)springcloud灰度發(fā)布內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springCloud集成nacos config的過程
- SpringCloud-Nacos服務(wù)注冊與發(fā)現(xiàn)方式
- SpringCloud連接不上遠(yuǎn)程Nacos問題排查
- springcloud nacos的賦值均衡和動態(tài)刷新
- Alibaba?SpringCloud集成Nacos、openFeign實現(xiàn)負(fù)載均衡的解決方案
- SpringCloud Nacos集群搭建過程詳解
- SpringCloud及Nacos服務(wù)注冊IP選擇問題解決方法
- springCloud集成nacos啟動時報錯原因排查
- Spring Cloud Nacos配置管理方案
相關(guān)文章
Spring Boot中使用JSR-303實現(xiàn)請求參數(shù)校驗
這篇文章主要介紹了Spring Boot中使用JSR-303實現(xiàn)請求參數(shù)校驗,JSR-303校驗我們一般都是對Java的實體類對象進行校驗,主要檢驗JSR-303是Java中的一個規(guī)范,用于實現(xiàn)請求參數(shù)校驗在我們的實體類對象的屬性上,感興趣的朋友跟隨小編一起看看吧2023-10-10MybatisPlus分頁查詢與多條件查詢介紹及查詢過程中空值問題的解決
mybatisplus是個很好用的插件,相信小伙伴們都知道,下面這篇文章主要給大家介紹了關(guān)于mybatis-plus實現(xiàn)分頁查詢與多條件查詢介紹及查詢過程中空值問題的相關(guān)資料,需要的朋友可以參考下2022-10-10詳解如何把Java中if-else代碼重構(gòu)成高質(zhì)量代碼
這篇文章主要介紹了詳解如何把Java中if-else代碼重構(gòu)成高質(zhì)量代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11詳談Java中Object類中的方法以及finalize函數(shù)作用
下面小編就為大家?guī)硪黄斦凧ava中Object類中的方法以及finalize函數(shù)作用。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04springboot根據(jù)實體類生成表的實現(xiàn)方法
本文介紹了如何通過SpringBoot工程引入SpringDataJPA,并通過實體類自動生成數(shù)據(jù)庫表的過程,包括常見問題解決方法,感興趣的可以了解一下2024-09-09Spring Boot中的@ConfigurationProperties注解解讀
在SpringBoot框架中,@ConfigurationProperties注解是處理外部配置的強大工具,它允許開發(fā)者將配置文件中的屬性自動映射到Java類的字段上,實現(xiàn)配置的集中管理和類型安全,通過定義配置類并指定前綴,可以將配置文件中的屬性綁定到Java對象2024-10-10