灰度發(fā)布
gateway網(wǎng)關(guān)實(shí)現(xiàn)灰度路由
灰度發(fā)布實(shí)體
package com.scm.boss.common.bean;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 灰度發(fā)布實(shí)體
*/
@Data
@Accessors(chain = true)
public class GrayBean implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 版本
*/
private String preVersion;
}
灰度發(fā)布上下文信息
package com.scm.boss.common.utils;
import com.scm.boss.common.bean.GrayBean;
/**
* 灰度信息上下文
*/
public class CurrentGrayUtils {
private final static InheritableThreadLocal<GrayBean> CURRENT_GRE = new InheritableThreadLocal<>();
public static GrayBean getGray() {
GrayBean grayBean = CURRENT_GRE.get();
return grayBean;
}
public static void setGray(GrayBean grayBean) {
if(grayBean == null){
clear();
}else {
CURRENT_GRE.set(grayBean);
}
}
public static void clear() {
CURRENT_GRE.remove();
}
}
灰度過濾器設(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)識(shí)過濾器
*/
@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);
//請(qǐng)求頭添加灰度版本號(hào),用于灰度請(qǐng)求
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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
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ù)中進(jìn)行輪詢 getPredicate().chooseRoundRobinAfterFiltering()
server = getPredicate()
.chooseRoundRobinAfterFiltering(this.getServers(), key);
//獲取請(qǐng)求頭中的版本號(hào)
GrayBean grayBean = CurrentGrayUtils.getGray();
if (null != grayBean && !StringUtils.isEmpty(grayBean.getPreVersion())) {
log.info("灰度路由規(guī)則過濾后的服務(wù)實(shí)例:{}", server.isPresent() ? server.get().getHostPort() : null);
}
} finally {
CurrentGrayUtils.clear();
}
return server.isPresent() ? server.get() : null;
}
/**
* 灰度路由過濾服務(wù)實(shí)例
*
* 如果設(shè)置了期望版本, 則過濾出所有的期望版本 ,然后再走默認(rèn)的輪詢 如果沒有一個(gè)期望的版本實(shí)例,則不過濾,降級(jí)為原有的規(guī)則,進(jìn)行所有的服務(wù)輪詢。(灰度路由失效) 如果沒有設(shè)置期望版本
* 則不走灰度路由,按原有輪詢機(jī)制輪詢所有
*/
protected List<Server> getServers() {
// 獲取spring cloud默認(rèn)負(fù)載均衡器
// 獲取所有待選的服務(wù)
List<Server> allServers = getLoadBalancer().getReachableServers();
if (CollectionUtils.isEmpty(allServers)) {
log.error("沒有可用的服務(wù)實(shí)例");
throw new ApiException("沒有可用的服務(wù)實(shí)例");
}
//獲取請(qǐng)求頭中的版本號(hào)
GrayBean grayBean = CurrentGrayUtils.getGray();
// 如果沒有設(shè)置要訪問的版本,則不過濾,返回所有,走原有默認(rèn)的輪詢機(jī)制
if (null == grayBean || StringUtils.isEmpty(grayBean.getPreVersion())
|| !CommonConstants.GRAY_VERSION_VALUE.equals(grayBean.getPreVersion())) {
//這里需要過濾掉灰度服務(wù)實(shí)例
List<Server> list = allServers.stream().filter(f -> {
// 獲取服務(wù)實(shí)例在注冊(cè)中心上的元數(shù)據(jù)
Map<String, String> metadata = ((NacosServer) f).getMetadata();
// 如果注冊(cè)中心上服務(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í)例在注冊(cè)中心上的元數(shù)據(jù)
Map<String, String> metadata = ((NacosServer) server).getMetadata();
// 如果注冊(cè)中心上服務(wù)的版本標(biāo)簽和期望訪問的版本一致,則灰度路由匹配成功
if (null != metadata && grayBean.getPreVersion().equals(metadata.get(CommonConstants.GRAY_VERSION))) {
filterServer.add(server);
}
}
// 如果沒有匹配到期望的版本實(shí)例服務(wù),為了保證服務(wù)可用性,讓灰度規(guī)則失效,走原有的輪詢所有可用服務(wù)的機(jī)制
if (CollectionUtils.isEmpty(filterServer)) {
log.error("灰度路由規(guī)則失效,沒有找到期望的版本實(shí)例");
throw new ApiException("灰度路由規(guī)則失效,沒有找到期望的版本實(shí)例");
}
return filterServer;
}
}
gateway網(wǎng)關(guān)需要引入的pom
<dependencies>
<!-- Nacos注冊(cè)中心 -->
<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 {
/**
* 灰度請(qǐng)求頭參數(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 org.springframework.context.annotation.Scope;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
/**
* 灰度路由規(guī)則,需要是多例每一個(gè)服務(wù)單獨(dú)一個(gè)實(shí)例,否則會(huì)出現(xiàn)服務(wù)亂調(diào)用問題
*/
@Scope("prototype")
public class GrayRouteRule extends ZoneAvoidanceRule {
@Override
public Server choose(Object key) {
// 根據(jù)灰度路由規(guī)則,過濾出符合規(guī)則的服務(wù) this.getServers()
// 再根據(jù)負(fù)載均衡策略,過濾掉不可用和性能差的服務(wù),然后在剩下的服務(wù)中進(jìn)行輪詢 getPredicate().chooseRoundRobinAfterFiltering()
Optional<Server> server = getPredicate()
.chooseRoundRobinAfterFiltering(this.getServers(), key);
//獲取請(qǐng)求頭中的版本號(hào)
GrayBean grayBean = CurrentGrayUtils.getGray();
if (null != grayBean && !StringUtils.isEmpty(grayBean.getPreVersion())) {
log.info("灰度路由規(guī)則過濾后的服務(wù)實(shí)例:{}", server.isPresent() ? server.get().getHostPort() : null);
}
return server.isPresent() ? server.get() : null;
}
/**
* 灰度路由過濾服務(wù)實(shí)例
*
* 如果設(shè)置了期望版本, 則過濾出所有的期望版本 ,然后再走默認(rèn)的輪詢 如果沒有一個(gè)期望的版本實(shí)例,則不過濾,降級(jí)為原有的規(guī)則,進(jìn)行所有的服務(wù)輪詢。(灰度路由失效) 如果沒有設(shè)置期望版本
* 則不走灰度路由,按原有輪詢機(jī)制輪詢所有
*/
protected List<Server> getServers() {
// 獲取spring cloud默認(rèn)負(fù)載均衡器
// 獲取所有待選的服務(wù)
List<Server> allServers = getLoadBalancer().getReachableServers();
if (CollectionUtils.isEmpty(allServers)) {
log.error("沒有可用的服務(wù)實(shí)例");
throw new ApiException("沒有可用的服務(wù)實(shí)例");
}
//獲取請(qǐng)求頭中的版本號(hào)
GrayBean grayBean = CurrentGrayUtils.getGray();
// 如果沒有設(shè)置要訪問的版本,則不過濾,返回所有,走原有默認(rèn)的輪詢機(jī)制
if (null == grayBean || StringUtils.isEmpty(grayBean.getPreVersion())
|| !CommonConstants.GRAY_VERSION_VALUE.equals(grayBean.getPreVersion())) {
//這里需要過濾掉灰度服務(wù)實(shí)例
List<Server> list = allServers.stream().filter(f -> {
// 獲取服務(wù)實(shí)例在注冊(cè)中心上的元數(shù)據(jù)
Map<String, String> metadata = ((NacosServer) f).getMetadata();
// 如果注冊(cè)中心上服務(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í)例在注冊(cè)中心上的元數(shù)據(jù)
Map<String, String> metadata = ((NacosServer) server).getMetadata();
// 如果注冊(cè)中心上服務(wù)的版本標(biāo)簽和期望訪問的版本一致,則灰度路由匹配成功
if (null != metadata && grayBean.getPreVersion().equals(metadata.get(CommonConstants.GRAY_VERSION))) {
filterServer.add(server);
}
}
// 如果沒有匹配到期望的版本實(shí)例服務(wù),為了保證服務(wù)可用性,讓灰度規(guī)則失效,走原有的輪詢所有可用服務(wù)的機(jī)制
if (CollectionUtils.isEmpty(filterServer)) {
log.error("灰度路由規(guī)則失效,沒有找到期望的版本實(shí)例,version={}", grayBean.getPreVersion());
throw new ApiException("灰度路由規(guī)則失效,沒有找到期望的版本實(shí)例");
}
return filterServer;
}
}
需要傳遞灰度版本號(hào),所以需要把灰度版本請(qǐng)求參數(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)先級(jí)
*/
private final static int LOWER_PRECEDENCE = 10000;
/**
* 使用TransmittableThreadLocal可以跨線程傳遞
*/
@PostConstruct
public void init(){
new GlobalHystrixConcurrencyStrategy();
}
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcCommonConfigurer();
}
/**
* 優(yōu)先級(jí)
* @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)級(jí)別為NONE
* Logger.Level 的具體級(jí)別如下:
* NONE:不記錄任何信息
* BASIC:僅記錄請(qǐng)求方法、URL以及響應(yīng)狀態(tài)碼和執(zhí)行時(shí)間
* HEADERS:除了記錄 BASIC級(jí)別的信息外,還會(huì)記錄請(qǐng)求和響應(yīng)的頭信息
* FULL:記錄所有請(qǐng)求與響應(yīng)的明細(xì),包括頭信息、請(qǐng)求體、元數(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()方法的異常,因?yàn)槭蔷€程池異步請(qǐng)求的
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注冊(cè)中心 -->
<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>
文章來源地址http://www.zghlxwxcb.cn/news/detail-651159.html
文章來源:http://www.zghlxwxcb.cn/news/detail-651159.html
到了這里,關(guān)于springcloud+nacos實(shí)現(xiàn)灰度發(fā)布的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!