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 0000000..dfbf5af Binary files /dev/null and b/.idea/icon.png differ 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); - } - } -}