Springfox (Swagger) – ApiOperation 注解中的 position 方法无效的拓展解决方案

  • Post author:
  • Post category:其他




写在前面

未经允许不得转载!

如果你有现在使用

Springfox

的经历的话,可能就有对 api 进行分组以及排序等需求,而

Springfox

(应当说是

Swagger

更确切)默认是根据自然排序对 api 进行排序的,比如:”/aaa1″会排在”/aaa2″前面,会排在”/bbb1″前面。

当需要自定义排序规则时怎么办?

  • 那就是使用

    ApiOperation

    注解的

    position

    方法,但是很不幸的是该方法已经标记为过时了,现在保留这个方法只是为了兼容之前的版本,且高版本的已经一移除该功能了(包括后端和前端UI)。

那有其他办法实现自定义排序吗?

  • 自己花时间找文档及api看看有没有类似功能
  • 按照我的配置方式实现临时用的拓展解决方案(当然只是临时的 毕竟

    position

    已经过时了,之后的版本可能就移除了)



相关链接



临时用的拓展解决方案



第1步:在需要的位置添加

position

/*
 *  Copyright 2019 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Yahuan Jin
 * @since 2019.08.24
 */
@Api(tags = "test", description = "test")
@RequestMapping("test/apiOperation")
@RestController
public class ApiOperationTestController {
    @ApiOperation(value = "Test position 2", tags = {"position-test"}, position = 2)
    @PostMapping(value = "aaa")
    public String testPositionAaa() {
        return "aaa";
    }

    @ApiOperation(value = "Test position 1", tags = {"position-test"}, position = 1)
    @RequestMapping(value = "bbb")
    public String testPositionBbb() {
        return "bbb";
    }
}



第2步:完成对

position

的支持

拓展

springfox.documentation.spring.web.readers.operation.ApiOperationReade

类。

/*
 *  Copyright 2015-2019 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import com.google.common.base.Optional;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import springfox.documentation.OperationNameGenerator;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.service.Operation;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.RequestMappingContext;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.operation.OperationReader;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import static com.google.common.collect.Lists.newArrayList;
import static java.util.Arrays.asList;

/**
 * @author {@link springfox.documentation.spring.web.readers.operation.ApiOperationReader
 * the original author}
 * @author Yahuan Jin
 * @see springfox.documentation.spring.web.readers.operation.ApiOperationReader
 * @since 2019.08.24
 */
@Component
public class SwaggerSupportPositionApiOperationReader implements OperationReader {

    private static final Set<RequestMethod> allRequestMethods
            = new LinkedHashSet<>(asList(RequestMethod.values()));
    private final DocumentationPluginsManager pluginsManager;
    private final OperationNameGenerator nameGenerator;

    @Autowired
    public SwaggerSupportPositionApiOperationReader(DocumentationPluginsManager pluginsManager,
                                                    OperationNameGenerator nameGenerator) {
        this.pluginsManager = pluginsManager;
        this.nameGenerator = nameGenerator;
    }

    @Override
    public List<Operation> read(RequestMappingContext outerContext) {
        List<Operation> operations = newArrayList();

        Set<RequestMethod> requestMethods = outerContext.getMethodsCondition();
        Set<RequestMethod> supportedMethods = supportedMethods(requestMethods);

        //Setup response message list
        Integer currentCount = 0;
        // Get position, then support position. NOTE: not support sorted by RequestMethod.
        int position = getApiOperationPosition(outerContext, 0);

        for (RequestMethod httpRequestMethod : supportedMethods) {
            OperationContext operationContext = new OperationContext(
                    new OperationBuilder(nameGenerator),
                    httpRequestMethod,
                    outerContext,
                    (position + currentCount));

            Operation operation = pluginsManager.operation(operationContext);
            if (!operation.isHidden()) {
                operations.add(operation);
                currentCount++;
            }
        }
        Collections.sort(operations, outerContext.operationOrdering());

        return operations;
    }

    private Set<RequestMethod> supportedMethods(final Set<RequestMethod> requestMethods) {
        return requestMethods == null || requestMethods.isEmpty()
                ? allRequestMethods
                : requestMethods;
    }

    private int getApiOperationPosition(final RequestMappingContext outerContext, final int defaultValue) {
        final Optional<ApiOperation> annotation = outerContext.findAnnotation(ApiOperation.class);
        return annotation.isPresent() ? annotation.get().position() : defaultValue;
    }
}



第3步:实现根据

position

进行数据排序

拓展

springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl

类。

/*
 *  Copyright 2015-2019 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import com.google.common.base.Optional;
import com.google.common.collect.Multimap;
import io.swagger.models.Operation;
import io.swagger.models.Path;
import io.swagger.models.Swagger;
import org.springframework.stereotype.Component;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.ApiListing;
import springfox.documentation.service.Documentation;
import springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import static springfox.documentation.builders.BuilderDefaults.nullToEmptyList;

/**
 * @author {@link springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl
 * the original author}
 * @author Yahuan Jin
 * @see springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl
 * @since 2019.08.24
 */
@Component
public class SwaggerOriginalSortedServiceModelToSwagger2MapperImpl extends ServiceModelToSwagger2MapperImpl {

    @Override
    public Swagger mapDocumentation(Documentation from) {
        final Swagger swagger = super.mapDocumentation(from);

        if (Objects.isNull(swagger)) {
            return null;
        }

        Map<String, Path> map__ = mapOriginalSortedApiListings(from.getApiListings());
        if (map__ != null) {
            swagger.setPaths(map__);
        }

        return swagger;
    }

    private Map<String, Path> mapOriginalSortedApiListings(Multimap<String, ApiListing> apiListings) {
        Map<String, Path> paths = new LinkedHashMap<>();
        for (ApiListing each : apiListings.values()) {
            for (ApiDescription api : each.getApis()) {
                paths.put(api.getPath(), mapOperations(api, Optional.fromNullable(paths.get(api.getPath()))));
            }
        }
        return paths;
    }

    private Path mapOperations(ApiDescription api, Optional<Path> existingPath) {
        Path path = existingPath.or(new Path());
        for (springfox.documentation.service.Operation each : nullToEmptyList(api.getOperations())) {
            Operation operation = mapOperation(each);
            path.set(each.getMethod().toString().toLowerCase(), operation);
        }
        return path;
    }
}



第4步:用拓展的两个 bean 覆盖默认的; 第5步:指定排序规则

代码合在一起,方便查看

/*
 *  Copyright 2019 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import com.google.common.collect.Ordering;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import springfox.documentation.OperationNameGenerator;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.Operation;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.operation.OperationReader;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import springfox.documentation.swagger2.mappers.ServiceModelToSwagger2Mapper;

import java.util.List;
/**
 * @author Yahuan Jin
 * @since 2019.08.24
 */
@EnableSwagger2
@Configuration
public class Swagger2Config {

    @Bean
    public Docket v1ApiDocket() {
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com"))
                .paths(PathSelectors.any())
                .build();

        // Support position
        docket.apiDescriptionOrdering(new Ordering<ApiDescription>() {
            @Override
            public int compare(ApiDescription a, ApiDescription b) {
                final List<Operation> leftList = a.getOperations();
                final List<Operation> rightList = b.getOperations();
                return Integer.compare(leftList.get(0).getPosition(), rightList.get(0).getPosition());
            }
        });

        return docket;
    }

    @Primary
    @Bean(name = "default")
    public OperationReader operationReader(
            DocumentationPluginsManager pluginsManager, OperationNameGenerator nameGenerator) {
        return new SwaggerSupportPositionApiOperationReader(pluginsManager, nameGenerator);
    }

    @Primary
    @Bean(name = "serviceModelToSwagger2Mapper")
    public ServiceModelToSwagger2Mapper SwaggerOriginalSortedServiceModelToSwagger2MapperImpl() {
        return new SwaggerOriginalSortedServiceModelToSwagger2MapperImpl();
    }
}



最后查看效果



针对

2.7

(不包括)之后的版本需要做另外的处理

由于默认的前端UI包

springfox-swagger-ui

移除了排序功能,所以即使后端接口数据排过序了,前端照样会乱。

我的解决方案是使用第三方提供的前端UI包。比如


swagger-bootstrap-ui



knife4j

,新的 api doc 页面可以和默认的同时使用,如果不需要

springfox-swagger-ui

或者

swagger-bootstrap-ui

直接去掉依赖就行了。

http://localhost:8080/swagger-ui.htm

http://localhost:8080/doc.html

maven 的配置方式,自己补上版本号

<properties>
        <!-- springfox-swagger: swagger api doc -->
        <springfox-swagger.version>2.7.0</springfox-swagger.version>
        <!-- knife4j: swagger ui -->
        <knife4j.version>2.0.8</knife4j.version>
</properties>

<dependencies>
		<dependency>
               <groupId>io.springfox</groupId>
			   <artifactId>springfox-swagger2</artifactId>
			   <version>${springfox.version}</version>
		</dependency>
		<dependency>
			   <groupId>io.springfox</groupId>
			   <artifactId>springfox-swagger-ui</artifactId>
			   <version>${springfox.version}</version>
		</dependency>
		<dependency>
				<groupId>com.github.xiaoymin</groupId>
				<artifactId>knife4j-spring-ui</artifactId>
				<version>${knife4j.version}</version>
		</dependency>
</dependencies>