目录

Swagger问题汇总

前言

SpringFox是一个开源的用于生成API文档接口的框架,支持多种API文档的格式。可以用SpringFox来整合Spring和Swagger,本文使用的Swagger和SpringFox版本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
  <version>2.9.2</version>
</dependency>
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>2.9.2</version>
</dependency>

隐藏指定的接口

使用@ApiIgnore

在想要隐藏的方法上添加@ApiIgnore注解即可,该注解还可以添加在类上和方法参数上。

使用SpringFox提供的Docket类的paths()来定制

paths()支持两种表达式,一种是Java的正则表达式,一种是Spring框架的Ant表达式。

通过Docket类的链式调用来实现:new Docket().select().apis().paths().build()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Bean
public Docket api() {
    return new Docket(DocumentationType.SWAGGER_2)
        .groupName("api")
        .apiInfo(metaData())
        .ignoredParameterTypes(Authentication.class)
        .select()
        .apis(RequestHandlerSelectors.basePackage("com.test"))
        .paths(PathSelectors.regex("/api/.*"))
        .build()
        .securitySchemes(Collections.singletonList(securitySchema()))
        .securityContexts(Collections.singletonList(securityContext()));
}

private ApiInfo metaData() {
    return new ApiInfoBuilder()
        .title("Test API")
        .description("Test Application")
        .version("0.0.1")
        .contact(new Contact("Lewky", "lewky.cn", "lewky@test.com"))
        .build();
}

private OAuth securitySchema() {
    final List<AuthorizationScope> authorizationScopeList = new ArrayList<>();
    authorizationScopeList.add(new AuthorizationScope("read", "read all"));
    authorizationScopeList.add(new AuthorizationScope("trust", "trust all"));
    authorizationScopeList.add(new AuthorizationScope("write", "access all"));

    final List<GrantType> grantTypes = new ArrayList<>();
    final GrantType creGrant = new ResourceOwnerPasswordCredentialsGrant("/oauth/token");

    grantTypes.add(creGrant);

    return new OAuth("oauth2schema", authorizationScopeList, grantTypes);
}


private SecurityContext securityContext() {
    return SecurityContext.builder()
        .securityReferences(defaultAuth()).forPaths(PathSelectors.ant("/api/**"))
        .build();
}

下面是ant表达式的写法,用的是AntPathMatcher来匹配文档路径:

  • ?匹配一个字符
  • *匹配0个或多个字符
  • **匹配0个或多个目录
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Bean
public Docket oauthApi() {
    return new Docket(DocumentationType.SWAGGER_2)
        .groupName("oauth")
        .apiInfo(metaData())
        .ignoredParameterTypes(Authentication.class)
        .select()
        .apis(RequestHandlerSelectors.any())
        .paths(PathSelectors.ant("/oauth/**"))
        .build()
        .securitySchemes(Collections.singletonList(securitySchema()))
        .securityContexts(Collections.singletonList(securityContext()));
}

定义Response中Model的map字段显示

Swagger2在显示一个接口的Response时,如果Model中存在map类型的字段(比如下面的customFields),则会在Example Value中显示为:

1
2
3
4
5
"customFields": {
  "additionalProp1": {},
  "additionalProp2": {},
  "additionalProp3": {}
}

这个map里的字段是动态生成的,如果想要显示成对应的字段,需要实现ModelPropertyBuilderPlugin接口,然后重写supports()apply()这两个方法,可以参考框架提供的实现类ApiModelPropertyPropertyBuilder来写。

对于自定义的类,需要注意的是注入的顺序,需要在框架的默认实现类之后注入。可以使用@Order来控制注入顺序,默认是最低优先级的注入顺序。

功能需求:map对象的字段是由Hibernate的hbm.xml配置的动态table,需要读取这个xml里的字段,然后将其转为对应的pojo中的字段。

Map对象的字段重写的具体思路如下:

  • 在map字段上添加@ApiModelProperty(notes = "xxx")。使用notes属性的原因是,该字段被Swagger废弃了,这里用来实现自定义的功能就不会与原框架的功能产生冲突。
  • 读取注解中notes的值,解析Hibernate的hbm.xml,根据notes值找到对应的结点并解析。
  • 将解析得到的结点用javassist生成一个类,同一个类生成一次即可,别反复生成,浪费性能。
  • 将生成的类作为当前map字段的解析类型,swagger是用的fasterxml来将pojo转化为json的。
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
@Component
@Order
@Slf4j
public class HashMapModelPropertyBuilder implements ModelPropertyBuilderPlugin {
    @Autowired
    private TypeResolver typeResolver;

    @Override
    public boolean supports(final DocumentationType delimiter) {
        return true;
    }

    @Override
    public void apply(final ModelPropertyContext context) {
        Optional<ApiModelProperty> annotation = Optional.absent();

        if (context.getAnnotatedElement().isPresent()) {
          annotation = annotation.or(findApiModePropertyAnnotation(context.getAnnotatedElement().get()));
        }
        if (context.getBeanPropertyDefinition().isPresent()) {
          annotation = annotation.or(findPropertyAnnotation(
              context.getBeanPropertyDefinition().get(),
              ApiModelProperty.class));
        }

		// 只有在map类型的字段上使用了ApiModelProperty注解,并使用了notes属性才进行字段的解析
        if (annotation.isPresent() && context.getBeanPropertyDefinition().isPresent()) {
            final String tableName = annotation.get().notes();
            if (StringUtils.isBlank(applyToEntity)) {
                return;
            }

            final BeanPropertyDefinition beanPropertyDefinition = context.getBeanPropertyDefinition().get();
            if (!HashMap.class.equals(beanPropertyDefinition.getField().getRawType())) {
                return;
            }

			// 最关键的功能实现:解析xml并生成对应的类,再设置为当前的Map字段的解析类型
            context.getBuilder().type(typeResolver.resolve(createRefModel(tableName)));
        }
    }

    // Dynamic generated class package prefix for HashMap model.
    private final static String BASE_PACKAGE_RPEFIX = "com.test.swagger.model.";

    private Class createRefModel(final String name) {
        final ClassPool pool = ClassPool.getDefault();
        final String className = BASE_PACKAGE_RPEFIX + name;
        CtClass ctClass = pool.getOrNull(className);
        Class result = null;

        if (ctClass == null) {
            // Create new public class.
            ctClass = pool.makeClass(BASE_PACKAGE_RPEFIX + name);
            ctClass = loadCustomTableHbmXml(name, ctClass);
            try {
                result = ctClass.toClass();
            } catch (final CannotCompileException e) {
                log.error("Cannot create ref model.", e);
            }
        } else {
            try {
                result = Class.forName(className);
            } catch (final ClassNotFoundException e) {
                log.error("Cannot load Class: {}.", className, e);
            }
        }

        return result;
    }

    private CtClass loadCustomTableHbmXml(final String entityName, final CtClass ctClass) {
		// 解析hbm.xml
        final Resource resource = new ClassPathResource("hibernate/CustomTable.hbm.xml");
        final SAXReader saxReader = new SAXReader();
        Document doc = null;
        try {
            doc = saxReader.read(resource.getInputStream());
        } catch (final Exception e) {
            log.error("Failed to read CustomTable.hbm.xml.", e);
        }
        final Element rootElement = doc.getRootElement();

        final Iterator<Element> iterator = rootElement.elementIterator("class");
        Element target = null;
        while(iterator.hasNext()) {
            final Element element = iterator.next();
            if (StringUtils.equals(entityName, element.attributeValue("entity-name", StringUtils.EMPTY))) {
                target = element;
                break;
            }
        }
        if (target == null) {
            return ctClass;
        }

        List<Element> elements = new ArrayList<>();
        final List<Element> properties = target.elements("property");
        final List<Element> components = target.elements("component");
        elements.addAll(properties);
        elements.addAll(components);
        try {
            for (final Element element : elements) {
                createField(element, ctClass);
            }
        } catch (final Exception e) {
            log.error("Cannot create ref model.", e);
        }

        return ctClass;
    }

    private CtField createField(final Element element, final CtClass ctClass) throws NotFoundException, CannotCompileException {
        final String key = element.attributeValue("name", StringUtils.EMPTY);
		// 过滤掉一些不需要的结点
        if (IsIgnoreElement(key)) {
            return null;
        }

		// 属于业务逻辑,获取到字段的类型:String、LocalDate等
        final CustomFieldType customFieldType = CustomFieldType.findTypeByColumnName(key).orElse(null);
        if (customFieldType == null) {
            return null;
        }

        CtClass fieldType = null;
        CtField ctField = null;
        final ClassPool pool = ClassPool.getDefault();
		// 这里可以根据需要将字段名字替换为其他名字
        final String jsonKey = key;

        switch (customFieldType) {
        case SELECTION:
            fieldType = pool.get(EmbedCodelistListDemo.class.getName());
            break;

        default:
            fieldType = pool.get(customFieldType.getType().getName());
            break;
        }

        ctField = new CtField(fieldType, jsonKey, ctClass);
        ctField.setModifiers(Modifier.PUBLIC);
        ctClass.addField(ctField);

        return ctField;
    }

    private boolean IsIgnoreElement(final String key) {
        boolean result = false;
        switch (key) {
        case "domain_id":
        case "ref_entity_name":
            result = true;
            break;
        default:
            break;
        }
        return result;
    }

    // Demo model
    static class EmbedCodelistListDemo extends ArrayList<EmbedCodelist> {
        private static final long serialVersionUID = 1L;
    }

}

这里唯一需要强调的一点是:如果Map中存在List类型的字段,比如List<xxDto>,若要在Swagger的文档中也将这个xxDto也显示到Example Value里,可以定义一个类,继承List<xxDto>,如上述代码中最后定义的静态内部类EmbedCodelistListDemo

之所以这样实现是因为javassist来生成一个泛型List太困难(可能是我没找到正确的接口),还是直接定义这样一个类,让Java自己帮我们搞定类型来得更简单准确。

按类中字段定义的顺序展示字段

Swagger默认按照首字母顺序来显示接口和字段。

字段可以通过@ApiModelPropertyposition属性来指定顺序,而接口相关的注解@ApiOperation则不行。但不管如何,直接靠人工添加注解来排序是不现实的;可以通过重写插件来便捷地解决这个问题。

可以通过实现ModelPropertyBuilderPlugin重写字段顺序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package test;

import static springfox.documentation.schema.Annotations.findPropertyAnnotation;
import static springfox.documentation.swagger.schema.ApiModelProperties.findApiModePropertyAnnotation;

import java.lang.reflect.Field;

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.google.common.base.Optional;

import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.ModelPropertyBuilderPlugin;
import springfox.documentation.spi.schema.contexts.ModelPropertyContext;

@Component
@Order
@Slf4j
public class CustomApiModelPropertyPositionBuilder implements ModelPropertyBuilderPlugin {

    @Override
    public boolean supports(final DocumentationType delimiter) {
        return true;
    }

    @Override
    public void apply(final ModelPropertyContext context) {
        final Optional<BeanPropertyDefinition> beanPropertyDefinitionOpt = context.getBeanPropertyDefinition();
        Optional<ApiModelProperty> annotation = Optional.absent();
        if (context.getAnnotatedElement().isPresent()) {
            annotation = annotation.or(findApiModePropertyAnnotation(context.getAnnotatedElement().get()));
        }
        if (context.getBeanPropertyDefinition().isPresent()) {
            annotation = annotation.or(findPropertyAnnotation(context.getBeanPropertyDefinition().get(), ApiModelProperty.class));
        }
        if (beanPropertyDefinitionOpt.isPresent()) {
            final BeanPropertyDefinition beanPropertyDefinition = beanPropertyDefinitionOpt.get();
            if (annotation.isPresent() && annotation.get().position() != 0) {
                return;
            }
            final AnnotatedField field = beanPropertyDefinition.getField();
            final Class<?> clazz = field.getDeclaringClass();
            final Field[] declaredFields = clazz.getDeclaredFields();
            Field declaredField;
            try {
                declaredField = clazz.getDeclaredField(field.getName());
            } catch (NoSuchFieldException | SecurityException e) {
                log.error("Error.", e);
                return;
            }
            final int indexOf = ArrayUtils.indexOf(declaredFields, declaredField);
            if (indexOf != -1) {
                context.getBuilder().position(indexOf);
            }
        }
    }

}

参考链接

警告
本文最后更新于 September 1, 2021,文中内容可能已过时,请谨慎使用。