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

sentinel集成网关支持restful接口进行url粒度的流量治理

  • 前言
  • 使用网关进行总体流量治理(sentinel版本:1.8.6)
    • 1、cloud gateway添加依赖:
    • 2、sentinel配置
    • 3、网关类型项目配置
    • 4、通过zk事件监听刷新上报api分组信息
      • 1、非网关项目上报api分组信息
      • 2、网关添加监听事件
      • 3、网关监听事件处理
    • 5、sentinel控制台启动

前言

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信息,同时尝试初始化zk节点,然后注册监听watch。

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控制台,请求几次接口后
spring cloud gateway集成sentinel并扩展支持restful api进行url粒度的流量治理_第1张图片

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

你可能感兴趣的:(sentinel,restful,java)