Skip to content

[Enhancement] Add composable annotations support #918

Description

@bengbengbalabalabeng

Search before asking

  • I searched in the issues and found nothing similar.

Motivation

This proposal aims to introduce Composable Annotation Support for Fesod-Sheet's Java Model mode, allowing users to create custom, reusable annotation combinations (presets) that group multiple built-in Fesod annotations into a single meta-annotation. This dramatically reduces boilerplate when repetitive style/format configurations are applied across many model classes.

Solution

Content

Composable annotations solve the above problems by letting users define their own annotations that bundle multiple built-in annotations with preset defaults.

Here, two meta-annotations @FesodMarked and @FesodMarked.AliasFor are introduced to implement the following composite annotation strategy:

1. Preset mode: composable-annotations (marked by @FesodMarked) do not declare any attributes themselves; they simply bundle several meta-annotations with fixed default values together into a preset configuration, and users cannot override its attributes.
2. AliasFor mode: composable-annotations (marked with @FesodMarked) can explicitly declare attribute mapping relationships through @FesodMarked.AliasFor, forwarding their own attribute values to the specified attributes of the target annotation. Users can override its attributes.

Both class-level and field-level composable annotations are supported. When a direct annotation and a composable annotation of the same type coexist at the same level, the direct annotation takes priority.

Supported Annotations

In the fesod-sheet module, all built-in annotations except @ExcelIgnore and @ExcelIgnoreUnannotated can be used within composable annotations.

API Usage Example

  1. Define a composable annotation with @FesodMarked.AliasFor

Create a custom annotation that forwards attribute values to @ExcelProperty:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ExcelProperty
@Inherited
public @interface ComposableExcelProperty {

  @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "value")
  String value() default "";

  @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "index")
  int index() default -1;
  
  // If the 'attribute' value is not explicitly set, the name of the current attribute is used by default for mapping
  @FesodMarked.AliasFor(annotation = ExcelProperty.class)
  int order() default Integer.MAX_VALUE;
}
public class ExcelModel {

  // Same as @ExcelProperty(value = {"Order ID"}, index = 0)
  @ComposableExcelProperty(value = "Order ID", index = 0)
  private String orderId;

  // Same as @ExcelProperty(value = {"Total Amount"}, order = 1)
  @ComposableExcelProperty(value = "Total Amount", order = 1)
  private BigDecimal amount;
}
  1. Define a no-attributes style preset

Group multiple annotations with fixed defaults into a single reusable annotation:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@HeadRowHeight(30)
@ContentRowHeight(20)
@OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 3)
@Inherited
public @interface CommonTableStyle {}
// Same as:
// @HeadRowHeight(30)
// @ContentRowHeight(20)
// @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 3)
@CommonTableStyle
public class ExcelModel {

  @ExcelProperty("Date")
  @DateTimeFormat("yyyy-MM-dd")
  private Date date;

  @ExcelProperty("Revenue")
  @NumberFormat("#,##0.00")
  private BigDecimal revenue;
}
  1. Combine field-level and class-level composable annotations
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ContentStyle(wrapped = BooleanEnum.TRUE, fillForegroundColor = 10)
@ContentFontStyle(fontName = "Arial", fontHeightInPoints = 12, bold = BooleanEnum.TRUE)
@Inherited
public @interface ContentPreset {}
// Same as:
// @HeadRowHeight(30)
// @ContentRowHeight(20)
// @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 3)
@CommonTableStyle
public class ExcelModel {

  // Same as @ExcelProperty(value = {"Product Name"})
  @ComposableExcelProperty("Product Name")
  private String product;

  // Same as:
  // @ContentStyle(wrapped = BooleanEnum.TRUE, fillForegroundColor = 10)
  // @ContentFontStyle(fontName = "Arial", fontHeightInPoints = 12, bold = BooleanEnum.TRUE)
  @ContentPreset
  @NumberFormat("#,##0.00")
  private BigDecimal sales;
}
  1. Direct annotations override composable annotations

When both a direct annotation and a composable annotation of the same type exist at the same level, the direct annotation wins:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ExcelProperty(value = {"Full Name"})
@Inherited
public @interface FullNamePreset {}
public class ExcelModel {

  @ExcelProperty("First Name")   // takes priority
  @FullNamePreset
  private String firstName;
}
  1. @FesodMarked.AliasFor targets must be meta-present

Every @FesodMarked.AliasFor must reference an annotation that is meta-present on the composable.

// INVALID: @ColumnWidth is NOT meta-present on this annotation
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@Inherited
public @interface BadComposable {

  // will throw
  @FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value")
  int width() default -1;
}
  1. Custom annotation attribute values must be explicitly marked with @FesodMarked.AliasFor if they need to be forwarded.
// INVALID: value() value is not forward into @ExcelProperty because there is no mark @FesodMarked.AliasFor.
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ExcelProperty
@Inherited
public @interface BadComposable {

  // not work
  String[] value() default {"Name"};
}
  1. No changes required for existing API usage
FesodSheet.write(pathname, ExcelModel.class)
    .sheet()
    .doWrite(dataList);

Alternatives

No response

Anything else?

No response

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions