代码
- 自定义注解
@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 {}; }
- 核心代码
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;
}
注意点
- 字节码相关的包,原博客中使用的是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;
- 在createRefModelIgp方法中的下面这段代码很重要,如果在使用devTool的时候丝袜哥不能正常使用,那么很可能就是少了下面这句
CtClass ctClass = pool.getOrNull(newClassName); if (ctClass != null) { // 这行代码很重要 如果没有的话 那就不能和devTool 一起使用 // 销毁旧的类,如果没有销毁,那么在热部署的时候会抛出一个异常 // className : frozen class (cannot edit) ctClass.detach(); }
- 由于业务需要在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接口中使用到的对象添加了自定义注解,那么我们会根据原有对象类型和注解内容给重新生成一个对象,然后再将文档中的构建对象换成我们通过字节码创建的类,以达到狸猫换太子的目的。
对于外层使用者而言只是一个简单的注解和配置几个参数,在开发过程中完全不用关心实现,这不正是框架的魅力吗。