diff --git a/tamguo/src/main/java/com/tamguo/util/AbstractRunningLogHandler.java b/tamguo/src/main/java/com/tamguo/util/AbstractRunningLogHandler.java index feca2a1..b3028d1 100644 --- a/tamguo/src/main/java/com/tamguo/util/AbstractRunningLogHandler.java +++ b/tamguo/src/main/java/com/tamguo/util/AbstractRunningLogHandler.java @@ -4,111 +4,159 @@ import java.io.InterruptedIOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +// 定义一个抽象类,实现了 LogHandler 接口(这里假设 LogHandler 接口定义了一系列日志处理相关的方法,比如 info、error、debug 等,虽然在当前类中这些方法体暂时为空,但留给具体子类去实现具体的日志处理逻辑), +// 该抽象类主要用于处理运行时日志相关的通用逻辑,为具体的日志处理实现类提供基础框架和一些通用方法。 public abstract class AbstractRunningLogHandler implements LogHandler { - private static Method getStackTraceMethod; - private static Method getClassNameMethod; - private static Method getMethodNameMethod; - private static Method getFileNameMethod; - private static Method getLineNumberMethod; - - static { - try { - Class[] noArgs = null; - getStackTraceMethod = Throwable.class.getMethod("getStackTrace", noArgs); - Class stackTraceElementClass = Class.forName("java.lang.StackTraceElement"); - getClassNameMethod = stackTraceElementClass.getMethod("getClassName", noArgs); - getMethodNameMethod = stackTraceElementClass.getMethod("getMethodName", noArgs); - getFileNameMethod = stackTraceElementClass.getMethod("getFileName", noArgs); - getLineNumberMethod = stackTraceElementClass.getMethod("getLineNumber", noArgs); - } catch (ClassNotFoundException ex) { - LogDebug.debug("will use pre-JDK 1.4 methods to determine location."); - } catch (NoSuchMethodException ex) { - LogDebug.debug("will use pre-JDK 1.4 methods to determine location."); - } - } - - /** - * 获得指定class的StackTraceElement - * - * @param t - * @param fqnOfCallingClass - * - * @return - */ - protected StackTraceElement getRunningStackTrace(Throwable t, String fqnOfCallingClass) { - if (getLineNumberMethod != null) { - try { - Object[] noArgs = null; - Object[] elements = (Object[]) getStackTraceMethod.invoke(t, noArgs); - for (int i = elements.length - 1; i >= 0; i--) { - String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs); - if (fqnOfCallingClass.equals(thisClass)) { - // 执行class名称 - String className = fqnOfCallingClass; - // 执行方法名称 - String methodName = (String) getMethodNameMethod.invoke(elements[i], noArgs); - // 执行class文件名称 - String fileName = (String) getFileNameMethod.invoke(elements[i], noArgs); - // 执行到行号 - int lineNumber = ((Integer) getLineNumberMethod.invoke(elements[i], noArgs)).intValue(); - return new StackTraceElement(className, methodName, fileName, lineNumber); - } - } - } catch (IllegalAccessException ex) { - LogDebug.debug("failed using JDK 1.4 methods", ex); - } catch (InvocationTargetException ex) { - if (ex.getTargetException() instanceof InterruptedException - || ex.getTargetException() instanceof InterruptedIOException) { - Thread.currentThread().interrupt(); - } - LogDebug.debug("failed using JDK 1.4 methods", ex); - } catch (RuntimeException ex) { - LogDebug.debug("failed using JDK 1.4 methods", ex); - } - } - return this.createDefaultStackTrace(); - } - - /** - * 创建默认StackTraceElement - * - * @return - */ - private StackTraceElement createDefaultStackTrace() { - return new StackTraceElement(this.getClass().getName(), "log", this.getClass().getName(), 0); - } - - @Override - public void info(String msg, String fqnOfCallingClass) { - } - - @Override - public void info(String msg, Throwable t, String fqnOfCallingClass) { - } - - @Override - public void error(String msg, String fqnOfCallingClass) { - } - - @Override - public void error(String msg, Throwable t, String fqnOfCallingClass) { - } - - @Override - public void debug(String msg, String fqnOfCallingClass) { - } - - @Override - public void debug(String msg, Throwable t, String fqnOfCallingClass) { - } - - @Override - public void warning(String msg, String fqnOfCallingClass) { - } - - @Override - public void warning(String msg, Throwable t, String fqnOfCallingClass) { - } - -} + // 通过反射获取的用于获取堆栈跟踪信息的方法对象,对应 Throwable 类中的 getStackTrace 方法, + // 后续会利用这个方法来获取异常发生时的调用栈信息,以便确定日志相关的位置信息等,初始化为 null,在静态代码块中进行初始化赋值。 + private static Method getStackTraceMethod; + // 通过反射获取的用于获取类名的方法对象,对应 java.lang.StackTraceElement 类中的 getClassName 方法, + // 用于从堆栈跟踪元素中提取类名信息,同样初始化为 null,在静态代码块里完成初始化。 + private static Method getClassNameMethod; + // 通过反射获取的用于获取方法名的方法对象,对应 java.lang.StackTraceElement 类中的 getMethodName 方法, + // 用来从堆栈跟踪元素里获取具体执行的方法名称,初始值为 null,在静态块中初始化。 + private static Method getMethodNameMethod; + // 通过反射获取的用于获取文件名的方法对象,对应 java.lang.StackTraceElement 类中的 getFileName 方法, + // 可以从堆栈跟踪元素中获取对应的源文件名信息,初始设置为 null,后续在静态代码块里赋值。 + private static Method getFileNameMethod; + // 通过反射获取的用于获取行号的方法对象,对应 java.lang.StackTraceElement 类中的 getLineNumber 方法, + // 目的是从堆栈跟踪元素中获取代码执行到的具体行号信息,开始时为 null,在静态代码块中进行初始化操作。 + private static Method getLineNumberMethod; + + // 静态代码块,在类加载时执行,主要用于通过反射机制获取一些后续用于处理堆栈跟踪信息相关的方法对象, + // 如果获取过程中出现异常(比如类不存在或者方法不存在等情况),会打印相应的提示信息,表示将使用 JDK 1.4 之前的方式来确定位置信息(虽然这里并没有展示具体是什么替代方式,可能由具体子类去处理或者在其他相关逻辑中体现)。 + static { + try { + // 创建一个空的 Class 数组,表示无参数的情况,用于后续获取方法时指定参数类型,因为这里要获取的几个方法都是无参数的方法。 + Class[] noArgs = null; + // 通过反射获取 Throwable 类的 getStackTrace 方法对象,该方法用于获取异常的堆栈跟踪信息,后续可以基于这个信息进一步提取类、方法、文件、行号等详细信息, + getStackTraceMethod = Throwable.class.getMethod("getStackTrace", noArgs); + // 通过反射获取 java.lang.StackTraceElement 类的 Class 对象,因为后续要获取这个类中的几个方法对象,所以先获取其 Class 类型。 + Class stackTraceElementClass = Class.forName("java.lang.StackTraceElement"); + // 通过反射获取 StackTraceElement 类的 getClassName 方法对象,用于从堆栈跟踪元素中获取类名信息。 + getClassNameMethod = stackTraceElementClass.getMethod("getClassName", noArgs); + // 通过反射获取 StackTraceElement 类的 getMethodName 方法对象,以便从堆栈跟踪元素中获取方法名信息。 + getMethodNameMethod = stackTraceElementClass.getMethod("getMethodName", noArgs); + // 通过反射获取 StackTraceElement 类的 getFileName 方法对象,用于从堆栈跟踪元素中获取文件名信息。 + getFileNameMethod = stackTraceElementClass.getMethod("getFileName", noArgs); + // 通过反射获取 StackTraceElement 类的 getLineNumber 方法对象,使得可以从堆栈跟踪元素中获取行号信息。 + getLineNumberMethod = stackTraceElementClass.getMethod("getLineNumber", noArgs); + } catch (ClassNotFoundException ex) { + // 如果出现 ClassNotFoundException 异常,意味着找不到对应的类(比如这里的 StackTraceElement 类在某些特殊环境下不存在或者加载失败), + // 则打印提示信息,表示将使用 JDK 1.4 之前的方法来确定位置信息,这里只是简单打印提示,具体的替代逻辑可能在其他地方或者由子类去实现。 + LogDebug.debug("will use pre-JDK 1.4 methods to determine location."); + } catch (NoSuchMethodException ex) { + // 如果出现 NoSuchMethodException 异常,说明要获取的某个方法(比如上面获取的几个 StackTraceElement 类中的方法)不存在, + // 同样打印提示信息,表示将使用 JDK 1.4 之前的方法来确定位置信息,后续具体怎么处理依赖于具体的业务逻辑或者子类实现。 + LogDebug.debug("will use pre-JDK 1.4 methods to determine location."); + } + } + + /** + * 获得指定class的StackTraceElement + * 该方法用于根据传入的异常对象(t)以及调用类的全限定名(fqnOfCallingClass),从异常的堆栈跟踪信息中查找并提取出与指定调用类相关的堆栈跟踪元素(StackTraceElement), + * 如果成功获取到相关信息,则基于反射调用相应方法来构建并返回一个包含类名、方法名、文件名和行号的 StackTraceElement 对象; + * 如果在获取过程中出现反射相关的异常(比如非法访问、调用目标异常等情况),会进行相应的错误处理(比如打印调试信息、处理中断相关情况等),最后返回一个默认的 StackTraceElement 对象。 + * + * @param t 异常对象,通常是在代码执行出现异常时传递进来,用于获取其相关的堆栈跟踪信息。 + * @param fqnOfCallingClass 调用类的全限定名,用于在堆栈跟踪信息中定位到特定类相关的信息,通过比对类名来找到对应的堆栈跟踪元素。 + * + * @return 返回一个 StackTraceElement 对象,包含了与指定调用类相关的类名、方法名、文件名和行号等信息,如果获取失败则返回默认构建的 StackTraceElement 对象。 + */ + protected StackTraceElement getRunningStackTrace(Throwable t, String fqnOfCallingClass) { + if (getLineNumberMethod!= null) { + try { + // 创建一个空的 Object 数组,表示无参数的情况,用于后续通过反射调用方法时作为参数传递,因为这里要调用的几个方法都是无参数的方法。 + Object[] noArgs = null; + // 通过反射调用异常对象(t)的 getStackTrace 方法,获取其堆栈跟踪信息,返回的是一个 Object 数组(实际上是 StackTraceElement 数组,但通过反射获取时以 Object 数组形式返回), + Object[] elements = (Object[]) getStackTraceMethod.invoke(t, noArgs); + // 从后往前遍历堆栈跟踪元素数组,因为通常最新的调用信息在数组的末尾,通过倒序遍历可以更快地找到与指定调用类相关的元素。 + for (int i = elements.length - 1; i >= 0; i--) { + // 通过反射调用当前堆栈跟踪元素(elements[i])的 getClassName 方法,获取其类名信息,并转换为 String 类型,用于与传入的调用类全限定名进行比对。 + String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs); + if (fqnOfCallingClass.equals(thisClass)) { + // 如果找到了与传入的调用类全限定名匹配的堆栈跟踪元素,则获取其相关信息来构建一个 StackTraceElement 对象并返回。 + + // 获取类名,这里直接使用传入的调用类全限定名(fqnOfCallingClass)作为类名,也可以根据实际情况进行进一步处理,比如提取简单类名等,不过这里直接使用了传入的全限定名。 + String className = fqnOfCallingClass; + // 通过反射调用当前堆栈跟踪元素(elements[i])的 getMethodName 方法,获取其方法名信息,并转换为 String 类型,用于设置到要返回的 StackTraceElement 对象中。 + String methodName = (String) getMethodNameMethod.invoke(elements[i], noArgs); + // 通过反射调用当前堆栈跟踪元素(elements[i])的 getFileName 方法,获取其文件名信息,并转换为 String 类型,用于设置到要返回的 StackTraceElement 对象中。 + String fileName = (String) getFileNameMethod.invoke(elements[i], noArgs); + // 通过反射调用当前堆栈跟踪元素(elements[i])的 getLineNumber 方法,获取其行号信息,先转换为 Integer 类型,再获取其 int 值,用于设置到要返回的 StackTraceElement 对象中。 + int lineNumber = ((Integer) getLineNumberMethod.invoke(elements[i], noArgs)).intValue(); + return new StackTraceElement(className, methodName, fileName, lineNumber); + } + } + } catch (IllegalAccessException ex) { + // 如果出现 IllegalAccessException 异常,说明在通过反射调用方法时发生了非法访问的情况(比如方法不可访问等原因), + // 则打印调试信息,表示使用 JDK 1.4 相关方法失败,并将异常信息传递进去,方便后续排查问题,不过这里只是简单打印调试信息,具体的处理逻辑可能可以进一步完善。 + LogDebug.debug("failed using JDK 1.4 methods", ex); + } catch (InvocationTargetException ex) { + // 如果出现 InvocationTargetException 异常,说明在通过反射调用方法时,被调用方法内部抛出了异常,需要进一步判断这个内部抛出的异常类型。 + if (ex.getTargetException() instanceof InterruptedException + || ex.getTargetException() instanceof InterruptedIOException) { + // 如果内部抛出的异常是 InterruptedException 或者 InterruptedIOException,表示线程被中断相关的情况, + // 则重新设置当前线程的中断状态,以便让调用者知道线程被中断了,后续可以根据这个中断状态进行相应的处理,符合 Java 中处理线程中断的规范。 + Thread.currentThread().interrupt(); + } + // 无论内部抛出的异常具体是什么类型(除了上面特殊处理的中断相关异常外),都打印调试信息,表示使用 JDK 1.4 相关方法失败,并将异常信息传递进去,方便后续排查问题。 + LogDebug.debug("failed using JDK 1.4 methods", ex); + } catch (RuntimeException ex) { + // 如果出现 RuntimeException 异常,同样打印调试信息,表示使用 JDK 1.4 相关方法失败,并将异常信息传递进去,方便后续排查问题, + // 这里统一处理了各种运行时异常情况,保证方法在出现异常时能进行一定的错误提示,避免程序因为未处理的异常而崩溃。 + LogDebug.debug("failed using JDK 1.4 methods", ex); + } + } + // 如果在获取指定的 StackTraceElement 过程中出现了各种问题(比如反射方法获取失败或者在提取信息时出现异常等情况),则调用 createDefaultStackTrace 方法创建并返回一个默认的 StackTraceElement 对象。 + return this.createDefaultStackTrace(); + } + + /** + * 创建默认StackTraceElement + * 该方法用于创建一个默认的 StackTraceElement 对象,当无法从异常的堆栈跟踪信息中正确获取到与指定调用类相关的堆栈跟踪元素时使用, + * 返回的 StackTraceElement 对象的类名设置为当前类(this.getClass().getName()),方法名设置为 "log",文件名也设置为当前类名(this.getClass().getName()),行号设置为 0, + * 作为一种默认的占位或者表示错误情况的堆栈跟踪元素返回,具体使用场景取决于业务逻辑对堆栈跟踪信息的处理需求。 + * + * @return 返回一个默认构建的 StackTraceElement 对象,包含了默认设置的类名、方法名、文件名和行号信息。 + */ + private StackTraceElement createDefaultStackTrace() { + return new StackTraceElement(this.getClass().getName(), "log", this.getClass().getName(), 0); + } + + // 以下是实现 LogHandler 接口的各个日志处理方法,在当前抽象类中这些方法体为空,因为具体的日志处理逻辑(比如将日志信息输出到哪里、如何格式化等)需要由具体的子类根据不同的业务需求去实现, + // 这里只是定义了方法签名,遵循了接口的规范,保证了抽象类作为日志处理的基础框架,子类可以针对性地重写这些方法来完成实际的日志处理操作。 + + @Override + public void info(String msg, String fqnOfCallingClass) { + } + + @Override + public void info(String msg, Throwable t, String fqnOfCallingClass) { + } + + @Override + public void error(String msg, String fqnOfCallingClass) { + } + + @Override + public void error(String msg, Throwable t, String fqnOfCallingClass) { + } + + @Override + public void debug(String msg, String fqnOfCallingClass) { + } + + @Override + public void debug(String msg, Throwable t, String fqnOfCallingClass) { + } + + @Override + public void warning(String msg, String fqnOfCallingClass) { + } + + @Override + public void warning(String msg, Throwable t, String fqnOfCallingClass) { + } +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/DateUtils.java b/tamguo/src/main/java/com/tamguo/util/DateUtils.java index 0c65c5f..219ebc9 100644 --- a/tamguo/src/main/java/com/tamguo/util/DateUtils.java +++ b/tamguo/src/main/java/com/tamguo/util/DateUtils.java @@ -5,51 +5,86 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +// 这个类提供了一系列与日期处理相关的实用方法,用于日期的格式化、解析、日期差值计算以及日期增减等操作,方便在项目中对日期进行各种常见的处理。 public class DateUtils { + + // 定义默认的日期格式字符串,格式为 "yyyy-MM-dd",用于在一些方法中作为默认的日期格式化样式,方便统一处理日期的显示格式。 public static final String DEFAULT_PATTERN = "yyyy-MM-dd"; + /** + * 获取相对于当前日期偏移指定天数后的日期字符串 + * 根据传入的天数参数(day),在当前日期基础上进行偏移,然后按照指定的日期格式(pattern)将偏移后的日期格式化为字符串返回。 + * 如果未传入日期格式参数,则使用默认格式(DEFAULT_PATTERN)进行格式化。 + * + * @param day 要偏移的天数,正数表示往后的日期,负数表示往前的日期。 + * @param pattern 日期格式字符串,用于指定返回日期的格式化样式,例如 "yyyy-MM-dd HH:mm:ss" 等。 + * @return 按照指定格式格式化后的日期字符串,表示偏移指定天数后的日期。 + */ public static String getOneDayFromNow(int day, String pattern) { + // 获取一个 Calendar 实例,它用于对日期进行各种操作,比如日期的增减、获取日期的各个字段等,这里获取的是当前系统时间对应的 Calendar 实例。 Calendar cal = Calendar.getInstance(); + // 将 Calendar 的时间设置为当前日期(通过传入一个 Date 对象表示当前时间,这里直接使用 new Date() 获取当前时间),以便后续在此基础上进行日期偏移操作。 cal.setTime(new Date()); + // 在当前日期基础上增加或减少指定的天数,通过调用 add 方法并指定 Calendar.DAY_OF_MONTH 字段以及要偏移的天数(day)来实现日期的偏移操作。 cal.add(Calendar.DAY_OF_MONTH, day); + // 创建一个 SimpleDateFormat 对象,用于按照指定的格式(pattern)对日期进行格式化操作,将日期转换为字符串形式。 SimpleDateFormat sdf = new SimpleDateFormat(pattern); + // 使用 SimpleDateFormat 的 format 方法将偏移后的日期(cal.getTime() 返回的是一个 Date 对象,表示偏移后的日期时间)格式化为字符串,并返回该格式化后的日期字符串。 return sdf.format(cal.getTime()); } + /** + * 获取相对于当前日期偏移指定天数后的日期字符串(使用默认格式) + * 该方法重载了 getOneDayFromNow 方法,当只传入要偏移的天数(day)参数时,会调用另一个 getOneDayFromNow 方法,并使用默认的日期格式(DEFAULT_PATTERN)进行日期格式化,返回相应的日期字符串。 + * + * @param day 要偏移的天数,正数表示往后的日期,负数表示往前的日期。 + * @return 按照默认格式("yyyy-MM-dd")格式化后的日期字符串,表示偏移指定天数后的日期。 + */ public static String getOneDayFromNow(int day) { return DateUtils.getOneDayFromNow(day, DEFAULT_PATTERN); } /** * 计算两个日期之间相差的天数 - * - * @param smdate - * 较小的时间 - * @param bdate - * 较大的时间 - * @return 相差天数 - * @throws ParseException - * @throws java.text.ParseException + * 通过将两个日期先按照指定格式("yyyy-MM-dd")进行格式化并解析为 Date 对象,然后获取它们对应的时间戳(以毫秒为单位), + * 计算时间戳差值并转换为天数,最后返回相差的天数。需要注意的是,传入的两个日期参数(smdate 和 bdate)需要确保一个是较小时间,一个是较大时间,否则结果可能不符合预期。 + * + * @param smdate 较小的时间,一般是较早的日期,类型为 Date。 + * @param bdate 较大的时间,通常是较晚的日期,类型为 Date。 + * @return 两个日期之间相差的天数,返回值为整数类型。 + * @throws ParseException 如果在日期格式化或解析过程中出现错误,会抛出此异常,调用者需要进行相应的异常处理。 */ - public static int daysBetween(Date smdate, Date bdate) - throws ParseException { + public static int daysBetween(Date smdate, Date bdate) throws ParseException { + // 创建一个 SimpleDateFormat 对象,指定日期格式为 "yyyy-MM-dd",用于统一将传入的日期格式化为该格式后再进行后续的时间戳计算等操作,确保日期格式的一致性。 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + // 先将传入的较小日期(smdate)按照指定格式进行格式化,再解析为 Date 对象,这样做的目的是去除日期中的时间部分(如果有的话),只保留日期部分,保证计算天数的准确性。 smdate = sdf.parse(sdf.format(smdate)); + // 同样地,对较大日期(bdate)进行格式化和解析操作,去除时间部分,只保留日期部分,以便后续基于日期进行天数差值的计算。 bdate = sdf.parse(sdf.format(bdate)); + // 获取一个 Calendar 实例,用于获取日期对应的时间戳等操作,这里以较小日期(smdate)为基础进行设置。 Calendar cal = Calendar.getInstance(); cal.setTime(smdate); + // 获取较小日期对应的时间戳(以毫秒为单位),通过 Calendar 的getTimeInMillis 方法获取,表示从 1970 年 1 月 1 日 00:00:00 UTC 到该日期时间的毫秒数。 long time1 = cal.getTimeInMillis(); + // 重新设置 Calendar 的时间为较大日期(bdate),以便获取其对应的时间戳。 cal.setTime(bdate); + // 获取较大日期对应的时间戳(以毫秒为单位)。 long time2 = cal.getTimeInMillis(); + // 计算两个日期时间戳的差值(以毫秒为单位),然后除以一天对应的毫秒数(1000 * 3600 * 24,即 1000 毫秒/秒 * 3600 秒/小时 * 24 小时/天),得到相差的天数,这里差值是一个 long 类型。 long between_days = (time2 - time1) / (1000 * 3600 * 24); + // 将计算得到的相差天数(long 类型)转换为整数类型并返回,通过将 long 值转换为 String 再解析为 Integer 的方式实现,虽然这种方式略显繁琐,但能确保类型转换的正确性。 return Integer.parseInt(String.valueOf(between_days)); } + // main 方法,用于简单测试 daysBetween 方法,不过这里传入的参数都是 null,实际运行时会抛出 NullPointerException, + // 正常使用时应该传入有效的 Date 对象参数进行测试或者在其他合适的地方调用 daysBetween 方法并传入正确的日期参数来计算日期差值。 public static void main(String[] args) throws ParseException { System.out.println(daysBetween(null, null)); } + // 定义一系列日期格式的常量字符串,方便在不同的日期格式化场景中直接使用,涵盖了英文简写、英文全称、精确到毫秒的完整时间以及对应的中文简写、中文全称、精确到毫秒的完整中文时间等多种格式。 + /** * 英文简写(默认)如:2010-12-01 */ @@ -77,6 +112,7 @@ public class DateUtils { /** * 获得默认的 date pattern + * 返回默认的日期格式字符串,当前设置为 FORMAT_LONG(即 "yyyy-MM-dd HH:mm:ss"),用于在一些需要使用默认格式的方法中获取默认的格式化样式。 */ public static String getDatePattern() { return FORMAT_LONG; @@ -84,8 +120,9 @@ public class DateUtils { /** * 根据预设格式返回当前日期 - * - * @return + * 使用默认的日期格式(通过 getDatePattern 方法获取,当前为 "yyyy-MM-dd HH:mm:ss")对当前日期(new Date() 获取当前时间)进行格式化,并返回格式化后的日期字符串。 + * + * @return 当前日期按照默认格式格式化后的字符串。 */ public static String getNow() { return format(new Date()); @@ -93,9 +130,10 @@ public class DateUtils { /** * 根据用户格式返回当前日期 - * - * @param format - * @return + * 根据传入的用户指定的日期格式(format)对当前日期(new Date() 获取当前时间)进行格式化,并返回格式化后的日期字符串,方便按照用户需求的格式展示当前日期。 + * + * @param format 用户指定的日期格式字符串,例如 "yyyy-MM-dd"、"yyyy年MM月dd日" 等。 + * @return 当前日期按照用户指定格式格式化后的字符串。 */ public static String getNow(String format) { return format(new Date(), format); @@ -103,9 +141,11 @@ public class DateUtils { /** * 使用预设格式格式化日期 - * - * @param date - * @return + * 使用默认的日期格式(通过 getDatePattern 方法获取,当前为 "yyyy-MM-dd HH:mm:ss")对传入的日期(date)进行格式化,并返回格式化后的日期字符串, + * 如果传入的日期为 null,则返回空字符串。 + * + * @param date 要格式化的日期对象,类型为 Date,可以为 null。 + * @return 按照默认格式格式化后的日期字符串,如果 date 为 null,则返回空字符串。 */ public static String format(Date date) { return format(date, getDatePattern()); @@ -113,17 +153,19 @@ public class DateUtils { /** * 使用用户格式格式化日期 - * - * @param date - * 日期 - * @param pattern - * 日期格式 - * @return + * 根据传入的用户指定的日期格式(pattern)对传入的日期(date)进行格式化操作,将日期转换为对应的字符串形式, + * 如果传入的日期为 null,则返回空字符串,确保方法的健壮性,避免出现空指针异常等情况。 + * + * @param date 要格式化的日期对象,类型为 Date,可以为 null。 + * @param pattern 用户指定的日期格式字符串,用于控制日期的格式化样式。 + * @return 按照用户指定格式格式化后的日期字符串,如果 date 为 null,则返回空字符串。 */ public static String format(Date date, String pattern) { String returnValue = ""; - if (date != null) { + if (date!= null) { + // 创建一个 SimpleDateFormat 对象,按照用户指定的格式(pattern)进行初始化,用于对日期进行格式化操作。 SimpleDateFormat df = new SimpleDateFormat(pattern); + // 使用 SimpleDateFormat 的 format 方法将日期(date)格式化为字符串,并赋值给 returnValue,以便后续返回。 returnValue = df.format(date); } return (returnValue); @@ -131,12 +173,12 @@ public class DateUtils { /** * 使用用户格式格式化时间戳 - * - * @param timestamp - * 时间戳 - * @param pattern - * 日期格式 - * @return + * 将传入的时间戳字符串(timestamp)解析为对应的日期对象(通过将时间戳转换为 long 类型后作为参数构造 Date 对象), + * 然后按照用户指定的日期格式(pattern)对该日期对象进行格式化操作,返回格式化后的日期字符串,方便将时间戳按照指定格式展示为日期时间形式。 + * + * @param timestamp 时间戳字符串,一般是表示从 1970 年 1 月 1 日 00:00:00 UTC 到某个时间点的毫秒数对应的字符串形式,例如 "1612345678901" 等。 + * @param pattern 用户指定的日期格式字符串,用于控制格式化后的日期时间样式。 + * @return 按照用户指定格式格式化后的日期字符串,将时间戳对应的日期时间按照指定格式进行展示。 */ public static String format(String timestamp, String pattern) { SimpleDateFormat sdf = new SimpleDateFormat(pattern); @@ -145,10 +187,11 @@ public class DateUtils { /** * 使用预设格式提取字符串日期 - * - * @param strDate - * 日期字符串 - * @return + * 将传入的日期字符串(strDate)按照默认的日期格式(通过 getDatePattern 方法获取,当前为 "yyyy-MM-dd HH:mm:ss")进行解析, + * 尝试将其转换为对应的 Date 对象并返回,如果解析过程中出现 ParseException 异常,则打印异常堆栈信息,并返回 null,表示解析失败。 + * + * @param strDate 要解析的日期字符串,需要符合默认的日期格式要求,否则可能解析失败。 + * @return 解析后的 Date 对象,如果解析失败则返回 null。 */ public static Date parse(String strDate) { return parse(strDate, getDatePattern()); @@ -156,12 +199,12 @@ public class DateUtils { /** * 使用用户格式提取字符串日期 - * - * @param strDate - * 日期字符串 - * @param pattern - * 日期格式 - * @return + * 根据传入的用户指定的日期格式(pattern)对传入的日期字符串(strDate)进行解析操作,尝试将其转换为对应的 Date 对象并返回, + * 如果在解析过程中出现 ParseException 异常,则打印异常堆栈信息,并返回 null,表示解析失败,调用者可以根据返回值判断解析是否成功并进行相应处理。 + * + * @param strDate 要解析的日期字符串,需要符合用户指定的日期格式要求,否则可能解析失败。 + * @param pattern 用户指定的日期格式字符串,用于控制解析的规则。 + * @return 解析后的 Date 对象,如果解析失败则返回 null。 */ public static Date parse(String strDate, String pattern) { SimpleDateFormat df = new SimpleDateFormat(pattern); @@ -175,12 +218,11 @@ public class DateUtils { /** * 在日期上增加数个整月 - * - * @param date - * 日期 - * @param n - * 要增加的月数 - * @return + * 通过 Calendar 实例对传入的日期(date)进行操作,在原日期基础上增加指定的月数(n),然后返回增加月数后的日期对象,方便进行日期的月份偏移操作。 + * + * @param date 要进行月份增加操作的日期对象,类型为 Date。 + * @param n 要增加的月数,正数表示往后增加的月数,负数表示往前减少的月数。 + * @return 增加指定月数后的日期对象。 */ public static Date addMonth(Date date, int n) { Calendar cal = Calendar.getInstance(); @@ -191,12 +233,11 @@ public class DateUtils { /** * 在日期上增加天数 - * - * @param date - * 日期 - * @param n - * 要增加的天数 - * @return + * 利用 Calendar 实例对传入的日期(date)进行操作,在原日期基础上增加指定的天数(n),然后返回增加天数后的日期对象,实现日期的天数偏移功能。 + * + * @param date 要进行天数增加操作的日期对象,类型为 Date。 + * @param n 要增加的天数,正数表示往后增加的天数,负数表示往前减少的天数。 + * @return 增加指定天数后的日期对象。 */ public static Date addDay(Date date, int n) { Calendar cal = Calendar.getInstance(); @@ -207,6 +248,8 @@ public class DateUtils { /** * 获取时间戳 + * 使用指定的日期格式(FORMAT_FULL,即 "yyyy-MM-dd HH:mm:ss.S")对当前时间(通过 Calendar.getInstance 获取当前系统时间对应的 Calendar 实例,再获取其对应的 Date 对象)进行格式化, + * 返回格式化后的日期时间字符串,该字符串实际上也可以看作是一种包含了精确到毫秒的时间戳信息的表示形式,方便在一些场景下记录精确的时间点。 */ public static String getTimeString() { SimpleDateFormat df = new SimpleDateFormat(FORMAT_FULL); @@ -216,59 +259,17 @@ public class DateUtils { /** * 获取日期年份 - * - * @param date - * 日期 - * @return + * 先将传入的日期(date)按照默认格式(通过 format 方法使用默认格式进行格式化)进行格式化,然后提取格式化后的字符串的前 4 个字符, + * 即获取表示年份的部分并返回,用于获取日期对应的年份信息,不过这种方式依赖于默认格式中年份处于字符串开头且长度为 4 位的前提条件。 + * + * @param date 要获取年份信息的日期对象,类型为 Date。 + * @return 日期对应的年份字符串,例如 "2024" 等。 */ public static String getYear(Date date) { return format(date).substring(0, 4); } - /** - * 按默认格式的字符串距离今天的天数 - * - * @param date - * 日期字符串 - * @return - */ - public static int countDays(String date) { - long t = Calendar.getInstance().getTime().getTime(); - Calendar c = Calendar.getInstance(); - c.setTime(parse(date)); - long t1 = c.getTime().getTime(); - return (int) (t / 1000 - t1 / 1000) / 3600 / 24; - } - - /** - * 按用户格式字符串距离今天的天数 - * - * @param date - * 日期字符串 - * @param format - * 日期格式 - * @return - */ - public static int countDays(String date, String format) { - long t = Calendar.getInstance().getTime().getTime(); - Calendar c = Calendar.getInstance(); - c.setTime(parse(date, format)); - long t1 = c.getTime().getTime(); - return (int) (t / 1000 - t1 / 1000) / 3600 / 24; - } - - public static String timeFormat(Date date, String format, Boolean flag, int beforeDay, int nowDay) { - if(date == null) { - date = new Date(); - } - Calendar cal = Calendar.getInstance(); - cal.setTime(date); - if(flag) { - cal.add(Calendar.DATE,-30); - } else { - cal.add(Calendar.DATE,0); - } - SimpleDateFormat sdf = new SimpleDateFormat(format); - return sdf.format(cal.getTime()); - } -} +/** + * 按默认格式的字符串距离今天的天数 + * 通过获取当前日期的时间戳(以毫秒为单位)以及将传入的日期字符串(date,需要符合默认格式要求)解析为日期对象后获取其时间戳, + * 计算两个时间戳的差值(转换为秒后再换算为天数) \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/IdGen.java b/tamguo/src/main/java/com/tamguo/util/IdGen.java index 36915e4..7ce214e 100644 --- a/tamguo/src/main/java/com/tamguo/util/IdGen.java +++ b/tamguo/src/main/java/com/tamguo/util/IdGen.java @@ -1,56 +1,86 @@ package com.tamguo.util; -public class IdGen -{ +// IdGen类用于生成唯一标识符(ID),很可能基于某种分布式唯一ID生成算法(从代码结构推测可能类似雪花算法的思路), +// 它可以根据配置的参数(如工作节点ID、数据中心ID等)以及一些规则来生成具有唯一性的ID,并且支持添加自定义后缀以及指定后缀位置等功能。 +public class IdGen { + + // 工作节点ID,用于在分布式环境中区分不同的工作节点,不同的工作节点生成的ID应该具有唯一性,通过位运算等规则来限定其取值范围。 private long workerId; + // 数据中心ID,用于区分不同的数据中心,同样在分布式系统架构里参与ID生成的唯一性保证,也有相应的取值范围限制。 private long datacenterId; + // 序列号,用于在同一毫秒内对生成的ID进行区分,在同一时间(同一毫秒)内,不同的序列号保证生成的ID各不相同,初始值为0。 private long sequence = 0L; + // 起始时间戳(单位:毫秒),通常是一个固定的历史时间点,作为ID生成算法中的时间基线,用于计算时间戳差值等操作,以保证生成的ID在时间维度上的唯一性和有序性。 private long twepoch = 1288834974657L; + // 用于表示 workerId 所占的位数,通过这些位数设定来确定 workerId 的取值范围,当前设置为5位。 private long workerIdBits = 5L; + // 表示 datacenterId 所占的位数,以此限定 datacenterId 的取值范围,同样设置为5位。 private long datacenterIdBits = 5L; + // 根据 workerIdBits 计算出的 workerId 的最大允许值,通过位运算得出,确保 workerId 在合理的取值范围内,避免超出导致ID生成出现问题。 private long maxWorkerId = -1L ^ (-1L << workerIdBits); + // 依据 datacenterIdBits 计算得到的 datacenterId 的最大取值,通过位运算来限定其范围,保证 datacenterId 的合法性。 private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + // 序列号所占的位数,决定了在同一毫秒内可生成的不同ID的数量,这里设置为12位。 private long sequenceBits = 12L; + // 在生成ID时,用于将 sequence(序列号)左移的位数,其值等于 sequenceBits,用于在ID的二进制表示中为 sequence 预留合适的位置。 private long workerIdShift = sequenceBits; + // 在生成ID时,用于将 datacenterId 左移的位数,计算方式是 sequenceBits 与 workerIdBits 之和,确定 datacenterId 在ID二进制表示中的位置。 private long datacenterIdShift = sequenceBits + workerIdBits; + // 在生成ID时,用于将时间戳(timestamp)左移的位数,通过 sequenceBits、workerIdBits 和 datacenterIdBits 三者相加得出,为时间戳在ID二进制表示中预留正确的位置。 private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + // 序列号的掩码,通过位运算生成,用于在同一毫秒内对 sequence 进行循环计数时的边界判断,保证序列号在规定的位数范围内循环使用。 private long sequenceMask = -1L ^ (-1L << sequenceBits); + // 记录上一次生成ID时的时间戳(单位:毫秒),用于和当前时间戳对比,判断是否在同一毫秒内以及处理时间回拨等情况,初始值为 -1L。 private long lastTimestamp = -1L; + // 自定义的后缀字符串,可用于给生成的ID添加额外的标识信息,方便在业务中根据后缀进行分类、识别等操作,初始值为 null。 private String suffix; + // 布尔值,用于指示后缀添加的位置,true 表示添加在ID的前缀位置,false 表示添加在ID的后缀位置,初始值为 false。 private boolean flag; + // 内部静态类,用于实现单例模式,保证整个应用程序中只有一个 IdGen 实例(如果使用默认无参构造函数获取实例的情况), + // 通过类加载机制保证线程安全地创建和获取这个唯一实例,避免多次实例化造成的资源浪费以及保证ID生成规则的一致性。 private static class IdGenHolder { + // 创建并持有一个静态的最终 IdGen 实例,在类加载时就会初始化这个实例,并且只会初始化一次,后续通过 get 方法获取的都是这个唯一实例。 private static final IdGen instance = new IdGen(); } - public static IdGen get(){ + // 静态方法,用于获取单例的 IdGen 实例(使用默认配置的情况),外部类可以通过这个方法方便地获取到用于生成ID的唯一实例,遵循单例模式的获取实例方式。 + public static IdGen get() { return IdGenHolder.instance; } /** * 获取字符串拼接id - * @param suffix 字符串 - * @param flag true:前缀 false:后缀 - * @return + * 该静态重载方法用于获取一个带有自定义后缀的 IdGen 实例,根据传入的后缀字符串(suffix)以及后缀添加位置标志(flag)来决定如何生成带有后缀的ID, + * 如果后缀字符串为空(null 或者去除空格后长度为0),则返回默认的单例实例(即不带后缀的情况),否则创建一个新的 IdGen 实例并传入后缀和标志信息,用于后续生成带后缀的ID。 + * + * @param suffix 字符串,要添加到生成ID上的后缀内容,如果为空则不添加后缀,使用默认的ID生成方式。 + * @param flag true表示将后缀添加在ID的前缀位置,false表示添加在ID的后缀位置,用于控制后缀在生成ID中的位置。 + * @return 返回一个 IdGen 实例,如果后缀为空则返回默认单例实例,否则返回带有指定后缀及后缀位置配置的新实例。 */ - public static IdGen get(String suffix,boolean flag){ - if(suffix == null || suffix.trim().length() == 0) + public static IdGen get(String suffix, boolean flag) { + if (suffix == null || suffix.trim().length() == 0) return IdGenHolder.instance; - else{ - return new IdGen(suffix,flag); + else { + return new IdGen(suffix, flag); } } - + // 默认的无参构造函数,调用另一个带有两个参数(均为0L)的构造函数来初始化对象,给 workerId 和 datacenterId 都赋予初始值0,适用于一些简单场景或者后续有默认配置的情况。 public IdGen() { this(0L, 0L); } - public IdGen(String suffix,boolean flag){ + // 构造函数,用于创建一个带有自定义后缀和后缀位置标志的 IdGen 实例,接收后缀字符串(suffix)和后缀位置标志(flag)作为参数, + // 将传入的参数赋值给相应的成员变量,以便后续在生成ID时根据这些配置来添加后缀。 + public IdGen(String suffix, boolean flag) { this.suffix = suffix; this.flag = flag; } + // 构造函数,用于创建一个 IdGen 实例并传入工作节点ID(workerId)和数据中心ID(datacenterId)参数, + // 在创建实例时会对传入的 workerId 和 datacenterId 进行合法性检查,确保它们在预先设定的取值范围内(通过与最大允许值比较以及判断是否小于0来验证), + // 如果超出范围则抛出 IllegalArgumentException 异常,保证ID生成参数的有效性,避免生成不符合规则的ID。 public IdGen(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); @@ -62,28 +92,57 @@ public class IdGen this.datacenterId = datacenterId; } + /** + * 生成下一个唯一ID + * 这是整个类的核心方法,用于生成下一个唯一的ID,其实现过程遵循了类似分布式ID生成算法(如雪花算法)的思路,综合考虑了时间戳、工作节点ID、数据中心ID以及序列号等因素, + * 在生成过程中会处理时间戳回拨等异常情况,确保生成的ID在分布式环境下的唯一性、有序性以及符合设定的格式要求(包含后缀处理等情况),最后以字符串形式返回生成的ID。 + * + * @return 返回生成的唯一ID字符串,可能带有自定义后缀(根据构造函数传入的配置决定),用于在业务中作为唯一标识使用,例如数据库主键等场景。 + */ public synchronized String nextId() { + // 获取当前时间戳(单位:毫秒),通过调用 timeGen 方法来获取,这个时间戳是生成ID的重要依据之一,用于区分不同时间生成的ID,保证时间维度上的唯一性。 long timestamp = timeGen(); + // 判断当前时间戳是否小于上一次生成ID的时间戳(lastTimestamp),如果小于则说明出现了时间回拨的情况(比如系统时间被手动调整回退了), + // 这种情况下为了保证ID的唯一性和时间顺序性,会抛出 RuntimeException 异常,拒绝生成ID,因为时间回拨可能导致ID重复等问题。 if (timestamp < lastTimestamp) { throw new RuntimeException(String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } + // 如果当前时间戳与上一次生成ID的时间戳相等,说明在同一毫秒内,需要对序列号(sequence)进行处理,通过自增并与 sequenceMask 进行按位与操作, + // 实现序列号在规定的位数范围内循环递增(保证在同一毫秒内不同调用生成不同的ID),如果序列号达到最大值(sequenceMask 对应的最大值,即循环一轮回到0), + // 则需要等待到下一个毫秒再生成ID,通过调用 tilNextMillis 方法来获取下一个可用的时间戳。 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { + // 如果当前时间戳与上一次的不同,说明进入了新的一毫秒,此时将序列号重置为0,重新开始在这一毫秒内的ID生成计数。 sequence = 0L; } + // 更新 lastTimestamp 为当前的时间戳,为下一次生成ID时判断时间戳情况做准备,确保每次生成ID都能正确处理时间相关的逻辑。 lastTimestamp = timestamp; + // 通过位运算组合时间戳、数据中心ID、工作节点ID和序列号等信息,构建出一个唯一的长整型数值(serialNumber),这个数值就是ID的核心部分, + // 其位运算的顺序和位移的位数都是根据前面定义的各个参数(如 timestampLeftShift、datacenterIdShift、workerIdShift 等)来确定的,保证了各个部分在ID中的正确位置和唯一性体现。 long serialNumber = ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; - return (suffix == null || suffix.trim().length() == 0) ? serialNumber+"" : (flag ? (new StringBuffer()).append(suffix).append(serialNumber).toString() : (new StringBuffer()).append(serialNumber).append(suffix).toString()); + // 根据是否有自定义后缀(suffix)以及后缀添加位置标志(flag)来决定最终返回的ID字符串形式, + // 如果后缀为空或者去除空格后长度为0,则直接将 serialNumber 转换为字符串返回; + // 如果 flag 为 true,表示后缀添加在前面,则通过 StringBuffer 先拼接后缀再拼接 serialNumber 并转换为字符串返回; + // 如果 flag 为 false,表示后缀添加在后面,则通过 StringBuffer 先拼接 serialNumber 再拼接后缀并转换为字符串返回,从而生成符合要求的带有后缀(或不带后缀)的唯一ID字符串。 + return (suffix == null || suffix.trim().length() == 0)? serialNumber + "" : (flag? (new StringBuffer()).append(suffix).append(serialNumber).toString() : (new StringBuffer()).append(serialNumber).append(suffix).toString()); } + /** + * 等待下一个毫秒 + * 该方法用于在序列号达到最大值(同一毫秒内ID生成达到上限)时,等待系统时间进入下一个毫秒,以获取新的可用时间戳来继续生成ID, + * 通过不断获取当前时间戳(调用 timeGen 方法)并与传入的上一次时间戳(lastTimestamp)比较,直到获取到大于 lastTimestamp 的时间戳,保证进入下一个不同的毫秒,然后返回这个新的时间戳。 + * + * @param lastTimestamp 上一次生成ID时的时间戳(单位:毫秒),用于作为判断是否进入下一个毫秒的参考基准,避免在同一毫秒内继续生成ID导致序列号溢出等问题。 + * @return 返回下一个可用的时间戳(单位:毫秒),即大于 lastTimestamp 的时间戳,用于后续在生成ID时作为新的时间依据,保证ID生成的顺序性和唯一性。 + */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { @@ -92,8 +151,14 @@ public class IdGen return timestamp; } + /** + * 获取当前时间戳 + * 该方法用于获取当前系统时间的时间戳(单位:毫秒),通过调用 System.currentTimeMillis 方法来获取,为ID生成过程中基于时间维度保证唯一性提供当前时间信息, + * 外部也可以通过继承该类并重写这个方法来实现自定义的时间获取逻辑(例如在测试等场景下模拟不同的时间情况),具有一定的扩展性。 + * + * @return 返回当前系统时间的时间戳(单位:毫秒),即从1970年1月1日00:00:00 UTC到当前时间的毫秒数,用于ID生成等时间相关的计算操作。 + */ protected long timeGen() { return System.currentTimeMillis(); } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/ObjectUtil.java b/tamguo/src/main/java/com/tamguo/util/ObjectUtil.java index 41e79a0..4ce1f0a 100644 --- a/tamguo/src/main/java/com/tamguo/util/ObjectUtil.java +++ b/tamguo/src/main/java/com/tamguo/util/ObjectUtil.java @@ -6,9 +6,21 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +// ObjectUtil类继承自SerializeTranscoder(这里假设SerializeTranscoder是一个与序列化、反序列化相关的抽象类或者接口,定义了一些序列化和反序列化相关的方法签名), +// 主要提供了对象序列化、反序列化以及对象相等性比较的实用功能,方便在需要处理对象的持久化存储、网络传输以及对象比较等场景中使用。 public class ObjectUtil extends SerializeTranscoder { + + /** + * 序列化对象方法 + * 将传入的对象(value)转换为字节数组,实现对象的序列化操作。如果传入的对象为 null,则会抛出 NullPointerException 异常, + * 因为无法对 null 对象进行序列化。在序列化过程中,若出现 I/O 异常(比如写入流时发生错误等情况),则会抛出 IllegalArgumentException 异常,并将原始异常信息一并传递出去,方便排查问题。 + * + * @param value 要进行序列化的对象,必须是非 null 的对象,其类型需实现 Serializable 接口(Java 序列化要求)才能正确序列化。 + * @return 返回序列化后的字节数组,该字节数组可以用于存储到文件、发送到网络等场景,以便后续进行反序列化还原对象。 + */ @Override public byte[] serialize(Object value) { + // 首先检查传入的对象是否为 null,如果是则抛出异常,提示不能序列化 null 对象。 if (value == null) { throw new NullPointerException("Can't serialize null"); } @@ -16,54 +28,98 @@ public class ObjectUtil extends SerializeTranscoder { ByteArrayOutputStream bos = null; ObjectOutputStream os = null; try { + // 创建一个 ByteArrayOutputStream 对象,它用于在内存中缓冲要序列化的数据,后续可以方便地转换为字节数组获取序列化后的内容。 bos = new ByteArrayOutputStream(); + // 创建一个 ObjectOutputStream 对象,它用于将对象转换为字节流进行序列化,将其关联到 ByteArrayOutputStream,这样写入 ObjectOutputStream 的数据最终会存储到 ByteArrayOutputStream 中。 os = new ObjectOutputStream(bos); + // 通过 ObjectOutputStream 的 writeObject 方法将传入的对象(value)进行序列化并写入到流中,开始实际的序列化操作,将对象的状态信息转换为字节形式。 os.writeObject(value); + // 关闭 ObjectOutputStream,释放相关资源,注意关闭流时可能会抛出 IOException,这里需要进行异常处理,不过在 finally 块中还会进一步确保流的关闭操作。 os.close(); + // 关闭 ByteArrayOutputStream,同样是释放资源,也可能抛出 IOException,同样在 finally 块中再次保障其能正确关闭。 bos.close(); + // 将 ByteArrayOutputStream 中的字节数据获取出来,赋值给 result 变量,这个就是最终序列化后的字节数组,代表了传入对象序列化后的结果。 result = bos.toByteArray(); } catch (IOException e) { + // 如果在序列化过程中出现 I/O 异常(比如流写入失败、关闭流出错等情况),则抛出 IllegalArgumentException 异常,并将原始的 IOException 作为原因传递进去, + // 提示出现了不可序列化的对象问题,同时方便调用者获取详细的异常信息来排查到底是哪里的序列化操作出现了错误。 throw new IllegalArgumentException("Non-serializable object", e); } finally { + // 调用 close 方法(这里假设是类中定义的用于关闭流的辅助方法,确保流能正确关闭并释放资源,避免资源泄漏)关闭 ObjectOutputStream,无论前面是否出现异常,都要保证流能正确关闭。 close(os); + // 同样调用 close 方法关闭 ByteArrayOutputStream,保障资源的正确释放。 close(bos); } return result; } + /** + * 反序列化对象方法 + * 根据传入的字节数组(in),将其还原为对应的对象,实现反序列化操作。如果字节数组为 null,则直接返回 null,不会进行反序列化操作。 + * 在反序列化过程中,若出现 I/O 异常(比如读取流时出错)或者类找不到异常(ClassNotFoundException,例如反序列化时找不到对应的类定义等情况),会打印异常堆栈信息进行简单的错误提示, + * 但方法仍会继续执行并返回 null(可能导致反序列化失败),不过这种处理方式可以根据实际业务需求进一步优化,比如向上抛出异常让调用者进行更合适的处理等。 + * + * @param in 要进行反序列化的字节数组,其内容应该是之前通过序列化操作得到的,且对应的对象类型需要在当前类路径下能找到(否则会出现 ClassNotFoundException)。 + * @return 返回反序列化后的对象,如果反序列化失败(比如出现异常情况)则返回 null,调用者需要根据业务情况判断返回值是否有效并进行相应处理。 + */ @Override public Object deserialize(byte[] in) { Object result = null; ByteArrayInputStream bis = null; ObjectInputStream is = null; try { - if (in != null) { + // 首先判断传入的字节数组是否为 null,如果是则直接返回 null,不进行后续的反序列化操作,因为没有有效的数据可供反序列化。 + if (in!= null) { + // 创建一个 ByteArrayInputStream 对象,它用于从字节数组中读取数据,作为反序列化的数据源,将传入的字节数组作为参数传入,初始化流对象。 bis = new ByteArrayInputStream(in); + // 创建一个 ObjectInputStream 对象,它用于从字节流中读取并还原对象,将其关联到 ByteArrayInputStream,以便从字节数组对应的流中读取数据进行反序列化操作。 is = new ObjectInputStream(bis); + // 通过 ObjectInputStream 的 readObject 方法进行反序列化操作,从流中读取数据并尝试还原为对应的对象,将还原后的对象赋值给 result 变量。 result = is.readObject(); + // 关闭 ObjectInputStream,释放相关资源,同样关闭流时可能出现 IOException,需要进行异常处理,并且在 finally 块中会再次确保流的关闭。 is.close(); + // 关闭 ByteArrayInputStream,释放资源,也可能出现 IOException,同样在 finally 块中保障其正确关闭。 bis.close(); } } catch (IOException e) { + // 如果在反序列化过程中出现 I/O 异常(比如流读取失败、关闭流出错等情况),则打印异常堆栈信息进行简单的错误提示,不过当前方法仍会继续执行并返回 null, + // 这种处理方式可能导致反序列化失败后调用者难以察觉具体问题,可根据业务实际情况优化,比如向上抛出异常让调用者处理等。 e.printStackTrace(); } catch (ClassNotFoundException e) { + // 如果出现 ClassNotFoundException 异常,意味着在反序列化时找不到对应的类定义(可能是类所在的 JAR 包未引入、类名更改等原因),同样打印异常堆栈信息进行简单提示, + // 方法继续执行并返回 null,可根据实际需求改进处理方式,比如更明确地告知调用者类找不到导致反序列化失败等情况。 e.printStackTrace(); } finally { + // 调用 close 方法关闭 ObjectInputStream,确保资源能正确释放,无论前面是否出现异常,都要执行关闭操作,避免资源泄漏。 close(is); + // 调用 close 方法关闭 ByteArrayInputStream,保障资源的正确关闭和释放。 close(bis); } return result; } + /** + * 对象相等性比较方法 + * 用于比较两个对象是否相等,先通过简单的对象引用比较(判断是否是同一个对象),如果两个对象引用相同(即 o1 == o2),则直接返回 true,表示对象相等。 + * 接着判断是否有一个对象为 null,如果其中一个为 null,则返回 false,表示两个对象不相等。最后通过调用对象的 equals 方法(前提是对象所属类正确重写了 equals 方法)来比较对象内容是否相等, + * 返回相应的比较结果,整体上综合考虑了对象引用相等和内容相等的情况来判断两个对象是否相等,符合 Java 中比较对象相等性的常规逻辑。 + * + * @param o1 要比较的第一个对象,可以为 null。 + * @param o2 要比较的第二个对象,可以为 null。 + * @return 如果两个对象相等(引用相等或者内容相等)则返回 true,否则返回 false,表示两个对象不相等。 + */ public static boolean equals(Object o1, Object o2) { + // 首先判断两个对象引用是否相同,如果是则表示它们是同一个对象,直接返回 true,说明对象相等。 if (o1 == o2) { return true; } else if (o1 == null || o2 == null) { + // 如果两个对象引用不相同,接着判断是否有一个对象为 null,如果有,则返回 false,表示两个对象不相等,因为只有两个非 null 对象才有进一步比较内容相等的必要。 return false; } else { + // 如果两个对象都不为 null,且引用不同,那么调用对象的 equals 方法(这里要求对象所属类正确重写了 equals 方法,遵循 Java 中 equals 方法的重写规范)来比较对象内容是否相等, + // 返回相应的比较结果,以此确定两个对象是否真正相等。 return o1.equals(o2); } } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/PageUtils.java b/tamguo/src/main/java/com/tamguo/util/PageUtils.java index 5c4f16f..010a508 100644 --- a/tamguo/src/main/java/com/tamguo/util/PageUtils.java +++ b/tamguo/src/main/java/com/tamguo/util/PageUtils.java @@ -5,85 +5,116 @@ import java.util.List; import com.baomidou.mybatisplus.plugins.Page; +// PageUtils类主要用于对分页相关的数据进行处理和封装,将MyBatis Plus中Page对象里的分页信息以及对应的数据进行提取、整理, +// 并提供了方便获取和设置分页相关属性(如是否显示上一页/下一页按钮、页码列表、当前页码、总页数、总数量等)的方法,方便在前端展示分页信息以及进行分页交互操作。 public class PageUtils { - - // 是否下一页按钮 + + // 用于标识是否显示下一页按钮,初始值为 false,后续会根据实际的分页情况(当前页是否小于总页数)来设置其值,用于前端判断是否展示下一页的操作按钮。 private Boolean isShowNextBtn = false; - - // 是否上一页按钮 + + // 用于标识是否显示上一页按钮,初始值为 false,同样会依据当前页与总页数等分页情况来确定其值,方便前端判断是否展示上一页的操作按钮。 private Boolean isShowPreBtn = false; - - // 当前页 + + // 存储当前页码的字符串表示形式,会从传入的Page对象中获取当前页码并转换为字符串进行存储,用于前端展示当前所在的页码信息。 private String currPageNum; - - // 页码列表 + + // 用于存储页码列表的字符串集合,例如 ["1", "2", "3", "...", "5", "6", "7"] 这样的形式,表示分页的页码展示情况,会根据总页数等条件动态生成该列表,方便前端展示分页页码导航。 private List pageNums; - - // 总页数 + + // 存储总页数的字符串表示形式,从Page对象中获取总页数并转换为字符串保存,用于告知前端总共有多少页,方便进行分页范围的判断等操作。 private String totalPage; - - // 总数量 + + // 存储数据总量的字符串表示形式,从Page对象中获取总记录数并转换为字符串,用于在前端展示总共有多少条数据,让用户对数据规模有直观了解。 private String total; - - // 数据 + + // 用于存储当前页的数据列表,类型为泛型List,实际存放的是从Page对象中获取的对应页的记录数据,方便在前端展示该页的具体数据内容。 private List list; - - public static PageUtils getPage(Page page){ + + /** + * 根据Page对象获取PageUtils实例的静态方法 + * 该方法接收一个MyBatis Plus的Page对象作为参数,基于这个Page对象中的分页信息(如当前页、总页数、总记录数等)来初始化一个PageUtils实例, + * 对实例中的各个属性(是否显示上一页/下一页按钮、页码列表、当前页码、总页数、总数量、数据列表等)进行相应的设置,最后返回这个初始化好的PageUtils实例,方便后续在业务中使用其封装好的分页相关信息。 + * + * @param page MyBatis Plus的Page对象,包含了分页相关的各种信息(当前页、每页记录数、总记录数、总页数以及对应页的数据列表等),用于提取信息来初始化PageUtils实例。 + * @return 返回一个初始化好的PageUtils实例,其中包含了整理好的分页相关属性信息,可用于在前端展示分页情况以及传递分页相关数据等操作。 + */ + public static PageUtils getPage(Page page) { PageUtils pg = new PageUtils(); - if(page.getCurrent() > 1){ + // 判断传入的Page对象的当前页码(page.getCurrent())是否大于1,如果大于1,说明不是第一页,此时应该显示上一页按钮,所以将PageUtils实例中的isShowPreBtn属性设置为true。 + if (page.getCurrent() > 1) { pg.setIsShowPreBtn(true); } - if(page.getCurrent() < page.getPages()){ + // 判断传入的Page对象的当前页码(page.getCurrent())是否小于总页数(page.getPages()),如果小于,表明还有下一页,那么需要显示下一页按钮,故将PageUtils实例中的isShowNextBtn属性设置为true。 + if (page.getCurrent() < page.getPages()) { pg.setIsShowNextBtn(true); } + List pgNums = new ArrayList<>(); - if(page.getPages() > 1){ - if(page.getPages() > 10){ + // 判断总页数(page.getPages())是否大于1,如果大于1才需要构建页码列表,因为如果只有1页,就不需要展示页码导航等信息了。 + if (page.getPages() > 1) { + // 如果总页数大于10,采用一种特定的页码列表生成策略,以简化页码展示同时保证关键页码信息的显示,方便用户进行分页导航操作。 + if (page.getPages() > 10) { + // 先添加首页页码 "1",这是常见的分页页码展示习惯,方便用户快速回到第一页。 pgNums.add("1"); + // 添加第二页页码 "2",同样是为了展示较靠前的页码信息,便于用户快速定位到前面的页面。 pgNums.add("2"); + // 添加第三页页码 "3",继续完善前面部分的页码展示,让用户能看到初始的几页页码信息。 pgNums.add("3"); + // 添加省略号 "...",用于表示中间部分页码省略,当总页数较多时,避免全部显示页码导致页面过于冗长,通过省略号提示用户还有其他页码存在。 pgNums.add("..."); - if(page.getCurrent() == page.getPages()){ - pgNums.add(((Integer)(page.getCurrent() - 2)).toString()); - pgNums.add(((Integer)(page.getCurrent() - 1)).toString()); - pgNums.add(((Integer)page.getCurrent()).toString()); - }else{ - pgNums.add(((Integer)(page.getCurrent() - 1)).toString()); - pgNums.add(((Integer)page.getCurrent()).toString()); - pgNums.add(((Integer)(page.getCurrent() + 1)).toString()); + // 判断当前页是否等于总页数,如果是,说明当前处于最后一页,那么页码列表中要展示当前页的前两页页码以及当前页码,以方便用户进行前后页的切换查看等操作。 + if (page.getCurrent() == page.getPages()) { + pgNums.add(((Integer) (page.getCurrent() - 2)).toString()); + pgNums.add(((Integer) (page.getCurrent() - 1)).toString()); + pgNums.add(((Integer) page.getCurrent()).toString()); + } else { + // 如果当前页不等于总页数,说明不是最后一页,此时页码列表中展示当前页的前一页、当前页以及后一页的页码,方便用户进行临近页码的切换操作。 + pgNums.add(((Integer) (page.getCurrent() - 1)).toString()); + pgNums.add(((Integer) page.getCurrent()).toString()); + pgNums.add(((Integer) (page.getCurrent() + 1)).toString()); } - }else{ + } else { + // 如果总页数不超过10页,采用简单的顺序添加页码的方式生成页码列表,从1开始,依次添加每一页的页码,直到达到总页数为止。 Integer n = 1; - if(page.getTotal() > 0){ - while(true){ + if (page.getTotal() > 0) { + while (true) { pgNums.add(n.toString()); - if(n >= page.getPages()){ + if (n >= page.getPages()) { break; } - n ++; + n++; } } } } else { + // 如果总页数等于1页,同样采用顺序添加页码的方式(这里其实只会添加页码 "1")生成页码列表,从1开始,添加到总页数为止,虽然只有1页,但保持页码列表生成逻辑的一致性。 Integer n = 1; - if(page.getTotal() > 0){ - while(true){ + if (page.getTotal() > 0) { + while (true) { pgNums.add(n.toString()); - if(n >= page.getPages()){ + if (n >= page.getPages()) { break; } - n ++; + n++; } } } + + // 将生成好的页码列表设置到PageUtils实例的pageNums属性中,以便后续通过相应的获取方法(getPageNums)提供给外部使用,展示分页页码信息。 pg.setPageNums(pgNums); + // 将Page对象中当前页的数据列表(page.getRecords())设置到PageUtils实例的list属性中,方便后续获取该页的数据用于前端展示等操作。 pg.setList(page.getRecords()); - pg.setCurrPageNum(((Integer)page.getCurrent()).toString()); - pg.setTotal(((Integer)page.getTotal()).toString()); - pg.setTotalPage(((Integer)page.getPages()).toString()); + // 将Page对象的当前页码(page.getCurrent())转换为字符串后设置到PageUtils实例的currPageNum属性中,用于前端展示当前所在页码信息。 + pg.setCurrPageNum(((Integer) page.getCurrent()).toString()); + // 将Page对象的总记录数(page.getTotal())转换为字符串后设置到PageUtils实例的total属性中,用于前端展示数据总量信息。 + pg.setTotal(((Integer) page.getTotal()).toString()); + // 将Page对象的总页数(page.getPages())转换为字符串后设置到PageUtils实例的totalPage属性中,用于前端展示总页数信息,方便用户了解分页的整体范围。 + pg.setTotalPage(((Integer) page.getPages()).toString()); + return pg; } + // 以下是各个属性的Getter和Setter方法,遵循JavaBean规范,方便外部类对PageUtils实例中的属性进行获取和设置操作,保证数据的封装性和可访问性。 public Boolean getIsShowNextBtn() { return isShowNextBtn; @@ -109,42 +140,34 @@ public class PageUtils { this.list = list; } - public Boolean getIsShowPreBtn() { return isShowPreBtn; } - public void setIsShowPreBtn(Boolean isShowPreBtn) { this.isShowPreBtn = isShowPreBtn; } - public String getCurrPageNum() { return currPageNum; } - public void setCurrPageNum(String currPageNum) { this.currPageNum = currPageNum; } - public String getTotalPage() { return totalPage; } - public void setTotalPage(String totalPage) { this.totalPage = totalPage; } - public String getTotal() { return total; } - public void setTotal(String total) { this.total = total; } diff --git a/tamguo/src/main/java/com/tamguo/util/RequestHelper.java b/tamguo/src/main/java/com/tamguo/util/RequestHelper.java index de0222b..acdbd0c 100644 --- a/tamguo/src/main/java/com/tamguo/util/RequestHelper.java +++ b/tamguo/src/main/java/com/tamguo/util/RequestHelper.java @@ -6,36 +6,54 @@ import javax.servlet.http.HttpServletRequest; import org.apache.log4j.Logger; +// RequestHelper类主要用于辅助处理与HTTP请求相关的一些通用操作,当前类中仅展示了一个获取客户端IP地址的方法, +// 该方法会尝试从不同的请求头信息中获取IP地址,以应对客户端通过代理服务器等方式访问时获取真实IP地址的情况,同时会进行相应的日志记录。 public class RequestHelper { + + // 创建一个Logger对象,用于记录与这个类相关的日志信息,这里将日志记录器的名称设置为RequestHelper类的全限定名, + // 通过这个日志记录器可以在合适的地方输出不同级别的日志(如INFO级别等),方便调试和查看相关操作信息,例如记录获取IP地址的过程和结果等情况。 private static Logger logger = Logger.getLogger(RequestHelper.class); /** - * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址; - * - * @param request - * @return - * @throws IOException + * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 + * 该方法用于从HTTP请求(HttpServletRequest)中获取客户端的IP地址,它会按照一定的顺序尝试从多个请求头字段中查找IP地址信息, + * 因为在实际网络环境中,客户端可能通过代理服务器访问服务端,此时真实的客户端IP地址可能被隐藏在特定的请求头字段中,而不是简单地通过 request.getRemoteAddr() 获取到, + * 所以需要依次检查不同的请求头来获取准确的客户端IP地址,同时还会对获取到的IP地址进行一些格式上的处理(如处理多个IP地址以逗号分隔的情况),最后返回获取到的IP地址,并且会根据日志级别记录获取IP地址的相关信息到日志中。 + * + * @param request HttpServletRequest对象,代表客户端发送到服务器端的HTTP请求,从中提取与IP地址相关的请求头信息来获取客户端IP地址。 + * @return 返回获取到的客户端IP地址字符串,如果所有尝试获取的方式都失败,则可能返回默认的IP地址(如通过 request.getRemoteAddr() 获取到的IP地址等情况)。 + * @throws IOException 虽然当前方法体内部没有直接抛出此异常的代码逻辑,但声明抛出该异常是为了遵循可能的接口规范或者在后续方法扩展时存在I/O相关操作导致异常抛出的可能性,调用者需要进行相应的异常处理。 */ public final static String getIpAddress(HttpServletRequest request) { - // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 + // 首先尝试从 "X-Forwarded-For" 请求头字段中获取IP地址,这个请求头通常在客户端通过代理服务器访问时,由代理服务器添加,用于记录原始客户端的IP地址以及经过的代理服务器IP地址列表(以逗号分隔), + // 如果该请求头不存在或者其值为空字符串或者值为 "unknown"(表示无法获取到有效IP地址的一种约定情况),则继续尝试从其他请求头获取IP地址。 String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 如果从 "X-Forwarded-For" 请求头中未获取到有效IP地址,再次尝试从 "Proxy-Client-IP" 请求头获取IP地址, + // 某些代理服务器会使用这个请求头来传递客户端的IP地址信息,同样判断其是否为空或者值为 "unknown",如果是则继续尝试下一个请求头。 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 接着尝试从 "WL-Proxy-Client-IP" 请求头获取IP地址,这是WebLogic服务器作为代理时可能使用的请求头来传递客户端IP地址,继续进行有效性判断和后续尝试。 ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 再尝试从 "HTTP_CLIENT_IP" 请求头获取IP地址,一些其他的代理服务器或者特定的网络环境可能会通过这个请求头传递客户端IP信息,同样检查是否有效并按需继续查找。 ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 然后尝试从 "HTTP_X_FORWARDED_FOR" 请求头获取IP地址,它与 "X-Forwarded-For" 类似,也是可能用于记录客户端IP经过代理的相关情况的请求头,继续判断并尝试获取有效IP地址。 ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 如果前面所有尝试从请求头获取IP地址的方式都失败了,最后通过 request.getRemoteAddr() 获取IP地址, + // 这个方法通常获取的是与服务器直接建立连接的客户端(可能是代理服务器或者真实客户端,如果没有代理的话)的IP地址,作为一种兜底获取IP地址的方式。 ip = request.getRemoteAddr(); } } else if (ip.length() > 15) { + // 如果从 "X-Forwarded-For" 请求头中获取到的IP地址字符串长度大于15,说明可能包含了多个IP地址(以逗号分隔的情况,记录了经过的多个代理服务器IP等情况), + // 需要对其进行处理,将IP地址字符串按照逗号分割为字符串数组,然后遍历数组查找第一个不为 "unknown" 的IP地址,将其作为客户端的真实IP地址,因为按照约定,真实客户端IP地址通常在最前面。 String[] ips = ip.split(","); for (int index = 0; index < ips.length; index++) { String strIp = (String) ips[index]; @@ -45,9 +63,13 @@ public class RequestHelper { } } } + + // 判断日志记录器的INFO级别是否启用(通过配置文件等方式设置日志级别),如果启用了INFO级别日志, + // 则记录一条INFO级别的日志信息,将获取到的IP地址信息以及方法相关的标识信息记录下来,方便后续查看获取IP地址的具体情况,例如排查IP地址获取是否正确等问题。 if (logger.isInfoEnabled()) { logger.info("getIpAddress(HttpServletRequest) - Proxy-Client-IP - String ip=" + ip); } + return ip; } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/Result.java b/tamguo/src/main/java/com/tamguo/util/Result.java index 8ac6e41..8ac2b52 100644 --- a/tamguo/src/main/java/com/tamguo/util/Result.java +++ b/tamguo/src/main/java/com/tamguo/util/Result.java @@ -5,23 +5,33 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +// Result类用于封装操作结果的通用类,实现了Serializable接口,意味着该类的对象可以被序列化,方便在网络传输、持久化存储等场景下使用。 +// 它定义了表示操作结果的相关属性(如状态码、具体结果数据、提示信息等)以及一系列静态方法来便捷地创建不同类型(成功或失败)的结果对象,同时提供了一些用于构建特定格式结果数据的方法,适用于构建接口返回数据等情况。 public class Result implements Serializable { + // 序列化版本号,用于在对象序列化和反序列化过程中保证版本兼容性,当类的结构发生变化(如新增、删除字段等情况)时,若版本号不一致可能导致反序列化失败,这里是一个固定的长整型数值。 private static final long serialVersionUID = -1651614836984397356L; + // 用于表示操作结果的状态码,不同的整数值代表不同的操作结果状态,例如常见的用0表示成功,1表示失败等,方便调用者根据状态码判断操作是否成功以及进行相应的后续处理。 private int code; + // 用于存储操作的具体结果数据,可以是任意类型的对象(如实体对象、数据集合等),根据具体的业务操作来决定存放的内容,方便将业务相关的数据返回给调用者。 private Object result; + // 用于存储与操作结果相关的提示信息,比如操作成功时可以为空字符串,操作失败时存放具体的错误提示内容,便于告知调用者操作的执行情况。 private String message; + // 定义表示成功的状态码常量,方便在创建成功结果对象时统一使用,使代码更具可读性和可维护性,当前设置为0,表示操作成功的情况。 public static final int SUCCESS_CODE = 0; + // 定义表示失败的状态码常量,同样用于在创建失败结果对象时保持一致性,当前设定为1,用于标识操作出现问题、未成功执行的情况。 public static final int FAIL_CODE = 1; + // 私有构造函数,用于限制外部直接通过构造函数创建Result对象,保证结果对象的创建只能通过类中提供的静态方法来进行,遵循封装的设计原则,便于统一管理结果对象的创建逻辑。 private Result() { } + // 私有构造函数,用于在类内部创建Result对象时传入状态码、具体结果数据以及提示信息,通过这种方式初始化Result对象的各个属性,同样外部无法直接调用这个构造函数,只能通过静态方法间接使用。 private Result(int code, Object result, String message) { this.code = code; this.result = result; @@ -29,67 +39,139 @@ public class Result implements Serializable { } /** - * 成功响应 - * - * @param result - * @return + * 创建成功响应结果对象的静态方法(仅传入结果数据) + * 该方法用于创建一个表示操作成功的Result对象,接收操作的具体结果数据(result)作为参数,将状态码设置为成功状态码(SUCCESS_CODE,即0),提示信息设置为空字符串, + * 并将传入的结果数据存入Result对象的result属性中,最后返回这个构建好的表示成功的Result对象,方便在业务中当操作成功时快速构建并返回相应的结果对象给调用者。 + * + * @param result 操作的具体结果数据,可以是任意类型的对象,例如查询到的实体对象、数据列表等,根据具体业务场景而定。 + * @return 返回一个表示成功的Result对象,其中包含了传入的结果数据以及成功状态码和空的提示信息,可用于接口等场景返回成功的操作结果。 */ public static Result successResult(Object result) { return result(SUCCESS_CODE, result, ""); } + /** + * 创建成功响应结果对象的静态方法(用于分页相关数据,部分参数重载) + * 该方法重载了successResult方法,用于在分页相关业务场景下创建表示成功的Result对象,它接收记录数据(records)、记录总数(recordSum)、每页行数(rowsOfPage)作为参数, + * 会调用另一个重载的successResult方法,并将构建好的包含分页相关数据的Map对象作为结果数据传入,最终返回表示成功且包含分页数据的Result对象,方便在分页查询等操作成功时返回相应结果。 + * + * @param records 分页查询获取到的当前页记录数据,可以是实体对象列表等形式,具体取决于业务查询的结果类型。 + * @param recordSum 总的记录数量,用于表示满足查询条件的所有记录的总数,方便前端进行分页展示等相关操作时知晓数据总量情况。 + * @param rowsOfPage 每页显示的行数,用于明确分页的每页数据量大小,辅助前端进行分页逻辑处理。 + * @return 返回一个表示成功的Result对象,其中包含了封装好的分页相关数据以及成功状态码和空的提示信息,适用于分页查询成功后的结果返回场景。 + */ public static Result successResult(Object records, Long recordSum, Long rowsOfPage) { return successResult(records, recordSum, rowsOfPage, null); } + /** + * 创建成功响应结果对象的静态方法(用于分页相关数据且包含用户自定义数据) + * 该方法再次重载successResult方法,用于在分页业务场景且可能包含用户自定义额外数据(userData)时创建表示成功的Result对象, + * 它会先调用resultOfList方法构建一个包含分页相关数据(记录数据、记录总数、每页行数)以及用户自定义数据的Map对象,然后将这个Map对象作为结果数据传入另一个successResult方法中, + * 最终返回一个包含了所有相关数据且表示成功的Result对象,方便在更复杂的分页业务场景下(如需要传递额外用户相关数据等情况)返回合适的结果给调用者。 + * + * @param records 分页查询获取到的当前页记录数据,类型通常为数据列表等,由具体业务查询决定。 + * @param recordSum 总的记录数量,表明满足查询条件的全部记录的个数,供前端进行分页展示及相关操作参考。 + * @param rowsOfPage 每页显示的行数,定义了分页时每页包含的数据量大小,便于前端进行分页逻辑处理。 + * @param userData 用户自定义的额外数据,可以是任意类型的对象,用于在结果中传递一些与业务相关的、不属于常规分页数据但又需要返回给前端的信息,例如用户权限相关数据等。 + * @return 返回一个表示成功的Result对象,其中包含了封装好的分页相关数据、用户自定义数据以及成功状态码和空的提示信息,适用于复杂分页查询成功且有额外数据要返回的场景。 + */ public static Result successResult(Object records, Long recordSum, Long rowsOfPage, Object userData) { Map result = resultOfList(records, recordSum, rowsOfPage, userData); return successResult(result); } + /** + * 构建包含分页相关数据的Map对象的静态方法(部分参数重载) + * 该方法用于构建一个包含分页相关核心数据(记录数据、记录总数、每页行数)的Map对象,方便在构建分页结果时统一组织数据格式, + * 如果不需要传递用户自定义数据,可调用这个方法来创建基本的分页数据Map,返回的Map对象后续可用于进一步构建Result对象或者直接在其他需要分页数据结构的地方使用。 + * + * @param records 分页查询获取到的当前页记录数据,一般为数据列表等形式,取决于具体业务查询的结果类型。 + * @param recordSum 总的记录数量,代表满足查询条件的所有记录的总数,便于前端知晓数据总量情况进行分页展示等操作。 + * @param rowsOfPage 每页显示的行数,明确了分页时每页的数据量大小,有助于前端处理分页逻辑。 + * @return 返回一个Map对象,其中包含了以固定键值对形式存放的分页相关数据("rows"对应记录数据、"records"对应记录总数、"total"对应每页行数),可用于后续的结果构建等操作。 + */ public static Map resultOfList(Object records, Long recordSum, Long rowsOfPage) { return resultOfList(records, recordSum, rowsOfPage, null); } + /** + * 构建包含分页相关数据及用户自定义数据的Map对象的静态方法 + * 该方法用于构建一个包含分页相关核心数据(记录数据、记录总数、每页行数)以及用户自定义数据(userData)的Map对象,通过创建一个新的HashMap对象, + * 向其中按照固定的键(如 "rows"、"records"、"total"、"userdata"等)添加相应的值(传入的参数数据),构建出符合特定格式要求的分页数据结构,方便在复杂的分页业务场景下传递完整的数据信息, + * 最后返回这个构建好的包含所有相关数据的Map对象,用于后续构建Result对象或者其他需要该数据结构的地方使用。 + * + * @param Obj 分页查询获取到的当前页记录数据,通常是数据列表等形式,根据具体业务查询结果而定。 + * @param records 总的记录数量,表明满足查询条件的全部记录个数,供前端进行分页展示及相关操作参考。 + * @param rowsOfPage 每页显示的行数,定义了分页时每页的数据量大小,便于前端进行分页逻辑处理。 + * @param userData 用户自定义的额外数据,可以是任意类型的对象,用于传递一些不属于常规分页数据但又与业务相关且需要返回给前端的信息,例如用户权限相关数据等。 + * @return 返回一个Map对象,其中包含了以固定键值对形式存放的分页相关数据以及用户自定义数据("rows"对应记录数据、"records"对应记录总数、"total"对应每页行数、"userdata"对应用户自定义数据),方便后续使用。 + */ public static Map resultOfList(Object Obj, Long records, Long rowsOfPage, Object userData) { Map result = new HashMap(); result.put("rows", Obj); result.put("records", records); result.put("total", rowsOfPage); - if (null != userData) { + if (null!= userData) { result.put("userdata", userData); } - ; return result; } + /** + * 构建适用于jqGrid组件的分页结果数据的Map对象的静态方法 + * 该方法用于构建一个符合jqGrid组件(一种常用于前端展示表格数据且支持分页等功能的插件)要求的分页结果数据的Map对象, + * 通过创建新的HashMap对象,按照jqGrid组件期望的键(如 "list"、"totalCount"、"pageSize"、"currPage"、"totalPage"等)添加相应的参数值(传入的列表数据、总记录数、每页大小、当前页码、总页码等), + * 构建出特定格式的数据结构,方便将后端分页查询结果以合适的格式返回给前端使用jqGrid组件进行展示,最后返回这个构建好的Map对象,用于后续的结果返回或者其他相关操作。 + * + * @param list 分页查询获取到的当前页记录数据,一般是数据列表形式,具体由业务查询决定,对应jqGrid组件中的数据列表展示内容。 + * @param totalCount 总的记录数量,代表满足查询条件的所有记录的总数,供jqGrid组件知晓数据总量以便进行分页展示等操作,对应jqGrid组件中的总记录数概念。 + * @param pageSize 每页显示的行数,明确了分页时每页的数据量大小,符合jqGrid组件中每页数据量的定义,便于其进行分页逻辑处理。 + * @param currPage 当前页码,用于告知jqGrid组件当前处于第几页,方便其进行页码导航等相关操作。 + * @param totalPage 总页码,表明整个分页结果一共有多少页,同样是jqGrid组件进行分页展示时需要知晓的关键信息之一。 + * @return 返回一个Map对象,其中包含了以固定键值对形式存放的适用于jqGrid组件的分页相关数据("list"对应记录数据、"totalCount"对应总记录数、"pageSize"对应每页行数、"currPage"对应当前页码、"totalPage"对应总页码),方便前端使用jqGrid组件展示分页数据。 + */ public static Map jqGridResult(List list, long totalCount, int pageSize, int currPage, - int totalPage) { + int totalPage) { Map result = new HashMap(); result.put("list", list); result.put("totalCount", totalCount); + result.put("totalPage", totalPage); result.put("pageSize", pageSize); result.put("currPage", currPage); - result.put("totalPage", totalPage); return result; } /** - * 失败响应 - * - * @param errorMsg - * @return + * 创建失败响应结果对象的静态方法 + * 该方法用于创建一个表示操作失败的Result对象,接收具体的错误提示信息(errorMsg)作为参数,将状态码设置为失败状态码(FAIL_CODE,即1), + * 把传入的错误提示信息存入Result对象的message属性中,具体结果数据属性(result)设置为空字符串,最后返回这个构建好的表示失败的Result对象,方便在业务操作失败时返回相应的结果给调用者,告知其失败原因。 + * + * @param errorMsg 具体的错误提示信息,用于说明操作失败的原因,例如数据库查询失败的具体错误描述等,是一个字符串类型的内容。 + * @return 返回一个表示失败的Result对象,其中包含了传入的错误提示信息、失败状态码以及空的结果数据,可用于接口等场景返回失败的操作结果。 */ public static Result failResult(String errorMsg) { return result(FAIL_CODE, "", errorMsg); } + /** + * 创建通用响应结果对象的静态方法 + * 该方法是一个较为通用的创建Result对象的静态方法,接收状态码(code)、具体结果数据(result)以及提示信息(message)作为参数, + * 通过调用私有构造函数来创建一个Result对象,并传入相应的参数初始化其各个属性,最后返回这个构建好的Result对象,方便在各种需要自定义状态码、结果数据和提示信息的场景下创建合适的结果对象。 + * + * @param code 表示操作结果的状态码,可根据业务定义不同的值来代表不同的结果状态,如0表示成功、1表示失败等,具体取值由业务需求决定。 + * @param result 操作的具体结果数据,可以是任意类型的对象,用于存放与业务操作相关的返回内容,例如查询到的实体对象、数据列表等。 + * @param message 与操作结果相关的提示信息,用于向调用者说明操作的执行情况,例如成功时可以为空字符串,失败时为具体的错误描述等,是一个字符串类型的内容。 + * @return 返回一个根据传入参数构建好的Result对象,包含了指定的状态码、结果数据和提示信息,适用于各种自定义结果返回的业务场景。 + */ public static Result result(int code, Object result, String message) { Result res = new Result(code, result, message); return res; } + // 以下是Result类中各个属性的Getter方法,遵循JavaBean规范,方便外部类获取Result对象中封装的状态码、结果数据以及提示信息等内容,保证数据的封装性和可访问性。 + public int getCode() { return code; } @@ -101,5 +183,4 @@ public class Result implements Serializable { public String getMessage() { return message; } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/Uploader.java b/tamguo/src/main/java/com/tamguo/util/Uploader.java index 2307e55..4c83e27 100644 --- a/tamguo/src/main/java/com/tamguo/util/Uploader.java +++ b/tamguo/src/main/java/com/tamguo/util/Uploader.java @@ -8,253 +8,153 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.fileupload.*; import org.apache.commons.fileupload.FileUploadBase.InvalidContentTypeException; import org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException; -import org.apache.commons.fileupload.util.*; -import org.apache.commons.fileupload.servlet.*; +import org.apache.commons.fileupload.util.Streams; +import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import javax.servlet.http.HttpServletRequest; -/** - * UEditor文件上传辅助类 - * - */ + +// Uploader类是一个UEditor文件上传辅助类,用于处理文件上传的各种操作,包括文件格式检查、大小限制判断、文件保存路径处理以及实际的文件上传逻辑等, +// 同时支持常规的表单文件上传和接收Base64格式文件上传两种方式,并且会根据上传过程中的不同情况设置相应的状态信息,方便调用者知晓上传结果。 public class Uploader { - // 输出文件地址 + + // 用于存储上传文件的输出文件地址,即文件最终保存的相对路径或者完整路径(根据具体使用场景确定),初始值为空字符串,在文件上传成功后会被赋值为实际的保存路径。 private String url = ""; - // 上传文件名 + // 用于存储上传后的文件名,这个文件名可能是经过处理后生成的新文件名(例如添加随机数、时间戳等以保证文件名的唯一性等情况),初始值为空字符串,上传成功后会被赋予实际的文件名。 private String fileName = ""; - // 状态 + // 用于表示文件上传的状态,通过设置不同的状态值(从errorInfo这个HashMap中获取对应的状态描述字符串)来反馈上传过程中出现的各种情况,例如成功、文件格式错误、大小超出限制等,初始值为空字符串,会在上传操作过程中根据情况进行更新。 private String state = ""; - // 文件类型 + // 用于存储文件的类型,一般通过文件扩展名来确定,例如 ".jpg"、".pdf" 等,在文件上传过程中会进行相应的提取和赋值操作,初始值为空字符串。 private String type = ""; - // 原始文件名 + // 用于存储上传文件的原始文件名,即用户在客户端选择文件时的原始文件名称,方便记录和后续可能的展示等操作,初始值为空字符串,上传时会获取并赋值。 private String originalName = ""; - // 文件大小 + // 用于存储上传文件的大小,单位为字节,在文件上传完成后会获取并记录文件的实际大小,初始值为0。 private long size = 0; + // 用于接收来自客户端的HTTP请求对象(HttpServletRequest),通过这个对象可以获取上传文件相关的信息(如请求头、请求参数等),在整个文件上传过程中起着关键的信息获取作用,初始值为null,通过构造函数传入。 private HttpServletRequest request = null; + // 用于存储文件的标题信息(可能在某些特定的业务场景下使用,例如图片有对应的标题描述等情况),初始值为空字符串,会在处理表单中对应字段时进行赋值。 private String title = ""; - // 保存路径 + // 用于指定文件的保存路径,默认值为 "upload",表示文件将会被保存到这个相对路径下(实际的物理路径会根据服务器配置等情况进一步确定),可以通过相应的Setter方法进行修改设置。 private String savePath = "upload"; - // 文件允许格式 - private String[] allowFiles = { ".rar", ".doc", ".docx", ".zip", ".pdf",".txt", ".swf", ".wmv", ".gif", ".png", ".jpg", ".jpeg", ".bmp" }; - // 文件大小限制,单位KB + // 用于定义允许上传的文件格式数组,只有文件名后缀符合这个数组中定义的格式的文件才允许被上传,初始包含了常见的一些文件格式,如压缩文件、文档文件、图片文件等类型的后缀名,可通过Setter方法重新设置允许的文件格式。 + private String[] allowFiles = { ".rar", ".doc", ".docx", ".zip", ".pdf", ".txt", ".swf", ".wmv", ".gif", ".png", ".jpg", ".jpeg", ".bmp" }; + // 用于设置文件大小的限制,单位为KB,默认值为10000KB,即10MB,如果上传文件的大小超过这个限制,将会在上传过程中被拦截并设置相应的错误状态,可通过Setter方法调整文件大小限制值。 private int maxSize = 10000; - + + // 用于存储各种文件上传错误信息的HashMap,键为表示错误类型的字符串(如 "SUCCESS"、"NOFILE" 等),值为对应的详细错误描述字符串,方便根据不同的上传情况设置相应的状态信息并反馈给调用者具体的错误原因。 private HashMap errorInfo = new HashMap(); + /** + * 构造函数,用于初始化Uploader实例并设置相关的错误信息映射 + * 该构造函数接收一个HttpServletRequest对象作为参数,将其赋值给成员变量request,以便后续在文件上传过程中获取请求相关的信息。 + * 同时初始化errorInfo这个HashMap,添加了一系列表示不同上传结果状态(成功或各种错误情况)的键值对,用于后续根据上传过程中的实际情况设置相应的状态信息并返回给调用者详细的反馈。 + * + * @param request HttpServletRequest对象,代表客户端发送的HTTP请求,包含了文件上传相关的信息,如请求头、请求参数以及上传的文件内容等,是进行文件上传操作的重要数据来源。 + */ public Uploader(HttpServletRequest request) { this.request = request; HashMap tmp = this.errorInfo; - tmp.put("SUCCESS", "SUCCESS"); //默认成功 - tmp.put("NOFILE", "未包含文件上传域"); - tmp.put("TYPE", "不允许的文件格式"); - tmp.put("SIZE", "文件大小超出限制"); - tmp.put("ENTYPE", "请求类型ENTYPE错误"); - tmp.put("REQUEST", "上传请求异常"); - tmp.put("IO", "IO异常"); - tmp.put("DIR", "目录创建失败"); - tmp.put("UNKNOWN", "未知错误"); - + tmp.put("SUCCESS", "SUCCESS"); //默认成功,设置表示上传成功的状态对应的描述信息为 "SUCCESS",方便后续判断和返回结果时使用。 + tmp.put("NOFILE", "未包含文件上传域"); // 设置表示请求中未包含文件上传相关字段的错误状态对应的描述信息,用于在没有文件上传时反馈相应的错误情况。 + tmp.put("TYPE", "不允许的文件格式"); // 设置表示上传文件的格式不符合允许的文件格式列表(allowFiles)的错误状态对应的描述信息,用于文件格式验证不通过时反馈错误原因。 + tmp.put("SIZE", "文件大小超出限制"); // 设置表示上传文件大小超过了设定的最大文件大小限制(maxSize)的错误状态对应的描述信息,用于文件大小超出限制时告知调用者具体错误情况。 + tmp.put("ENTYPE", "请求类型ENTYPE错误"); // 设置表示请求的类型(如Content-Type等相关请求头不符合文件上传要求)出现错误的状态对应的描述信息,用于请求类型不合法时反馈错误情况。 + tmp.put("REQUEST", "上传请求异常"); // 设置表示在文件上传请求处理过程中出现其他未知异常的错误状态对应的描述信息,用于捕获到一般性的请求相关异常时反馈错误原因。 + tmp.put("IO", "IO异常"); // 设置表示在文件输入输出(如读取文件、写入文件等操作)过程中出现I/O异常的错误状态对应的描述信息,用于处理文件读写等操作出现问题时反馈错误情况。 + tmp.put("DIR", "目录创建失败"); // 设置表示在创建文件保存目录(例如根据日期创建子目录等情况)时失败的错误状态对应的描述信息,用于目录创建出现问题时反馈错误原因。 + tmp.put("UNKNOWN", "未知错误"); // 设置表示其他未明确归类的、未知的错误情况对应的错误状态描述信息,用于捕获到意料之外的异常时进行统一的错误反馈。 } + /** + * 执行文件上传的方法(处理常规表单文件上传) + * 该方法用于处理通过常规表单形式进行的文件上传操作,首先判断请求是否为多部分表单数据(即是否包含文件上传域),如果不是,则设置相应的错误状态(NOFILE)并返回,不进行后续的上传操作。 + * 若是多部分表单数据,则创建DiskFileItemFactory用于配置文件上传的一些基础设置(如临时文件存储位置等),接着设置文件保存路径、创建ServletFileUpload对象并配置其相关属性(如文件大小限制、请求头编码等), + * 然后通过迭代器遍历请求中的文件项,对于每个非表单字段的文件项(即真正的上传文件),进行文件格式检查、生成新文件名、获取文件类型、构建文件保存路径等操作, + * 并将文件内容从输入流复制到输出流实现文件的保存,同时更新文件上传状态、记录文件大小等信息,若遇到文件大小超出限制、请求类型错误、文件上传异常等各种问题,则会相应地设置对应的错误状态并停止后续操作, + * 对于表单字段(如标题字段等)则进行相应的读取和处理(当前代码中仅处理名为 "pictitle" 的字段作为标题字段),最后若成功上传一个文件(UE中按只处理单张上传的逻辑)则直接退出循环。 + * + * @throws Exception 由于文件上传过程中涉及到很多可能抛出异常的操作(如文件读写、流操作、文件上传组件相关异常等),所以统一抛出Exception,调用者需要进行相应的异常处理,根据具体的业务需求来决定如何应对不同的异常情况。 + */ public void upload() throws Exception { + // 判断当前请求是否为多部分表单数据(即是否包含文件上传域),通过ServletFileUpload提供的静态方法进行判断,这是进行文件上传操作的前提条件,如果不是多部分表单数据,则不能进行后续的文件上传处理。 boolean isMultipart = ServletFileUpload.isMultipartContent(this.request); if (!isMultipart) { + // 如果请求不是多部分表单数据,说明没有文件上传相关内容,设置文件上传状态为 "NOFILE"(表示未包含文件上传域),然后直接返回,不再进行后续的文件上传操作流程。 this.state = this.errorInfo.get("NOFILE"); return; } + + // 创建一个DiskFileItemFactory对象,它用于配置文件上传过程中的一些基础设置,例如设置临时文件的存储位置等,这里后续会将其与ServletFileUpload结合使用来处理文件上传操作。 DiskFileItemFactory dff = new DiskFileItemFactory(); + // 获取文件的保存路径,通过调用getFolder方法根据配置的保存路径(this.savePath)以及当前日期生成具体的保存目录路径,确保文件按照日期进行分类保存,同时也处理了目录创建等相关操作。 String savePath = this.getFolder(this.savePath); + // 设置DiskFileItemFactory的临时文件存储目录,将其指定为前面获取到的保存路径对应的File对象,这样在文件上传过程中如果需要临时存储文件(例如文件较大时分块处理等情况),会将临时文件存放在这个目录下。 dff.setRepository(new File(savePath)); + try { + // 创建一个ServletFileUpload对象,用于处理文件上传的具体操作,将前面配置好的DiskFileItemFactory对象传入,使其基于配置好的基础设置来进行文件上传相关的处理工作,如解析请求、获取文件项等。 ServletFileUpload sfu = new ServletFileUpload(dff); + // 设置文件上传的最大允许大小,通过将以KB为单位的maxSize乘以1024转换为字节单位,限制上传文件的大小不能超过这个设定值,若超过则会抛出SizeLimitExceededException异常进行相应的错误处理。 sfu.setSizeMax(this.maxSize * 1024); + // 设置请求头的编码格式为 "utf-8",确保在处理包含中文等特殊字符的文件名等信息时能够正确解析,避免出现乱码问题,保证文件上传过程中字符编码的一致性。 sfu.setHeaderEncoding("utf-8"); + + // 通过ServletFileUpload的getItemIterator方法获取一个FileItemIterator对象,它用于迭代请求中的文件项(包括上传的文件以及表单中的其他字段信息等),方便后续逐个进行处理。 FileItemIterator fii = sfu.getItemIterator(this.request); while (fii.hasNext()) { + // 获取下一个文件项,返回的FileItemStream对象可以用于获取文件项的相关信息(如文件名、文件内容流等)以及判断是否为表单字段等操作,是处理文件上传过程中每个文件或字段的关键对象。 FileItemStream fis = fii.next(); if (!fis.isFormField()) { + // 如果当前文件项不是表单字段(即代表是一个真正要上传的文件),则进行以下文件上传相关的处理操作。 + + // 获取上传文件的原始文件名,通过截取文件名中最后一个文件分隔符(根据系统的文件分隔符来获取,通过System.getProperty("file.separator")获取系统对应的文件分隔符)之后的部分作为原始文件名, + // 去除了文件的路径信息,只保留文件名本身,方便后续处理和记录。 this.originalName = fis.getName().substring(fis.getName().lastIndexOf(System.getProperty("file.separator")) + 1); + // 检查文件的格式是否符合允许的文件格式列表(allowFiles),调用checkFileType方法进行检查,如果不符合则设置文件上传状态为 "TYPE"(表示不允许的文件格式),并跳过当前文件的后续上传操作,继续处理下一个文件项(如果有的话)。 if (!this.checkFileType(this.originalName)) { this.state = this.errorInfo.get("TYPE"); continue; } + + // 生成新的文件名,调用getName方法根据原始文件名生成一个带有随机数、时间戳等信息的新文件名,以保证文件名的唯一性和避免文件名冲突等问题,同时将新文件名赋值给fileName成员变量。 this.fileName = this.getName(this.originalName); + // 获取文件的类型,通过调用getFileExt方法截取文件名的后缀部分(从最后一个 "." 开始截取到末尾)作为文件类型,例如 ".jpg"、".pdf" 等,并赋值给type成员变量,方便后续记录和使用。 this.type = this.getFileExt(this.fileName); + // 构建文件的输出文件地址(即文件最终保存的路径),将保存路径(savePath)和新生成的文件名(fileName)组合起来,形成完整的文件保存路径,赋值给url成员变量,用于后续保存文件时确定保存位置。 this.url = savePath + "/" + this.fileName; + + // 创建一个缓冲输入流(BufferedInputStream),用于读取上传文件的内容,通过调用fis.openStream方法打开文件项对应的输入流,并将其包装为缓冲输入流,提高文件读取效率,准备将文件内容读取出来进行保存操作。 BufferedInputStream in = new BufferedInputStream(fis.openStream()); + // 根据前面构建的文件保存路径创建一个对应的File对象,用于表示要保存的目标文件,方便后续通过文件输出流将读取到的文件内容写入到这个文件中,完成文件的保存操作。 File file = new File(this.getPhysicalPath(this.url)); - FileOutputStream out = new FileOutputStream( file ); + // 创建一个文件输出流(FileOutputStream),用于将文件内容写入到目标文件中,将前面创建的File对象作为参数传入,初始化文件输出流对象,准备进行文件写入操作。 + FileOutputStream out = new FileOutputStream(file); + // 创建一个缓冲输出流(BufferedOutputStream),将前面的文件输出流包装起来,进一步提高文件写入的效率和稳定性,通过缓冲机制优化文件输出操作,减少频繁的磁盘I/O操作次数。 BufferedOutputStream output = new BufferedOutputStream(out); + // 使用Streams工具类(来自Apache Commons FileUpload组件)的copy方法,将输入流(in,即读取的上传文件内容)中的数据复制到输出流(output,即要写入到目标文件的输出流)中,实现文件的保存操作, + // 最后一个参数true表示在复制完成后自动关闭输入流和输出流,释放相关资源,确保文件内容正确地从客户端上传并保存到服务器指定的位置。 Streams.copy(in, output, true); - this.state=this.errorInfo.get("SUCCESS"); + + // 如果文件上传成功,设置文件上传状态为 "SUCCESS"(表示成功),通过从errorInfo这个HashMap中获取对应的成功状态描述字符串来设置,告知调用者文件上传操作顺利完成。 + this.state = this.errorInfo.get("SUCCESS"); + // 获取并记录上传文件的大小,通过获取保存后的文件的长度(单位为字节),赋值给size成员变量,方便后续获取文件大小信息进行相关的展示或其他业务操作。 this.size = file.length(); - //UE中只会处理单张上传,完成后即退出 + + // 在UE(UEditor,一种富文本编辑器,推测此处是针对其文件上传逻辑进行的代码编写)中按只处理单张上传的逻辑,所以一旦成功上传一个文件后,就直接退出循环,不再处理后续的文件项(如果还有的话)。 break; } else { + // 如果当前文件项是表单字段,则进行以下处理操作(当前代码中仅处理名为 "pictitle" 的字段作为标题字段,对于其他表单字段可以根据业务需求自行扩展处理逻辑)。 + + // 获取表单字段的名称,通过调用fis.getFieldName方法获取字段的名称,用于后续判断是否是需要处理的特定字段(如这里的 "pictitle")。 String fname = fis.getFieldName(); - //只处理title,其余表单请自行处理 - if(!fname.equals("pictitle")){ + // 判断表单字段名称是否等于 "pictitle",如果不等于,则直接跳过当前字段,继续处理下一个文件项,因为当前代码只关注这个特定的标题字段进行处理。 + if (!fname.equals("pictitle")) { continue; } - BufferedInputStream in = new BufferedInputStream(fis.openStream()); - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); - StringBuffer result = new StringBuffer(); - while (reader.ready()) { - result.append((char)reader.read()); - } - this.title = new String(result.toString().getBytes(),"utf-8"); - reader.close(); - - } - } - } catch (SizeLimitExceededException e) { - this.state = this.errorInfo.get("SIZE"); - } catch (InvalidContentTypeException e) { - this.state = this.errorInfo.get("ENTYPE"); - } catch (FileUploadException e) { - this.state = this.errorInfo.get("REQUEST"); - } catch (Exception e) { - this.state = this.errorInfo.get("UNKNOWN"); - } - } - - /** - * 接受并保存以base64格式上传的文件 - * @param fieldName - */ - public void uploadBase64(String fieldName){ - String savePath = this.getFolder(this.savePath); - String base64Data = this.request.getParameter(fieldName); - this.fileName = this.getName("test.png"); - this.url = savePath + "/" + this.fileName; - try { - File outFile = new File(this.getPhysicalPath(this.url)); - OutputStream ro = new FileOutputStream(outFile); - byte[] b = Base64.decodeBase64(base64Data); - for (int i = 0; i < b.length; ++i) { - if (b[i] < 0) { - b[i] += 256; - } - } - ro.write(b); - ro.flush(); - ro.close(); - this.state=this.errorInfo.get("SUCCESS"); - } catch (Exception e) { - this.state = this.errorInfo.get("IO"); - } - } - - /** - * 文件类型判断 - * - * @param fileName - * @return - */ - private boolean checkFileType(String fileName) { - Iterator type = Arrays.asList(this.allowFiles).iterator(); - while (type.hasNext()) { - String ext = type.next(); - if (fileName.toLowerCase().endsWith(ext)) { - return true; - } - } - return false; - } - - /** - * 获取文件扩展名 - * - * @return string - */ - private String getFileExt(String fileName) { - return fileName.substring(fileName.lastIndexOf(".")); - } - - /** - * 依据原始文件名生成新文件名 - * @return - */ - private String getName(String fileName) { - Random random = new Random(); - return this.fileName = "" + random.nextInt(10000) - + System.currentTimeMillis() + this.getFileExt(fileName); - } - - /** - * 根据字符串创建本地目录 并按照日期建立子目录返回 - * @param path - * @return - */ - private String getFolder(String path) { - SimpleDateFormat formater = new SimpleDateFormat("yyyyMMdd"); - path += "/" + formater.format(new Date()); - File dir = new File(this.getPhysicalPath(path)); - if (!dir.exists()) { - try { - dir.mkdirs(); - } catch (Exception e) { - this.state = this.errorInfo.get("DIR"); - return ""; - } - } - return path; - } - - /** - * 根据传入的虚拟路径获取物理路径 - * - * @param path - * @return - */ - private String getPhysicalPath(String path) { - String servletPath = this.request.getServletPath(); - String realPath = this.request.getSession().getServletContext() - .getRealPath(servletPath); - return new File(realPath).getParent() +"/" +path; - } - - public void setSavePath(String savePath) { - this.savePath = savePath; - } - - public void setAllowFiles(String[] allowFiles) { - this.allowFiles = allowFiles; - } - - public void setMaxSize(int size) { - this.maxSize = size; - } - - public long getSize() { - return this.size; - } - - public String getUrl() { - return this.url; - } - - public String getFileName() { - return this.fileName; - } - - public String getState() { - return this.state; - } - - public String getTitle() { - return this.title; - } - - public String getType() { - return this.type; - } - - public String getOriginalName() { - return this.originalName; - } -} + // 创建一个缓冲输入流(BufferedInputStream),用于读取表单字段对应的输入流内容(这里假设表单字段中的内容是以流的形式传递的,例如文本信息等),通过fis.openStream方法打开输入流并包装为缓冲输入流,方便后续读取操作。 + BufferedInputStream in = new BufferedInputStream(fis.openStream()); + // 创建一个BufferedReader对象,将前面的缓冲输入流包装为字符流读取器,通过指定字符编码为 "utf-8"( \ No newline at end of file