From 61776d4c8809a92e414dc6e1adf9b137a57838ce Mon Sep 17 00:00:00 2001 From: minglipro Date: Fri, 25 Jul 2025 21:25:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(utils):=20=E6=B7=BB=E5=8A=A0=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E5=B7=A5=E5=85=B7=E7=B1=BB=E5=92=8C=20Minecraft=20SLA?= =?UTF-8?q?=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Collection 工具类,提供集合操作方法 - 新增 ComponentBean 注解,用于组件标识- 新增 DateTime、DateTimeOffset 和 DateTimeUnit 类,提供时间处理功能 - 新增 Debouncer 类,实现防抖功能 - 新增 Minecraft SLA 相关类,包括 Description 和 Extra - 更新 .gitattributes,添加 PNG 文件的二进制处理 - 更新 .gitignore,排除 Idea 项目图标- 重构 build.gradle.kts,优化项目构建配置 --- .gitattributes | 1 + .gitignore | 2 + .idea/icon.png | Bin 0 -> 100849 bytes build.gradle.kts | 155 ++----- gradle.properties | 7 +- lombok.config | 1 + package.json | 28 +- settings.gradle.kts | 3 +- src/com/mingliqiye/utils/Main.java | 6 + src/com/mingliqiye/utils/bean/Factory.java | 249 +++++++++++ .../utils/bean/annotation/ComponentBean.java | 12 + .../utils/bean/annotation/InjectBean.java | 12 + .../utils/bean/springboot/SpringBeanUtil.java | 78 ++++ .../utils/collection/Collection.java | 305 +++++++++++++ .../mingliqiye/utils/collection/Lists.java | 249 +++++++++++ src/com/mingliqiye/utils/collection/Maps.java | 187 ++++++++ src/com/mingliqiye/utils/collection/Sets.java | 145 ++++++ .../utils/concurrent/IsChanged.java | 84 ++++ .../utils/data/ThreadLocalDataHolder.java | 83 ++++ src/com/mingliqiye/utils/file/FileUtil.java | 344 +++++++++++++++ .../mingliqiye/utils/functions/Debouncer.java | 71 +++ src/com/mingliqiye/utils/hash/HashUtils.java | 93 ++++ src/com/mingliqiye/utils/http/Response.java | 47 ++ .../utils/minecraft/slp/Description.java | 10 + .../mingliqiye/utils/minecraft/slp/Extra.java | 12 + .../minecraft/slp/MinecraftServerStatus.java | 15 + .../utils/minecraft/slp/PlayerSample.java | 10 + .../utils/minecraft/slp/Players.java | 11 + .../mingliqiye/utils/minecraft/slp/SLP.java | 198 +++++++++ .../utils/minecraft/slp/Version.java | 10 + .../mingliqiye/utils/network/NetWorkUtil.java | 10 + .../utils/network/NetworkAddress.java | 196 +++++++++ .../utils/network/NetworkEndpoint.java | 142 ++++++ .../utils/network/NetworkException.java | 27 ++ .../mingliqiye/utils/network/NetworkPort.java | 42 ++ .../mingliqiye/utils/string/StringUtil.java | 214 +++++++++ .../mingliqiye/utils/system/SystemUtil.java | 198 +++++++++ src/com/mingliqiye/utils/time/DateTime.java | 414 ++++++++++++++++++ .../mingliqiye/utils/time/DateTimeOffset.java | 43 ++ .../mingliqiye/utils/time/DateTimeUnit.java | 43 ++ src/com/mingliqiye/utils/time/Formatter.java | 93 ++++ .../utils/time/serialization/Jackson.java | 188 ++++++++ .../typehandlers/DateTimeTypeHandler.java | 107 +++++ src/com/mingliqiye/utils/uuid/UUID.java | 252 +++++++++++ .../mingliqiye/utils/uuid/UUIDException.java | 12 + .../utils/uuid/serialization/Jackson.java | 109 +++++ .../typehandlers/UUIDBinaryTypeHandler.java | 111 +++++ src/main/java/com/mingliqiye/Main.java | 13 - 48 files changed, 4487 insertions(+), 155 deletions(-) create mode 100644 .idea/icon.png create mode 100644 lombok.config create mode 100644 src/com/mingliqiye/utils/Main.java create mode 100644 src/com/mingliqiye/utils/bean/Factory.java create mode 100644 src/com/mingliqiye/utils/bean/annotation/ComponentBean.java create mode 100644 src/com/mingliqiye/utils/bean/annotation/InjectBean.java create mode 100644 src/com/mingliqiye/utils/bean/springboot/SpringBeanUtil.java create mode 100644 src/com/mingliqiye/utils/collection/Collection.java create mode 100644 src/com/mingliqiye/utils/collection/Lists.java create mode 100644 src/com/mingliqiye/utils/collection/Maps.java create mode 100644 src/com/mingliqiye/utils/collection/Sets.java create mode 100644 src/com/mingliqiye/utils/concurrent/IsChanged.java create mode 100644 src/com/mingliqiye/utils/data/ThreadLocalDataHolder.java create mode 100644 src/com/mingliqiye/utils/file/FileUtil.java create mode 100644 src/com/mingliqiye/utils/functions/Debouncer.java create mode 100644 src/com/mingliqiye/utils/hash/HashUtils.java create mode 100644 src/com/mingliqiye/utils/http/Response.java create mode 100644 src/com/mingliqiye/utils/minecraft/slp/Description.java create mode 100644 src/com/mingliqiye/utils/minecraft/slp/Extra.java create mode 100644 src/com/mingliqiye/utils/minecraft/slp/MinecraftServerStatus.java create mode 100644 src/com/mingliqiye/utils/minecraft/slp/PlayerSample.java create mode 100644 src/com/mingliqiye/utils/minecraft/slp/Players.java create mode 100644 src/com/mingliqiye/utils/minecraft/slp/SLP.java create mode 100644 src/com/mingliqiye/utils/minecraft/slp/Version.java create mode 100644 src/com/mingliqiye/utils/network/NetWorkUtil.java create mode 100644 src/com/mingliqiye/utils/network/NetworkAddress.java create mode 100644 src/com/mingliqiye/utils/network/NetworkEndpoint.java create mode 100644 src/com/mingliqiye/utils/network/NetworkException.java create mode 100644 src/com/mingliqiye/utils/network/NetworkPort.java create mode 100644 src/com/mingliqiye/utils/string/StringUtil.java create mode 100644 src/com/mingliqiye/utils/system/SystemUtil.java create mode 100644 src/com/mingliqiye/utils/time/DateTime.java create mode 100644 src/com/mingliqiye/utils/time/DateTimeOffset.java create mode 100644 src/com/mingliqiye/utils/time/DateTimeUnit.java create mode 100644 src/com/mingliqiye/utils/time/Formatter.java create mode 100644 src/com/mingliqiye/utils/time/serialization/Jackson.java create mode 100644 src/com/mingliqiye/utils/time/typehandlers/DateTimeTypeHandler.java create mode 100644 src/com/mingliqiye/utils/uuid/UUID.java create mode 100644 src/com/mingliqiye/utils/uuid/UUIDException.java create mode 100644 src/com/mingliqiye/utils/uuid/serialization/Jackson.java create mode 100644 src/com/mingliqiye/utils/uuid/typehandlers/UUIDBinaryTypeHandler.java delete mode 100644 src/main/java/com/mingliqiye/Main.java diff --git a/.gitattributes b/.gitattributes index 0f4764e..30e0ce8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ *.bat text eol=crlf *.jar binary *.class binary +*.png binary diff --git a/.gitignore b/.gitignore index bf9e148..79621be 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ out/ !**/src/main/**/out/ !**/src/test/**/out/ +!.idea/icon.png + ### Eclipse ### .apt_generated .classpath diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dfbf5af06689b341dfa23cc416835745f12f9cee GIT binary patch literal 100849 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 literal 0 HcmV?d00001 diff --git a/build.gradle.kts b/build.gradle.kts index 72c96e7..004474f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,164 +1,77 @@ -import java.security.MessageDigest -import java.text.SimpleDateFormat -import java.util.Date - plugins { - id("java") + idea id("java-library") id("maven-publish") - id("com.github.johnrengelman.shadow") version ("8.1.1") } val GROUPSID = project.properties["GROUPSID"] as String val VERSIONS = project.properties["VERSIONS"] as String val ARTIFACTID = project.properties["ARTIFACTID"] as String -val MAINCLASS = project.properties["MAINCLASS"] as String -val buildTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date()) -val jarNameStr = "${ARTIFACTID}-${VERSIONS}" -val jarName = "${jarNameStr}.jar" -val srcJarName = "${jarNameStr}-sources.jar" -val fatJarName = "${jarNameStr}-all.jar" group = GROUPSID version = VERSIONS -val libDir = rootDir.resolve("build").resolve("libs") -val publicationsDir = - rootDir.resolve("build").resolve("publications").resolve("mavenJava") +base { + archivesName.set(ARTIFACTID) +} java { + withJavadocJar() withSourcesJar() + toolchain.languageVersion.set(JavaLanguageVersion.of(8)) } - dependencies { - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + compileOnly("org.springframework.boot:spring-boot-starter:2.7.14") + compileOnly("com.fasterxml.jackson.core:jackson-databind:2.19.2") + compileOnly("org.mybatis:mybatis:3.5.19") + + implementation("org.bouncycastle:bcprov-jdk18on:1.81") + implementation("com.github.f4b6a3:uuid-creator:6.1.0") + implementation("org.mindrot:jbcrypt:0.4") implementation("org.jetbrains:annotations:24.0.0") + compileOnly("org.projectlombok:lombok:1.18.38") + annotationProcessor("org.jetbrains:annotations:24.0.0") -} - -tasks.test { - useJUnitPlatform() -} - -private fun generateHash(file: File, string: String): String { - val md = MessageDigest.getInstance(string) - file.forEachBlock(4096) { bytes, size -> - md.update(bytes, 0, size) - } - return md.digest().joinToString("") { - "%02x".format(it) - } -} - -private fun getHash(outpath: String, file: File) { - val md5 = generateHash(file, "MD5") - val sha1 = generateHash(file, "SHA-1") - val sha256 = generateHash(file, "SHA-256") - val sha512 = generateHash(file, "SHA-512") - val md5f = File(outpath, file.getName() + ".md5") - val sha1f = File(outpath, file.getName() + ".sha1") - val sha256f = File(outpath, file.getName() + ".sha256") - val sha512f = File(outpath, file.getName() + ".sha512") - md5f.writeText(md5) - sha1f.writeText(sha1) - sha256f.writeText(sha256) - sha512f.writeText(sha512) + annotationProcessor("org.projectlombok:lombok:1.18.38") } +sourceSets.main.configure { + java.setSrcDirs(files("src")) + resources.setSrcDirs(files("resources")) +} -tasks.shadowJar { - archiveFileName.set(fatJarName) - archiveClassifier.set("all") - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(sourceSets.main.get().output) // 编译输出 - from(project.configurations.runtimeClasspath) // 运行时依赖 - from("src/main/resources") { // 资源文件 - into(".") - } - manifest { - attributes["Main-Class"] = MAINCLASS - attributes["Implementation-GroupId"] = GROUPSID - attributes["Implementation-ArtifactId"] = ARTIFACTID - attributes["Implementation-Version"] = VERSIONS - attributes["Email"] = "minglipro@163.com" - attributes["Implementation-Vendor"] = "minglipro|Armamem0t" - attributes["Copyright"] = - "Copyright 2026 minglipro All rights reserved." - attributes["Env"] = "prod" - attributes["LICENSE"] = "Apache License 2.0" - attributes["Created"] = "2025-06-26 09:13:51" - attributes["Updated"] = buildTime - } - mergeServiceFiles() - exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") // 排除签名文件 - from("LICENSE") - into(".") +tasks.withType { + options.encoding = "UTF-8" +} + +tasks.withType().configureEach { + jvmArgs = listOf( + "-Dfile.encoding=UTF-8", + "-Dsun.stdout.encoding=UTF-8", + "-Dsun.stderr.encoding=UTF-8" + ) } -tasks.jar { - archiveFileName.set(jarName) - doFirst { - delete(libDir) - mkdir(libDir) - } - manifest { - attributes["Main-Class"] = MAINCLASS - attributes["Implementation-GroupId"] = GROUPSID - attributes["Implementation-ArtifactId"] = ARTIFACTID - attributes["Implementation-Version"] = VERSIONS - attributes["Email"] = "minglipro@163.com" - attributes["Implementation-Vendor"] = "minglipro|Armamem0t" - attributes["Copyright"] = - "Copyright 2026 minglipro All rights reserved." - attributes["Env"] = "prod" - attributes["LICENSE"] = "Apache License 2.0" - attributes["Created"] = "2025-06-26 09:13:51" - attributes["Updated"] = buildTime - } - from("LICENSE") - into(".") -} -tasks.register("build-jar") { - dependsOn(tasks.jar) - dependsOn(tasks.shadowJar) - dependsOn(tasks["sourcesJar"]) - dependsOn(tasks["generatePomFileForMavenJavaPublication"]) - dependsOn(tasks["generateMetadataFileForMavenJavaPublication"]) - doLast { - getHash(libDir.toString(),File(libDir, jarName)) - getHash(libDir.toString(),File(libDir, fatJarName)) - getHash(libDir.toString(),File(libDir, srcJarName)) - getHash(publicationsDir.toString(),File(publicationsDir, "module.json")) - getHash(publicationsDir.toString(),File(publicationsDir, "pom-default.xml")) - } -} -components { - withType().configureEach { - withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) { - skip() - } - } +tasks.withType { + options.encoding = "UTF-8" } + publishing { repositories { maven { + name = "localMaven" url = uri("D:/git/maven-repository-raw") } } publications { create("mavenJava") { from(components["java"]) - artifact(tasks.shadowJar.get()) groupId = GROUPSID artifactId = ARTIFACTID version = VERSIONS } } } - - diff --git a/gradle.properties b/gradle.properties index b5598b5..20f4f88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,4 @@ -GROUPSID=com.mingliqiye -ARTIFACTID=socket-utilts -VERSIONS=1.0.1 -MAINCLASS=com.mingliqiye.Main JDKVERSIONS=1.8 +GROUPSID=com.mingliqiye.utils +ARTIFACTID=mingli-utils +VERSIONS=1.0.4 diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..7164ffe --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = false diff --git a/package.json b/package.json index 7b1338a..6f6039e 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,14 @@ { - "name": "maven-repository", - "version": "1.0.0", - "description": "", - "scripts": { - "build": "gradle build-jar", - "buildw": "gradlew build-jar", - "format": "prettier --write \"**/*.java\"" - }, - "keywords": [], - "author": "", - "license": "ISC", - "packageManager": "pnpm@10.4.1", - "devDependencies": { - "prettier-plugin-java": "^2.7.1", - "prettier": "^3.6.2" - } + "name": "maven-repository", + "version": "1.0.0", + "scripts": { + "build": "gradle build-jar", + "buildw": "gradlew build-jar", + "format": "prettier --write \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,astro,json,java}\"" + }, + "packageManager": "pnpm@10.4.1", + "devDependencies": { + "prettier-plugin-java": "^2.7.1", + "prettier": "^3.6.2" + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index e314529..94b1490 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,3 @@ val ARTIFACTID: String by settings.extra -rootProject.name= ARTIFACTID +val VERSIONS: String by settings.extra +rootProject.name = "$ARTIFACTID $VERSIONS" diff --git a/src/com/mingliqiye/utils/Main.java b/src/com/mingliqiye/utils/Main.java new file mode 100644 index 0000000..cbfcb1b --- /dev/null +++ b/src/com/mingliqiye/utils/Main.java @@ -0,0 +1,6 @@ +package com.mingliqiye.utils; + +public class Main { + + public static void main(String[] args) {} +} diff --git a/src/com/mingliqiye/utils/bean/Factory.java b/src/com/mingliqiye/utils/bean/Factory.java new file mode 100644 index 0000000..039e709 --- /dev/null +++ b/src/com/mingliqiye/utils/bean/Factory.java @@ -0,0 +1,249 @@ +package com.mingliqiye.utils.bean; + +import com.mingliqiye.utils.bean.annotation.ComponentBean; +import com.mingliqiye.utils.bean.annotation.InjectBean; + +import java.io.File; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.Enumeration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 类似于SpringBoot的Bean管理器 + * + * @author MingLiPro + */ +public class Factory { + + /** + * 存储所有已注册的Bean实例,键为Bean名称,值为Bean实例 + */ + public static final ConcurrentMap beans = + new ConcurrentHashMap<>(); + + /** + * 存储按类型查找的Bean实例,键为Bean的Class对象,值为Bean实例 + */ + private static final ConcurrentMap, Object> typeBeans = + new ConcurrentHashMap<>(); + + /** + * 私有构造函数,防止外部实例化该类 + */ + private Factory() {} + + /** + * 自动扫描指定类所在包下的所有类并注册为Bean + * + * @param c 指定的类,用于获取其所在的包 + * @throws IllegalArgumentException 如果传入的类为null或位于默认包中 + */ + public static void autoScan(Class c) { + if (c == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + Package pkg = c.getPackage(); + if (pkg == null) { + throw new IllegalArgumentException( + "Class is in the default package" + ); + } + scan(pkg.getName()); + } + + /** + * 扫描指定包路径下的所有类文件,并注册其中带有@ComponentBean注解的类为Bean + * + * @param basePackage 要扫描的基础包名 + * @throws RuntimeException 如果在扫描过程中发生异常 + */ + public static void scan(String basePackage) { + try { + String path = basePackage.replace('.', '/'); + ClassLoader classLoader = + Thread.currentThread().getContextClassLoader(); + Enumeration resources = null; + resources = classLoader.getResources(path); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + File file = new File(resource.toURI()); + scanDirectory(file, basePackage); + } + injectDependencies(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 递归扫描目录中的所有类文件,并注册符合条件的类为Bean + * + * @param directory 当前要扫描的目录 + * @param packageName 当前目录对应的包名 + * @throws Exception 如果在扫描或类加载过程中发生异常 + */ + private static void scanDirectory(File directory, String packageName) + throws Exception { + File[] files = directory.listFiles(); + if (files == null) { + return; + } + + for (File file : files) { + if (file.isDirectory()) { + scanDirectory(file, packageName + "." + file.getName()); + } else if (file.getName().endsWith(".class")) { + String className = + packageName + '.' + file.getName().replace(".class", ""); + registerComponent(Class.forName(className)); + } + } + } + + /** + * 注册一个带有@ComponentBean注解的类为Bean实例 + * + * @param clazz 要注册的类 + * @throws Exception 如果在实例化类或处理注解时发生异常 + */ + private static void registerComponent(Class clazz) throws Exception { + if (clazz.isAnnotationPresent(ComponentBean.class)) { + ComponentBean component = clazz.getAnnotation(ComponentBean.class); + String name = component.value().isEmpty() + ? clazz.getName() + : component.value(); + Object instance = clazz.getDeclaredConstructor().newInstance(); + beans.put(name, instance); + typeBeans.put(clazz, instance); + + for (Class interfaceClass : clazz.getInterfaces()) { + typeBeans.putIfAbsent(interfaceClass, instance); + } + } + } + + /** + * 对所有已注册的Bean进行依赖注入处理 + * + * @throws Exception 如果在注入过程中发生异常 + */ + private static void injectDependencies() throws Exception { + for (Object bean : beans.values()) { + for (Field field : bean.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(InjectBean.class)) { + InjectBean inject = field.getAnnotation(InjectBean.class); + Object dependency = findDependency( + field.getType(), + inject.value() + ); + if (dependency == null) { + throw new IllegalStateException( + "No suitable dependency found for field " + + field.getName() + + " in class " + + bean.getClass().getName() + ); + } + field.setAccessible(true); + field.set(bean, dependency); + } + } + } + } + + /** + * 根据类型和名称查找对应的依赖实例 + * + * @param type 依赖的类型 + * @param name 依赖的名称(可为空) + * @return 找到的依赖实例,未找到则返回null + */ + private static Object findDependency(Class type, String name) { + if (!name.isEmpty()) { + return beans.get(name); + } + + Object dependency = typeBeans.get(type); + if (dependency != null) { + return dependency; + } + + for (Class interfaceType : typeBeans.keySet()) { + if (type.isAssignableFrom(interfaceType)) { + return typeBeans.get(interfaceType); + } + } + + return null; + } + + /** + * 将一个对象添加到Bean容器中,使用其类名作为键 + * + * @param object 要添加的对象 + * @throws RuntimeException 如果在注入依赖时发生异常 + */ + public static void add(Object object) { + Class clazz = object.getClass(); + String name = clazz.getName(); + beans.put(name, object); + typeBeans.put(clazz, object); + try { + injectDependencies(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 将一个对象以指定名称添加到Bean容器中 + * + * @param name Bean的名称 + * @param object 要添加的对象 + * @throws RuntimeException 如果在注入依赖时发生异常 + */ + public static void add(String name, Object object) { + beans.put(name, object); + typeBeans.put(object.getClass(), object); + try { + injectDependencies(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 根据类型获取对应的Bean实例 + * + * @param objclass Bean的类型 + * @param Bean的泛型类型 + * @return 对应类型的Bean实例,未找到则返回null + */ + public static T get(Class objclass) { + return objclass.cast(typeBeans.get(objclass)); + } + + /** + * 根据名称和类型获取对应的Bean实例 + * + * @param name Bean的名称 + * @param objclass Bean的类型 + * @param Bean的泛型类型 + * @return 对应名称和类型的Bean实例,未找到则返回null + */ + public static T get(String name, Class objclass) { + return objclass.cast(beans.get(name)); + } + + /** + * 根据名称获取对应的Bean实例 + * + * @param name Bean的名称 + * @return 对应名称的Bean实例,未找到则返回null + */ + public static Object get(String name) { + return beans.get(name); + } +} diff --git a/src/com/mingliqiye/utils/bean/annotation/ComponentBean.java b/src/com/mingliqiye/utils/bean/annotation/ComponentBean.java new file mode 100644 index 0000000..c594e9f --- /dev/null +++ b/src/com/mingliqiye/utils/bean/annotation/ComponentBean.java @@ -0,0 +1,12 @@ +package com.mingliqiye.utils.bean.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ComponentBean { + String value() default ""; +} diff --git a/src/com/mingliqiye/utils/bean/annotation/InjectBean.java b/src/com/mingliqiye/utils/bean/annotation/InjectBean.java new file mode 100644 index 0000000..be480d0 --- /dev/null +++ b/src/com/mingliqiye/utils/bean/annotation/InjectBean.java @@ -0,0 +1,12 @@ +package com.mingliqiye.utils.bean.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface InjectBean { + String value() default ""; +} diff --git a/src/com/mingliqiye/utils/bean/springboot/SpringBeanUtil.java b/src/com/mingliqiye/utils/bean/springboot/SpringBeanUtil.java new file mode 100644 index 0000000..d703662 --- /dev/null +++ b/src/com/mingliqiye/utils/bean/springboot/SpringBeanUtil.java @@ -0,0 +1,78 @@ +package com.mingliqiye.utils.bean.springboot; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * Spring Bean工具类 + * 实现ApplicationContextAware接口,并加入Component注解,让spring扫描到该bean + * 该类用于在普通Java类中注入bean,普通Java类中用@Autowired是无法注入bean的 + *

+ * 需要放入扫描类中 + * + * @author MingLiPro + */ +@Component +public class SpringBeanUtil implements ApplicationContextAware { + + /** + * 获取applicationContext + */ + @Getter + private static ApplicationContext applicationContext; + + /** + * 通过bean名称获取Bean实例 + * + * @param name bean名称 + * @return bean实例对象 + */ + public static Object getBean(String name) { + return getApplicationContext().getBean(name); + } + + /** + * 通过bean类型获取Bean实例 + * + * @param clazz bean的Class类型 + * @param 泛型类型 + * @return 指定类型的bean实例 + */ + public static T getBean(Class clazz) { + return getApplicationContext().getBean(clazz); + } + + /** + * 通过bean名称和类型获取指定的Bean实例 + * + * @param name bean名称 + * @param clazz bean的Class类型 + * @param 泛型类型 + * @return 指定名称和类型的bean实例 + */ + public static T getBean(String name, Class clazz) { + return getApplicationContext().getBean(name, clazz); + } + + /** + * 设置ApplicationContext上下文对象 + * 当Spring容器初始化时会自动调用此方法,将ApplicationContext注入到本工具类中 + * 通过判断避免重复赋值,确保只设置一次ApplicationContext + * + * @param applicationContext Spring应用上下文对象 + * @throws BeansException bean异常 + */ + @Override + public void setApplicationContext( + @NotNull ApplicationContext applicationContext + ) throws BeansException { + // 避免重复赋值,确保只设置一次ApplicationContext + if (SpringBeanUtil.applicationContext == null) { + SpringBeanUtil.applicationContext = applicationContext; + } + } +} diff --git a/src/com/mingliqiye/utils/collection/Collection.java b/src/com/mingliqiye/utils/collection/Collection.java new file mode 100644 index 0000000..8c0ccbe --- /dev/null +++ b/src/com/mingliqiye/utils/collection/Collection.java @@ -0,0 +1,305 @@ +package com.mingliqiye.utils.collection; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +/** + * 集合工具类,提供对列表和数组的常用操作方法。 + * + * @author MingLiPro + */ +public class Collection { + + /** + * 获取集合的第一个元素。 + * + * @param collection 集合 + * @param 元素类型 + * @return 第一个元素;如果集合为空或为null则返回 null + */ + @Nullable + public static T getFirst(@Nullable java.util.Collection collection) { + if (collection == null || collection.isEmpty()) { + return null; + } + + // 对于List类型,直接获取第一个元素 + if (collection instanceof List) { + return ((List) collection).get(0); + } + + // 对于其他Collection类型,使用迭代器获取第一个元素 + return collection.iterator().next(); + } + + /** + * 获取数组的第一个元素。 + * + * @param list 数组,不能为空 + * @param 元素类型 + * @return 第一个元素;如果数组为空则返回 null + */ + @Nullable + public static T getFirst(@NotNull T[] list) { + if (list.length == 0) { + return null; + } + return list[0]; + } + + /** + * 获取集合的最后一个元素。 + * + * @param collection 集合 + * @param 元素类型 + * @return 最后一个元素;如果集合为空或为null则返回 null + */ + @Nullable + public static T getLast(@Nullable java.util.Collection collection) { + if (collection == null || collection.isEmpty()) { + return null; + } + + // 对于List类型,直接获取最后一个元素 + if (collection instanceof List) { + List list = (List) collection; + return list.get(list.size() - 1); + } + + // 对于其他Collection类型,需要遍历到最后一个元素 + T lastElement = null; + for (T element : collection) { + lastElement = element; + } + return lastElement; + } + + /** + * 获取数组的最后一个元素。 + * + * @param list 数组,不能为空 + * @param 元素类型 + * @return 最后一个元素;如果数组为空则返回 null + */ + @Nullable + public static T getLast(@NotNull T[] list) { + if (list.length == 0) { + return null; + } + return list[list.length - 1]; + } + + /** + * 获取列表中指定索引的元素,如果索引超出范围则返回默认值。 + * + * @param list 列表 + * @param index 索引 + * @param defaultValue 默认值 + * @param 元素类型 + * @return 指定索引的元素或默认值 + */ + @Nullable + public static T getOrDefault( + @NotNull List list, + int index, + @Nullable T defaultValue + ) { + if (index < 0 || index >= list.size()) { + return defaultValue; + } + return list.get(index); + } + + /** + * 获取数组中指定索引的元素,如果索引超出范围则返回默认值。 + * + * @param array 数组 + * @param index 索引 + * @param defaultValue 默认值 + * @param 元素类型 + * @return 指定索引的元素或默认值 + */ + @Nullable + public static T getOrDefault( + @NotNull T[] array, + int index, + @Nullable T defaultValue + ) { + if (index < 0 || index >= array.length) { + return defaultValue; + } + return array[index]; + } + + /** + * 获取列表的安全子列表,自动处理边界情况。 + * + * @param list 原始列表 + * @param fromIndex 起始索引(包含) + * @param toIndex 结束索引(不包含) + * @param 元素类型 + * @return 子列表 + */ + @NotNull + public static List safeSubList( + @NotNull List list, + int fromIndex, + int toIndex + ) { + int size = list.size(); + if (size == 0) { + return Collections.emptyList(); + } + + // 调整边界 + fromIndex = Math.max(0, fromIndex); + toIndex = Math.min(size, toIndex); + + if (fromIndex >= toIndex) { + return Collections.emptyList(); + } + + return list.subList(fromIndex, toIndex); + } + + /** + * 判断列表是否为空或null。 + * + * @param list 待检查的列表 + * @return 如果列表为null或空则返回true,否则返回false + */ + public static boolean isEmpty(@Nullable List list) { + return list == null || list.isEmpty(); + } + + /** + * 判断数组是否为空或null。 + * + * @param array 待检查的数组 + * @return 如果数组为null或空则返回true,否则返回false + */ + public static boolean isEmpty(@Nullable Object[] array) { + return array == null || array.length == 0; + } + + /** + * 查找列表中第一个满足条件的元素。 + * + * @param list 列表 + * @param predicate 条件谓词 + * @param 元素类型 + * @return 第一个满足条件的元素,如果没有则返回null + */ + @Nullable + public static T findFirst( + @NotNull List list, + @NotNull Predicate predicate + ) { + for (T item : list) { + if (predicate.test(item)) { + return item; + } + } + return null; + } + + /** + * 查找数组中第一个满足条件的元素。 + * + * @param array 数组 + * @param predicate 条件谓词 + * @param 元素类型 + * @return 第一个满足条件的元素,如果没有则返回null + */ + @Nullable + public static T findFirst( + @NotNull T[] array, + @NotNull Predicate predicate + ) { + for (T item : array) { + if (predicate.test(item)) { + return item; + } + } + return null; + } + + /** + * 过滤列表中满足条件的元素。 + * + * @param list 原始列表 + * @param predicate 条件谓词 + * @param 元素类型 + * @return 包含满足条件元素的新列表 + */ + @NotNull + public static List filter( + @NotNull List list, + @NotNull Predicate predicate + ) { + List result = new ArrayList<>(); + for (T item : list) { + if (predicate.test(item)) { + result.add(item); + } + } + return result; + } + + /** + * 过滤数组中满足条件的元素。 + * + * @param array 原始数组 + * @param predicate 条件谓词 + * @param 元素类型 + * @return 包含满足条件元素的新列表 + */ + @NotNull + public static List filter( + @NotNull T[] array, + @NotNull Predicate predicate + ) { + return filter(Arrays.asList(array), predicate); + } + + /** + * 将列表转换为数组。 + * + * @param list 列表 + * @param clazz 元素类型class + * @param 元素类型 + * @return 转换后的数组 + */ + @SuppressWarnings("unchecked") + @NotNull + public static T[] toArray( + @NotNull List list, + @NotNull Class clazz + ) { + T[] array = (T[]) java.lang.reflect.Array.newInstance( + clazz, + list.size() + ); + return list.toArray(array); + } + + /** + * 将集合转换为列表。 + * + * @param collection 集合 + * @param 元素类型 + * @return 转换后的列表 + */ + @NotNull + public static List toList( + @NotNull java.util.Collection collection + ) { + return new ArrayList<>(collection); + } +} diff --git a/src/com/mingliqiye/utils/collection/Lists.java b/src/com/mingliqiye/utils/collection/Lists.java new file mode 100644 index 0000000..6b18597 --- /dev/null +++ b/src/com/mingliqiye/utils/collection/Lists.java @@ -0,0 +1,249 @@ +package com.mingliqiye.utils.collection; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * Lists工具类提供了一系列创建List实现的便捷方法。 + * + * @author MingLiPro + */ +public class Lists { + + /** + * 创建一个空的ArrayList实例。 + * + * @param 列表元素的类型 + * @return 新创建的空ArrayList实例 + */ + public static List newArrayList() { + return new ArrayList<>(); + } + + /** + * 根据可变参数创建一个包含指定元素的ArrayList实例。 + * + * @param ts 要添加到列表中的元素,可以为0个或多个 + * @param 列表元素的类型 + * @return 包含指定元素的新ArrayList实例 + */ + public static List newArrayList(T... ts) { + List list = newArrayList(); + list.addAll(Arrays.asList(ts)); + return list; + } + + /** + * 根据已有列表创建一个新的ArrayList实例。 + * + * @param list 要复制的列表 + * @param 列表元素的类型 + * @return 包含原列表所有元素的新ArrayList实例 + */ + public static List newArrayList(List list) { + List newList = newArrayList(); + newList.addAll(list); + return newList; + } + + /** + * 根据可迭代对象创建一个ArrayList实例。 + * + * @param iterable 可迭代对象 + * @param 列表元素的类型 + * @return 包含可迭代对象中所有元素的新ArrayList实例 + */ + public static List newArrayList(Iterable iterable) { + List list = newArrayList(); + for (T t : iterable) { + list.add(t); + } + return list; + } + + /** + * 创建一个指定初始容量的空ArrayList实例。 + * + * @param size 初始容量大小 + * @param 列表元素的类型 + * @return 指定初始容量的空ArrayList实例 + */ + public static List newArrayList(int size) { + return new ArrayList<>(size); + } + + /** + * 创建一个指定大小并用单个元素填充的ArrayList实例。 + * + * @param size 列表大小 + * @param t 用于填充列表的元素 + * @param 列表元素的类型 + * @return 指定大小且所有元素都相同的ArrayList实例 + */ + public static List newArrayList(int size, T t) { + List list = newArrayList(size); + for (int i = 0; i < size; i++) { + list.add(t); + } + return list; + } + + /** + * 创建一个指定大小并交替使用两个元素填充的ArrayList实例。 + * + * @param size 列表大小 + * @param t 第一个填充元素(索引为偶数时使用) + * @param t1 第二个填充元素(索引为奇数时使用) + * @param 列表元素的类型 + * @return 指定大小且交替填充两个元素的ArrayList实例 + */ + public static List newArrayList(int size, T t, T t1) { + List list = newArrayList(size); + for (int i = 0; i < size; i++) { + list.add(i % 2 == 0 ? t : t1); + } + return list; + } + + /** + * 创建一个指定大小并循环使用三个元素填充的ArrayList实例。 + * + * @param size 列表大小 + * @param t 第一个填充元素(索引模3等于0时使用) + * @param t1 第二个填充元素(索引模3等于1时使用) + * @param t2 第三个填充元素(索引模3等于2时使用) + * @param 列表元素的类型 + * @return 指定大小且循环填充三个元素的ArrayList实例 + */ + public static List newArrayList(int size, T t, T t1, T t2) { + List list = newArrayList(size); + for (int i = 0; i < size; i++) { + list.add(i % 3 == 0 ? t : i % 3 == 1 ? t1 : t2); + } + return list; + } + + /** + * 创建一个指定大小并循环使用四个元素填充的ArrayList实例。 + * + * @param size 列表大小 + * @param t 第一个填充元素(索引模4等于0时使用) + * @param t1 第二个填充元素(索引模4等于1时使用) + * @param t2 第三个填充元素(索引模4等于2时使用) + * @param t3 第四个填充元素(索引模4等于3时使用) + * @param 列表元素的类型 + * @return 指定大小且循环填充四个元素的ArrayList实例 + */ + public static List newArrayList(int size, T t, T t1, T t2, T t3) { + List list = newArrayList(size); + for (int i = 0; i < size; i++) { + list.add(i % 4 == 0 ? t : i % 4 == 1 ? t1 : i % 4 == 2 ? t2 : t3); + } + return list; + } + + /** + * 创建一个空的LinkedList实例。 + * + * @param 列表元素的类型 + * @return 新创建的空LinkedList实例 + */ + public static List newLinkedList() { + return new LinkedList<>(); + } + + /** + * 根据可变参数创建一个包含指定元素的LinkedList实例。 + * + * @param ts 要添加到列表中的元素,可以为0个或多个 + * @param 列表元素的类型 + * @return 包含指定元素的新LinkedList实例 + */ + public static List newLinkedList(T... ts) { + List list = newLinkedList(); + list.addAll(Arrays.asList(ts)); + return list; + } + + /** + * 根据已有列表创建一个新的LinkedList实例。 + * + * @param list 要复制的列表 + * @param 列表元素的类型 + * @return 包含原列表所有元素的新LinkedList实例 + */ + public static List newLinkedList(List list) { + List newList = newLinkedList(); + newList.addAll(list); + return newList; + } + + /** + * 创建一个空的Vector实例。 + * + * @param 列表元素的类型 + * @return 新创建的空Vector实例 + */ + public static List newVector() { + return new Vector<>(); + } + + /** + * 根据可变参数创建一个包含指定元素的Vector实例。 + * + * @param ts 要添加到列表中的元素,可以为0个或多个 + * @param 列表元素的类型 + * @return 包含指定元素的新Vector实例 + */ + public static List newVector(T... ts) { + List list = newVector(); + list.addAll(Arrays.asList(ts)); + return list; + } + + /** + * 根据已有列表创建一个新的Vector实例。 + * + * @param list 要复制的列表 + * @param 列表元素的类型 + * @return 包含原列表所有元素的新Vector实例 + */ + public static List newVector(List list) { + List newList = newVector(); + newList.addAll(list); + return newList; + } + + /** + * 将指定列表中的每个元素转换为字符串表示形式 + * + * @param 列表元素的类型 + * @param list 要转换的列表,不能为空 + * @return 包含原列表各元素字符串表示的新列表,保持相同的顺序 + */ + public static List toStringList(@NotNull List list) { + // 创建与原列表相同大小的新列表,用于存储字符串转换结果 + List newList = newArrayList(list.size()); + for (T t : list) { + newList.add(t == null ? "null" : t.toString()); + } + return newList; + } + + /** + * 将指定数组中的每个元素转换为字符串表示形式 + * + * @param 数组元素的类型 + * @param list 要转换的数组,不能为空 + * @return 包含原数组各元素字符串表示的新字符串数组 + */ + public static String[] toStringList(@NotNull T[] list) { + // 创建新的字符串列表,用于存储转换后的结果 + List newList = newArrayList(list.length); + for (T t : list) { + newList.add(t == null ? "null" : t.toString()); + } + return newList.toArray(new String[0]); + } +} diff --git a/src/com/mingliqiye/utils/collection/Maps.java b/src/com/mingliqiye/utils/collection/Maps.java new file mode 100644 index 0000000..da7b2fe --- /dev/null +++ b/src/com/mingliqiye/utils/collection/Maps.java @@ -0,0 +1,187 @@ +package com.mingliqiye.utils.collection; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Maps工具类提供了一系列创建Map实现的便捷方法。 + * + * @author MingLiPro + */ +public class Maps { + + /** + * 创建一个空的HashMap实例。 + * + * @param Map键的类型 + * @param Map值的类型 + * @return 新创建的空HashMap实例 + */ + public static Map newHashMap() { + return new HashMap<>(); + } + + /** + * 创建一个指定初始容量的空HashMap实例。 + * + * @param size 初始容量大小 + * @param Map键的类型 + * @param Map值的类型 + * @return 指定初始容量的空HashMap实例 + */ + public static Map newHashMap(int size) { + return new HashMap<>(size); + } + + /** + * 根据已有Map创建一个新的HashMap实例。 + * + * @param map 要复制的Map + * @param Map键的类型 + * @param Map值的类型 + * @return 包含原Map所有元素的新HashMap实例 + */ + public static Map newHashMap(Map map) { + Map newMap = newHashMap(); + newMap.putAll(map); + return newMap; + } + + /** + * 创建一个空的LinkedHashMap实例。 + * + * @param Map键的类型 + * @param Map值的类型 + * @return 新创建的空LinkedHashMap实例 + */ + public static Map newLinkedHashMap() { + return new LinkedHashMap<>(); + } + + /** + * 创建一个指定初始容量的空LinkedHashMap实例。 + * + * @param size 初始容量大小 + * @param Map键的类型 + * @param Map值的类型 + * @return 指定初始容量的空LinkedHashMap实例 + */ + public static Map newLinkedHashMap(int size) { + return new LinkedHashMap<>(size); + } + + /** + * 根据已有Map创建一个新的LinkedHashMap实例。 + * + * @param map 要复制的Map + * @param Map键的类型 + * @param Map值的类型 + * @return 包含原Map所有元素的新LinkedHashMap实例 + */ + public static Map newLinkedHashMap(Map map) { + Map newMap = newLinkedHashMap(); + newMap.putAll(map); + return newMap; + } + + /** + * 创建一个空的TreeMap实例。 + * + * @param Map键的类型,必须实现Comparable接口 + * @param Map值的类型 + * @return 新创建的空TreeMap实例 + */ + public static , V> Map newTreeMap() { + return new TreeMap<>(); + } + + /** + * 根据已有Map创建一个新的TreeMap实例。 + * + * @param map 要复制的Map + * @param Map键的类型,必须实现Comparable接口 + * @param Map值的类型 + * @return 包含原Map所有元素的新TreeMap实例 + */ + public static , V> Map newTreeMap( + Map map + ) { + Map newMap = newTreeMap(); + newMap.putAll(map); + return newMap; + } + + /** + * 创建一个空的Hashtable实例。 + * + * @param Map键的类型 + * @param Map值的类型 + * @return 新创建的空Hashtable实例 + */ + public static Map newHashtable() { + return new Hashtable<>(); + } + + /** + * 创建一个指定初始容量的空Hashtable实例。 + * + * @param size 初始容量大小 + * @param Map键的类型 + * @param Map值的类型 + * @return 指定初始容量的空Hashtable实例 + */ + public static Map newHashtable(int size) { + return new Hashtable<>(size); + } + + /** + * 根据已有Map创建一个新的Hashtable实例。 + * + * @param map 要复制的Map + * @param Map键的类型 + * @param Map值的类型 + * @return 包含原Map所有元素的新Hashtable实例 + */ + public static Map newHashtable(Map map) { + Map newMap = newHashtable(); + newMap.putAll(map); + return newMap; + } + + /** + * 创建一个空的ConcurrentHashMap实例。 + * + * @param Map键的类型 + * @param Map值的类型 + * @return 新创建的空ConcurrentHashMap实例 + */ + public static Map newConcurrentHashMap() { + return new ConcurrentHashMap<>(); + } + + /** + * 创建一个指定初始容量的空ConcurrentHashMap实例。 + * + * @param size 初始容量大小 + * @param Map键的类型 + * @param Map值的类型 + * @return 指定初始容量的空ConcurrentHashMap实例 + */ + public static Map newConcurrentHashMap(int size) { + return new ConcurrentHashMap<>(size); + } + + /** + * 根据已有Map创建一个新的ConcurrentHashMap实例。 + * + * @param map 要复制的Map + * @param Map键的类型 + * @param Map值的类型 + * @return 包含原Map所有元素的新ConcurrentHashMap实例 + */ + public static Map newConcurrentHashMap(Map map) { + Map newMap = newConcurrentHashMap(); + newMap.putAll(map); + return newMap; + } +} diff --git a/src/com/mingliqiye/utils/collection/Sets.java b/src/com/mingliqiye/utils/collection/Sets.java new file mode 100644 index 0000000..6a457af --- /dev/null +++ b/src/com/mingliqiye/utils/collection/Sets.java @@ -0,0 +1,145 @@ +package com.mingliqiye.utils.collection; + +import java.util.*; + +/** + * Sets工具类提供了一系列创建Set实现的便捷方法。 + * + * @author MingLiPro + */ +public class Sets { + + /** + * 创建一个空的HashSet实例。 + * + * @param 集合元素的类型 + * @return 新创建的空HashSet实例 + */ + public static Set newHashSet() { + return new HashSet<>(); + } + + /** + * 根据可变参数创建一个包含指定元素的HashSet实例。 + * + * @param ts 要添加到集合中的元素,可以为0个或多个 + * @param 集合元素的类型 + * @return 包含指定元素的新HashSet实例 + */ + public static Set newHashSet(T... ts) { + Set set = newHashSet(); + set.addAll(Arrays.asList(ts)); + return set; + } + + /** + * 根据已有集合创建一个新的HashSet实例。 + * + * @param set 要复制的集合 + * @param 集合元素的类型 + * @return 包含原集合所有元素的新HashSet实例 + */ + public static Set newHashSet(Set set) { + Set newSet = newHashSet(); + newSet.addAll(set); + return newSet; + } + + /** + * 根据可迭代对象创建一个HashSet实例。 + * + * @param iterable 可迭代对象 + * @param 集合元素的类型 + * @return 包含可迭代对象中所有元素的新HashSet实例 + */ + public static Set newHashSet(Iterable iterable) { + Set set = newHashSet(); + for (T t : iterable) { + set.add(t); + } + return set; + } + + /** + * 创建一个指定初始容量的空HashSet实例。 + * + * @param size 初始容量大小 + * @param 集合元素的类型 + * @return 指定初始容量的空HashSet实例 + */ + public static Set newHashSet(int size) { + return new HashSet<>(size); + } + + /** + * 创建一个空的LinkedHashSet实例。 + * + * @param 集合元素的类型 + * @return 新创建的空LinkedHashSet实例 + */ + public static Set newLinkedHashSet() { + return new LinkedHashSet<>(); + } + + /** + * 根据可变参数创建一个包含指定元素的LinkedHashSet实例。 + * + * @param ts 要添加到集合中的元素,可以为0个或多个 + * @param 集合元素的类型 + * @return 包含指定元素的新LinkedHashSet实例 + */ + public static Set newLinkedHashSet(T... ts) { + Set set = newLinkedHashSet(); + set.addAll(Arrays.asList(ts)); + return set; + } + + /** + * 根据已有集合创建一个新的LinkedHashSet实例。 + * + * @param set 要复制的集合 + * @param 集合元素的类型 + * @return 包含原集合所有元素的新LinkedHashSet实例 + */ + public static Set newLinkedHashSet(Set set) { + Set newSet = newLinkedHashSet(); + newSet.addAll(set); + return newSet; + } + + /** + * 创建一个空的TreeSet实例。 + * + * @param 集合元素的类型,必须实现Comparable接口 + * @return 新创建的空TreeSet实例 + */ + public static > Set newTreeSet() { + return new TreeSet<>(); + } + + /** + * 根据可变参数创建一个包含指定元素的TreeSet实例。 + * + * @param ts 要添加到集合中的元素,可以为0个或多个 + * @param 集合元素的类型,必须实现Comparable接口 + * @return 包含指定元素的新TreeSet实例 + */ + public static > Set newTreeSet(T... ts) { + Set set = newTreeSet(); + set.addAll(Arrays.asList(ts)); + return set; + } + + /** + * 根据已有集合创建一个新的TreeSet实例。 + * + * @param set 要复制的集合 + * @param 集合元素的类型,必须实现Comparable接口 + * @return 包含原集合所有元素的新TreeSet实例 + */ + public static > Set newTreeSet(Set set) { + Set newSet = newTreeSet(); + newSet.addAll(set); + return newSet; + } +} diff --git a/src/com/mingliqiye/utils/concurrent/IsChanged.java b/src/com/mingliqiye/utils/concurrent/IsChanged.java new file mode 100644 index 0000000..4ed7fa4 --- /dev/null +++ b/src/com/mingliqiye/utils/concurrent/IsChanged.java @@ -0,0 +1,84 @@ +package com.mingliqiye.utils.concurrent; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +/** + * IsChanged 类提供了一个线程安全的包装器,用于检测值是否发生变化。 + * 它基于 AtomicReference 实现,适用于需要监控数据变更的并发场景。 + * + * @param 泛型类型,表示被包装的数据类型 + * @author MingLiPro + */ +public class IsChanged { + + /** + * 使用 AtomicReference 来保证对数据的原子操作 + */ + private final AtomicReference atomicReferenceData; + + /** + * 默认构造函数,初始化数据为 null + */ + public IsChanged() { + this(null); + } + + /** + * 带参数的构造函数,使用指定的初始值初始化 + * + * @param data 初始数据值 + */ + public IsChanged(T data) { + atomicReferenceData = new AtomicReference<>(data); + } + + /** + * 设置新的数据值,不检查是否发生变化 + * + * @param data 要设置的新数据值 + */ + public void set(T data) { + atomicReferenceData.set(data); + } + + /** + * 获取当前数据值 + * + * @return 当前数据值 + */ + public T get() { + return atomicReferenceData.get(); + } + + /** + * 设置新的数据值并返回旧的数据值 + * + * @param data 要设置的新数据值 + * @return 设置前的旧数据值 + */ + public T setAndGet(T data) { + return atomicReferenceData.getAndSet(data); + } + + /** + * 设置新的数据值,如果新值与当前值不同则更新并返回 true,否则返回 false + * 使用 CAS(Compare-And-Swap) 操作确保线程安全 + * + * @param data 要设置的新数据值 + * @return 如果值发生变化返回 true,否则返回 false + */ + public boolean setAndChanged(T data) { + T currentData; + do { + currentData = get(); + // 如果新值与当前值相等,则认为没有变化,直接返回 false + if (Objects.equals(data, currentData)) { + return false; + } + // 使用 CAS 操作尝试更新值,如果失败则重试 + } while (!atomicReferenceData.compareAndSet(currentData, data)); + // 成功更新值,返回 true 表示发生了变化 + return true; + } +} diff --git a/src/com/mingliqiye/utils/data/ThreadLocalDataHolder.java b/src/com/mingliqiye/utils/data/ThreadLocalDataHolder.java new file mode 100644 index 0000000..99014a5 --- /dev/null +++ b/src/com/mingliqiye/utils/data/ThreadLocalDataHolder.java @@ -0,0 +1,83 @@ +package com.mingliqiye.utils.data; + +/** + * 泛型线程局部变量持有器 + *

+ * 封装了 ThreadLocal 的常用操作,提供更便捷的 API 来管理线程本地变量。 + * + * @param 存储的数据类型 + * @author MingLiPro + */ +public class ThreadLocalDataHolder { + + private final ThreadLocal threadLocal; + + /** + * 构造函数,初始化 ThreadLocal 实例 + */ + public ThreadLocalDataHolder() { + this.threadLocal = new ThreadLocal<>(); + } + + /** + * 获取当前线程存储的值 + * + * @return 当前线程存储的值,如果没有则返回null + */ + public T get() { + return threadLocal.get(); + } + + /** + * 设置当前线程的值 + * + * @param value 要存储的值 + */ + public void set(T value) { + threadLocal.set(value); + } + + /** + * 移除当前线程存储的值 + *

+ * 防止内存泄漏,使用完毕后应调用此方法清理资源。 + */ + public void remove() { + threadLocal.remove(); + } + + /** + * 获取当前线程存储的值,如果不存在则返回默认值 + * + * @param defaultValue 默认值 + * @return 当前线程存储的值或默认值 + */ + public T getOrDefault(T defaultValue) { + T value = threadLocal.get(); + return value != null ? value : defaultValue; + } + + /** + * 安全获取值(避免NPE) + *

+ * 在某些异常情况下防止抛出异常,直接返回 null。 + * + * @return 值或null + */ + public T safeGet() { + try { + return threadLocal.get(); + } catch (Exception e) { + return null; + } + } + + /** + * 检查当前线程是否有值 + * + * @return 是否有值 + */ + public boolean isPresent() { + return threadLocal.get() != null; + } +} diff --git a/src/com/mingliqiye/utils/file/FileUtil.java b/src/com/mingliqiye/utils/file/FileUtil.java new file mode 100644 index 0000000..7c3f52d --- /dev/null +++ b/src/com/mingliqiye/utils/file/FileUtil.java @@ -0,0 +1,344 @@ +package com.mingliqiye.utils.file; + +import com.mingliqiye.utils.string.StringUtil; +import lombok.Getter; +import lombok.Setter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; + +/** + * 文件工具类,提供常用的文件操作方法 + * + * @author MingLiPro + */ +public class FileUtil { + + /** + * 默认字符集 + */ + @Getter + @Setter + private static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + /** + * 读取文件内容为字符串 + * + * @param filePath 文件路径 + * @return 文件内容字符串 + * @throws IOException 读取文件时发生错误 + */ + public static String readFileToString(String filePath) throws IOException { + return readFileToString(filePath, DEFAULT_CHARSET); + } + + /** + * 读取文件内容为字符串 + * + * @param filePath 文件路径 + * @param charset 字符集 + * @return 文件内容字符串 + * @throws IOException 读取文件时发生错误 + */ + public static String readFileToString(String filePath, Charset charset) + throws IOException { + Path path = Paths.get(filePath); + byte[] bytes = Files.readAllBytes(path); + return new String(bytes, charset); + } + + /** + * 将字符串写入文件 + * + * @param filePath 文件路径 + * @param content 要写入的内容 + * @throws IOException 写入文件时发生错误 + */ + public static void writeStringToFile(String filePath, String content) + throws IOException { + writeStringToFile(filePath, content, DEFAULT_CHARSET); + } + + /** + * 将字符串写入文件 + * + * @param filePath 文件路径 + * @param content 要写入的内容 + * @param charset 字符集 + * @throws IOException 写入文件时发生错误 + */ + public static void writeStringToFile( + String filePath, + String content, + Charset charset + ) throws IOException { + Path path = Paths.get(filePath); + Files.createDirectories(path.getParent()); + Files.write(path, content.getBytes(charset)); + } + + /** + * 读取文件内容为字符串列表(按行分割) + * + * @param filePath 文件路径 + * @return 文件内容按行分割的字符串列表 + * @throws IOException 读取文件时发生错误 + */ + public static List readLines(String filePath) throws IOException { + return readLines(filePath, DEFAULT_CHARSET); + } + + /** + * 读取文件内容为字符串列表(按行分割) + * + * @param filePath 文件路径 + * @param charset 字符集 + * @return 文件内容按行分割的字符串列表 + * @throws IOException 读取文件时发生错误 + */ + public static List readLines(String filePath, Charset charset) + throws IOException { + Path path = Paths.get(filePath); + return Files.readAllLines(path, charset); + } + + /** + * 将字符串列表写入文件(每行一个元素) + * + * @param filePath 文件路径 + * @param lines 要写入的行内容列表 + * @throws IOException 写入文件时发生错误 + */ + public static void writeLines(String filePath, List lines) + throws IOException { + writeLines(filePath, lines, DEFAULT_CHARSET); + } + + /** + * 将字符串列表写入文件(每行一个元素) + * + * @param filePath 文件路径 + * @param lines 要写入的行内容列表 + * @param charset 字符集 + * @throws IOException 写入文件时发生错误 + */ + public static void writeLines( + String filePath, + List lines, + Charset charset + ) throws IOException { + Path path = Paths.get(filePath); + Files.createDirectories(path.getParent()); + Files.write(path, lines, charset); + } + + /** + * 复制文件 + * + * @param sourcePath 源文件路径 + * @param targetPath 目标文件路径 + * @throws IOException 复制文件时发生错误 + */ + public static void copyFile(String sourcePath, String targetPath) + throws IOException { + Path source = Paths.get(sourcePath); + Path target = Paths.get(targetPath); + Files.createDirectories(target.getParent()); + Files.copy(source, target); + } + + /** + * 删除文件 + * + * @param filePath 文件路径 + * @return 如果文件删除成功返回true,否则返回false + */ + public static boolean deleteFile(String filePath) { + try { + Path path = Paths.get(filePath); + return Files.deleteIfExists(path); + } catch (IOException e) { + return false; + } + } + + /** + * 检查文件是否存在 + * + * @param filePath 文件路径 + * @return 如果文件存在返回true,否则返回false + */ + public static boolean exists(String filePath) { + Path path = Paths.get(filePath); + return Files.exists(path); + } + + /** + * 获取文件大小 + * + * @param filePath 文件路径 + * @return 文件大小(字节),如果文件不存在返回-1 + */ + public static long getFileSize(String filePath) { + try { + Path path = Paths.get(filePath); + return Files.size(path); + } catch (IOException e) { + return -1; + } + } + + /** + * 创建目录 + * + * @param dirPath 目录路径 + * @return 如果目录创建成功返回true,否则返回false + */ + public static boolean createDirectory(String dirPath) { + try { + Path path = Paths.get(dirPath); + Files.createDirectories(path); + return true; + } catch (IOException e) { + return false; + } + } + + /** + * 获取文件扩展名 + * + * @param fileName 文件名 + * @return 文件扩展名(不包含点号),如果无扩展名返回空字符串 + */ + public static String getFileExtension(String fileName) { + if (StringUtil.isEmpty(fileName)) { + return ""; + } + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1) { + return ""; + } + return fileName.substring(lastDotIndex + 1); + } + + /** + * 获取不带扩展名的文件名 + * + * @param fileName 文件名 + * @return 不带扩展名的文件名 + */ + public static String getFileNameWithoutExtension(String fileName) { + if (StringUtil.isEmpty(fileName)) { + return ""; + } + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex == -1) { + return fileName; + } + return fileName.substring(0, lastDotIndex); + } + + /** + * 读取文件内容为字节数组 + * + * @param filePath 文件路径 + * @return 文件内容的字节数组 + * @throws IOException 读取文件时发生错误 + */ + public static byte[] readFileToByteArray(String filePath) + throws IOException { + Path path = Paths.get(filePath); + return Files.readAllBytes(path); + } + + /** + * 将字节数组写入文件 + * + * @param filePath 文件路径 + * @param data 要写入的字节数据 + * @throws IOException 写入文件时发生错误 + */ + public static void writeByteArrayToFile(String filePath, byte[] data) + throws IOException { + Path path = Paths.get(filePath); + Files.createDirectories(path.getParent()); + Files.write(path, data); + } + + /** + * 将字节数组追加到文件末尾 + * + * @param filePath 文件路径 + * @param data 要追加的字节数据 + * @throws IOException 追加数据时发生错误 + */ + public static void appendByteArrayToFile(String filePath, byte[] data) + throws IOException { + Path path = Paths.get(filePath); + Files.createDirectories(path.getParent()); + Files.write( + path, + data, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND + ); + } + + /** + * 分块读取大文件为字节数组列表 + * + * @param filePath 文件路径 + * @param chunkSize 每块大小(字节) + * @return 文件内容按指定大小分割的字节数组列表 + * @throws IOException 读取文件时发生错误 + */ + public static List readFileToByteArrayChunks( + String filePath, + int chunkSize + ) throws IOException { + List chunks = new ArrayList<>(); + Path path = Paths.get(filePath); + + try (InputStream inputStream = Files.newInputStream(path)) { + byte[] buffer = new byte[chunkSize]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + byte[] chunk = new byte[bytesRead]; + System.arraycopy(buffer, 0, chunk, 0, bytesRead); + chunks.add(chunk); + } + } + + return chunks; + } + + /** + * 将字节数组列表写入文件 + * + * @param filePath 文件路径 + * @param chunks 字节数组列表 + * @throws IOException 写入文件时发生错误 + */ + public static void writeByteArrayChunksToFile( + String filePath, + List chunks + ) throws IOException { + Path path = Paths.get(filePath); + Files.createDirectories(path.getParent()); + + try (OutputStream outputStream = Files.newOutputStream(path)) { + for (byte[] chunk : chunks) { + outputStream.write(chunk); + } + } + } +} diff --git a/src/com/mingliqiye/utils/functions/Debouncer.java b/src/com/mingliqiye/utils/functions/Debouncer.java new file mode 100644 index 0000000..52f631b --- /dev/null +++ b/src/com/mingliqiye/utils/functions/Debouncer.java @@ -0,0 +1,71 @@ +package com.mingliqiye.utils.functions; + +import java.util.concurrent.*; + +/** + * 防抖器类,用于实现防抖功能,防止在短时间内重复执行相同任务 + * + * @author MingLiPro + */ +public class Debouncer { + + private final ScheduledExecutorService scheduler = + Executors.newSingleThreadScheduledExecutor(); + private final ConcurrentHashMap> delayedMap = + new ConcurrentHashMap<>(); + private final long delay; + + /** + * 构造函数,创建一个防抖器实例 + * + * @param delay 延迟时间 + * @param unit 时间单位 + */ + public Debouncer(long delay, TimeUnit unit) { + this.delay = unit.toMillis(delay); + } + + /** + * 执行防抖操作,如果在指定延迟时间内再次调用相同key的任务,则取消之前的任务并重新计时 + * + * @param key 任务的唯一标识符,用于区分不同任务 + * @param task 要执行的任务 + */ + public void debounce(final Object key, final Runnable task) { + // 提交新任务并获取之前可能存在的任务 + final Future prev = delayedMap.put( + key, + scheduler.schedule( + () -> { + try { + task.run(); + } finally { + // 任务执行完成后从映射中移除 + delayedMap.remove(key); + } + }, + delay, + TimeUnit.MILLISECONDS + ) + ); + + // 如果之前存在任务,则取消它 + if (prev != null) { + prev.cancel(true); + } + } + + /** + * 关闭防抖器,取消所有待执行的任务并关闭调度器 + */ + public void shutdown() { + // 先取消所有延迟任务 + for (Future future : delayedMap.values()) { + future.cancel(true); + } + delayedMap.clear(); + + // 再关闭调度器 + scheduler.shutdownNow(); + } +} diff --git a/src/com/mingliqiye/utils/hash/HashUtils.java b/src/com/mingliqiye/utils/hash/HashUtils.java new file mode 100644 index 0000000..e40755d --- /dev/null +++ b/src/com/mingliqiye/utils/hash/HashUtils.java @@ -0,0 +1,93 @@ +package com.mingliqiye.utils.hash; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.mindrot.jbcrypt.BCrypt; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Security; + +/** + * 提供常用的哈希计算工具方法,包括文件哈希值计算、BCrypt 加密等。 + * + * @author MingLiPro + */ +public class HashUtils { + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * 计算指定文件的哈希值。 + * + * @param file 要计算哈希值的文件对象 + * @param algorithm 使用的哈希算法名称(如 SHA-256、MD5 等) + * @return 文件的十六进制格式哈希值字符串 + * @throws IOException 当文件不存在或读取过程中发生 I/O 错误时抛出 + * @throws NoSuchAlgorithmException 当指定的哈希算法不可用时抛出 + */ + public static String calculateFileHash(File file, String algorithm) + throws IOException, NoSuchAlgorithmException { + // 检查文件是否存在 + if (!file.exists()) { + throw new IOException("File not found: " + file.getAbsolutePath()); + } + + MessageDigest digest = MessageDigest.getInstance(algorithm); + + try (FileInputStream fis = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; // 8KB 缓冲区 + int bytesRead; + + // 分块读取文件内容并更新摘要 + while ((bytesRead = fis.read(buffer)) != -1) { + digest.update(buffer, 0, bytesRead); + } + } + + return bytesToHex(digest.digest()); + } + + /** + * 将字节数组转换为十六进制字符串表示。 + * + * @param bytes 输入的字节数组 + * @return 对应的十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(2 * bytes.length); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + /** + * 使用 BCrypt 算法对字符串进行加密。 + * + * @param string 需要加密的明文字符串 + * @return 加密后的 BCrypt 哈希字符串 + */ + public static String bcrypt(String string) { + return BCrypt.hashpw(string, BCrypt.gensalt()); + } + + /** + * 验证给定字符串与 BCrypt 哈希是否匹配。 + * + * @param string 明文字符串 + * @param bcrypted 已经使用 BCrypt 加密的哈希字符串 + * @return 如果匹配返回 true,否则返回 false + */ + public static boolean checkBcrypt(String string, String bcrypted) { + return BCrypt.checkpw(string, bcrypted); + } +} diff --git a/src/com/mingliqiye/utils/http/Response.java b/src/com/mingliqiye/utils/http/Response.java new file mode 100644 index 0000000..d93aca5 --- /dev/null +++ b/src/com/mingliqiye/utils/http/Response.java @@ -0,0 +1,47 @@ +package com.mingliqiye.utils.http; + +import com.mingliqiye.utils.time.DateTime; +import com.mingliqiye.utils.time.Formatter; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@ToString +@EqualsAndHashCode +@Getter +public class Response { + + private final String time = DateTime.now().format( + Formatter.STANDARD_DATETIME_MILLISECOUND7 + ); + private String message; + private T data; + private int statusCode; + + public Response(String message, T data, int statusCode) { + this.message = message; + this.data = data; + this.statusCode = statusCode; + } + + public static Response ok(T data) { + return new Response<>("操作成功", data, 200); + } + + public Response setMessage(String message) { + this.message = message; + return this; + } + + public Response setData(T data) { + this.data = data; + return Response.ok(getData()) + .setMessage(getMessage()) + .setStatusCode(getStatusCode()); + } + + public Response setStatusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } +} diff --git a/src/com/mingliqiye/utils/minecraft/slp/Description.java b/src/com/mingliqiye/utils/minecraft/slp/Description.java new file mode 100644 index 0000000..a9a5489 --- /dev/null +++ b/src/com/mingliqiye/utils/minecraft/slp/Description.java @@ -0,0 +1,10 @@ +package com.mingliqiye.utils.minecraft.slp; + +import lombok.Data; + +@Data +public class Description { + + private String text; + private Extra[] extra; +} diff --git a/src/com/mingliqiye/utils/minecraft/slp/Extra.java b/src/com/mingliqiye/utils/minecraft/slp/Extra.java new file mode 100644 index 0000000..ff78b0f --- /dev/null +++ b/src/com/mingliqiye/utils/minecraft/slp/Extra.java @@ -0,0 +1,12 @@ +package com.mingliqiye.utils.minecraft.slp; + +import lombok.Data; + +@Data +public class Extra { + + private String text; + private String color; + private Boolean bold; + private Boolean italic; +} diff --git a/src/com/mingliqiye/utils/minecraft/slp/MinecraftServerStatus.java b/src/com/mingliqiye/utils/minecraft/slp/MinecraftServerStatus.java new file mode 100644 index 0000000..dbfb85b --- /dev/null +++ b/src/com/mingliqiye/utils/minecraft/slp/MinecraftServerStatus.java @@ -0,0 +1,15 @@ +package com.mingliqiye.utils.minecraft.slp; + +import lombok.Data; + +@Data +public class MinecraftServerStatus { + + private Description description; + private Players players; + private Version version; + private String favicon; + private boolean enforcesSecureChat; + private boolean previewsChat; + private String jsonData; +} diff --git a/src/com/mingliqiye/utils/minecraft/slp/PlayerSample.java b/src/com/mingliqiye/utils/minecraft/slp/PlayerSample.java new file mode 100644 index 0000000..8585b91 --- /dev/null +++ b/src/com/mingliqiye/utils/minecraft/slp/PlayerSample.java @@ -0,0 +1,10 @@ +package com.mingliqiye.utils.minecraft.slp; + +import lombok.Data; + +@Data +public class PlayerSample { + + private String name; + private String id; +} diff --git a/src/com/mingliqiye/utils/minecraft/slp/Players.java b/src/com/mingliqiye/utils/minecraft/slp/Players.java new file mode 100644 index 0000000..41a33d4 --- /dev/null +++ b/src/com/mingliqiye/utils/minecraft/slp/Players.java @@ -0,0 +1,11 @@ +package com.mingliqiye.utils.minecraft.slp; + +import lombok.Data; + +@Data +public class Players { + + private int max; + private int online; + private PlayerSample[] sample; +} diff --git a/src/com/mingliqiye/utils/minecraft/slp/SLP.java b/src/com/mingliqiye/utils/minecraft/slp/SLP.java new file mode 100644 index 0000000..1202160 --- /dev/null +++ b/src/com/mingliqiye/utils/minecraft/slp/SLP.java @@ -0,0 +1,198 @@ +package com.mingliqiye.utils.minecraft.slp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mingliqiye.utils.network.NetworkEndpoint; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Minecraft 服务器列表协议(Server List Ping, SLP)工具类。 + * 提供了与 Minecraft 服务器通信以获取其状态信息的功能。 + */ +public class SLP { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 将 int32 值截断为无符号 short(2 字节)并按大端序写入字节数组。 + * + * @param value 需要转换的整数(int32) + * @return 包含两个字节的数组,表示无符号 short + */ + public static byte[] toUnsignedShort(int value) { + byte[] array = new byte[2]; + ByteBuffer.wrap(array, 0, 2) + .order(ByteOrder.BIG_ENDIAN) + .putShort((short) (value & 0xFFFF)); + return array; + } + + /** + * 构造 Minecraft 握手包数据。 + * 握手包用于初始化客户端与服务器之间的连接。 + * + * @param serverIP 服务器 IP 地址或域名 + * @param serverPort 服务器端口号 + * @param type 连接类型(通常为 1 表示获取状态) + * @return 握手包的完整字节数组 + * @throws IOException 如果构造过程中发生 IO 错误 + */ + public static byte[] getHandshakePack( + String serverIP, + int serverPort, + int type + ) throws IOException { + ByteArrayOutputStream pack = new ByteArrayOutputStream(); + ByteArrayOutputStream byteArrayOutputStream = + new ByteArrayOutputStream(); + pack.write(0x00); // 握手包标识符 + pack.write(toVarInt(1156)); // 协议版本号(示例值) + byte[] sip = serverIP.getBytes(); + pack.write(toVarInt(sip.length)); // 服务器地址长度 + pack.write(sip); // 服务器地址 + pack.write(toUnsignedShort(serverPort)); // 服务器端口 + pack.write(toVarInt(type)); // 下一阶段类型(1 表示状态请求) + byteArrayOutputStream.write(toVarInt(pack.size())); // 包长度前缀 + byteArrayOutputStream.write(pack.toByteArray()); + + return byteArrayOutputStream.toByteArray(); + } + + /** + * 获取状态请求包的固定字节表示。 + * 此包用于向服务器请求当前状态信息。 + * + * @return 状态请求包的字节数组 + */ + public static byte[] getStatusPack() { + return new byte[] { 0x01, 0x00 }; + } + + /** + * 从输入流中读取服务器返回的状态 JSON 数据,并解析为 MinecraftServerStatus 实体对象。 + * + * @param inputStream 输入流,包含服务器响应的数据 + * @return 解析后的 MinecraftServerStatus 对象 + * @throws IOException 如果读取过程中发生 IO 错误 + */ + public static MinecraftServerStatus getStatusJsonEntity( + DataInputStream inputStream + ) throws IOException { + readVarInt(inputStream); // 忽略第一个 VarInt(包长度) + inputStream.readByte(); // 忽略包标识符 + int lengthjson = readVarInt(inputStream); // 读取 JSON 数据长度 + byte[] data = new byte[lengthjson]; + inputStream.readFully(data); // 读取完整的 JSON 数据 + MinecraftServerStatus serverStatus = objectMapper.readValue( + data, + MinecraftServerStatus.class + ); + serverStatus.setJsonData(new String(data)); // 设置原始 JSON 字符串 + return serverStatus; + } + + /** + * 从输入流中读取一个 VarInt 类型的整数(最多 5 个字节)。 + * + * @param in 输入流 + * @return 解码后的整数值 + * @throws IOException 如果读取过程中发生 IO 错误 + */ + public static int readVarInt(DataInputStream in) throws IOException { + int value = 0; + int length = 0; + byte currentByte; + do { + currentByte = in.readByte(); + value |= (currentByte & 0x7F) << (length * 7); + length += 1; + if (length > 5) { + throw new RuntimeException("VarInt too long"); + } + } while ((currentByte & 0x80) != 0); + return value; + } + + /** + * 将一个 int32 整数编码为 VarInt 格式的字节数组(1 到 5 个字节)。 + * + * @param value 需要编码的整数 + * @return 编码后的 VarInt 字节数组 + */ + public static byte[] toVarInt(int value) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + while (true) { + if ((value & 0xFFFFFF80) == 0) { + buffer.write(value); // 最后一个字节 + break; + } + buffer.write((value & 0x7F) | 0x80); // 写入带继续位的字节 + value >>>= 7; // 右移 7 位继续处理 + } + return buffer.toByteArray(); + } + + /** + * 创建一个新的 Socket 连接到指定的网络端点,并设置超时时间。 + * + * @param networkEndpoint 目标网络端点(包括主机和端口) + * @return 已连接的 Socket 实例 + * @throws IOException 如果连接失败或发生 IO 错误 + */ + public static Socket getNewConnect(NetworkEndpoint networkEndpoint) + throws IOException { + Socket socket = new Socket(); + socket.setSoTimeout(5000); // 设置读取超时时间为 5 秒 + socket.connect(networkEndpoint.toInetSocketAddress()); // 执行连接操作 + return socket; + } + + /** + * 使用 "host:port" 格式的字符串连接到 Minecraft 服务器并获取其状态信息。 + * + * @param s 域名或 IP 地址加端口号组成的字符串,例如 "127.0.0.1:25565" + * @return 服务器状态实体对象 + * @throws IOException 如果连接失败或发生 IO 错误 + */ + public static MinecraftServerStatus getServerStatus(String s) + throws IOException { + return getServerStatus(NetworkEndpoint.of(s)); + } + + /** + * 使用指定的主机名和端口号连接到 Minecraft 服务器并获取其状态信息。 + * + * @param s 主机名或 IP 地址 + * @param i 端口号 + * @return 服务器状态实体对象 + * @throws IOException 如果连接失败或发生 IO 错误 + */ + public static MinecraftServerStatus getServerStatus(String s, Integer i) + throws IOException { + return getServerStatus(NetworkEndpoint.of(s, i)); + } + + /** + * 使用 NetworkEndpoint 实例连接到 Minecraft 服务器并获取其状态信息。 + * + * @param e 网络端点实例,包含主机和端口信息 + * @return 服务器状态实体对象 + * @throws IOException 如果连接失败或发生 IO 错误 + * @see NetworkEndpoint + */ + public static MinecraftServerStatus getServerStatus(NetworkEndpoint e) + throws IOException { + Socket socket = getNewConnect(e); // 建立 TCP 连接 + OutputStream out = socket.getOutputStream(); // 获取输出流发送数据 + DataInputStream in = new DataInputStream(socket.getInputStream()); // 获取输入流接收数据 + out.write(getHandshakePack(e.getHost(), e.getPort(), 1)); // 发送握手包 + out.write(getStatusPack()); // 发送状态请求包 + return getStatusJsonEntity(in); // 读取并解析服务器响应 + } +} diff --git a/src/com/mingliqiye/utils/minecraft/slp/Version.java b/src/com/mingliqiye/utils/minecraft/slp/Version.java new file mode 100644 index 0000000..59c84bb --- /dev/null +++ b/src/com/mingliqiye/utils/minecraft/slp/Version.java @@ -0,0 +1,10 @@ +package com.mingliqiye.utils.minecraft.slp; + +import lombok.Data; + +@Data +public class Version { + + private String name; + private int protocol; +} diff --git a/src/com/mingliqiye/utils/network/NetWorkUtil.java b/src/com/mingliqiye/utils/network/NetWorkUtil.java new file mode 100644 index 0000000..811db93 --- /dev/null +++ b/src/com/mingliqiye/utils/network/NetWorkUtil.java @@ -0,0 +1,10 @@ +package com.mingliqiye.utils.network; + +public class NetWorkUtil { + + public static void main(String[] args) { + System.out.println(NetworkEndpoint.of("127.0.0.1", 25565)); + System.out.println(NetworkEndpoint.of("127.0.0.1:25565")); + System.out.println(NetworkEndpoint.of("127.0.0.1:25565")); + } +} diff --git a/src/com/mingliqiye/utils/network/NetworkAddress.java b/src/com/mingliqiye/utils/network/NetworkAddress.java new file mode 100644 index 0000000..c524704 --- /dev/null +++ b/src/com/mingliqiye/utils/network/NetworkAddress.java @@ -0,0 +1,196 @@ +package com.mingliqiye.utils.network; + +import com.mingliqiye.utils.string.StringUtil; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +/** + * 网络地址类,用于表示一个网络地址(IP或域名),并提供相关操作。 + * 支持IPv4和IPv6地址的解析与验证。 + * + * @author MingLiPro + */ +public class NetworkAddress implements Serializable { + + /** + * IPv6标识 + */ + public static int IPV6 = 6; + + /** + * IPv4标识 + */ + public static int IPV4 = 4; + + /** + * IPv4地址正则表达式 + */ + static String IPV4REG = + "^((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(\\.((2" + + "(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}$"; + + /** + * 编译后的IPv4地址匹配模式 + */ + private static final Pattern IPV4_PATTERN = Pattern.compile(IPV4REG); + + /** + * IPv6地址正则表达式 + */ + static String IPV6REG = + "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|" + + "^(::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})$" + + "|" + + "^(::)$|" + + "^([0-9a-fA-F]{1,4}::([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4})$|" + + "^(([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4})$|" + + "^(([0-9a-fA-F]{1,4}:){6}(([0-9]{1,3}\\.){3}[0-9]{1,3}))$|" + + "^::([fF]{4}:)?(([0-9]{1,3}\\.){3}[0-9]{1,3})$"; + + /** + * 编译后的IPv6地址匹配模式 + */ + private static final Pattern IPV6_PATTERN = Pattern.compile(IPV6REG); + + /** + * IP地址类型:4 表示 IPv4,6 表示 IPv6 + */ + @Getter + private int IPv; + + /** + * IP地址字符串 + */ + @Getter + private String ip; + + /** + * 域名(如果输入的是域名) + */ + private String domain; + + /** + * 标识是否是域名解析来的IP + */ + private boolean isdom; + + /** + * 构造方法,根据传入的字符串判断是IP地址还是域名,并进行相应处理。 + * + * @param domip 可能是IP地址或域名的字符串 + */ + NetworkAddress(String domip) { + try { + // 尝试将输入识别为IP地址 + IPv = testIp(domip); + ip = domip; + } catch (NetworkException e) { + try { + // 如果不是有效IP,则尝试作为域名解析 + String ips = getHostIp(domip); + IPv = testIp(ips); + ip = ips; + isdom = true; + domain = domip; + } catch (UnknownHostException ex) { + throw new NetworkException(ex); + } + } + } + + /** + * 静态工厂方法,创建 NetworkAddress 实例。 + * + * @param domip 可能是IP地址或域名的字符串 + * @return 新建的 NetworkAddress 实例 + */ + public static NetworkAddress of(String domip) { + return new NetworkAddress(domip); + } + + /** + * 静态工厂方法,通过 InetAddress 创建 NetworkAddress 实例。 + * + * @param inetAddress InetAddress 对象 + * @return 新建的 NetworkAddress 实例 + */ + public static NetworkAddress of(InetAddress inetAddress) { + return new NetworkAddress(inetAddress.getHostAddress()); + } + + /** + * 从DNS服务器解析域名获取对应的IP地址。 + * + * @param domain 域名 + * @return 解析出的第一个IP地址 + * @throws UnknownHostException 如果域名无法解析 + */ + public static String getHostIp(@NotNull String domain) + throws UnknownHostException { + InetAddress[] addresses = InetAddress.getAllByName(domain.trim()); + return addresses[0].getHostAddress(); + } + + /** + * 检测给定字符串是否为有效的IPv4或IPv6地址。 + * + * @param ip 要检测的IP地址字符串 + * @return 4 表示IPv4,6 表示IPv6 + * @throws NetworkException 如果IP格式无效 + */ + public static int testIp(String ip) { + if (ip == null) { + throw new NetworkException("IP地址不能为null"); + } + String trimmedIp = ip.trim(); + + // 判断是否匹配IPv4格式 + if (IPV4_PATTERN.matcher(trimmedIp).matches()) { + return IPV4; + } + + // 判断是否匹配IPv6格式 + if (IPV6_PATTERN.matcher(trimmedIp).matches()) { + return IPV6; + } + + // 不符合任一格式时抛出异常 + throw new NetworkException( + StringUtil.format("[{}] 不是有效的IPv4或IPv6地址", ip) + ); + } + + /** + * 将当前 NetworkAddress 转换为 InetAddress 对象。 + * + * @return InetAddress 对象 + */ + public InetAddress toInetAddress() { + try { + return InetAddress.getByName(ip != null ? ip : domain); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + /** + * 返回 NetworkAddress 的字符串表示形式。 + * + * @return 字符串表示 + */ + public String toString() { + return isdom + ? StringUtil.format( + "NetworkAddress(IP='{}',type='{}'," + "domain='{}')", + ip, + IPv, + domain + ) + : StringUtil.format("NetworkAddress(IP='{}',type='{}')", ip, IPv); + } +} diff --git a/src/com/mingliqiye/utils/network/NetworkEndpoint.java b/src/com/mingliqiye/utils/network/NetworkEndpoint.java new file mode 100644 index 0000000..028a810 --- /dev/null +++ b/src/com/mingliqiye/utils/network/NetworkEndpoint.java @@ -0,0 +1,142 @@ +package com.mingliqiye.utils.network; + +import com.mingliqiye.utils.string.StringUtil; +import lombok.Getter; + +import java.io.Serializable; +import java.net.InetSocketAddress; + +/** + * IP和端口聚集类,用于封装网络地址与端口信息。 + * 该类提供了与InetSocketAddress之间的相互转换功能。 + * + * @author MingLiPro + * @see InetSocketAddress + */ +public class NetworkEndpoint implements Serializable { + + @Getter + private final NetworkAddress networkAddress; + + @Getter + private final NetworkPort networkPort; + + /** + * 构造函数,使用指定的网络地址和端口创建NetworkEndpoint实例。 + * + * @param networkAddress 网络地址对象 + * @param networkPort 网络端口对象 + * @see NetworkAddress + * @see NetworkPort + */ + private NetworkEndpoint( + NetworkAddress networkAddress, + NetworkPort networkPort + ) { + this.networkAddress = networkAddress; + this.networkPort = networkPort; + } + + /** + * 根据给定的InetSocketAddress对象创建NetworkEndpoint实例。 + * + * @param address InetSocketAddress对象 + * @return 新建的NetworkEndpoint实例 + * @see InetSocketAddress + */ + public static NetworkEndpoint of(InetSocketAddress address) { + return new NetworkEndpoint( + new NetworkAddress(address.getHostString()), + new NetworkPort(address.getPort()) + ); + } + + /** + * 根据主机名或IP字符串和端口号创建NetworkEndpoint实例。 + * + * @param s 主机名或IP地址字符串 + * @param i 端口号 + * @return 新建的NetworkEndpoint实例 + */ + public static NetworkEndpoint of(String s, Integer i) { + NetworkAddress networkAddress = new NetworkAddress(s); + NetworkPort networkPort = new NetworkPort(i); + return new NetworkEndpoint(networkAddress, networkPort); + } + + /** + * 根据"host:port"格式的字符串创建NetworkEndpoint实例。 + * 例如:"127.0.0.1:8080" + * + * @param s "host:port"格式的字符串 + * @return 新建的NetworkEndpoint实例 + */ + public static NetworkEndpoint of(String s) { + // 查找最后一个冒号的位置,以支持IPv6地址中的冒号 + int lastColonIndex = s.lastIndexOf(':'); + return of( + s.substring(0, lastColonIndex), + Integer.parseInt(s.substring(lastColonIndex + 1)) + ); + } + + /** + * 将当前NetworkEndpoint转换为InetSocketAddress对象。 + * + * @return 对应的InetSocketAddress对象 + * @see InetSocketAddress + */ + public InetSocketAddress toInetSocketAddress() { + return new InetSocketAddress( + networkAddress.toInetAddress(), + networkPort.getPort() + ); + } + + /** + * 将当前NetworkEndpoint转换为"host:port"格式的字符串。 + * 例如:"127.0.0.1:25563" + * + * @return 格式化后的字符串 + */ + public String toHostPortString() { + return StringUtil.format( + "{}:{}", + networkAddress.getIp(), + networkPort.getPort() + ); + } + + /** + * 返回NetworkEndpoint的详细字符串表示形式。 + * 格式:NetworkEndpoint(IP=...,Port=...,Endpoint=...) + * + * @return 包含详细信息的字符串 + */ + public String toString() { + return StringUtil.format( + "NetworkEndpoint(IP={},Port={},Endpoint={})", + networkAddress.getIp(), + networkPort.getPort(), + toHostPortString() + ); + } + + /** + * 获取主机名或IP地址字符串。 + * + * @return 主机名或IP地址 + */ + public String getHost() { + return networkAddress.getIp(); + } + + /** + * 获取端口号。 + * + * @return 端口号 + */ + public Integer getPort() { + return networkPort.getPort(); + } +} diff --git a/src/com/mingliqiye/utils/network/NetworkException.java b/src/com/mingliqiye/utils/network/NetworkException.java new file mode 100644 index 0000000..597aea1 --- /dev/null +++ b/src/com/mingliqiye/utils/network/NetworkException.java @@ -0,0 +1,27 @@ +package com.mingliqiye.utils.network; + +/** + * 网络异常类,用于处理网络相关的运行时异常 + * + * @author MingLiPro + */ +public class NetworkException extends RuntimeException { + + /** + * 构造一个带有指定详细消息的网络异常 + * + * @param message 异常的详细消息 + */ + public NetworkException(String message) { + super(message); + } + + /** + * 构造一个网络异常,指定原因异常 + * + * @param e 导致此异常的原因异常 + */ + public NetworkException(Exception e) { + super(e); + } +} diff --git a/src/com/mingliqiye/utils/network/NetworkPort.java b/src/com/mingliqiye/utils/network/NetworkPort.java new file mode 100644 index 0000000..948bd26 --- /dev/null +++ b/src/com/mingliqiye/utils/network/NetworkPort.java @@ -0,0 +1,42 @@ +package com.mingliqiye.utils.network; + +import com.mingliqiye.utils.string.StringUtil; +import lombok.Getter; + +import java.io.Serializable; + +/** + * 网络端口类 + * + * @author MingLiPro + */ +public class NetworkPort implements Serializable { + + @Getter + private final int port; + + /** + * 构造函数,创建一个网络端口对象 + * + * @param port 端口号,必须在0-65535范围内 + */ + public NetworkPort(int port) { + testPort(port); + this.port = port; + } + + /** + * 验证端口号是否合法 + * + * @param port 待验证的端口号 + * @throws NetworkException 当端口号不在合法范围(0-65535)内时抛出异常 + */ + public static void testPort(int port) { + // 验证端口号范围是否在0-65535之间 + if (!(0 <= port && 65535 >= port)) { + throw new NetworkException( + StringUtil.format("{} 不是正确的端口号", port) + ); + } + } +} diff --git a/src/com/mingliqiye/utils/string/StringUtil.java b/src/com/mingliqiye/utils/string/StringUtil.java new file mode 100644 index 0000000..23c6cef --- /dev/null +++ b/src/com/mingliqiye/utils/string/StringUtil.java @@ -0,0 +1,214 @@ +package com.mingliqiye.utils.string; + +import com.mingliqiye.utils.collection.Lists; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 字符串工具类,提供常用的字符串处理方法 + */ +public class StringUtil { + + /** + * 将对象转换为字符串表示形式 + * + * @param obj 需要转换的对象,可以为null + * @return 如果对象为null则返回空字符串,否则返回对象的字符串表示 + */ + public static String toString(Object obj) { + // 如果对象为null,返回空字符串;否则调用对象的toString方法 + return obj == null ? "" : obj.toString(); + } + + /** + * 格式化字符串,将格式字符串中的占位符{}替换为对应的参数值
+ * 示例:输出 {} StringUtil.format("{},{},{}", "666", "{}", "777") - "666,{},777"
+ * 示例 StringUtil.format("{},{},{},{}", "666", "{}", "777") - "666,{},777,"
+ * 没有实际{} 会替换为 "" 空字符串 + * + * @param format 格式字符串,使用{}作为占位符,如果为null则返回null + * @param args 用于替换占位符的参数数组 + * @return 格式化后的字符串 + */ + public static String format(String format, Object... args) { + if (format == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + int placeholderCount = 0; + int lastIndex = 0; + int len = format.length(); + + for (int i = 0; i < len - 1; i++) { + if (format.charAt(i) == '{' && format.charAt(i + 1) == '}') { + // 添加前面的部分 + sb.append(format, lastIndex, i); + // 替换为 MessageFormat 占位符 {index} + sb.append('{').append(placeholderCount).append('}'); + placeholderCount++; + i++; // 跳过 '}' + lastIndex = i + 1; + } + } + + // 添加剩余部分 + sb.append(format.substring(lastIndex)); + + // 构造实际参数数组 + Object[] actualArgs; + if (args.length < placeholderCount) { + actualArgs = new String[placeholderCount]; + System.arraycopy(args, 0, actualArgs, 0, args.length); + for (int i = args.length; i < placeholderCount; i++) { + actualArgs[i] = ""; + } + } else { + actualArgs = args; + } + + // 如果没有占位符,直接返回格式化后的字符串 + if (placeholderCount == 0) { + return sb.toString(); + } + + try { + return MessageFormat.format( + sb.toString(), + (Object[]) Lists.toStringList(actualArgs) + ); + } catch (IllegalArgumentException e) { + // 返回原始格式化字符串或抛出自定义异常,视业务需求而定 + return sb.toString(); + } + } + + /** + * 判断字符串是否为空 + * + * @param str 待检查的字符串 + * @return 如果字符串为null或空字符串则返回true,否则返回false + */ + public static boolean isEmpty(String str) { + return str == null || str.isEmpty(); + } + + /** + * 使用指定的分隔符将多个对象连接成一个字符串 + * + * @param spec 用作分隔符的字符串 + * @param objects 要连接的对象数组 + * @return 使用指定分隔符连接后的字符串 + */ + public static String joinOf(String spec, String... objects) { + return join(spec, Arrays.asList(objects)); + } + + /** + * 将字符串按照指定分隔符分割成字符串列表,并移除列表开头的空字符串元素 + * + * @param str 待分割的字符串 + * @param separator 用作分割符的字符串 + * @return 分割后的字符串列表,不包含开头的空字符串元素 + */ + public static List split(String str, String separator) { + List data = new ArrayList<>( + Arrays.asList(str.split(separator)) + ); + // 移除列表开头的所有空字符串元素 + while (!data.isEmpty() && data.get(0).isEmpty()) { + data.remove(0); + } + return data; + } + + /** + * 将列表中的元素按照指定分隔符连接成字符串 + * + * @param

列表元素的类型 + * @param separator 分隔符,用于连接各个元素 + * @param list 待连接的元素列表 + * @param fun 转换函数,用于将列表元素转换为字符串,如果为null则使用toString()方法 + * @return 连接后的字符串,如果列表为空或null则返回空字符串 + */ + public static

String join( + String separator, + List

list, + PRFunction fun + ) { + // 处理空列表情况 + if (list == null || list.isEmpty()) { + return ""; + } + + // 构建结果字符串 + StringBuilder sb = StringUtil.stringBuilder(list.size() * 16); + for (int i = 0; i < list.size(); i++) { + P item = list.get(i); + // 将元素转换为字符串 + String itemStr = fun == null + ? (item == null ? "null" : item.toString()) + : fun.call(item); + + // 第一个元素直接添加,其他元素先添加分隔符再添加元素 + if (i == 0) { + sb.append(itemStr); + } else { + sb.append(separator).append(itemStr); + } + } + return sb.toString(); + } + + /** + * 使用指定分隔符连接字符串列表 + * + * @param separator 分隔符,不能为null + * @param list 字符串列表,不能为null + * @return 连接后的字符串 + * @throws IllegalArgumentException 当separator或list为null时抛出 + */ + public static String join(String separator, List list) { + if (separator == null) { + throw new IllegalArgumentException("Separator cannot be null"); + } + if (list == null) { + throw new IllegalArgumentException("List cannot be null"); + } + return join(separator, list, null); + } + + /** + * 创建一个新的StringBuilder实例 + * + * @param i 指定StringBuilder的初始容量 + * @return 返回一个新的StringBuilder对象,其初始容量为指定的大小 + */ + public static StringBuilder stringBuilder(int i) { + return new StringBuilder(i); + } + + /** + * PRFunction接口表示一个接收参数P并返回结果R的函数式接口 + *

+ * 该接口使用@FunctionalInterface注解标记,表明它是一个函数式接口, + * 只包含一个抽象方法call,可用于Lambda表达式和方法引用 + *

+ * + * @param

函数接收的参数类型 + * @param 函数返回的结果类型 + */ + @FunctionalInterface + public interface PRFunction { + /** + * 执行函数调用 + * + * @param p 输入参数 + * @return 函数执行结果 + */ + R call(P p); + } +} diff --git a/src/com/mingliqiye/utils/system/SystemUtil.java b/src/com/mingliqiye/utils/system/SystemUtil.java new file mode 100644 index 0000000..0bd9a19 --- /dev/null +++ b/src/com/mingliqiye/utils/system/SystemUtil.java @@ -0,0 +1,198 @@ +package com.mingliqiye.utils.system; + +import com.mingliqiye.utils.collection.Lists; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * 系统工具类,提供操作系统类型判断和JDK版本检测功能 + * + * @author MingLiPro + */ +public class SystemUtil { + + private static final String osName = System.getProperties().getProperty( + "os.name" + ); + + /** + * 判断当前操作系统是否为Windows系统 + * + * @return 如果是Windows系统返回true,否则返回false + */ + public static boolean isWindows() { + return osName != null && osName.startsWith("Windows"); + } + + /** + * 判断当前操作系统是否为Mac系统 + * + * @return 如果是Mac系统返回true,否则返回false + */ + public static boolean isMac() { + return osName != null && osName.startsWith("Mac"); + } + + /** + * 判断当前操作系统是否为Unix/Linux系统 + * + * @return 如果是Unix/Linux系统返回true,否则返回false + */ + public static boolean isUnix() { + if (osName == null) { + return false; + } + return ( + osName.startsWith("Linux") || + osName.startsWith("AIX") || + osName.startsWith("SunOS") + ); + } + + /** + * 获取JDK版本号 + * + * @return JDK版本号字符串 + */ + public static String getJdkVersion() { + return System.getProperty("java.specification.version"); + } + + /** + * 获取Java版本号的整数形式 + * + * @return Java版本号的整数形式(如:8、11、17等) + */ + public static Integer getJavaVersionAsInteger() { + String version = getJdkVersion(); + if (version == null || version.isEmpty()) { + throw new IllegalStateException( + "Unable to determine Java version from property 'java.specification.version'" + ); + } + + String uversion; + if (version.startsWith("1.")) { + if (version.length() < 3) { + throw new IllegalStateException( + "Invalid Java version format: " + version + ); + } + uversion = version.substring(2, 3); + } else { + if (version.length() < 2) { + throw new IllegalStateException( + "Invalid Java version format: " + version + ); + } + uversion = version.substring(0, 2); + } + return Integer.parseInt(uversion); + } + + /** + * 判断当前JDK版本是否大于8 + * + * @return 如果JDK版本大于8返回true,否则返回false + */ + public static boolean isJdk8Plus() { + return getJavaVersionAsInteger() > 8; + } + + /** + * 获取本地IP地址数组 + * + * @return 本地IP地址字符串数组 + * @throws RuntimeException 当获取网络接口信息失败时抛出 + */ + public static String[] getLocalIps() { + try { + List ipList = new ArrayList<>(); + Enumeration interfaces = + NetworkInterface.getNetworkInterfaces(); + + while (interfaces.hasMoreElements()) { + NetworkInterface networkInterface = interfaces.nextElement(); + // 跳过回环接口和虚拟接口 + if ( + networkInterface.isLoopback() || + networkInterface.isVirtual() || + !networkInterface.isUp() + ) { + continue; + } + + Enumeration addresses = + networkInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + // 只获取IPv4地址 + if (address instanceof Inet4Address) { + ipList.add(address.getHostAddress()); + } + } + } + + return ipList.toArray(new String[0]); + } catch (SocketException e) { + throw new RuntimeException("Failed to get local IP addresses", e); + } + } + + /** + * 获取本地IP地址列表 + * + * @return 本地IP地址的字符串列表 + */ + public static List getLocalIpsByList() { + return Lists.newArrayList(getLocalIps()); + } + + /** + * 获取本地回环地址 + * + * @return 回环地址字符串,通常为"127.0.0.1" + */ + public static String[] getLoopbackIps() { + List strings = new ArrayList<>(3); + try { + Enumeration interfaces = + NetworkInterface.getNetworkInterfaces(); + + while (interfaces.hasMoreElements()) { + NetworkInterface networkInterface = interfaces.nextElement(); + + // 只处理回环接口 + if (networkInterface.isLoopback() && networkInterface.isUp()) { + Enumeration addresses = + networkInterface.getInetAddresses(); + + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + strings.add(address.getHostAddress()); + } + } + } + return strings.toArray(new String[0]); + } catch (SocketException e) { + // 可考虑添加日志记录 + return new String[] { "127.0.0.1" }; + } + } + + /** + * 获取本地回环地址IP列表 + * + * @return 本地回环地址IP字符串列表的副本 + */ + public static List getLoopbackIpsByList() { + // 将本地回环地址IP数组转换为列表并返回 + return Lists.newArrayList(getLoopbackIps()); + } +} diff --git a/src/com/mingliqiye/utils/time/DateTime.java b/src/com/mingliqiye/utils/time/DateTime.java new file mode 100644 index 0000000..e21e47f --- /dev/null +++ b/src/com/mingliqiye/utils/time/DateTime.java @@ -0,0 +1,414 @@ +package com.mingliqiye.utils.time; + +import lombok.Getter; +import lombok.Setter; +import lombok.var; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +/** + * 时间工具类,用于处理日期时间的转换、格式化等操作。 + * 提供了多种静态方法来创建 DateTime 实例,并支持与 Date、LocalDateTime 等类型的互转。 + * + * @author MingLiPro + * @see LocalDateTime + */ +public final class DateTime { + + @Getter + @Setter + private ZoneId zoneId = ZoneId.systemDefault(); + + @Getter + private LocalDateTime localDateTime; + + /** + * 私有构造函数,使用指定的 LocalDateTime 初始化实例。 + * + * @param time LocalDateTime 对象 + */ + private DateTime(LocalDateTime time) { + setLocalDateTime(time); + } + + /** + * 私有构造函数,使用当前系统时间初始化实例。 + */ + private DateTime() { + setLocalDateTime(LocalDateTime.now()); + } + + /** + * 获取当前时间的 DateTime 实例。 + * + * @return 返回当前时间的 DateTime 实例 + */ + public static DateTime now() { + return new DateTime(); + } + + /** + * 将 Date 对象转换为 DateTime 实例。 + * + * @param zoneId 时区信息 + * @param date Date 对象 + * @return 返回对应的 DateTime 实例 + */ + public static DateTime of(Date date, ZoneId zoneId) { + return new DateTime(date.toInstant().atZone(zoneId).toLocalDateTime()); + } + + /** + * 将 Date 对象转换为 DateTime 实例,使用系统默认时区。 + * + * @param date Date 对象 + * @return 返回对应的 DateTime 实例 + */ + public static DateTime of(Date date) { + return new DateTime( + date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime() + ); + } + + /** + * 解析时间字符串并生成 DateTime 实例。 + * + * @param timestr 时间字符串 + * @param formatter 格式化模板 + * @param fillZero 是否补零到模板长度 + * @return 返回解析后的 DateTime 实例 + */ + public static DateTime parse( + String timestr, + String formatter, + boolean fillZero + ) { + return new DateTime( + LocalDateTime.parse( + fillZero ? getFillZeroByLen(timestr, formatter) : timestr, + DateTimeFormatter.ofPattern(formatter) + ) + ); + } + + /** + * 使用 Formatter 枚举解析时间字符串并生成 DateTime 实例。 + * + * @param timestr 时间字符串 + * @param formatter 格式化模板枚举 + * @param fillZero 是否补零到模板长度 + * @return 返回解析后的 DateTime 实例 + */ + public static DateTime parse( + String timestr, + Formatter formatter, + boolean fillZero + ) { + return parse(timestr, formatter.getValue(), fillZero); + } + + /** + * 使用 Formatter 枚举解析时间字符串并生成 DateTime 实例,默认不补零。 + * + * @param timestr 时间字符串 + * @param formatter 格式化模板枚举 + * @return 返回解析后的 DateTime 实例 + */ + public static DateTime parse(String timestr, Formatter formatter) { + return parse(timestr, formatter.getValue()); + } + + /** + * 解析时间字符串并生成 DateTime 实例,默认不补零。 + * + * @param timestr 时间字符串 + * @param formatter 格式化模板 + * @return 返回解析后的 DateTime 实例 + */ + public static DateTime parse(String timestr, String formatter) { + return parse(timestr, formatter, false); + } + + /** + * 补零处理时间字符串以匹配格式化模板长度。 + * + * @param dstr 原始时间字符串 + * @param formats 格式化模板 + * @return 补零后的时间字符串 + */ + private static String getFillZeroByLen(String dstr, String formats) { + if (dstr.length() == formats.length()) { + return dstr; + } + if (formats.length() > dstr.length()) { + if (dstr.length() == 19) { + dstr += "."; + } + var sb = new StringBuilder(dstr); + for (int i = 0; i < formats.length() - dstr.length(); i++) { + sb.append("0"); + } + return sb.toString(); + } + throw new IllegalArgumentException( + String.format( + "Text: '%s' len %s < %s %s", + dstr, + dstr.length(), + formats, + formats.length() + ) + ); + } + + /** + * 根据年、月、日创建 DateTime 实例 + * + * @param year 年份 + * @param month 月份 (1-12) + * @param day 日期 (1-31) + * @return 返回指定日期的 DateTime 实例(时间部分为 00:00:00) + */ + public static DateTime of(int year, int month, int day) { + return new DateTime(LocalDateTime.of(year, month, day, 0, 0)); + } + + /** + * 根据年、月、日、时、分创建 DateTime 实例 + * + * @param year 年份 + * @param month 月份 (1-12) + * @param day 日期 (1-31) + * @param hour 小时 (0-23) + * @param minute 分钟 (0-59) + * @return 返回指定日期时间的 DateTime 实例(秒部分为 00) + */ + public static DateTime of( + int year, + int month, + int day, + int hour, + int minute + ) { + return new DateTime(LocalDateTime.of(year, month, day, hour, minute)); + } + + /** + * 根据年、月、日、时、分、秒创建 DateTime 实例 + * + * @param year 年份 + * @param month 月份 (1-12) + * @param day 日期 (1-31) + * @param hour 小时 (0-23) + * @param minute 分钟 (0-59) + * @param second 秒 (0-59) + * @return 返回指定日期时间的 DateTime 实例 + */ + public static DateTime of( + int year, + int month, + int day, + int hour, + int minute, + int second + ) { + return new DateTime( + LocalDateTime.of(year, month, day, hour, minute, second) + ); + } + + /** + * 根据年、月、日、时、分、秒、纳秒创建 DateTime 实例 + * + * @param year 年份 + * @param month 月份 (1-12) + * @param day 日期 (1-31) + * @param hour 小时 (0-23) + * @param minute 分钟 (0-59) + * @param second 秒 (0-59) + * @param nano 纳秒 (0-999,999,999) + * @return 返回指定日期时间的 DateTime 实例 + */ + public static DateTime of( + int year, + int month, + int day, + int hour, + int minute, + int second, + int nano + ) { + return new DateTime( + LocalDateTime.of(year, month, day, hour, minute, second, nano) + ); + } + + /** + * 根据毫秒时间戳创建 DateTime 实例 + * + * @param epochMilli 毫秒时间戳 + * @return 返回对应时间的 DateTime 实例 + */ + public static DateTime of(long epochMilli) { + return new DateTime( + Instant.ofEpochMilli(epochMilli) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + ); + } + + /** + * 根据毫秒时间戳和时区创建 DateTime 实例 + * + * @param epochMilli 毫秒时间戳 + * @param zoneId 时区信息 + * @return 返回对应时间的 DateTime 实例 + */ + public static DateTime of(long epochMilli, ZoneId zoneId) { + return new DateTime( + Instant.ofEpochMilli(epochMilli).atZone(zoneId).toLocalDateTime() + ); + } + + /** + * 设置 LocalDateTime 实例。 + * + * @param localDateTime LocalDateTime 对象 + */ + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + /** + * 将当前 DateTime 转换为 Date 对象。 + * + * @return 返回对应的 Date 对象 + */ + public Date toDate() { + return Date.from(localDateTime.atZone(getZoneId()).toInstant()); + } + + /** + * 获取当前 DateTime 中的 LocalDateTime 实例。 + * + * @return 返回 LocalDateTime 对象 + */ + public LocalDateTime toLocalDateTime() { + return localDateTime; + } + + /** + * 在当前时间基础上增加指定的时间偏移量。 + * + * @param dateTimeOffset 时间偏移对象 + * @return 返回修改后的 DateTime 实例 + */ + public DateTime add(DateTimeOffset dateTimeOffset) { + this.localDateTime = this.localDateTime.plus( + dateTimeOffset.getOffset(), + dateTimeOffset.getOffsetType() + ); + return this; + } + + /** + * 在当前时间基础上减少指定的时间偏移量。 + * + * @param dateTimeOffset 时间偏移对象 + * @return 返回修改后的 DateTime 实例 + */ + public DateTime sub(DateTimeOffset dateTimeOffset) { + this.localDateTime = this.localDateTime.plus( + -dateTimeOffset.getOffset(), + dateTimeOffset.getOffsetType() + ); + return this; + } + + /** + * 使用指定格式化模板将当前时间格式化为字符串。 + * + * @param formatter 格式化模板 + * @return 返回格式化后的时间字符串 + */ + public String format(String formatter) { + return format(formatter, false); + } + + /** + * 使用 Formatter 枚举将当前时间格式化为字符串。 + * + * @param formatter 格式化模板枚举 + * @return 返回格式化后的时间字符串 + */ + public String format(Formatter formatter) { + return format(formatter.getValue()); + } + + /** + * 使用指定格式化模板将当前时间格式化为字符串,并可选择是否去除末尾多余的零。 + * + * @param formatter 格式化模板 + * @param repcZero 是否去除末尾多余的零 + * @return 返回格式化后的时间字符串 + */ + public String format(String formatter, boolean repcZero) { + var formatted = DateTimeFormatter.ofPattern(formatter).format( + toLocalDateTime() + ); + if (repcZero) { + // 处理小数点后多余的0 + formatted = formatted.replaceAll("(\\.\\d*?)0+\\b", "$1"); + formatted = formatted.replaceAll("\\.$", ""); + } + return formatted; + } + + /** + * 使用 Formatter 枚举将当前时间格式化为字符串,并可选择是否去除末尾多余的零。 + * + * @param formatter 格式化模板枚举 + * @param repcZero 是否去除末尾多余的零 + * @return 返回格式化后的时间字符串 + */ + public String format(Formatter formatter, boolean repcZero) { + return format(formatter.getValue(), repcZero); + } + + /** + * 返回当前时间的标准字符串表示形式。 + * + * @return 返回标准格式的时间字符串 + */ + @Override + public String toString() { + return String.format( + "DateTime(%s)", + format(Formatter.STANDARD_DATETIME_MILLISECOUND7, true) + ); + } + + /** + * 比较当前DateTime对象与指定对象是否相等 + * + * @param obj 要比较的对象 + * @return 如果对象相等则返回true,否则返回false + */ + @Override + public boolean equals(Object obj) { + // 检查对象类型是否为DateTime + if (obj instanceof DateTime) { + // 比较两个DateTime对象转换为LocalDateTime后的值 + return toLocalDateTime().equals(((DateTime) obj).toLocalDateTime()); + } + return false; + } + + public Instant toInstant() { + return localDateTime.atZone(zoneId).toInstant(); + } +} diff --git a/src/com/mingliqiye/utils/time/DateTimeOffset.java b/src/com/mingliqiye/utils/time/DateTimeOffset.java new file mode 100644 index 0000000..2b071b8 --- /dev/null +++ b/src/com/mingliqiye/utils/time/DateTimeOffset.java @@ -0,0 +1,43 @@ +package com.mingliqiye.utils.time; + +import java.time.temporal.ChronoUnit; +import lombok.Getter; + +/** + * 时间位移 类 + * + * @author MingLiPro + */ +@Getter +public class DateTimeOffset { + + private final ChronoUnit offsetType; + private final Long offset; + + private DateTimeOffset(ChronoUnit offsetType, Long offset) { + this.offsetType = offsetType; + this.offset = offset; + } + + /** + * 创建一个新的DateTimeOffset实例 + * + * @param offsetType 偏移量的单位类型,指定偏移量的计算单位 + * @param offset 偏移量的数值,可以为正数、负数或零 + * @return 返回一个新的DateTimeOffset对象,包含指定的偏移量信息 + */ + public static DateTimeOffset of(ChronoUnit offsetType, Long offset) { + return new DateTimeOffset(offsetType, offset); + } + + /** + * 创建一个 DateTimeOffset 实例 + * + * @param offset 偏移量数值 + * @param offsetType 偏移量的时间单位类型 + * @return 返回一个新的 DateTimeOffset 实例 + */ + public static DateTimeOffset of(Long offset, ChronoUnit offsetType) { + return new DateTimeOffset(offsetType, offset); + } +} diff --git a/src/com/mingliqiye/utils/time/DateTimeUnit.java b/src/com/mingliqiye/utils/time/DateTimeUnit.java new file mode 100644 index 0000000..516f936 --- /dev/null +++ b/src/com/mingliqiye/utils/time/DateTimeUnit.java @@ -0,0 +1,43 @@ +package com.mingliqiye.utils.time; + +/** + * 时间单位常量定义 + * + * @author MingLiPro + */ +public interface DateTimeUnit { + // 时间单位常量 + String YEAR = "year"; + String MONTH = "month"; + String WEEK = "week"; + String DAY = "day"; + String HOUR = "hour"; + String MINUTE = "minute"; + String SECOND = "second"; + String MILLISECOND = "millisecond"; + String MICROSECOND = "microsecond"; + String NANOSECOND = "nanosecond"; + + // 时间单位缩写 + String YEAR_ABBR = "y"; + String MONTH_ABBR = "M"; + String WEEK_ABBR = "w"; + String DAY_ABBR = "d"; + String HOUR_ABBR = "h"; + String MINUTE_ABBR = "m"; + String SECOND_ABBR = "s"; + String MILLISECOND_ABBR = "ms"; + String MICROSECOND_ABBR = "μs"; + String NANOSECOND_ABBR = "ns"; + + // 时间单位转换系数(毫秒为基准) + long MILLIS_PER_SECOND = 1000L; + long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND; + long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE; + long MILLIS_PER_DAY = 24 * MILLIS_PER_HOUR; + long MILLIS_PER_WEEK = 7 * MILLIS_PER_DAY; + + // 月份和年的毫秒数仅为近似值 + long MILLIS_PER_MONTH = 30 * MILLIS_PER_DAY; // 近似值 + long MILLIS_PER_YEAR = 365 * MILLIS_PER_DAY; // 近似值 +} diff --git a/src/com/mingliqiye/utils/time/Formatter.java b/src/com/mingliqiye/utils/time/Formatter.java new file mode 100644 index 0000000..e7e0152 --- /dev/null +++ b/src/com/mingliqiye/utils/time/Formatter.java @@ -0,0 +1,93 @@ +package com.mingliqiye.utils.time; + +import lombok.Getter; + +/** + * 时间格式化枚举类 + *

+ * 定义了常用的时间格式化模式,用于日期时间的解析和格式化操作 + * 每个枚举常量包含对应的格式化字符串和字符串长度 + *

+ */ +public enum Formatter { + /** + * 标准日期时间格式:yyyy-MM-dd HH:mm:ss + */ + STANDARD_DATETIME("yyyy-MM-dd HH:mm:ss"), + + /** + * 标准日期时间格式(7位毫秒):yyyy-MM-dd HH:mm:ss.SSSSSSS + */ + STANDARD_DATETIME_MILLISECOUND7("yyyy-MM-dd HH:mm:ss.SSSSSSS"), + + /** + * 标准日期时间格式(6位毫秒):yyyy-MM-dd HH:mm:ss.SSSSSS + */ + STANDARD_DATETIME_MILLISECOUND6("yyyy-MM-dd HH:mm:ss.SSSSSS"), + + /** + * 标准日期时间格式(5位毫秒):yyyy-MM-dd HH:mm:ss.SSSSS + */ + STANDARD_DATETIME_MILLISECOUND5("yyyy-MM-dd HH:mm:ss.SSSSS"), + + /** + * 标准日期时间格式(4位毫秒):yyyy-MM-dd HH:mm:ss.SSSS + */ + STANDARD_DATETIME_MILLISECOUND4("yyyy-MM-dd HH:mm:ss.SSSS"), + + /** + * 标准日期时间格式(3位毫秒):yyyy-MM-dd HH:mm:ss.SSS + */ + STANDARD_DATETIME_MILLISECOUND3("yyyy-MM-dd HH:mm:ss.SSS"), + + /** + * 标准日期时间格式(2位毫秒):yyyy-MM-dd HH:mm:ss.SS + */ + STANDARD_DATETIME_MILLISECOUND2("yyyy-MM-dd HH:mm:ss.SS"), + + /** + * 标准日期时间格式(1位毫秒):yyyy-MM-dd HH:mm:ss.S + */ + STANDARD_DATETIME_MILLISECOUND1("yyyy-MM-dd HH:mm:ss.S"), + + /** + * 标准ISO格式:yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + */ + STANDARD_ISO("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"), + + /** + * 标准日期时间秒格式:yyyy-MM-dd HH:mm:ss + */ + STANDARD_DATETIME_SECOUND("yyyy-MM-dd HH:mm:ss"), + + /** + * 标准日期格式:yyyy-MM-dd + */ + STANDARD_DATE("yyyy-MM-dd"), + + /** + * ISO8601格式:yyyy-MM-dd'T'HH:mm:ss.SSS'000' + */ + ISO8601("yyyy-MM-dd'T'HH:mm:ss.SSS'000'"), + + /** + * 紧凑型日期时间格式:yyyyMMddHHmmss + */ + COMPACT_DATETIME("yyyyMMddHHmmss"); + + @Getter + private final String value; + + @Getter + private final int len; + + /** + * 构造函数 + * + * @param value 格式化模式字符串 + */ + Formatter(String value) { + this.value = value; + this.len = value.length(); + } +} diff --git a/src/com/mingliqiye/utils/time/serialization/Jackson.java b/src/com/mingliqiye/utils/time/serialization/Jackson.java new file mode 100644 index 0000000..fabbff9 --- /dev/null +++ b/src/com/mingliqiye/utils/time/serialization/Jackson.java @@ -0,0 +1,188 @@ +package com.mingliqiye.utils.time.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.WritableTypeId; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.mingliqiye.utils.time.DateTime; +import com.mingliqiye.utils.time.Formatter; + +import java.io.IOException; + +/** + * Jackson 适配器 + * + * @author MingLiPro + */ +public class Jackson { + + /** + * yyyy-MM-dd HH:mm:ss.SSSSSSS 的反序列化适配器 + *

+ * 将 JSON 字符串按照指定格式解析为 DateTime 对象。 + */ + public static class DateTimeJsonDeserializerM7 + extends DateTimeJsonDeserializer { + + /** + * 获取当前使用的日期时间格式化器 + * + * @return 返回标准的 7 位毫秒时间格式化器 + */ + @Override + public Formatter getFormatter() { + return Formatter.STANDARD_DATETIME_MILLISECOUND7; + } + } + + /** + * 默认日期时间反序列化器 + *

+ * 提供基础的日期时间反序列化功能,支持自定义格式化器。 + */ + public static class DateTimeJsonDeserializer + extends JsonDeserializer { + + /** + * 获取当前使用的日期时间格式化器 + * + * @return 返回标准的日期时间格式化器 + */ + public Formatter getFormatter() { + return Formatter.STANDARD_DATETIME; + } + + /** + * 获取格式化器对应的字符串表达式 + * + * @return 格式化器的字符串值 + */ + public String getFormatterString() { + return getFormatter().getValue(); + } + + /** + * 反序列化方法:将 JSON 解析为 DateTime 对象 + * + * @param p JSON 解析器对象 + * @param ctxt 反序列化上下文 + * @return 解析后的 DateTime 对象,若输入为 NaN 则返回 null + * @throws IOException 当解析过程中发生 IO 异常时抛出 + */ + @Override + public DateTime deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException { + // 如果是 NaN 值则返回 null + if (p.isNaN()) { + return null; + } + // 使用指定格式将字符串解析为 DateTime 对象 + return DateTime.parse( + p.getValueAsString(), + getFormatterString(), + true + ); + } + } + + /** + * yyyy-MM-dd HH:mm:ss.SSSSSSS 的序列化适配器 + *

+ * 将 DateTime 对象按指定格式转换为 JSON 字符串。 + */ + public static class DateTimeJsonSerializerM7 + extends DateTimeJsonSerializer { + + /** + * 获取当前使用的日期时间格式化器 + * + * @return 返回标准的 7 位毫秒时间格式化器 + */ + @Override + public Formatter getFormatter() { + return Formatter.STANDARD_DATETIME_MILLISECOUND7; + } + } + + /** + * 默认日期时间序列化器 + *

+ * 提供基础的日期时间序列化功能,支持自定义格式化器。 + */ + public static class DateTimeJsonSerializer + extends JsonSerializer { + + /** + * 获取当前使用的日期时间格式化器 + * + * @return 返回标准的日期时间格式化器 + */ + public Formatter getFormatter() { + return Formatter.STANDARD_DATETIME; + } + + /** + * 获取格式化器对应的字符串表达式 + * + * @return 格式化器的字符串值 + */ + public String getFormatterString() { + return getFormatter().getValue(); + } + + /** + * 序列化方法:将 DateTime 对象写入 JSON 生成器 + * + * @param value 要序列化的 DateTime 对象 + * @param gen JSON 生成器 + * @param serializers 序列化提供者 + * @throws IOException 当写入过程中发生 IO 异常时抛出 + */ + @Override + public void serialize( + DateTime value, + JsonGenerator gen, + SerializerProvider serializers + ) throws IOException { + // 若值为 null,则直接写入 null + if (value == null) { + gen.writeNull(); + return; + } + // 按照指定格式将 DateTime 写入为字符串 + gen.writeString(value.format(getFormatterString(), true)); + } + + /** + * 带类型信息的序列化方法:用于支持多态类型处理 + * + * @param value 要序列化的 DateTime 对象 + * @param gen JSON 生成器 + * @param serializers 序列化提供者 + * @param typeSer 类型序列化器 + * @throws IOException 当写入过程中发生 IO 异常时抛出 + */ + @Override + public void serializeWithType( + DateTime value, + JsonGenerator gen, + SerializerProvider serializers, + TypeSerializer typeSer + ) throws IOException { + // 写入类型前缀 + WritableTypeId typeId = typeSer.writeTypePrefix( + gen, + typeSer.typeId(value, JsonToken.VALUE_STRING) + ); + // 执行实际序列化 + serialize(value, gen, serializers); + // 写入类型后缀 + typeSer.writeTypeSuffix(gen, typeId); + } + } +} diff --git a/src/com/mingliqiye/utils/time/typehandlers/DateTimeTypeHandler.java b/src/com/mingliqiye/utils/time/typehandlers/DateTimeTypeHandler.java new file mode 100644 index 0000000..2190679 --- /dev/null +++ b/src/com/mingliqiye/utils/time/typehandlers/DateTimeTypeHandler.java @@ -0,0 +1,107 @@ +package com.mingliqiye.utils.time.typehandlers; + +import com.mingliqiye.utils.time.DateTime; +import com.mingliqiye.utils.time.Formatter; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import java.sql.*; + +/** + * DateTime类型处理器类 + * 用于在MyBatis中处理DateTime类型与数据库VARCHAR类型之间的转换 + */ +@MappedTypes({ DateTime.class }) +@MappedJdbcTypes(JdbcType.VARCHAR) +public class DateTimeTypeHandler extends BaseTypeHandler { + + /** + * 设置非空参数值 + * 将DateTime对象转换为Timestamp并设置到PreparedStatement中 + * + * @param ps PreparedStatement对象 + * @param i 参数索引位置 + * @param parameter DateTime参数值 + * @param jdbcType JDBC类型 + * @throws SQLException SQL异常 + */ + @Override + public void setNonNullParameter( + PreparedStatement ps, + int i, + DateTime parameter, + JdbcType jdbcType + ) throws SQLException { + ps.setTimestamp(i, Timestamp.valueOf(parameter.getLocalDateTime())); + } + + /** + * 从ResultSet中获取可为空的结果值 + * 根据列名获取字符串值并解析为DateTime对象 + * + * @param rs ResultSet对象 + * @param columnName 列名 + * @return DateTime对象,如果值为null则返回null + * @throws SQLException SQL异常 + */ + @Override + public DateTime getNullableResult(ResultSet rs, String columnName) + throws SQLException { + return parse(rs.getString(columnName)); + } + + /** + * 从ResultSet中获取可为空的结果值 + * 根据列索引获取字符串值并解析为DateTime对象 + * + * @param rs ResultSet对象 + * @param columnIndex 列索引 + * @return DateTime对象,如果值为null则返回null + * @throws SQLException SQL异常 + */ + @Override + public DateTime getNullableResult(ResultSet rs, int columnIndex) + throws SQLException { + return parse(rs.getString(columnIndex)); + } + + /** + * 从CallableStatement中获取可为空的结果值 + * 根据列索引获取字符串值并解析为DateTime对象 + * + * @param cs CallableStatement对象 + * @param columnIndex 列索引 + * @return DateTime对象,如果值为null则返回null + * @throws SQLException SQL异常 + */ + @Override + public DateTime getNullableResult(CallableStatement cs, int columnIndex) + throws SQLException { + return parse(cs.getString(columnIndex)); + } + + /** + * 解析字符串为DateTime对象 + * + * @param s 待解析的字符串 + * @return DateTime对象,如果字符串为null则返回null + */ + public DateTime parse(String s) { + if (s == null) { + return null; + } + return DateTime.parse(s, Formatter.STANDARD_DATETIME_MILLISECOUND7); + } + + /** + * 格式化DateTime对象为字符串 + * + * @param t DateTime对象 + * @return 格式化后的字符串 + */ + public String format(DateTime t) { + return t.format(Formatter.STANDARD_DATETIME_MILLISECOUND7); + } +} diff --git a/src/com/mingliqiye/utils/uuid/UUID.java b/src/com/mingliqiye/utils/uuid/UUID.java new file mode 100644 index 0000000..84ed5e1 --- /dev/null +++ b/src/com/mingliqiye/utils/uuid/UUID.java @@ -0,0 +1,252 @@ +package com.mingliqiye.utils.uuid; + +import com.github.f4b6a3.uuid.UuidCreator; +import com.mingliqiye.utils.string.StringUtil; +import com.mingliqiye.utils.time.DateTime; +import com.mingliqiye.utils.time.DateTimeOffset; +import com.mingliqiye.utils.time.Formatter; +import lombok.Setter; + +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.time.temporal.ChronoUnit; +import java.util.Locale; +import java.util.Objects; + +/** + * UUID 工具类,用于生成、解析和操作 UUID。 + * 支持时间戳型 UUID(版本1)以及标准 UUID 的创建与转换。 + * + * @author MingLiPro + */ +@Setter +public class UUID implements Serializable { + + /** + * 内部封装的 java.util.UUID 实例 + */ + private java.util.UUID uuid; + + /** + * 构造一个由指定高位和低位组成的 UUID。 + * + * @param msb 高64位 + * @param lsb 低64位 + */ + public UUID(long msb, long lsb) { + uuid = new java.util.UUID(msb, lsb); + } + + /** + * 构造一个基于当前时间的时间戳型 UUID(版本1)。 + */ + public UUID() { + uuid = UuidCreator.getTimeBased(); + } + + /** + * 使用给定的 java.util.UUID 对象构造一个新的 UUID 实例。 + * + * @param uuid java.util.UUID 实例 + */ + public UUID(java.util.UUID uuid) { + this.uuid = uuid; + } + + /** + * 根据字符串表示的 UUID 构造一个新的 UUID 实例。 + * + * @param uuid 字符串形式的 UUID + */ + public UUID(String uuid) { + this.uuid = java.util.UUID.fromString(uuid); + } + + /** + * 将字节数组转换为 UUID 实例。 + * + * @param bytes 表示 UUID 的 16 字节数据 + * @return 新建的 UUID 实例 + */ + public static UUID of(byte[] bytes) { + ByteBuffer bb = ByteBuffer.wrap(bytes); + long msb = bb.getLong(); + long lsb = bb.getLong(); + return new UUID(msb, lsb); + } + + /** + * 将字符串解析为 UUID 实例,如果解析失败则抛出 UUIDException。 + * + * @param data UUID 字符串 + * @return 解析后的 UUID 实例 + * @throws UUIDException 如果解析失败 + */ + public static UUID ofString(String data) { + try { + java.util.UUID uuid1 = java.util.UUID.fromString(data); + UUID uuid = new UUID(); + uuid.setUuid(uuid1); + return uuid; + } catch (Exception e) { + throw new UUIDException(e.getMessage(), e); + } + } + + /** + * 将 UUID 转换为 16 字节的字节数组。 + * + * @return 表示该 UUID 的字节数组 + */ + public byte[] toBytes() { + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } + + /** + * 获取内部封装的 java.util.UUID 实例。 + * + * @return java.util.UUID 实例 + */ + public java.util.UUID GetUUID() { + return uuid; + } + + /** + * 将 UUID 转换为字符串表示,默认使用小写格式。 + * + * @return UUID 字符串 + */ + public String toUUIDString() { + return toUUIDString(false); + } + + /** + * 将 UUID 转换为字符串表示,并可选择是否使用大写。 + * + * @param u 是否使用大写格式 + * @return UUID 字符串 + * @throws UUIDException 如果 uuid 为 null + */ + public String toUUIDString(boolean u) { + if (uuid == null) { + throw new UUIDException("uuid is null : NullPointerException"); + } + if (u) { + return uuid.toString().toUpperCase(Locale.ROOT); + } + return uuid.toString(); + } + + /** + * 计算此 UUID 的哈希码。 + * + * @return 哈希码值 + */ + @Override + public int hashCode() { + return Objects.hash(uuid); + } + + /** + * 判断两个 UUID 是否相等。 + * + * @param o 比较对象 + * @return 如果相等返回 true,否则返回 false + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UUID uuid = (UUID) o; + return Objects.equals(this.uuid, uuid.uuid); + } + + /** + * 返回此 UUID 的字符串表示,包含版本信息和时间戳(如果是版本1)。 + * + * @return UUID 的详细字符串表示 + */ + @Override + public String toString() { + if (uuid == null) { + return "UUID(null)"; + } + if (uuid.version() == 1) { + return StringUtil.format( + "UUID(uuid={},time={},mac={},version={})", + toUUIDString(true), + getDateTime().format(Formatter.STANDARD_DATETIME), + extractMACFromUUID(), + uuid.version() + ); + } + return StringUtil.format( + "UUID(uuid={},version={})", + toUUIDString(true), + uuid.version() + ); + } + + /** + * 从时间戳型 UUID 中提取时间戳并转换为 DateTime 对象。 + * + * @return 对应的 DateTime 对象;如果 uuid 为 null,则返回 null + */ + public DateTime getDateTime() { + if (uuid == null) { + return null; + } + return DateTime.of(uuid.timestamp() / 10_000).add( + DateTimeOffset.of(-141427L, ChronoUnit.DAYS) + ); + } + + /** + * 从时间戳型 UUID 中提取 MAC 地址,默认使用冒号分隔符。 + * + * @return MAC 地址字符串 + * @throws UUIDException 如果 uuid 为 null + */ + public String extractMACFromUUID() { + return extractMACFromUUID(null); + } + + /** + * 从时间戳型 UUID 中提取 MAC 地址,并允许自定义分隔符。 + * + * @param spec 分隔符字符,默认为 ":" + * @return MAC 地址字符串 + * @throws UUIDException 如果 uuid 为 null + */ + public String extractMACFromUUID(String spec) { + if (uuid == null) { + throw new UUIDException("uuid is null : NullPointerException"); + } + if (spec == null) { + spec = ":"; + } + long leastSigBits = uuid.getLeastSignificantBits(); + long macLong = leastSigBits & 0xFFFFFFFFFFFFL; + byte[] macBytes = new byte[6]; + // 提取 MAC 地址的每个字节 + for (int i = 0; i < 6; i++) { + macBytes[5 - i] = (byte) (macLong >> (8 * i)); + } + StringBuilder mac = new StringBuilder(); + // 构造 MAC 地址字符串 + for (int i = 0; i < 6; i++) { + mac.append(String.format("%02X", macBytes[i])); + if (i < 5) { + mac.append(spec); + } + } + return mac.toString(); + } +} diff --git a/src/com/mingliqiye/utils/uuid/UUIDException.java b/src/com/mingliqiye/utils/uuid/UUIDException.java new file mode 100644 index 0000000..49361ad --- /dev/null +++ b/src/com/mingliqiye/utils/uuid/UUIDException.java @@ -0,0 +1,12 @@ +package com.mingliqiye.utils.uuid; + +public class UUIDException extends RuntimeException { + + public UUIDException(String message) { + super(message); + } + + public UUIDException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/com/mingliqiye/utils/uuid/serialization/Jackson.java b/src/com/mingliqiye/utils/uuid/serialization/Jackson.java new file mode 100644 index 0000000..7439e4b --- /dev/null +++ b/src/com/mingliqiye/utils/uuid/serialization/Jackson.java @@ -0,0 +1,109 @@ +package com.mingliqiye.utils.uuid.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.WritableTypeId; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.mingliqiye.utils.uuid.UUID; +import com.mingliqiye.utils.uuid.UUIDException; + +import java.io.IOException; + +/** + * Jackson 序列化/反序列化适配器类,用于处理自定义 UUID 类的 JSON 转换 + * + * @author MingLiPro + */ +public class Jackson { + + /** + * UUID 反序列化器 + *

+ * 将 JSON 字符串反序列化为自定义 UUID 对象 + */ + public static class UUIDJsonDeserializer extends JsonDeserializer { + + /** + * 反序列化方法:将 JSON 解析为 UUID 对象 + * + * @param p JSON 解析器对象 + * @param ctxt 反序列化上下文 + * @return 解析后的 UUID 对象,若输入为 NaN 则返回 null + * @throws IOException 当解析过程中发生 IO 异常时抛出 + */ + @Override + public UUID deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException { + // 如果是 NaN 值则返回 null + if (p.isNaN()) { + return null; + } + // 使用指定字符串值创建新的 UUID 对象 + return new UUID(p.getValueAsString()); + } + } + + /** + * UUID 序列化器 + *

+ * 将自定义 UUID 对象序列化为 JSON 字符串 + */ + public static class UUIDJsonSerializer extends JsonSerializer { + + /** + * 序列化方法:将 UUID 对象写入 JSON 生成器 + * + * @param uuid 要序列化的 UUID 对象 + * @param jsonGenerator JSON 生成器 + * @param serializerProvider 序列化提供者 + * @throws UUIDException 当 UUID 处理过程中发生异常时抛出 + * @throws IOException 当写入过程中发生 IO 异常时抛出 + */ + @Override + public void serialize( + UUID uuid, + JsonGenerator jsonGenerator, + SerializerProvider serializerProvider + ) throws UUIDException, IOException { + // 若值为 null,则直接写入 null + if (uuid == null) { + jsonGenerator.writeNull(); + return; + } + // 将 UUID 写入为字符串 + jsonGenerator.writeString(uuid.toUUIDString()); + } + + /** + * 带类型信息的序列化方法:用于支持多态类型处理 + * + * @param value 要序列化的 UUID 对象 + * @param gen JSON 生成器 + * @param serializers 序列化提供者 + * @param typeSer 类型序列化器 + * @throws IOException 当写入过程中发生 IO 异常时抛出 + */ + @Override + public void serializeWithType( + UUID value, + JsonGenerator gen, + SerializerProvider serializers, + TypeSerializer typeSer + ) throws IOException { + // 写入类型前缀 + WritableTypeId typeId = typeSer.writeTypePrefix( + gen, + typeSer.typeId(value, JsonToken.VALUE_STRING) + ); + // 执行实际序列化 + serialize(value, gen, serializers); + // 写入类型后缀 + typeSer.writeTypeSuffix(gen, typeId); + } + } +} diff --git a/src/com/mingliqiye/utils/uuid/typehandlers/UUIDBinaryTypeHandler.java b/src/com/mingliqiye/utils/uuid/typehandlers/UUIDBinaryTypeHandler.java new file mode 100644 index 0000000..f53a9f9 --- /dev/null +++ b/src/com/mingliqiye/utils/uuid/typehandlers/UUIDBinaryTypeHandler.java @@ -0,0 +1,111 @@ +package com.mingliqiye.utils.uuid.typehandlers; + +import com.mingliqiye.utils.uuid.UUID; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import java.nio.ByteBuffer; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * UUIDBinaryTypeHandler 类用于处理 UUID 类型与数据库 BINARY 类型之间的转换 + * 该类继承自 BaseTypeHandler,专门处理 UUID 对象的序列化和反序列化 + * + * @author MingLiPro + */ +@MappedTypes({ UUID.class }) +@MappedJdbcTypes(JdbcType.BINARY) +public class UUIDBinaryTypeHandler extends BaseTypeHandler { + + /** + * 将 UUID 对象转换为二进制字节数组 + * + * @param uuid 要转换的 UUID 对象 + * @return 包含 UUID 数据的 16 字节二进制数组 + */ + public static byte[] UUID_TO_BIN(UUID uuid) { + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.GetUUID().getMostSignificantBits()); + bb.putLong(uuid.GetUUID().getLeastSignificantBits()); + return bb.array(); + } + + /** + * 将二进制字节数组转换为 UUID 对象 + * + * @param bytes 包含 UUID 数据的二进制字节数组 + * @return 转换后的 UUID 对象,如果输入为 null 则返回 null + */ + public static UUID BIN_TO_UUID(byte[] bytes) { + if (bytes == null) { + return null; + } + return UUID.of(bytes); + } + + /** + * 设置非空参数到 PreparedStatement 中 + * + * @param ps PreparedStatement 对象 + * @param i 参数在 SQL 语句中的位置索引 + * @param parameter 要设置的 UUID 参数值 + * @param jdbcType JDBC 类型信息 + * @throws SQLException 当数据库操作发生错误时抛出 + */ + @Override + public void setNonNullParameter( + PreparedStatement ps, + int i, + UUID parameter, + JdbcType jdbcType + ) throws SQLException { + ps.setBytes(i, UUID_TO_BIN(parameter)); + } + + /** + * 从 ResultSet 中根据列名获取可为空的 UUID 结果 + * + * @param rs ResultSet 对象 + * @param columnName 数据库列名 + * @return 转换后的 UUID 对象,可能为 null + * @throws SQLException 当数据库操作发生错误时抛出 + */ + @Override + public UUID getNullableResult(ResultSet rs, String columnName) + throws SQLException { + return BIN_TO_UUID(rs.getBytes(columnName)); + } + + /** + * 从 ResultSet 中根据列索引获取可为空的 UUID 结果 + * + * @param rs ResultSet 对象 + * @param columnIndex 数据库列索引 + * @return 转换后的 UUID 对象,可能为 null + * @throws SQLException 当数据库操作发生错误时抛出 + */ + @Override + public UUID getNullableResult(ResultSet rs, int columnIndex) + throws SQLException { + return BIN_TO_UUID(rs.getBytes(columnIndex)); + } + + /** + * 从 CallableStatement 中根据参数索引获取可为空的 UUID 结果 + * + * @param cs CallableStatement 对象 + * @param columnIndex 参数索引 + * @return 转换后的 UUID 对象,可能为 null + * @throws SQLException 当数据库操作发生错误时抛出 + */ + @Override + public UUID getNullableResult(CallableStatement cs, int columnIndex) + throws SQLException { + return BIN_TO_UUID(cs.getBytes(columnIndex)); + } +} diff --git a/src/main/java/com/mingliqiye/Main.java b/src/main/java/com/mingliqiye/Main.java deleted file mode 100644 index 1f285d4..0000000 --- a/src/main/java/com/mingliqiye/Main.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.mingliqiye; - -public class Main { - /** - * @param args [] - */ - public static void main(String[] args) { - System.out.print("Hello and welcome!"); - for (int i = 1; i <= 5; i++) { - System.out.println("i = " + i); - } - } -}