Java注解处理(Annotation Processor)实战:Excel导出接口自动生成

      距离之前的Java APT的相关文章已经过了接近半年时间,这半年间也做了一些有关APT的应用,最近趁着过年在家整理开源了一个Excel导出接口自动生成的项目至Githubhttps://github.com/DreamJM/DreamSpringExcel),各位要是喜欢可以点击链接去点个星支持一下。

1. 需求&背景

       在类似后台管理系统中,会有一些Table数据查询与展示页面,同时会要求能将数据导出成Excel,而且导出数据基本上与Table页面展示数据相同。而利用poi以及相同的数据源进行Excel导出接口实现时,通常伴随着大量的雷同代码。

       为此,最好的方式即在现有数据查询接口的基础上,通过注解定义以及编译器的注解处理器,自动生成Excel导出接口。

2. 当前实现

       此次开源项目针对上述需求进行实现,注解及相关通用工具定义在excel-base中,注解处理器相关逻辑在excel-processor中,有兴趣的各位可以去看一下说明与源码,目前先对使用进行说明。

@ExcelSupport("com.dream.spring.excel.test.controller.excel.ExcelController")
@RestController
public class TestController {

    @ExcelExport(value = "/api/excel/test",
            annotations = {@AnnotationDef(clazz = TestAnnotation.class, members = {@AnnotationMember(name = "value", value = "\"hello\""),
                    @AnnotationMember(name = "children", value = "value=\"child\"", annotation = ChildValue.class)})})
    @GetMapping("/api/test")
    public Result<PageResult<Test>> test(@RequestParam(required = false) String param1, @ParamIgnore("-1") @RequestParam int type,
                                         @ParamIgnore @RequestParam(required = false) Integer pageNum,
                                         @ParamIgnore @RequestParam(required = false) Integer pageSize) {
        ... ...
    }
}

如上示例所示,在原有接口的基础上增加了如下两个注解:

  • ExcelSupport:注解处理器的入口,同时其value值指示自动生成的Excel导出接口的Controller名,默认将会在被注解Controller同级的包下生成ExcelController,并且多个被注解Controller可指定同一Controller名。
  • ExcelExport:指定Excel生成的数据源方法,根据该方法生成Excel的导出接口。注解的value指定Excel导出接口的url路径,annotations用于在生成接口上增加额外的注解。生成的接口方法入参保持一致,包括注解,但是@ParamIgnore标识的注解会忽略。在生成Excel导出接口时,实际上会调用被@ExcelExport注解的方法。

       上述注解用于导出接口的生成,而具体的Excel导出数据与格式的定义则需要根据该方法返回值类型上的注解来进行解析,如上述示例所示,其Excel每一行的数据基于Test类进行生成,Test类上的注解示例如下:

@Sheet(value = "Test", i18nSupport = false, indexIncluded = true,
        categories = {@Category(value = "test.child", start = 4, end = 5)},
        headers = {
                @Header(value = "test.name", field = "name", width = 15, note = @HeaderNote(necessary = true, content = "test_note", i18nSupport = false)),
                @Header(value = "test.value", field = "value"), @Header(value = "test.type", field = "type", width = 8),
                @Header(value = "test.date", field = "date", width = 20), @Header(value = "test.childName", field = "component.childName"),
                @Header(value = "test.childValue", field = "component.childValue")})
public class Test extends BaseTest {

    private String name;

    @Column(converter = @Converter(clazz = ConverterUtils.class, method = "formatType"),
            cellStyles = @CellItemStyle(condition = "{value} == 1", style = @CellStyle(backgroundColor = IndexedColors.BLUE, fontColor = IndexedColors.WHITE)))
    private int type;

    @Column(converter = @Converter(clazz = ConverterUtils.class, method = "formatDate"))
    private Date date;
    
    private Component component;
    ... ...
}
  • Sheet:定义Excel导出Sheet数据的内容和基本样式,value指示sheet名称,indexIncluded指示是否包含序号列,headers则标识所有的列的标题名称以及列的宽度等样式;特别的@Header中的field属性标识该列对应的Test类中的字段名称,该名称可通过"."来进行嵌套,例如上例中的component.childValue就表示该列填充component属性下的childValue属性值。
  • Column:用来对该列的数据做特殊处理,例如将类型转换为可读字符串,或者对日期类型进行格式化等;或者对某些单元格的样式进行定义,例如:对于值小于60的单元格背景色设置为红色等。

       但是ExcelExport所标记方法的返回值并非Collection或Array类型的,而是被其它类型所包装,为此需要通过@SheetWrapper标签来简单标记,注解处理器会根据@ExcelExport返回值类型及@SheetWrapper标签一直向下找,直至找到被标记为@SheetWrapper标签且类型为Collection或Array的值为止,该Collection或Array的数据类型即为导出数据类型,从而解析@Sheet注解:

public class Result<T> {

    @SheetWrapper
    private T data;
    
    ... ...
}
public class PageResult<T> {

    @SheetWrapper
    private List<T> values;
    
    ....
 }

根据上述目前自动生成的代码示例如下:

@RestController
public class ExcelController {
  @Autowired
  TestController ref0;
  
  ... ...

  @GetMapping("/api/excel/test")
  @TestAnnotation(
      value = "hello",
      children = @com.dream.spring.excel.test.annotation.ChildValue(value="child")
  )
  public void test(@RequestParam(required = false) String param1, HttpServletResponse response)
      throws IOException {
      ... ...
      Result<PageResult<Test>> result = ref0.test(param1,-1,null,null);
      ... ...
  }
  ... ...
}

  从而直接调用"/api/excel/test"接口即可直接导出Excel文件,不需要再去重复的写Excel导出代码。

3. Annotation Processor&Poet的常见坑

     具体的实现就不赘述了,有兴趣的各位可以看一下项目中"excel-processor"的实现,在此主要描述一下过程中常见的问题:

a) TypeElement的使用

       TypeElement是在处理中最常用到的类,其代表了类和接口类型的元素,相对应的方法元素为ExecutableElement。在实际处理过程中,得到的Element需要根据实际情况强转为对应的TypeElement或ExecutableElement,例如想获得ExcelSupport所注解的类的全名:

for (Element element : roundEnv.getElementsAnnotatedWith(ExcelSupport.class)) {
    TypeElement typeElement = (TypeElement) element;
    String fullName = typeElement.getQualifiedName().toString();
    ... ...
}

       此外,在注解处理过程中,需要判断某类是否在工程中存在,可使用如下方式:

TypeElement apiElement = processingEnv.getElementUtils().getTypeElement("io.swagger.annotations.Api");
if (apiElement != null) {
    ... ...
}

b) TypeMirror & DeclaredType

      DeclaredType是TypeMirror的子类型,代表所有的类和接口类型,很多时候也是需要强转,例如,需要通过方法返回类型来获取其中的带有SheetWrapper标签的子元素:

DeclaredType returnType = ((DeclaredType) method.getMethodElement().getReturnType());
Element element = returnType.asElement();
for (Element childElem : element.getEnclosedElements()) {
     if (childElem.getAnnotation(SheetWrapper.class) != null) {
         ... ...
     }
}

c) 注解中使用Class

      很多时候,在注解中需要定义一些类型,此时会用到Class类,然而在注解处理中,一旦调用获取该class,就会抛出异常(也可以理解,毕竟是在编译过程中)。因此在注解处理时,遇到class,一定需要捕获其异常,然后根据异常提示获得class所对应类的全名。

String annName = null;
try {
    annDef.clazz();
} catch (MirroredTypeException mte) {
    annName = mte.getTypeMirror().toString();
}

d) 注解处理器中未引用依赖的注解处理

      在注解处理器中获取对应元素的注解可通过TypeElement的getAnnotation(Class<A> annotationType)方法。但是如未引入依赖(即没有对应的Class),则需要通过遍历TypeElement的getAnnotationMirrors的方式:

for (AnnotationMirror ann : typeElement.getAnnotationMirrors()) {
    if ("io.swagger.annotations.Api".equals(ann.getAnnotationType().toString())) {
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : ann.getElementValues().entrySet()) {
            if ("tags()".equals(entry.getKey().toString())) {
                String values = entry.getValue().toString();
                ... ...
            }
        }
    }
}

e) Poet中的成员变量泛型的支持

      在使用Poet,如希望定义带泛型的成员变量,则需要通过ParameterizedTypeName,如下所示:

TypeElement opElem = processingEnv.getElementUtils().getTypeElement("org.springframework.beans.factory.ObjectProvider");
TypeElement configElem = processingEnv.getElementUtils().getTypeElement("com.dream.spring.excel.bean.ExcelExportConfig");
typeBuilder.addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(opElem), TypeName.get(configElem.asType())), "configProvider").addAnnotation(AnnotationSpec.builder(ClassName.get(autowiredElem)).build()).build());

f) 判断类型是否为Collection子类型

      在processingEnv.getTypeUtils()中,包含有isSubtype用来判断子类型,但是在判断的时候,判断目标TypeMirror是否为Collection的子类型的时候,一直是false,如下所示:

TypeElement collectionType = 
processingEnv.getElementUtils().getTypeElement("java.util.Collection")
boolean isCollection = processingEnv.getTypeUtils().isSubtype(targetType, collectionType.asType())

       主要原因就在于泛型,Collection带有泛型,为此无法之间判断其父子关系,为此,可以使用processingEnv.getTypeUtils()中的isAssignable方法来判断是否是Collection。isAssignable顾名思义就是判断目标类型是否可直接赋予另一类型,换一个思路,只要判断目标类型是否可以直接赋值于Collection<?>即可:

private boolean isCollection(TypeMirror type) {
    TypeElement collectionType = processingEnv.getElementUtils().getTypeElement("java.util.Collection");
    WildcardType wildcardTypeNull = processingEnv.getTypeUtils().getWildcardType(null, null);
    DeclaredType parentType = processingEnv.getTypeUtils().getDeclaredType(collectionType, wildcardTypeNull);
    return processingEnv.getTypeUtils().isAssignable(type, parentType);
}

  具体问题也可参照:https://stackoverflow.com/questions/12749517/types-isassignable-and-issubtype-misunderstanding?r=SearchResults


      该库个人用起来还是OK的,除了上述的功能之外,还额外了添加了文件缓存、国际化支持以及文件导入支持等,性能方面方面也在实际项目中检验过,后续应该会长期维护一下。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值