关于Swagger2中position属性排序的问题

在swagger2当中在简单java类属性上添加ApiModelProperty用于扩展前端文档的显示功能,比如如下配置如下

@ApiModelProperty(value = "外部资金账号id", required = true, position = 1)

前两个属性value和required没啥问题,但是position其实并不能起到作用。仔细分析源码发现如下几个问题:

  • 排序后使用TreeMap保存结果
    对应源码springfox.documentation.swagger2.mappers.ModelSpecificationMapper#mapProperties,在这里可以看到首先通过position进行排序(position相同则使用name),可以接下来发现收集时竟然使用了TreeMap。TreeMap排序是根据主键值来排序的,而很明显这里其实是需要通过LinkedHashMap来排序的。
protected Map<String, Property> mapProperties(
    Map<String, PropertySpecification> properties,
    ModelNamesRegistry modelNamesRegistry) {
  return properties.entrySet().stream()
      .sorted(Map.Entry.comparingByValue(
          Comparator.comparing(PropertySpecification::getPosition)
              .thenComparing(PropertySpecification::getName)))
      .collect(toMap(
          Map.Entry::getKey,
          e -> propertyMapper.fromProperty(e.getValue(), modelNamesRegistry),
          (p1, p2) -> p1,
          TreeMap::new));
}

如果以为仅仅这里有问题,那就大错特错了,因为在调用这个方法的上一级方法(springfox.documentation.swagger2.mappers.ModelSpecificationMapper#mapComposedModel)中源码如下

Map<String, Property> modelProperties = new TreeMap<>(Comparator.naturalOrder());
modelProperties.putAll(mapProperties(properties, namesRegistry));
model.setProperties(modelProperties);
return model;

简直晕死,这里还是TreeMap,所以上面通过position和name排序有个屁用。如果说考虑实现子类覆盖这些方法的话,你会发现mapComposedModel是个私有方法。

  • 目标Property对象中position未赋值

上面的方法无非是将springfox.documentation.schema.PropertySpecification转为io.swagger.models.properties.Property对象,后者其实也是有Position的getter/setter方法的。

Integer getPosition();

void setPosition(Integer position);

可以在上面转换的代码propertyMapper.fromProperty(e.getValue(), modelNamesRegistry)中,根本就忘了position一样。以下是其中的一些片段(springfox.documentation.swagger2.mappers.PropertyMapper#fromProperty)

Map<String, Object> extensions = new VendorExtensionsMapper()
    .mapExtensions(source.getVendorExtensions());

if (property != null) {
  property.setDescription(source.getDescription());
  property.setName(source.getName());
  property.setRequired(source.getRequired() == null ? false : source.getRequired());
  property.setReadOnly(source.getReadOnly());
  property.setAllowEmptyValue(source.getAllowEmptyValue());
  property.setExample(source.getExample());
  property.getVendorExtensions().putAll(extensions);
  property.setXml(mapXml(source.getXml()));
}

return property;

你可能想这里通过修改propertyMapper来扩展下,将position设置进去,后面再想办法排序。

@Mapper
public abstract class ModelSpecificationMapper {
  private final PropertyMapper propertyMapper = new PropertyMapper();

一看这个源码,是不是再次晕倒,final?这下完了。只好考虑到调用ModelSpecificationMapper的外面想办法了。对应的方法为springfox.documentation.swagger2.mappers.CompatibilityModelMapper#modelsFromApiListings

@Mapper(componentModel = "spring")
public abstract class CompatibilityModelMapper {
  @Autowired
  @Value("${springfox.documentation.swagger.v2.use-model-v3:true}")
  private boolean useModelV3;

  @SuppressWarnings("deprecation")
  Map<String, Model> modelsFromApiListings(Map<String, List<ApiListing>> apiListings) {
    if (useModelV3) {
      return Mappers.getMapper(ModelSpecificationMapper.class).modelsFromApiListings(apiListings);
    } else {
      Map<String, springfox.documentation.schema.Model> definitions = new TreeMap<>();
      apiListings.values().stream()
          .flatMap(Collection::stream)
          .forEachOrdered(each -> definitions.putAll(each.getModels()));
      return Mappers.getMapper(ModelMapper.class).mapModels(definitions);
    }
  }
}

对应的源码如上所示。这里首先modelsFromApiListings方法是包内安全的,所以想覆盖很难了。那是不是可以通过org.mapstruct.factory.Mappers来解决呢?毕竟上面ModelSpecificationMapper对象是通过Mappers来获取的,一看源码简直气的不行。

private static final String IMPLEMENTATION_SUFFIX = "Impl";
/**
* Returns an instance of the given mapper type.
*
* @param clazz The type of the mapper to return.
* @param <T> The type of the mapper to create.
*
* @return An instance of the given mapper type.
*/
public static <T> T getMapper(Class<T> clazz) {
   try {
       List<ClassLoader> classLoaders = collectClassLoaders( clazz.getClassLoader() );

       return getMapper( clazz, classLoaders );
   }
   catch ( ClassNotFoundException | NoSuchMethodException e ) {
       throw new RuntimeException( e );
   }
}

private static <T> T getMapper(Class<T> mapperType, Iterable<ClassLoader> classLoaders)
          throws ClassNotFoundException, NoSuchMethodException {

      for ( ClassLoader classLoader : classLoaders ) {
          T mapper = doGetMapper( mapperType, classLoader );
          if ( mapper != null ) {
              return mapper;
          }
      }

      throw new ClassNotFoundException("Cannot find implementation for " + mapperType.getName() );
  }

  private static <T> T doGetMapper(Class<T> clazz, ClassLoader classLoader) throws NoSuchMethodException {
      try {
          @SuppressWarnings( "unchecked" )
          Class<T> implementation = (Class<T>) classLoader.loadClass( clazz.getName() + IMPLEMENTATION_SUFFIX );
          Constructor<T> constructor = implementation.getDeclaredConstructor();
          constructor.setAccessible( true );

          return constructor.newInstance();
      }
      catch (ClassNotFoundException e) {
          return getMapperFromServiceLoader( clazz, classLoader );
      }
      catch ( InstantiationException | InvocationTargetException | IllegalAccessException e) {
          throw new RuntimeException( e );
      }
  }

在这里就是查找类名称+固定后缀的类,虽然多个classLoader,好像可以存在多个同名类,但是在was服务器当中是不能存在两个同名类的。现在在swagger包中已经存在一个实现类springfox.documentation.swagger2.mappers.ModelSpecificationMapperImpl了。其实这个类就是默认使用的,虽说默认,其实也没法修改。因为在Mappers中如下属性是写死的,又是final

public class Mappers {
    private static final String IMPLEMENTATION_SUFFIX = "Impl";

真不知道这些开发人员是不是觉得自己的代码不需要扩展还是怎么回事,处处都提防着。到现在为止PropertyMapper没法改、ModelSpecificationMapper没法改、那么CompatibilityModelMapper呢?你会发现在springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl这是一个Spring的Bean。

@Component
public class ServiceModelToSwagger2MapperImpl extends ServiceModelToSwagger2Mapper {
    @Autowired
    private CompatibilityModelMapper compatibilityModelMapper;

既然是一个Spring的Bean,是不是可以通过BeanFactoryPostProcessor修改对应的bean定义中beanClass呢?比如自己实现一个CompatibilityModelMapper子类, 然后作为对应bean的真实实现?很可惜,CompatibilityModelMapper的唯一方法modelsFromApiListings是一个包内访问的(上面已经谈及)。所以通过扩展子类,然后再替换bean定义的默认实现是不可行的。

所谓的修改bean真实实现是指修改org.springframework.beans.factory.support.AbstractBeanDefinition#beanClass属性,通过BeanFactoryPostProcessor可以轻松实现

看来只能来强的了,代理实现。既然代理,首先想好谁来代理,这里首先是要修改ModelSpecificationMapper、其次是CompatibilityModelMapper,实现以下两个类

import io.swagger.models.properties.Property;
import springfox.documentation.schema.PropertySpecification;
import springfox.documentation.service.ModelNamesRegistry;
import springfox.documentation.swagger2.mappers.ModelSpecificationMapperImpl;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author: guanglai.zhou
 * @date: 2021/9/10 16:32
 */
public class XamsModelSpecificationMapper extends ModelSpecificationMapperImpl {

    @Override
    protected Map<String, Property> mapProperties(Map<String, PropertySpecification> properties, ModelNamesRegistry modelNamesRegistry) {
        Map<String, Property> treeMap = super.mapProperties(properties, modelNamesRegistry);
        List<String> keyList = properties.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(
                        Comparator.comparing(PropertySpecification::getPosition)
                                .thenComparing(PropertySpecification::getName)))
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
        for (String key : keyList) {
            Property property = treeMap.get(key);
            PropertySpecification propertySpecification = properties.get(key);
            property.setPosition(propertySpecification.getPosition());
        }
        return treeMap;
    }
}

继承默认ModelSpecificationMapper的实现,然后再调用父类映射逻辑之后,将position属性设置过去。这样position属性就可以继续传递了。接下来要控制CompatibilityModelMapper调用到这个方法,自己实现如下

import io.swagger.models.Model;
import springfox.documentation.service.ApiListing;

import java.util.List;
import java.util.Map;


/**
 * @author: guanglai.zhou
 * @date: 2021/9/10 20:56
 */
public class XamsCompatibilityModelMapper {

    public static Map<String, Model> modelsFromApiListings(Map<String, List<ApiListing>> apiListings) {
        return new XamsModelSpecificationMapper().modelsFromApiListings(apiListings);
    }
}

接下来就是通过代理来强制调用到modelsFromApiListings这里了。这里通过创建一个BeanPostProcessor来实现

import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import springfox.documentation.service.ApiListing;
import springfox.documentation.swagger2.mappers.CompatibilityModelMapper;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

/**
 * @author: guanglai.zhou
 * @date: 2021/9/10 16:17
 */
@Component
public class ModelSpecificationMapperPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof CompatibilityModelMapper) {
            return createCompatibilityModelMapperProxy(bean);
        }
        return bean;
    }

    private Object createCompatibilityModelMapperProxy(Object bean) {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setProxyTargetClass(true);
        proxyFactory.setTargetClass(bean.getClass());
        proxyFactory.setTarget(bean);
        proxyFactory.addAdvice((MethodInterceptor) invocation -> {
            Object[] arguments = invocation.getArguments();
            Method method = invocation.getMethod();
            if ("modelsFromApiListings".equals(method.getName())) {
                return XamsCompatibilityModelMapper.modelsFromApiListings((Map<String, List<ApiListing>>) arguments[0]);
            }
            return invocation.proceed();
        });
        return proxyFactory.getProxy();
    }
}

在CompatibilityModelMapper类型的Bean的初始化后,通过CGLIB进行代理,如果是modelsFromApiListings方法,就代理到自己的类中调用modelsFromApiListings方法的。这样通过以上一波操作,Model对象就包含了position的属性了。接下来相对比较简单了,springfox.documentation.swagger2.web.Swagger2ControllerWebMvc#getDocumentation方法对应前台的swagger的请求。也就是所谓的http://localhost:8888/v2/api-docs?group=1.0

private final PluginRegistry<WebMvcSwaggerTransformationFilter, DocumentationType> transformations;

@RequestMapping(
    method = RequestMethod.GET,
    produces = {APPLICATION_JSON_VALUE, HAL_MEDIA_TYPE})
public ResponseEntity<Json> getDocumentation(
    @RequestParam(value = "group", required = false) String swaggerGroup,
    HttpServletRequest servletRequest) {

  String groupName = ofNullable(swaggerGroup).orElse(Docket.DEFAULT_GROUP_NAME);
  Documentation documentation = documentationCache.documentationByGroup(groupName);
  if (documentation == null) {
    LOGGER.warn("Unable to find specification for group {}", groupName);
    return new ResponseEntity<>(HttpStatus.NOT_FOUND);
  }
  Swagger swagger = mapper.mapDocumentation(documentation);
  SwaggerTransformationContext<HttpServletRequest> context
      = new SwaggerTransformationContext<>(swagger, servletRequest);
  List<WebMvcSwaggerTransformationFilter> filters = transformations.getPluginsFor(DocumentationType.SWAGGER_2);
  for (WebMvcSwaggerTransformationFilter each : filters) {
    context = context.next(each.transform(context));
  }
  return new ResponseEntity<>(jsonSerializer.toJson(context.getSpecification()), HttpStatus.OK);
}

在上面返回之前可以通过WebMvcSwaggerTransformationFilter进行过滤处理。所以创建一个自定义的WebMvcSwaggerTransformationFilter实现来根据position进行排序。

import io.swagger.models.Model;
import io.swagger.models.Swagger;
import io.swagger.models.properties.Property;
import org.springframework.stereotype.Component;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.swagger2.web.SwaggerTransformationContext;
import springfox.documentation.swagger2.web.WebMvcSwaggerTransformationFilter;

import javax.servlet.http.HttpServletRequest;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author: guanglai.zhou
 * @date: 2021/9/10 13:45
 */
@Component
public class Swagger2WebMvcSwaggerTransformationFilter implements WebMvcSwaggerTransformationFilter {

    @Override
    public boolean supports(DocumentationType delimiter) {
        return DocumentationType.SWAGGER_2.equals(delimiter);
    }

    @Override
    public Swagger transform(SwaggerTransformationContext<HttpServletRequest> context) {
        Swagger swagger = context.getSpecification();
        Map<String, Model> definitions = swagger.getDefinitions();
        for (String key : definitions.keySet()) {
            Model model = definitions.get(key);
            Map<String, Property> properties = model.getProperties();
            Map<String, Property> sortedProperties = new LinkedHashMap<>(properties.size());
            List<String> fieldNameList = properties.entrySet().stream()
                    .sorted(Map.Entry.comparingByValue(
                            Comparator.comparing(Property::getPosition)
                                    .thenComparing(Property::getName)))
                    .map(Map.Entry::getKey)
                    .collect(Collectors.toList());
            for (String fieldName : fieldNameList) {
                sortedProperties.put(fieldName, properties.get(fieldName));
            }
            model.getProperties().clear();
            model.setProperties(sortedProperties);
        }
        return swagger;
    }
}

通过以上一波操作,终于实现了position的功能了。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值