使用自定义类解决swagger2统一返回类型和处理json文档显示

文章背景

最近,作者在做web开发的时候,使用swagger解决前后端沟通的问题,由于作者的轻微的强迫症,一直想统一后端springboot的返回值类型,但是存在的问题如下所示:
1. 统一返回类型;
2. swagger使用反射来创建文档,但是只能读取静态的类型数据,统一类型后,缺少对于data字段的描述;
3. 不能生成json数据的文档;
声明:本文档原创,且不涉及swagger的基础使用,这个需要折腾swagger,有门槛基础,本文参考了互联网先驱的文档,参考文档在底部。

实验

实验环境

springboot=2.3.7
spring-boot-starter-aop=2.6.2
swagger-spring-boot-starter=1.9.0.RELEASE

解决方案

问题一

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "ResultBean", description = "json返回结果类")
public class ResultBean
{
    @ApiModelProperty(value = "状态码", example = "2")
    private int code;
    @ApiModelProperty("返回消息")
    private String msg;
    @ApiModelProperty("返回数据")
    private Object data;
}

实验结果:
实验结果

问题二

上面的代码当然只是简单的统一了返回类型,其中data字段不能显示更加细节的信息,于是需要使用泛型。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "ResultBean", description = "json返回结果类")
public class ResultBean<V>
{
    @ApiModelProperty(value = "状态码", example = "2")
    private int code;
    @ApiModelProperty("返回消息")
    private String msg;
    @ApiModelProperty("返回数据")
    private V data;
}

把想要被swagger识别到的类型直接使用ResultBean,T为pojo类型,即可被swagger扫描到类型,生成T类型的文档信息。
实验结果:
泛型
这里只是要了一下boolean,自定义类型也是一样的操作,到此已经能处理很多问题了,但是还是不能处理json的文档。
首先,需要考虑的是,swagger会不会很申请,能在没有任何json数据的情况下,读取到数据的key,从而生成文档。答案是不能!!!swagger没这么神奇,既然不能自动,就只能手动来写文档了。

问题三

该实现我主要参考了两篇文章,主要参考的参考文献1(链接在文章底部),但是文章1有点不是很好用,于是我改了一下。
直接上代码,代码很多:
ApiResponseArray.java

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

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiResponseArray {
    String name() default "";
    String description() default "";
    String type() default "array";
    ApiResponseObject[] items();
    String point() default "";
}

ApiResponseBodyReader.java

import cn.edu.swu.blockchain.entity.Value2;
import com.fasterxml.classmate.ResolvedType;
import com.google.common.base.Optional;
import io.swagger.annotations.ApiModel;
import io.swagger.models.ModelImpl;
import io.swagger.models.properties.*;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import java.util.*;

/**
 * 注册一个OperationBuilderPlugin
 * @author lsp
 */
@Component
public class ApiResponseBodyReader implements OperationBuilderPlugin {

    /**
     * 是否支持此类DocumentationType
     * @param delimiter 当前文档类型
     * @return 如果支持当前类型,则返回true
     */
    @Override
    public boolean supports(DocumentationType delimiter) {
        return true;
    }

    /**
     * 如果有需要,可对operationContext执行操作
     * @param operationContext 方法上下文
     */
    @Override
    public void apply(OperationContext operationContext) {
        boolean responseFields = apiResponseFields(operationContext);
        if (!responseFields)
            apiResponseObjectHandle(operationContext);
    }

    /**
     * 处理ApiResponseFields注解
     * @param operationContext
     */
    private boolean apiResponseFields(OperationContext operationContext){
        Optional<ApiResponseFields> fieldsOptional = operationContext.findAnnotation(ApiResponseFields.class);
        Optional<ApiResponseObject> objectOptional = operationContext.findAnnotation(ApiResponseObject.class);
        Optional<ApiResponseArray> arrayOptionalOptional = operationContext.findAnnotation(ApiResponseArray.class);
        if(fieldsOptional.isPresent() && !isVoid(operationContext)){
            ApiResponseFields responseFields = fieldsOptional.get();
            String model_name =responseFields.modelName();
            if("".equals(model_name)){
                model_name = getModelName(operationContext);
            }
            String uuid = model_name + "-" + UUID();
            String[] fields = responseFields.fields();
            Value2<String, String[]> value2 = new Value2<>(model_name, fields);
            ModelCache.specified_cache.put(uuid,value2);

            if (arrayOptionalOptional.isPresent() && !StringUtils.isEmpty(arrayOptionalOptional.get().point())) {
                ApiResponseArray apiResponseArray = arrayOptionalOptional.get();
                ModelCache.point_properties_cache.put(uuid, new Value2<>(apiResponseArray.point(), createArrayProperty(apiResponseArray)));
            }else if (objectOptional.isPresent() && !StringUtils.isEmpty(objectOptional.get().point())) {
                ApiResponseObject apiResponseObject = objectOptional.get();
                ModelCache.point_properties_cache.put(uuid, new Value2<>(apiResponseObject.point(), createObjectProperty(apiResponseObject)));
            }

            addResponseMessage(operationContext,uuid);
            return true;
        }
        return false;
    }

    /**
     * 处理ApiResponseObject注解
     * @param operationContext
     * @return 若此方法上有ApiResponseObject注解,则返回true,否则返回false
     */
    private boolean apiResponseObjectHandle(OperationContext operationContext){
        Optional<ApiResponseObject> optional = operationContext.findAnnotation(ApiResponseObject.class);
        if(optional.isPresent()){
            ApiResponseObject apiResponseObject = optional.get();
            ModelImpl model = createModel(apiResponseObject);
            String model_name = null;
            if(isVoid(operationContext)){
                model_name = "Map";
            }else{
                model_name = getModelName(operationContext);
            }
            String uuid = model_name + "-" + UUID();
            model.setTitle(uuid);
            ModelCache.extra_cache.put(uuid,model);
            addResponseMessage(operationContext,uuid);
            return true;
        }
        return false;
    }


    private boolean isVoid(OperationContext operationContext){
        ResolvedType type = operationContext.getReturnType();
        Class<?> aClass = type.getErasedType();
        return aClass == void.class;
    }

    /**
     * 获取返回值信息的名字
     * @param operationContext
     * @return
     */
    private String getModelName(OperationContext operationContext){
        ResolvedType type = operationContext.getReturnType();
        Class<?> aClass = type.getErasedType();
        ApiModel apiModel = aClass.getAnnotation(ApiModel.class);
        String model_name = null;
        if(apiModel != null){
            model_name = apiModel.value();
        }
        if(model_name==null || "".equals(model_name)){
            model_name = aClass.getSimpleName();
        }
        return model_name;
    }

    /**
     * 为operationContext添加状态为200的返ResponseMessage
     * @param operationContext
     * @param typeName
     */
    private void addResponseMessage(OperationContext operationContext,String typeName){
        ResponseMessage responseMessage = new ResponseMessageBuilder()
                .code(200).responseModel(new ModelRef(typeName))
                .build();
        HashSet<ResponseMessage> responseMessages = new HashSet<>();
        responseMessages.add(responseMessage);
        operationContext.operationBuilder().responseMessages(responseMessages);
    }

    /**
     * 生成UUID
     * @return
     */
    private String UUID(){
        return UUID.randomUUID().toString();
    }

    private Map<String, Property> extraProperties(ApiResponseObject apiResponseObject) {
        HashMap<String, Property> res = new HashMap<>();
        ApiResponseProperty[] properties = apiResponseObject.properties();
        for(ApiResponseProperty apiResponseProperty : properties){
            String name = apiResponseProperty.name();
            String description = apiResponseProperty.description();
            String type = apiResponseProperty.type();
            Property property = null;
            if("string".equalsIgnoreCase(type)){
                property = new StringProperty();
            }else if("int".equalsIgnoreCase(type)){
                property = new IntegerProperty();
            }else if("date".equalsIgnoreCase(type)){
                property = new DateProperty();
            }else if("uuid".equalsIgnoreCase(type)){
                property = new UUIDProperty();
            }else{
                throw new RuntimeException("未支持的类型");
            }
            property.setDescription(description);
            res.put(name,property);
        }
        return res;
    }

    /**
     * 根据apiResponseObject注解生成一个ModelImpl
     * @param apiResponseObject
     * @return
     */
    private ModelImpl createModel(ApiResponseObject apiResponseObject){
        ModelImpl result = new ModelImpl();
        //apiResponseObject的类型指定是object
        result.setType("object");
        result.setDescription(apiResponseObject.description());
        result.setProperties(extraProperties(apiResponseObject));
        return result;
    }

    private ObjectProperty createObjectProperty(ApiResponseObject apiResponseObject) {
        ObjectProperty objectProperty = new ObjectProperty();
        objectProperty.setType(StringUtils.isEmpty(apiResponseObject.type())?"object":apiResponseObject.type());
        objectProperty.setName(apiResponseObject.name());
        objectProperty.setDescription(apiResponseObject.description());
        objectProperty.properties(extraProperties(apiResponseObject));
        return objectProperty;
    }

    private ArrayProperty createArrayProperty(ApiResponseArray apiResponseArray) {
        ArrayProperty arrayProperty = new ArrayProperty();
        arrayProperty.setType(StringUtils.isEmpty(apiResponseArray.type())?"array":apiResponseArray.type());
        arrayProperty.setDescription(apiResponseArray.description());
        arrayProperty.setName(apiResponseArray.name());
        ApiResponseObject items = apiResponseArray.items()[0];
        ObjectProperty objectProperty = createObjectProperty(items);
        return arrayProperty.items(objectProperty);
    }

}

ApiResponseFields.java

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiResponseFields {
    String modelName() default "";
    String[] fields();
}

ApiResponseObject.java

public @interface ApiResponseProperty {
    String name();
    String description() default "";
    String type();
}

ModelCache.java

import io.swagger.models.Model;
import io.swagger.models.properties.Property;

import java.util.HashMap;
import java.util.Map;

public class ModelCache {
    static Map<String, Model> extra_cache = new HashMap<>();
    static Map<String, Value2<String,String[]>> specified_cache = new HashMap<>();
    static Map<String, Value2<String, Property>> point_properties_cache = new HashMap<>();
}

SwaggerAop.java

import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.Swagger;
import io.swagger.models.properties.Property;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Map;

@Aspect
@Component
public class SwaggerAop {

    @Pointcut(value = "execution(public * springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl.mapDocumentation(..))")
    public void point(){

    }

    @Around("point()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Swagger swagger = (Swagger) proceedingJoinPoint.proceed();
        Map<String, Model> returnValue = swagger.getDefinitions() ;
        returnValue.putAll(ModelCache.extra_cache);
        ModelCache.specified_cache.entrySet().forEach(e->{
            String key = e.getKey();
            String model_name = e.getValue().v1;
            String[] fields = e.getValue().v2;

            Model model = returnValue.get(model_name);
            if(model == null){
                throw new RuntimeException("不存在的类型"+model_name);
            }

            Map<String, Property> properties = model.getProperties();
            ModelImpl newModel = new ModelImpl();
            newModel.setDescription(model.getDescription());
            newModel.setTitle(key);
            for(String field : fields){
                Property property = properties.get(field);
                if(property == null){
                    throw new RuntimeException("不存在的属性"+field);
                }
                newModel.property(field,property);
            }

            if (ModelCache.point_properties_cache.size() > 0) {
                Value2<String, Property> stringPropertyValue2 = ModelCache.point_properties_cache.remove(key);
                newModel.property(stringPropertyValue2.v1, stringPropertyValue2.v2);
            }

            returnValue.put(key,newModel);

        });

        return swagger;
    }

}

Value2.java

/**
 * 存放多个实例的便捷类
 */
public class Value2<T,V> {
    public final T v1;
    public final V v2;
    public Value2(T v1, V v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
}

实验结果:
controller

使用接口的时候,需要保证point的数据一定要和fields的泛型的数据一致,实验结果如下:
实验结果
成功生成了json的信息。还有一个叫ApiResponseArray,用来处理[]的。

参考文献

[1] https://blog.csdn.net/lzhcoder/article/details/108348430
[2] https://blog.csdn.net/pingzi_kendy/article/details/83577443

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐