@@ -0,0 +1,38 @@
|
||||
# Module `guid`
|
||||
|
||||
## Introduction
|
||||
|
||||
Module `guid` serves as a guid creator for other `JDevKit` modules. You can also use this module as a guid creator standards.
|
||||
|
||||
We have already implemented `SnowflakeGuidCreator`, you can also implement a custom guid creations by implementing `com.onixbyte.identitygenerator.IdentityGenerator`.
|
||||
|
||||
## Example usage
|
||||
|
||||
### A UUID creator
|
||||
|
||||
```java
|
||||
GuidCreator<UUID> uuidCreator = (GuidCreator<UUID>) UUID::randomUUID;
|
||||
```
|
||||
|
||||
### A custom guid creator
|
||||
|
||||
Assume that you need serial guid creator.
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class CustomGuidCreator implementes GuidCreator<String> {
|
||||
|
||||
public final RedisTemplate<String, Long> serialRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
public CustomGuidCreator(RedisTemplate<String, Long> serialRedisTemplate) {
|
||||
this.serialRedisTemplate = serialRedisTemplate;
|
||||
}
|
||||
|
||||
@Override public String nextId() {
|
||||
return "SOME_PREFIX" + serialRedisTemplate.opsForValue().get("some_serial_key");
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 OnixByte.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import java.net.URI
|
||||
|
||||
plugins {
|
||||
java
|
||||
id("java-library")
|
||||
id("maven-publish")
|
||||
id("signing")
|
||||
}
|
||||
|
||||
val artefactVersion: String by project
|
||||
val projectUrl: String by project
|
||||
val projectGithubUrl: String by project
|
||||
val licenseName: String by project
|
||||
val licenseUrl: String by project
|
||||
|
||||
group = "com.onixbyte"
|
||||
version = artefactVersion
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
tasks.withType<Jar> {
|
||||
exclude("logback.xml")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.slf4j)
|
||||
implementation(libs.logback)
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("guid") {
|
||||
groupId = group.toString()
|
||||
artifactId = "guid"
|
||||
version = artefactVersion
|
||||
|
||||
pom {
|
||||
name = "DevKit - GUID"
|
||||
description = "The module for generating GUIDs of JDevKit."
|
||||
url = projectUrl
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = licenseName
|
||||
url = licenseUrl
|
||||
}
|
||||
}
|
||||
|
||||
scm {
|
||||
connection = "scm:git:git://github.com:OnixByte/JDevKit.git"
|
||||
developerConnection = "scm:git:git://github.com:OnixByte/JDevKit.git"
|
||||
url = projectGithubUrl
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
id = "zihluwang"
|
||||
name = "Zihlu Wang"
|
||||
email = "really@zihlu.wang"
|
||||
timezone = "Asia/Hong_Kong"
|
||||
}
|
||||
|
||||
developer {
|
||||
id = "siujamo"
|
||||
name = "Siu Jam'o"
|
||||
email = "jamo.siu@outlook.com"
|
||||
timezone = "Asia/Shanghai"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
from(components["java"])
|
||||
|
||||
signing {
|
||||
sign(publishing.publications["guid"])
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name = "sonatypeNexus"
|
||||
url = URI(providers.gradleProperty("repo.maven-central.host").get())
|
||||
credentials {
|
||||
username = providers.gradleProperty("repo.maven-central.username").get()
|
||||
password = providers.gradleProperty("repo.maven-central.password").get()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 OnixByte.
|
||||
*
|
||||
* 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 com.onixbyte.identitygenerator;
|
||||
|
||||
/**
|
||||
* The {@code IdentityGenerator} is a generic interface for generating globally unique identifiers (GUIDs)
|
||||
* of a specific type.
|
||||
* <p>
|
||||
* The type of ID is determined by the class implementing this interface.
|
||||
* </p>
|
||||
*
|
||||
* <p><b>Example usage:</b></p>
|
||||
* <pre>{@code
|
||||
* public class StringGuidCreator implements IdentityGenerator<String> {
|
||||
* private final AtomicLong counter = new AtomicLong();
|
||||
*
|
||||
* @Override
|
||||
* public String nextId() {
|
||||
* return UUID.randomUUID().toString() + "-" + counter.incrementAndGet();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* public class Example {
|
||||
* public static void main(String[] args) {
|
||||
* IdentityGenerator<String> guidCreator = new StringGuidCreator();
|
||||
* String guid = guidCreator.nextId();
|
||||
* System.out.println("Generated GUID: " + guid);
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @param <IdType> this represents the type of the Global Unique Identifier
|
||||
* @author Zihlu Wang
|
||||
* @version 1.1.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface IdentityGenerator<IdType> {
|
||||
|
||||
/**
|
||||
* Generates and returns the next globally unique ID.
|
||||
*
|
||||
* @return the next globally unique ID
|
||||
*/
|
||||
IdType nextId();
|
||||
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 OnixByte.
|
||||
*
|
||||
* 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 com.onixbyte.identitygenerator.exceptions;
|
||||
|
||||
/**
|
||||
* The {@code TimingException} class represents an exception that is thrown when there is an error
|
||||
* related to time sequence.
|
||||
* <p>
|
||||
* Instances of TimingException can be created with or without a message and a cause. The message
|
||||
* provides a description of the exception, while the cause represents the underlying cause of the
|
||||
* exception and provides additional information about the error.
|
||||
*
|
||||
* @author Zihlu Wang
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class TimingException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* A custom exception that is thrown when there is an issue with timing or scheduling.
|
||||
*/
|
||||
public TimingException() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom exception that is thrown when there is an issue with timing or scheduling with
|
||||
* customised error message.
|
||||
*
|
||||
* @param message customised message
|
||||
*/
|
||||
public TimingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom exception that is thrown when there is an issue with timing or scheduling with
|
||||
* customised error message.
|
||||
*
|
||||
* @param message customised message
|
||||
* @param cause the cause of this exception
|
||||
*/
|
||||
public TimingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom exception that is thrown when there is an issue with timing or scheduling with
|
||||
* customised error message.
|
||||
*
|
||||
* @param cause the cause of this exception
|
||||
*/
|
||||
public TimingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 OnixByte.
|
||||
*
|
||||
* 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 com.onixbyte.identitygenerator.impl;
|
||||
|
||||
import com.onixbyte.identitygenerator.IdentityGenerator;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A {@code SequentialUuidGenerator} is responsible for generating UUIDs following the UUID version 7 specification, which
|
||||
* combines a timestamp with random bytes to create time-ordered unique identifiers.
|
||||
* <p>
|
||||
* This implementation utilises a cryptographically strong {@link SecureRandom} instance to produce the random
|
||||
* component of the UUID. The first 6 bytes of the UUID encode the current timestamp in milliseconds, ensuring that
|
||||
* generated UUIDs are roughly ordered by creation time.
|
||||
* <p>
|
||||
* The generated UUID adheres strictly to the layout and variant bits of UUID version 7 as defined in the specification.
|
||||
* </p>
|
||||
*/
|
||||
public class SequentialUuidGenerator implements IdentityGenerator<UUID> {
|
||||
|
||||
private final SecureRandom random;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code SequentialUuidGenerator} with its own {@link SecureRandom} instance.
|
||||
*/
|
||||
public SequentialUuidGenerator() {
|
||||
this.random = new SecureRandom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and returns the next UUID version 7 identifier.
|
||||
*
|
||||
* @return a {@link UUID} instance representing a unique, time-ordered identifier
|
||||
*/
|
||||
@Override
|
||||
public UUID nextId() {
|
||||
var value = randomBytes();
|
||||
var buf = ByteBuffer.wrap(value);
|
||||
var high = buf.getLong();
|
||||
var low = buf.getLong();
|
||||
return new UUID(high, low);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a byte array representation of a UUID version 7,
|
||||
* combining the current timestamp with random bytes.
|
||||
*
|
||||
* @return a 16-byte array conforming to UUIDv7 layout and variant bits
|
||||
*/
|
||||
private byte[] randomBytes() {
|
||||
var value = new byte[16];
|
||||
random.nextBytes(value);
|
||||
|
||||
var timestamp = ByteBuffer.allocate(Long.BYTES);
|
||||
timestamp.putLong(System.currentTimeMillis());
|
||||
|
||||
System.arraycopy(timestamp.array(), 2, value, 0, 6);
|
||||
|
||||
// Set version to 7 (UUIDv7)
|
||||
value[6] = (byte) ((value[6] & 0x0F) | 0x70);
|
||||
|
||||
// Set variant bits as per RFC 4122
|
||||
value[8] = (byte) ((value[8] & 0x3F) | 0x80);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 OnixByte.
|
||||
*
|
||||
* 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 com.onixbyte.identitygenerator.impl;
|
||||
|
||||
import com.onixbyte.identitygenerator.IdentityGenerator;
|
||||
import com.onixbyte.identitygenerator.exceptions.TimingException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* The {@code SnowflakeIdentityGenerator} generates unique identifiers using the Snowflake algorithm,
|
||||
* which combines a timestamp, worker ID, and data centre ID to create 64-bit long integers. The bit
|
||||
* distribution for the generated IDs is as follows:
|
||||
* <ul>
|
||||
* <li>1 bit for sign</li>
|
||||
* <li>41 bits for timestamp (in milliseconds)</li>
|
||||
* <li>5 bits for data centre ID</li>
|
||||
* <li>5 bits for worker ID</li>
|
||||
* <li>12 bits for sequence number (per millisecond)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* When initializing a {@link SnowflakeIdentityGenerator}, you must provide the worker ID and data centre
|
||||
* ID, ensuring they are within the valid range defined by the bit size. The generator maintains an
|
||||
* internal sequence number that increments for IDs generated within the same millisecond. If the
|
||||
* system clock moves backward, an exception is thrown to prevent generating IDs with
|
||||
* repeated timestamps.
|
||||
*
|
||||
* @author Zihlu Wang
|
||||
* @version 1.1.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public final class SnowflakeIdentityGenerator implements IdentityGenerator<Long> {
|
||||
|
||||
/**
|
||||
* Constructs a SnowflakeGuidGenerator with the default start epoch and custom worker ID, data
|
||||
* centre ID.
|
||||
*
|
||||
* @param dataCentreId the data centre ID (between 0 and 31)
|
||||
* @param workerId the worker ID (between 0 and 31)
|
||||
*/
|
||||
public SnowflakeIdentityGenerator(long dataCentreId, long workerId) {
|
||||
this(dataCentreId, workerId, DEFAULT_CUSTOM_EPOCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a SnowflakeGuidGenerator with a custom epoch, worker ID, and data centre ID.
|
||||
*
|
||||
* @param dataCentreId the data centre ID (between 0 and 31)
|
||||
* @param workerId the worker ID (between 0 and 31)
|
||||
* @param startEpoch the custom epoch timestamp (in milliseconds) to start generating IDs from
|
||||
* @throws IllegalArgumentException if the start epoch is greater than the current timestamp,
|
||||
* or if the worker ID or data centre ID is out of range
|
||||
*/
|
||||
public SnowflakeIdentityGenerator(long dataCentreId, long workerId, long startEpoch) {
|
||||
if (startEpoch > currentTimestamp()) {
|
||||
throw new IllegalArgumentException("Start Epoch can not be greater than current timestamp!");
|
||||
}
|
||||
|
||||
var maxWorkerId = ~(-1L << workerIdBits);
|
||||
if (workerId > maxWorkerId || workerId < 0) {
|
||||
throw new IllegalArgumentException(String.format("Worker Id can't be greater than %d or less than 0",
|
||||
maxWorkerId));
|
||||
}
|
||||
|
||||
var maxDataCentreId = ~(-1L << dataCentreIdBits);
|
||||
if (dataCentreId > maxDataCentreId || dataCentreId < 0) {
|
||||
throw new IllegalArgumentException(String.format("Data Centre Id can't be greater than %d or less than 0",
|
||||
maxDataCentreId));
|
||||
}
|
||||
|
||||
this.startEpoch = startEpoch;
|
||||
this.workerId = workerId;
|
||||
this.dataCentreId = dataCentreId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next unique ID.
|
||||
*
|
||||
* @return the generated unique ID
|
||||
* @throws TimingException if the system clock moves backwards, indicating an invalid sequence
|
||||
* of timestamps.
|
||||
*/
|
||||
@Override
|
||||
public synchronized Long nextId() {
|
||||
var timestamp = currentTimestamp();
|
||||
|
||||
// if the current time is less than the timestamp of the last ID generation, it means that
|
||||
// the system clock has been set back and an exception should be thrown
|
||||
if (timestamp < lastTimestamp) {
|
||||
throw new TimingException("Clock moved backwards. Refusing to generate id for %d milliseconds"
|
||||
.formatted(lastTimestamp - timestamp));
|
||||
}
|
||||
|
||||
// if generated at the same time, perform intra-millisecond sequences
|
||||
long sequenceBits = 12L;
|
||||
if (lastTimestamp == timestamp) {
|
||||
long sequenceMask = ~(-1L << sequenceBits);
|
||||
sequence = (sequence + 1) & sequenceMask;
|
||||
// sequence overflow in milliseconds
|
||||
if (sequence == 0) {
|
||||
// block to the next millisecond, get a new timestamp
|
||||
timestamp = awaitToNextMillis(lastTimestamp);
|
||||
}
|
||||
}
|
||||
// timestamp change, sequence reset in milliseconds
|
||||
else {
|
||||
sequence = 0L;
|
||||
}
|
||||
|
||||
// timestamp of last ID generation
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
// shifted and put together by or operations to form a 64-bit ID
|
||||
var timestampLeftShift = sequenceBits + workerIdBits + dataCentreIdBits;
|
||||
var dataCentreIdShift = sequenceBits + workerIdBits;
|
||||
return ((timestamp - startEpoch) << timestampLeftShift)
|
||||
| (dataCentreId << dataCentreIdShift)
|
||||
| (workerId << sequenceBits)
|
||||
| sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks until the next millisecond to obtain a new timestamp.
|
||||
*
|
||||
* @param lastTimestamp the timestamp when the last ID was generated
|
||||
* @return the current timestamp
|
||||
*/
|
||||
private long awaitToNextMillis(long lastTimestamp) {
|
||||
var timestamp = currentTimestamp();
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = currentTimestamp();
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current timestamp in milliseconds.
|
||||
*
|
||||
* @return the current timestamp
|
||||
*/
|
||||
private long currentTimestamp() {
|
||||
return LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default custom epoch.
|
||||
*
|
||||
* @value 2015-01-01T00:00:00Z
|
||||
*/
|
||||
private static final long DEFAULT_CUSTOM_EPOCH = 1_420_070_400_000L;
|
||||
|
||||
/**
|
||||
* The start epoch timestamp to generate IDs from.
|
||||
*/
|
||||
private final long startEpoch;
|
||||
|
||||
/**
|
||||
* The number of bits reserved for the worker ID.
|
||||
*/
|
||||
private final long workerIdBits = 5L;
|
||||
|
||||
/**
|
||||
* The number of bits reserved for the data centre ID.
|
||||
*/
|
||||
private final long dataCentreIdBits = 5L;
|
||||
|
||||
/**
|
||||
* The worker ID assigned to this generator.
|
||||
*/
|
||||
private final long workerId;
|
||||
|
||||
/**
|
||||
* The data centre ID assigned to this generator.
|
||||
*/
|
||||
private final long dataCentreId;
|
||||
|
||||
/**
|
||||
* The current sequence number.
|
||||
*/
|
||||
private long sequence = 0L;
|
||||
|
||||
/**
|
||||
* The timestamp of the last generated ID.
|
||||
*/
|
||||
private long lastTimestamp = -1L;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2024-2025 OnixByte.
|
||||
~
|
||||
~ 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.
|
||||
-->
|
||||
<configuration>
|
||||
<property name="COLOURFUL_OUTPUT" value="%black(%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK}) %highlight(%-5level) %black(---) %black([%10.10t]) %cyan(%-20.20logger{20}) %black(:) %msg%n"/>
|
||||
<property name="STANDARD_OUTPUT" value="%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK} %-5level %black(---) [%10.10t] %-20.20logger{20} : %msg%n"/>
|
||||
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<pattern>${COLOURFUL_OUTPUT}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user