灰度发布
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.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();}}
灰度过滤器设置灰度上下文信息
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 {@Overridepublic 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);}@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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可以跨线程传递*/@PostConstructpublic void init(){new GlobalHystrixConcurrencyStrategy();}@Beanpublic 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 {@Overridepublic 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 {@Beanpublic RequestInterceptor requestInterceptor() {return new RequestInterceptor() {@Overridepublic 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:记录所有请求与响应的明细,包括头信息、请求体、元数据*/@BeanLogger.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());}@Overridepublic <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;}@Overridepublic 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@ConditionalOnMissingBeanpublic 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;}@Overridepublic 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);}@Overridepublic 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 {@Resourceprivate RedisRepository redisRepository;@Overridepublic 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> {@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {return true;}@Overridepublic 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 {@Overridepublic 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>