SpringCloud微服务配合Swagger

SpringCloud微服务配合Swagger

前言

在SpringBoot项目中目前普遍使用Swagger作为Api文档以及测试工具。

而在SpringCloud的微服务环境下,自然也少不了Api文档和测试工具,但使用Swagger对单一模块来进行查看的话,十分的繁琐,而且使用的接口测试并没有通过微服务的路由请求,所以在SpringCloud微服务项目下的Swagger如果进行配置,这是一直以来的难题。

解决方法

在微服务项目中,接口通常是路由方式进行跳转实现的。所以在需要考虑使用在路由中暴露一个Swagger网页,要求所有的模块Swagger文档都能在这里查询出来,并且可以直接在路由的Swagger这个网页上运行测试。

实现操作

由于是微服务,所有我们需要在common模块下导入Swagger依赖,这里我们选择一个可以快速配置的Swagger-Spring-boot-starter:

<!--Swagger2-->
<dependency>
    <groupId>com.spring4all</groupId>
    <artifactId>swagger-spring-boot-starter</artifactId>
    <version>2.0.1.RELEASE</version>
</dependency>

这个版本中使用的是Swagger-3,这里需要注意下,因为后面需要讲述一下需要注意的问题。

在接口模块中的Swagger配置

由于我们常用的是第三方的Swagger-starter配置,这个依赖的特点是 支持直接在Yaml、Properties文件下直接进行配置Swagger信息,不需要单独创建一个Swagger配置类。

来到一个接口模块下:

swagger:
  base-package: com.test.microservice.login.api.controller
  global-operation-parameters:

这里的swagger.base-package用来设置模块下的Controller的位置的,用来寻找接口。

swagger.global-operation-parameters这个配置是当前这个依赖包的Bug问题解决方法,因为Swagger要求全局参数必须存在,否则的话模块启动将会包NPE错误。所以这里我们设置了一个空参数。

随后我们在Controller下写入常规的Swagger注释即可:

@Api(value = "登录模块",tags = {"登录模块"})
public class LoginController {
	...
	@ApiOperation("用户登录")
	...
}

在路由中配置Swagger

接下来就是这次的问题重点了,在路由中配置Swagger,这里我们使用的是目前SpringCloud官方推荐的

还是和上面用于在Xml、Properties文件下进行配置文件:

swagger:
  global-operation-parameters:

这里由于我们在路由中所以不需要设置Controller的位置,它本身是没有文档内容的。

随后我们需要对其Swagger的连接特殊处理:

@RestController
@RequestMapping("/swagger-resources")
public class SwaggerHandler {
    @Autowired(required = false)
    private SecurityConfiguration securityConfiguration;
    @Autowired(required = false)
    private UiConfiguration uiConfiguration;
    private final SwaggerResourcesProvider swaggerResources;

    @Autowired
    public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
        this.swaggerResources = swaggerResources;
    }

    @GetMapping("/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
    }

    @GetMapping("/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
    }

    @GetMapping("")
    public Mono<ResponseEntity> swaggerResources() {
        return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
}

由于Spring GateWay并没有采用传统的SpringWeb结构,而是使用SpringFlux。

所以需要进行重写Swagger的请求。

这时我们访问路由的Swagger地址 localhost:XXXX/swagger-ui/index.html 即可成功进入Swagger文档页面了。

但是这是Swagger文档的页面并没有归纳在一起,我们需要将其模块的Swagger文档归纳到路由Swagger页面来。

@Component
@Primary
public class SwaggerConfig implements SwaggerResourcesProvider {

    @Override
    public List<SwaggerResource> get() {
        List resources = new ArrayList<>();

        //login-component --->  添加登录模块
        resources.add(swaggerResource("login-component", "/login-api/v3/api-docs", "2.0"));

        return resources;
    }

    private Object swaggerResource(String name, String location, String version) {

        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(name);
        swaggerResource.setLocation(location);
        swaggerResource.setSwaggerVersion(version);
        return swaggerResource;
    }
}

上面我们就添加了一个叫login-component的模块页在Swagger页面上了,它的地址对应到模块的路由Swagger文档Api地址,这里我们采用的Swagger依赖包基于Swagger3所以这里的Api地址默认在v3/api-docs下。

对于后续的模块我们可以依次加入进去即可。

页面成功显示了,模块Api也归纳了,看似几乎成功了…

但其实这里有个问题,这时虽然看的见文档接口,但是对于执行测试的时候,执行的连接并不是对于路由到模块的连接地址,而是直接在路由的连接后拼接,所以执行测试的地址错误。

这是目前的Swagger3的问题,目前的Swagger3无法进行自动对微服务的测试执行拼接请求连接,而是基于当前的Swagger地址连接根目录进行的。

为了解决这个方法,目前暂时可以通过覆盖本地包来重写Swagger请求逻辑。

在项目创建一个springfox.documentation.oas.web包,在其中创建SpecGeneration类。

/**
 * SwaggerV3在当前版本下存在BUG,Swagger无法正确获取到微服务模块请求连接,
 * 从而导致文档请求连接错误。
 * 这个类,以及包名是为了解决其SwaggerV3中不能正确获取到微服务模块地址连接问题
 * 尝试用该类替换框架中的类
 *
 * @author vains
 * @date 2021/4/6 11:21
 */
public class SpecGeneration {

    private static final String HEADER_NAME = "X-Forwarded-Prefix";
    private static final Logger LOGGER = getLogger(SpecGeneration.class);
    public static final String OPEN_API_SPECIFICATION_PATH
            = "${springfox.documentation.open-api.v3.path:/v3/api-docs}";
    protected static final String HAL_MEDIA_TYPE = "application/hal+json";

    private SpecGeneration() {
        throw new UnsupportedOperationException();
    }

    /**
     * 创建一个默认的 swagger 的server
     *
     * @param requestPrefix /v3/api-docs
     * @param requestUrl    请求的url
     * @return Server
     */
    public static Server inferredServer(String requestPrefix, String requestUrl) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String serverUrl = requestUrl.replace(requestPrefix, "");
        String header = null;
        try {
            URI url = new URI(requestUrl);
            serverUrl = String.format("%s://%s:%s", url.getScheme(), url.getHost(), url.getPort());
            header = request.getHeader(HEADER_NAME);
            if (!StringUtils.isEmpty(header)) {
                serverUrl += header;
            }
        } catch (URISyntaxException e) {
            LOGGER.error("Unable to parse request url:" + requestUrl);
        }
        String description = "Inferred Url";
        if (!StringUtils.isEmpty(header)) {
            description += " For " + header.substring(1);
        }
        return new Server()
                .url(serverUrl)
                .description(description);
    }

    public static String decode(String requestURI) {
        try {
            return URLDecoder.decode(requestURI, StandardCharsets.UTF_8.toString());
        } catch (UnsupportedEncodingException e) {
            return requestURI;
        }
    }
}

当我们重写了原本的Swagger3内容后,即解决了模块请求连接的问题。即可正常在路由中执行Swagger中的模块接口。