spring cloud gateway集成sentinel并扩展支持restful api进行url粒度的流量治理

前言

sentinel作为开源的微服务、流量治理组件,在对restful接口的支持上,在1.7之后才开始友好起来,对于带有@PathVariable的restful接口未作支持,在sentinel中/api/{id}这样的接口,其中/api/1与/api/2会被当做两个不同的接口处理,因此很难去做类似接口的流量治理,但在之后,sentinel团队已经提供了响应的csp扩展依赖,下文将会逐步讲述如何通过sentinel扩展来支持相应的服务流量治理

使用网关进行总体流量治理(sentinel版本:1.8.6)

这里选型为spring cloud gateway,而sentinel也对spring cloud gateway做了特殊照顾

1、cloud gateway添加依赖:

 <!-- alibaba封装的sentinel的starter -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>2021.1</version>
        </dependency>
        <!-- alibaba封装的sentinel支持网关的starter -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
            <version>2021.1</version>
        </dependency>
 <!-- 此包即为sentinel提供的扩展支持restful接口的依赖 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-webmvc-adapter</artifactId>
            <version>1.8.0</version>
        </dependency>

上述需要重点关注的是sentinel-spring-webmvc-adapter包,此依赖是支持restful接口的关键,不需要我们自己改造。

2、sentinel配置

spring:
  cloud:
    sentinel:
      transport:
      #sentinel控制台地址
        dashboard: 1.1.1.1:8600
        #sentinel通信端口,默认为8179,被占用会继续扫描,一般固定起来
        port: 8700
        #项目所在服务器ip
        client-ip: 2.2.2.2
        #心跳启动
      eager: true

client-ip在某些情况下不配置会出现sentinl控制台页面只有首页,服务一直注册不上去的情况,如果出现这种情况一定要配置上,如果没有这种情况,client-IP可以不配置,同时上述配置的这些ip端口都需要连通。

3、网关类型项目配置

/**
 * @classDesc:
 * @author: cyjer
 * @date: 2023/1/30 9:53
 */
@SpringBootApplication
@EnableCaching
@Slf4j
public class SiriusApplication {

    public static void main(String[] args) {
        System.getProperties().setProperty("csp.sentinel.app.type", "1");
        SpringApplication.run(SiriusApplication.class, args);
        log.info("<<<<<<<<<<启动成功>>>>>>>>>>");
    }

}

如果是网关类型的项目,需要配置csp.sentinel.app.type= 1,普通项目与网关项目,在控制台上所展示和可使用的功能是不同的

4、通过zk事件监听刷新上报api分组信息

通过将接口分组按照不同粒度,如controller粒度,和具体api接口粒度,通过zookeeper修改数据监听的方式,通过网关监听该事件,实现将api分组信息写入到sentinel中。

1、非网关项目上报api分组信息

/**
 * @classDesc: 扫描项目接口上报api
 * @author: cyjer
 * @date: 2023/2/10 13:46
 */
@Configuration
@Slf4j
@Order(1)
@RequiredArgsConstructor
public class ApiDefinitionReporter implements BeanPostProcessor, CommandLineRunner, Constraint {
    private final List<ApiSiriusDefinition> apiSiriusDefinitionList = new ArrayList<>();
    private final GatewayServiceProperties gatewayServiceProperties;
    private final Environment environment;
    private final static char JTR = '/';
    private final static String PASS = "/**";
    private final static String APPLICATION_NAME = "spring.application.name";
    private final static String CONTEXT_PATH = "server.servlet.context-path";
    private final static List<String> PASS_LIST = Arrays.asList("swaggerWelcome", "basicErrorController", "swaggerConfigResource", "openApiResource");
    private final ZookeeperService zookeeperService;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // url访问路径为:访问基地址basePath+classMappingPath+methodPath
        if (!gatewayServiceProperties.isAutoReportAndRegister() || PASS_LIST.contains(beanName)) {
            return bean;
        }
        Class<?> beanClass = bean.getClass();
        Class<?> targetClass = AopUtils.getTargetClass(bean);
        //判断类上有无controller注解 spring代理类需用spring的注解扫描工具类查找
        RestController restController = AnnotationUtils.findAnnotation(beanClass, RestController.class);
        Controller controller = AnnotationUtils.findAnnotation(beanClass, Controller.class);
        //没有注解直接跳过扫描
        if (null == controller && null == restController) {
            return bean;
        }
        String applicationName = this.getApplicationName();
        //项目访问基地址
        String basePath = this.getBasePath();

        //如果类上有controller注解再查找requestMapping注解
        RequestMapping requestMapping = AnnotationUtils.findAnnotation(beanClass, RequestMapping.class);
        String classMappingPath = this.getClassMappingPath(requestMapping);

        //按照controller分组上报
        if (StringUtils.isNotBlank(classMappingPath)) {
            String controllerGroupPath = basePath + classMappingPath + PASS;
            ApiSiriusDefinition controllerGroup = new ApiSiriusDefinition();
            controllerGroup.setGatewayId(gatewayServiceProperties.getGatewayId());
            controllerGroup.setResource("服务:" + applicationName + ",控制器:" + targetClass.getSimpleName() + ",路径:" + controllerGroupPath);
            controllerGroup.setUrlPath(controllerGroupPath);
            apiSiriusDefinitionList.add(controllerGroup);
        }

        //查找类中所有方法,进行遍历
        Method[] methods = targetClass.getMethods();
        for (Method method : methods) {
            //查找方法上RequestMapping注解
            String methodPath = "";
            String requestType = "";
            RequestMapping methodRequestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
            if (methodRequestMapping != null) {
                String[] value = methodRequestMapping.value();
                RequestMethod[] requestMethods = methodRequestMapping.method();
                if (value.length == 0) {
                    if (requestMethods.length == 0) {
                        return bean;
                    }
                    RequestMethod requestMethod = requestMethods[0];
                    requestType = requestMethod.name();
                    if (requestMethod.equals(RequestMethod.POST)) {
                        PostMapping postMapping = AnnotationUtils.findAnnotation(method, PostMapping.class);
                        methodPath = this.joinMethodPath(postMapping.value());
                    } else if (requestMethod.equals(RequestMethod.GET)) {
                        GetMapping getMapping = AnnotationUtils.findAnnotation(method, GetMapping.class);
                        methodPath = this.joinMethodPath(getMapping.value());
                    } else if (requestMethod.equals(RequestMethod.DELETE)) {
                        DeleteMapping deleteMapping = AnnotationUtils.findAnnotation(method, DeleteMapping.class);
                        methodPath = this.joinMethodPath(deleteMapping.value());
                    } else if (requestMethod.equals(RequestMethod.PATCH)) {
                        PatchMapping patchMapping = AnnotationUtils.findAnnotation(method, PatchMapping.class);
                        methodPath = this.joinMethodPath(patchMapping.value());
                    } else if (requestMethod.equals(RequestMethod.PUT)) {
                        PutMapping putMapping = AnnotationUtils.findAnnotation(method, PutMapping.class);
                        methodPath = this.joinMethodPath(putMapping.value());
                    }
                }

                ApiSiriusDefinition apiSiriusDefinition = new ApiSiriusDefinition();
                String urlPath = basePath + classMappingPath + methodPath;
                apiSiriusDefinition.setUrlPath(urlPath);
                apiSiriusDefinition.setRequestType(requestType);
                apiSiriusDefinition.setGatewayId(gatewayServiceProperties.getGatewayId());
                apiSiriusDefinition.setResource("服务:" + applicationName + ",请求类型:" + requestType + ",路径:" + urlPath);
                apiSiriusDefinitionList.add(apiSiriusDefinition);
            }

        }
        return bean;
    }

    private String joinMethodPath(String[] value) {
        if (value.length != 0) {
            String str = this.trimStrWith(value[0], JTR);
            return JTR + str;
        }
        return "";
    }

    private String getContextPath() {
        String contextPath = environment.getProperty(CONTEXT_PATH);
        contextPath = this.trimStrWith(contextPath, JTR);
        return StringUtils.isBlank(contextPath) ? "" : contextPath;
    }

    public String getApplicationName() {
        String applicationName = environment.getProperty(APPLICATION_NAME);
        applicationName = this.trimStrWith(applicationName, JTR);
        return StringUtils.isBlank(applicationName) ? "" : applicationName;
    }

    private String getBasePath() {
        String contextPath = this.getContextPath();
        String applicationName = this.getApplicationName();
        if (StringUtils.isBlank(contextPath)) {
            return JTR + applicationName;
        }
        return JTR + applicationName + JTR + contextPath;
    }

    private String getClassMappingPath(RequestMapping requestMapping) {
        if (null != requestMapping) {
            String requestMappingUrl = requestMapping.value().length == 0 ? "" : requestMapping.value()[0];
            requestMappingUrl = this.trimStrWith(requestMappingUrl, JTR);
            return JTR + requestMappingUrl;
        }
        return "";
    }

    public String trimStrWith(String str, char trimStr) {
        if (StringUtils.isBlank(str)) {
            return str;
        }
        int st = 0;
        int len = str.length();
        char[] val = str.toCharArray();
        while ((st < len) && (val[st] <= trimStr)) {
            st++;
        }
        while ((st < len) && (val[len - 1] <= trimStr)) {
            len--;
        }
        return ((st > 0) || (len < str.length())) ? str.substring(st, len) : str;
    }

    @Override
    public void run(String... args) {
        if (StringUtils.isBlank(this.getApplicationName())) {
            throw new RuntimeException(APPLICATION_NAME + " should not be null");
        }
        if (!apiSiriusDefinitionList.isEmpty()) {
            log.info("<<<<< start to report api information to api governance platform >>>>>");
            try {
                zookeeperService.create(API_DEFINITION + SPLIT + getApplicationName(), JSONArray.toJSONString(apiSiriusDefinitionList));
                zookeeperService.update(API_DEFINITION + SPLIT + getApplicationName(), JSONArray.toJSONString(apiSiriusDefinitionList));
            } catch (Exception e) {
                log.error("reported api information failed,stack info:", e);
            }
            log.info("<<<<< successfully reported api information >>>>>");
        }
    }

}

通过扫描项目下的controller和相应的mapping注解中的属性拼接出url来,通过zk来更新节点数据

2、网关添加监听事件

zk操作查看另一篇文章zookeeper相关操作

/**
 * @classDesc: 网关核心应用
 * @author: cyjer
 * @date: 2023/1/30 9:53
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class GatewayApplication implements ApplicationListener<ContextRefreshedEvent> {
    private final GatewayServiceProperties properties;
    private final ApiDefinitionService apiDefinitionService;
    private final ZookeeperService zookeeperService;
    private final ApiGroupProcesser apiGroupProcesser;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        //拉取api governance platform 信息
        apiDefinitionService.refreshApiGovernanceInfo(properties.getGatewayId());
        log.info("<<<<<<<<<<刷新api分组信息完成>>>>>>>>>>");
        zookeeperService.create(Constraint.API_DEFINITION, "init");
        zookeeperService.addWatchChildListener(Constraint.API_DEFINITION, apiGroupProcesser);
        log.info("<<<<<<<<<<api上报监听器配置完成>>>>>>>>>>");

    }
}

通过事件处理,首先启动时刷新api信息,同时尝试初始化zk节点,然后注册监听watch。

刷新api信息: 如下图所示通过获取ApiGroupProcesser 中持久化到redis的分组信息,来通知sentinel刷新信息,这是为了在网关进行启动时能够实时刷新最新数据并告知sentinel

    public void refreshApiGovernanceInfo(String gatewayId) {
        log.info("<<<<< get api information from api governance platform >>>>>");
        HashMapCache<String, List<ApiSiriusDefinition>> cache = HashMapCache.cacheGenerate();
        List<List<ApiSiriusDefinition>> values = cache.values(CacheKeyGenerator.builder()
                .group(API_INFO_REDIS_PREFIX)
                .key(gatewayId)
                .build());
        Set<ApiDefinition> definitions = new HashSet<>();
        for (List<ApiSiriusDefinition> apiInfo : values) {
            SiriusUtil.reportApi(apiInfo, definitions);
        }
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
        log.info("<<<<< finished loading api information >>>>>");
    }

上报sentinel如下图所示

public class SiriusUtil {
    public static Set<ApiDefinition> reportApi(List<ApiSiriusDefinition> list, Set<ApiDefinition> definitions) {
        for (ApiSiriusDefinition apiDefinition : list) {
            ApiDefinition api;
            if (apiDefinition.getPrefixMatch()) {
                api = new ApiDefinition(apiDefinition.getResource())
                        .setPredicateItems(new HashSet<ApiPredicateItem>() {
                            {
                                add(new ApiPathPredicateItem().setPattern(apiDefinition.getUrlPath())
                                        .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                            }
                        });
            } else {
                api = new ApiDefinition(apiDefinition.getResource())
                        .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                            add(new ApiPathPredicateItem().setPattern(apiDefinition.getUrlPath()));
                        }});
            }

            definitions.add(api);
        }
        return definitions;
    }
}

ApiSiriusDefinition 如下图所示

@Data
public class ApiSiriusDefinition {
    private String gatewayId;
    private String resource;
    private String urlPath;
    private String requestType;
    private Boolean prefixMatch;
}

网关配置

/**
 * @classDesc: 网关配置
 * @author: cyjer
 * @date: 2023/1/31 15:14
 */
@ConfigurationProperties("api-gateway")
@Configuration
@Data
public class GatewayServiceProperties {
    /**
     * 网关ID
     */
    private String gatewayId;
    /**
     * 网关名称
     */
    private String gatewayName;
    /**
     * 是否自动上报api分组注册
     */
    private boolean autoReportAndRegister = true;

}

3、网关监听事件处理

/**
 * @classDesc: api分组上报
 * @author: cyjer
 * @date: 2023/2/10 11:13
 */
@Slf4j
@Component
public class ApiGroupProcesser extends AbstractChildListenerProcess implements ApiDefinitionConstraint {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private GatewayServiceProperties gatewayServiceProperties;

    @Override
    public void process(CuratorFramework curatorFramework, PathChildrenCacheEvent cacheEvent) {
        ChildData data = cacheEvent.getData();
        if (Objects.nonNull(data) && cacheEvent.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
            log.info("<<<<<<<<<<上报api分组到sentinel>>>>>>>>>>");
            String path = data.getPath();
            String content = new String(data.getData(), StandardCharsets.UTF_8);
            Set<ApiDefinition> definitions = GatewayApiDefinitionManager.getApiDefinitions();
            List<ApiSiriusDefinition> list = JSONArray.parseArray(content, ApiSiriusDefinition.class);
            for (ApiSiriusDefinition apiGroup : list) {
                ApiDefinition api = new ApiDefinition(apiGroup.getResource())
                        .setPredicateItems(new HashSet<ApiPredicateItem>() {
                            {
                                add(new ApiPathPredicateItem().setPattern(apiGroup.getUrlPath())
                                        .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                            }
                        });

                definitions.add(api);
            }
            GatewayApiDefinitionManager.loadApiDefinitions(definitions);
            redisTemplate.opsForHash().put(API_INFO_REDIS_PREFIX + gatewayServiceProperties.getGatewayId(), path, JSONArray.toJSONString(list));
            log.info("<<<<<<<<<<上报api分组到sentinel成功>>>>>>>>>>");
        }
    }
}

5、sentinel控制台启动

java -Dserver.port=8600 -Dcsp.sentinel.dashboard.server=localhost:8600 -Dproject.name=sentinel-dashboard -Xms512m -Xmx512m -Xmn256m -XX:MaxMetaspaceSize=100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/oom/log -Dfile.encoding=UTF-8 -XX:+UseG1GC -jar sentinel-dashboard-1.8.6.jar

打开sentinel控制台,请求几次接口后
在这里插入图片描述

可以看到相应的api分组信息和url路径匹配都已加载,在进行流量治理的时候就可以支持restful接口和controller粒度的治理了

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

皮特猫.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值