XStream学习使用、null值保留标签及特殊字符不转义

文章目录


前言

闲时记录下项目上XStream的使用,及特殊要求。


一、XStream简介

一个XML的序列化工具,可以实现 Java对象序列化成XML,或者将XML反序列为Java对象。

java 复制代码
//简单例子
XStream xstream = new XStream();
String xml = xstream.toXML(myObject); // serialize to XML
Object myObject2 = xstream.fromXML(xml); // deserialize from XML

二、XStream使用

参考:企微依赖包的xml使用

直接上工具类
XStreamInitializer

java 复制代码
public class XStreamInitializer {

  public static ClassLoader classLoader;

  public static void setClassLoader(ClassLoader classLoaderInfo) {
    classLoader = classLoaderInfo;
  }

  private static final XppDriver XPP_DRIVER = new XppDriver(new NoNameCoder());

  /**
   * Gets instance.
   *
   * @return the instance
   */
  public static XStream getInstance() {
    XStream xstream = new XStream(new PureJavaReflectionProvider(), XPP_DRIVER) {
      // only register the converters we need; other converters generate a private access warning in the console on Java9+...
      @Override
      protected void setupConverters() {
        registerConverter(new NullConverter(), PRIORITY_VERY_HIGH);
        registerConverter(new IntConverter(), PRIORITY_NORMAL);
        registerConverter(new FloatConverter(), PRIORITY_NORMAL);
        registerConverter(new DoubleConverter(), PRIORITY_NORMAL);
        registerConverter(new LongConverter(), PRIORITY_NORMAL);
        registerConverter(new ShortConverter(), PRIORITY_NORMAL);
        registerConverter(new BooleanConverter(), PRIORITY_NORMAL);
        registerConverter(new ByteConverter(), PRIORITY_NORMAL);
        registerConverter(new StringConverter(), PRIORITY_NORMAL);
        registerConverter(new BigDecimalConverter(), PRIORITY_NORMAL);
        registerConverter(new DateConverter(), PRIORITY_NORMAL);
        registerConverter(new CollectionConverter(getMapper()), PRIORITY_NORMAL);
        registerConverter(new ReflectionConverter(getMapper(), getReflectionProvider()), PRIORITY_VERY_LOW);
      }
    };
    xstream.ignoreUnknownElements();
    xstream.setMode(XStream.NO_REFERENCES);
    xstream.autodetectAnnotations(true);

    // setup proper security by limiting which classes can be loaded by XStream
    xstream.addPermission(NoTypePermission.NONE);
    xstream.addPermission(new WildcardTypePermission(new String[]{
            //允许序列化权限的包
            "com.example.**"
    }));
    if (null == classLoader) {
      classLoader = Thread.currentThread().getContextClassLoader();
    }
    xstream.setClassLoader(classLoader);
    return xstream;
  }

}

XStreamTransformer

java 复制代码
/**
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 */
public class XStreamTransformer {
  private static final Map<Class<?>, XStream> CLASS_2_XSTREAM_INSTANCE = new HashMap<>();

  static {
    //需要转换的类
    registerClass(TestXStream.class);
  }

  /**
   * xml -> pojo.
   */
  @SuppressWarnings("unchecked")
  public static <T> T fromXml(Class<T> clazz, String xml) {
    return (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(xml);
  }

  @SuppressWarnings("unchecked")
  public static <T> T fromXml(Class<T> clazz, InputStream is) {
    return (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(is);
  }

  /**
   * pojo -> xml.
   */
  public static <T> String toXml(Class<T> clazz, T object) {
    return CLASS_2_XSTREAM_INSTANCE.get(clazz).toXML(object);
  }

  /**
   * 注册扩展消息的解析器.
   *
   * @param clz     类型
   * @param xStream xml解析器
   */
  public static void register(Class<?> clz, XStream xStream) {
    CLASS_2_XSTREAM_INSTANCE.put(clz, xStream);
  }
  /**
   * 注册第三方的该类及其子类.
   * 便利第三方类使用 XStreamTransformer进行序列化, 以及支持XStream 1.4.18 以上增加安全许可
   * @param clz 要注册的类
   */
  public static void registerExtendClass(Class<?> clz){
    XStream xstream = XStreamInitializer.getInstance();

    Class<?>[] innerClz = getInnerClasses(clz);
    xstream.processAnnotations(clz);
    xstream.processAnnotations(innerClz);
    xstream.allowTypes(new Class[]{clz});
    xstream.allowTypes(innerClz);

    register(clz, xstream);
  }
  /**
   * 会自动注册该类及其子类.
   *
   * @param clz 要注册的类
   */
  private static void registerClass(Class<?> clz) {
    XStream xstream = XStreamInitializer.getInstance();

    xstream.processAnnotations(clz);
    xstream.processAnnotations(getInnerClasses(clz));

    register(clz, xstream);
  }

  private static Class<?>[] getInnerClasses(Class<?> clz) {
    Class<?>[] innerClasses = clz.getClasses();
    if (innerClasses == null) {
      return null;
    }
    List<Class<?>> result = new ArrayList<>(Arrays.asList(innerClasses));

    for (Class<?> inner : innerClasses) {
      Class<?>[] innerClz = getInnerClasses(inner);
      if (innerClz == null) {
        continue;
      }

      result.addAll(Arrays.asList(innerClz));
    }

    return result.toArray(new Class<?>[0]);
  }
}

使用:

java 复制代码
@Data
@XStreamAlias("HEADER")
public class TestXStream {
    @XStreamAlias("INVOICE_NUM")
    private String invoiceNum;
    @XStreamAlias("INVOICE_DESCRIPTION")
    private String invoiceDescription;
    @XStreamImplicit(itemFieldName = "LINE")
    private List<TestXStreamLine> testXStreamLines;
    /**
     * pojo -> xml
     * @return
     */
    public String toXml(){
        return XStreamTransformer.toXml((Class)this.getClass(), this);
    }
}
@Data
@XStreamAlias("LINE")
public class TestXStreamLine {
    @XStreamAlias("LINE_DESCRIPTION")
    private String lineDescription;
}

测试

java 复制代码
public static void main(String[] args) {
        TestXStream testXStream = new TestXStream();
        testXStream.setInvoiceDescription("invoiceDescription");
        testXStream.setInvoiceNum("invoiceNum");
        TestXStreamLine testXStreamLine1 = new TestXStreamLine();
        testXStreamLine1.setLineDescription("lineDescription1");
        TestXStreamLine testXStreamLine2 = new TestXStreamLine();
        testXStreamLine2.setLineDescription("lineDescription2");

        testXStream.setTestXStreamLines(List.of(testXStreamLine1,testXStreamLine2));
        String xml = testXStream.toXml();
        System.out.println(xml);
        //记得更改XStreamInitializer中xstream.addPermission的路径
        TestXStream fromXml = XStreamTransformer.fromXml(TestXStream.class, xml);
        System.out.println(JSON.toJSONString(fromXml));
    }

输出:

xml 复制代码
<HEADER>
  <INVOICE_NUM>invoiceNum</INVOICE_NUM>
  <INVOICE_DESCRIPTION>invoiceDescription</INVOICE_DESCRIPTION>
  <LINE>
    <LINE_DESCRIPTION>lineDescription1</LINE_DESCRIPTION>
  </LINE>
  <LINE>
    <LINE_DESCRIPTION>lineDescription2</LINE_DESCRIPTION>
  </LINE>
</HEADER>
{"invoiceDescription":"invoiceDescription","invoiceNum":"invoiceNum","testXStreamLines":[{"lineDescription":"lineDescription1"},{"lineDescription":"lineDescription2"}]}

三.null值保留标签

  • 新增转换器继承ReflectionConverter
  • 重写AbstractReflectionConverter#doMarshal(Object, HierarchicalStreamWriter, MarshallingContext)
  • 注意在使用的时候,记得将转换器注册到非常低的位置:xStream.registerConverter(nullConverter, XStream.PRIORITY_VERY_LOW)

代码如下:

java 复制代码
/**
 * 在xstream中注册这个converter,可以输出值为null的标签
 * 注意在使用的时候,记得将转换器注册到非常低的位置:xStream.registerConverter(nullConverter, XStream.PRIORITY_VERY_LOW);
 * 重写{@link AbstractReflectionConverter#doMarshal(Object, HierarchicalStreamWriter, MarshallingContext)}
 * 使用的是xstream旧版本的重写方式
 */
public class EmptyConverter extends ReflectionConverter {
    public EmptyConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
        super(mapper, reflectionProvider);
    }

    public EmptyConverter(Mapper mapper, ReflectionProvider reflectionProvider, Class type) {
        super(mapper, reflectionProvider, type);
    }

    @Override
    protected void doMarshal(final Object source, final HierarchicalStreamWriter writer,
                             final MarshallingContext context) {
        final List fields = new ArrayList();
        final Map defaultFieldDefinition = new HashMap();
        final Class sourceType = source.getClass();

        // Attributes might be preferred to child elements ...
        reflectionProvider.visitSerializableFields(source, new ReflectionProvider.Visitor() {
            final Set writtenAttributes = new HashSet();

            @Override
            public void visit(String fieldName, Class type, Class definedIn, Object value) {
                if (!mapper.shouldSerializeMember(definedIn, fieldName)) {
                    return;
                }
                if (!defaultFieldDefinition.containsKey(fieldName)) {
                    Class lookupType = source.getClass();
                    // See XSTR-457 and OmitFieldsTest
                    if (definedIn != sourceType
                            && !mapper.shouldSerializeMember(lookupType, fieldName)) {
                        lookupType = definedIn;
                    }
                    defaultFieldDefinition.put(
                            fieldName, reflectionProvider.getField(lookupType, fieldName));
                }

                SingleValueConverter converter = mapper.getConverterFromItemType(
                        fieldName, type, definedIn);
                if (converter != null) {
                    final String attribute = mapper.aliasForAttribute(mapper.serializedMember(
                            definedIn, fieldName));
                    if (value != null) {
                        if (writtenAttributes.contains(fieldName)) {
                            ConversionException exception =
                                    new ConversionException("Cannot write field as attribute for object, attribute name already in use");
                            exception.add("field-name", fieldName);
                            exception.add("object-type", sourceType.getName());
                            throw exception;
                        }
                        final String str = converter.toString(value);
                        if (str != null) {
                            writer.addAttribute(attribute, str);
                        }
                    }
                    writtenAttributes.add(fieldName);
                } else {
                    fields.add(new FieldInfo(fieldName, type, definedIn, value));
                }
            }
        });

        new Object() {
            {
                final Map hiddenMappers = new HashMap();
                for (Iterator fieldIter = fields.iterator(); fieldIter.hasNext(); ) {
                    FieldInfo info = (FieldInfo) fieldIter.next();
                    if (info.value != null) {
                        final Field defaultField = (Field) defaultFieldDefinition.get(info.fieldName);
                        Mapper.ImplicitCollectionMapping mapping = mapper
                                .getImplicitCollectionDefForFieldName(
                                        defaultField.getDeclaringClass() == info.definedIn ? sourceType : info.definedIn,
                                        info.fieldName);
                        if (mapping != null) {
                            Set mappings = (Set) hiddenMappers.get(info.fieldName);
                            if (mappings == null) {
                                mappings = new HashSet();
                                mappings.add(mapping);
                                hiddenMappers.put(info.fieldName, mappings);
                            } else {
                                if (!mappings.add(mapping)) {
                                    mapping = null;
                                }
                            }
                        }
                        if (mapping != null) {
                            if (context instanceof ReferencingMarshallingContext) {
                                if (info.value != Collections.EMPTY_LIST
                                        && info.value != Collections.EMPTY_SET
                                        && info.value != Collections.EMPTY_MAP) {
                                    ReferencingMarshallingContext refContext = (ReferencingMarshallingContext) context;
                                    refContext.registerImplicit(info.value);
                                }
                            }
                            final boolean isCollection = info.value instanceof Collection;
                            final boolean isMap = info.value instanceof Map;
                            final boolean isEntry = isMap && mapping.getKeyFieldName() == null;
                            final boolean isArray = info.value.getClass().isArray();
                            for (Iterator iter = isArray
                                    ? new ArrayIterator(info.value)
                                    : isCollection ? ((Collection) info.value).iterator() : isEntry
                                    ? ((Map) info.value).entrySet().iterator()
                                    : ((Map) info.value).values().iterator(); iter.hasNext(); ) {
                                Object obj = iter.next();
                                final String itemName;
                                final Class itemType;
                                if (obj == null) {
                                    itemType = Object.class;
                                    itemName = mapper.serializedClass(null);
                                } else if (isEntry) {
                                    final String entryName = mapping.getItemFieldName() != null
                                            ? mapping.getItemFieldName()
                                            : mapper.serializedClass(Map.Entry.class);
                                    Map.Entry entry = (Map.Entry) obj;
                                    ExtendedHierarchicalStreamWriterHelper.startNode(
                                            writer, entryName, entry.getClass());
                                    writeItem(entry.getKey(), context, writer);
                                    writeItem(entry.getValue(), context, writer);
                                    writer.endNode();
                                    continue;
                                } else if (mapping.getItemFieldName() != null) {
                                    itemType = mapping.getItemType();
                                    itemName = mapping.getItemFieldName();
                                } else {
                                    itemType = obj.getClass();
                                    itemName = mapper.serializedClass(itemType);
                                }
                                writeField(
                                        info.fieldName, itemName, itemType, info.definedIn, obj);
                            }
                        } else {
                            writeField(
                                    info.fieldName, null, info.type, info.definedIn, info.value);
                        }
                    }else{
                        // 处理null值的标签也输出
                        writeField(info.fieldName,null,info.type,info.definedIn,"");
                    }
                }

            }

            void writeField(String fieldName, String aliasName, Class fieldType,
                            Class definedIn, Object newObj) {
                Class actualType = newObj != null ? newObj.getClass() : fieldType;
                ExtendedHierarchicalStreamWriterHelper.startNode(writer, aliasName != null
                        ? aliasName
                        : mapper.serializedMember(sourceType, fieldName), actualType);

                if (newObj != null) {
                    Class defaultType = mapper.defaultImplementationOf(fieldType);
                    if (!actualType.equals(defaultType)) {
                        String serializedClassName = mapper.serializedClass(actualType);
                        if (!serializedClassName.equals(mapper.serializedClass(defaultType))) {
                            String attributeName = mapper.aliasForSystemAttribute("class");
                            if (attributeName != null) {
                                writer.addAttribute(attributeName, serializedClassName);
                            }
                        }
                    }

                    final Field defaultField = (Field) defaultFieldDefinition.get(fieldName);
                    if (defaultField.getDeclaringClass() != definedIn) {
                        String attributeName = mapper.aliasForSystemAttribute("defined-in");
                        if (attributeName != null) {
                            writer.addAttribute(
                                    attributeName, mapper.serializedClass(definedIn));
                        }
                    }

                    Field field = reflectionProvider.getField(definedIn, fieldName);
                    marshallField(context, newObj, field);
                }
                writer.endNode();
            }

            void writeItem(Object item, MarshallingContext context,
                           HierarchicalStreamWriter writer) {
                if (item == null) {
                    String name = mapper.serializedClass(null);
                    ExtendedHierarchicalStreamWriterHelper.startNode(
                            writer, name, Mapper.Null.class);
                    writer.endNode();
                } else {
                    String name = mapper.serializedClass(item.getClass());
                    ExtendedHierarchicalStreamWriterHelper.startNode(
                            writer, name, item.getClass());
                    context.convertAnother(item);
                    writer.endNode();
                }
            }
        };
    }

    private static class FieldInfo extends FieldLocation {
        final Class type;
        final Object value;

        FieldInfo(final String fieldName, final Class type, final Class definedIn, final Object value) {
            super(fieldName, definedIn);
            this.type = type;
            this.value = value;
        }
    }

    private static class FieldLocation {
        final String fieldName;
        final Class definedIn;

        FieldLocation(final String fieldName, final Class definedIn) {
            this.fieldName = fieldName;
            this.definedIn = definedIn;
        }

        @Override
        public int hashCode() {
            final int prime = 7;
            int result = 1;
            result = prime * result + (definedIn == null ? 0 : definedIn.getName().hashCode());
            result = prime * result + (fieldName == null ? 0 : fieldName.hashCode());
            return result;
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final EmptyConverter.FieldLocation other = (EmptyConverter.FieldLocation) obj;
            if (definedIn != other.definedIn) {
                return false;
            }
            if (fieldName == null) {
                if (other.fieldName != null) {
                    return false;
                }
            } else if (!fieldName.equals(other.fieldName)) {
                return false;
            }
            return true;
        }
    }
}

XStreamInitializer中将

registerConverter(new ReflectionConverter(getMapper(), getReflectionProvider()), PRIORITY_VERY_LOW);

替换为

registerConverter(new EmptyConverter(getMapper(), getReflectionProvider()), PRIORITY_VERY_LOW);


四.特殊字符不转义

  • 源码参考com.thoughtworks.xstream.io.xml.PrettyPrintWriter
  • 重写writeText(String, boolean)
java 复制代码
public class NoneEscapePrettyPintWriter extends AbstractWriter {

    public static int XML_QUIRKS = -1;
    public static int XML_1_0 = 0;
    public static int XML_1_1 = 1;

    private final QuickWriter writer;
    private final FastStack elementStack = new FastStack(16);
    private final char[] lineIndenter;
    private final int mode;

    private boolean tagInProgress;
    protected int depth;
    private boolean readyForNewLine;
    private boolean tagIsEmpty;
    private String newLine;

    private static final char[] NULL = "&#x0;".toCharArray();
    private static final char[] AMP = "&amp;".toCharArray();
    private static final char[] LT = "&lt;".toCharArray();
    private static final char[] GT = "&gt;".toCharArray();
    private static final char[] CR = "&#xd;".toCharArray();
    private static final char[] QUOT = "&quot;".toCharArray();
    private static final char[] APOS = "'".toCharArray();
    private static final char[] CLOSE = "</".toCharArray();

    private NoneEscapePrettyPintWriter(
            Writer writer, int mode, char[] lineIndenter, NameCoder nameCoder,
            String newLine) {
        super(nameCoder);
        this.writer = new QuickWriter(writer);
        this.lineIndenter = lineIndenter;
        this.newLine = newLine;
        this.mode = mode;
        if (mode < XML_QUIRKS || mode > XML_1_1) {
            throw new IllegalArgumentException("Not a valid XML mode");
        }
    }

    /**
     * @since 1.4
     */
    public NoneEscapePrettyPintWriter(Writer writer, NameCoder nameCoder) {
        this(writer, XML_QUIRKS, new char[]{' ', ' '}, nameCoder, "\n");
    }


    @Override
    public void startNode(String name) {
        String escapedName = encodeNode(name);
        tagIsEmpty = false;
        finishTag();
        writer.write('<');
        writer.write(escapedName);
        elementStack.push(escapedName);
        tagInProgress = true;
        depth++;
        readyForNewLine = true;
        tagIsEmpty = true;
    }

    @Override
    public void startNode(String name, Class clazz) {
        startNode(name);
    }

    @Override
    public void setValue(String text) {
        readyForNewLine = false;
        tagIsEmpty = false;
        finishTag();

        writeText(writer, text);
    }

    @Override
    public void addAttribute(String key, String value) {
        writer.write(' ');
        writer.write(encodeAttribute(key));
        writer.write('=');
        writer.write('\"');
        writeAttributeValue(writer, value);
        writer.write('\"');
    }

    protected void writeAttributeValue(QuickWriter writer, String text) {
        writeText(text, true);
    }

    protected void writeText(QuickWriter writer, String text) {
        writeText(text, false);
    }

    private void writeText(String text, boolean isAttribute) {
        int length = text.length();
        for (int i = 0; i < length; i++) {
            char c = text.charAt(i);
            switch (c) {
                case '\0':
                    if (mode == XML_QUIRKS) {
                        this.writer.write(NULL);
                    } else {
                        throw new StreamException("Invalid character 0x0 in XML stream");
                    }
                    break;
                case '&':
                    if (isAttribute)
                        this.writer.write(AMP);
                    else
                        this.writer.write(c);
                    break;
                case '<':
                    if (isAttribute)
                        this.writer.write(LT);
                    else
                        this.writer.write(c);
                    break;
                case '>':
                    if (isAttribute)
                        this.writer.write(GT);
                    else
                        this.writer.write(c);
                    break;
                case '"':
                    if (isAttribute)
                        this.writer.write(QUOT);
                    else
                        this.writer.write(c);
                    break;
                case '\'':
                    if (isAttribute)
                        this.writer.write(APOS);
                    else
                        this.writer.write(c);
                    break;
                case '\r':
                    if (isAttribute)
                        this.writer.write(CR);
                    else
                        this.writer.write(c);
                    break;
                case '\t':
                case '\n':
                    if (!isAttribute) {
                        this.writer.write(c);
                        break;
                    }
                default:
                    if (Character.isDefined(c) && !Character.isISOControl(c)) {
                        if (mode != XML_QUIRKS) {
                            if (c > '\ud7ff' && c < '\ue000') {
                                throw new StreamException("Invalid character 0x"
                                        + Integer.toHexString(c)
                                        + " in XML stream");
                            }
                        }
                        this.writer.write(c);
                    } else {
                        if (mode == XML_1_0) {
                            if (c < 9
                                    || c == '\u000b'
                                    || c == '\u000c'
                                    || c == '\u000e'
                                    || (c >= '\u000f' && c <= '\u001f')) {
                                throw new StreamException("Invalid character 0x"
                                        + Integer.toHexString(c)
                                        + " in XML 1.0 stream");
                            }
                        }
                        if (mode != XML_QUIRKS) {
                            if (c == '\ufffe' || c == '\uffff') {
                                throw new StreamException("Invalid character 0x"
                                        + Integer.toHexString(c)
                                        + " in XML stream");
                            }
                        }
                        this.writer.write("&#x");
                        this.writer.write(Integer.toHexString(c));
                        this.writer.write(';');
                    }
            }
        }
    }

    @Override
    public void endNode() {
        depth--;
        if (tagIsEmpty) {
            writer.write('/');
            readyForNewLine = false;
            finishTag();
            elementStack.popSilently();
        } else {
            finishTag();
            writer.write(CLOSE);
            writer.write((String) elementStack.pop());
            writer.write('>');
        }
        readyForNewLine = true;
        if (depth == 0) {
            writer.flush();
        }
    }

    private void finishTag() {
        if (tagInProgress) {
            writer.write('>');
        }
        tagInProgress = false;
        if (readyForNewLine) {
            endOfLine();
        }
        readyForNewLine = false;
        tagIsEmpty = false;
    }

    protected void endOfLine() {
        writer.write(getNewLine());
        for (int i = 0; i < depth; i++) {
            writer.write(lineIndenter);
        }
    }

    @Override
    public void flush() {
        writer.flush();
    }

    @Override
    public void close() {
        writer.close();
    }

    /**
     * Retrieve the line terminator.
     * <p>
     * This method returns always a line feed, since according the XML specification any parser
     * must ignore a carriage return. Overload this method, if you need different behavior.
     *
     * @return the line terminator
     * @since 1.3
     */
    protected String getNewLine() {
        return newLine;
    }
}

XStreamInitializer类中将

private static final XppDriver XPP_DRIVER = new XppDriver(new NoNameCoder());

替换为

private static final XppDriver XPP_DRIVER = new XppDriver(new NoNameCoder()){

@Override

public HierarchicalStreamWriter createWriter(Writer out) {

//重写createWriter

return new NoneEscapePrettyPintWriter(out, getNameCoder());

}

};

参考:
XStream null值序列化时不会显示标签
xstream特殊字符转义问题
企微依赖包

相关推荐
神仙别闹7 分钟前
基于Java实现的(GUI)华容道小游戏
java·gui
JosieBook8 分钟前
【面试题】2025年百度校招Java后端面试题
java·开发语言·网络·百度
请你打开电视看看10 分钟前
观察者模式
java·观察者模式·设计模式
Mr.朱鹏13 分钟前
设计模式之策略模式-工作实战总结与实现
java·spring·设计模式·kafka·maven·策略模式·springbbot
计算机毕设指导615 分钟前
基于SpringBoot共享汽车管理系统【附源码】
java·spring boot·后端·mysql·spring·汽车·intellij idea
春风十里不如你95271 小时前
【设计模式】【行为型模式(Behavioral Patterns)】之责任链模式(Chain of Responsibility Pattern)
java·设计模式·责任链模式
小宋10211 小时前
实现Excel文件和其他文件导出为压缩包,并导入
java·javascript·excel·etl
guihong0041 小时前
JAVA面试题、八股文学习之JVM篇
java·jvm·学习
QQ_1154320312 小时前
基于Java+SpringBoot+Mysql在线简单拍卖竞价拍卖竞拍系统功能设计与实现九
java·spring boot·mysql·毕业设计·毕业源码·竞拍系统·竞拍项目