• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Swagger自定义字段展示

武飞扬头像
Ralph Chen
帮助1

前言

swagger是目前使用最多的文档生成工具,它能满足绝大多数的需求,但总有一些场景是它默认的逻辑所不能满足的,为此,springfox提供了一些扩展方法,只需要实现对应的Plugin接口就可以覆盖掉swagger默认的逻辑,以自定义的逻辑执行

接下来讲述本文所自定义的方法:在swagger中,如果在get请求中使用了Object参数,并且没有加@Requestbody或者@RequestParam这类的注解时,swagger会将该Object的属性拆解为一个个字段。

本贴使用的sprinfox版本为springfox-swagger2-3.0.0

3.0.0与之前版本有较大不同所以网上相关贴子比较少

接口:

@ApiOperation(value = "分页查询", notes = "分页查询")
@GetMapping("/item/page")
public Result<Page<SystemDictItem>> getSystemDictItemPage(Page<SystemDictItem> page, Integer dictId) {
    return Result.success(systemDictService.getItemPage(page, dictId));
}

接口由一个page对象和一个Integer类的id组成

swagger展示效果:

学新通

可以看到,swagger将page对象的所有字段全部拆解了出来,包括子对象records的字段

protected List<T> records;
protected long total;
protected long size;
protected long current;
protected List<OrderItem> orders;
protected boolean optimizeCountSql;
protected boolean isSearchCount;
protected boolean hitCount;
protected String countId;
protected Long maxLimit;

问题一:我们其实并不需要这么多字段,关于page对象,我们一般只需要size和current这两个字段即可,其他字段会显得比较多余,如果这是我们自定义的对象,可以通过加@ApiModelProperty(hidden = true)来进行字段的隐藏,但page对象是mybatisplus自带的,我们无法进行修改,所以说,使用swagger的默认逻辑是无法隐藏page的其他字段的,这就需要我们实现springfox的plugin接口来进行自定义逻辑了

问题二:当我们在对象属性上加了@NotNull、@NotBlan等注解时,swagger在拆解的过程中会将该字段的必填改为true,但其实在查询的时候我们并不希望该字段必填。这个问题我们同样可以通过问题一同样的方法进行修改。

实现Plugin接口

能过debug源码我们发现,swagger将Object对象解析成单个属性的过程是在OperationParameterReader类中进行完成的,而这个类实现了``OperationBuilderPlugin`接口

那接下来,我们只需要也实现``OperationBuilderPlugin接口并重写它的apply和supports方法即可,关于这一步可以模仿OperationParameterReader`类的实现逻辑,只需要将我们需要的内容加进去即可

首先需要自定义两个注解

@ApiIgnoreField:用来指定该对象需要哪些属性

package com.ralph.common.docs.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIgnoreField {
    
    String[] value();
}

@ApiNeedField:用来指定哪些字段需要排除掉

package com.ralph.common.docs.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiNeedField {

    String[] value();
}

自定义处理类:

package com.ralph.common.docs.support;

import cn.hutool.core.collection.CollUtil;
import com.fasterxml.classmate.ResolvedType;
import com.谷歌.common.collect.Lists;
import com.ralph.common.docs.annotation.ApiIgnoreField;
import com.ralph.common.docs.annotation.ApiNeedField;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import springfox.documentation.builders.BuilderDefaults;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.common.Compatibility;
import springfox.documentation.schema.Collections;
import springfox.documentation.schema.Maps;
import springfox.documentation.schema.ScalarTypes;
import springfox.documentation.service.Parameter;
import springfox.documentation.service.RequestParameter;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.EnumTypeDeterminer;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.ParameterContext;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.operation.OperationParameterReader;
import springfox.documentation.spring.web.readers.operation.ParameterAggregator;
import springfox.documentation.spring.web.readers.parameter.ExpansionContext;
import springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterExpander;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Component
@Order(Integer.MIN_VALUE)
@Slf4j
public class RalphCusParamHandler implements OperationBuilderPlugin {

    @Autowired
    public RalphCusParamHandler(ModelAttributeParameterExpander expander, EnumTypeDeterminer enumTypeDeterminer, ParameterAggregator aggregator) {
        this.expander = expander;
        this.enumTypeDeterminer = enumTypeDeterminer;
        this.aggregator = aggregator;
    }
    private static final Logger LOGGER = LoggerFactory.getLogger(OperationParameterReader.class);
    private final ModelAttributeParameterExpander expander;
    private final EnumTypeDeterminer enumTypeDeterminer;
    private final ParameterAggregator aggregator;

    private boolean change = false;
    @Autowired
    private DocumentationPluginsManager pluginsManager;

    public void apply(OperationContext context) {
        change = false;
        context.operationBuilder().parameters(context.getGlobalOperationParameters());
        List<Compatibility<Parameter, RequestParameter>> compatibilities = this.readParameters(context);

        if (change) {
            // 由于OperationBuilder类没有提供set方法,需要通过反射给parameters赋值
            try {
                Field parametersField = OperationBuilder.class.getDeclaredField("parameters");
                parametersField.setAccessible(true);
                List<Parameter> parameters = compatibilities.stream().map(Compatibility::getLegacy).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
                List<Parameter> source = BuilderDefaults.nullToEmptyList(parameters);
                parametersField.set(context.operationBuilder(), source);

                Field requestParameters = OperationBuilder.class.getDeclaredField("requestParameters");
                requestParameters.setAccessible(true);
                Set<RequestParameter> requestParametersList = compatibilities.stream().map(Compatibility::getModern).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
                Set<RequestParameter> requestSource = BuilderDefaults.nullToEmptySet(requestParametersList);
                requestParameters.set(context.operationBuilder(), requestSource);

            } catch (Exception e) {
                log.error("动态更改swagger参数错误", e);
            }
        }else {
            context.operationBuilder().parameters((List)compatibilities.stream().map(Compatibility::getLegacy).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList()));
            context.operationBuilder().requestParameters(new HashSet(context.getGlobalRequestParameters()));
            Collection<RequestParameter> requestParameters = (Collection)compatibilities.stream().map(Compatibility::getModern).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
            context.operationBuilder().requestParameters(this.aggregator.aggregate(requestParameters));
        }
    }

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

    private List<Compatibility<Parameter, RequestParameter>> readParameters(OperationContext context) {
        List<ResolvedMethodParameter> methodParameters = context.getParameters();
        List<Compatibility<Parameter, RequestParameter>> parameters = new ArrayList();
        LOGGER.debug("Reading parameters for method {} at path {}", context.getName(), context.requestMappingPattern());
        int index = 0;
        Iterator var5 = methodParameters.iterator();

        while(var5.hasNext()) {
            ResolvedMethodParameter methodParameter = (ResolvedMethodParameter)var5.next();

            // 判断参数是否加了自定义注解以及注解的值
            Optional<ApiIgnoreField> ignoreFieldAnnotation = methodParameter.findAnnotation(ApiIgnoreField.class);
            Optional<ApiNeedField> needFieldAnnotation = methodParameter.findAnnotation(ApiNeedField.class);
            List<String> needList = new ArrayList<>();
            List<String> ignoreList = new ArrayList<>();
            if (needFieldAnnotation.isPresent()) {
                String[] fields = needFieldAnnotation.get().value();
                needList.addAll(Lists.newArrayList(fields));
            }else if (ignoreFieldAnnotation.isPresent()){
                String[] fields = ignoreFieldAnnotation.get().value();
                ignoreList.addAll(Lists.newArrayList(fields));
            }

            LOGGER.debug("Processing parameter {}", methodParameter.defaultName().orElse("<unknown>"));
            ResolvedType alternate = context.alternateFor(methodParameter.getParameterType());
            if (!this.shouldIgnore(methodParameter, alternate, context.getIgnorableParameterTypes())) {
                ParameterContext parameterContext = new ParameterContext(methodParameter, context.getDocumentationContext(), context.getGenericsNamingStrategy(), context, index  );
                if (this.shouldExpand(methodParameter, alternate)) {
                    List<Compatibility<Parameter, RequestParameter>> expand = this.expander.expand(new ExpansionContext("", alternate, context));
                    // 处理需要的字段
                    if (CollUtil.isNotEmpty(needList)){
                        expand = expand.stream().filter(item->{
                            String modelName = item.getModern().map(RequestParameter::getName).orElse("");
                            String legacyName = item.getLegacy().map(Parameter::getName).orElse("");
                            return needList.stream().anyMatch(need-> need.matches(modelName) && need.matches(legacyName));
                        }).collect(Collectors.toList());
                        change = true;
                    }else if (CollUtil.isNotEmpty(ignoreList)){
                         // 处理不需要的字段
                        expand = expand.stream().filter(item->{
                            String modelName = item.getModern().map(RequestParameter::getName).orElse("");
                            String legacyName = item.getLegacy().map(Parameter::getName).orElse("");
                            return ignoreList.stream().noneMatch(ignore-> ignore.matches(modelName) && ignore.matches(legacyName));
                        }).collect(Collectors.toList());
                        change = true;
                    }
                    // 对象类型的参数都改为非必填
                    expand = expand.stream().map(item->{
                        RequestParameterBuilder requestParameterBuilder = new RequestParameterBuilder();
                        RequestParameterBuilder resRequestParameter = requestParameterBuilder.copyOf(item.getModern().get());
                        Parameter sourceParameter = item.getLegacy().get();
                        ParameterBuilder parameterBuilder = new ParameterBuilder();
                        ParameterBuilder resParameter = parameterBuilder.name(sourceParameter.getName()).allowableValues(sourceParameter.getAllowableValues()).allowMultiple(sourceParameter.isAllowMultiple()).defaultValue(sourceParameter.getDefaultValue()).description(sourceParameter.getDescription()).modelRef(sourceParameter.getModelRef()).parameterAccess(sourceParameter.getParamAccess()).parameterType(sourceParameter.getParamType()).required(sourceParameter.getRequired()).type((ResolvedType)sourceParameter.getType().orElse((ResolvedType) null)).hidden(sourceParameter.isHidden()).allowEmptyValue(sourceParameter.isAllowEmptyValue()).order(sourceParameter.getOrder()).vendorExtensions(sourceParameter.getVendorExtentions()).collectionFormat(sourceParameter.getCollectionFormat());
                        if (item.getModern().isPresent() && item.getLegacy().isPresent() &&
                                (item.getModern().get().getRequired() || item.getLegacy().get().isRequired())){
                            resParameter.required(false);
                            try {
                                Field required = RequestParameterBuilder.class.getDeclaredField("required");
                                required.setAccessible(true);
                                required.set(resRequestParameter,false);
                            } catch (Exception e){
                                // 无作用
                                resRequestParameter.required(false);
                            }
                            change = true;
                            return new Compatibility<Parameter, RequestParameter>(resParameter.build(), resRequestParameter.build());
                        }
                        return item;
                    }).collect(Collectors.toList());
                    parameters.addAll(expand);
                } else {
                    parameters.add(this.pluginsManager.parameter(parameterContext));
                }
            }

        }
        return (List)parameters.stream().filter(this.hiddenParameter().negate()).collect(Collectors.toList());
    }

    private Predicate<Compatibility<Parameter, RequestParameter>> hiddenParameter() {
        return (c) -> {
            return (Boolean)c.getLegacy().map(Parameter::isHidden).orElse(false);
        };
    }

    private boolean shouldIgnore(
            final ResolvedMethodParameter parameter,
            ResolvedType resolvedParameterType,
            final Set<Class> ignorableParamTypes) {
        if (ignorableParamTypes.contains(resolvedParameterType.getErasedType())) {
            return true;
        } else {
            Stream var10000 = ignorableParamTypes.stream();
            Objects.requireNonNull(Annotation.class);
            var10000 = var10000.filter(item-> Annotation.class.isAssignableFrom((Class<?>) item));
            Objects.requireNonNull(parameter);
            return var10000.anyMatch(item->parameter.hasParameterAnnotation((Class<? extends Annotation>) item));
        }
    }

    private boolean shouldExpand(final ResolvedMethodParameter parameter, ResolvedType resolvedParamType) {
        return !parameter.hasParameterAnnotation(RequestBody.class) && !parameter.hasParameterAnnotation(RequestPart.class) && !parameter.hasParameterAnnotation(RequestParam.class) && !parameter.hasParameterAnnotation(PathVariable.class) && !ScalarTypes.builtInScalarType(resolvedParamType.getErasedType()).isPresent() && !this.enumTypeDeterminer.isEnum(resolvedParamType.getErasedType()) && !Collections.isContainerType(resolvedParamType) && !Maps.isMapType(resolvedParamType);
    }
}

处理类完成后,只需要在接口上增加对应注解即可实现想要的效果

@ApiOperation(value = "分页查询", notes = "分页查询")
@GetMapping("/page")
public Page<SystemDict> getSystemDictPage(@ApiNeedField({"size","current"}) Page<SystemDict> page,
                                          @ApiIgnoreField({"updateDate","updateBy"}) SystemDict systemDict) {
    return systemDictService.page(page, Wrappers.query(systemDict).lambda()
                                  .like(SystemDict::getType,systemDict.getType())
                                  .orderByDesc(SystemDict::getCreateDate));
}

如上接口,在swagger中展示的效果如下:

学新通

我们看到关于page的字段只展示了size和current,而SystemDict相关的字段并没有updateDate和updateBy这两个字段,这说明我们的改动已经成功了

如果想参考源码可以通过贴子底部跳转连接跳转查看相关代码


👍 欢迎前往博客主页查看更多内容

👍 如果觉得不错,期待您的点赞、收藏、评论、关注

👍 如有错误欢迎指正!

👍 Gitee地址:https://gitee.com/ralphchen/ralph-cloud

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfajgif
系列文章
更多 icon
同类精品
更多 icon
继续加载