当前位置:网站首页>基于注解和反射生成xml

基于注解和反射生成xml

2022-06-10 17:23:00 llp1110

基于注解和反射生成xml

​ 大家好,我是llp。最近在对接第三方接口时,发现他们都是xml形式的报文,而且各有差异有的集合的地方包含父标签而有的又没有,于是就想着自己做一个基于注解反射去生成xml的方案。今天正好完成了,做个记录顺着做个分享,程序肯定有很多不足的地方,非常欢迎大家提出建议。

1.XMl语法

在此之前我们需要先知道一个XML文件分为如下几部分内容

1.文档声明

2.元素

3.属性

4.注释

5.CDATA区、特殊字符

具体细节,可以参考官网文档

2.定义自己的一套注解

这里我将注解分为下面几类:

1.@XmlBeanElement  用于修饰类
2.@XmlPropertyELement 用于修饰普通类型的字段
3.@XmlBeanElement 用于修饰bean类型的字段
4.@XmlCDATA 用于修饰生成CDATA区的字段
5.@XmlGatherElement 用于修饰集合类型的字段 List<String>List<Person>List<Map>
6.@XmlMapElement 用于修饰Map类型的字段 Map<String,String>

这里Map只考虑了普通类型的key,val方式

[email protected]

/** * xml跟标签注解(xml元素) * ElementType.TYPE 修饰类 * @Retention(RetentionPolicy.RUNTIME) 运行时生效 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlRootElement {
    

    /** * 别名 */
    String name() default "";

    /** * 命名空间 */
    String[] namespace() default "";

    /** * 版本号 用于文档声明 */
    String version() default "v1.0";

    /** * 编码 用于文档声明 */
    String encoding() default "utf-8";

}

[email protected]

/** * xml属性注解 * @Target(ElementType.FIELD) 修饰字段 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlPropertyELement {
    

    /** * 别名 */
    String name() default "";

    /** * 命名空间 */
    String namespace() default "";

    /** * 是否解析空值 */
    boolean analysisEmpty() default false;

}

[email protected]

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlBeanElement {
    

    /** * 别名 */
    String name() default "";

    /** * 命名空间 */
    String namespace() default "";

    /** * 是否解析空值 * @return */
    boolean analysisEmpty() default false;

}

[email protected]

/** * CDATA区 * <![CDATA[ * 这里可以把你输入的字符原样显示,不会解析 xml * ]]> * 1.因为这里设计@XmlCDATA是配合@XmlPropertyELement注解进行使用 * 是否解析空值、命名空间、别名等交由@XmlPropertyELement控制 * [email protected]注解只关注于xml中CDATA区的处理 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlCDATA {
    
    /** * <code> * <!--如果希望把某些字符串,当做普通文本,使用CDATA包括 --> * <![CDATA[ * <script data-compress=strip> * function h(obj){ * obj.style.behavior='url(#default#homepage)'; * var a = obj.setHomePage('//www.baidu.com/'); * } * </script> * ]]> * </code> */

}

[email protected]

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlGatherElement {
    

    /** * 别名 */
    String name() default "";

    /** * 命名空间 */
    String namespace() default "";

    /** * 子标签 */
    String childName() default "";

    /** * 是否解析空值 */
    boolean analysisEmpty() default false;

    /** * 是否显示父标签名 */
    boolean showName() default true;

    /** * 是否是bean */
    boolean isBean() default false;
}

[email protected]

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlMapElement {
    

    /** * 标签别名 */
    String name() default "";

    /** * 命名空间 */
    String namespace() default "";

    /** * 是否解析空值 */
    boolean analysisEmpty() default false;
}

3.生成XML的工具类

public class XmlUtil {
    

    /** * 生成xml字符串 * * @param obj xml元素注解修饰的对象 * @return */
    public static String generateXml(Object obj) {
    
        //1.获取到class对象的
        Class<?> clazz = obj.getClass();
        //2.获取xml元素注解
        XmlRootElement xmlRootElement = clazz.getAnnotation(XmlRootElement.class);
        //3.定义一个StringBuffer对象用于拼接xml字符串
        StringBuffer xml = new StringBuffer();
        //4.文档声明 组装 <?xml version="1.0" encoding="UTF-8"?>
        xml.append("<?xml ")
                .append("version=" + xmlRootElement.version())
                .append(" ")
                .append("encoding=" + xmlRootElement.encoding())
                .append("?>");

        //5.xml元素(跟标签)拼接
        String name = xmlRootElement.name();
        //如果别名为空,以类名作为标签
        if ("".equals(name) || name == null) {
    
            name = clazz.getSimpleName();
        }
        xml.append("<")
                .append(name)
                .append(" ");

        String[] namespace = xmlRootElement.namespace();
        //6.遍历命名空间,拼接xml元素命名空间
        for (int i = 0; i < namespace.length; i++) {
    
            if (StringUtils.isNotBlank(namespace[i])) {
    
                xml.append(namespace[i])
                        .append(" ");
            }

        }
        //移除末尾多余的空格
        xml.deleteCharAt(xml.lastIndexOf(" "));
        xml.append(">");

        //7.拼接xml属性
        element(xml, clazz, obj);

        //8.xml元素结尾
        xml.append("</")
                .append(xmlRootElement.name())
                .append(">");
        return xml.toString();
    }

    /** * 组装xml属性标签内容 * * @param xml * @param clazz * @param obj */
    private static void element(StringBuffer xml, Class<?> clazz, Object obj) {
    
        //1.获取类的所有字段
        Field[] fields = clazz.getDeclaredFields();
        //2.遍历所有字段
        for (Field field : fields) {
    
            Object val = getFieldVal(field, obj);
            //3.判断每个字段被什么类型的注解修饰
            if (field.isAnnotationPresent(XmlPropertyELement.class)) {
    
                //基本类型字段内容处理
                propertyElement(field, xml, val);
            } else if (field.isAnnotationPresent(XmlBeanElement.class)) {
    
                //对象字段内容处理
                beanELement(field, xml, val);
            } else if (field.isAnnotationPresent(XmlGatherElement.class)) {
    
                //集合字段内容处理
                gatherELement(field, xml, val);
            } else if (field.isAnnotationPresent(XmlMapElement.class)) {
    
                //map字段内容处理
                mapELement(field, xml, val);
            }
        }
    }

    /** * map字段内容处理 * * @param field * @param xml * @param val */
    private static void mapELement(Field field, StringBuffer xml, Object val) {
    
        //1.获取@XmlMapElement注解
        XmlMapElement xmlMapElement = field.getAnnotation(XmlMapElement.class);
        //2.判断是否解析空值
        if (!xmlMapElement.analysisEmpty() && isEmpty(val)) {
    
            return;
        }
        //3.获取别名,如果别名为空则以字段名作为标签
        String name = xmlMapElement.name();
        if ("".equals(name) || name == null) {
    
            name = field.getName();
        }
        xml.append("<").append(name);
        //4.获取命名空间,如果存在则拼接
        if (!"".equals(xmlMapElement.namespace()) && xmlMapElement.namespace() != null) {
    
            xml.append(" ").append(xmlMapElement.namespace());
        }
        xml.append(">");
        //遍历map拼接
        if (val instanceof Map) {
    
            mapToXmlString(xml, (Map) val);
        } else {
    
            throw new RuntimeException("@XmlMapElement标识字段必须为Map类型");
        }
        //结束标签拼接
        xml.append("</").append(name).append(">");
    }

    /** * 集合字段内容处理 * * @param field * @param xml * @param val */
    private static void gatherELement(Field field, StringBuffer xml, Object val) {
    
        //1.获取@XmlGatherElement注解
        XmlGatherElement gatherElement = field.getAnnotation(XmlGatherElement.class);
        //判断是否处理空数据
        if (!gatherElement.analysisEmpty() && isEmpty(val)) {
    
            return;
        }
        String name = gatherElement.name();
        //别名为空按名字装配
        if (gatherElement == null || name.length() == 0) {
    
            name = field.getName();
        }
        //判断是否组装父标签
        boolean showName = gatherElement.showName();
        if (showName) {
    
            xml.append("<").append(name);
            if (gatherElement.namespace() != null && !"".equals(gatherElement.namespace()))
                xml.append(" ").append(gatherElement.namespace());
            xml.append(">");
        }
        //获取子标签名称
        String childName = gatherElement.childName();
        //子标签为空则已父标签作为子标签
        if (isEmpty(childName))
            childName = name;
        if (val instanceof Array) {
    
            //处理数组数据
            //判断子标签是否需要解析
            if (!gatherElement.isBean()) {
    
                //不为bean对象则直接遍历数组,对每个元素进行拼接
                for (Array a : (Array[]) val) {
    
                    xml.append("<").append(childName).append(">").append(a).append("</").append(childName).append(">");
                }
            } else {
    
                //每个元素都是bean对象的数组
                for (Object ob : (Array[]) val) {
    
                    //组装子标签数据
                    beanOrMapEment(childName, xml, ob);
                }
            }
        } else if (val instanceof Collection) {
    
            //处理集合数据
            Collection list = (Collection) val;
            //判断集合子标签是否需要解析
            if (!gatherElement.isBean()) {
    
                for (Object ob : list) {
    
                    xml.append("<").append(childName).append(">").append(ob).append("</").append(childName).append(">");
                }
            } else {
    
                for (Object ob : list) {
    
                    //解析子标签
                    beanOrMapEment(childName, xml, ob);
                }
            }
        }
        //如果显示父标签,拼接父标签结束标签
        if (showName) {
    
            xml.append("</").append(name).append(">");
        }
    }

    /** * 每个元素为bean或map数据拼接 * * @param childName * @param xml * @param ob */
    private static void beanOrMapEment(String childName, StringBuffer xml, Object ob) {
    
        xml.append("<").append(childName).append(">");
        //当为空时不处理
        if (ob != null) {
    
            //判断集合内元素是否为map
            if (ob instanceof Map) {
    
                //解析map数据
                mapToXmlString(xml, (Map) ob);
            } else {
    
                //解析bean,获取元素的class对象
                Class clazz = ob.getClass();
                //组装元素xml报文
                element(xml, clazz, ob);
            }
        }
        xml.append("</").append(childName).append(">");
    }

    /** * map遍历组装 * * @param xml * @param map */
    private static void mapToXmlString(StringBuffer xml, Map map) {
    
        if (map == null || map.size() == 0) {
    
            return;
        }
        for (Object key : map.keySet()) {
    
            xml.append("<").append(key).append(">").append(map.get(key)).append("</").append(key).append(">");
        }
    }

    /** * bean属性字段内容处理 * * @param field * @param xml * @param val */
    private static void beanELement(Field field, StringBuffer xml, Object val) {
    
        //1.获取@XmlBeanElement注解
        XmlBeanElement xmlBeanElement = field.getAnnotation(XmlBeanElement.class);
        //2.判断是否对空值进行解析
        if (!xmlBeanElement.analysisEmpty() && isEmpty(val)) {
    
            //如果不对空值解析且属性值为空则直接return
            return;
        }
        //3.拼接bean对象起始标签
        String name = xmlBeanElement.name();
        //如果别名为空则已字段名进行拼接
        if (name == null || "".equals(name)) {
    
            name = field.getName();
        }
        xml.append("<").append(name);
        //4.判断属性是否设置命名空间
        if (!"".equals(xmlBeanElement.namespace()) && xmlBeanElement.namespace() != null) {
    
            xml.append(" ").append(xmlBeanElement.namespace());
        }
        xml.append(">");
        //5.判断是否字段是否被@XmlCDATA注解修饰
        if (field.isAnnotationPresent(XmlCDATA.class)) {
    
            //对内容进行包裹
            xml.append("<![CDATA[").append(val).append("]]>");
        }
        //6.获取对象字段的class对象,对bean对象的属性进行组装,如果字段中还含有bean对象则进行递归处理,依次类推
        Class<?> fieldClass = field.getType();
        element(xml, fieldClass, val);
        //7.拼接bean对象结束标签
        xml.append("</").append(xmlBeanElement.name()).append(">");
    }

    /** * 基本类型字段内容处理 * * @param field * @param xml * @param val */
    private static void propertyElement(Field field, StringBuffer xml, Object val) {
    
        //1.获取@XmlPropertyELement
        XmlPropertyELement xmlPropertyELement = field.getAnnotation(XmlPropertyELement.class);
        //2/判断是否解析空值,如果不解析且值为空则返回
        if (!xmlPropertyELement.analysisEmpty() && isEmpty(val)) {
    
            return;
        }
        //3.属性标签名称拼接
        //如果别名为空则已字段名进行拼接
        String name = xmlPropertyELement.name();
        if (name == null || "".equals(name)) {
    
            name = field.getName();
        }
        xml.append("<").append(name);
        //4.判断属性是否设置命名空间,如果存在则进行拼接
        if (!"".equals(xmlPropertyELement.namespace()) && xmlPropertyELement.name() != null) {
    
            xml.append(" ").append(xmlPropertyELement.namespace());
        }
        xml.append(">");
        /** * * <![CDATA[ * 这里可以把你输入的字符原样显示,不会解析 xml * ]]> */
        //5.拼接属性标签内容
        if (field.isAnnotationPresent(XmlCDATA.class)) {
    
            //如果字段被@XmlCDATA注解就用<![CDATA[标签内容]]>将标签体进行包裹
            xml.append("<![CDATA[").append(val).append("]]>");
        } else {
    
            //如果字段没有被@XmlCDATA注解修饰则直接拼接标签体
            xml.append(val);
        }
        //6.拼接属性标签结束标记
        xml.append("</").append(xmlPropertyELement.name()).append(">");
    }

    /** * 判断对象值是否为空 * * @param val * @return */
    private static boolean isEmpty(Object val) {
    
        return val == null || "".equals(val);
    }

    /** * 反射获取字段值 * * @param field * @param obj * @return */
    private static Object getFieldVal(Field field, Object obj) {
    
        try {
    
            //允许调用私有的方法
            field.setAccessible(true);
            //反射获取字段值
            return field.get(obj);
        } catch (IllegalAccessException e) {
    
            e.printStackTrace();
        }
        return null;
    }

}

4.测试使用

1.使用步骤

1.首先我们需要用注解去修饰含有xml字段的bean

test01—FileDto

@Data
@XmlRootElement(name = "file",
        namespace = {
    "xmlns=\"urn:gsma:params:xml:ns:rcs:rcs:fthttp\"",
                "xmlns:x=\"urn:gsma:params:xml:ns:rcs:rcs:up:fthttpext\""})
public class FileDto {
    

    @XmlBeanElement(name = "file-info" ,namespace = "type=\"thumbnail\"")
    private FileInfoDto thumbFileInfoDto;

    @XmlBeanElement(name = "file-info", namespace = "file-disposition=\"file\"")
    private FileInfoDto fileInfoDto;

}
@Data
public class FileInfoDto {
    

    @XmlPropertyELement(name = "file-size")
    private String fileSize;

    @XmlPropertyELement(name = "file-name")
    private String fileName;

    @XmlPropertyELement(name = "content-type")
    private String contentType;

    @XmlPropertyELement(name = "x:branded-url")
    private String brandedUrl;

}

test02—OutboundMessageRequestDto

/** * <?xml version="1.0" encoding="UTF-8"?> * <msg:outboundMessageRequest xmlns:msg="urn:oma:xml:rest:netapi:messaging:1"> * <messageId>5eae954c-42ca-4181-9ab4-9c0ef2e2ac66</messageId> * <clientCorrelator>567895</clientCorrelator> * </msg:outboundMessageRequest> */
@Data
@XmlRootElement(name = "msg:outboundMessageRequest",namespace = "xmlns:msg=\"urn:oma:xml:rest:netapi:messaging:1\"")
public class OutboundMessageRequestDto {
    

    @XmlPropertyELement(name = "messageId")
    private String messageId;

    @XmlPropertyELement(name = "clientCorrelator")
    private String clientCorrelator;

    @XmlCDATA
    @XmlPropertyELement(name = "bodyText",analysisEmpty = true)
    private String bodyText;
}

test03-RequestDto

@Data
@XmlRootElement(name = "Request",namespace = "xmlns:msg=\"urn:oma:xml:rest:netapi:messaging:1\"")
public class RequestDto {
    

    @XmlPropertyELement(name = "address")
    private String address;

    @XmlGatherElement(name = "destinationAddress",childName = "phone")
    private List<String> destinationAddress;

    @XmlPropertyELement(name = "senderAddress")
    private String senderAddress;

    @XmlBeanElement(name = "Message")
    private MessageDto messageDto;

    @XmlPropertyELement(name = "clientCorrelator")
    private String clientCorrelator;

}
@Data
public class MessageDto {
    

    @XmlPropertyELement(name = "contentType")
    private String contentType;

    @XmlPropertyELement(name = "conversationID")
    private String conversationID;

    @XmlPropertyELement(name = "contributionID")
    private String contributionID;

    @XmlGatherElement(name = "reportRequest",showName = true,analysisEmpty = true)
    private List<String> reportRequest;

    @XmlBeanElement(name = "serviceCapability")
    private ServiceCapabilityDto serviceCapabilityDto;

    @XmlPropertyELement(name = "messageId")
    private String messageId;

    @XmlPropertyELement(name = "bodyText")
    private String bodyText;
}
@Data
public class ServiceCapabilityDto {
    

    @XmlPropertyELement(name = "capabilityId")
    private String capabilityId;

    @XmlPropertyELement(name = "version")
    private String version;
}

2.测试案例

public class MyTest {
    

    /** 一个XML文件分为如下几部分内容 1.文档声明 2.元素 3.属性 4.注释 5.CDATA区、特殊字符 * <?xml version="1.0" encoding="UTF-8"?> * <file * xmlns="urn:gsma:params:xml:ns:rcs:rcs:fthttp" * xmlns:x="urn:gsma:params:xml:ns:rcs:rcs:up:fthttpext"> * <file-info type="thumbnail"> * <file-size>2048</file-size> * <content-type>png</content-type> * </file-info> * <file-info type="file" file-disposition="file"> * <file-size>1024</file-size> * <file-name>1.jpg</file-name> * <content-type>jpeg</content-type> * <x:branded-url>[alternative branded HTTP URL of the file]</x:branded-url> * </file-info> * </file> */
    @Test
    public void test01(){
    
        FileDto fileDto = new FileDto();

        FileInfoDto thumbFileInfo = new FileInfoDto();
        thumbFileInfo.setFileSize("2048");
        thumbFileInfo.setContentType("png");
        FileInfoDto fileInfoDto = new FileInfoDto();
        fileInfoDto.setFileSize("1024");
        fileInfoDto.setFileName("1.jpg");
        fileInfoDto.setContentType("jpeg");

        fileDto.setThumbFileInfoDto(thumbFileInfo);
        fileDto.setFileInfoDto(fileInfoDto);

        String xml = XmlUtil.generateXml(fileDto);
        System.out.println(xml);
    }

    /** * <?xml version=v1.0 encoding=utf-8?> * <msg:outboundMessageRequest * xmlns:msg="urn:oma:xml:rest:netapi:messaging:1"> * <messageId>c8c43a68-c7dd-44c6-b047-2e7e43d5d1b9</messageId> * <clientCorrelator>123456</clientCorrelator> * <bodyText> * <![CDATA[null]]> * </bodyText> * </msg:outboundMessageRequest> */
    @Test
    public void test02(){
    
        OutboundMessageRequestDto outboundMessageRequestDto = new OutboundMessageRequestDto();
        outboundMessageRequestDto.setMessageId(UUID.randomUUID().toString());
        outboundMessageRequestDto.setClientCorrelator("123456");
        outboundMessageRequestDto.setBodyText(null);
        String xml = XmlUtil.generateXml(outboundMessageRequestDto);
        System.out.println(xml);
    }

    /** * <?xml version="1.0" encoding="UTF-8"?> * <Request * xmlns:msg="urn:oma:xml:rest:netapi:messaging:1"> * <address>tel:+8619585550103</address> * <destinationAddress> * <phone>tel:+8619585550103</phone> * <phone>tel:+8619585550104</phone> * </destinationAddress> * <senderAddress>sip:[email protected]</senderAddress> * <Message> * <contentType> text/plain * </contentType> * <conversationID>XSFDEREW#%$^$%^^&^% * </conversationID> * <contributionID>SFF$#%$%%^%&^%THT * </contributionID> * <reportRequest>Delivered</reportRequest> * <reportRequest>Failed</reportRequest> * <serviceCapability> * <capabilityId>ChatbotSA</capabilityId> * <version>+g.gsma.rcs.botversion=&quot;#=1&quot;</version> * </serviceCapability> * <messageId>5eae954c-42ca-4181-9ab4-9c0ef2e2ac66</messageId> * <bodyText>Hello * </bodyText> * </Message> * <clientCorrelator>567895</clientCorrelator> * </Request> */
    @Test
    public void test03(){
    
        RequestDto requestDto = new RequestDto();
        requestDto.setAddress("tel:+8619585550103");
        List<String> destinationAddress = new ArrayList<String>(2);
        destinationAddress.add("tel:+8619585550103");
        destinationAddress.add("tel:+8619585550104");
        requestDto.setDestinationAddress(destinationAddress);
        requestDto.setSenderAddress("sip:[email protected]");

        MessageDto messageDto = new MessageDto();
        messageDto.setContentType("text/plain");
        messageDto.setConversationID(UUID.randomUUID().toString());
        messageDto.setContributionID(UUID.randomUUID().toString());
        List<String> reportRequest = null;
// reportRequest.add("Delivered");
// reportRequest.add("Failed");
        messageDto.setReportRequest(reportRequest);

        ServiceCapabilityDto serviceCapabilityDto = new ServiceCapabilityDto();
        serviceCapabilityDto.setCapabilityId("ChatbotSA");
        serviceCapabilityDto.setVersion("+g.gsma.rcs.botversion=&quot;#=1&quot;");
        messageDto.setServiceCapabilityDto(serviceCapabilityDto);
        messageDto.setMessageId(UUID.randomUUID().toString());
        messageDto.setBodyText("Hello");

        requestDto.setMessageDto(messageDto);
        requestDto.setClientCorrelator("567895");

        String xml = XmlUtil.generateXml(requestDto);
        System.out.println(xml);
    }
}

3.测试结果

test01

<?xml version=v1.0 encoding=utf-8?>
<file xmlns="urn:gsma:params:xml:ns:rcs:rcs:fthttp" xmlns:x="urn:gsma:params:xml:ns:rcs:rcs:up:fthttpext">
    <file-info type="thumbnail">
        <file-size>2048</file-size>
        <content-type>png</content-type>
    </file-info>
    <file-info file-disposition="file">
        <file-size>1024</file-size>
        <file-name>1.jpg</file-name>
        <content-type>jpeg</content-type>
    </file-info>
</file>

test02

<?xml version=v1.0 encoding=utf-8?>
<msg:outboundMessageRequest xmlns:msg="urn:oma:xml:rest:netapi:messaging:1">
    <messageId>50e1ba7a-dbb5-4f2f-8811-d81859d029ff</messageId>
    <clientCorrelator>123456</clientCorrelator>
    <bodyText>
        <![CDATA[null]]>
    </bodyText>
</msg:outboundMessageRequest>

test03

<?xml version=v1.0 encoding=utf-8?>
<Request xmlns:msg="urn:oma:xml:rest:netapi:messaging:1">
    <address>tel:+8619585550103</address>
    <destinationAddress>
        <phone>tel:+8619585550103</phone>
        <phone>tel:+8619585550104</phone>
    </destinationAddress>
    <senderAddress>sip:[email protected]</senderAddress>
    <Message>
        <contentType>text/plain</contentType>
        <conversationID>3869f2e2-aedf-424f-8928-3082dc284104</conversationID>
        <contributionID>f9d6b351-a245-4e79-b850-0700114361dd</contributionID>
        <reportRequest></reportRequest>
        <serviceCapability>
            <capabilityId>ChatbotSA</capabilityId>
            <version>+g.gsma.rcs.botversion=&quot;#=1&quot;</version>
        </serviceCapability>
        <messageId>35a43af0-df1d-4a0d-94a0-60492e520bc4</messageId>
        <bodyText>Hello</bodyText>
    </Message>
    <clientCorrelator>567895</clientCorrelator>
</Request>
原网站

版权声明
本文为[llp1110]所创,转载请带上原文链接,感谢
https://blog.csdn.net/qq_44981526/article/details/125156483