前言
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默认按照首字母顺序来显示接口和字段。
字段可以通过@ApiModelProperty
的position
属性来指定顺序,而接口相关的注解@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); } } } } |
---|
参考链接
- 重新认识Swagger和Springfox
- Swagger2 @ApiIgnore注解忽略接口在swagger-ui.html中显示
- spring boot集成swagger之springfox-boot-starter配置指定paths()(四)
- swagger扩展为按代码定义顺序展示接口和字段
注意
本文最后更新于 September 1, 2021,文中内容可能已过时,请谨慎使用。