Spring Gateway聚合Swagger在线文档

为什么需要聚合?

  微服务模块众多,如果不聚合文档,则访问每个服务的API文档都需要单独访问一个Swagger UI界面,这么做客户端能否接受?
  既然使用了微服务,就应该有统一的API文档入口。

如何聚合?

统一的文档入口显然应该聚合到网关中,通过网关的入口统一映射到各个模块。
在这里插入图片描述

本文采用Spring Cloud Gateway 聚合 Swagger的方式生成API文档。

案例源码结构如下:
| spring-gateway-swagger
  |–gateway – 网关
  |–swagger-ui – swagger配置工程
  |–user-center – 用户服务
  |–order – 订单服务

单个服务如何聚合Swagger?

创建swagger配置工程
在这里插入图片描述

1.添加依赖
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-autoconfigure</artifactId>
		<scope>provided</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
       	<artifactId>spring-webmvc</artifactId>
		<scope>provided</scope>
	</dependency>
	
	<!-- swagger ui -->
 		<dependency>
           <groupId>io.springfox</groupId>
           <artifactId>springfox-swagger2</artifactId>
       </dependency>
       <dependency>
           <groupId>io.springfox</groupId>
           <artifactId>springfox-swagger-ui</artifactId>
       </dependency>
       <dependency>
	  	<groupId>com.github.xiaoymin</groupId>
	  	<artifactId>swagger-bootstrap-ui</artifactId>
	</dependency>
2.基础配置类

后续每个微服务依赖后即可通过配置控制文档属性

@Configuration
@ConfigurationProperties(prefix = "swagger")
public class SwaggerConfig {
	
	private String title;
	private String host;
	private String description;
	private String version;
	private String docs = "v2/api-docs";
	private boolean disable = false;
	private String basePackage = "com.gitee.xqxyxchy";
	private String termsOfServiceUrl = "https://gitee.com/xqxyxchy";
	private String contactName = "xqxyxchy";
	private String contactEmail = "xqxyxchy@126.com";
	private String contactUrl = "https://gitee.com/xqxyxchy";
	private Set<StringVendorExtension> stringExtensions = new HashSet<>();
	
	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getHost() {
		return host;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public String getDocs() {
		return docs;
	}

	public void setDocs(String docs) {
		this.docs = docs;
	}

	public boolean isDisable() {
		return disable;
	}

	public void setDisable(boolean disable) {
		this.disable = disable;
	}

	public String getBasePackage() {
		return basePackage;
	}

	public void setBasePackage(String basePackage) {
		this.basePackage = basePackage;
	}

	public String getTermsOfServiceUrl() {
		return termsOfServiceUrl;
	}

	public void setTermsOfServiceUrl(String termsOfServiceUrl) {
		this.termsOfServiceUrl = termsOfServiceUrl;
	}

	public String getContactName() {
		return contactName;
	}

	public void setContactName(String contactName) {
		this.contactName = contactName;
	}

	public String getContactEmail() {
		return contactEmail;
	}

	public void setContactEmail(String contactEmail) {
		this.contactEmail = contactEmail;
	}

	public String getContactUrl() {
		return contactUrl;
	}

	public void setContactUrl(String contactUrl) {
		this.contactUrl = contactUrl;
	}

	public Set<StringVendorExtension> getStringExtensions() {
		return stringExtensions;
	}

	public void setStringExtensions(Set<StringVendorExtension> stringExtensions) {
		this.stringExtensions = stringExtensions;
	}

	public String getVersion() {
		return version;
	}

	public void setVersion(String version) {
		this.version = version;
	}

	@SuppressWarnings("rawtypes")
	public List<VendorExtension> getExtensions() {
		List<VendorExtension> result = new ArrayList<>();
		if(null == getStringExtensions()) {
			return result;
		}
		
		for(StringVendorExtension stringExtension : getStringExtensions()) {
			result.add(new springfox.documentation.service.StringVendorExtension(stringExtension.getName(), stringExtension.getValue()));
		}
		
		return result;
	}
	
	@Override
	public String toString() {
		return "SwaggerConfig [disable=" + isDisable()  + ", host=" + getHost() + ", basePackage=" + getBasePackage() + "]";
	}
	
}

class StringVendorExtension implements VendorExtension<String> {

	private String name;
	private String value;
	
	public StringVendorExtension() {
		super();
	}

	public StringVendorExtension(String name, String value) {
		super();
		this.name = name;
		this.value = value;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setValue(String value) {
		this.value = value;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public String getValue() {
		return value;
	}
	
}
3.Swagger文档信息装配类

支持多个基础包扫描
支持添加自定义参数

@Configuration
@EnableSwagger2
public class Swagger2Configuration extends WebMvcConfigurationSupport {

	private static Logger logger = LoggerFactory.getLogger(Swagger2Configuration.class);
	@Autowired
	@Lazy
	private SwaggerConfig swaggerConfig;
	
	@Bean
    public Docket createRestApi() {
		if(logger.isDebugEnabled()) {
			logger.debug("Starting Swagger");
		}
		logger.info("swaggerConfig ==> {}", swaggerConfig);
		StopWatch watch = new StopWatch();
	    watch.start();
	    
	    Docket docket0 = new Docket(DocumentationType.SWAGGER_2)
				.apiInfo(apiInfo());
	    
	    String host = swaggerConfig.getHost();
	    if(StringUtils.hasText(host)) {
	    	docket0.host(host);
	    }
	    
	    ApiSelectorBuilder builder = docket0.select();
	    
    	builder.apis(basePackage(swaggerConfig.getBasePackage()));
	    builder.apis(RequestHandlerSelectors.withClassAnnotation(Api.class));
	    builder.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class));
	    if(swaggerConfig.isDisable()){
	    	builder.paths(PathSelectors.none());
	    }else{
	    	builder.paths(PathSelectors.any());
	    }
	    
	    Docket docket = builder.build();
	    watch.stop();
	    if(logger.isDebugEnabled()) {
	    	logger.debug("Started Swagger in {} ms", watch.getTotalTimeMillis());
	    }
	    return docket;
    }
	
	/**
     * Predicate that matches RequestHandler with given base package name for the class of the handler method.
     * This predicate includes all request handlers matching the provided basePackage
     *
     * @param basePackage - base package of the classes
     * @return this
     */
    public Predicate<RequestHandler> basePackage(final String basePackage) {
        return new Predicate<RequestHandler>() {
            
            @Override
            public boolean apply(RequestHandler input) {
                return declaringClass(input).transform(handlerPackage(basePackage)).or(true);
            }
        };
    }
    
    /**
     * 处理包路径配置规则,支持多路径扫描匹配以逗号隔开
     * 
     * @param basePackage 扫描包路径
     * @return Function
     */
    private static Function<Class<?>, Boolean> handlerPackage(final String basePackage) {
        return new Function<Class<?>, Boolean>() {
            
            @Override
            public Boolean apply(Class<?> input) {
                for (String strPackage : basePackage.split(",")) {
                	if(Objects.isNull(input)) {
                		continue;
                	}
                	if(Objects.isNull(input.getPackage())) {
                		continue;
                	}
                	if(!StringUtils.hasText(strPackage)) {
                		continue;
                	}
                    boolean isMatch = input.getPackage().getName().startsWith(strPackage);
                    if (isMatch) {
                        return true;
                    }
                }
                return false;
            }
        };
    }
    
    /**
     * @param input RequestHandler
     * @return Optional
     */
    @SuppressWarnings("deprecation")
	private static Optional<? extends Class<?>> declaringClass(RequestHandler input) {
        return Optional.fromNullable(input.declaringClass());
    }
	
	@Bean
    public UiConfiguration uiConfig() {
        return UiConfigurationBuilder.builder().validatorUrl("").build();
    }
	
	@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    private ApiInfo apiInfo() {
    	ApiInfo info = new ApiInfoBuilder()
        		.extensions(swaggerConfig.getExtensions())
                .title(swaggerConfig.getTitle())
                .description(swaggerConfig.getDescription())
                .termsOfServiceUrl(swaggerConfig.getTermsOfServiceUrl())
                .version(swaggerConfig.getVersion())
                .contact(new Contact(swaggerConfig.getContactName(), swaggerConfig.getContactUrl(), swaggerConfig.getContactEmail()))
                .build();
    	return info;
    }
    
	@Value("${spring.jackson.date-format}")
	private String pattern;
	
	@Bean
	public LocalDateTimeSerializer localDateTimeSerializer() {
		return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
	}

}
4.微服务添加引用
	<dependency>
		<groupId>com.gitee.xqxyxchy</groupId>
		<artifactId>swagger-ui</artifactId>
	</dependency>
5.微服务添加配置
swagger:
  title: 用户中心API文档
  description: 用户中心接口文档说明
  string-extensions:
  - name: apiPrefix
    value: /xqxyxchy/uc

至此单个服务的配置完成,此时我们可以验证一下,直接访问:http://127.0.0.1:5101/v2/api-docs,结果如下图:
在这里插入图片描述

网关如何聚合Swagger?

网关聚合的思想很简单,就是从路由中获取微服务的访问地址,然后拼接上 /v2/api-docs 即可。

1.添加依赖
		<!-- swagger ui -->
  		<dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>
        <dependency>
		  	<groupId>com.github.xiaoymin</groupId>
		  	<artifactId>swagger-bootstrap-ui</artifactId>
		</dependency>
2.实现资源接口SwaggerResourcesProvider

生成聚合文档路由数据

@Component
@Primary
public class GatewaySwaggerResourcesProvider implements SwaggerResourcesProvider {
	
	/**
     * swagger3默认的url后缀
     */
    private static final String SWAGGER2URL = "/v2/api-docs";
    /**
     * 网关路由
     */
    @Autowired
    private RouteLocator routeLocator;
    /**
     * 网关应用名称
     */
    @Value("${spring.application.name}")
    private String self;
    
    /**
     * 对于gateway来说这块比较重要 让swagger能找到对应的服务
     */
    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> resources = new ArrayList<>();
        Map<String, String> routeHosts = Maps.newLinkedHashMap();
        // 获取所有可用的host:serviceId
        routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
                .filter(route -> !self.equals(route.getUri().getHost()))
                .subscribe(route -> {
                	Object routeObject = route.getMetadata().get("route");
                	if(Objects.nonNull(routeObject)) {
                		routeHosts.put(route.getUri().getHost(), routeObject.toString());
                	}
                });
        // 记录已经添加过的server
        Set<String> dealed = new HashSet<>();
        routeHosts.keySet().forEach(instance -> {
        	String uri = routeHosts.get(instance);
            // 拼接url
            String url = uri + SWAGGER2URL;
            if (!dealed.contains(url)) {
                dealed.add(url);
                SwaggerResource swaggerResource = new SwaggerResource();
                swaggerResource.setUrl(url);
                swaggerResource.setName(instance);
                resources.add(swaggerResource);
            }
        });
        return resources;
    }

}
3.重写ApiResourceController接口类

spring gateway不支持webmvc,需要手动注入资源接口类

@Controller
@ApiIgnore
@RequestMapping("/swagger-resources")
public class GatewayApiResourceController {

	@Autowired(required = false)
	private SecurityConfiguration securityConfiguration;
	@Autowired(required = false)
	private UiConfiguration uiConfiguration;

	private final SwaggerResourcesProvider swaggerResources;

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

	@RequestMapping(value = "/configuration/security")
	@ResponseBody
	public ResponseEntity<SecurityConfiguration> securityConfiguration() {
		return new ResponseEntity<SecurityConfiguration>(
				Optional.fromNullable(securityConfiguration).or(SecurityConfigurationBuilder.builder().build()),
				HttpStatus.OK);
	}

	@RequestMapping(value = "/configuration/ui")
	@ResponseBody
	public ResponseEntity<UiConfiguration> uiConfiguration() {
		return new ResponseEntity<UiConfiguration>(
				Optional.fromNullable(uiConfiguration).or(UiConfigurationBuilder.builder().build()), HttpStatus.OK);
	}

	@RequestMapping
	@ResponseBody
	public ResponseEntity<List<SwaggerResource>> swaggerResources() {
		return new ResponseEntity<List<SwaggerResource>>(swaggerResources.get(), HttpStatus.OK);
	}

}

网关的配置这里就完成,此时启动网关、用户、订单服务,直接访问网关的文档:http://localhost:5100/doc.html,结果如下图:
在这里插入图片描述

相关源码已上传gitee SpringGateway聚合Swagger接口文档

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

浅尝则止否

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

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

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

打赏作者

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

抵扣说明:

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

余额充值