diff --git a/CHANGELOG.md b/CHANGELOG.md index 270e388e1..1b6d9fa30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * 【json 】 用户自定义日期时间格式时,解析也读取此格式 * 【core 】 增加可自定义日期格式GlobalCustomFormat * 【jwt 】 JWT修改默认有序,并规定payload日期格式为秒数 +* 【json 】 增加JSONWriter ### 🐞Bug修复 * 【json 】 修复XML转义字符的问题(issue#I3XH09@Gitee) diff --git a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java index 40d4e3a43..6f8eee2b5 100644 --- a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java +++ b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java @@ -78,13 +78,13 @@ final class InternalJSONUtil { /** * 缩进,使用空格符 * - * @param writer writer + * @param appendable writer * @param indent 缩进空格数 * @throws IOException IO异常 */ - protected static void indent(Writer writer, int indent) throws IOException { + protected static void indent(Appendable appendable, int indent) throws IOException { for (int i = 0; i < indent; i += 1) { - writer.write(CharUtil.SPACE); + appendable.append(CharUtil.SPACE); } } diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONArray.java b/hutool-json/src/main/java/cn/hutool/json/JSONArray.java index d6e15f077..aa76fbc6e 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONArray.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONArray.java @@ -4,14 +4,12 @@ import cn.hutool.core.bean.BeanPath; import cn.hutool.core.collection.ArrayIter; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.CharUtil; -import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.TypeUtil; import cn.hutool.json.serialize.GlobalSerializeMapping; import cn.hutool.json.serialize.JSONSerializer; +import cn.hutool.json.serialize.JSONWriter; -import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Collection; @@ -539,54 +537,14 @@ public class JSONArray implements JSON, JSONGetter, List, Rando @Override public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { - try { - return doWrite(writer, indentFactor, indent); - } catch (IOException e) { - throw new JSONException(e); - } - } - - // ------------------------------------------------------------------------------------------------- Private method start - - /** - * 将JSON内容写入Writer - * - * @param writer writer - * @param indentFactor 缩进因子,定义每一级别增加的缩进量 - * @param indent 本级别缩进量 - * @return Writer - * @throws IOException IO相关异常 - */ - private Writer doWrite(Writer writer, int indentFactor, int indent) throws IOException { - writer.write(CharUtil.BRACKET_START); - final int newindent = indent + indentFactor; - final boolean isIgnoreNullValue = this.config.isIgnoreNullValue(); - boolean isFirst = true; - for (Object obj : this.rawList) { - if (ObjectUtil.isNull(obj) && isIgnoreNullValue) { - continue; - } - if (isFirst) { - isFirst = false; - } else { - writer.write(CharUtil.COMMA); - } - - if (indentFactor > 0) { - writer.write(CharUtil.LF); - } - InternalJSONUtil.indent(writer, newindent); - InternalJSONUtil.writeValue(writer, obj, indentFactor, newindent, this.config); - } - - if (indentFactor > 0) { - writer.write(CharUtil.LF); - } - InternalJSONUtil.indent(writer, indent); - writer.write(CharUtil.BRACKET_END); + final JSONWriter jsonWriter = new JSONWriter(writer, indentFactor, indent, config); + jsonWriter.beginArray(); + this.forEach(jsonWriter::writeValue); + jsonWriter.end(); return writer; } + // ------------------------------------------------------------------------------------------------- Private method start /** * 初始化 * diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONObject.java b/hutool-json/src/main/java/cn/hutool/json/JSONObject.java index ae1d5c2fe..c5c7401c9 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONObject.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONObject.java @@ -10,15 +10,14 @@ import cn.hutool.core.map.CaseInsensitiveLinkedMap; import cn.hutool.core.map.CaseInsensitiveMap; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.serialize.GlobalSerializeMapping; import cn.hutool.json.serialize.JSONObjectSerializer; import cn.hutool.json.serialize.JSONSerializer; +import cn.hutool.json.serialize.JSONWriter; -import java.io.IOException; import java.io.Writer; import java.math.BigDecimal; import java.math.BigInteger; @@ -555,64 +554,15 @@ public class JSONObject implements JSON, JSONGetter, Map @Override public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { - try { - return doWrite(writer, indentFactor, indent); - } catch (IOException exception) { - throw new JSONException(exception); - } - } + final JSONWriter jsonWriter = new JSONWriter(writer, indentFactor, indent, this.config); + jsonWriter.beginObj(); + this.forEach((key, value)-> jsonWriter.writeKey(key).writeValue(value)); + jsonWriter.end(); - // ------------------------------------------------------------------------------------------------- Private method start - - /** - * 将JSON内容写入Writer - * - * @param writer writer - * @param indentFactor 缩进因子,定义每一级别增加的缩进量 - * @param indent 本级别缩进量 - * @return Writer - * @throws JSONException JSON相关异常 - */ - private Writer doWrite(Writer writer, int indentFactor, int indent) throws IOException { - writer.write(CharUtil.DELIM_START); - boolean isFirst = true; - final boolean isIgnoreNullValue = this.config.isIgnoreNullValue(); - final int newIndent = indent + indentFactor; - for (Entry entry : this.entrySet()) { - if (ObjectUtil.isNull(entry.getKey()) || (ObjectUtil.isNull(entry.getValue()) && isIgnoreNullValue)) { - continue; - } - - if (isFirst) { - isFirst = false; - } else { - // 键值对分隔 - writer.write(CharUtil.COMMA); - } - // 换行缩进 - if (indentFactor > 0) { - writer.write(CharUtil.LF); - } - - InternalJSONUtil.indent(writer, newIndent); - writer.write(JSONUtil.quote(entry.getKey())); - writer.write(CharUtil.COLON); - if (indentFactor > 0) { - // 冒号后的空格 - writer.write(CharUtil.SPACE); - } - InternalJSONUtil.writeValue(writer, entry.getValue(), indentFactor, newIndent, this.config); - } - - // 结尾符 - if (indentFactor > 0) { - writer.write(CharUtil.LF); - } - InternalJSONUtil.indent(writer, indent); - writer.write(CharUtil.DELIM_END); return writer; } + // ------------------------------------------------------------------------------------------------- Private method start /** * Bean对象转Map * diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONTokener.java b/hutool-json/src/main/java/cn/hutool/json/JSONTokener.java index c132b6758..fd5acd111 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONTokener.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONTokener.java @@ -393,7 +393,7 @@ public class JSONTokener { * @return A JSONException object, suitable for throwing */ public JSONException syntaxError(String message) { - return new JSONException(message + this.toString()); + return new JSONException(message + this); } /** diff --git a/hutool-json/src/main/java/cn/hutool/json/serialize/JSONWriter.java b/hutool-json/src/main/java/cn/hutool/json/serialize/JSONWriter.java new file mode 100755 index 000000000..286985561 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/serialize/JSONWriter.java @@ -0,0 +1,338 @@ +package cn.hutool.json.serialize; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.date.format.GlobalCustomFormat; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSON; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONConfig; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONNull; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONString; +import cn.hutool.json.JSONUtil; + +import java.io.IOException; +import java.io.Writer; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; +import java.util.Iterator; +import java.util.Map; + +/** + * JSON数据写出器
+ * 通过简单的append方式将JSON的键值对等信息写出到{@link Writer}中。 + * + * @author looly + * @since 5.7.3 + */ +public class JSONWriter { + + /** + * 缩进因子,定义每一级别增加的缩进量 + */ + private final int indentFactor; + /** + * 本级别缩进量 + */ + private final int indent; + /** + * Writer + */ + private final Writer writer; + /** + * JSON选项 + */ + private final JSONConfig config; + + /** + * 写出当前值是否需要分隔符 + */ + private boolean needSeparator; + /** + * 是否为JSONArray模式 + */ + private boolean arrayMode; + + public JSONWriter(Writer writer, int indentFactor, int indent, JSONConfig config) { + this.writer = writer; + this.indentFactor = indentFactor; + this.indent = indent; + this.config = config; + } + + /** + * JSONObject写出开始,默认写出"{" + * + * @return this + */ + public JSONWriter beginObj() { + writeRaw(CharUtil.DELIM_START); + return this; + } + + /** + * JSONArray写出开始,默认写出"[" + * + * @return this + */ + public JSONWriter beginArray() { + writeRaw(CharUtil.BRACKET_START); + arrayMode = true; + return this; + } + + /** + * 结束,默认根据开始的类型,补充"}"或"]" + * + * @return this + */ + public JSONWriter end() { + // 换行缩进 + writeLF().writeSpace(indent); + writeRaw(arrayMode ? CharUtil.BRACKET_END : CharUtil.DELIM_END); + try { + writer.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + arrayMode = false; + needSeparator = true; + return this; + } + + /** + * 写出键,自动处理分隔符和缩进,并包装键名 + * @param key 键名 + * @return this + */ + public JSONWriter writeKey(String key) { + if (needSeparator) { + writeRaw(CharUtil.COMMA); + } + // 换行缩进 + writeLF().writeSpace(indentFactor + indent); + return writeRaw(JSONUtil.quote(key)); + } + + /** + * 写出值,自动处理分隔符和缩进,自动判断类型,并根据不同类型写出特定格式的值 + * + * @param value 值 + * @return this + */ + public JSONWriter writeValue(Object value) { + if (arrayMode) { + if (needSeparator) { + writeRaw(CharUtil.COMMA); + } + // 换行缩进 + writeLF().writeSpace(indentFactor + indent); + } else { + writeRaw(CharUtil.COLON).writeSpace(1); + } + needSeparator = true; + return writeObjValue(value); + } + + // ------------------------------------------------------------------------------ Private methods + /** + * 写出JSON的值,根据值类型不同,输出不同内容 + * + * @param value 值 + * @return this + */ + private JSONWriter writeObjValue(Object value) { + final int indent = indentFactor + this.indent; + if (value == null || value instanceof JSONNull) { + writeRaw(JSONNull.NULL.toString()); + } else if (value instanceof JSON) { + ((JSON) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + new JSONObject(value).write(writer, indentFactor, indent); + } else if (value instanceof Iterable || value instanceof Iterator || ArrayUtil.isArray(value)) { + new JSONArray(value).write(writer, indentFactor, indent); + } else if (value instanceof Number) { + writeNumberValue((Number) value); + } else if (value instanceof Date || value instanceof Calendar || value instanceof TemporalAccessor) { + final String format = (null == config) ? null : config.getDateFormat(); + writeRaw(formatDate(value, format)); + } else if (value instanceof Boolean) { + writeBooleanValue((Boolean) value); + } else if (value instanceof JSONString) { + writeJSONStringValue((JSONString) value); + } else { + writeStrValue(value.toString()); + } + + return this; + } + + /** + * 写出数字,根据{@link JSONConfig#isStripTrailingZeros()} 配置不同,写出不同数字
+ * 主要针对Double型是否去掉小数点后多余的0
+ * 此方法输出的值不包装引号。 + * + * @param number 数字 + * @return this + */ + private JSONWriter writeNumberValue(Number number) { + // since 5.6.2可配置是否去除末尾多余0,例如如果为true,5.0返回5 + final boolean isStripTrailingZeros = null == config || config.isStripTrailingZeros(); + return writeRaw(NumberUtil.toStr(number, isStripTrailingZeros)); + } + + /** + * 写出Boolean值,直接写出true或false,不适用引号包装 + * + * @param value Boolean值 + * @return this + */ + private JSONWriter writeBooleanValue(Boolean value) { + return writeRaw(value.toString()); + } + + /** + * 输出实现了{@link JSONString}接口的对象,通过调用{@link JSONString#toJSONString()}获取JSON字符串
+ * {@link JSONString}按照JSON对象对待,此方法输出的JSON字符串不包装引号。
+ * 如果toJSONString()返回null,调用toString()方法并使用双引号包装。 + * + * @param jsonString {@link JSONString} + * @return this + */ + private JSONWriter writeJSONStringValue(JSONString jsonString) { + String valueStr; + try { + valueStr = jsonString.toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (null != valueStr) { + writeRaw(valueStr); + } else { + writeStrValue(jsonString.toString()); + } + return this; + } + + /** + * 写出字符串值,并包装引号并转义字符
+ * 对所有双引号做转义处理(使用双反斜杠做转义)
+ * 为了能在HTML中较好的显示,会将</转义为<\/
+ * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠 + * + * @param csq 字符串 + * @return this + */ + private JSONWriter writeStrValue(String csq) { + try { + JSONUtil.quote(csq, writer); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 写出空格 + * + * @param count 空格数 + * @return this + */ + private JSONWriter writeSpace(int count) { + if (indentFactor > 0) { + for (int i = 0; i < count; i++) { + writeRaw(CharUtil.SPACE); + } + } + return this; + } + + /** + * 写出换换行符 + * + * @return this + */ + private JSONWriter writeLF() { + if (indentFactor > 0) { + writeRaw(CharUtil.LF); + } + return this; + } + + /** + * 写入原始字符串值,不做任何处理 + * + * @param csq 字符串 + * @return this + */ + private JSONWriter writeRaw(String csq) { + try { + writer.append(csq); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 写入原始字符值,不做任何处理 + * + * @param c 字符串 + * @return this + */ + private JSONWriter writeRaw(char c) { + try { + writer.write(c); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 按照给定格式格式化日期,格式为空时返回时间戳字符串 + * + * @param dateObj Date或者Calendar对象 + * @param format 格式 + * @return 日期字符串 + */ + private static String formatDate(Object dateObj, String format) { + if (StrUtil.isNotBlank(format)) { + final String dateStr; + if (dateObj instanceof TemporalAccessor) { + dateStr = TemporalAccessorUtil.format((TemporalAccessor) dateObj, format); + } else { + dateStr = DateUtil.format(Convert.toDate(dateObj), format); + } + + if (GlobalCustomFormat.FORMAT_SECONDS.equals(format) + || GlobalCustomFormat.FORMAT_MILLISECONDS.equals(format)) { + // Hutool自定义的秒和毫秒表示,默认不包装双引号 + return dateStr; + } + //用户定义了日期格式 + return JSONUtil.quote(dateStr); + } + + //默认使用时间戳 + long timeMillis; + if (dateObj instanceof TemporalAccessor) { + timeMillis = TemporalAccessorUtil.toEpochMilli((TemporalAccessor) dateObj); + } else if (dateObj instanceof Date) { + timeMillis = ((Date) dateObj).getTime(); + } else if (dateObj instanceof Calendar) { + timeMillis = ((Calendar) dateObj).getTimeInMillis(); + } else { + throw new UnsupportedOperationException("Unsupported Date type: " + dateObj.getClass()); + } + return String.valueOf(timeMillis); + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java index df5d9959b..66fee7cc2 100644 --- a/hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java +++ b/hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java @@ -64,6 +64,7 @@ public class JSONUtilTest { map.put("rows", list); String str = JSONUtil.toJsonPrettyStr(map); + JSONUtil.parse(str); Assert.assertNotNull(str); }