swagger2自定义注解 ---你还在为每一个api创建一个ApiModel类而烦恼吗?

本文的实现是依赖于这一篇博客,原文传送门点击这里!

代码

  1. 自定义注解
    @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DaisyApiIgnoreProps {
    
        /**
         * 忽略的属性值
         * @return
         */
        String[] value() default {};
    }
    
    
    @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DaisyApiNeedProps {
    
        /**
         * 需要的属性值
         * @return
         */
        String[] value() default {};
    }
    
  2. 核心代码
    
    package daisy.framework.api.config.plugins;
    
    
    import com.fasterxml.classmate.TypeResolver;
    import com.google.common.base.Optional;
    import daisy.framework.api.annotations.DaisyApiIgnoreProps;
    import daisy.framework.api.annotations.DaisyApiNeedProps;
    import io.swagger.annotations.ApiModelProperty;
    import org.apache.ibatis.javassist.*;
    import org.apache.ibatis.javassist.bytecode.AnnotationsAttribute;
    import org.apache.ibatis.javassist.bytecode.ConstPool;
    import org.apache.ibatis.javassist.bytecode.annotation.Annotation;
    import org.apache.ibatis.javassist.bytecode.annotation.BooleanMemberValue;
    import org.apache.ibatis.javassist.bytecode.annotation.StringMemberValue;
    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 springfox.documentation.schema.ModelRef;
    import springfox.documentation.service.ResolvedMethodParameter;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spi.service.ParameterBuilderPlugin;
    import springfox.documentation.spi.service.contexts.ParameterContext;
    
    import java.lang.reflect.Field;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.List;
    import java.util.stream.Collectors;
    
    @Order
    @Component
    public class SwaggerModelReader implements ParameterBuilderPlugin {
    
        private TypeResolver typeResolver;
    
        private static final Logger logger = LoggerFactory.getLogger(SwaggerModelReader.class);
    
        @Autowired
        public void setTypeResolver(TypeResolver typeResolver) {
            this.typeResolver = typeResolver;
        }
    
        @Override
        public void apply(ParameterContext parameterContext) {
            ResolvedMethodParameter methodParameter = parameterContext.resolvedMethodParameter();
            Optional<DaisyApiIgnoreProps> ignorePropsOptional = methodParameter.findAnnotation(DaisyApiIgnoreProps.class);
            Optional<DaisyApiNeedProps> needPropsOptional = methodParameter.findAnnotation(DaisyApiNeedProps.class);
            String apiUrl = parameterContext.getOperationContext().requestMappingPattern();
            if (ignorePropsOptional.isPresent() && needPropsOptional.isPresent()) {
                // 2个注解不能同时存在于一个参数上
                StringBuilder errorMsg = new StringBuilder("@DaisyApiIgnoreProps with @DaisyApiNeedProps cannot exist at the same time! ");
                errorMsg.append("api路径:").append(apiUrl).append("\t");
                Optional<String> defaultName = parameterContext.resolvedMethodParameter().defaultName();
                if (defaultName.isPresent()) {
                    errorMsg.append("参数名:").append(defaultName.get()).append("\t");
                }
                // 写一个错误日志 但是不影响启动 因为这个插件并不会影响到系统的正常运行
                logger.error(errorMsg.toString());
            }
            if (ignorePropsOptional.isPresent() || needPropsOptional.isPresent()) {
                // 如果是泛型的话 那么获取泛型里面的数据
                Class originClass = parameterContext.resolvedMethodParameter().getParameterType().getErasedType();
                boolean isCollection = Collection.class.isAssignableFrom(originClass);
                // 如果参数是集合类型的话 那么原始类应该
                if (isCollection) {
                    // 获取泛型类型
                    originClass = parameterContext.resolvedMethodParameter().getParameterType().getTypeBindings().getBoundType(0).getErasedType();
                }
                //model 名称
                String name = originClass.getSimpleName() + apiUrl.replaceAll("/", "_");
                String[] properties;
                boolean ignore = ignorePropsOptional.isPresent();
                if (ignore) {
                    properties = ignorePropsOptional.get().value();
                } else {
                    properties = needPropsOptional.get().value();
                }
                // 向文档内容中添加新的模型对象
                parameterContext.getDocumentationContext()
                        .getAdditionalModels()
                        //像documentContext的Models中添加我们新生成的Class
                        .add(typeResolver.resolve(createRefModelIgp(properties, name, originClass, ignore)));
                ModelRef modelRef;
                if (!isCollection) {
                    modelRef = new ModelRef(name);
                } else {
                    modelRef = new ModelRef("List", new ModelRef(name));
                }
                //修改model参数的ModelRef为我们动态生成的class
                parameterContext.parameterBuilder()
                        .parameterType("body")
                        .modelRef(modelRef)
                        .name(name);
            }
    
        }
    
        /**
         * 根据propertys中的值动态生成含有Swagger注解的javaBeen
         */
        private Class createRefModelIgp(String[] propertys, String name, Class origin, boolean ignore) {
            ClassPool pool = ClassPool.getDefault();
            String newClassName = origin.getPackage().getName() + "." + name;
            try {
                CtClass ctClass = pool.getOrNull(newClassName);
                if (ctClass != null) {
                    // 这行代码很重要 如果没有的话 那就不能和devTool 一起使用
                    // 销毁旧的类
                    ctClass.detach();
                }
                // 通过javassit 技术创建一个类
                ctClass = pool.makeClass(origin.getPackage().getName() + "." + name);
                // 获取所有的属性
                List<Field> fieldList = getAllField(origin, null);
                List<String> dealProperties = Arrays.asList(propertys);
                List<Field> dealFileds = fieldList.stream().filter(
                        s -> ignore ? (!(dealProperties.contains(s.getName()))) : dealProperties.contains(s.getName())
                ).collect(Collectors.toList());
                // 创建属性
                createCtFileds(dealFileds, ctClass);
                return ctClass.toClass();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 获取一个类中的所以属性
         *
         * @param clazz
         * @param fieldList
         * @return
         */
        private static List<Field> getAllField(Class clazz, List<Field> fieldList) {
            if (fieldList == null) {
                fieldList = new ArrayList<>(16);
            }
            Field[] declaredFields = clazz.getDeclaredFields();
            if (declaredFields.length > 0) {
                fieldList.addAll(Arrays.asList(declaredFields));
            }
            // 获取父类属性
            Class superclass = clazz.getSuperclass();
            if (!superclass.equals(Object.class)) {
                // 重复处理父类
                fieldList = getAllField(superclass, fieldList);
            }
            return fieldList;
        }
    
        /**
         * 通过字节码重新创建一些属性
         *
         * @param dealFileds
         * @param ctClass
         * @throws CannotCompileException
         * @throws NotFoundException
         */
        public void createCtFileds(List<Field> dealFileds, CtClass ctClass) throws CannotCompileException, NotFoundException {
            // 获取常量池
            ConstPool constPool = ctClass.getClassFile().getConstPool();
            for (Field field : dealFileds) {
                CtField ctField = new CtField(ClassPool.getDefault().get(field.getType().getName()), field.getName(), ctClass);
                ctField.setModifiers(Modifier.PUBLIC);
                //添加model属性说明
                AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
                Annotation ann = new Annotation(ApiModelProperty.class.getName(), constPool);
                ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
                if (apiModelProperty != null) {
                    String[] valNames = new String[]{"name", "value", "example", "allowableValues", "access", "notes", "dataType", "reference"};
                    String[] vals = new String[]{apiModelProperty.name(),
                            apiModelProperty.value(), apiModelProperty.example(),
                            apiModelProperty.allowableValues(), apiModelProperty.access(),
                            apiModelProperty.notes(), apiModelProperty.dataType(), apiModelProperty.reference()};
    
                    for (int i = 0; i < valNames.length; i++) {
                        if (!"".equals(vals[i])) {
                            ann.addMemberValue(valNames[i], new StringMemberValue(vals[i], constPool));
                        }
                    }
    
                    // 处理hidden
                    ann.addMemberValue("hidden", new BooleanMemberValue(apiModelProperty.hidden(), constPool));
    
                }
    
                attr.addAnnotation(ann);
                ctField.getFieldInfo().addAttribute(attr);
                ctClass.addField(ctField);
            }
        }
    
    
        @Override
        public boolean supports(DocumentationType delimiter) {
            return true;
        }
    }
    
    

使用

设置需要忽略属性

   @PostMapping("/create")
    @Override
    public DaisyResponseInfo<VO> create(@DaisyApiIgnoreProps({"id"})  @RequestBody List<VO> vos) 	{
        DaisyResponseInfo<VO> responseInfo = new DaisyResponseInfo<>();
        List<VO> newVos = daisyCRUDService.create(vos);
        responseInfo.setData(newVos);
        return responseInfo;
    }

注意点

  1. 字节码相关的包,原博客中使用的是com.alibaba.ttl.internal.javassist.*包
    import org.apache.ibatis.javassist.*;
    import org.apache.ibatis.javassist.bytecode.AnnotationsAttribute;
    import org.apache.ibatis.javassist.bytecode.ConstPool;
    import org.apache.ibatis.javassist.bytecode.annotation.Annotation;
    import org.apache.ibatis.javassist.bytecode.annotation.BooleanMemberValue;
    import org.apache.ibatis.javassist.bytecode.annotation.StringMemberValue;
    
  2. 在createRefModelIgp方法中的下面这段代码很重要,如果在使用devTool的时候丝袜哥不能正常使用,那么很可能就是少了下面这句
      CtClass ctClass = pool.getOrNull(newClassName);
      if (ctClass != null) {
          // 这行代码很重要 如果没有的话 那就不能和devTool 一起使用
          // 销毁旧的类,如果没有销毁,那么在热部署的时候会抛出一个异常
          // className : frozen class (cannot edit)
          ctClass.detach();
      }
    
  3. 由于业务需要在controller层的参数可能是由集合对象包装的泛型对象,代码如下:
      @DeleteMapping("/delete")
    @Override
    public DaisyResponseInfo<VO> delete(@RequestBody List<VO> vos) {
        DaisyResponseInfo<VO> responseInfo = new DaisyResponseInfo<VO>();
        daisyCRUDService.delete(vos);
        return responseInfo;
    }
    
    这个时候就要处理泛型了。核心代码如下:
     // 如果是泛型的话 那么获取泛型里面的数据
     Class originClass = parameterContext.resolvedMethodParameter().getParameterType().getErasedType();
      boolean isCollection = Collection.class.isAssignableFrom(originClass);
      // 如果参数是集合类型的话 那么原始类应该
      if (isCollection) {
          // 获取泛型类型
          originClass = parameterContext.resolvedMethodParameter().getParameterType().getTypeBindings().getBoundType(0).getErasedType();
      }
    // 省略构建类代码
    ModelRef modelRef;
    if (!isCollection) {
         modelRef = new ModelRef(name);
     } else {
     	// 对于集合类 需要在外面再包一层对象
         modelRef = new ModelRef("List", new ModelRef(name));
     }
    
    

总结

我们通过一大堆代码完成了一个对象被多个api接口使用,并再丝袜哥上显示不同的内容,但是我们回头想一下,它的实现原理其实很简单,在项目启动过程中,如果检测到api接口中使用到的对象添加了自定义注解,那么我们会根据原有对象类型和注解内容给重新生成一个对象,然后再将文档中的构建对象换成我们通过字节码创建的类,以达到狸猫换太子的目的。

对于外层使用者而言只是一个简单的注解和配置几个参数,在开发过程中完全不用关心实现,这不正是框架的魅力吗。

补充

swagger2自定义注解 --为什么总有一个自定义model不能被识别!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 26
    评论
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值