JSP自定义标签开发

一般情况下开发jsp自定义标签需要引用以下两个包

 

import javax.servlet.jsp.*;

import javax.servlet.jsp.tagext.*;

 

首先我们需要大致了解开发自定义标签所涉及到的接口与类的层次结构(其中SimpleTag接口与SimpleTagSupport类是JSP2.0中新引入的)。

一, 自定义标签  :实现了特定接口的java类,封装了java代码编写的预定义行为。 
 * 在运行时,标签被替换成相应的预定义java代码。

 * JSP 自定义标记提供了替代简单的 JavaBean 和 Java 脚本的方法。
  更好的是在 JSTL 中已存在一组已定义的标准的自定义标记库。

 * 目的在于将业务和表示逻辑分离,代码的可重用性,可移植性。

二,标签库:按照功能或实现进行分组的自定义标签的集合。

 1,API : javax.servlet.jsp.tagext包中的三个接口和2个类
  
 2,组成:
  * 标签处理程序类
  * 标签库描述符文件
  * 应用程序部署描述符
  * jsp文件

 3,自定义标签步骤:
  1,实现:实现tagext包定义的接口或类;
  2,配置:配置TLD文件;
  3,关联:配置web.xml文件;
 
三,标签处理程序类:一个自定义标签的实现类;

 1,标签处理程序是在运行时期调用。必须实现或扩展javax.servlet.jsp.tagext包定义的三个Java接口中的一个。

  * Tag接口 : 标签处理程序和JSP页面的基本协议。定义了所有标签处理程序的基本方法。
  * IterationTag接口:扩展Tag接口,控制对标签体的重复处理。
  * BodyTag接口:继承IterationTag接口,对标签体中内容进行处理。

 2,接口的默认实现:TagSupport和BodyTagSupprt类:

  * TagSupport类为Tag和IterationTag接口的默认实现。支持简单标签和带主体迭代的标签。
  * BodyTagSupprt类为BodyTag接口的默认实现。支持需要访问和操作标签主体内容的标签。

 3,实现Tag接口的标签处理程序类的生命周期:

  1,setPageContext():在标签处理器之前被JSP容器调用。设置标签的页面上下文,调用 setParent()方法设置该标签的父标签,没有则设置为NULL;
    setParent()
 
  2, 设置标签的属性: 有属性,调用setXxx方法设置标签属性,没有属性则跳过。
 
  3,doStartTag():返回EVAL_BODY_INCLUDE或SKIP_BODY
 
   EVAL_BODY_INCLUDE :输出标签体到当前的输出流
 
   SKIP_BODY:忽略标签体
 
  4,doEndTag(): 返回EVAL_PAGE或SKIP_PAGE
 
   EVAL_PAGE:执行页面余下部分
 
   SKIP_PAGE:忽略页面余下部分
 
  5,容器缓存标签处理器类实例,重复使用缓存的标签处理类实例
 
  6,release():释放实例,
 
 4,实现IterationTag接口的标签处理程序类的生命周期:
 
  1,setPageContext()
     setParent()
 
  2, 设置标签的属性
 
  3,doStartTag(): 返回EVAL_BODY_INCLUDE或SKIP_BODY
 
   EVAL_BODY_INCLUDE:
    执行标签体(被执行一遍);
    调用doAfterBody()方法:返回EVAL_BODY_AFTER或SKIP_BODY;
 
     EVAL_BODY_AFTER: 跳转到前面重新执行标签体
     SKIP_BODY:
   
   SKIP_BODY:
 
  4,doEndTag() : EVAL_PAGE或SKIP_PAGE
 
   EVAL_PAGE:执行页面余下部分
   SKIP_PAGE:
 
  5,从1开始
 
  6,release()

 
 5,实现BodyTag接口的标签处理程序类的生命周期:
 
  1,setPageContext()
    setParent()
 
  2, 设置标签的属性
 
  3,doStartTag(): 返回EVAL_BODY_BUFFERED或SKIP_BODY_INCLUDE,SKIP_BODY.
 
   EVAL_BODY_BUFFERED且标签体不为null:
    setBodyContent():
     设置标签处理器类的bodyContent属性,提供了BodyContent对象,缓存标签体静态内容和动态内容。
     调用doInitBody()为标签体执行做准备
      跳转到 执行标签体:
   SKIP_BODY_INCLUDE : 直接跳转到 执行标签体:
 
   SKIP_BODY : 忽略标签体,直接跳到 5
 
  4, 执行标签体(被执行一遍后);
  
   调用doAfterBody()方法:返回EVAL_BODY_AFTER或SKIP_BODY;
 
     EVAL_BODY_AFTER: 跳转到前面重新执行标签体
     SKIP_BODY:
   
   SKIP_BODY:不重复执行
 
  5,doEndTag() : EVAL_PAGE或SKIP_PAGE
 
   EVAL_PAGE:执行页面余下部分
   SKIP_PAGE:
 
  6,从1开始
 
  7,release()

 6,TagSupport类:支持简单标签和带主体迭代的标签。
 

 7, BodyTagSupprt类:支持需要访问和操作标签主体内容的标签。

 

 8,BodyContent类 :缓存输出内容。

  * JSP容器处理页面签创建JspWriter类实例,把所有输出引入该实例中(静态和动态内容等行为),
   如果JSP容器遇到的“自定义标签实现类”实现了BodyTag接口,就会临时将所以缓存在JspWriter实例中的所有输出重新定位到BodyContent类实例中缓存,直到自定义标签结束。
   所以BodyContent也是JspWriter类的子类,而JSP标签处理器也就可以通过该对象的getString()方法读到标签体中的内容了。

  public void flush()throws IOException    复写JspWrite.flush()方法以便它总是产生溢出。刷新写入已失效,因为它没有连接到将被写入的实际输出流中。
  public void clearBody()    重置BodyContent缓存为空。
  public Reader getReader()    返回Reader读取体内容。
  public String getString()    返回标签体中的内容。
  public void writeOut(Write w)    将体内容写入指定输出。
  public JspWrite getEnclosingWrite()    返回栈中下一个更高的写入者对象(可能是另一个BodyContent对象)。
 
 9,JSP标签处理器的生命周期
  1,初始化实例:创建标签实例,调用所有的设置方法(setPageContext,setParent方法和所有属性的设置方法)来初始化实例。
  2,调用doStartTag()方法将实例变量设为仅在当前调用的有效值。如果该方法正在处理元素的标签体,就会调用doEndTag方法。
  3,标签实例被重用,如果属性有不同的值则调用对应的设置方法,重复2的操作。只有在具有相同属性集合时,标签处理器才将实例重用。
  4,使用release方法让标签处理器释放内部占有资源。

 10,注意:
  * 为"属性"提供默认值。

  * 由于容器缓存标签处理器类实例,重复使用缓存的标签处理类实例。
    所以在改变类变量后,后面使用该类变量也会受影响。
    所以,要注意并发访问的问题。
     所以,要每次重设调用的状态。最佳地点是doStartTag方法中。

  * 标签在调用期间绝不会调用release();

  * 不要在setBodyContent方法和doInitBody方法中使用BodyContent对象,只用于获取该对象和做准备工作。

  * 对于实现BodyTag接口的自定义标签,同时使用空标签和不为空的标签,那就会抛出异常。因为空标签不会调用一些方法。

 
  
四,标签库描述符(TLD)文件:TLD是由在JSP页面中使用的标记所组成的库,用于配置标签;

 1,标签库描述符文件是描述一个标签库的XML文档,包含了标签的名字,属性,标签处理类和标签的属性等信息。
 2,扩展名为.tld,必须放在WEB-INF目录下。 

<taglib version="2.0"
 xmlns="http://java.sun.com/xml/ns/j2ee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd">
 
    <tlib-version>1.0</tlib-version>
    <jsp-version>1.2</jsp-version>
    <short-name>标签的简称</short-name>

    <tag>
      <name>标签的名字</name>
      <tag-class>标签处理类完整名</tag-class>
      <body-content>标记主体是否可以嵌入内容,以及内容如何处理。不可以使用empty</body-content>
    </tag>

</taglib>

tag元素的配置说明:
 
<tag>
 <name>tagNmae</name>-----这个Tag的名字
 <tagclass>package.Class</tagclass>-----这个Tag是由那个类实现的(这个class可以在struts.jar包中找到)
 <bodycontent>empty</bodycontent>-----这个Tag可以直接结尾,不需要填写内容
     这里bodycontent有三个可选值
         jsp  :标签体由其他jsp元素组成  
           如果其有jsp元素,那么标签会先解释,然后将元素的实际值传入。比如标签体里含有<%=attributeName%>这样子的jsp元素,此时标签会按attributeName的实际值是什么就传入什么。这个是最常用的一个。
         empty :标签体必须为空  
           在引用这个Tag的时候,可以<bean:write bundle="attributeName" />,而不必<bean:write bundle="attributeName" ></bean:write>
         tagdependent : 由标签解释,不带jsp转换

 <attribute> -----这里标识的是这个Tag的一个参数
  <name>attrName</name>--这个参数的名字
  <required>false</required>-----这个参数是否是必填,如果为true则必须写这个参数,否则会报错
  <rtexprvalue>true</rtexprvalue>------是说这个属性的值是否可以接收运行时的表达式。rtexprvalue:"RUN-TIME EXPRESSION VALUE",是否可以动态赋值,在jsp中如value="<%=attributeName%>"
 </attribute>
</tag>

 

六,建立 JSP 页面与标记库之间的引用 :静态引用与动态引用,关联标签;

 1,可以通过 Web 应用程序描述符(web.xml)声明一个静态引用,
 <?xml version="1.0" encoding="UTF-8"?>
 <web-app version="2.4"
  xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">


  <jsp-config>
   <taglib>
    <taglib-uri>/myTags</taglib-uri>
    <taglib-location>/WEB-INF/MyTLD.tld</taglib-location>
   </taglib>
  </jsp-config>
 
 </web-app>


 然后,将 JSP 声明加入到所有需要使用自定义标记库的页面中:

 <%@ taglib uri="myTags" prefix="abc" %>

  注意指定的 uri 属性与在 web.xml 文件中指定的 taglib-uri 值相匹配。

 2,可以直接在页面中声明一个动态引用。

 只需在所有需要使用这个库的页面中加入一个 JSP 声明即可:

 <%@ taglib uri="/WEB-INF/lib/DateTagLib.tld" prefix="abc" %>

  3,静态引用与动态引用比较:

 在进行标记库的静态引用时,JSP 声明必须查询 web.xml 文件以执行库查询。
 这意味着如果移动或者重命名了库,或者希望在 web.xml 文件中加入更多的库,
 就必须停止服务器、更新 web.xml 文件、然后重新启动服务器。
 动态方法让 JSP 页直接指向 TLD 位置,因而是在解释 JSP 页面时进行处理。

 静态方法提供了页面与库的实际名和位置之间一定程度的非直接性,
 这可以为您提供一些改变这些属性而不修改页面的灵活性。另一方面,动态方法提供了更大的灵活性,
 让您可以在运行时增加和移动标记声明。如果您对动态方法感兴趣,
 但是又担心做了一些改变后、有可能要更新多个页面的维护负担,
 那么您可以始终将 JSP 声明放到一个单独的 JSP 文件中,并在每一个要访问 Web 应用程序的自定义库的页面中加入这一页。
 这使您具有在运行时只需要更新信息一次就可以增加库的灵活性。


七,开发协作的行为
 1,使用显示的父子协作: 通过内层的子标签为父标签提供参数,以构成完整或补充的功能实现; 比方<jsp:include>动作元素中的子元素<jsp:param>.
  
  * 使用TagSupport.findAncestorWithClass(this, ParamParent.class)方法在子标签中获取父标签的引用。
  * 调用父标签的方法将子类的参数传递给父类。
  * 父标签为实现了特定的接口,该接口中定义了子标签中的参数如何传递给父标签,供子标签调用。
 
 2,通过变量使用隐式协作:
  通过设置JSP作用域的变量而隐式进行协作。一个行为将其处理结果作为某个jsp作用域中的变量,而另外一个行为则使用变量作为其输入。例如迭代行为。

八,在标签库中使用监听器:
 使用<Listener>元素放在<validator>后面在TLD中为自己的库定义监听器实现。
 当容器载入WEB应用程序的时候,会遍历所有的TLD来查找监听器定义,并且注册所找到的监听器。
  
九,数据类型转换

 1,JSP容器自动处理从文本值到属性值的基本数据类型的转换。
 2,使用PropertyEditor进行转换:将文本值转换为任何java数据类型。

 

十,异常处理

 如果JSP元素抛出一个异常,对它的默认处理是发送到一个错误页面。
 但对于特殊标记(如文件操作)处理必须作出显示的处理。JSP1.2新接口TryCatchFinally接口专门来处理异常。
 如果嵌套行为体中的元素doStartTag(),doEndTag(),doInitBody()或doAfterBody方法中的任何一个抛出异常。
 容器会调用doCatch方法。我们可以在该方法中记录问题,抛出自己的异常。页面对其他部分的处理也会继续。

 doFinally方法总是由容器调用。当在doStartTag方法抛出异常,就会无法创建PrintWriter对象输出信息,所以要在检查null。

 

例子 

目标1:自定义一个用表格显示用户信息的简单标签

 

效果图:

在jsp页面使用此自定义标签:

 

假设我们有一个UserInfo的javabean,那么在JSP页面使用此标签只需调用此标签即可

 

<!-- 创建需要展现UserInfo的实例(用于测试数据) -->

    <%

    UserInfo user = new UserInfo();

    user.setUserName("Xuwei");

    user.setAge(33);

    user.setEmail("test@test.test");

    pageContext.setAttribute("userinfo", user);  

    %>

 

    <!-- 给标签设置user属性绑定要展现的UserInfo对象  -->

    <cc:showUserInfo user="${pageScope.userinfo }" />

 

开发步骤:

 

简单标签的开发我们只要实现Tag接口即可,为了简单起见可以直接继承实现了此接口的TagSupport类

 

1 创建自定义标签类

 

public class UserInfoTag extends TagSupport {

   

    private UserInfo user;

 

    @Override

    public int doStartTag() throws JspException {

        try {

            JspWriter out = this.pageContext.getOut();

            if(user == null) {

                out.println("No UserInfo Found...");

                return SKIP_BODY;

            }

            out.println("<table width='500px' border='1' align='center'>");

            out.println("<tr>");

            out.println("<td width='20%'>Username:</td>");

            out.println("<td>" + user.getUserName() + "</td>");

            out.println("</tr>");

            out.println("<tr>");

            out.println("<td>Age:</td>");

            out.println("<td>" + user.getAge() + "</td>");

            out.println("</tr>");

            out.println("<tr>");

            out.println("<td>Email:</td>");

            out.println("<td>" + user.getEmail() + "</td>");

            out.println("</tr>");

            out.println("</table>");

        } catch(Exception e) {

            throw new JspException(e.getMessage());

        }

        return SKIP_BODY;

    }

   

    @Override

    public int doEndTag() throws JspException {

        return EVAL_PAGE;

    }

 

    @Override

    public void release() {

        super.release();

        this.user = null;

    }

   

    //getter and setters

    public UserInfo getUser() {

        return user;

    }

    public void setUser(UserInfo user) {

        this.user = user;

    }

}

 

2 在Web-Inf创建标签库描述文件.tdl(Tag Library Description)

 

<?xml version="1.0" encoding="UTF-8"?>

<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"

 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd">

<tlib-version>1.0</tlib-version>

<jsp-version>2.0</jsp-version>

<short-name>cc</short-name>

<uri>/mytaglib</uri>

<tag>

    <name>showUserInfo</name>

    <tag-class>com.mytags.UserInfoTag</tag-class>

    <body-content>empty</body-content>

    <attribute>

        <name>user</name>

         <required>false</required>

         <rtexprvalue>true</rtexprvalue>

    </attribute>

 </tag>

</taglib>

 

3 配置web.xml

 

<jsp-config>

    <taglib>

        <taglib-uri>/mytaglib</taglib-uri>

        <taglib-location>/WEB-INF/mytaglib.tld</taglib-location>

    </taglib>

  </jsp-config>

 

4 在需要使用此标签的jsp页面头部引入

 

<%@ taglib uri="/mytaglib" prefix="cc"%>

 

5 使用(参照上面的使用步骤)

 

此致,一个简单的JSP标签开发完成

 

标签类说明:

我们创建的UserInfoTag类继承了TagSupport类,而它又实现了Tag接口,Tag接口的生命周期由其所在的容器控制,如下图:

 

setPageContext() 将所在jsp页面的pageContext注入进来,目的是为了在后面的方法中可以访问到jsp页面对象的pageContext属性

 

setParent()       设置此标签的父标签

 

setAttribute()   将标签中的属性注入到此class的属性,不需要自己实现但要提供属性的get与set方法

 

doStartTag()      在开始标签属性设置后调用,如果返回SKIP_BODY则忽略标签之中的内容,如果返回EVAL_BODY_INCLUDE则将标签体的内容进行输出

 

doEndTag()         在结束标签之前调用,返回SKIP_PAGE跳过整个jsp页面后面的输出,返回EVAL_PAGE执行页面余下部分

 

release()          生命周期结束时调用

 

特别说明:在tomcat4.1之后的版本中默认开启了标签缓冲池(websphere和weblogic并不会这么做),所以执行完标签后并不会执行release()方法(_jspDestroy()时才释放),也就是说同一个jsp页面自定义标签不管使用多少次只会存在一个实例,但也并不是每一个标签都会为其创建一个缓冲池,要根据参数来判断,例如:

<cc:UserInfoTag user=”…” />

<cc:UserInfoTag />

上面例子中由于参数不同就会创建两个标签缓冲池。

 

这个问题可以通过设定tomcat的配置文件加以解决:
在%tomcat%\conf\web.xml加入enablePooling参数,并设置为false(不缓存自定义标签)。

<init-param>
  <param-name>enablePooling</param-name>
  <param-value>false</param-value>
</init-param>
 

清空%tomcat%\conf\目录

-------------------------------------------------------------------------------------------------------------------------------

TagSupport类已经为我们实现并扩展了一些方法(比如在上述方法中我们可以直接使用pageContext对象,调用父标签getParent()等),所以一般情况下我们只需重写doStartTag(),doEndTag() 即可

 

TLD文件说明:

<!--版本号-->

<tlib-version>1.0</tlib-version>

<jsp-version>2.0</jsp-version>

<short-name>cc</short-name>

<tag>

<!—指定标签名 -->

    <name>showUserInfo</name>

<!—指定标签类文件的全路径 -->

    <tag-class>com.mytags.UserInfoTag</tag-class>

<!--如果不需要标签体则设置empty,反之设定jsp -->

    <body-content>empty</body-content>

<!—设定属性(如果有的话) -->

    <attribute>

<!—指定标签名 -->

       <name>user</name>

<!—是否是必须,如果非必须没设置则为空 -->

        <required>false</required>

<rtexprvalue>true</rtexprvalue><!—是否可在属性中使用表达式 -->

    </attribute>

</tag>

 

 

Web.xml文件说明:

<jsp-config>

    <taglib>

<!--

标签库的uri路径

即jsp头文件中声明<%@ taglib uri="/mytaglib" prefix="cc"%>

的uri

 -->

        <taglib-uri>/mytaglib</taglib-uri>

<!—tld文件所在的位置-->

        <taglib-location>/WEB-INF/mytaglib.tld</taglib-location>

    </taglib>

  </jsp-config>

 

 

目标2:自定义一个类似于Asp.Net中的Reapter控件的标签

效果图:

 

在jsp页面使用此自定义标签:

 

<!-- 创建需要展现javabean(UserInfo)集合的实例(用于测试数据) -->

<%

    List<UserInfo> users = new ArrayList<UserInfo>();   

    users.add(new UserInfo("Zhangsan", 12, "Zhangsan@163.com"));

    users.add(new UserInfo("Lisi", 22, "Lisi@sina.com"));

    users.add(new UserInfo("Wangwu", 33, "Wangwu@qq.com"));

    pageContext.setAttribute("users", users);

%>

 

<!-- 给标签绑定数据源  -->

<table width='500px' border='1' align='center'>

    <tr>

        <td width='20%'>UserName</td>

        <td width='20%'>Age</td>

        <td>Email</td>

    </tr>

    <cc:repeater var="item" items="${pageScope.users }">

        <tr>

            <td>${item.userName }</td>

            <td>${item.age }</td>

            <td>${item.email }</td>

        </tr>

    </cc:repeater>

</table>

 

开发步骤:

 

要完成此控件我们需要实现一个迭代接口,即IterationTag,由于TagSupport同样实现了此接口,所以我们继承此类

 

1 创建自定义标签类

 

public class Repeater extends TagSupport {

    private Collection items;

    private Iterator it;

    private String var;

 

    @Override

    public int doStartTag() throws JspException {

        if(items == null || items.size() == 0) return SKIP_BODY;

        it = items.iterator();  

        if(it.hasNext()) {

            pageContext.setAttribute(var, it.next());

        }

        return EVAL_BODY_INCLUDE;

    }

   

    @Override

    public int doAfterBody() throws JspException {

        if(it.hasNext()) {

            pageContext.setAttribute(var, it.next());

            return EVAL_BODY_AGAIN;

        }

        return SKIP_BODY;

    }

   

    @Override

    public int doEndTag() throws JspException {

        return EVAL_PAGE;

    }

   

    public void setItems(Collection items) {

        this.items = items;

    }

   

    public void setVar(String var) {

        this.var = var;

    }

}

 

2在Web-Inf创建标签库描述文件.tdl(Tag Library Description)

由于目标1种已经创建了此文件,我们只需增加此标签的配置即可

 

<tag>

    <name>repeater</name>

    <tag-class>com.mytags.Repeater</tag-class>

    <body-content>jsp</body-content>

    <attribute>

        <name>items</name>

        <required>false</required>

        <rtexprvalue>true</rtexprvalue>

    </attribute>

    <attribute>

        <name>var</name>

        <required>true</required>

        <rtexprvalue>true</rtexprvalue>

    </attribute>

 </tag>

 

3 配置web.xml (目标1中已完成,无需修改)

 

4 在需要使用此标签的jsp页面头部引入

<%@ taglib uri="/mytaglib" prefix="cc"%>

 

5 使用(参照上面的使用步骤)

 

标签类说明:

我们用到了迭代接口,以下是容器处理此接口的流程

 

 

作为目标1中的补充: 在doAfterBody()如果返回值是EVAL_BODY_AGAIN那么将重新执行此方法

 

目标3:使用BodyTagSupport

此目标并不会使用实际例子进行显示,主要是说明为什么,什么情况下需要使用到BodyTag接口或者BodyTagSupport类?

如果我们需要在<test>  ….   </test>之间的标签体的头部和尾部加上一些标记或者是其他处理,一般的处理方法是在doStartTag和doEndTag方法中进行, 但是如果是个迭代标签,标签体的每段内容在循环输出时每次都需要在头部和尾部加上一些标记,我们使用BodyTagSupport就很方便了,

 

此接口在doStartTag()方法返回值多了一个EVAL_BODY_BUFFERED,它将对主体进行计算,并输出到缓冲区(注:此处是缓冲区并非直接输出到客户端,需要我们手动(this.bodyContent.getEnclosingWriter().write(this.bodyContent.getString());)进行输出客户端的调用,否则主体内容不会进行显示)

 

标签类说明:

关于BodyTagSupport接口的说明

 

目标4:自定义的函数库

1 创建函数库类

 

public class MyFunctions {

    public static String formatMyName(String name) {

       return "your name is " + name;

    }

    public static int add(int a, int b) {

       return a+b;

    }

}

 

2 在TLD文件中配置 (引用于目标1中的tld文件)

<function>

    <name>formatMyName</name>

    <function-class>com.taglib.MyFunctions</function-class>

    <function-signature>java.lang.String formatMyName(java.lang.String)</function-signature>

 </function>

 

<function>

    <name>add</name>

    <function-class>com.taglib.MyFunctions</function-class>

    <function-signature>java.lang.String add(int, int)</function-signature>

 </function>

 

3 JSP中调用

 

${cc:formatMyName("wangfei") }

${cc:add(12, 34) }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值