Commit e8f5fbb8 authored by xiaoguang.xu's avatar xiaoguang.xu

feat : IDGenerator AutoConfiguration

parent 9b8246e4
package cn.quantgroup.tech.util;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 注意事项:
* 1. 使用前在配置文件内配置 DATA_CENTER_ID
* <p>
* <p>
*
* @author zhiguo.liu
* @date 2017/5/18
*/
@Slf4j
@Component
@ConditionalOnClass(RedisTemplate.class)
@ConditionalOnProperty(name = "data.center.id")
public class OIDGenerator {
private static final String REDIS_WORK_ID_KEY = "GLOBAL:WORK:ID:";
private static final String ID_FORMAT = "yyyyMMddHHmmss";
private static Lock lock = new ReentrantLock();
/**
* data center,默认为 1
*/
private static int DATA_CENTER_ID = 1;
/**
* 最高支持同时1W台机器
*/
private static final int MAX_WORK_ID = 10000;
/**
* 最高每秒发号 100w
*/
private static final int MAX_COUNT = 999999;
private static AtomicInteger COUNTER = new AtomicInteger(0);
private static long MAX_TIME_SECOND;
/**
* Worker ID 字符串
*/
private static String WORKER_ID_STR;
/**
* data center 字符串
*/
private static String DATA_CENTER_STR;
/**
* 最长回退时间,120 秒
*/
private static int MAX_BACK_SECOND = 120;
static {
Date now = new Date();
MAX_TIME_SECOND = now.getTime() / 1000;
}
private static LoadingCache cache = CacheBuilder.newBuilder()
.expireAfterWrite(MAX_BACK_SECOND, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicInteger>() {
@Override
public AtomicInteger load(Long key) throws Exception {
return new AtomicInteger(0);
}
});
@Autowired
private StringRedisTemplate redis;
@Value("${data.center.id}")
public void setDataCenterId(Integer dataCenterId) {
DATA_CENTER_ID = dataCenterId;
}
@PostConstruct
public void init() {
int workerId = (int) (redis.opsForValue().increment(REDIS_WORK_ID_KEY + DATA_CENTER_ID, 1) % MAX_WORK_ID);
WORKER_ID_STR = String.format("%04d", workerId);
DATA_CENTER_STR = String.format("%03d", DATA_CENTER_ID);
}
/**
* 1. 需要获取 dataCenterId 和 workeId
*/
public static String getId(String prefix) {
Date now = new Date();
Long timeSecond = now.getTime() / 1000;
Integer counter = 0;
if (timeSecond > MAX_TIME_SECOND) {
lock.lock();
if (timeSecond > MAX_TIME_SECOND) {
cache.put(MAX_TIME_SECOND, COUNTER);
COUNTER = new AtomicInteger(0);
MAX_TIME_SECOND = timeSecond;
}
lock.unlock();
}
if (timeSecond == MAX_TIME_SECOND) {
counter = COUNTER.incrementAndGet();
}
// 时间回退时到 cache 里拿,或者直接抛出错误
if (timeSecond < MAX_TIME_SECOND) {
if (timeSecond + MAX_BACK_SECOND < MAX_TIME_SECOND) {
throw new RuntimeException("时间回撤, 请稍后再试");
}
try {
AtomicInteger historyCounter = (AtomicInteger) cache.get(timeSecond);
counter = historyCounter.incrementAndGet();
} catch (ExecutionException e) {
log.error("取出缓存时出错");
}
}
// 达到计数器上上限, 休眠半秒并重试
if (counter >= MAX_COUNT) {
try {
Thread.sleep(500);
return getId(prefix);
} catch (InterruptedException e) {
log.error("发号器休眠时发生错误:{}", e);
}
}
String currentTimeStr = new SimpleDateFormat(ID_FORMAT, Locale.SIMPLIFIED_CHINESE).format(now);
return prefix + currentTimeStr + DATA_CENTER_STR + WORKER_ID_STR + String.format("%06d", counter);
}
}
package cn.quantgroup.tech.util.id;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.springframework.util.Assert;
import java.math.BigInteger;
/**
* Allocate 64 bits for the UID(long)<br>
* sign (fixed 1bit) -> deltaSecond -> workerId -> sequence(within the same second)
*/
public class BitsAllocator {
/**
* Total 64 bits
* dataCenterIdBits + workerIdBits + sequenceBits
*/
public static final int TOTAL_BITS = 1 << 6;
private final int timestampBits;
private final int dataCenterIdBits;
private final int workerIdBits;
private final int sequenceBits;
/**
* Max value for dataCenterId & workerId & sequence
*/
private final long maxDeltaSeconds;
private final long maxDataCenterId;
private final long maxWorkerId;
private final long maxSequence;
/**
* Shift for dataCenterId & workerId & sequence
*/
private final int timestampShift;
private final int dataCenterIdShift;
private final int workerIdShift;
/**
* Constructor with timestampBits, workerIdBits, sequenceBits<br>
* The highest bit used for sign, so <code>63</code> bits for timestampBits, workerIdBits, sequenceBits
*/
public BitsAllocator(int timestampBits, int dataCenterIdBits, int workerIdBits, int sequenceBits) {
// make sure allocated 64 bits
int allocateTotalBits = dataCenterIdBits + workerIdBits + sequenceBits;
Assert.isTrue(allocateTotalBits + 1 < TOTAL_BITS, "allocate greater than 64 bits");
// initialize bits
this.timestampBits = timestampBits;
this.dataCenterIdBits = dataCenterIdBits;
this.workerIdBits = workerIdBits;
this.sequenceBits = sequenceBits;
// initialize max value
this.maxDeltaSeconds = ~(-1L << timestampBits);
this.maxDataCenterId = ~(-1L << dataCenterIdBits);
this.maxWorkerId = ~(-1L << workerIdBits);
this.maxSequence = ~(-1L << sequenceBits);
// initialize shift
this.timestampShift = dataCenterIdBits + workerIdBits + sequenceBits;
this.dataCenterIdShift = workerIdBits + sequenceBits;
this.workerIdShift = sequenceBits;
}
/**
* Allocate bits for UID according to delta seconds & workerId & sequence<br>
*
* @param deltaSeconds
* @param workerId
* @param sequence
* @return
*/
public long allocate(long deltaSeconds, long dataCenterId, long workerId, long sequence) {
return (deltaSeconds << timestampShift) | (dataCenterId << dataCenterIdShift) | (workerId << workerIdShift) | sequence;
}
public BigInteger allocateBigInteger(long deltaSeconds, long dataCenterId, long workerId, long sequence) {
return BigInteger.ZERO.or(BigInteger.valueOf(deltaSeconds).shiftLeft(timestampShift))
.or(BigInteger.valueOf(dataCenterId).shiftLeft(dataCenterIdShift))
.or(BigInteger.valueOf(workerId).shiftLeft(workerIdShift))
.or(BigInteger.valueOf(sequence));
}
public int getTimestampBits() {
return timestampBits;
}
public int getDataCenterIdBits() {
return dataCenterIdBits;
}
public int getWorkerIdBits() {
return workerIdBits;
}
public int getSequenceBits() {
return sequenceBits;
}
public long getMaxDeltaSeconds() {
return maxDeltaSeconds;
}
public long getMaxDataCenterId() {
return maxDataCenterId;
}
public long getMaxWorkerId() {
return maxWorkerId;
}
public long getMaxSequence() {
return maxSequence;
}
public int getTimestampShift() {
return timestampShift;
}
public int getWorkerIdShift() {
return workerIdShift;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
\ No newline at end of file
/*
* Copyright (c) 2017 Baidu, Inc. All Rights Reserve.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.quantgroup.tech.util.id;
/**
* IDGenerateException
*/
public class IDGenerateException extends RuntimeException {
/**
* Serial Version UID
*/
private static final long serialVersionUID = -27048199131316992L;
/**
* Default constructor
*/
public IDGenerateException() {
super();
}
/**
* Constructor with cause
*
* @param cause
*/
public IDGenerateException(Throwable cause) {
super(cause);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>commons-parent</artifactId>
<groupId>cn.quantgroup</groupId>
<version>0.2.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>idgenerator-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package cn.quantgroup.tech.util.id;
package cn.quantgroup.tech.generator;
import org.apache.commons.lang3.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.math.BigInteger;
import java.text.ParseException;
import java.util.Date;
import java.util.concurrent.TimeUnit;
......@@ -57,65 +44,41 @@ import java.util.concurrent.TimeUnit;
*
* @author zhiguo.liu
*/
@Component
@ConditionalOnClass(RedisTemplate.class)
@ConditionalOnProperty(name = "id.epochStr")
public class IDGenerator implements InitializingBean {
@Slf4j
public class IDGenerator {
private static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final String DAY_PATTERN = "yyyy-MM-dd";
private static final Logger LOGGER = LoggerFactory.getLogger(IDGenerator.class);
private static final String REDIS_WORK_ID_KEY = "GLOBAL:WORK:ID:";
/**
* Bits allocate
*/
@Value("${id.dataCenterIdBits:0}")
protected int dataCenterIdBits;
@Value("${id.seqBits:13}")
protected int seqBits;
@Value("${id.workerBits:8}")
protected int workerBits;
protected static long DATA_CENTER_ID;
private long dataCenterId;
/**
* Customer epoch, unit as second. For example 2018-03-01 (ms: 1463673600000)
*/
protected String epochStr;
private static long EPOCH_SECONDS;
private long epochSeconds;
/**
* Stable fields after spring bean initializing
*/
private static BitsAllocator ALLOCATOR;
private static long WORKER_ID;
private BitsAllocator allocator;
private long workerId;
/**
* Volatile fields caused by nextId()
*/
private static long SEQUENCE = 0L;
private static long LAST_SECOND = -1L;
/**
* Spring property
*/
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void afterPropertiesSet() throws Exception {
// initialize bits allocator
int timeBits = 64 - 1 - dataCenterIdBits - workerBits - seqBits;
ALLOCATOR = new BitsAllocator(timeBits, dataCenterIdBits, workerBits, seqBits);
// initialize worker id
WORKER_ID = redisTemplate.opsForValue().increment(REDIS_WORK_ID_KEY + DATA_CENTER_ID, 1) % ALLOCATOR.getMaxWorkerId();
Assert.isTrue(WORKER_ID < ALLOCATOR.getMaxWorkerId(), "WORKER_ID is too big");
Assert.isTrue(ALLOCATOR.getMaxDataCenterId() != 0 && DATA_CENTER_ID < ALLOCATOR.getMaxDataCenterId(), "DATA_CENTER_ID is too big");
LOGGER.info("Initialized bits dataCenterBits:{}, workerBits:{}, seqBits:{}", dataCenterIdBits, workerBits, seqBits);
LOGGER.info("Initialized nodes, WORKER_ID:{}, DATA_CENTER_ID:{}", WORKER_ID, DATA_CENTER_ID);
private long sequence = 0L;
private long lastSecond = -1L;
public IDGenerator(Long dataCenterId, Long epochSeconds, Long workerId, BitsAllocator allocator) {
this.dataCenterId = dataCenterId;
this.epochSeconds = epochSeconds;
this.workerId = workerId;
this.allocator = allocator;
}
public String getID(String prefix) throws IDGenerateException {
try {
return nextId(prefix);
} catch (Exception e) {
LOGGER.error("Generate unique id exception. ", e);
log.error("Generate unique id exception. ", e);
throw new IDGenerateException(e);
}
}
......@@ -124,9 +87,9 @@ public class IDGenerator implements InitializingBean {
BigInteger bigInteger = new BigInteger(idStr);
int totalBits = bigInteger.bitLength();
long dataCenterIdBits = ALLOCATOR.getDataCenterIdBits();
long workerIdBits = ALLOCATOR.getWorkerIdBits();
long sequenceBits = ALLOCATOR.getSequenceBits();
long dataCenterIdBits = allocator.getDataCenterIdBits();
long workerIdBits = allocator.getWorkerIdBits();
long sequenceBits = allocator.getSequenceBits();
if (totalBits < 64) {
totalBits = 64;
long id = bigInteger.longValue();
......@@ -137,7 +100,7 @@ public class IDGenerator implements InitializingBean {
dataCenterId = 0;
}
long deltaSeconds = id >>> (dataCenterIdBits + workerIdBits + sequenceBits);
Date thatTime = new Date(TimeUnit.SECONDS.toMillis(EPOCH_SECONDS + deltaSeconds));
Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
String thatTimeStr = DateFormatUtils.format(thatTime, DATETIME_PATTERN);
return String.format("{\"ID\":\"%d\",\"timestamp\":\"%s\",\"DATA_CENTER_ID\":\"%d\",\"WORKER_ID\":\"%d\",\"SEQUENCE\":\"%d\"}",
id, thatTimeStr, dataCenterId, workerId, sequence);
......@@ -149,7 +112,7 @@ public class IDGenerator implements InitializingBean {
dataCenterId = 0;
}
long deltaSeconds = bigInteger.shiftRight((int) dataCenterIdBits + (int) workerIdBits + (int) sequenceBits).longValue();
Date thatTime = new Date(TimeUnit.SECONDS.toMillis(EPOCH_SECONDS + deltaSeconds));
Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
String thatTimeStr = DateFormatUtils.format(thatTime, DATETIME_PATTERN);
return String.format("{\"ID\":\"%d\",\"timestamp\":\"%s\",\"DATA_CENTER_ID\":\"%d\",\"WORKER_ID\":\"%d\",\"SEQUENCE\":\"%d\"}",
bigInteger, thatTimeStr, dataCenterId, workerId, sequence);
......@@ -170,29 +133,29 @@ public class IDGenerator implements InitializingBean {
long currentSecond = getCurrentSecond();
// Clock moved backwards, wait for newest time
if (currentSecond < LAST_SECOND) {
getNextSecond(LAST_SECOND);
if (currentSecond < lastSecond) {
getNextSecond(lastSecond);
}
// At the same second, increase SEQUENCE
if (currentSecond == LAST_SECOND) {
SEQUENCE = (SEQUENCE + 1) & ALLOCATOR.getMaxSequence();
if (currentSecond == lastSecond) {
sequence = (sequence + 1) & allocator.getMaxSequence();
// Exceed the max SEQUENCE, we wait the next second to generate ID
if (SEQUENCE == 0) {
currentSecond = getNextSecond(LAST_SECOND);
if (sequence == 0) {
currentSecond = getNextSecond(lastSecond);
}
// At the different second, SEQUENCE restart from zero
} else {
SEQUENCE = 0L;
sequence = 0L;
}
LAST_SECOND = currentSecond;
lastSecond = currentSecond;
// 当前时间小于设定的最大时间,即总位数在 64 位以下,用 long 生成数字
if (currentSecond - EPOCH_SECONDS <= ALLOCATOR.getMaxDeltaSeconds()) {
return preFix + ALLOCATOR.allocate(currentSecond - EPOCH_SECONDS, DATA_CENTER_ID, WORKER_ID, SEQUENCE);
if (currentSecond - epochSeconds <= allocator.getMaxDeltaSeconds()) {
return preFix + allocator.allocate(currentSecond - epochSeconds, dataCenterId, workerId, sequence);
}
return preFix + ALLOCATOR.allocateBigInteger(currentSecond - EPOCH_SECONDS, DATA_CENTER_ID, WORKER_ID, SEQUENCE);
return preFix + allocator.allocateBigInteger(currentSecond - epochSeconds, dataCenterId, workerId, sequence);
}
/**
......@@ -214,21 +177,4 @@ public class IDGenerator implements InitializingBean {
return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
}
@Value("${data.center.id}")
public void setDataCenterId(Integer dataCenterId) {
DATA_CENTER_ID = dataCenterId;
}
@Value("${id.epochStr:2018-04-01}")
public void setEpochStr(String epochStr) {
if (StringUtils.isNotBlank(epochStr)) {
this.epochStr = epochStr;
try {
EPOCH_SECONDS = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseDate(epochStr, new String[]{DAY_PATTERN}).getTime());
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}
package cn.quantgroup.tech.generator.configuration;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 只要有 redis 就可以使用id generator
*
* @author zhiguo.liu
*/
@ConditionalOnClass(RedisTemplate.class)
@EnableConfigurationProperties(GeneratorAutoConfiguration.IdGeneratorProperties.class)
public class GeneratorAutoConfiguration {
private StringRedisTemplate redisTemplate;
@Autowired
public void setApplicationContext(ApplicationContext applicationContext) {
redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
}
@Bean
public GeneratorFactoryBean generatorFactoryBean(IdGeneratorProperties properties) {
return GeneratorFactoryBean.builder()
.dataCenterId(properties.getDataCenter())
.dataCenterIdBits(properties.getDataCenterIdBits())
.epochStr(properties.getEpochStr())
.seqBits(properties.getSeqBits())
.workerBits(properties.getWorkerBits())
.stringRedisTemplate(redisTemplate)
.build();
}
@Data
@ConfigurationProperties(prefix = "tech.id")
protected static class IdGeneratorProperties {
/**
* 这里每个服务都需要单独配置一个
*/
private int dataCenter = 1;
/**
* 下面每一个配置,如果不懂就不建议修改.
* dataCenterIdBits 1024个服务
*/
private int dataCenterIdBits = 10;
private int seqBits = 13;
private int workerBits = 8;
private String epochStr = "2018-04-01";
}
}
package cn.quantgroup.tech.generator.configuration;
import cn.quantgroup.tech.generator.BitsAllocator;
import cn.quantgroup.tech.generator.IDGenerator;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.Assert;
import java.util.concurrent.TimeUnit;
@Slf4j
@Builder
public class GeneratorFactoryBean implements FactoryBean<IDGenerator>, InitializingBean {
private static final String REDIS_WORK_ID_KEY = "GLOBAL:WORK:ID:";
private static final String DAY_PATTERN = "yyyy-MM-dd";
/**
* from config
*/
private int dataCenterIdBits;
private int seqBits;
private int workerBits;
private long dataCenterId;
private String epochStr;
private long workerId;
private StringRedisTemplate stringRedisTemplate;
/**
* local construct
*/
private long epochSeconds;
private BitsAllocator allocator;
private IDGenerator idGenerator;
@Override
public IDGenerator getObject() throws Exception {
return idGenerator;
}
@Override
public Class<?> getObjectType() {
return IDGenerator.class;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void afterPropertiesSet() throws Exception {
if (stringRedisTemplate == null) {
log.error("redis template is null ");
return;
}
// initialize bits allocator
int timeBits = 64 - 1 - dataCenterIdBits - workerBits - seqBits;
allocator = new BitsAllocator(timeBits, dataCenterIdBits, workerBits, seqBits);
// initialize worker id
workerId = stringRedisTemplate.opsForValue().increment(REDIS_WORK_ID_KEY + dataCenterId, 1) % allocator.getMaxWorkerId();
Assert.isTrue(workerId <= allocator.getMaxWorkerId(), "WORKER_ID is too big");
Assert.isTrue(dataCenterId <= allocator.getMaxDataCenterId(), "DATA_CENTER_ID is too big");
epochSeconds = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseDate(epochStr, new String[]{DAY_PATTERN}).getTime());
log.info("Initialized bits dataCenterBits:{}, workerBits:{}, seqBits:{}", dataCenterIdBits, workerBits, seqBits);
log.info("Initialized nodes, WORKER_ID:{}, DATA_CENTER_ID:{}", workerId, dataCenterId);
idGenerator = new IDGenerator(dataCenterId, epochSeconds, workerId, allocator);
}
}
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.quantgroup.tech.generator.configuration.GeneratorAutoConfiguration
\ No newline at end of file
......@@ -19,6 +19,7 @@
<module>commons-spring</module>
<module>shutdown-spring-boot-starter</module>
<module>brave-spring-boot-starter</module>
<module>idgenerator-spring-boot-starter</module>
</modules>
<packaging>pom</packaging>
......@@ -83,6 +84,11 @@
<artifactId>brave-spring-boot-starter</artifactId>
<version>${common.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.quantgroup</groupId>
<artifactId>idgenerator-spring-boot-starter</artifactId>
<version>${common.parent.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment