Skip to content

[Enhancement] Add composable annotations support #918

@bengbengbalabalabeng

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;

  @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "order")
  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"}, index = 1)
  @ComposableExcelProperty(value = {"Total Amount"}, index = 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. Enable composable annotation processing

Composable annotations must be explicitly enabled via enableMetaMarked(true):

FesodSheet.write(pathname, ExcelModel.class)
    // default false
    .enableMetaMarked(true)
    .sheet(0)
    .doWrite(dataList);
  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. (For now)
// 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"};
}

More

Priority:

  • Direct annotations on a field > composable annotations on the same field
  • Annotations on the model class > annotations inherited from parent annotations

Compatibility:

The feature is opt-in (enableMetaMarked defaults to false). Existing code using only direct Fesod annotations is completely unaffected.

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions