springcloud+nacos实现灰度发布

灰度发布

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 {
 
	@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 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 {
            // 根据灰度路由规则,过滤出符合规则的服务 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())
                || !CommonConstants.GRAY_VERSION_VALUE.equals(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 org.springframework.context.annotation.Scope;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
/**
 * 灰度路由规则,需要是多例每一个服务单独一个实例,否则会出现服务乱调用问题
 */
@Scope("prototype")
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())
                || !CommonConstants.GRAY_VERSION_VALUE.equals(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>
相关推荐
WZTTMoon7 分钟前
Spring Boot 4.0 迁移核心注意点总结
java·spring boot·后端
寻kiki7 分钟前
scala 函数类?
后端
疯狂的程序猴18 分钟前
iOS App 混淆的真实世界指南,从构建到成品 IPA 的安全链路重塑
后端
旷野说25 分钟前
为什么 MyBatis 原生二级缓存“难以修复”?
java·java-ee·mybatis
8***235528 分钟前
【wiki知识库】07.用户管理后端SpringBoot部分
java
bcbnb29 分钟前
iOS 性能测试的工程化方法,构建从底层诊断到真机监控的多工具测试体系
后端
开心就好202532 分钟前
iOS 上架 TestFlight 的真实流程复盘 从构建、上传到审核的团队协作方式
后端
小周在成长41 分钟前
Java 泛型支持的类型
后端
aiopencode41 分钟前
Charles 抓不到包怎么办?HTTPS 抓包失败、TCP 数据流异常与底层补抓方案全解析
后端
阿蔹44 分钟前
JavaWeb-Selenium 配置以及Selenim classnotfound问题解决
java·软件测试·python·selenium·测试工具·自动化