Servlet 2.3:新特征(转)

题注:目前servlet和jsp是用来开发web应用程序最流行的工具之一,本文由权威的servlet专家Jason Hunter撰写,全面的而准 确的介绍了从Servlet API 2.2 到 2.3(目前)的变化和原因,并展示了在servlets中如何使用filter的激动人心的新技术。

翻 译者加:虽然Servlet2.4的规范即将出台,但相信此篇文章对于那些刚刚开始运用Servlet的爱好者们还会有所帮助。(鉴于此篇文章是在 Servlet 2.3规范正式出台之后转译,故有关Filter的部分未按原文,而是按照2.3最终规范中的修改的部分所翻译)

英文原文

改变:
· 需要JDK1.2或更高版本
· 增加FILTER机制
· 增加应用程序生命周期事件
· 增加新的国际化支持
· 增加有关inter-JAR依赖关系的规定
· 澄清了加载类的规则
· 增加新的错误和安全属性
· 废弃HttpUtils类
· 增加了各种各样有用的方法
· 扩展和澄清了几个DTD行为
· 其他有关Server开发商的有关说明改动

与J2EE的关系
· Servlet API 2.3成为J2EE 1.3的核心API
· 在web.xml中定义了J2EE相关的deployment descriptor
· <resource-env-ref>标记被用来支持如Java Messaging System (JMS)所必须的"administered objects"
· <res-ref-sharing-scope>标记规定了对于资源引用的访问方式
· <run-as>规定了EJB调用者的安全特性
· 大部分的Servlet开发者不需要关心这些J2EE标记

过滤器(filters)
Servlet API 2.3 中最重大的改变是增加了filters,filters能够传递request或者修改response。 Filters并不是servlets;它们 不能产生response。 你可以把过滤器看作是还没有到达servlet的request的预处理器,或从servlet发出的response的后 处理器。 一句话,filter代表了原先的"servlet chaining"概念,且比"servlet chaining"更成 熟。 filter能够:
· 在servlet被调用之前截取servlet
· 在servlet被调用之前查看request
· 提供一个自定义的封装原有request的request对象,修改request头和request内容
· 提供一个自定义的封装原有response的response对象,修改response头和response内
· 在servlet被调用之后截取servlet
· 对serlvet进行分组管理;每个servlet或每组servlet可以配置多个filters

javax.servlet.Filter实现了以下方法:
· public void init(FilterConfig filterConfig)throws ServletException:
初始化操作
· public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)throws java.io.IOException,ServletException: 执行实际的过滤的操作
· public void destroy():释放资源

每个request和response都会传到 filter的doFilter()方法中, 也就是说在FilterChain对象中包含着所有将被执行的filters。一个filter可以在 doFilter()方法中对request和response进行处理。 (它能够通过调用其他的方法获得数据,或赋予获得的对象新的行为。) 然后, 当前的filter调用chain.doFilter()方法把控制权传递给下一个filter。 在调用返回时, filter能够在 doFilter()方法结束的地方,对response进行附加的处理,例如,对response进行日志管理。如果filter想停止request 处理过程并获得对response的完全控制,则不要去调用下一个filter。

filter能够通过包装request和/或 response对象来提供自定义的行为,改变某个方法的调用实现影响以后的request操作动作.API 2.3提供了新的 HttpServletRequestWrapper和HttpServletResponseWrapper类来协助实现;这两个类提供了所有 request和response方法的缺省实现, 并且缺省地代理了所有对原有request和response的调用。这意味着要改变一个方法的行为 只需要从封装类继承并重新实现一个方法即可。封装类在处理request和生成response方面为filters提供了极大的控制方式。对所有的 request的执行过程进行记录的一个简单的日志过滤器的例子代码如下:

  1. import java.io.*;
  2. import javax.servlet.*;
  3. import javax.servlet.http.*;
  4. public class LogFilter implements Filter {
  5.     FilterConfig config;
  6. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
  7. throws ServletExceptionIOException {
  8.         ServletContext context =config.getServletContext();
  9.         long bef = System.currentTimeMillis();
  10.         chain.doFilter(req, res); // no chain parameter needed here
  11.         long aft = System.currentTimeMillis();
  12.         context.log("Request to " + ((HttpServletRequest)req).getRequestURI() + ": " + (aft-bef));
  13.     }
  14.     public void init (FilterConfig config) throws ServletException{
  15.         this.config = config;
  16.     }
  17.     public void destroy () {
  18.         this.config = null;
  19.     }
  20. }


当server调用init()方法时, filter把config的引用保存到config变量中, 在后 面用到的doFilter()方法中将使用这个变量获得ServletContext.。doFilter()方法中的实现很简单:对request进行 计时并在处理完成的时候记录下来。 要使用filter, 必须在web.xml文件中定义<filter>标记, 如下所示:[pre]
<filter>
    <filter-name>log</filter-name>
    <filter-class>LogFilter</filter-class>
</filter>[/pre]
上述定义告诉server名字为log的filter的实现类是LogFilter类,然后使用<filter-mapping>标记把注册过的filter与特定的URL映射模式或servlet名对应起来:
[pre]
<filter-mapping>
    <filter-name>log</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>[/pre]
这个配置使filter对所有发送到server的请求起作用(包括静态请求和动态请求),达到了日志过滤器的作用。 当你访问一个简单的页面上时,输出的日志有可能如下:

Request to /index.jsp: 10

生命周期事件
Servlet API 2.3第二个意义重大的改变是增加了应用程序生命周期事件,即当servlet contexts和sessions被初始化或释放时,当向context或session上添加或删除属性的时候,通知"listener"对象.

Servlet生命周期事件与Swing的事件机制类似。对ServletContext生命周期进行观察的监听器必须实现 ServletContextListener接口。这个接口有两个方法:
· void contextInitialized(ServletContextEvent e): 当Web application第一次准备处理requests的时候(例如:Web server启动并且context被加载和重新加载时)。 本 方法返回之前不开始处理Requests
· void contextDestroyed(ServletContextEvent e):在 Web application要被关闭的时候(例如:当Web server关闭或当一个context被移去或重新加载的时候)。本方法调用时 Request处理已经停止.

传入这些方法的ServletContextEvent类只有一个返回被初始化或被移去的context的getServletContext()方法。

对ServletContext属性生命周期进行观察的监听器必须实现ServletContextAttributesListener接口,这个接口有三个方法:
· void attributeAdded(ServletContextAttributeEvent e):当属性被加到servlet context上时调用
· void attributeRemoved(ServletContextAttributeEvent e):当属性被从servlet context上移走时调用
· void attributeReplaced(ServletContextAttributeEvent e):当servlet context上的属性被另一个属性所代替的时候调用

继 承于ServletContextEvent的ServletContextAttributeEvent类,增加了getName()和 getValue()方法,这两个方法使监听器得到正在被改变的属性的相关信息。现在对于需要同步应用程序状态(context属性)的应用程序来说,如 数据库处理,能够方便的在这个地方进行统一的处理。

session listener model与context listener model类似。在session事件模型中, 有一个拥有两个方法的HttpSessionListener接口:
· void sessionCreated(HttpSessionEvent e):当session创建时被调用
· void sessionDestroyed(HttpSessionEvent e):当session被释放时或无效时被调用

这些方法接受一个拥有返回被创建或被释放的session的getSession()方法的HttpSessionEvent类实例。可以使用这些方法实现一个对Web application中所有的活动用户进行跟踪的管理者接口。

session事件模型还有一个HttpSessionAttributesListener接口,这个接口有三个方法。这些方法告诉监听器属性何时改变,何时能够被使用:
· void attributeAdded(HttpSessionBindingEvent e):当属性被加到session上时调用
· void attributeRemoved(HttpSessionBindingEvent e):当属性被从session上移去时调用
· void attributeReplaced(HttpSessionBindingEvent e):当session上的属性被另一个属性所代替的时候调用

与 你所猜想的一样, HttpSessionBindingEvent类继承于HttpSessionEvent接口,并且加入了getName()和 getValue()方法. 唯一一点不一样的地方是事件类的名字是HttpSessionBindingEvent,而不 是 HttpSessionAttributeEvent。合理的解释是:原有的API已经存在了一个名为 HttpSessionBindingEvent的事件类, 因此这个事件类被重用。 

生命周期事件的一个实际应用由context监听器管理共享数据库连接。在web.xml中如下定义监听器:[pre]
<listener>
    <listener-class>com.acme.MyConnectionManager</listener-class>
</listener>[/pre]server创建监听器的实例接受事件并自动判断实现监听器接口的类型。要记住的是由于监听器是配置在deployment descriptor中,所以不需要改变任何代码就可以添加新的监听器。监听器的例子如下:

  1. public class MyConnectionManager implements ServletContextListener {
  2.     public void contextInitialized(ServletContextEvent e) {
  3.         Connection con = // create connection
  4.         e.getServletContext().setAttribute("con", con);
  5.     }
  6.     public void contextDestroyed(ServletContextEvent e) {
  7.         Connection con = (Connection) e.getServletContext().getAttribute("con");
  8.         try { con.close(); } catch (SQLException ignored) { } // close connection
  9.     }
  10. }


监听器保证每新生成一个servlet context都会有一个可用的数据库连接,并且所有的连接对会在context关闭的时候 随之关闭。

API 2.3 新增的HttpSessionActivationListener监听器接口的目的是处理在server之间传送的session。实现 HttpSessionActivationListener的监听器在session要被钝化(移动)和session在其它的主机上要被激活(开始活 动)的时候被通知。使用这个接口可以在JVM之间长久保存没有进行序列化的数据,或者是在移动session前后对序列化的对象执行一些附加的操作。这个 接口有两个方法:
· void sessionWillPassivate(HttpSessionEvent e):session将被钝化。 此时session已经超出了service的管理范围
· void sessionDidActivate(HttpSessionEvent e):session已经被激活。此时session还没有进入service的管理范围

这个监听器接口与其他的监听器接口在原理上类似。然而与其它监听器不同的是,钝化和激活的调用很有可能发生在两个不同的server中!

选择字符编码方式
API 2.3 对处理不同语言的表单提交方式的迫切需要提供了新方法,request.setCharacterEncoding(String encoding), 告诉server一个request的字符编码。字符编码也叫做字符集,反映了字节到字符的映射模式。server能够使用指定的字符集正确的解析参数和 POST数据。缺省情况下,server使用常用的Latin-1(ISO 8859-1)字符集解析参数。然而这只能在使用西方和欧洲的语言的情况下正 常工作。当浏览器使用其他的字符集时,假设在request的Content-Type头中传送编码信息,但是没有多少浏览器这样做。通过使用这个方 法,servlet能够通知server使用哪一种字符集,server只需关注其余部分。例如, servlet从以Shift_JIS编码方式的表单 接受并读取Japanese参数的代码如下:[pre]
// Set the charset as Shift_JIS
req.setCharacterEncoding("Shift_JIS");

// Read a parameter using that charset
String name = req.getParameter("name");[/pre]
记住在调用getParameter()方法或getReader()方法之前设置编码。setCharacterEncoding()对于不支持的编码会抛出java.io.UnsupportedEncodingException。

JAR dependencies
通 常情况下,WAR文件(Web application归档文件,在API 2.2中加入)需要各种JAR类库对server提供必要的支持和正确的操 作。例如,一个使用ParameterParser类的Web application需要在它的classpath存在cos.jar文件。使用 WebMacro的Web application需要webmacro.jar文件。在API 2.3之前,这些要用到的从属文件必须在文档中说明(相 当于每个人都要读说明文档!) 或者是每个Web application不得不把它所需要的所有的jar文件包含在自身的WEB-INF/lib目录中 (完全无必要的使Web application变得膨胀起来)。

在Servlet API 2.3中,可以通过WAR文件中的 META-INF/MANIFEST.MF文件来表示从属的jar文件与WAR文件之间的关系。 这是定义jar文件依赖关系的标准方法,但是在 API 2.3中,WAR 文件必须正式的支持上述的机制。如果一个规定的从属条件不能够被满足,server能够在拒绝Web application 的发布,而不是在运行时刻产生晦涩难懂的错误消 息。 The mechanism allows a high degree of granularity(不知如何翻译)。例如,可以指定一个可选 包的特定的版本作为Web application的从属包,那么server就必须以一种算法找到所指定的那个包。

Class loaders
在这儿虽然只改动了一点点,但是影响很大:在API 2.3中,一个servlet容器必须确保server的实现类对于Web application所使用的类是不可见的。换句话说,class loaders应该被独立的保存起来。

这 听起来好像没什么,但是这样做消除了Web application的classes和server classes之间可能存在冲突的机会。在使用 XML解析器上这已经成为一个严重的冲突问题。每个server都需要一个XML解析器解析web.xml文件,并且许多 Web applications也使用XML解析器对XML数据进行读,写或其他处理。如果解析器支持不同版本DOM或SAX,就可能导致无法挽回的冲 突。规定不同的类的独立性很好的解决了这个问题。

新的错误属性
在Servlet API 2.2中引入了几个 request属性,引入的属性在满足<error-page>规定的条件时,能够被Servlet和JSP所用到。它的作用是让你可以在 Web application中配置当出现特定的错误类型和特定的异常类型时,可以显示个用户指定的页面:[pre]
<web-app>
<!-- ..... -->
    <error-page>
        <error-code>404</error-code>
        <location>/404.html</location>
    </error-page>
    <error-page>
        <exception-type>javax.servlet.ServletException</exception-type>
        <location>/servlet/ErrorDisplay</location>
    </error-page>
<!-- ..... -->
</web-app>[/pre]
由<error-page>标记所规定的的在<location>标记中的servlet能够使用下表的三个属性:
· javax.servlet.error.status_code:错误类型的整形值
· javax.servlet.error.exception_type:产生错误的异常类的实例
· javax.servlet.error.message:异常信息字符串

使用这些属性,servlet能够自定义错误页面,例:

  1. import java.io.*;
  2. import javax.servlet.*;
  3. import javax.servlet.http.*;
  4. public class ErrorDisplay extends HttpServlet {
  5.     public void doGet(HttpServletRequest req, HttpServletResponse res)
  6. throws ServletExceptionIOException {
  7.         res.setContentType("text/html");
  8.         PrintWriter out = res.getWriter();
  9.         String code = null, message = null, type = null;
  10.         Object codeObj, messageObj, typeObj;
  11.         // Retrieve the three possible error attributes, some may be null
  12.         codeObj = req.getAttribute("javax.servlet.error.status_code");
  13.         messageObj = req.getAttribute("javax.servlet.error.message");
  14.         typeObj = req.getAttribute("javax.servlet.error.exception_type");
  15.         // Convert the attributes to string values
  16.         // We do things this way because some old servers return String
  17.         // types while new servers return Integer, String, and Class types.
  18.         // This works for all.
  19.         if (codeObj != null) code = codeObj.toString();
  20.         if (messageObj != null) message = messageObj.toString();
  21.         if (typeObj != null) type = typeObj.toString();
  22.         // The error reason is either the status code or exception type
  23.         String reason = (code != null ? code : type);
  24.         out.println("<HTML>");
  25.         out.println("<HEAD><TITLE>" + reason + ": " + message + "</TITLE></HEAD>");
  26.         out.println("<BODY>");
  27.         out.println("<H1>" + reason + "</H1>");
  28.         out.println("<H2>" + message + "</H2>");
  29.         out.println("<HR>");
  30.         out.println("<I>Error accessing " + req.getRequestURI() + "</I>");
  31.         out.println("</BODY></HTML>");
  32.     }
  33. }


设想一下,如果错误页面中能够包含产生异常的堆栈信息或者包含导致问题产生的servlet的URI(导致问题 产生的servlet的URI通常都不是最开始进行请求的那个URI),情况如何?在API 2.2中是不可能的。而API 2.3中可以从以下表的两个 属性获得这些信息:
· javax.servlet.error.exception:实际的异常掷出的Throwable对象
· javax.servlet.error.request_uri:导致问题产生的资源的URI字符串对象

这些属性能够让错误页面包含异常的堆栈信息和产生问题的资源的URI。下面的servlet例子使用了新属性。

  1. import java.io.*;
  2. import javax.servlet.*;
  3. import javax.servlet.http.*;
  4. public class ErrorDisplay extends HttpServlet {
  5.     public void doGet(HttpServletRequest req, HttpServletResponse res)
  6. throws ServletExceptionIOException {
  7.         res.setContentType("text/html");
  8.         PrintWriter out = res.getWriter();
  9.         String code = null, message = null, type = null, uri = null;
  10.         Object codeObj, messageObj, typeObj;
  11.         Throwable throwable;
  12.         // Retrieve the three possible error attributes, some may be null
  13.         codeObj = req.getAttribute("javax.servlet.error.status_code");
  14.         messageObj = req.getAttribute("javax.servlet.error.message");
  15.         typeObj = req.getAttribute("javax.servlet.error.exception_type");
  16.         throwable = (Throwable) req.getAttribute
  17.            ("javax.servlet.error.exception");
  18.         uri = (String) req.getAttribute("javax.servlet.error.request_uri");
  19.         if (uri == null) {
  20.             uri = req.getRequestURI(); // in case there’s no URI given
  21.         }
  22.         // Convert the attributes to string values
  23.         if (codeObj != null) code = codeObj.toString();
  24.         if (messageObj != null) message = messageObj.toString();
  25.         if (typeObj != null) type = typeObj.toString();
  26.         // The error reason is either the status code or exception type
  27.         String reason = (code != null ? code : type);
  28.         out.println("<HTML>");
  29.         out.println("<HEAD><TITLE>" + reason + ": " + message 
  30.           + "</TITLE></HEAD>");
  31.         out.println("<BODY>");
  32.         out.println("<H1>" + reason + "</H1>");
  33.         out.println("<H2>" + message + "</H2>");
  34.         out.println("<PRE>");
  35.         if (throwable != null) {
  36.             throwable.printStackTrace(out);
  37.         }
  38.         out.println("</PRE>");
  39.         out.println("<HR>");
  40.         out.println("<I>Error accessing " + uri + "</I>");
  41.         out.println("</BODY></HTML>");
  42.     }
  43. }


新的安全属性
Servlet API 2.3增加了两个新的request属性,有助于一个使用HTTPS连接的servlet获得有关的某些信息。对于使用HTTPS加密的servlet,server支持下面列出的新加的request属性:
· javax.servlet.request.cipher_suite:HTTPS所使用的密码组
· javax.servlet.request.key_size:加密算法所使用的密钥长度

servlet可以在程序中使用这些属性决定是否一个网络连接拥有足够的安全强度。应用程序可以拒绝加密位数不够长或使用不信任算法的那些连接。例如,下面的方法判断连接是否使用了不低于128位长度的密钥进行加密。

  1. public boolean isAbove128(HttpServletRequest req) {
  2.     Integer size = (Integer) req.getAttribute("javax.servlet.request.key_size");
  3.     if (size == null || size.intValue() < 128) {
  4.         return false;
  5.     }
  6.     else {
  7.         return true;
  8.     }
  9. }


细微的调整
在API 2.3中还存在一些细微的调整之处。首先,在 HttpServletReques类中定义了新的四个静态的不可变的字符串型的常 量:BASIC_AUTH,DIGEST_AUTH,CLIENT_CERT_AUTH,和FORM_AUTH。与之对应的是用来获得客户端使用哪一种认 证方式的getAuthType()方法的代码可以如下书写:

  1. if (req.getAuthType() == req.BASIC_AUTH) {
  2.     // handle basic authentication
  3. }


与API 2.2兼容,原有的判断方式同样能够使用,但是不符合代码的规范性。注意将"BASIC"放在equals()之前能够避免在getAuthType()方法返回null的时候产生空指针异常:

  1. if ("BASIC".equals(req.getAuthType())) {
  2.     // handle basic authentication
  3. }


另一个API 2.3中的改变是HttpUtils类,这个类不再被推荐使用。HttpUtils可以被认为是 一组静态方法的集合,这些方法有时非常有用,但是可以把他们放到其他更合适的地方。 API 2.3把这些功能移到了request对象中,显得更为合 理。request对象中的新方法如下:
· StringBuffer req.getRequestURL():返回一个从request信息中获得的包含原始· request URL的StringBuffer对象
· java.util.Map req.getParameterMap(): 返回一个包含request参数的不可变的Map对象。参数名和参数值分别对应Map对象中的关键字和值。但是不能确定拥有多个参数值的参数的处理方式, 很可能,所有的值由一个String[]对象表示。

这些方法使用新加的req.setCharacterEncoding()方法来处理字符编码转换。

API 2.3同样增加了两个关于ServletContext的新方法,这两个新方法可以得到context的名字和context的资源列表
· String context.getServletContextName():返回在web.xml中定义的context的名字
· java.util.Set context.getResourcePaths(): 返回context中所有可用的资源路径,返回值类型为一个包含String对象的不可变的Set对象。每个String对象的内容以斜线(’/’)开始 并相对于context的根目录

response对象也增加了一个新的方法,这个方法增加了对response buffer的程序上的 控制。API 2.2中引入了res.reset()方法重置response和清空response的内容,头和状态码。API 2.3添加了 res.resetBuffer()方法只用来清空response内容: 
· void res.resetBuffer():清空response buffer,不清空头和状态码。如果response已经被输出,掷出IllegalStateException异常。

最 后,在一组专家们长期的辩论后,Servlet API 2.3确定了当一个不在context根目录的servlet调用 res.sendRedirect("/index.html")的时候,将会产生什么行为。在Servlet API 2.2 中对一个不完整的路径 如"/index.html"发出请求,将会被servlet container转换成一个完整的路径,但是没有说明context路径是如何被转换 的。如果context中的servlet要访问的路径是"/index.html",redirect URI将这个路径转换成相对容器的根目录 (http://server:port/index.html),还是相对于context的根目录(http://server:port /contextpath/index.html)呢?从最大的可移植性方面考虑,这个行为必须被确定下来;在争论了很长时间之后,专家们选择了将其转换 化成相对于容器根目录的方案。对于那些坚持相对于context的人,可以使用getContextPath()方法获得相对于context的URL结 果。

DTD的修正
最后,Servlet API 2.3增加了对web.xml deployment descriptor行 为的一些要求。在使用web.xml文件之前,必须除去文本值两端的空格。(在不需要进行校验的标准XML中,通常情况下保留所有的空格。)这个规定保证 了如下的两个条目以相同的方式被进行处理:

  1. <servlet-name>hello<servlet-name>
  2. <servlet-name>
  3. hello
  4. </servlet-name>


API 2.3提供了<auth-constraint>标记规则,特殊值"*"作 为<role-name>的通配符表示允许所有的roles访问。如下的xml定义表示当前Web application中所有的用户只要 属于Web application中所定义任何一个role,就可以对响应的web资源进行访问:[pre]
<auth-constraint>
    <role-name>*</role-name> <!-- allow all recognized roles -->
</auth-constraint>[/pre]
最后,澄清了在isUserRole()方法中所用到的参数问题,参数可以使用在<security-role>中定义的角色。 例,在web.xml实体中定义的脚本片段:[pre]
<servlet>
    <servlet-name>secret</servlet-name>
    <servlet-class>SalaryViewer</servlet-class>
    <security-role-ref>
        <role-name>mgr<!-- name used by servlet --></role-name>
        <role-link>manager<!-- name used in deployment descriptor --></role-link>
    </security-role-ref>
</servlet>

<!-- ... -->

<security-role>
    <role-name>manager</role-name>
</security-role>[/pre]
无 论servlet调用isUserInRole("mgr")还是调用isUserInRole("manager")方法,所获得的结果是一致的。从根 本上说,security-role-ref作用是生成一个别名,但并不是必须的。虽然这显得好像本来就应该如此,但是API 2.2规范中叙述到只能够 使用那些明确在<security-rike-ref>别名规则中定义的角色。
               Servlet API 2.3的变更
新增加的类
Filter, FilterChain, FilterConfig, ServletContextAttributeEvent, ServletContextAttributesListener, ServletContextEvent, ServletContextListener, ServletRequestWrapper, ServletResponseWrapper, HttpServletRequestWrapper, HttpServletResponseWrapper, HttpSessionActivationListener, HttpSessionAttributesListener, HttpSessionEvent, HttpSessionListener
新增加的方法
getResourcePaths(), getServletContextName(), resetBuffer(), HttpSessionBindingEvent.getValue()
新增加的属性
BASIC_AUTH, DIGEST_AUTH, CLIENT_CERT_AUTH, 和FORM_AUTH
不再被推荐使用的类
HttpUtils

转载于:https://www.cnblogs.com/liupeizhi/articles/1942714.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值