Chuyển tới nội dung chính

Sinh converter cho Enum

govex-cloud-apt là annotation processor sinh converter cho enum tại compile-time: JPA AttributeConverter (lưu DB) và Spring Converter (binding request), kể cả biến thể cho List. Thêm vào build khi service có nhiều enum cần chuyển đổi và muốn loại bỏ code viết tay.

Khi nào sử dụng

  • Enum của service cần lưu xuống database theo giá trị code thay vì ordinal/name.
  • Enum cần binding tự động từ request param/path variable dạng chuỗi.
  • Entity có field List<Enum> muốn lưu dưới dạng chuỗi phân cách dấu phẩy.
  • Muốn converter được sinh và cập nhật tự động mỗi lần đổi enum, không phải bảo trì thủ công.

Cài đặt

Khai báo dependency (không khai version — version do BOM quản lý, xem Cài đặt):

<dependency>
<groupId>vn.govex.cloud</groupId>
<artifactId>govex-cloud-apt</artifactId>
<scope>provided</scope>
</dependency>

Điều kiện tiên quyết: govex-cloud-parent đã khai báo sẵn danh sách annotationProcessorPaths (lombok, spring-boot-configuration-processor). Khi annotationProcessorPaths được cấu hình, javac chỉ chạy processor trong danh sách đó — vì vậy bắt buộc khai báo thêm dependency trong annotationProcessorPaths của maven-compiler-plugin:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<!-- giữ các path có sẵn từ parent (lombok, spring-boot-configuration-processor) -->
<path>
<groupId>vn.govex.cloud</groupId>
<artifactId>govex-cloud-apt</artifactId>
<version>${govex-cloud.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

annotationProcessorPaths không tự lấy version từ BOM; khai version khớp BOM đang dùng bằng property trong pom.xml (ví dụ govex-cloud.version).

Cấu hình

Toàn bộ điều khiển qua annotation trên enum:

Annotation / thuộc tínhMô tảMặc định
@EnumJpaConverterSinh {Enum}AttributeConverter (JPA, @Converter(autoApply = true))
@EnumJpaConverter.listSinh thêm {Enum}ListAttributeConverter cho List<Enum> (lưu chuỗi phân cách dấu phẩy)false
@EnumJpaConverter.packageNamePackage chứa class sinh rapackage của enum
@StringToEnumConverterSinh StringTo{Enum}Converter (Spring Converter<String, Enum>, @Component)
@StringToEnumConverter.listSinh thêm StringToList{Enum}Convertertrue
@StringToEnumConverter.packageNamePackage chứa class sinh rapackage của enum

Sử dụng

Chuẩn bị enum có value type xác định

Enum cần xác định được giá trị đại diện qua CodeEnum<T> hoặc @JsonValue + @JsonCreator:

@EnumJpaConverter(list = true)
@StringToEnumConverter
public enum Status implements CodeEnum<Integer> {
ACTIVE(1, "Hoạt động"),
INACTIVE(0, "Không hoạt động");

private final Integer code;
private final String name;

Status(Integer code, String name) {
this.code = code;
this.name = name;
}

@Override
public Integer getCode() {
return code;
}

@Override
public String getName() {
return name;
}
}

Kết quả sinh ra

Trong target/generated-sources/annotations/ (cùng package với enum nếu không ghi đè):

  • StatusAttributeConverter — JPA AttributeConverter<Status, Integer>, @Converter(autoApply = true).
  • StatusListAttributeConverter — JPA AttributeConverter<List<Status>, String> (do list = true).
  • StringToStatusConverter — Spring Converter<String, Status>, gắn @Component.
  • StringToListStatusConverter — Spring Converter<String, List<Status>> (do list mặc định true).

Đổi package converter

@EnumJpaConverter(packageName = "vn.govex.app.converter")
public enum Status implements CodeEnum<Integer> {
// ...
}

Output sinh ra

Ví dụ với enum Status ở trên, processor ghi 4 file vào target/generated-sources/annotations/ (cùng package với enum nếu không ghi đè packageName; một số import java.lang/Override được lược bớt cho gọn).

StatusAttributeConverter.java — JPA converter cho một giá trị:

// This codes are generated automatically. Do not modify!
package vn.govex.app.enums;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter(autoApply = true)
public final class StatusAttributeConverter implements AttributeConverter<Status, Integer> {

@Override
public Integer convertToDatabaseColumn(Status i) {
return i == null ? null : i.getCode();
}

@Override
public Status convertToEntityAttribute(Integer i) {
if (i == null) return null;
for (Status value : Status.values()) {
if (value.getCode().equals(i)) {
return value;
}
}
return null;
}
}

StatusListAttributeConverter.java — JPA converter cho List<Status>, lưu chuỗi phân cách dấu phẩy:

// This codes are generated automatically. Do not modify!
package vn.govex.app.enums;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

@Converter(autoApply = true)
public final class StatusListAttributeConverter implements AttributeConverter<List<Status>, String> {

@Override
public String convertToDatabaseColumn(List<Status> list) {
if (CollectionUtils.isEmpty(list)) {
return null;
}
return list.stream().map(Status::getCode).map(Objects::toString).collect(Collectors.joining(","));
}

@Override
public List<Status> convertToEntityAttribute(String sources) {
if (!StringUtils.hasText(sources)) {
return null;
}
List<Status> list = new ArrayList<>();
for (String source : sources.split(",")) {
if (StringUtils.hasText(source)) {
for (Status value : Status.values()) {
if (source.equals(value.getCode().toString())) {
list.add(value);
}
}
}
}
return list;
}
}

StringToStatusConverter.java — Spring Converter<String, Status> dùng cho binding request:

// This codes are generated automatically. Do not modify!
package vn.govex.app.enums;

import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
@RequiredArgsConstructor
public final class StringToStatusConverter implements Converter<String, Status> {

private final ApplicationContext context;

@Override
public Status convert(String source) {
if (!StringUtils.hasText(source)) return null;
for (Status value : Status.values()) {
if (source.equals(value.getCode().toString())) {
return value;
}
}
return null;
}
}

StringToListStatusConverter.java — Spring Converter<String, List<Status>> (do list mặc định true):

// This codes are generated automatically. Do not modify!
package vn.govex.app.enums;

import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;

@Component
@RequiredArgsConstructor
public final class StringToListStatusConverter implements Converter<String, List<Status>> {

private final ApplicationContext context;

@Override
public List<Status> convert(String sources) {
if (!StringUtils.hasText(sources)) {
return null;
}
List<Status> list = new ArrayList<>();
for (String source : sources.split(",")) {
if (StringUtils.hasText(source)) {
for (Status value : Status.values()) {
if (source.equals(value.getCode().toString())) {
list.add(value);
}
}
}
}
return list;
}
}

Khi @JsonCreator nhận tham số String, converter sinh ra gọi trực tiếp method đó; khi tham số không phải String, code dùng context.getBean(ConversionService.class).convert(source, ...) như mô tả ở mục Lưu ý.

Lưu ý

  • Processor tự phát hiện value type theo thứ tự: @JsonValueCodeEnum<T> → tham số đầu tiên của @JsonCreator; không xác định được sẽ báo lỗi ngay lúc biên dịch.
  • JPA converter dùng autoApply = true nên tự áp dụng cho mọi entity field cùng kiểu, không cần khai báo @Convert.
  • Spring converter được đánh @Component: package chứa nó phải nằm trong phạm vi component-scan — cẩn thận khi ghi đè packageName sang package chưa được scan.
  • Khi @JsonCreator nhận tham số không phải String, converter sinh ra dùng ConversionService để chuyển trung gian (StringInteger → enum); ứng dụng cần có ConversionService tương ứng.
  • Dependency hỗ trợ Java 17+ (SourceVersion.RELEASE_17); file sinh ra có ghi chú không được sửa tay — mọi thay đổi nằm ở enum hoặc annotation.