[Enhancement] Add composable annotations support
- Dominant language
- Java
- Stars
- 6.2k
- Forks
- 532
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 42
Description
### Search before asking
- [x] I searched in the [issues](https://github.com/apache/fesod/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`:
```java
@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;
}
```
```java
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;
}
```
2. Define a no-attributes style preset
Group multiple annotations with fixed defaults into a single reusable annotation:
```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@HeadRowHeight(30)
@ContentRowHeight(20)
@OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 3)
@Inherited
public @interface CommonTableStyle {}
```
```java
// 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;
}
```
3. Combine field-level and class-level composable annotations
```java
@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 {}
```
```java
// 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;
}
```
4. 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:
```java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ExcelProperty(value = {"Full Name"})
@Inherited
public @interface FullNamePreset {}
```
```java
public class ExcelModel {
@ExcelProperty("First Name") // takes priority
@FullNamePreset
private String firstName;
}
```
5. `@FesodMarked.AliasFor` targets must be meta-present
Every `@FesodMarked.AliasFor` must reference an annotation that is meta-present on the composable.
```java
// 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;
}
```
6. Custom annotation attribute values must be explicitly marked with `@FesodMarked.AliasFor` if they need to be forwarded.
```java
// 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"};
}
```
7. No changes required for existing API usage
```java
FesodSheet.write(pathname, ExcelModel.class)
.sheet()
.doWrite(dataList);
```
### Alternatives
_No response_
### Anything else?
_No response_
### Are you willing to submit a PR?
- [x] I'm willing to submit a PR!
Contributor guide
Assessment
This issue has not been assessed yet.