深入学习Java Web服务器系列二

一个简单的servlet容器

在上一篇博客已经介绍了如何去实现一个简单的静态web容器,我们实现了一个可以解析静态html资源的web容器,下面,我们将进一步实现一个简单的servlet容器,来了解一下servlet容器的基本原理。

我们将在系列一的基础上进行相应的修改。服务器启动监听,当用户在浏览器输入URL发送http请求时,服务器进行解析requst,判断请求的资源类型,如果是静态资源并则返回请求的静态资源,如果是servlet资源,则进行解析并返回页面。系统的时序图如下所示:

下面我们一起来实现这个servlet容器吧,这篇博文分成三个部分,第一部分介绍servlet的生命周期,这个可以帮助我们明确容器的功能需求,第二部分介绍了这个简单的servlet容器的功能和相应的类关系,第三部分就是进行容器的代码实现了。

1. 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, java.io.IOException
public void destroy()
public ServletConfig getServletConfig()
public java.lang.String getServletInfo()

在Servlet的五个方法中,init,service和destroy是servlet的生命周期方法。在servlet类已经初始化之后,init方法将会被servlet容器所调用。servlet容器只调用一次,以此表明servlet已经被加载进服务中。init方法必须在servlet可以接受任何请求之前成功运行完毕。一个servlet程序员可以通过覆盖这个方法来写那些仅仅只要运行一次的初始化代码,例如加载数据库驱动,值初始化等等。在其他情况下,这个方法通常是留空的。

servlet容器为servlet请求调用它的service方法。servlet容器传递一个javax.servlet.ServletRequest对象和javax.servlet.ServletResponse对象。ServletRequest对象包括客户端的HTTP请求信息,而ServletResponse对象封装servlet的响应。在servlet的生命周期中,service方法将会给调用多次。

当从服务中移除一个servlet实例的时候,servlet容器调用destroy方法。这通常发生在servlet容器正在被关闭或者servlet容器需要一些空闲内存的时候。仅仅在所有servlet线程的service方法已经退出或者超时淘汰的时候,这个方法才被调用。在servlet容器已经调用完destroy方法之后,在同一个servlet里边将不会再调用service方法。destroy方法提供了一个机会来清理任何已经被占用的资源,例如内存,文件句柄和线程,并确保任何持久化状态和servlet的内存当前状态是同步的。

2. 结构分析

功能分析

通过上面servlet的生命周期,我们可以知道,一个全功能的servlet容器会为servlet的每个HTTP请求做下面一些工作:

  • 当第一次调用servlet的时候,加载该servlet类并调用servlet的init方法(仅仅一次)。
  • 对每次请求,构造一个javax.servlet.ServletRequest实例和一个javax.servlet.ServletResponse实例。
  • 调用servlet的service方法,同时传递ServletRequest和ServletResponse对象。
  • 当servlet类被关闭的时候,调用servlet的destroy方法并卸载servlet类。

我们下面实现的简单的servlet容器并不是一个全功能的servlet容器,我们先实现一个简单的servlet容器,实现的功能如下:

  • 等待HTTP请求。
  • 构造一个ServletRequest对象和一个ServletResponse对象。
  • 假如该请求需要一个静态资源的话,调用StaticResourceProcessor实例的process方法,同时传递ServletRequest和ServletResponse对象。
  • 假如该请求需要一个servlet的话,加载servlet类并调用servlet的service方法,同时传递ServletRequest和ServletResponse对象。

类图关系

我们可以把这个容器分成6个类,

  • HttpServer
  • Request
  • Response
  • StaticResourceProcessor
  • ServletProcessor
  • Constants

这个web容器的入口点(静态main方法)可以在HttpServer类里边找到。main方法创建了一个HttpServer的实例并调用了它的await方法。await方法等待HTTP请求,为每次请求创建一个Request对象和一个Response对象,并把他们分发到一个StaticResourceProcessor实例或者一个ServletProcessor实例中去,这取决于请求一个静态资源还是一个servlet。 Constants类包括涉及其他类的静态final变量WEB_ROOT。WEB_ROOT显示了PrimitiveServlet和这个容器可以提供的静态资源的位置。

3. 代码实现

  • Constants
    这里存放web资源的指定目录,用来约定资源的存放位置。
import java.io.File;public class Constants {public static final String WEB_ROOT =System.getProperty("user.dir") + File.separator  + "webroot";
}
  • HttpServer

HttpServer包含服务器的启动方法,这里要实现监听,当有http请求到达时,启动request的解析,获取请求资源uri,判断请求资源的类型,如果是静态资源,则交给StaticResourceProcessor处理,如果是servlet,则交给ServletProcessor处理。

import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;public class HttpServer {public static void main(String[] args) {HttpServer server = new HttpServer();server.await();}public void await() {ServerSocket serverSocket = null;int port = 8100;try {serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));}catch (IOException e) {e.printStackTrace();System.exit(1);}//while循环管等待socket请求并进行处理while (true) {Socket socket = null;InputStream input = null;OutputStream output = null;try {socket = serverSocket.accept();input = socket.getInputStream();output = socket.getOutputStream();// 创建一个requestRequest request = new Request(input);request.parse();// 创建一个responseResponse response = new Response(output);response.setRequest(request);// 检查请求资源是静态资源还是servlet,servlet的请求以"/servlet/"开头if (request.getUri().startsWith("/servlet/")) {ServletProcessor processor = new ServletProcessor();processor.process(request, response);}else {StaticResourceProcessor processor = new StaticResourceProcessor();processor.process(request, response);}socket.close();}catch (Exception e) {e.printStackTrace();System.exit(1);}}}
}
  • Request
    Request类实现了ServletRequest接口,这是为了方便我们进行servlet的构建,request实现解析http请求获取请求资源uri。
import java.io.InputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;public class Request  implements ServletRequest{private InputStream input;private String uri;public Request(InputStream input) {this.input = input;}public String getUri() {return uri;}//解析获取请求uriprivate String parseUri(String requestString) {int index1, index2;index1 = requestString.indexOf(' ');if (index1 != -1) {index2 = requestString.indexOf(' ', index1 + 1);if (index2 > index1)return requestString.substring(index1 + 1, index2);}return null;}// 获取http请求的数据public void parse() {StringBuffer request = new StringBuffer(2048);int i;byte[] buffer = new byte[2048];try {i = input.read(buffer);}catch (IOException e) {e.printStackTrace();i = -1;}for (int j=0; j<i; j++) {request.append((char) buffer[j]);}System.out.print(request.toString());uri = parseUri(request.toString());}@Override
public AsyncContext getAsyncContext() {// TODO Auto-generated method stubreturn null;
}@Override
public Object getAttribute(String arg0) {// TODO Auto-generated method stubreturn null;
}@Override
public Enumeration<String> getAttributeNames() {// TODO Auto-generated method stubreturn null;
}@Override
public String getCharacterEncoding() {// TODO Auto-generated method stubreturn null;
}@Override
public int getContentLength() {// TODO Auto-generated method stubreturn 0;
}@Override
public long getContentLengthLong() {// TODO Auto-generated method stubreturn 0;
}@Override
public String getContentType() {// TODO Auto-generated method stubreturn null;
}@Override
public DispatcherType getDispatcherType() {// TODO Auto-generated method stubreturn null;
}@Override
public ServletInputStream getInputStream() throws IOException {// TODO Auto-generated method stubreturn null;
}@Override
public String getLocalAddr() {// TODO Auto-generated method stubreturn null;
}@Override
public String getLocalName() {// TODO Auto-generated method stubreturn null;
}@Override
public int getLocalPort() {// TODO Auto-generated method stubreturn 0;
}@Override
public Locale getLocale() {// TODO Auto-generated method stubreturn null;
}@Override
public Enumeration<Locale> getLocales() {// TODO Auto-generated method stubreturn null;
}@Override
public String getParameter(String arg0) {// TODO Auto-generated method stubreturn null;
}@Override
public Map<String, String[]> getParameterMap() {// TODO Auto-generated method stubreturn null;
}@Override
public Enumeration<String> getParameterNames() {// TODO Auto-generated method stubreturn null;
}@Override
public String[] getParameterValues(String arg0) {// TODO Auto-generated method stubreturn null;
}@Override
public String getProtocol() {// TODO Auto-generated method stubreturn null;
}@Override
public BufferedReader getReader() throws IOException {// TODO Auto-generated method stubreturn null;
}@Override
public String getRealPath(String arg0) {// TODO Auto-generated method stubreturn null;
}@Override
public String getRemoteAddr() {// TODO Auto-generated method stubreturn null;
}@Override
public String getRemoteHost() {// TODO Auto-generated method stubreturn null;
}@Override
public int getRemotePort() {// TODO Auto-generated method stubreturn 0;
}@Override
public RequestDispatcher getRequestDispatcher(String arg0) {// TODO Auto-generated method stubreturn null;
}@Override
public String getScheme() {// TODO Auto-generated method stubreturn null;
}@Override
public String getServerName() {// TODO Auto-generated method stubreturn null;
}@Override
public int getServerPort() {// TODO Auto-generated method stubreturn 0;
}@Override
public ServletContext getServletContext() {// TODO Auto-generated method stubreturn null;
}@Override
public boolean isAsyncStarted() {// TODO Auto-generated method stubreturn false;
}@Override
public boolean isAsyncSupported() {// TODO Auto-generated method stubreturn false;
}@Override
public boolean isSecure() {// TODO Auto-generated method stubreturn false;
}@Override
public void removeAttribute(String arg0) {// TODO Auto-generated method stub}@Override
public void setAttribute(String arg0, Object arg1) {// TODO Auto-generated method stub}@Override
public void setCharacterEncoding(String arg0) throws UnsupportedEncodingException {// TODO Auto-generated method stub}@Override
public AsyncContext startAsync() throws IllegalStateException {// TODO Auto-generated method stubreturn null;
}@Override
public AsyncContext startAsync(ServletRequest arg0, ServletResponse arg1) throws IllegalStateException {// TODO Auto-generated method stubreturn null;
}}
  • Response

Response类实现静态资源的发送。

import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.File;
import java.io.PrintWriter;
import java.util.Locale;import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;public class Response  implements ServletResponse {private static final int BUFFER_SIZE = 1024;Request request;OutputStream output;PrintWriter writer;public Response(OutputStream output) {this.output = output;}public void setRequest(Request request) {this.request = request;}//处理静态资源请求public void sendStaticResource() throws IOException {byte[] bytes = new byte[BUFFER_SIZE];FileInputStream fis = null;try {File file = new File(Constants.WEB_ROOT, request.getUri());fis = new FileInputStream(file);int ch = fis.read(bytes, 0, BUFFER_SIZE);while (ch!=-1) {output.write(bytes, 0, ch);ch = fis.read(bytes, 0, BUFFER_SIZE);}}catch (FileNotFoundException e) {String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +"Content-Type: text/html\r\n" +"Content-Length: 23\r\n" +"\r\n" +"<h1>File Not Found</h1>";output.write(errorMessage.getBytes());}finally {if (fis!=null)fis.close();}}@Override
public void flushBuffer() throws IOException {// TODO Auto-generated method stub}@Override
public int getBufferSize() {// TODO Auto-generated method stubreturn 0;
}@Override
public String getCharacterEncoding() {// TODO Auto-generated method stubreturn null;
}@Override
public String getContentType() {// TODO Auto-generated method stubreturn null;
}@Override
public Locale getLocale() {// TODO Auto-generated method stubreturn null;
}@Override
public ServletOutputStream getOutputStream() throws IOException {// TODO Auto-generated method stubreturn null;
}@Override
public PrintWriter getWriter() throws IOException {writer = new PrintWriter(output, true);return writer;
}@Override
public boolean isCommitted() {// TODO Auto-generated method stubreturn false;
}@Override
public void reset() {// TODO Auto-generated method stub}@Override
public void resetBuffer() {// TODO Auto-generated method stub}@Override
public void setBufferSize(int arg0) {// TODO Auto-generated method stub}@Override
public void setCharacterEncoding(String arg0) {// TODO Auto-generated method stub}@Override
public void setContentLength(int arg0) {// TODO Auto-generated method stub}@Override
public void setContentLengthLong(long arg0) {// TODO Auto-generated method stub}@Override
public void setContentType(String arg0) {// TODO Auto-generated method stub}@Override
public void setLocale(Locale arg0) {// TODO Auto-generated method stub}}
  • StaticResourceProcessor
    StaticResourceProcessor用来实现静态资源的处理

import java.io.IOException;public class StaticResourceProcessor {public void process(Request request, Response response) {try {response.sendStaticResource();}catch (IOException e) {e.printStackTrace();}}
}
  • ServletProcessor
    在ServletProcessor中,我们将根据request解析到servlet名字,进行类加载并获取类的实例。
    这里,我们使用URLClassLoader来进行类加载,
    public URLClassLoader(URL[] urls)
    URL 代表一个统一资源定位符,它是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用。
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.io.File;
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;public class ServletProcessor {public void process(Request request, Response response) {String uri = request.getUri();String servletName = uri.substring(uri.lastIndexOf("/") + 1);URLClassLoader loader = null;try {// 创建一个URLClassLoader//初始化数组,这里我们为了方便指定数组的长度为1URL[] urls = new URL[1];//servlet文件的根目录 File classPath = new File(Constants.WEB_ROOT);urls[0] = new URL("file", null, classPath.getCanonicalPath() + File.separator);loader = new URLClassLoader(urls);}catch (IOException e) {System.out.println(e.toString() );}//加载类Class myClass = null;try {myClass = loader.loadClass(servletName);}catch (ClassNotFoundException e) {System.out.println(e.toString());}Servlet servlet = null;try {//获取类的实例servlet = (Servlet) myClass.newInstance();System.out.println("servlet=="+servlet);servlet.service((ServletRequest) request, (ServletResponse) response);}catch (Exception e) {System.out.println(e.toString());}catch (Throwable e) {System.out.println(e.toString());}}
}

这样子,我们的servlet容器就实现了。
为了进行测试,我们新建一个servlet。

  • PrimitiveServlet
import java.io.IOException;
import java.io.PrintWriter;import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;public class PrimitiveServlet implements Servlet{public void service(ServletRequest request, ServletResponse response) throws IOException{   System.out.println("from service");try{PrintWriter out = response.getWriter();out.print("HelloWorld");out.close();}catch(Exception e){e.printStackTrace();}}@Overridepublic void destroy() {System.out.println("destroy");}@Overridepublic ServletConfig getServletConfig() {// TODO Auto-generated method stubreturn null;}@Overridepublic String getServletInfo() {// TODO Auto-generated method stubreturn null;}@Overridepublic void init(ServletConfig arg0) throws ServletException {System.out.println("init");}
}

并进行编译,生成PrimitiveServlet.class
在项目的根目录下新建一个文件夹webroot,并把PrimitiveServlet.class放在目录下。

下面就可以进行测试了。
http://localhost:8100/servlet/PrimitiveServlet
在浏览器上输入上面的地址。

到这里,一个简单的servlet容器就实现了。

4. 程序优化

在上面的程序中,存在一个很严重的问题。

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

我们把Request和Response分别向上转换为了ServletRequest和ServletResponse。
这会危害安全性。知道这个servlet容器的内部运作的Servlet程序员可以分别把ServletRequest和ServletResponse实例向下转换为Request和Response,并调用他们的公共方法。拥有一个Request实例,它们就可以调用parse方法。拥有一个Response实例,就可以调用sendStaticResource方法。

在Tomcat中,是采用结构模式(facade模式)来弥补这个弊端的。
我们增加两个facade类:

  • RequestFacade
  • ResponseFacade

RequestFacade实现了ServletRequest接口并通过在构造方法中传递一个引用了ServletRequest对象的Request实例作为参数来实例化。ServletRequest接口中每个方法的实现都调用了Request对象的相应方法。然而ServletRequest对象本身是私有的,并不能在类的外部访问。我们构造了一个RequestFacade对象并把它传递给service方法,而不是向下转换Request对象为ServletRequest对象并传递给service方法。Servlet程序员仍然可以向下转换ServletRequest实例为RequestFacade,不过它们只可以访问ServletRequest接口里边的公共方法。现在parseUri方法就是安全的了。

ResponseFacade

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import javax.servlet.ServletResponse;
import javax.servlet.ServletOutputStream;public class ResponseFacade implements ServletResponse {private ServletResponse response;public ResponseFacade(Response response) {this.response = response;}public void flushBuffer() throws IOException {response.flushBuffer();}public int getBufferSize() {return response.getBufferSize();}public String getCharacterEncoding() {return response.getCharacterEncoding();}public Locale getLocale() {return response.getLocale();}public ServletOutputStream getOutputStream() throws IOException {return response.getOutputStream();}public PrintWriter getWriter() throws IOException {return response.getWriter();}public boolean isCommitted() {return response.isCommitted();}public void reset() {response.reset();}public void resetBuffer() {response.resetBuffer();}public void setBufferSize(int size) {response.setBufferSize(size);}public void setContentLength(int length) {response.setContentLength(length);}public void setContentType(String type) {response.setContentType(type);}public void setLocale(Locale locale) {response.setLocale(locale);}}

RequestFacade

import java.io.IOException;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;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();}public String getRealPath(String path) {return request.getRealPath(path);}public RequestDispatcher getRequestDispatcher(String path) {return request.getRequestDispatcher(path);}public boolean isSecure() {return request.isSecure();}public String getCharacterEncoding() {return request.getCharacterEncoding();}public int getContentLength() {return request.getContentLength();}public String getContentType() {return request.getContentType();}public ServletInputStream getInputStream() throws IOException {return request.getInputStream();}public Locale getLocale() {return request.getLocale();}public Enumeration getLocales() {return request.getLocales();}public String getParameter(String name) {return request.getParameter(name);}public Map getParameterMap() {return request.getParameterMap();}public Enumeration getParameterNames() {return request.getParameterNames();}public String[] getParameterValues(String parameter) {return request.getParameterValues(parameter);}public String getProtocol() {return request.getProtocol();}public BufferedReader getReader() throws IOException {return request.getReader();}public String getRemoteAddr() {return request.getRemoteAddr();}public String getRemoteHost() {return request.getRemoteHost();}public String getScheme() {return request.getScheme();}public String getServerName() {return request.getServerName();}public int getServerPort() {return request.getServerPort();}public void removeAttribute(String attribute) {request.removeAttribute(attribute);}public void setAttribute(String key, Object value) {request.setAttribute(key, value);}public void setCharacterEncoding(String encoding)throws UnsupportedEncodingException {request.setCharacterEncoding(encoding);}}

修改ServletProcessor

Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request); ResponseFacade responseFacade = new ResponseFacade(response);
try
{
servlet = (Servlet) myClass.newInstance();           servlet.service((ServletRequest)requestFacade(ServletResponse)responseFacade);
}

tomcat之servlet容器相关推荐

  1. tomcat(5)servlet容器

    [0]README 0.0)本文部分文字描述转自:"深入剖析tomcat",旨在学习 tomcat(5)servlet容器 的基础知识: 0.1)intro to servlet容 ...

  2. tomcat(5)servlet容器(lastest version)

    [0]README 0.0)本文部分文字描述转自:"深入剖析tomcat",旨在学习 tomcat(5)servlet容器 的基础知识: 0.1)intro to servlet容 ...

  3. SpringBoot之配置嵌入式Servlet容器

    1.概述 文章目录 1.概述 2.如何修改SpringBoot的默认配置 3.定制和修改Servlet容器的相关配置 4.注册Servlet三大组件 5.替换为其他嵌入式Servlet容器 6.嵌入式 ...

  4. Spring Boot切换其他嵌入式的Servlet容器

    Spring Boot默认支持: Tomcat(默认使用) <dependency><groupId>org.springframework.boot</groupId& ...

  5. Spring boot切换Servlet容器

    切换Servlet容器 Spring boot默认配置Tomcat作为Servlet容器 引入web模块,默认使用嵌入式的Tomcat 可以切换Jetty.Undertow 默认配置 Pom文件,查看 ...

  6. servlet容器_Tomcat 容器与servlet的交互原理

    点击蓝字"程序员考拉"欢迎关注! Tomcat 是Web应用服务器,是一个Servlet/JSP容器. Tomcat 作为Servlet容器,负责处理客户请求,把请求传送给Serv ...

  7. tomcat和servlet的关系

    tomcat和servlet的关系 Tomcat 是Web应用服务器,是一个Servlet/JSP容器. Tomcat 作为Servlet容器,负责处理客户请求,把请求传送给Servlet,并将Ser ...

  8. spring boot没有web.xml,如何向嵌入式的servlet容器中注册servlet组件

    1. Spring boot默认使用Tomcat作为嵌入式的servlet容器,只要引入spring-boot-starter-web依赖,就会默认用Tomcat作为servlet容器. 2. Spr ...

  9. tomcat和servlet的关系及区别

    tomcat和servlet的关系及区别 Tomcat 是Web应用服务器,是一个Servlet/JSP容器. Tomcat 作为Servlet容器,负责处理客户请求,把请求传送给Servlet,并将 ...

  10. springboot-嵌入式Servlet容器(Tomcat)源码分析以及容器切换

    目录 一.springboot的嵌入式Servlet容器(tomcat) 1.原理 2.源码 (1)ServletWebServerApplicationContext的createWebServer ...

最新文章

  1. [PHP] JQuery+Layer实现添加删除自定义标签代码
  2. CSDN上究竟可以上载多大的GIF文件?
  3. Start Activity for Result(Bug)
  4. (转)jQuery第五课:Ajax
  5. 八皇后java_经典八皇后问题:Java语言
  6. MVC中提示错误:从客户端中检测到有潜在危险的 Request.Form 值的详细解决方法...
  7. 【原创】关于ASP.NET WebForm与ASP.NET MVC的比较
  8. Mysql之七种连接查询
  9. 引入react文件报错_react.js引入router文件后报错
  10. java中如何引用非静态变量_java为什么不能从静态环境引用非静态变量?
  11. Netbeans 适配C/C++、JAVA防坑秘笈
  12. Navicat连接Oracle数据库
  13. 倍福PLC通过CANOpen通信控制伺服
  14. als算法参数_矩阵分解之交替最小二乘ALS
  15. 电子实验记录本促进科研诚信建设
  16. 7、粗略的调整图片对比度和亮度
  17. 【论文笔记】PassGAN: A Deep Learning Approach for Password Guessing
  18. 生态圈——小组制组织结构思考
  19. html实体手册,完整的 HTML 4 + HTML 5 实体参考手册
  20. Obsolete和Conditional属性

热门文章

  1. 如何更改PDF文件中的字体?
  2. java cropper_Image Cropper 的 JAVA 支持
  3. 水环境指标 中文对照
  4. 数据总线、地址总线、控制总线
  5. mac文件管理服务器,Path Finder For Mac v8.6 高效的文件管理工具 _ 黑苹果乐园
  6. python画同心圆程序_如何用python画同心圆并内接一个五角星?
  7. 精心整理10个高逼格的优质素材(视频、图片)网站
  8. jwplayer html插件,jQuery插件JWPlayer视频播放器用法实例分析
  9. java 电子签章 开源_java操作pdf制作电子签章 - CSDN博客
  10. 基于Python的FreeCAD二次开发