MyException - 我的异常网
当前位置:我的异常网» Web前端 » Servlet根本工作原理

Servlet根本工作原理

www.MyException.Cn  网友分享于:2015-08-26  浏览:9次
Servlet基本工作原理

Servlet容器工作原理  

 

 

本 文介绍一个简单 servlet 容器的基本原理。现有两个servlet容器,第一个很简单,第二个则是根据第一个写出。为了使第一个容器尽量简单,所以没有做得很完整。复杂一些的 servlet容器(包括TOMCAT4和5)在TOMCAT运行内幕的其他章节有介绍。

两个servlet容器都处理简单的 servlet及staticResource。您可以使用 webroot/ 目录下的 PrimitiveServlet 来测试它。复杂一些的 servlet会超出这些容器的容量,您可以从 TOMCAT 运行内幕 一书学习创建复杂的 servlet 容器。

两个应用程序的类都封装在ex02.pyrmont 包下。在理解应用程序如何运作之前,您必须熟悉 javax.servlet.Servlet 接口。首先就来介绍这个接口。随后,就介绍servlet容器服务servlet的具体内容。

javax.servlet.Servlet 接口

servlet 编程,需要引用以下两个类和接口:javax.servlet 和 javax.servlet.http,在这些类和接口中,javax.servlet.Servlet接口尤为重要。所有的 servlet 必须实现这个接口或继承已实现这个接口的类。

Servlet 接口有五个方法,如下

public void init(ServletConfig config) throws ServletException
public void service(ServletRequest request, ServletResponse response)
throws ServletException, jsp servlet ejb 





.io.IOException
  public void destroy()
  public ServletConfig getServletConfig()
  public jsp servlet ejb 





.lang.String getServletInfo()


init、 service和 destroy 方法是 Servlet 生命周期的方法。当 Servlet 类实例化后,容器加载 init,以通知 servlet 它已进入服务行列。init 方法必须被加载,Servelt 才能接收和请求。如果要载入数据库驱动程序、初始化一些值等等,程序员可以重写这个方法。在其他情况下,这个方法一般为空。

service 方法由 Servlet 容器调用,以允许 Servlet 响应一个请求。Servlet 容器传递 javax.servlet.ServletRequest 对象和 javax.servlet.ServletResponse 对象。ServletRequest 对象包含客户端 HTTP 请求信息,ServletResponse 则封装servlet 响应。这两个对象,您可以写一些需要 servlet 怎样服务和客户怎样请求的代码。

从service中删除Servlet实例之 前,容器调用destroy方法。在servlet容器关闭或servlet容器需要更多的内存时,就调用它。这个方法只有在servlet的 service方法内的所有线程都退出的时候,或在超时的时候才会被调用。在 servlet 容器调用 destroy方法之后,它将不再调用servlet的service方法。destroy 方法给了 servlet 机会,来清除所有候住的资源(比如:内存,文件处理和线程),以确保在内存中所有的持续状态和 servlet的当前状态是同步的。Listing 2.1 包含了PrimitiveServlet 的代码,此servlet非常简单,您 可以用它来测试本文中的servlet容器应用程序。

PrimitiveServlet 类实现了javax.servlet.Servlet 并提供了五个servlet方法的接口 。它做的事情也很简单:每次调用 init,service 或 destroy方法的时候,servlet就向控制口写入方法名。service 方法也从ServletResponsec对象中获得java.io.PrintWriter 对象,并发送字符串到浏览器。

Listing 2.1.PrimitiveServlet.java
import javax.servlet.*;
import jsp servlet ejb 





.io.IOException;
import jsp servlet ejb 





.io.PrintWriter;
public class PrimitiveServlet implements Servlet 
{
 public void init(ServletConfig config) throws ServletException 
{
   System.out.println("init");
    }
 public void service(ServletRequest request, ServletResponse  response)
 throws ServletException, IOException 
{
      System.out.println("from service");
      PrintWriter out = response.getWriter();
      out.println("Hello.Roses are red.");
      out.print("Violets are blue.");
    }
 public void destroy() 
{
    System.out.println("destroy");
    }
    public String getServletInfo()
 {
   return null;
    }

 public ServletConfig getServletConfig() 
{
    return null;
    }
}



Application 1

现在,我们从 servlet容器的角度来看看 servlet 编程。一个功能健全的 servlet容器对于每个 servlet 的HTTP请求会完成以下事情:

当servlet 第一次被调用的时候,加载了 servlet类并调用它的init方法(仅调用一次)

响应每次请求的时候 ,构建一个javax.servlet.ServletRequest 和 javax.servlet.ServletResponse实例。

激活servlet的service方法,传递 ServletRequest 和 ServletResponse 对象。

当servlet类关闭的时候,调用servlet的destroy方法,并卸载servlet类。

发生在 servlet 容器内部的事就复杂多了。只是这个简单的servlet容器的功能不很健全,所以,这它只能运行非常简单的servelt ,并不能调用servlet的init和destroy方法。然而,它也执行了以下动作:

等待HTTP请求。

构建ServletRequest和ServletResponse对象

如果请求的是一个staticResource,就会激活StaticResourceProcessor实例的 process方法,传递ServletRequest 和 ServletResponse 对象。

如果请求的是一个servlet ,载入该类,并激活它的service方法,传递ServletRequest和ServletResponse 对象。注意:在这个servlet 容器,每当 servlet被请求的时候该类就被载入。

在第一个应用程序中,servlet容器由六个类组成 。

HttpServer1

Request

Response

StaticResourceProcessor

ServletProcessor1

Constants



证 如前文中的应用程序一样,这个程序的进入口(静态 main 方法)是HttpServer 类。这个方法创建了HttpServer实例,并调用它的await方法。这个方法等待 HTTP 请示,然后创建一个 request 对象和 response对象,根据请求是否是staticResource还是 servlet 来分派它们到 StaticResourceProcessor实例或ServletProcessor实例。

Constants 类包含 static find WEB_ROOT,它是从其他类引用的。 WEB_ROOT 指明 PrimitiveServlet 位置 和容器服务的staticResource。

HttpServer1 实例等待 HTTP 请求,直到它收到一个 shutdown 命令。发布 shutdown命令和前文是一样的。

HttpServer1 类

此应用程序内的 HttpServer1类 与前文简单的 WEB 服务器应用程序中的HttpServer 十分相似。但是,此应用程序内的 HttpServer1 能服务静态资源和 servlet。如果要请求一个静态资源,请输入以下 URL:

http://machineName:port/staticResource

它就是前文中提到的怎样在 WEB 服务器应用程序里请求静态资源。如果要请求一个 servlet,请输入以下 URL:

http://machineName:port/servlet/servletClass

如果您想在本地浏览器请求一个 PrimitiveServle servlet ,请输入以下 URL:

http://localhost:8080/servlet/PrimitiveServlet

下面 Listing 2.2 类的 await 方法,是等待一个 HTTP 请求,直到一个发布 shutdown 命令。与前文的 await 方法相似。

Listing 2.2. HttpServer1 类的 await 方法
public void await() {
ServerSocket serverSocket = null;
int       port  = 8080;

try {
serverSocket =  new ServerSocket(port, 1,
InetAddress.getByName("127.0.0.1"));
    }
catch (IOException e) {
e.printStackTrace();
System.exit(1);
    }

// 循环,等待一个请求
while (!shutdown) {
Socket socket       = null;
InputStream input   = null;
OutputStream output = null;

try {
socket = serverSocket.accept();
input  = socket.getInputStream();
output = socket.getOutputStream();

// 创建请求对象并解析
Request request = new Request(input);
request.parse();

// 创建回应对象
Response response = new Response(output);
response.setRequest(request);

//检测是否是 servlet 或静态资源的请求
//servlet 请求以 "/servlet/" 开始 
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
            }
else {
StaticResourceProcessor processor =
new StaticResourceProcessor();
processor.process(request, response);
            }

// 关闭socket
socket.close();

//检测是否前面的 URI 是一个 shutdown 命令
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
        }
catch (Exception e) {
e.printStackTrace();
System.exit(1);
        }
    }
}


此文 await 方法和前文的不同点就是,此文的 await 方法中的请求调度到StaticResourceProcessor 或 ervletProcessor 。

如果 URI中包含 "/servlet/.",请求推进到后面,否则,请求传递到 StaticResourceProcessor 实例

Request 类

Servlet service 方法接受 servlet 容器的 javax.servlet.ServletRequest 和javax.servlet.ServletResponse 实例。因此,容器必须构建 ServletRequest和ServletResponse对象,然后将其传递到正在被服务的service 方法。

ex02.pyrmont.Request 类代表一个请求对象传递到 service 方法。同样地,它必须实现 javax.servlet.ServletRequest 接口。这个类必须提供接口内所有方法的实现。这里尽量简化它并只实现几个方法。要编译 Request 类的话,必须提供这些方法的空实现。再来看看 request 类,内部所有需要返回一个对象实例都返回null,如下:

public Object getAttribute(String attribute) {
return null;
  }

public Enumeration getAttributeNames() {
return null;
  }

public String getRealPath(String path) {
return null;
  }


另外,request 类仍需有前文有介绍的 parse 和getUri 方法。

Response 类

response 类实现 javax.servlet.ServletResponse,同样,该类也必须提供接口内所有方法的实现。类似于 Request 类,除 getWriter 方法外,其他方法的实现都为空。

public PrintWriter getWriter() {
// autoflush is true, println() will flush,
// but print() will not.
writer = new PrintWriter(output, true);
return writer;

}


PrintWriter 类构建器的第二个参数是一个代表是否启用 autoflush 布尔值 ,如果为真,所有调用println 方法都 flush 输出。而 print 调用则不 flush 输出。因此,如果在servelt 的service 方法的最后一行调用 print方法,则从浏览器上看不到此输出 。这个不完整性在后面的应用程序内会有调整。

response 类也包含有前文中介绍的 sendStaticResource方法。

StaticResourceProcessor 类

StaticResourceProcessor 类用于服务静态资源的请求。它唯一的方法是 process。

Listing 2.3.StaticResourceProcessor 类的 process方法。
public void process(Request request, Response response) {
try {
response.sendStaticResource();
    }
catch (IOException e) {
e.printStackTrace();
    }
}


process 方法接受两个参数:Request 和 Response 实例。它仅仅是调用 response 类的 sendStaticResource 方法。
ServletProcessor1 类

ServletProcessor1 类用来处理对 servlet 的 HTTP 请求。 它非常简单,只包含了一个 process 方法。 而这个方法接受两个参数: 一个javax.servlet.ServletRequest 实例和一个 avax.servlet.ServletResponse实例。 process 方法也构建了一个 jsp servlet ejb .net.URLClassLoader 对象并使用它装载 servlet 类文件。 在从类装载器获得的 Class 对象上,process 方法创建一个 servlet 实例并调用它的 service 方法。

process 方法

Listing 2.4. ServletProcessor1 类中 process 方法

public void process(Request request, Response response) {
    String uri            = request.getUri();
    String servletName    = uri.substring(uri.lastIndexOf("/") + 1);
    URLClassLoader loader = null;

    try {
        // create a URLClassLoader
        URLStreamHandler streamHandler = null;

        URL[] urls        = new URL[1];
        File classPath    = new File(Constants.WEB_ROOT);
        String repository = (new URL("file", null, 
            classPath.getCanonicalPath() + File.separator)).toString() 
        urls[0]           = new URL(null, repository, streamHandler);
        loader            = new URLClassLoader(urls);
    }
    catch (IOException e) {
        System.out.println(e.toString());
    }

    Class myClass = null;

    try {
        myClass = loader.loadClass(servletName);
    }
    catch (Exception e) {
        System.out.println(e.toString());
    }

    Servlet servlet = null;
    try {
        servlet = (Servlet) myClass.newInstance();
        servlet.service((ServletRequest) request, (ServletResponse) response);
    }
    catch (Exception e) {
        System.out.println(e.toString());
    }
    catch (Throwable e) {
        System.out.println(e.toString());
    }
}


process方法接受两个参数:一个 ServletRequest实例和一个 ServletResponse 实例。process方法通过调用 getRequestUri 方法从 ServletRequest获取 URI。

String uri = request.getUri();切记 URI 的格式:

/servlet/servletName

servletName是servlet类的名称。

如 果要装载 servlet 类,则需要使用以下代码从 URI 获知 servlet 名称:String servletName = uri.substring(uri.lastIndexOf("/") + 1);然后 process 方法装载 servlet。 要做到这些,需要创建一个类装载器,并告诉装载器该类的位置, 该 servlet 容器可以指引类装载器在 Constants.WEB_ROOT 指向的目录中查找。 在工作目录下,WEB_ROOT 指向 webroot/ 目录。

如果要装载一个 servlet,则要使用 jsp servlet ejb .net.URLClassLoader 类,它是java.lang.ClassLoader 的间接子类。 一旦有了 URLClassLoader 类的实例,就可以使用 loadClass 方法来装载一个 servlet 类。 实例化 URLClassLoader 是很简单的。 该类有三个构建器,最简单的是:

public URLClassLoader(URL[] urls);

urls 是一组指向其位置 jsp servlet ejb .net.URL 对象, 当装载一个类时它会自动搜索其位置。任一以 / 结尾的 URL 都被假定为一目录, 否则,就假定其为 .jar 文件,在需要时可以下载并打开。

在 一个 servlet 容器内,类装载器查找 servlet 类的位置称为储存库 (repository)。在所举的应用程序中,类装载器只可在当前工作目录下的 webroot/ 目录查找,所以,首先得创建一组简单的 URL。 URL 类提供了多个构建器,因此有许多的方法来构建一个URL 对象。 在这个应用程序内,使用了和 TOMCAT 内另外一个类所使用的相同的构建器。 该构建器头部 (signature) 如下:

public URL(URL context, String spec, URLStreamHandler hander)

throws MalformedURLException

可以通过传递给第二个参数一个规范,传递给第一个和第三个参数 null 值来使用这个构建器, 但在些有另外一种可接受三个参数的构建器:

public URL(String protocol, String host, String file)

throws MalformedURLException

因此,如果只写了以下代码,编译器将不知道是使用的哪个构建器:

new URL(null, aString, null);

当然也可以能过告诉编译器第三个参数的类型来避开这个问题,如:

URLStreamHandler streamHandler = null;

new URL(null, aString, streamHandler);

对于第二个参数,可以传递包含储存库 (repository) 的 String 。 以下代码可创建:

String repository = (new URL("file", null,

classPath.getCanonicalPath() + File.separator)).toString();

结合起来,以下是构建正确 URLClassLoader 实例的 process 方法的部分代码

// create a URLClassLoader
URLStreamHandler streamHandler = null;
URL[] urls        = new URL[1];
File classPath    = new File(Constants.WEB_ROOT);
String repository = (new URL("file", null, 
    classPath.getCanonicalPath() + File.separator)).toString() 
urls[0]           = new URL(null, repository, streamHandler);
loader            = new URLClassLoader(urls);


创 建储存库 (repository)的代码摘自org.apache.catalina.startup.ClassLoaderFactory内的 createClassLoader 方法,而创建 URL 的代码摘自org.apache.catalina.loader.StandardClassLoader 类内的 addRepository 方法。 但在此阶段您还没有必要去关心这些类。

有了类装载器,您可以使用loadClass方法装载servlet类:

Class myClass = null;
try {
    myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
    System.out.println(e.toString());
}


然后,process方法创建已装载的 servlet类的实例,传递给 javax.servlet.Servlet ,并激活 servlet 的 service 方法:

Servlet servlet = null;
try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) request, (ServletResponse) response);
}
catch (Exception e) {
    System.out.println(e.toString());
}
catch (Throwable e) {
    System.out.println(e.toString());
}


编译并运行该应用程序

如果要编译该应用程序,在工作目录下键入以下命令:

javac -d . -classpath ./lib/servlet.jar src/ex02/pyrmont/*.java

如果要在 windows 下运行该应用程序,在工作目录下键入以下命令:

jsp servlet ejb -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer1

在 linux 环境下,使用冒号来隔开类库:

jsp servlet ejb -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer1

如果要测试该应用程序,请在 URL 或浏览器地址栏键入以下命令:

http://localhost:8080/index.html

或者是:

http://localhost:8080/servlet/PrimitiveServlet

您将会在浏览器中看到以下文本:

Hello. Roses are red.

注意:您不能看到第二行字符 (Violets are blue),因为只有第一行字符送入到浏览器。 Tomcat 运行工作原理 随后的章节会告诉您怎样来解决这个问题。
Application 2

第 一个应用程序里存在一个值得注意的问题。 在ServletProcessor1 类的 process 方法里,上溯 (upcast)ex02.pyrmont.Request 实例到 javax.servlet.ServletRequest,将其作为第一个参数传递给 servlet 的 service 方法。 另上溯(upcast) ex02.pyrmont.Response 实例到 javax.servlet.ServletResponse ,并将其作为第二个参数传递给 servlet 的 service 方法。

try {
   servlet = (Servlet) myClass.newInstance();
   servlet.service((ServletRequest) request, (ServletResponse) response);
}


这 样会使安全性能大打折扣。 知道 servlet 容器工作原理的程序员可以将 ServletRequest 和 ServletResponse 实例向下转型 (downcast) 到Request 和 Response ,并调用它们的 public 方法。 Request 实例能调用它的 parse 方法; Request 实例能调用它的 sendStaticResource 方法。

可以将 parse 和 sendStaticResource 方法设为 private,因为在 ex02.pyrmont 里将会从其他类里调用它们。 然而,这两个方法在 servlet 内应该是不可用的。 一个解决方法是:给 Request 和 Response 类一个默认的访问修饰符,以致他们在 ex02.pyrmont 外不能被使用。 但还有一个更好的解决方法: 使用 facade 类。

在 第二个应用程序内,添加两个 facade 类:RequestFacade 和 ResponseFacade。 RequestFacade 类实现 ServletRequest 接口,并通过传递 Request 实例来实例化, Request 实例将在 ServletRequest 对象的构建器里被引用 。 ServletRequest 对象本身是 private 类型的,不能在类之外访问。 就构建 RequestFacade 对象,并将其传递给 service 方法,而不上溯 (upcast) Request 对象给 ServletRequest,并将其传递给 service 方法。 servlet 程序员仍旧可以向下转型 (downcast) ServletRequest 到 RequestFacade,但是,只要访问 ServletRequest 接口的可用方法就可以了。 现在,parseUri 就安全了。

Listing 2.5 显示 RequestFacade 类部分代码:

Listing 2.5. RequestFacade 类

package ex02.pyrmont;

public class RequestFacade implements ServletRequest {
    private ServletRequest request = null;

    public RequestFacade(Request request) {
        this.request = request;
    }

    /* implementation of the ServletRequest*/
    public Object getAttribute(String attribute) {
        return request.getAttribute(attribute);
    }

    public Enumeration getAttributeNames() {
        return request.getAttributeNames();
    }

    ...
}


注意 RequestFacade 构造函数。 它会接受一个 Request 对象,即刻分配给私有的 servletRequest 对象引用。 还要注意,RequestFacade 内的每个方法调用 ServletRequest 对象内相应的方法。

ResponseFacade 类也是如此。

以下是 application 2 所包含的类

HttpServer2 
Request 
Response 
StaticResourceProcessor 
ServletProcessor2 
Constants 
HttpServer2 类类似于 HttpServer1,
只是它在 await 方法内使用了 ServletProcessor2 而不是ServletProcessor1。
if (request.getUri().startsWith("/servlet/")) {
   ServletProcessor2 processor = new ServletProcessor2();
   processor.process(request, response);
}
else {
    ...
}
ServletProcessor2 类也类似于 ServletProcessor1,
只是在以下 process 方法的部分代码有点不同:
Servlet servlet = null;
RequestFacade requestFacade   = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);

try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) requestFacade, 
        (ServletResponse) responseFacade);
}



编译并运行该应用程序

如果要编译该应用程序,在工作目录下键入以下命令:

javac -d . -classpath ./lib/servlet.jar src/ex02/pyrmont/*.java

如果要在 windows 下运行该应用程序,在工作目录下键入以下命令:

jsp servlet ejb -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer2

在linux环境下,使用分号来隔开类库:

jsp servlet ejb -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer2

您可以使用和 application 1 相同的 URL 以收到同样的结果。

总结

本文讨论了简单的能够用于服务静态资源,以及处理如 PrimitiveServlet 一样简单的 servlet 的 servlet 容器。 同时也提供 javax.servlet.Servlet 的背景信息。

 

 

转自:http://lxs9731.blog.163.com/blog/static/34793702007112263521960/

 

 

资料:http://www.ibm.com/developerworks/cn/java/j-lo-servlet/


我的异常网推荐解决方案:Servlet.service() for servlet default threw exception,http://www.myexception.cn/eclipse/181756.html

文章评论

Java程序员必看电影
Java程序员必看电影
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
每天工作4小时的程序员
每天工作4小时的程序员
程序员的鄙视链
程序员的鄙视链
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
漫画:程序员的工作
漫画:程序员的工作
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
我是如何打败拖延症的
我是如何打败拖延症的
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
鲜为人知的编程真相
鲜为人知的编程真相
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
如何成为一名黑客
如何成为一名黑客
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
那些争议最大的编程观点
那些争议最大的编程观点
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
为什么程序员都是夜猫子
为什么程序员都是夜猫子
我的丈夫是个程序员
我的丈夫是个程序员
中美印日四国程序员比较
中美印日四国程序员比较
10个调试和排错的小建议
10个调试和排错的小建议
程序员和编码员之间的区别
程序员和编码员之间的区别
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
代码女神横空出世
代码女神横空出世
老程序员的下场
老程序员的下场
程序员都该阅读的书
程序员都该阅读的书
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
程序员应该关注的一些事儿
程序员应该关注的一些事儿
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
 程序员的样子
程序员的样子
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
一个程序员的时间管理
一个程序员的时间管理
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有