Servlet基本结构的源码解析

如何写一个Servlet类?围绕这个问题,可以看一下Servlet的类结构。首先Servlet类也是一个Java类,只不过这个类比较特殊,它不能单独运行,必须要依托Servlet容器才能运行,Servlet类是一个组件,供Servlet引擎调用。既然是这样,那么Servlet类和Servlet引擎必然要遵循一套规范,以约束彼此的行为,遵循规范编写的Servlet类可以运行在任何符合规范的Servlet引擎上。那我们就看看,如何编写一个符合规范的Servlet。首先看一下要编写一个Servlet基本类图的结构(图是从网上找的):


其中Servlet接口是所有Servlet的父接口,Servlet规范为了方便实现Servlet类,提供了一个抽象类GenericServlet,它提供了Servlet的默认实现,可以通过继承自GenericServlet类来简化编程。Servlet主要是处理Http请求的,所以有提供了HttpServlet,它继承自GenericServlet,一般来说,我们编写的大部分Servlet都继承自HttpServlet。在上边的类图结构中,ServletConfig接口封装了代表Servlet容器的对象(ServletContext),和Servlet初始化信息,为了方便编程GenericServlet实现了此接口。在下面的源码分析中有详细的解释。

Servlet的生命周期

Servlet相当与Servlet容器的组件,Servlet接口定义了三个方法用于实现Servlet的声明周期,分别是:

(1)初始化init(ServletConfig config):每个Servlet只能初始化一次,初始化的时候,Servlet引擎将一个ServletConfig对象传递进来,里面包含了这个Servlet的初始化参数,具体的Servlet类应该使用一个私有引用保存这个config。Servlet可以有构造方法,init在构造方法之后执行;

(2)处理请求service(ServletRequest,ServletResponse):只有在init方法成功执行后才能处理请求,每次请求都会调用service方法;

(3)销毁destroy():当要从容器中移除该Servlet时,会调用destroy方法,用来关闭资源,做一些清理工作,只调用一次。

同时Servlet接口还定义了getServletConfig()方法,就是返回init方法中传递进来的config,下面是Servlet的源码:

  1. import java.io.IOException;  
  2.   
  3. /** 
  4.  * 所有servlet都要实现的接口 
  5.  * Servlet是供Servlet引擎调用的Java类,它相当于Servlet引擎的组件 
  6.  * 在Servlet接口中定义了三个方法,也被称为Servlet的声明周期: 
  7.  * void init(ServletConfig config):Servlet被初始化时被Servlet引擎调用,只执行一次,且后于构造方法 
  8.  * void service(ServletRequet,ServletResponse):响应请求,每当有一个请求,Servlet引擎就调用该方法 
  9.  * void destroy():Servlet被销毁时被Servlet引擎调用,用于关闭资源等服务 
  10.  * 以上三个方法都是被Servlet引擎(容器)调用 
  11.  * 除了以上方法之外,还提供了个方法: 
  12.  * ServletConfig getServletConfig:一个ServletConfig对象,在init方法被传递进来,包含了Servlet的初始化信息和 
  13.  * 代表Servlet容器的对象(ServletContext) 
  14.  * String getServletInfo():返回该Servlet的基本信息 
  15.  */  
  16. public interface Servlet {  
  17.   
  18.     /** 
  19.      * 被servlet引擎调用用于初始化该Servlet,只有在该方法完成之后,才能响应请求 
  20.      * 如果init方法抛出了异常,则一定不能响应请求。init方法在构造方法之后执行。 
  21.      * Servlet引擎会把ServletConfig对象传进来 
  22.      */  
  23.     public void init(ServletConfig config) throws ServletException;  
  24.   
  25.     /** 
  26.      * 返回ServletConfig对象,其中包含着初始化信息和初始化参数 
  27.      * @see #init 
  28.      */  
  29.     public ServletConfig getServletConfig();  
  30.   
  31.     /** 
  32.      * 响应请求,必须在init(Servletconfig config)成功后才能执行 
  33.      */  
  34.     public void service(ServletRequest req, ServletResponse res)  
  35.             throws ServletException, IOException;  
  36.   
  37.     /**返回基本信息*/  
  38.     public String getServletInfo();  
  39.   
  40.     /** 
  41.      * 被Servlet引擎调用,用来关闭资源,或者做一些清楚工作 
  42.      */  
  43.     public void destroy();  
  44. }  
Servlet的默认实现

GenericServlet实现了Servlet接口,同时也实现了ServletConfig接口,之所以实现ServletConfig接口是为了方便编程。下面代码中的注释已经很清楚了,不过还是要说一下init方法,在GenericServlet中提供了两个init方法:

(1)重写Servlet接口的init(ServletConfig config)方法,GenericSerlvet内部用私有属性保存此config,初始化工作调用无参的init()方法;

(2)重载的无参init()方法,子类应该覆盖这个方法,它会被有参数的init方法调用。之所以这么设计是为了方便编程,前面提到,如果重写父类的init(ServletConfig)方法,需要调用super.init(config),或者自己保存config,没必要增加这麻烦,直接继承自无参的init。Servlet引擎调用的init(ServletConfig)方法,而在init(ServletConfig)在最后调用了无参的init()。

  1. /** 
  2.  * 对Servlet接口的方法提供了默认是实现,通过继承GenericServlet方法,可以方便别写Servlet 
  3.  * 1.首先需要注意的一点是它实现了ServletConfig接口,在Servlet的init(ServletConfig config)方法 
  4.  * 中,Servlet容器会把一个ServletConfig传递进来,但是这个config只能在init()方法中使用,如果想在其他 
  5.  * 方法中是使用,则需要使用一个似有引用把config保存起来,供getServletConfig()调用。 
  6.  * GenericServlet也是这样做的,它有一个ServletConfig属性,在init(ServletConfig config)中 
  7.  * 保存传递进来的config。 
  8.  * 但同时它还实现了ServletConfig接口,这样做的目的是方便编程,在需要使用ServletConfig的方法时, 
  9.  * 无需先获得ServletConfig对象,而可以直接使用这些方法。其实GenericServlet也仅仅是简单的调用 
  10.  * config的相关方法,相当于一个代理。 
  11.  *  
  12.  * 2.GenericServlet实现了两个init方法 
  13.  * 一个是重写Serlvet接口的init(ServletConfig)方法,使用该方法获得ServletConfig对象 
  14.  * 它会在最后调用无参的init()方法; 
  15.  * 一个是重载的无参init()方法 
  16.  * 子类应该选择重写无参的init()方法,为什么呢? 
  17.  * 如果重写init(ServletConfig)方法,那么首先需要在第一行调用super(config) 
  18.  * 要不然,就得自己去保存传递进来的ServletConfig对象,增加不必要的麻烦 
  19.  * 其次,重写无参的init()方法,会被有参数的init(ServletConfig)调用,达到同样的目的 
  20.  * 结论:无参的init()方法就是为了方便子类覆盖 
  21.  */  
  22. public abstract class GenericServlet implements Servlet, ServletConfig,  
  23.         java.io.Serializable {  
  24.   
  25.     private static final long serialVersionUID = 1L;  
  26.   
  27.     private transient ServletConfig config;  
  28.   
  29.     public GenericServlet() {  
  30.         // NOOP  
  31.     }  
  32.   
  33.     @Override  
  34.     public void destroy() {  
  35.         // NOOP by default  
  36.     }  
  37.   
  38.     /**实现ServletConfig接口的方法*/  
  39.     public String getInitParameter(String name) {  
  40.         return getServletConfig().getInitParameter(name);  
  41.     }  
  42.     public Enumeration<String> getInitParameterNames() {  
  43.         return getServletConfig().getInitParameterNames();  
  44.     }  
  45.     @Override  
  46.     public ServletContext getServletContext() {  
  47.         return getServletConfig().getServletContext();  
  48.     }  
  49.     @Override  
  50.     public String getServletName() {  
  51.         return config.getServletName();  
  52.     }  
  53.     @Override  
  54.     public ServletConfig getServletConfig() {  
  55.         return config;  
  56.     }  
  57.    
  58.     @Override  
  59.     public String getServletInfo() {  
  60.         return "";  
  61.     }  
  62.   
  63.     /** 
  64.      * 注意,这个方法会在最后调用无参的init()方法 
  65.      * 子类应该选择覆盖init()方法 
  66.      */  
  67.     @Override  
  68.     public void init(ServletConfig config) throws ServletException {  
  69.         this.config = config;  
  70.         this.init();  
  71.     }  
  72.   
  73.     /**默认空实现,子类如果要覆盖init方法的话,应该选择覆盖此方法 
  74.      * init(ServletConfig)会在最后调用这个方法 
  75.      * */  
  76.     public void init() throws ServletException {  
  77.         // NOOP by default  
  78.     }  
  79.   
  80.     public void log(String msg) {  
  81.         getServletContext().log(getServletName() + ": " + msg);  
  82.     }  
  83.   
  84.     public void log(String message, Throwable t) {  
  85.         getServletContext().log(getServletName() + ": " + message, t);  
  86.     }  
  87.   
  88.     @Override  
  89.     public abstract void service(ServletRequest req, ServletResponse res)  
  90.             throws ServletException, IOException;  
  91. }  
处理HTTP请求

为了方便处理HTTP请求,Servlet规范定义了HttpServlet抽象类,通过实现HttpServlet类可以把重点放到业务逻辑的实现上,而不是去处理和Servlet引擎的交互。HttpServlet针对每种HTTP的请求方式都提供了具体的doXXX方法,其中最常用的还是doGet和doPost方法。在理解HttpServlet类时,最重要的是理解Servlet的生命周期,Servlet引擎在处理请求是调用的是哪个方法?是Servlet接口规定的service(ServletRequest,ServletResponse)方法,在HttpServlet类中,处理请求的方法依然是首先调用service方法,service方法根据不同的请求方式,把请求转发到具体的doXXX方法中

在这里有两个HTTP字段需要注意,一个是响应实体首部Last-Modified(服务器端),表示Servlet的修改时间;一个是请求头部,If-Modified-Since(客户端),如果资源在指定的时间之后没有修改过,那么表示缓存有效,可以直接使用缓存。HttpServlet.getLastModified()方法用于返回当前内容修改的时间。Service在处理请求时,需要根据其返回值的不同需要做相应的处理:(服务端修改时间比客户端修改时间旧,本地缓存有效,否则无效,服务器响应请求)

(1)如果其返回值是-1,则二话不说,直接响应本次请求;

(2)如果是一个正数,且请求头中没有If-Modified-Since字段,或者If-Modified-Since字段表示的时间比getLastModified()返回的之间旧,表示缓存无效,需要处理本次请求。比如,如果当前修改时间是2014年9月1日,请求头中的If-Modified-Since表示的时间是2014年8月1日,也就是说请求中这样说到:如果在2014年8月1日之后没有修改过资源,那么缓存有效。但是很明显,这种情形下,缓存已经失效了。同时把Last-Modified字段写入响应头中

(3)返回值是整数,但是请求头中If-Modified-Since表示的时间新于getLastModified()表示的时间,那么表示缓存有效,服务器直接返回一个304(Not Modified),告诉浏览器直接使用缓存。

  1. import java.io.IOException;  
  2. import java.io.OutputStreamWriter;  
  3. import java.io.PrintWriter;  
  4. import java.io.UnsupportedEncodingException;  
  5. import java.lang.reflect.Method;  
  6. import java.text.MessageFormat;  
  7. import java.util.Enumeration;  
  8. import java.util.ResourceBundle;  
  9.   
  10. import javax.servlet.GenericServlet;  
  11. import javax.servlet.ServletException;  
  12. import javax.servlet.ServletOutputStream;  
  13. import javax.servlet.ServletRequest;  
  14. import javax.servlet.ServletResponse;  
  15.   
  16.   
  17. /** 
  18.  * 会方便处理HTTP请求的Servlet而提供的抽象类,继承了GenericServlet 
  19.  * 我们写的绝大多数Servlet都应该继承自HttpServlet 
  20.  * HttpServlet根据不同的http请求提供了不同的方法: 
  21.  * doGet:如果servlet支持http GET方法 
  22.  * doPost:如果servlet支持http POST方法 
  23.  * 以上两个方法最常用,其他的还有doPut,doDelete,doHead,doOptions,doTrace 
  24.  * 
  25.  * @author  Various 
  26.  */  
  27. public abstract class HttpServlet extends GenericServlet {  
  28.   
  29.     private static final long serialVersionUID = 1L;  
  30.       
  31.     //HTTP的各种方法  
  32.     private static final String METHOD_DELETE = "DELETE";  
  33.     private static final String METHOD_HEAD = "HEAD";  
  34.     private static final String METHOD_GET = "GET";  
  35.     private static final String METHOD_OPTIONS = "OPTIONS";  
  36.     private static final String METHOD_POST = "POST";  
  37.     private static final String METHOD_PUT = "PUT";  
  38.     private static final String METHOD_TRACE = "TRACE";  
  39.   
  40.     private static final String HEADER_IFMODSINCE = "If-Modified-Since";  
  41.     private static final String HEADER_LASTMOD = "Last-Modified";  
  42.   
  43.     private static final String LSTRING_FILE =  
  44.         "javax.servlet.http.LocalStrings";  
  45.     private static ResourceBundle lStrings =  
  46.         ResourceBundle.getBundle(LSTRING_FILE);  
  47.   
  48.   
  49.   
  50.     /** 
  51.      * 被service()方法调用去处理GET请求 
  52.      * 重写此方法以支持GET请求同样也自动的支持HEAD请求 
  53.      * 一个HEAD请求就是一个GET请求,只不过HEAD请求只返回首部,不返回响应实体部分 
  54.      * 子类如果要支持GET方法,就必须要重写次方法,因为HttpServlet的默认实现是 
  55.      * 发送错误 
  56.      */  
  57.     protected void doGet(HttpServletRequest req, HttpServletResponse resp)  
  58.         throws ServletException, IOException  
  59.     {  
  60.         //返回使用的协议,protocol/majorVersion,如HTTP/1.1  
  61.         String protocol = req.getProtocol();  
  62.         String msg = lStrings.getString("http.method_get_not_supported");  
  63.         //由于是默认实现,根据不同的协议,直接报错  
  64.         if (protocol.endsWith("1.1")) {  
  65.             resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);  
  66.         } else {  
  67.             resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);  
  68.         }  
  69.     }  
  70.   
  71.   
  72.     /** 
  73.      * 返回的值代表当前输出响应内容的修改时间,总是返回-1,子类可重写 
  74.      * getLastModified方法是一个回调方法,由HttpServlet的service方法调用, 
  75.      * service方法可以根据返回值在响应消息中自动生成Last_Modified头字段(最后被修改的时间) 
  76.      * Servlet受到一个GET方式的请求时,HttpServlet重载的service方法在调用doGet之前,会先 
  77.      * 调用本方法,并根据返回值来决定是否调用doGet方法和在响应消息是是否生成Last_Modified字段 
  78.      * 具体规则如下: 
  79.      * 1.如果是一个负数(本方法默认实现),直接调用doGet方法 
  80.      * 2.如果是一个正数,且请求消息中没有包含If-Modified-Since请求头,或者请求头中的时间值 
  81.      *   比返回值旧时,这说明要么是第一次请求,要么是缓存过期了,service将根据返回值生成一个 
  82.      *   Last-Modified字段,并调用doGet方法 
  83.      * 3.本方法返回值是一个正数,且请求消息中包含的If-Modified-Since的时间值比返回值新或者相同, 
  84.      * 说明缓存有效,service方法将不调用doGet方法,而是返回304(Not Modified)告诉浏览器缓存仍然有效 
  85.      */  
  86.     protected long getLastModified(HttpServletRequest req) {  
  87.         return -1;  
  88.     }  
  89.   
  90.   
  91.    /** 
  92.     * 没有相应实体,其他与GET方法相同,也正是通过调用doGet来完成请求 
  93.     */  
  94.     protected void doHead(HttpServletRequest req, HttpServletResponse resp)  
  95.         throws ServletException, IOException {  
  96.         //本类的内部类  
  97.         NoBodyResponse response = new NoBodyResponse(resp);  
  98.   
  99.         doGet(req, response);  
  100.         response.setContentLength();  
  101.     }  
  102.   
  103.   
  104.     /** 
  105.      * 也是被service方法调用,默认实现是报错,参考doGet方法 
  106.      */  
  107.     protected void doPost(HttpServletRequest req, HttpServletResponse resp)  
  108.         throws ServletException, IOException {  
  109.   
  110.         String protocol = req.getProtocol();  
  111.         String msg = lStrings.getString("http.method_post_not_supported");  
  112.         if (protocol.endsWith("1.1")) {  
  113.             resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);  
  114.         } else {  
  115.             resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);  
  116.         }  
  117.     }  
  118.   
  119.     /** 
  120.      * 被Servlet容器调用,完成请求 
  121.      * 把请求转发到具体的方法上,通过调用重载的service(HttpServletRequest,HttpServletResponse) 
  122.      * 完成这个方法做的事情就是把request和response转换成HttpServerRequest,HttpServletResponse, 
  123.      * 具体的转发工作由重载的service方法完成 
  124.      */  
  125.     @Override  
  126.     public void service(ServletRequest req, ServletResponse res)  
  127.         throws ServletException, IOException {  
  128.   
  129.         HttpServletRequest  request;  
  130.         HttpServletResponse response;  
  131.   
  132.         try {  
  133.             request = (HttpServletRequest) req;  
  134.             response = (HttpServletResponse) res;  
  135.         } catch (ClassCastException e) {  
  136.             throw new ServletException("non-HTTP request or response");  
  137.         }  
  138.         service(request, response);  
  139.     }  
  140.       
  141.   
  142.     /** 
  143.      * 处理标准的HTTP请求,此方法把具体的请求转发到相应的doXXX方法上 
  144.      * 被service(ServletRequest,ServletResponse)调用 
  145.      * 没有理由重写此方法 
  146.      */  
  147.     protected void service(HttpServletRequest req, HttpServletResponse resp)  
  148.         throws ServletException, IOException {  
  149.         //返回HTTP的请求方式,如GET,POST,HEAD  
  150.         String method = req.getMethod();  
  151.           
  152.         /** 
  153.          * 如果是GET方式 
  154.          * 1.首先通过getLastModified(req)获得修改时间 
  155.          *  i.如果修改时间==-1,不管请求是怎样的,直接调用doGet 
  156.          *  ii.如果不是-1,则需要获取请求中的If-Modified_Since的值,保存到ifModifiedSince变量中 
  157.          *     如果请求头中没有If-Modified-Since字段,则ifModifiedSince=-1,通过比较lastModified 
  158.          *     和ifModifiedSince的值来判断缓存是否过期: 
  159.          *     如果lastModified代表的日期比ifModifiedSince代表的日期新时,则说明缓存失效了, 
  160.          *     比如lastModified代表9月1号修改的,ifModifiedSince为8月1号,  
  161.          *     意思是如果自8月1号没有修改过的话,则可以使用缓存,很明显已经修改过了, 
  162.          *     所以不能使用缓存,这个时候要调用doGet方法响应,同时在相应头设置Last-Modified的值。 
  163.          *     如果lastModified代表的时间比ifModifiedSince旧时,也就是没有修改过, 
  164.          *     则返回304(Not Modified)告诉浏览器,直接使用缓存。 
  165.          * 2.判断是何种方式,调用具体的doXXX方法 
  166.          *      
  167.          */  
  168.         if (method.equals(METHOD_GET)) {  
  169.             long lastModified = getLastModified(req);  
  170.             if (lastModified == -1) {  
  171.                 // servlet doesn't support if-modified-since, no reason  
  172.                 // to go through further expensive logic  
  173.                 doGet(req, resp);  
  174.             } else {  
  175.                 long ifModifiedSince;  
  176.                 try {  
  177.                     ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);  
  178.                 } catch (IllegalArgumentException iae) {  
  179.                     // Invalid date header - proceed as if none was set  
  180.                     ifModifiedSince = -1;  
  181.                 }  
  182.                 if (ifModifiedSince < (lastModified / 1000 * 1000)) {  
  183.                     // If the servlet mod time is later, call doGet()  
  184.                     // Round down to the nearest second for a proper compare  
  185.                     // A ifModifiedSince of -1 will always be less  
  186.                     maybeSetLastModified(resp, lastModified);  
  187.                     doGet(req, resp);  
  188.                 } else {  
  189.                     resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);  
  190.                 }  
  191.             }  
  192.   
  193.         } else if (method.equals(METHOD_HEAD)) {  
  194.             long lastModified = getLastModified(req);  
  195.             maybeSetLastModified(resp, lastModified);  
  196.             doHead(req, resp);  
  197.   
  198.         } else if (method.equals(METHOD_POST)) {  
  199.             doPost(req, resp);  
  200.   
  201.         } else if (method.equals(METHOD_PUT)) {  
  202.             doPut(req, resp);  
  203.   
  204.         } else if (method.equals(METHOD_DELETE)) {  
  205.             doDelete(req, resp);  
  206.   
  207.         } else if (method.equals(METHOD_OPTIONS)) {  
  208.             doOptions(req,resp);  
  209.   
  210.         } else if (method.equals(METHOD_TRACE)) {  
  211.             doTrace(req,resp);  
  212.   
  213.         } else {  
  214.             //没有适合的响应方法,只能报错了  
  215.             String errMsg = lStrings.getString("http.method_not_implemented");  
  216.             Object[] errArgs = new Object[1];  
  217.             errArgs[0] = method;  
  218.             errMsg = MessageFormat.format(errMsg, errArgs);  
  219.   
  220.             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);  
  221.         }  
  222.     }  
  223.   
  224.   
  225.     /* 
  226.      * 设置响应的实体首部字段Lat-Modified 
  227.      */  
  228.     private void maybeSetLastModified(HttpServletResponse resp,  
  229.                                       long lastModified) {  
  230.         if (resp.containsHeader(HEADER_LASTMOD))  
  231.             return;  
  232.         if (lastModified >= 0)  
  233.             resp.setDateHeader(HEADER_LASTMOD, lastModified);  
  234.     }  
  235. }  
ServletConfig

ServletConfig封装了Serlvet的初始化参数,同时其内部包含代表Servlet容器的对象,具体的解释都在源码中了:

  1. /** 
  2.  * 在一个Servlet被初始化是被Serlvet容器传递给init方法 
  3.  * 包含了Servletd的配置信息。它提供了两个方法用于Servlet的初始化参数: 
  4.  * public String getInitParameter(String name):获取指定参数的值 
  5.  * public Enumeration<String> getInitParameterNames():获得所有的参数名称,遍历该Enumeration,可获得所有参数 
  6.  * 不得不吐槽一下,Enumeration都已经被Iterator取代了,竟然还在用这个,真土 
  7.  * 初始参数是在Servlet的配置中指定的,如下面的示例就指定了名字和年龄,可以通过上面两个方法获得这些值: 
  8.  * <servlet> 
  9.  *   <servlet-name>LifeCircle</servlet-name> 
  10.  *  <servlet-class>com.servlet.base.LifeCircle</servlet-class> 
  11.  *   <init-param> 
  12.  *    <param-name>name</param-name> 
  13.  *     <param-value>caoxiaoyong</param-value> 
  14.  *  </init-param> 
  15.  *  <init-param> 
  16.  *    <param-name>age</param-name> 
  17.  *    <param-value>25</param-value> 
  18.  *  </init-param> 
  19.  *</servlet> 
  20.  *---------------------------------------------- 
  21.  *还有一个方法用于获取Serlvet容器的对象: 
  22.  *public ServletContext getServletContext() 
  23.  */  
  24. public interface ServletConfig {  
  25.   
  26.     /**返回Servlet的名称 */  
  27.     public String getServletName();  
  28.     public ServletContext getServletContext();  
  29.     public String getInitParameter(String name);  
  30.     public Enumeration<String> getInitParameterNames();  
  31. }  
通过上面的分析,我们应该能很清晰的回答如何编写一个Servlet类了:如果是处理Http请求,则继承自HttpServlet类,并根据请求方法去覆盖相应的doXXX方法;如果不是Http请求,则继承GenericServlet类。如果认真的分析了上面四个接口或类的源码,我想收获的不止这一点。

转载请注明:喻红叶《Servlet基本结构的源码分析》

地址:http://blog.csdn.net/yuhongye111/article/details/38946909

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值