【swagger动态配置输入参数忽略某些字段】
文章目录
- 一,背景
- 1.介绍
- 2. 现有做法1
- 3. springfox支持的方式2
- 二,问题
- 三,思路
- 四,实现
- 1. 定义注解SwaggerInputFieldIgnore
- 2. 实现WebMvcOpenApiTransformationFilter
- 3. 实现WApiListingBuilderPlugin
- 五,结果
一,背景
1.介绍
使用springfox生成swagger.json文件,然后导入yapi,进行展示;当使用的request和response有共用的模型时,一般是需要输入的模型字段会少一些,而输出的模型信息字段会更多些。
比如:
以用户管理为例,定义了如下结构
/**
* swagger 用户测试方法
*
* @author ruoyi
*/
@Anonymous
@Api("用户信息管理")
@RestController
@RequestMapping("/test/user")
public class TestController extends BaseController
{
private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();
{
users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));
users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
}
@ApiOperation("获取用户列表")
@GetMapping("/list")
public R<List<UserEntity>> userList()
{
List<UserEntity> userList = new ArrayList<UserEntity>(users.values());
return R.ok(userList);
}
@ApiOperation("新增用户")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class),
@ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class)
// @ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class)
})
@PostMapping("/save")
public R<String> save(@ApiIgnore UserEntity user)
{
if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
{
return R.fail("用户ID不能为空");
}
users.put(user.getUserId(), user);
return R.ok();
}
@ApiOperation("更新用户")
@PutMapping("/update")
public R<String> update(@RequestBody UserEntity user)
{
if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
{
return R.fail("用户ID不能为空");
}
if (users.isEmpty() || !users.containsKey(user.getUserId()))
{
return R.fail("用户不存在");
}
users.remove(user.getUserId());
users.put(user.getUserId(), user);
return R.ok();
}
}
@Data
@ApiModel(value = "UserEntity", description = "用户实体")
class UserEntity extends BaseEntity
{
@ApiModelProperty("用户ID")
private Integer userId;
@ApiModelProperty("用户名称")
private String username;
@ApiModelProperty("用户密码")
private String password;
@ApiModelProperty("用户手机")
private String mobile;
}
如此,则在用户更新和用户列表查询时,共用了UserEntity 模型。对于大部分实体共用的字段(BaseEntity),比如:创建时间,更新时间,是否删除等一并在yapi侧的请求api进行了展示,而对于前端开发同学来讲,这些都是非必需的。
我们的目标是:在前端传参数时,只使用必要的参数,其他参数不展示。
2. 现有做法1
定义一个新的DTO,比如:UpdateUserDTO,只包含必要的部分字段
弊端:
增加了一个新的类,每次需要copy一些字段,有些冗余。
3. springfox支持的方式2
使用@ApiIgnore注解,如上action插入用户时所做。
弊端:
需要使用@ApiImplicitParams和 @ApiImplicitParam注解,在参数比较多时,编写麻烦。
二,问题
以上都会比较繁琐,新增模型DTO后会有转换的编码操作;使用@ApiIgnore和@ApiImplicitParam时需要写好多跟业务逻辑无关的注解配置;二者更新时都有一些工作量存在;那么有没有一种更好的方式,既可以共用模型实体(UserEntity),又可以使用少量的编码,去除掉在用户输入时的各种非必需字段
三,思路
step1. 添加注解@SwaggerInputFieldIgnore,标识作为输入参数时,忽略的字段
step2. 在swagger加载各种@API配置时,解析出使用了哪些模型实体,尤其是有@SwaggerInputFieldIgnore标识的模型
step3. 在生成swagger.json内容时,通过反射处理@SwaggerInputFieldIgnore标识的字段,在结果中忽略该字段
四,实现
思路算是比较清晰,在实际编码实现的过程中,对于swagger的自动化配置原理以及spring-plugin的机制需要有必要的知识储备,我把最终编码结果整理如下:
1. 定义注解SwaggerInputFieldIgnore
/**
* swagger schema模型中,
* 1. 在作为body入参的场景下,忽略某些字段
* 2. 在作为出参的场景下,不必忽略某些字段
*
* 比如:
* @ApiOperation("获取用户列表, UserEntity作为出参")
* @GetMapping("/list")
* public R<List<UserEntity>> userList();
*
* @ApiOperation("更新用户,UserEntity作为入参")
* @PutMapping("/update")
* public R<String> update(@RequestBody UserEntity user)
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SwaggerInputFieldIgnore {
}
2. 实现WebMvcOpenApiTransformationFilter
@Component
public class InputFieldIgnoreOpenApiTransformationFilter implements WebMvcOpenApiTransformationFilter {
private static final Logger log = getLogger(InputFieldIgnoreOpenApiTransformationFilter.class);
public static final String SUFFIX_INPUT = "-input";
private Map<String, InjectedInputSchema> injectedSchemaMap = new HashedMap();
@Autowired
private ModelRetriveApiListingPugin modelRetriveApiListingPugin;
@Value("${swagger.fieldIgnore.Enable:true}")
private Boolean fieldIgnoreEnable;
public static final class InjectedInputSchema {
String originKey;
String injectedKey;
Class schemaClazz;
Schema injectedSchema;
public InjectedInputSchema(String originKey, Class schemaClazz) {
this.originKey = originKey;
this.injectedKey = originKey + "-input";
this.schemaClazz = schemaClazz;
}
}
/**
* 1. 根据注解@SwaggerInputFieldIgnore,解析余下的字段
* 2. 增加对应的schema
*
* @param oas
*/
private void processInputInject(OpenAPI oas) {
if (!fieldIgnoreEnable) {
return;
}
// 1. 解析paths,替换$ref; 同时,收集schema元数据用于补充schema-input。
for (PathItem each : oas.getPaths().values()) {
appendRefWithInput(each.getPut());
appendRefWithInput(each.getPost());
}
if(MapUtils.isEmpty(injectedSchemaMap)){
return;
}
// 2. 补充schema-input
for (InjectedInputSchema each : injectedSchemaMap.values()) {
// 2.1. 构造schema
Schema old = oas.getComponents().getSchemas().get(each.originKey);
Schema schema = constructSchema(each, old);
// 2.2. 加入oas.components.schemas
oas.getComponents().getSchemas().put(each.injectedKey, schema);
}
}
private void appendRefWithInput(Operation operation) {
if (operation == null || operation.getRequestBody() == null) {
return;
}
try {
Content content = operation.getRequestBody().getContent();
MediaType mediaType = content.get(org.springframework.http.MediaType.APPLICATION_JSON_VALUE);
Schema schema = mediaType.getSchema();
String $ref = schema.get$ref();
String originName = $ref.substring($ref.lastIndexOf("/") + 1);
QualifiedModelName modelName = modelRetriveApiListingPugin.getApiModels().get(originName);
if (modelName != null) {
schema.set$ref($ref + SUFFIX_INPUT);
injectedSchemaMap.put(originName, new InjectedInputSchema(originName, constructClazz(modelName)));
}
} catch (Exception e) {
log.error("error occured", e);
}
}
private static Class<?> constructClazz(QualifiedModelName modelName) throws ClassNotFoundException {
return Class.forName(modelName.getNamespace() + "." + modelName.getName());
}
private Schema constructSchema(InjectedInputSchema each, Schema old) {
Schema result = new ObjectSchema();
result.title(each.injectedKey);
result.type(old.getType());
result.description(old.getDescription());
HashMap<String, Schema> props = new HashMap<>(old.getProperties());
Set<String> removingKey = new HashSet();
props.keySet().forEach(filedName -> {
Field field = ReflectionUtils.findField(each.schemaClazz, filedName);
SwaggerInputFieldIgnore anno = AnnotationUtils.findAnnotation(field, SwaggerInputFieldIgnore.class);
if (anno != null) {
removingKey.add(filedName);
}
});
removingKey.forEach(field -> props.remove(field));
result.setProperties(props);
return result;
}
@Override
public OpenAPI transform(OpenApiTransformationContext<HttpServletRequest> context) {
OpenAPI openApi = context.getSpecification();
processInputInject(openApi);
return openApi;
}
@Override
public boolean supports(DocumentationType delimiter) {
return delimiter == DocumentationType.OAS_30;
}
}
3. 实现WApiListingBuilderPlugin
@Order(value = Ordered.LOWEST_PRECEDENCE)
@Component
public class ModelRetriveApiListingPugin implements ApiListingBuilderPlugin {
private static final Logger log = LoggerFactory.getLogger(ModelRetriveApiListingPugin.class);
private Map<String, QualifiedModelName> apiModels = new HashedMap();
@Override
public void apply(ApiListingContext apiListingContext) {
Field filed = ReflectionUtils.findField(ApiListingBuilder.class, "modelSpecifications");
filed.setAccessible(true);
Map<String, ModelSpecification> specsMap = (Map<String, ModelSpecification>) ReflectionUtils.getField(filed, apiListingContext.apiListingBuilder());
retriveApiModels(specsMap.values());
}
private void retriveApiModels(Collection<ModelSpecification> specs) {
// Collection<ModelSpecification> specs = each.getModelSpecifications().values();
specs.forEach(spec -> {
ModelKey modelKey = spec.getCompound().get().getModelKey();
QualifiedModelName modelName = modelKey.getQualifiedModelName();
apiModels.put(modelName.getName(), modelName);
log.info(modelName.toString());
});
}
Map<String, QualifiedModelName> getApiModels() {
return apiModels;
}
@Override
public boolean supports(DocumentationType delimiter) {
return true;
}
}
五,结果
- 加入以上java文件,并设置模型字段注解后,可以看到产出的swagger.json内容已经在入参数中忽略掉了注解字段
- 导入yapi后,配置忽略的字段已经没有了,_ !