序列化方式三——XML

在Java中,XML序列化(Serialization)是指将Java对象的状态转换为XML格式的数据,以便存储或传输。而反序列化(Deserialization)则是将XML格式的数据转换回Java对象的过程。这一机制在数据交换、配置文件的读取与写入等方面有着广泛的应用。Java提供了多种方式来实现XML的序列化和反序列化,其中较为常用的有JAXB(Java Architecture for XML Binding)和DOM、SAX等解析技术。

方式一:JAXB

介绍

JAXB通过利用Jackson对JAXB注解的支持(即jackson-module-jaxb-annotations模块),不仅简化了XML的生成过程,同时也便于生成JSON。这种集成方式极大地增强了JavaBean与XML及JSON之间的互操作性,使得数据转换变得更加灵活高效。

JAXB(Java Architecture for XML Binding)作为业界标准,它提供了一种机制,可以根据XML Schema自动生成对应的Java类。同时,JAXB也支持将XML文档实例转换为Java对象树,以及将Java对象树的内容重新序列化为XML文档。这一特性为Java开发者提供了一种快捷、简便的方式,将XML模式与Java表示进行绑定,从而在Java应用中能够轻松地处理XML数据。

在JAXB中,常用的注解如@XmlRootElement和@XmlElement等,为开发者提供了丰富的手段来定制Java类与XML之间的映射关系。这些注解的使用,进一步简化了Java类与XML之间的转换过程,提高了开发效率。

优缺点

优点:
  1. 简化XML处理:JAXB能够自动将Java对象和XML文档进行相互转换,减少了手动解析和创建XML文档的工作量,使开发者能够更专注于业务逻辑。

  2. 提高代码可读性和可维护性:通过使用注解来描述Java对象和XML之间的映射关系,JAXB使代码更加清晰易懂,降低了维护成本。

  3. 支持标准化数据交换格式:JAXB支持标准的XML数据交换格式,便于与其他使用XML的系统进行交互,增强了系统的互操作性。

  4. 灵活性和扩展性:JAXB提供了丰富的注解和API,支持复杂的XML结构,并且可以轻松地扩展以支持自定义的数据处理需求。

缺点:
  1. 学习成本:使用JAXB需要了解XML和Java对象之间的映射关系,以及如何使用JAXB注解来描述这种映射,这可能需要一定的学习时间和实践经验。

  2. 性能问题:在处理大规模XML数据时,JAXB的解析和创建过程可能会消耗较多的时间和内存资源,导致性能瓶颈。

  3. 依赖性:JAXB是Java平台的一部分,因此它的使用受限于Java环境,对于非Java平台可能需要额外的桥接技术。

常用注解

注解作用
@XmlType将Java类或枚举类型映射到XML模式类型
@XmlAccessorType(XmlAccessType.FIELD)控制字段或属性的序列化。FIELD表示JAXB将自动绑定Java类中的每个非静态的(static)、非瞬态的(由@XmlTransient标注)字段到XML。其他值还有XmlAccessType.PROPERTY和XmlAccessType.NONE
@XmlAccessorOrder控制JAXB 绑定类中属性和字段的排序
@XmlJavaTypeAdapter使用定制的适配器(即扩展抽象类XmlAdapter并覆盖marshal()和unmarshal()方法),以序列化Java类为XML
@XmlElementWrapper对于数组或集合(即包含多个元素的成员变量),生成一个包装该数组或集合的XML元素(称为包装器)
@XmlRootElement将Java类或枚举类型映射到XML元素
@XmlElement将Java类的一个属性映射到与属性同名的一个XML元素
@XmlAttribute将Java类的一个属性映射到与属性同名的一个XML属性

数据类型绑定

XML Schema类型Java数据类型
xsd:stringjava.lang.String
xsd:positiveIntegerjava.math.BigInteger
xsd:intint
xsd:longlong
xsd:shortshort
xsd:decimaljava.math.BigDecimal
xsd:floatfloat
xsd:doubledouble
xsd:booleanboolean
xsd:bytebyte
xsd:!Namejavax.xml.namespace.QName
xsd:dateTimejavax.xml.datatype.XMLGregorianCalendar
xsd:base64Binarybyte[]
xsd:hexBinarybyte[]
xsd:unsignedIntlong
xsd:unsignedShortint
xsd:unsignedByteshirt
xsd:timejavax.xml.datatype.XMLGregorianCalendar
xsd:datejavax.xml.datatype.XMLGregorianCalendar
xsd:gjavax.xml.datatype.XMLGregorianCalendar
xsd:anySimpleTypejava.lang.Object
xsd:anySimpleTypejava.lang.String
xsd:durationjavax.xml.datatype.Duration
xsd:NOTATIONjavax.xml.namespace.QName

使用

添加依赖

<dependency>
  <groupId>jakarta.xml.bind</groupId>
  <artifactId>jakarta.xml.bind-api</artifactId>
  <version>4.0.2</version>
</dependency>
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-impl</artifactId>
  <version>4.0.5</version>
</dependency>
<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.15.1</version>
</dependency>
<dependency>
  <groupId>commons-lang</groupId>
  <artifactId>commons-lang</artifactId>
  <version>2.6</version>
</dependency>
入门demo

为了使用JAXB,你需要在Java类上使用特定的注解来指定如何映射到XML。例如:

package com.zhz.test.serialization.entity;

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@NoArgsConstructor
@XmlRootElement
public class Person {

    private String name;
    private Integer age;


    @XmlElement
    public String getName() {
        return name;
    }

    @XmlElement
    public Integer getAge() {
        return age;
    }
}

序列化

使用JAXB的Marshaller类将Java对象序列化为XML字符串或文件。

//依赖
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;


//测试代码
@Test
public void testMarshal() throws JAXBException {
    Person person = new Person();
    person.setName("John Doe");
    person.setAge(30);

    JAXBContext jaxbContext = JAXBContext.newInstance(Person.class);
    Marshaller marshaller = jaxbContext.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

    // 序列化到控制台
    marshaller.marshal(person, System.out);

    // 序列化到文件
     marshaller.marshal(person, new File("D:/ideaproject/test/src/test/resources/person.xml"));
}

反序列化

使用JAXB的Unmarshaller类将XML字符串或文件反序列化为Java对象。

通过字符串读取xml文件

//依赖
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;

//测试代码
@Test
public void testUnmarshal() throws JAXBException, FileNotFoundException {
    String xml = "<person><name>John Doe</name><age>30</age></person>";
    JAXBContext jaxbContext = JAXBContext.newInstance(Person.class);
    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

    // 从字符串中读取(实际应用中可能从文件或网络读取)
    StringReader reader = new StringReader(xml);

    Person person =(Person) unmarshaller.unmarshal(reader);
    System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());
}

image.png

通过文件读取xml文件


@Test
public void testUnmarshal() throws JAXBException, FileNotFoundException {
    JAXBContext jaxbContext = JAXBContext.newInstance(Person.class);
    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
    //通过文件读取xml配置
    InputStream inputStream = new FileInputStream("D:/ideaproject/test/src/test/resources/person.xml");
    Person person =(Person) unmarshaller.unmarshal(inputStream);
    System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());
}
高级用法
实体类
package com.zhz.test.serialization.entity;

import jakarta.xml.bind.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 城市集合
 *
 * @author zhouhengzhe
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@XmlRootElement(name = "Project")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"level", "cityName", "cityCode", "creationTime"})
public class City {
    @XmlAttribute(name = "level")
    private Integer level;

    @XmlAttribute(name = "cityName ")
    private String cityName;

    @XmlAttribute(name = "cityCode ")
    private String cityCode;

    @XmlAttribute(name = "creationTime")
    private String creationTime;
}
package com.zhz.test.serialization.entity;

import jakarta.xml.bind.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
 
/**
 *
 * 省份信息
 *
 * @author zhouhengzhe
 *
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@XmlRootElement(name = "Province")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"city"})
public class Province {
    @XmlAttribute(name = "level")
    private Integer level ;

    @XmlAttribute(name = "provinceCode")
    private String provinceCode;

    @XmlAttribute(name = "provinceName")
    private String provinceName;

    @XmlAttribute(name = "creationTime")
    private String creationTime;

    @XmlElement(name = "city", required = true)
    private List<City> city;
}
package com.zhz.test.serialization.entity;

import jakarta.xml.bind.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 多个省份信息集合
 *
 * @author zhouhengzhe
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@XmlRootElement(name = "Provinces")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"province"})
public class Provinces {
    @XmlElement(name = "province", required = true)
    private List<Province> provinces;
}
工具类
package com.zhz.test.serialization.xml.jaxb;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

/**
 * Jaxb工具类
 *
 * @author zhouhengzhe
 **/
@Slf4j
public class JaxbUtils {
    private static final String XML_FILE = ".xml";

    private JaxbUtils() {
    }

    /**
     * 将对象转为xml并写入{@code filePath}文件中, xml未格式化
     *
     * @param object   待转换写入的对象
     * @param filePath 待写入的文件路径
     * @param isFormatFlag 是否格式化
     * @param <T>      对象类型
     * @return 是否成功写入
     */
    public static <T> boolean marshal(T object, String filePath,Boolean isFormatFlag) {
        if (object == null) {
            return false;
        }
        boolean marshalResult = true;
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(object.getClass());
            Marshaller marshaller = jaxbContext.createMarshaller();
            if (isFormatFlag){
                marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            }
            marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
            File file = new File(filePath);
            marshaller.marshal(object, file);
        } catch (Exception e) {
            log.error("{}", ExceptionUtils.getStackTrace(e));
            marshalResult = false;
        }
        return marshalResult;
    }
    /**
     * 将xml文件输入流,转换为Java对象
     *
     * @param inputStream 输入流
     * @param clazz       待转换的类
     * @param <T>         类型
     * @return 转换的对象
     */
    public static <T> Optional<T> xmlConvert2Object(InputStream inputStream, Class<T> clazz) throws IOException {
        InputStreamReader inputStreamReader = null;
        Object obj = null;
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
            inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
            xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
            xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
            XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(inputStreamReader);
            obj = unmarshaller.unmarshal(xmlStreamReader);
        } catch (Exception e) {
            log.warn("xmlConvert2Object fail, ", e);
        } finally {
            IOUtils.close(inputStreamReader);
            IOUtils.close(inputStream);
        }
        if (clazz.isInstance(obj)) {
            return Optional.of(clazz.cast(obj));
        }
        return Optional.empty();
    }

    /**
     * 将xml文件转换为Java对象
     *
     * @param filePath 待转换xml对象的xml文件路径
     * @param clazz    待转换的类
     * @param <T>      类型
     * @return 转换的对象
     */
    public static <T> Optional<T> xmlConvert2Object(String filePath, Class<T> clazz) {
        if (StringUtils.isEmpty(filePath)) {
            return Optional.empty();
        }
        if (!StringUtils.endsWithIgnoreCase(filePath, XML_FILE)) {
            return Optional.empty();
        }
        File file = new File(filePath);
        if (!file.exists()) {
            return Optional.empty();
        }
        try {
            InputStream inputStream = org.apache.commons.io.FileUtils.openInputStream(file);
            return xmlConvert2Object(inputStream, clazz);
        } catch (Exception e) {
            log.error("{}", ExceptionUtils.getStackTrace(e));
            return Optional.empty();
        }
    }
}
测试用例

    @Test
    public void marshal() {
        Provinces provinces = Provinces.builder()
                .provinces(Lists.newArrayList(Province.
                        builder()
                        .level(1)
                        .provinceCode("2")
                        .provinceName("广东省")
                        .creationTime(LocalDateTime.now().toString())
                        .city(Lists.newArrayList(City
                                .builder()
                                .level(1)
                                .cityCode("1")
                                .cityName("广州市")
                                .creationTime(LocalDateTime.now().toString())
                                .build()))
                        .build()))
                .build();
        boolean marshalSuccess = JaxbUtils.marshal(provinces, "src/test/resources/jaxb/marshal/marshal_province.xml", false);
        Assertions.assertTrue(marshalSuccess);
        File file = new File("src/test/resources/jaxb/marshal/marshal_province.xml");
        Assertions.assertTrue(file.exists());
    }

    @Test
    public void marshalAndFormat() {
        Provinces provinces = Provinces.builder()
                .provinces(Lists.newArrayList(Province.
                        builder()
                        .level(1)
                        .provinceCode("2")
                        .provinceName("广东省")
                        .creationTime(LocalDateTime.now().toString())
                        .city(Lists.newArrayList(City
                                .builder()
                                .level(1)
                                .cityCode("1")
                                .cityName("广州市")
                                .creationTime(LocalDateTime.now().toString())
                                .build()))
                        .build()))
                .build();
        boolean marshalSuccess =
                JaxbUtils.marshal(provinces, "src/test/resources/jaxb/marshal/marshalAndFormat_province.xml", true);
        Assertions.assertTrue(marshalSuccess);
        File file = new File("src/test/resources/jaxb/marshal/marshalAndFormat_province.xml");
        Assertions.assertTrue(file.exists());
    }

    @Test
    public void xmlConvert2Object() throws IOException {
        InputStream inputStream = new FileInputStream("src/test/resources/jaxb/marshal/marshalAndFormat_province.xml");
        Optional<Provinces> optionalProvincesXmlInfo = JaxbUtils.xmlConvert2Object(inputStream, Provinces.class);
        Assertions.assertTrue(optionalProvincesXmlInfo.isPresent());
        Provinces provinces = optionalProvincesXmlInfo.get();
        Assertions.assertEquals(1, provinces.getProvinces().size());
        List<Province> provinceXmlInfos = provinces.getProvinces();
        Province province = provinceXmlInfos.get(0);
        Assertions.assertEquals(1, province.getCity().size());
        Assertions.assertEquals("广东省", province.getProvinceName());
    }

    @Test
    public void testXmlConvert2Object() {
        Optional<Provinces> optionalProvincesXmlInfo =
                JaxbUtils.xmlConvert2Object("src/test/resources/jaxb/marshal/marshalAndFormat_province.xml", Provinces.class);
        Assertions.assertTrue(optionalProvincesXmlInfo.isPresent());
        Provinces provinces = optionalProvincesXmlInfo.get();
        Assertions.assertEquals(1, provinces.getProvinces().size());
        List<Province> provinceXmlInfos = provinces.getProvinces();
        Province province = provinceXmlInfos.get(0);
        Assertions.assertEquals(1, province.getCity().size());
        Assertions.assertEquals("广东省", province.getProvinceName());
    }

image.png

image.png

2、方式二:DOM4j

介绍

DOM4J是一个优秀的Java XML API,它具有性能优异、功能强大和易于使用的特点。在SpringBoot项目中,你可以轻松地集成DOM4J来处理XML数据。

优缺点

优点:
  1. 高性能:DOM4J在处理大型XML文件时表现出色,提供了高效的解析和操作能力。

  2. 易于使用:DOM4J的API设计简洁明了,易于上手。它大量使用了Java集合类,使得Java开发人员能够更加方便地操作XML数据。

  3. 支持XPath:DOM4J支持XPath表达式,这使得查询XML文档中的特定元素变得更加简单。

缺点:
  1. API较为复杂:虽然DOM4J提供了丰富的功能,但其API相对较为复杂,可能需要一定的学习成本。

场景

  1. XML解析与生成:当你需要读取或生成XML文件时,可以使用DOM4J来解析XML数据或创建新的XML文档。

  2. XML数据操作:DOM4J提供了灵活的API来操作XML数据,包括添加、删除、修改和查询元素等。

  3. 与第三方系统交互:当与使用XML作为数据交换格式的第三方系统进行交互时,DOM4J可以帮助你处理XML请求和响应。

常用方法

  1. 创建SAXReader:使用SAXReader类来创建一个XML解析器实例。
SAXReader reader = new SAXReader();
  1. 读取XML文件:使用SAXReaderread方法来读取XML文件,并返回一个Document对象。
Document document = reader.read(file);
  1. 获取根元素:通过Document对象的getRootElement方法来获取XML文档的根元素。
Element root = document.getRootElement();
  1. 遍历元素:使用elementIteratorelements方法来遍历XML文档中的元素。
for (Element element : root.elements()) {
    // 处理元素
}
  1. 获取和设置元素属性:使用attributeValue方法来获取元素的属性值,使用addAttribute方法来设置元素的属性。
String value = element.attributeValue("attrName");
element.addAttribute("attrName", "newValue");
  1. 获取和设置元素文本:使用getText方法来获取元素的文本内容,使用setText方法来设置元素的文本。
String text = element.getText();
element.setText("newText");

使用

添加依赖
<dependency>
  <groupId>dom4j</groupId>
  <artifactId>dom4j</artifactId>
  <version>1.6.1</version>
</dependency>
实体类
package com.zhz.test.serialization.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Employee {
    private int id;
    private String name;
    private String position;
}
序列化

序列化成文件或者字符串


/**
 * 序列化
 */
@Test
public void testMarshal() {
    // 创建一个Employee对象
    Employee employee = new Employee();
    employee.setId(1);
    employee.setName("John Doe");
    employee.setPosition("Developer");
    //序列化成字符串
    String xml = serializeEmployeeToStr(employee);
    System.out.println(xml);
    //序列化成文件
    serializeEmployeeToFile(employee,"src/test/resources/dom4j/employeeToSer.xml");
}

/**
 * 序列化成字符串
 * @param employee
 * @return
 */
public static String serializeEmployeeToStr(Employee employee) {
    // 创建XML文档和根元素
    Document document = DocumentHelper.createDocument();
    Element root = document.addElement("employees");

    // 创建并填充employee元素
    Element employeeElement = root.addElement("employee")
            .addAttribute("id", String.valueOf(employee.getId()));
    employeeElement.addElement("name").addText(employee.getName());
    employeeElement.addElement("position").addText(employee.getPosition());

    // 序列化XML到字符串
    StringWriter writer = new StringWriter();
    XMLWriter xmlWriter = new XMLWriter(writer);
    try {
        xmlWriter.write(document);
        xmlWriter.close();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    return writer.toString();
}
/**
 * 序列化成字符串
 * @param employee
 * @return
 */
public static void serializeEmployeeToFile(Employee employee,String filePath) {
    // 创建XML文档和根元素
    Document document = DocumentHelper.createDocument();
    Element root = document.addElement("employees");

    // 创建并填充employee元素
    Element employeeElement = root.addElement("employee")
            .addAttribute("id", String.valueOf(employee.getId()));
    employeeElement.addElement("name").addText(employee.getName());
    employeeElement.addElement("position").addText(employee.getPosition());

    File file = new File(filePath);
    // 序列化XML到字符串
    try {
        XMLWriter xmlWriter = new XMLWriter(new FileOutputStream(file));
        xmlWriter.write(document);
        xmlWriter.close();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

image.png

反序列化

image.png

employees.xml的配置内容如下

<?xml version="1.0" encoding="UTF-8"?>
<employees>
    <employee id="1">
        <name>John Doe</name>
        <position>Developer</position>
    </employee>
    <employee id="2">
        <name>Jane Smith</name>
        <position>Manager</position>
    </employee>
</employees>
  /**
     * 反序列化
     */
    @Test
    public void testUnmarshal() {
        String filePath = "src/test/resources/dom4j/employees.xml";
        Iterator<Element> iterator = parseXmlFile(filePath);
        List<Employee> employees = Lists.newArrayList();
        while (iterator.hasNext()) {
            Element employeeElement = iterator.next();
            employees.add(Employee
                    .builder()
                    .id(Integer.parseInt(employeeElement.attributeValue("id")))
                    .name(employeeElement.elementText("name"))
                    .position(employeeElement.elementText("position"))
                    .build());
        }
        System.out.println("反序列化得到的数据为:" + employees);
    }

    public Iterator<Element> parseXmlFile(String filePath) {
        try {
            File xmlFile = new File(filePath);
            SAXReader reader = new SAXReader();
            Document document = reader.read(xmlFile);
            Element root = document.getRootElement();
            return root.elementIterator();
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
    }

打个号外

本人新搞的个人项目,有意者可到 DDD用户中台 这里购买

可以学习到的体系

  • 项目完全从0到1开始架构,包含前端,后端,架构,服务器,技术管理相关运维知识!

    • 最佳包名设计,项目分层
  • 破冰CRUD,手撕中间件!

    • 基于MybatisPlus封装属于自己的DDD ORM框架

    • 基于Easyexcel封装属于自己的导入导出组件

    • oss对象存储脚手架(阿里云,minio,腾讯云,七牛云等)

    • 邮件脚手架

    • completefuture脚手架

    • redis脚手架

    • xxl-job脚手架

    • 短信脚手架

    • 常用工具类等

  • 传统MVC代码架构弊端的解决方案

    • DDD+CQRS+ES最难架构
  • 结合实际代码的业务场景

    • 多租户单点登录中心

    • 用户中台

    • 消息中心

    • 配置中心

    • 监控设计

  • 程序员的职业规划,人生规划

    • 打工永远没有出路!

    • 打破程序员的35岁魔咒

    • 技术带给你的优势和竞争力【启发】

    • 万物互联网的淘金之路!

技术以外的赚钱路子

可以一起沟通

具体的文章目录

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

购买链接

DDD用户中台

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhz小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值