swagger忽略类属性,生成apiModel

1. swagger遇到的坑和解决方案

前言

借鉴了swagger2自定义隐藏实体类属性_Shengbao Li的博客-CSDN博客

1.1 问题报错

15:38:49.636 [restartedMain] ERROR s.d.s.w.p.DocumentationPluginsBootstrapper - [scanDocumentation,98] - Unable to scan documentation context default,如下图所示。

报错代码位置:

public class RequestParameter {
    
   @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    RequestParameter that = (RequestParameter) o;
    return parameterIndex == that.parameterIndex &&
        Objects.equals(name, that.name) &&
        in == that.in &&
        Objects.equals(description, that.description) &&
        Objects.equals(required, that.required) &&
        Objects.equals(deprecated, that.deprecated) &&
        Objects.equals(hidden, that.hidden) &&
        Objects.equals(parameterSpecification, that.parameterSpecification) &&
        Objects.equals(precedence, that.precedence) &&
        //报错之处,存在重复数据
        Objects.equals(scalarExample, that.scalarExample) &&
        Objects.equals(examples, that.examples) &&
        Objects.equals(extensions, that.extensions);
  }  
​
}
@SuppressWarnings("deprecation")
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class OperationParameterReader implements OperationBuilderPlugin {
​
  @Override
  public void apply(OperationContext context) {
    context.operationBuilder().parameters(context.getGlobalOperationParameters());
    List<Compatibility<springfox.documentation.service.Parameter, RequestParameter>> compatibilities
        = readParameters(context);
    context.operationBuilder().parameters(
        compatibilities.stream()
            .map(Compatibility::getLegacy)
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(Collectors.toList()));
    context.operationBuilder().requestParameters(new HashSet<>(context.getGlobalRequestParameters()));
    //这段报错了,数组经过统计生成Set集合,集合中有重复的数据
    Collection<RequestParameter> requestParameters = compatibilities.stream()
        .map(Compatibility::getModern)
        .filter(Optional::isPresent)
        .map(Optional::get)
        .collect(toSet());
    context.operationBuilder()
        .requestParameters(aggregator.aggregate(requestParameters));
  }
}

1.2 应用环境以及应用程序

(1) 应用环境

springboot版本:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.13</version>
    <relativePath />
</parent>
  <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>12</java.version>
        <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
        。。。。。。。    
        <swagger.version>3.0.0</swagger.version>
    </properties>

swagger依赖

<!-- Swagger3依赖 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>${swagger.version}</version>
</dependency>

(2) 应用程序

实体类信息:

@Data
@ApiModel(value = "房屋",description = "")
public class HouseVo extends House
{
    /***楼信息*****/
    @ApiModelProperty(value = "楼信息")
    private Building aBuilding;
    ......
}

@Data
@ApiModel(value = "楼栋信息",description = "")
public class Building implements Serializable {
{
    private static final long serialVersionUID = 1L;
​
    /** 主键 */
    @ApiModelProperty(name = "id",value = "主键")
    private String id;
​
    /** 区域id */
    @ApiModelProperty(name = "locationId",value = "区域id")
    private String locationId;
      。。。。。。。。
}

1.3 问题分析与解决

(1) 问题分析

        按照OperationParameterReader#readParameters()方法debug,发现了请求中的参数的model的属性相对来说比较多,就是 List<Compatibility<springfox.documentation.service.Parameter, RequestParameter>> compatibilities 对象把RoomVo类中building对象的属性也加入到Compatibility数组中去了,由于俩个对象中都有id 字符串属性,且ApiModelProperty设置的name 也是一样的,导致了冲突;

(2) 解决方案

RoomVo生成ApiModel对象的时候,需要忽略类中的属性Building;

具体代码实现:继承 ModelAttributeParameterExpander类,重写private Set<PropertyDescriptor> propertyDescriptors(final Class<?> clazz) 方法,构造函数首行加super,其他的代码和父类一样,代码如下所示:

package com.ruoyi.common.utils.swagger;

import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.members.ResolvedField;
import com.fasterxml.classmate.members.ResolvedMember;
import com.fasterxml.classmate.members.ResolvedMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.common.Compatibility;
import springfox.documentation.schema.Maps;
import springfox.documentation.schema.ResolvedTypes;
import springfox.documentation.schema.ScalarTypes;
import springfox.documentation.schema.property.bean.AccessorsProvider;
import springfox.documentation.schema.property.field.FieldProvider;
import springfox.documentation.service.Parameter;
import springfox.documentation.service.RequestParameter;
import springfox.documentation.spi.schema.AlternateTypeProvider;
import springfox.documentation.spi.schema.EnumTypeDeterminer;
import springfox.documentation.spi.service.contexts.ParameterExpansionContext;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.parameter.ExpansionContext;
import springfox.documentation.spring.web.readers.parameter.ModelAttributeField;
import springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterExpander;
import springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterMetadataAccessor;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static java.util.Collections.emptySet;
import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static org.springframework.util.StringUtils.isEmpty;
import static springfox.documentation.schema.Collections.collectionElementType;
import static springfox.documentation.schema.Collections.isContainerType;


/**
 * 用于覆盖 ModelAttributeParameterExpander,大部分为源码,切记升级 swagger 版本后需重新修改源码
 */
@Primary
@Component
public class MyModelAttributeParameterExpander extends ModelAttributeParameterExpander {
    private static final Logger LOG = LoggerFactory.getLogger(ModelAttributeParameterExpander.class);
    private final FieldProvider fields;
    private final AccessorsProvider accessors;
    private final EnumTypeDeterminer enumTypeDeterminer;

    @Autowired
    private DocumentationPluginsManager pluginsManager;

    @Autowired
    public MyModelAttributeParameterExpander(
            FieldProvider fields,
            AccessorsProvider accessors,
            EnumTypeDeterminer enumTypeDeterminer) {
        super(fields, accessors, enumTypeDeterminer);
        this.fields = fields;
        this.accessors = accessors;
        this.enumTypeDeterminer = enumTypeDeterminer;

    }

    public List<Compatibility<Parameter, RequestParameter>> expand(
            ExpansionContext context) {

        List<Compatibility<Parameter, RequestParameter>> parameters = new ArrayList<>();
        Set<PropertyDescriptor> propertyDescriptors = propertyDescriptors(context.getParamType().getErasedType());
        Map<Method, PropertyDescriptor> propertyLookupByGetter
                = propertyDescriptorsByMethod(context.getParamType().getErasedType(), propertyDescriptors);
        Iterable<ResolvedMethod> getters = accessors.in(context.getParamType()).stream()
                .filter(onlyValidGetters(propertyLookupByGetter.keySet())).collect(toList());

        Map<String, ResolvedField> fieldsByName =
                StreamSupport.stream(this.fields.in(context.getParamType()).spliterator(), false)
                        .collect(toMap((ResolvedMember::getName), identity()));


        LOG.debug("Expanding parameter type: {}", context.getParamType());
        AlternateTypeProvider alternateTypeProvider = context.getAlternateTypeProvider();
        List<ModelAttributeField> attributes =
                allModelAttributes(
                        propertyLookupByGetter,
                        getters,
                        fieldsByName,
                        alternateTypeProvider,
                        context.ignorableTypes());
        Predicate<ModelAttributeField> negate = recursiveType(context).negate();
        Predicate<ModelAttributeField> negate1 = simpleType().negate();
        attributes.stream()
                .filter(simpleType().negate())
                .filter(recursiveType(context).negate())
                .forEach((each) -> {
                    LOG.debug("Attempting to expand expandable property: {}", each.getName());
                    parameters.addAll(
                            expand(
                                    context.childContext(
                                            nestedParentName(context.getParentName(), each),
                                            each.getFieldType(),
                                            context.getOperationContext())));
                });

        Stream<ModelAttributeField> collectionTypes = attributes.stream()
                .filter(isCollection().and(recursiveCollectionItemType(context.getParamType()).negate()));
        collectionTypes.forEachOrdered((each) -> {
            LOG.debug("Attempting to expand collection/array field: {}", each.getName());

            ResolvedType itemType = collectionElementType(each.getFieldType());
            if (itemType == null) {
                return;
            }
            if (ScalarTypes.builtInScalarType(itemType).isPresent()
                    || enumTypeDeterminer.isEnum(itemType.getErasedType())) {
                parameters.add(simpleFields(context.getParentName(), context, each));
            } else {
                ExpansionContext childContext = context.childContext(
                        nestedParentName(context.getParentName(), each),
                        itemType,
                        context.getOperationContext());
                if (!context.hasSeenType(itemType)) {
                    parameters.addAll(expand(childContext));
                }
            }
        });

        Stream<ModelAttributeField> simpleFields = attributes.stream().filter(simpleType());
        simpleFields.forEach(each -> parameters.add(simpleFields(context.getParentName(), context, each)));
        return parameters.stream()
                .filter(hiddenParameter().negate())
                .filter(voidParameters().negate())
                .collect(toList());
    }

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

    private List<ModelAttributeField> allModelAttributes(
            Map<Method, PropertyDescriptor> propertyLookupByGetter,
            Iterable<ResolvedMethod> getters,
            Map<String, ResolvedField> fieldsByName,
            AlternateTypeProvider alternateTypeProvider,
            Collection<Class> ignorables) {

        Stream<ModelAttributeField> modelAttributesFromGetters =
                StreamSupport.stream(getters.spliterator(), false)
                        .filter(method -> !ignored(alternateTypeProvider, method, ignorables))
                        .map(toModelAttributeField(fieldsByName, propertyLookupByGetter, alternateTypeProvider));

        Stream<ModelAttributeField> modelAttributesFromFields =
                fieldsByName.values().stream()
                        .filter(ResolvedMember::isPublic)
                        .filter(ResolvedMember::isPublic)
                        .map(toModelAttributeField(alternateTypeProvider));

        return Stream.concat(
                modelAttributesFromFields,
                modelAttributesFromGetters)
                .collect(toList());
    }

    private boolean ignored(
            AlternateTypeProvider alternateTypeProvider,
            ResolvedMethod method,
            Collection<Class> ignorables) {
        boolean annotatedIgnorable = ignorables.stream()
                .filter(Annotation.class::isAssignableFrom)
                .anyMatch(annotation -> method.getAnnotations().asList().contains(annotation));
        return annotatedIgnorable
                || ignorables.contains(fieldType(alternateTypeProvider, method).getErasedType());
    }

    private Function<ResolvedField, ModelAttributeField> toModelAttributeField(
            final AlternateTypeProvider alternateTypeProvider) {

        return input -> new ModelAttributeField(
                alternateTypeProvider.alternateFor(input.getType()),
                input.getName(),
                input,
                input);
    }

    private Predicate<Compatibility<Parameter, RequestParameter>> voidParameters() {
        return input -> ResolvedTypes.isVoid(input.getLegacy()
                .flatMap(Parameter::getType)
                .orElse(null));
    }

    private Predicate<ModelAttributeField> recursiveCollectionItemType(final ResolvedType paramType) {
        return input -> Objects.equals(collectionElementType(input.getFieldType()), paramType);
    }

    private Compatibility<Parameter, RequestParameter> simpleFields(
            String parentName,
            ExpansionContext context,
            ModelAttributeField each) {
        LOG.debug("Attempting to expand field: {}", each);
        String dataTypeName =
                ofNullable(springfox.documentation.schema.Types.typeNameFor(each.getFieldType().getErasedType()))
                        .orElse(each.getFieldType().getErasedType().getSimpleName());
        LOG.debug("Building parameter for field: {}, with type: {}", each, each.getFieldType());
        ParameterExpansionContext parameterExpansionContext = new ParameterExpansionContext(
                dataTypeName,
                parentName,
                ParameterTypeDeterminer.determineScalarParameterType(
                        context.getOperationContext().consumes(),
                        context.getOperationContext().httpMethod()),
                new ModelAttributeParameterMetadataAccessor(
                        each.annotatedElements(),
                        each.getFieldType(),
                        each.getName()),
                context.getDocumentationType(),
                new springfox.documentation.builders.ParameterBuilder(),
                new RequestParameterBuilder());
        return pluginsManager.expandParameter(parameterExpansionContext);
    }

    private Predicate<ModelAttributeField> recursiveType(final ExpansionContext context) {
        return input -> context.hasSeenType(input.getFieldType());
    }

    private Predicate<ModelAttributeField> simpleType() {
        return isCollection().negate().and(isMap().negate())
                .and(
                        belongsToJavaPackage()
                                .or(isBaseType())
                                .or(isEnum()));
    }

    private Predicate<ModelAttributeField> isCollection() {
        return input -> isContainerType(input.getFieldType());
    }

    private Predicate<ModelAttributeField> isMap() {
        return input -> Maps.isMapType(input.getFieldType());
    }

    private Predicate<ModelAttributeField> isEnum() {
        return input -> enumTypeDeterminer.isEnum(input.getFieldType().getErasedType());
    }

    private Predicate<ModelAttributeField> belongsToJavaPackage() {
        return input -> ClassUtils.getPackageName(input.getFieldType().getErasedType()).startsWith("java.lang");
    }

    private Predicate<ModelAttributeField> isBaseType() {
        return input -> ScalarTypes.builtInScalarType(input.getFieldType()).isPresent()
                || input.getFieldType().isPrimitive();
    }

    private Function<ResolvedMethod, ModelAttributeField> toModelAttributeField(
            Map<String, ResolvedField> fieldsByName,
            Map<Method, PropertyDescriptor> propertyLookupByGetter,
            AlternateTypeProvider alternateTypeProvider) {
        return input -> {
            String name = propertyLookupByGetter.get(input.getRawMember()).getName();
            return new ModelAttributeField(
                    fieldType(alternateTypeProvider, input),
                    name,
                    input,
                    fieldsByName.get(name));
        };
    }

    private Predicate<ResolvedMethod> onlyValidGetters(final Set<Method> methods) {
        return input -> methods.contains(input.getRawMember());
    }

    private String nestedParentName(
            String parentName,
            ModelAttributeField attribute) {
        String name = attribute.getName();
        ResolvedType fieldType = attribute.getFieldType();
        if (isContainerType(fieldType) &&
                !ScalarTypes.builtInScalarType(collectionElementType(fieldType)).isPresent()) {
            name += "[0]";
        }

        if (isEmpty(parentName)) {
            return name;
        }
        return String.format("%s.%s", parentName, name);
    }

    private ResolvedType fieldType(
            AlternateTypeProvider alternateTypeProvider,
            ResolvedMethod method) {
        return alternateTypeProvider.alternateFor(method.getType());
    }

    //自定义实体类生成文档的信息
    private Set<PropertyDescriptor> propertyDescriptors(final Class<?> clazz) {
        try {
            //忽略实体类中的实体实体
            List<PropertyDescriptor> propertyDescriptorList = new ArrayList<>();
            for (PropertyDescriptor propertyDescriptor : getBeanInfo(clazz).getPropertyDescriptors()) {
                //getBeanInfo(clazz).getPropertyDescriptors()有点坑,强制要求属性命名规范;尽量用能识别的单词组合,否则会出现找不到类的属性
                Field field = ReflectUtil.getField(clazz, propertyDescriptor.getName());
                if (field != null && field.isAnnotationPresent(MyApiAttributeIgnore .class)) {
                    continue;
                }
                propertyDescriptorList.add(propertyDescriptor);
            }
            return new HashSet<>(propertyDescriptorList);
        } catch (IntrospectionException e) {
            LOG.warn(String.format("Failed to get bean properties on (%s)", clazz), e);
        }
        return emptySet();
    }

    private Map<Method, PropertyDescriptor> propertyDescriptorsByMethod(
            final Class<?> clazz,
            Set<PropertyDescriptor> propertyDescriptors) {
        return propertyDescriptors.stream()
                .filter(input -> input.getReadMethod() != null
                        && !clazz.isAssignableFrom(Collection.class)
                        && !"isEmpty".equals(input.getReadMethod().getName()))
                .collect(toMap(PropertyDescriptor::getReadMethod, identity()));

    }

    BeanInfo getBeanInfo(Class<?> clazz) throws IntrospectionException {
        return Introspector.getBeanInfo(clazz);
    }

    public DocumentationPluginsManager getPluginsManager() {
        return pluginsManager;
    }

    void setPluginsManager(DocumentationPluginsManager pluginsManager) {
        this.pluginsManager = pluginsManager;
    }
}


自定义注解:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyApiAttributeIgnore {
}

上述出问题的实体类改写:

@Data
@ApiModel(value = "房屋",description = "")
public class HouseVo extends House
{
    /***楼房信息*****/
    @ApiModelProperty(value = "楼房信息")
    @MyApiAttributeIgnore
    //实体类的属性不能随意命名,比如aBuilding ,会引起bean的PropertyDescriptor的name无法对应
    private Building building;
    ......
​
}

注意点实体类的属性不能随意命名,比如aBuilding ,像这种的属性名, getBeanInfo(clazz).getPropertyDescriptors()获取到的属性名可能就变成了ABuilding,最后就无法获取到属性的注解了。如果你是有引用别的jar,还是要遵循普遍认知的代码规范,否则会给你“惊喜”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值