微服务如何集成swagger3

文章目录


引言

我们在用springboot开发应用时,经常使用swagger来作为我们的接口文档可视化工具,方便前端同事调用,集成也是比较简单的,那在微服务系统中,如何使用swagger呢?总不能每个服务都集成一次吧,别急,笔者接下来的内容将十分详细的给大家展示如何集成swagger3到我们的系统中。


一、项目结构

上面是笔者正在开发的AI智能分析平台项目结构(开发中,还有很多模块待开发),我为几个红框标注的微服务模块集成了swagger3,文档的统一访问入口就在gateway模块。

环境如下:

  • JDK17
  • sping boot 版本:3.3.5
  • sping cloud 版本:2023.0.3
  • spring-cloud-alibaba 版本:2023.0.1.2

二、顶级pom依赖准备

上面的 ai-platform-server 根目录下的pom.xml即为我的顶级聚合pom,在其中引入如下依赖

xml 复制代码
            <!-- SpringDoc OpenAPI 这是网关gateway模块专用,因为它是webflux,不是webmvc-->
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
                <version>${springdoc.version}</version>
            </dependency>
            <!-- SpringDoc webmvc -->
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
                <version>${springdoc.version}</version>
            </dependency>
            <dependency>
                <groupId>io.swagger.core.v3</groupId>
                <artifactId>swagger-annotations-jakarta</artifactId>
                <version>${swagger-annotations-jakarta.version}</version>
            </dependency>

这几个依赖的版本如下

xml 复制代码
        <springdoc.version>2.6.0</springdoc.version>
        <swagger-annotations-jakarta.version>2.2.28</swagger-annotations-jakarta.version>

重点说明

springdoc-openapi-starter-webmvc-ui

这是为Spring MVC(传统Servlet栈)应用程序设计的SpringDoc OpenAPI集成模块。它适用于使用spring-boot-starter-web(基于Servlet)的项目。

主要特点:

  • 专为Spring MVC设计
  • 集成了Swagger UI界面
  • 适用于传统的Spring Boot Web应用程序
  • 基于Servlet API

springdoc-openapi-starter-webflux-ui

这是为Spring WebFlux(响应式栈)应用程序设计的SpringDoc OpenAPI集成模块。它适用于使用spring-boot-starter-webflux(基于Reactive)的项目。

主要特点:

  • 专为Spring WebFlux设计
  • 集成了Swagger UI界面
  • 适用于响应式Spring Boot应用程序
  • 基于Reactive Streams

网关模块基于Spring Cloud Gateway,它使用的是WebFlux响应式编程模型。所以网关gateway模块使用springdoc-openapi-starter-webflux-ui,其他模块使用springdoc-openapi-starter-webmvc-ui。

总结:

  • springdoc-openapi-starter-webmvc-ui:用于传统的基于Servlet的Spring MVC应用程序
  • springdoc-openapi-starter-webflux-ui:用于响应式的基于WebFlux的应用程序

以上的疑问解决掉后,再看具体模块怎么集成的


三、common-swagger模块

这个模块是干啥的呢,是除了gateway模块以外,其他微服务想要集成swagger需要引入的依赖,为了避免每个模块都引入重复的依赖,同事方便统一管理,我自定义一个模块,把对应的依赖集中到了一起,这个模块中的pom依赖内容如下

xml 复制代码
 <dependencies>
        <!-- SpringDoc webmvc 非gateway网关模块使用这个 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>

        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations-jakarta</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

这个common-swagger模块中定义了一些配置类


SpringDocProperties

java 复制代码
package com.aip.common.swagger.properties;

import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

/**
 * Swagger 配置属性
 */
@Setter
@Getter
@ConfigurationProperties(prefix = "springdoc")
public class SpringDocProperties {
    /**
     * 网关
     */
    private String gatewayUrl;

    /**
     * 文档基本信息
     */
    @NestedConfigurationProperty
    private InfoProperties info = new InfoProperties();

    /**
     * <p>
     * 文档的基础属性信息
     * </p>
     *
     * @see io.swagger.v3.oas.models.info.Info
     * <p>
     * 为了 springboot 自动生产配置提示信息,所以这里复制一个类出来
     */
    @Setter
    @Getter
    public static class InfoProperties {
        /**
         * 标题
         */
        private String title = null;

        /**
         * 描述
         */
        private String description = null;

        /**
         * 联系人信息
         */
        @NestedConfigurationProperty
        private Contact contact = null;

        /**
         * 许可证
         */
        @NestedConfigurationProperty
        private License license = null;

        /**
         * 版本
         */
        private String version = null;
    }

}

SpringDocAutoConfiguration

java 复制代码
package com.aip.common.swagger;

import com.aip.common.swagger.properties.SpringDocProperties;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

import java.util.ArrayList;
import java.util.List;

/**
 * Swagger 文档配置
 */
@AutoConfiguration(before = SpringDocConfiguration.class)
@EnableConfigurationProperties(SpringDocProperties.class)
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
public class SpringDocAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(OpenAPI.class)
    public OpenAPI openApi(SpringDocProperties properties) {
        return new OpenAPI().components(new Components()
                        // 设置认证的请求头
                        .addSecuritySchemes("apikey", securityScheme()))
                .addSecurityItem(new SecurityRequirement().addList("apikey"))
                .info(convertInfo(properties.getInfo()))
                .servers(servers(properties.getGatewayUrl()));
    }

    public SecurityScheme securityScheme() {
        return new SecurityScheme().type(SecurityScheme.Type.APIKEY)
                .name("Authorization")
                .in(SecurityScheme.In.HEADER)
                .scheme("Bearer");
    }

    private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
        Info info = new Info();
        info.setTitle(infoProperties.getTitle());
        info.setDescription(infoProperties.getDescription());
        info.setContact(infoProperties.getContact());
        info.setLicense(infoProperties.getLicense());
        info.setVersion(infoProperties.getVersion());
        return info;
    }

    public List<Server> servers(String gatewayUrl) {
        List<Server> serverList = new ArrayList<>();
        serverList.add(new Server().url(gatewayUrl));
        return serverList;
    }
}

resources文件下的那个是自动配置的,这个不用多说了里面就一行

bash 复制代码
com.aip.common.swagger.SpringDocAutoConfiguration

四、gateway模块配置

pom文件需要引入如下

xml 复制代码
        <!-- swagger,这里需要引入响应式的 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
        </dependency>

这个之前的顶级pom已经做好依赖管理了,这里直接引入就行

gateway模块配置要特殊点了,因为它是swagger访问入口,所以会有一些配置和接口访问权限的问题,首先是配置类 SpringDocConfig

java 复制代码
package com.aip.gateway.config;

import com.alibaba.nacos.client.naming.event.InstancesChangeEvent;
import com.alibaba.nacos.common.notify.Event;
import com.alibaba.nacos.common.notify.NotifyCenter;
import com.alibaba.nacos.common.notify.listener.Subscriber;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.properties.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigProperties;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Configuration;

import java.util.Set;
import java.util.stream.Collectors;

/**
 * SpringDoc配置类
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "springdoc.api-docs.enabled", matchIfMissing = true)
public class SpringDocConfig implements InitializingBean {
    @Resource
    private SwaggerUiConfigProperties swaggerUiConfigProperties;

    @Resource
    private DiscoveryClient discoveryClient;

    /**
     * 在初始化后调用的方法
     */
    @Override
    public void afterPropertiesSet() {
        NotifyCenter.registerSubscriber(new SwaggerDocRegister(swaggerUiConfigProperties, discoveryClient));
    }
}

/**
 * Swagger文档注册器
 */
class SwaggerDocRegister extends Subscriber<InstancesChangeEvent> {

    @Resource
    private SwaggerUiConfigProperties swaggerUiConfigProperties;

    @Resource
    private DiscoveryClient discoveryClient;

    //需要排除api的微服务模块应用名称
    private final static String[] EXCLUDE_ROUTES = new String[]{"ai-platform-auth","ai-platform-gateway"};

    public SwaggerDocRegister(SwaggerUiConfigProperties swaggerUiConfigProperties, DiscoveryClient discoveryClient) {
        this.swaggerUiConfigProperties = swaggerUiConfigProperties;
        this.discoveryClient = discoveryClient;
    }

    /**
     * 事件回调方法,处理InstancesChangeEvent事件
     *
     * @param event 事件对象
     */
    @Override
    public void onEvent(InstancesChangeEvent event) {
        Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> swaggerUrlSet = discoveryClient.getServices()
                .stream()
                .flatMap(serviceId -> discoveryClient.getInstances(serviceId).stream())
                .filter(instance -> !StringUtils.equalsAny(instance.getServiceId(), EXCLUDE_ROUTES))
                .map(instance -> {
                    AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl();
                    swaggerUrl.setName(instance.getServiceId());
                    //这里是v2还是v3看你的swagger-ui访问地址的请求路径
                    swaggerUrl.setUrl(String.format("/%s/v3/api-docs", instance.getServiceId()));
                    return swaggerUrl;
                })
                .collect(Collectors.toSet());

        swaggerUiConfigProperties.setUrls(swaggerUrlSet);
    }

    /**
     * 订阅类型方法,返回订阅的事件类型
     *
     * @return 订阅的事件类型
     */
    @Override
    public Class<? extends Event> subscribeType() {
        return InstancesChangeEvent.class;
    }
}

这个类的主要作用是注册各个子服务的接口文档访问url,以及排除哪些服务不需要展示接口文档


再有一个就是接口文档这个访问地址应该排除在我们的请求认证体系之外,比如笔者用的是JWT认证,这里把代码放上仅供参考

java 复制代码
package com.aip.gateway.filter;

import com.aip.common.constants.CommonConstants;
import com.aip.common.utils.JwtUtils;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

/**
 * JWT认证过滤器
 * 网关统一处理JWT认证,避免各服务重复验证
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {

    @Resource
    private JwtUtils jwtUtils;

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    // 不需要认证的路径
    private static final List<String> EXCLUDE_PATHS = Arrays.asList(
            "/api/v1/auth/login",
            "/api/v1/auth/refresh",
            "/api/v1/auth/register",
            "/api/v1/auth/send-verification-code",
            "/api/v1/auth/verify-code",
            "/api/v1/auth/forgot-password",
            "/api/v1/auth/reset-password",
            "/actuator/**",
            "/swagger-ui/**",
            "/**/v3/api-docs/**",
            "/health/**",
            "/ping/**",
            "/error"
    );

    // 不需要租户ID的路径
    private static final List<String> NO_TENANT_PATHS = Arrays.asList(
            "/api/v1/auth/login",
            "/api/v1/auth/register",
            "/api/v1/auth/forgot-password",
            "/api/v1/auth/reset-password",
            "/actuator/**",
            "/swagger-ui/**",
            "/**/v3/api-docs/**"
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        String method = request.getMethod().name();

        log.debug("网关JWT过滤器处理请求: {} {}", method, path);

        // 检查是否是需要排除的路径
        if (isExcludePath(path)) {
            log.debug("跳过JWT认证: {}", path);
            return chain.filter(exchange);
        }

        // 获取Authorization头
        String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (!StringUtils.hasText(authorization) || !authorization.startsWith("Bearer ")) {
            log.warn("请求缺少有效的Authorization头: {}", path);
            return unauthorizedResponse(exchange, "缺少认证Token");
        }

        // 提取Token
        String token = authorization.substring(7);

        // 验证Token格式(这里只做基本验证,详细验证由各服务完成)
        if (!isValidTokenFormat(token)) {
            log.warn("Token格式无效: {}", path);
            return unauthorizedResponse(exchange, "Token格式无效");
        }

        // 获取租户ID
        String tenantId = getTenantId(request);
        if (!StringUtils.hasText(tenantId) && isTenantRequired(path)) {
            log.warn("请求缺少租户ID: {}", path);
            return unauthorizedResponse(exchange, "缺少租户ID");
        }

        // 将Token和租户ID添加到请求头,传递给下游服务
        // 下游服务可以直接使用这些信息,无需再次解析JWT
        ServerHttpRequest modifiedRequest = request.mutate()
                .header(CommonConstants.Security.GATEWAY_TOKEN_HEADER, token)
                .header(CommonConstants.Security.GATEWAY_TENANT_ID_HEADER, tenantId != null ? tenantId : "")
                .header(CommonConstants.Security.GATEWAY_USER_ID_HEADER, extractUserIdFromToken(token)) // 从Token中提取用户ID
                .header(CommonConstants.Security.GATEWAY_USERNAME_HEADER, extractUsernameFromToken(token)) // 从Token中提取用户名
                .build();

        log.debug("JWT认证通过: path={}, tenantId={}, userId={}", path, tenantId, extractUserIdFromToken(token));

        return chain.filter(exchange.mutate().request(modifiedRequest).build());
    }

    @Override
    public int getOrder() {
        return -100; // 高优先级
    }

    /**
     * 检查是否为排除路径
     */
    private boolean isExcludePath(String requestPath) {
        return EXCLUDE_PATHS.stream()
                .anyMatch(pattern -> pathMatcher.match(pattern, requestPath));
    }

    /**
     * 检查是否需要租户ID
     */
    private boolean isTenantRequired(String requestPath) {
        return NO_TENANT_PATHS.stream()
                .noneMatch(pattern -> pathMatcher.match(pattern, requestPath));
    }

    /**
     * 获取租户ID
     */
    private String getTenantId(ServerHttpRequest request) {
        // 优先从请求头获取
        String tenantId = request.getHeaders().getFirst(CommonConstants.Tenant.TENANT_ID_HEADER);
        if (StringUtils.hasText(tenantId)) {
            return tenantId;
        }

        // 从请求参数获取
        String query = request.getURI().getQuery();
        if (StringUtils.hasText(query) && query.contains("tenantId=")) {
            String[] params = query.split("&");
            for (String param : params) {
                if (param.startsWith("tenantId=")) {
                    return param.substring("tenantId=".length());
                }
            }
        }

        // 从请求路径获取(如:/api/v1/tenant/{tenantId}/...)
        String path = request.getPath().value();
        String[] pathParts = path.split("/");
        for (int i = 0; i < pathParts.length - 1; i++) {
            if ("tenant".equals(pathParts[i]) && i + 1 < pathParts.length) {
                return pathParts[i + 1];
            }
        }

        return null;
    }

    /**
     * 验证Token格式(基本验证)
     */
    private boolean isValidTokenFormat(String token) {
        if (!StringUtils.hasText(token)) {
            return false;
        }

        // JWT Token应该包含两个点,分为三部分
        String[] parts = token.split("\\.");
        if (parts.length != 3) {
            return false;
        }

        // 每部分都不应该为空
        for (String part : parts) {
            if (!StringUtils.hasText(part)) {
                return false;
            }
        }

        return true;
    }

    /**
     * 从Token中提取用户ID
     */
    private String extractUserIdFromToken(String token) {
        return jwtUtils.extractUserIdFromToken(token);
    }

    /**
     * 从Token中提取用户名
     */
    private String extractUsernameFromToken(String token) {
        return jwtUtils.extractUsernameFromToken(token);
    }

    /**
     * 返回未授权响应
     */
    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");

        String responseBody = String.format(
                "{\"code\":%d,\"message\":\"%s\",\"data\":null}",
                401, message
        );

        return exchange.getResponse().writeWith(
                Mono.just(exchange.getResponse().bufferFactory().wrap(responseBody.getBytes()))
        );
    }
}

其中的 EXCLUDE_PATHS 即为跳过JWT认证的url路径列表,其中就包含了下面两个路径

bash 复制代码
"/swagger-ui/**",
"/**/v3/api-docs/**"

以上就是集成过程中的主要内容,各个子服务当然也需要一定的配置,就以ai-platform-user 这个子模块为例

首先是在该模块的pom下添加该之前创建的 common-swagger依赖

xml 复制代码
        <!-- Swagger 模块 -->
        <dependency>
            <groupId>com.aip</groupId>
            <artifactId>ai-platform-common-swagger</artifactId>
        </dependency>

然后是配置文件application.yml中配置如下内容

yaml 复制代码
springdoc:
  # 网关场景下的 API 文档地址(需网关路由支持)
  gatewayUrl: http://localhost:8080/api/${spring.application.name}
  api-docs:
    enabled: true
    path: /v3/api-docs
  swagger-ui:
    path: /swagger-ui.html
  info:
    title: '用户模块接口文档'
    version: 1.0.0
    description: '用户模块接口描述'
    contact:
      name: ai-platform
      url: https://www.baidu.com

这个gatewayUrl在前面的 SpringDocAutoConfiguration 这个类中有用到读取这个属性

其他的模块如engine、file模块、config模块等都按照这个来配置即可


五、结果演示

按照上面的步骤集成好后,启动你的微服务系统,浏览器访问如下地址

bash 复制代码
http://localhost:8080/swagger-ui.html

具体controller的参数和实体类上配套使用swagger3给定的一些参数注解,这个自行去查找资料

对于每个子服务的启动类上可加上以下注解,这样每个服务接口文档就会有标题了,如上图的 ai-platform-user: 用户模块

到这里,集成swagger3的教程就结束了,诸位如果对微服务架构比较熟悉,按照笔者的教程集成起来应该不是难事

相关推荐
京东零售技术6 分钟前
查收你的技术成长礼包
后端·算法·架构
gengsa12 分钟前
使用 Telepresence 做本地微服务项目开发
后端·微服务
Serverless社区20 分钟前
函数计算进化之路:AI Sandbox 新基座
阿里云·云原生·serverless
u01040583623 分钟前
基于微服务架构的电商返利APP技术架构设计与性能优化策略
微服务·性能优化·架构
云舟吖3 小时前
基于 electron-vite 从零到一搭建桌面端应用
前端·架构
喂完待续3 小时前
【Big Data】Amazon S3 专为从任何位置检索任意数量的数据而构建的对象存储
大数据·云原生·架构·big data·对象存储·amazon s3·序列晋升
程序猿阿伟3 小时前
《云原生边缘与AI训练场景:2类高频隐蔽Bug的深度排查与架构修复》
人工智能·云原生·bug
智码看视界5 小时前
老梁聊全栈系列:(阶段一)从单体到云原生的演进脉络
java·云原生·c5全栈