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特殊字符转义问题
企微依赖包

相关推荐
缺点内向3 小时前
Java:创建、读取或更新 Excel 文档
java·excel
带刺的坐椅4 小时前
Solon v3.4.7, v3.5.6, v3.6.1 发布(国产优秀应用开发框架)
java·spring·solon
四谎真好看5 小时前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程5 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t5 小时前
ZIP工具类
java·zip
lang201509286 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan7 小时前
第10章 Maven
java·maven
百锦再7 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说7 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多7 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring