# 字段编码——码段类型扩展SPI
码段分为有状态编码器
和无状态编码器
,比如序号(流水码)就是有状态编码器 -- 状态对象:计数器counter。
有状态编码器
SPI: com.primeton.mdm.management.spi.code.StatelessEncoder无状态编码器
SPI: com.primeton.mdm.management.spi.code.StatefulEncoder
# 有状态编码器扩展办法
# 定义编码器配置对象
即在WEB端数据模型配置向导——字段编码配置向导页中,定义给字段配置码段编码规则时能够配置的那些参数。(由于WEB端尚未完全支持扩展码段编码器扩展动态表单,目前WEB端对于扩展的码段编码配置都是以一个JSON编辑器来展现的)
@Getter
@Setter
public class SequenceNumber extends Config {
public static final String TYPE = "SEQUENCE";
private long start; // 起始值
private int step; // 步长(每次增长值)
private int length; // 码段长度限制
/**
* 其他字段值(一般情况下是枚举值不多,所选字段也不多1-2个)每组引用值独享一个计数器(隔离)。
*/
private String[] refFields;
/**
* 输出结果转成其他进制(一般是往大的转,16/32/36)高于十进制有助于缩短码段长度,减少存储空间和便于用户记忆
*/
private int radix = 10;
@Override
public final String getType() {
return TYPE;
}
}
# 定义状态对象(可选)
基类状态对象`State`只有一个序列号数值(计数器),如果不满足则编写子类继承它并添加需要存储的状态信息字段(存储采用JSON序列化状态信息)。
# 编写码段编码器实现
对于有状态字段编码器来说,分为单一状态对象(单一计数器)和多状态对象(多个计数器);后者需要实现`#key(...)`方法 —— 根码段配置/业务数据/系统时间等信息生成一个键值(`identity`|用于映射一个状态对象实例)
@Component("MDMSequenceNumberEncoder")
public class SequenceNumberEncoder implements StatefulEncoder<SequenceNumber, State>, Formatter<SequenceNumber> {
@Override
public final String name() {
return SequenceNumber.TYPE;
}
@Override
public Class<SequenceNumber> type() {
return SequenceNumber.class;
}
@Override
public Supplier<State> state() {
return State::new;
}
@Override
public String generate(String model, SequenceNumber config, State state, Map<String, Object> data, StateManager stateManager) {
// 最大长度 19 应该足够用了吧
final int length = (config.getLength() > 0 && config.getLength() < 20) ? config.getLength() : (config.getLength() > 19 ? 19 : 5);
long value = (state.getValue() > 0 ? state.getValue() : config.getStart())
+ (config.getStep() > 0 ? config.getStep() : 1);
StringBuilder sb = format(config, value, length);
state.setValue(value); // save-state
if (null != stateManager) {
stateManager.save(state);
}
return format(config, sb.toString());
}
private static StringBuilder format(SequenceNumber config, long value, int length) {
StringBuilder sb = new StringBuilder();
if (config.getRadix() >= 2 && config.getRadix() <= 36 && config.getRadix() != 10) {
sb.append(Long.toString(value, config.getRadix()));
} else {
sb.append(value);
}
if (sb.length() > length) {
throw new RuntimeException("Generated code snippet exceeding the length limit. " + value + " (limit length: " + length + ")");
}
while (sb.length() < length) {
sb.insert(0, 0);
}
return sb;
}
/**
* 1) 单状态模式:编码器无需实现这个方法;<br>
* 2) 多状态模式:编码器必须实现这个方法;可以根据系统当前时间和目标数据生成一个key值作为计数器名称方便映射一个状态对象的存取操作<br>
*
* <br>
* 有些编码常见需要多组状态值来记录序列。
* e.g. 餐厅每条下单流水号(日期流水码)需要每天从头重新编号<blockquote><pre>{DAY}{SEQ}, key=>{DAY}</pre></blockquote>
* e.g. 每个班级的学号独立流水编号<blockquote><pre>{届}{专业代号}{流水号}, key=>{届}{专业代号}</pre></blockquote>
* 如果你所扩展的有状态编码器存在多组状态值(对于同一个模型的某字段编码的某个码段,不同码段直接完全隔离)你需要根据某条数据以及系统时间等参数生成一个key,
* 不同行数据多次调用生成了相同的key则意味着他们使用了同一个状态对象(如流水码计数器),例如日期流水码按每天(或每月)到期自动重置(重新从头计数,
* 则同一天需要生成码段的时候它们使用了同一个状态对象)。
* <br>
* 使用生成的KEY关联(映射)一个状态对象(计数器)。
*
* <blockquote><pre>
* // e.g. 学生模型: 学号|届|专业代号|班级代号|身份证号|姓名|性别|出生日期|家庭住址|电话|邮箱|备注
* // 学号编码规则: {届}{专业代号}{流水号}
* // 码段1:{届} 选择日期码(系统时间|格式化年份)或引用码(`届`)
* // 码段2:{专业代号} 选择引用码(`专业代号` -- 配置关联字典)
* // 码段3:{流水号} 流水码(有状态码段)-- 计算key所需关联字段选择`届`和`专业代号` e.g. key1=2501 => 2025届数学专业 key2=2502=>2025届化学专业
* // 编码示例:
* // [key=2501] => 2025届|数学专业
* // sno=250101 => 01|张无忌
* // sno=250102 => 02|宋青书
* // ...
* // [key=2502] => 2025届|化学专业
* // sno=250201 => 01|周芷若
* // sno=250202 => 02|赵敏敏
* // ...
* </pre></blockquote>
*
* @param config 码段配置
* @param data 目标数据(部分码段类型依赖数据部分内容)
* @return 生成一个关键字(一般是用于多组状态值情况下)相关数据拼接一个字符串作为关键字,如日期流水码则关键字为日期值。这个值用于存储和读取码段状态以支持第2+次生成
*/
@Override
public String key(SequenceNumber config, Map<String, Object> data) {
if (ArrayUtils.isEmpty(config.getRefFields())) {
return null;
}
String snippets = Arrays.stream(config.getRefFields()).sorted().map(data::get).//filter(Objects::nonNull).
map(Objects::toString).collect(Collectors.joining("-"));
return StringUtils.isNotEmpty(snippets) ? snippets : null;
}
}
# 国际化配置(可选)
/META-INF/resource/i18n/encoder/SEQUENCE.properties
en_US=sequence
zh_CN=流水码
about.en_US=TODO-INTRODUCTION
about.zh_CN=TODO-INTRODUCTION
无状态编码
也适用。JAR:/META-INF/resource/i18n/encoder/{NAME}.properties
# 无状态编码器扩展办法
# 定义编码器配置对象
即在WEB端数据模型配置向导——字段编码配置向导页中,定义给字段配置码段编码规则时能够配置的那些参数。(由于WEB端尚未完全支持扩展码段编码器扩展动态表单,目前WEB端对于扩展的码段编码配置都是以一个JSON编辑器来展现的)
@Getter
@Setter
public class RandomULID extends Config {
public static final String TYPE = "ULID";
private boolean uppercase;
@Override
public final String getType() {
return TYPE;
}
}
# 编写码段编码器实现
@Component("MDMULIDEncoder")
public class ULIDEncoder implements StatelessEncoder<RandomULID>, Formatter<RandomULID> {
private static final ThreadLocal<Ulid> ctx = ThreadLocal.withInitial(UlidCreator::getUlid);
@Override
public final String name() {
return RandomULID.TYPE;
}
@Override
public Class<RandomULID> type() {
return RandomULID.class;
}
@Override
public String generate(String model, RandomULID config, Map<String, Object> data) {
Ulid ulid = ctx.get();
String uuid = ulid.toString();
ulid.increment();
return format(config, config.isUppercase() ? uuid : uuid.toLowerCase());
}
}
# 国际化配置(可选)
/META-INF/resource/i18n/encoder/ULID.properties
en_US=ULID
zh_CN=ULID
about.en_US=128 bit compatibility with UUID; 1.21e+24 unique ULIDs per millisecond; Sort in dictionary order (i.e. alphabetical order); Encode it into 26 strings in a standardized manner, instead of the 36 characters of UUID; Use Crockford's base32 for better efficiency and readability (5 bits per character); Not case sensitive; No special characters (URL safe); Monotonic sorting order (correctly detecting and processing the same milliseconds)
about.zh_CN=与UUID的128位兼容性;每毫秒1.21e + 24个唯一ULID;按字典顺序(也就是字母顺序)排序;规范地编码为26个字符串,而不是UUID的36个字符;使用Crockford的base32获得更好的效率和可读性(每个字符5位);不区分大小写;没有特殊字符(URL安全);单调排序顺序(正确检测并处理相同的毫秒)