• [Java] Servlet工作原理之一:体系结构及其容器


    一、Servlet体系结构

    在 servlet-api.jar (2.5) 中有两个包:javax.servlet 和 javax.servlet.http

               

    1 Servlet、GenericServlet及HttpServlet

    Servlet 是一个接口,其方法如下:

    • public void init(ServletConfig config);
    • public void service(ServletRequest req, ServletResponse res);
    • public void destroy();
    • public String getServletInfo(); // 返回servlet的信息,如作者、版本和版权
    • public ServletConfig getServletConfig(); // 获取servlet配置属性对象

    GenericServlet 实现了 Servlet接口,是一个通用的、不特定于任何协议的 Servlet

    • public void init()
    • public void init(ServletConfig config)
    • public abstract void service(ServletRequest req, ServletResponse res)
    • public void destroy()
    • public void log(String msg) // 将消息写入日志,利用ServletContext的方法写入
    • public void log(String message, Throwable t)
    • public String getInitParameter(String name) // 获取初始化参数,利用ServletConfig的方法获取
    • public Enumeration getInitParameterNames()
    • public String getServletName()
    • public String getServletInfo() // 返回servlet的信息,如作者、版本和版权
    • public ServletConfig getServletConfig()
    • public ServletContext getServletContext()

    HttpServlet 继承于 GenericServlet,针对于 HTTP 协议的类

    • public void service(ServletRequest req, ServletResponse res)
    • protected void service(HttpServletRequest req, HttpServletResponse resp)
    • protected void doGet(...)、doPost(...)、doHead(...)、doPut(...)、doDelete(...)、doOptions(...)、doTrace(...)
    • protected long getLastModified(HttpServletRequest req) // 最后修改时间,应该重写实现这个方法

    2 ServletConfig、ServletContext

    ServletConfig 对象储存了 Servlet 的一些配置属性,在 Servlet 执行 init() 方法时传入,其方法如下:

    • public String getServletName(); // 获取Servlet名称
    • public String getInitParameter(String name); // 获取初始化参数值
    • public Enumeration getInitParameterNames(); // 获取所有参数名称
    • public ServletContext getServletContext();

    另外在 ServletConfig 中还有一个 ServletContext,它定义了有关 Servlet 容器的方法,其方法如下:

    • public String getContextPath(); // 返回web项目的路径 
    • public String getRealPath(String path);
    • public URL getResource(String path); // 返回webapp下的文件路径对应的URL
    • public Set getResourcePaths(String path); // 返回path路径下的目录或文件
    • public InputStream getResourceAsStream(String path); // 返回path路径的资源
    • public ServletContext getContext(String uripath);
    • public RequestDispatcher getRequestDispatcher(String path);
    • public RequestDispatcher getNamedDispatcher(String name);
    • public String getMimeType(String file); // 返回指定文件的类型,如 text/html、image/gif
    • public String getServerInfo(); // 返回Servlet容器的名称和版本 
    • public String getServletContextName(); // 返回这个web应用程序名称
    • public String getInitParameter(String name);
    • public Enumeration getInitParameterNames();
    • public Enumeration getAttributeNames(); // 返回Servlet容器的所有属性
    • public Object getAttribute(String name); // 返回Servlet容器的指定属性 
    • public void setAttribute(String name, Object object);
    • public void removeAttribute(String name);
    • public void log(String msg); // 将消息写入到日志文件中 
    • public void log(String message, Throwable throwable);
    • public int getMajorVersion(); // 返回这个容器支持的Servlet主版本,如2.5返回2 
    • public int getMinorVersion(); // 返回这个容器支持的Servlet小版本,如2.5返回5 

    3 ServletRequest、ServletResponse

    当请求达到时,容器将 ServletRequest 和 ServletResponse 传递给 Servlet。

    ServletRequest 接口的方法如下:

    • public Enumeration getAttributeNames();
    • public Object getAttribute(String name);
    • public void setAttribute(String name, Object o);
    • public void removeAttribute(String name);
    • public void setCharacterEncoding(String env); // 设置请求体的编码类型,读取参数前使用
    • public String getCharacterEncoding(); // 获取请求体的编码类型
    • public String getContentType(); // 请求体的类型
    • public int getContentLength(); // 请求体的长度,长度未知则返回-1
    • public ServletInputStream getInputStream(); // 请求体的字节流
    • public BufferedReader getReader(); // 请求体的字符流
    • public String getParameter(String name); // 名为name的参数值
    • public String[] getParameterValues(String name); // 名为name的参数值,是一个数组
    • public Enumeration getParameterNames(); // 所有参数名称
    • public Map getParameterMap(); // 所有参数的名称和值
    • public String getProtocol(); // 请求的协议版本,如 HTTP/1.1
    • public String getScheme(); // 请求的协议方式,如 http https ftp
    • public String getServerName(); // 服务端的主机、服务器名或服务器IP地址
    • public int getServerPort(); // 服务端的端口号
    • public String getRemoteHost(); // 客户端或最终代理的主机名称
    • public String getRemoteAddr(); // 客户端或最终代理的IP地址
    • public int getRemotePort(); // 客户端或最终代理的端口号
    • public String getLocalName();
    • public String getLocalAddr();
    • public int getLocalPort();
    • public Locale getLocale(); // 返回请求头Accept-Language设置的语言环境
    • public Enumeration getLocales();
    • public boolean isSecure(); // 是否使用HTTPS等安全通道进行的请求
    • public RequestDispatcher getRequestDispatcher(String path);

    ServletResponse 接口的方法如下:

    • public String getCharacterEncoding(); // 获取响应体的编码类型
    • public void setCharacterEncoding(String charset); // 设置响应体编码类型
    • public String getContentType(); // 获取响应体的类型
    • public void setContentType(String type); // 设置响应体的类型
    • public void setContentLength(int len); // 设置响应体长度
    • public ServletOutputStream getOutputStream(); // 获取响应体的字节流
    • public PrintWriter getWriter(); // 获取响应体的字符流
    • public int getBufferSize(); // 返回实际缓冲大小,不使用缓冲则为0
    • public void setBufferSize(int size); // 设置响应体缓冲大小
    • public void flushBuffer(); // 将缓冲区内容写入到客户端
    • public void resetBuffer(); // 清除缓冲区数据,如果缓冲已经被写入客户端,则抛异常
    • public void reset(); // 清除缓冲区的数据、状态码及响应头,如果已经写入客户端,则抛异常
    • public boolean isCommitted(); // 响应是否已提交
    • public void setLocale(Locale loc);
    • public Locale getLocale();

    HttpServletRequest 和 HttpServletResponse 接口分别继承自 ServletRequest 和 ServletResponse,在其基础上

    HttpServletRequest 接口增加的方法如下:

    • public String getAuthType();
    • public Cookie[] getCookies();
    • public long getDateHeader(String name);
    • public int getIntHeader(String name);
    • public String getHeader(String name);
    • public Enumeration getHeaders(String name);
    • public Enumeration getHeaderNames();
    • public String getMethod();
    • public String getPathInfo();
    • public String getPathTranslated();
    • public String getContextPath();
    • public String getQueryString();
    • public String getRemoteUser();
    • public boolean isUserInRole(String role);
    • public java.security.Principal getUserPrincipal();
    • public String getRequestedSessionId();
    • public String getRequestURI();
    • public StringBuffer getRequestURL();
    • public String getServletPath();
    • public HttpSession getSession(boolean create);
    • public HttpSession getSession();
    • public boolean isRequestedSessionIdValid();
    • public boolean isRequestedSessionIdFromCookie();
    • public boolean isRequestedSessionIdFromURL();

    HttpServletResponse 接口增加的方法如下:

    • public void addCookie(Cookie cookie);
    • public boolean containsHeader(String name);
    • public String encodeURL(String url);
    • public String encodeRedirectURL(String url);
    • public void sendError(int sc, String msg) throws IOException;
    • public void sendError(int sc) throws IOException;
    • public void sendRedirect(String location) throws IOException;
    • public void setDateHeader(String name, long date);
    • public void addDateHeader(String name, long date);
    • public void setHeader(String name, String value);
    • public void addHeader(String name, String value);
    • public void setIntHeader(String name, int value);
    • public void addIntHeader(String name, int value);
    • public void setStatus(int sc);

    上述的四个接口分别有一个包装类的实现,利用了装饰者模式。

    4 Filter、FilterConfig、FilterChain

    Filter 即过滤器,它是 AOP 思想的一种实现(利用回调函数实现的),通过它我们可以实现权限访问控制、过滤敏感词汇、日志记录等等。为什么要使用 Filter 呢?或者说为什么要使用 AOP 的方式去做这个呢?如果我们不使用 Filter 而直接在 Servlet 的 doGet()、doPost() 方法中实现上述功能也是可以的,但是这样导致了代码冗余,所以我们需要把这些公共的代码抽象出来进行封装。像 OOP 的封装方式针对的是对具有上下关系的对象,而像访问控制、日志等功能并不适合这样的封装,它更像是一种左右关系,所以我们要用 AOP 的方式进行封装。

    Filter 可以实现在 Servlet 的 service() 调用的前后执行一段代码,从而实现了公共代码的复用。使用 Filter 与 Servlet 相似,首先要自己编写一个类实现 Filter 接口,然后在 web.xml 中配置好直接该 Filter 对应的 URL。Filter 中有一个 doFilter() 方法,其使用方式大致如下

    public class FilterTest implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("init");
            // 获取过滤器的名字
            String filterName = filterConfig.getFilterName();
               // 获取其初始化参数,在 web.xml 中指定的
               String param1 = filterConfig.getInitParameter("name");
               String param2 = filterConfig.getInitParameter("like");
            // 返回过滤器的所有初始化参数的名字的枚举集合。
            Enumeration<String> paramNames = filterConfig.getInitParameterNames();
            System.out.println(filterName);
            System.out.println(param1);
            System.out.println(param2);
            while (paramNames.hasMoreElements()) {
                    String paramName = (String) paramNames.nextElement();
                    System.out.println(paramName);
            }
        }
        @Override
        public void doFilter(ServletRequest request, ServletResponse response,    
                FilterChain chain) throws ServletException, IOException {
            // 执行前的操作
            request.setCharacterEncoding("UTF-8");
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html;charset=UTF-8");
            System.out.println("before");
            // 执行service()方法或下一个过滤器方法
            chain.doFilter(request, response); //让目标资源执行,放行
            // 执行后的操作
            System.out.println("after");
        }
        @Override
        public void destroy() {
            System.out.println("destroy");
        }
    }

    编写完 Filter 实现类后还要在 web.xml 文件中对其注册和映射

    <!-- filter注册 -->
    <filter>
        <filter-name>FilterTest</filter-name>
        <filter-class>com.filter.FilterTest</filter-class>
        <init-param>
            <param-name>name</param-name>
            <param-value>t</param-value>
        </init-param>
        <init-param>
            <param-name>like</param-name>
            <param-value>java</param-value>
        </init-param>
    </filter>
    <!-- filter映射 -->
    <filter-mapping>
        <filter-name>FilterTest</filter-name>
        <url-pattern>*.do</url-pattern>
        <!-- 指定过滤器所拦截的 Servlet 名称
        <servlet-name></servlet-name> -->
        <!-- 指定过滤器所拦截的资源被 Servlet 容器调用的方式,
             REQUEST:用户直接访问时调用,即不包括通过RequestDispatcher访问的情况
             INCLUDE:通过RequestDispatcher的include()方法访问时调用
             FORWARD:通过RequestDispatcher的forward()方法访问时调用
             ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用
             默认REQUEST,并且可以设置多个<dispatcher>
        <dispatcher></dispatcher> -->
    </filter-mapping>

    我们可以编写多个 Filter,组成了一个 Filter 链。执行顺序与它们在 web.xml 文件中配置顺序有关,先配置则先执行。在上述代码中,我们调用了 FilterChain 对象的 doFilter() 方法,此时会先检查 FilterChanin 对象中是否还有下一个 Filter,如果有则继续调用,如果没有则调用 Servlet 的 service() 方法。

    4.1 Filter

    Filter 的创建和销毁由其容器负责,容器启动的时候创建 Filter 实例对象,并调用 init() 方法完成初始化,Filter 只会实例化一次。

    • public void init(FilterConfig filterConfig); // 初始化并传入Filter的配置对象
    • public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain); // 执行拦截器的内容
    • public void destroy(); // Filter销毁时调用,当这个方法被调用后,容器还会再调用一次 doFilter() 方法

    4.2 FilterConfig

    • public String getFilterName();
    • public ServletContext getServletContext();
    • public String getInitParameter(String name);
    • public Enumeration getInitParameterNames();

    4.3 FilterChain

    Filter 类的核心就是传递 FilterChain 对象,在 Tomcat 中 FilterChain 的实现类是 ApplicationFilterChain,它在 filters 数组中保存了到最终 Servlet 对象的所有 Filter 对象,当执行完所有 Filter 对象后就会执行 Servlet。

    • public void doFilter(ServletRequest request, ServletResponse response);

    5 RequestDispatcher

    • public void forward(ServletRequest request, ServletResponse response)
    • public void include(ServletRequest request, ServletResponse response)

    6 Listener

    Listener 是基于观察者模式设计的,能够方便的从另一个纵向维度控制程序和数据。在 Servlet 中有两类共6中观察者接口,EventListeners 类型的 ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttrbuteListener,还有 LifecycleListeners 类型的 ServletContextListener、HttpSessionListener,如图所示

    这些标签的实现类可以配置在 web.xml 的 <listener> 标签中,也可以在程序中动态的添加。如 Spring 的 org.springframework.web.context.ContextLoaderLister 就实现了一个 ServletContextListener,当容器加载时启动 Spring,如下所示

    <!-- spring启动监听器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- spring配置文件,默认查找 WEB-INF 下的 applicationContext.xml 文件 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring.xml</param-value>
    </context-param>

    下面我们看一下各个 Listener 的具体方法:

    ServletContextListener

    • public void contextInitialized(ServletContextEvent sce); //在 Context 容器初始时,Filter 和 Servlet 的 init() 前调用
    • public void contextDestroyed(ServletContextEvent sce); //在 Context 容器销毁时,Filter 和 Servlet 的 destroy() 后调用

    ServletRequestListener

    • public void requestInitialized(ServletRequestEvent sre); // HttpServeltRequest 传递到 Servlet 前调用
    • public void requestDestroyed(ServletRequestEvent sre); 

    HttpSessionListener

    • public void sessionCreated(HttpSessionEvent se);
    • public void sessionDestroyed(HttpSessionEvent se);

    HttpSessionBindingListener

    • public void valueBound(HttpSessionBindingEvent event); // 对象被放入 session 时调用
    • public void valueUnbound(HttpSessionBindingEvent event); // 对象被移出 session 时调用

    ServletContextAttributeListener

    • public void attributeAdded(ServletContextAttributeEvent scab); //调用 servletContext 的 setAttribute() 时触发
    • public void attributeRemoved(ServletContextAttributeEvent scab); //调用 servletContext 的 removeAttribute() 时触发
    • public void attributeReplaced(ServletContextAttributeEvent scab); //调用 servletContext 的 setAttribute() 替换旧值时触发

    ServletRequestAttributeListener

    • public void attributeAdded(ServletRequestAttributeEvent srae);
    • public void attributeRemoved(ServletRequestAttributeEvent srae);
    • public void attributeReplaced(ServletRequestAttributeEvent srae);

    HttpSessionAttributeListener

    • public void attributeAdded(HttpSessionBindingEvent se);
    • public void attributeRemoved(HttpSessionBindingEvent se);
    • public void attributeReplaced(HttpSessionBindingEvent se);

    HttpSessionActivationListener

    • public void sessionWillPassivate(HttpSessionEvent se); // 通知 session 被钝化
    • public void sessionDidActivate(HttpSessionEvent se); // 通知 session 被激活

    7 ServletInputStream、ServletOutputStream

    8 ServletException

    二、Tomcat 组件

    Servlet 不能够独立运行,需要在它的容器中运行,容器管理着它创建到销毁的整个过程。在看 Servlet 的生命周期前,我们先看下 Servlet 的我们最熟悉的一个容器——Tomcat。

    Tomcat 有两个重要组件:连接器(Connector)和容器(Engine容器及其子容器),我们结合 server.xml 配置文件来看一下这两个组件。

    1 连接器(Connector)

    首先向 Tomcat 发送的请求可以分为两类:

    • Tomcat 作为应用服务器:请求来自前端的 Web 服务器,如 Nginx、Apache、IIS 等。
    • Tomcat 作为独立服务器:请求来自浏览器。

    这些不同的请求需要不同的连接器来接收,在 Service 中有一个引擎和多个连接器,以适应不同情况。常见的连接器有四种:HTTP连接器、SSL连接器、AJP连接器、proxy连接器。在定义连接器时可以配置的属性有很多,连接器公用属性如下:

    • className 指定实现 Connector 接口的类
    • enableLookups 是否通过request.getRemoteHost()获取客户端的主机名,默认true
    • redirectPort 如果连接器的协议是HTTP,当收到HTTPS请求时,转发到此端口

    HttpConnector 的属性:

    • className 指定实现 Connector 接口的类
    • port 监听端口,默认8080
    • address 指定监听地址,默认为所有地址
    • bufferSize 设置由端口创建的输入流缓存大小,默认2048byte
    • protocol 连接器使用的协议,默认HTTP/1.1
    • maxThreads 支持的最大并发连接数,默认200
    • connectionTimeout 等待客户端发送请求的超时时间,默认60000,即1分钟
    • acceptCount 设置等待队列的最大长度,默认为10。当tomcat所有处理线程均繁忙时,新链接被放置于等待队列中

    JkConnector 的属性:

    • className 指定实现 Connector 接口的类
    • port 设定AJP端口号
    • protocol 必须为 AJP/1.3

    2 容器(Engine容器及其子容器)

    在 Tomcat 中有 Engine、Host、Context 及 Wrapper 四种容器,它们的包含关系如下图所示

    上述的包含并不是继承关系,而是当子容器创建好后会放入到父容器中。Servlet 被包装成 Wrapper,然后真正管理 Servlet 的是 Context 容器,一个 Context 对应一个 Web 应用。

    • Wrapper 封装了具体访问的资源,即 Servlet;
    • Context 封装了各个 Wrapper 资源的集合;
    • Host 封装了 Context 资源的集合;
    • Engine 可以看成是对 Host 的逻辑封装。

    我们再来看一下它们的继承关系,这些容器的接口都继承自 Container 接口,为什么要按层次分别封装一个对象呢?为了方便统一管理,在不同层次的配置其作用域是不一样的。

    2.1 Engine

    Engine 下面拥有多个 Host,即虚拟主机,它的责任就是将用户的请求分配给一个虚拟主机处理。为什么要使用虚拟主机呢?当我们有两个应用时,如下图的 Love 应用和 SDJTU 应用。我们想访问“倪培.我爱你”域名时直接达到 Love 应用,访问“www.sdjtu.net.cn”域名时直接到达 SDJTU 应用,但是如果不设置虚拟主机是无法在一个 Tomcat中做到的。那么,我们可以设置两个虚拟主机,并指定请求到达这个虚拟主机后要去访问的目录。

    在 Engine 标签中有几个属性可以填写

    • name 定义 Engine 的名字
    • className 指定实现 Engine 接口的类,默认是 StandardEngine
    • defaultHost 指定处理请求的默认主机

    在 Engine 标签里还可以包含以下几个元素

    • Logger
    • Realm
    • Valve
    • Host

    2.2 Host

    Host 代表一个虚拟主机,在它下面有多个 Context,一个 Context 代表一个 Web 应用。

    在 Host 标签中的几个属性

    • name 定义 Host 的名字
    • className 指定实现 Host 接口的类,默认是 StandardHost
    • appBase 指定虚拟主机的目录,默认是 webapps
    • unpackWARs 是否先展开war文件再运行。如果为 false 将直接运行 war 文件
    • autoDeploy 表示是否支持热部署
    • alias 用来指定主机别名
    • deployOnStartup 是否在启动时自动发布目录下的所有Web应用

    在 Host 标签中还可以包含以下几个元素

    • Logger 
    • Realm
    • Valve
    • Host

    2.3 Context

    Context 代表运行在虚拟主机上的单个 Web 应用。

    在 Context 标签中的几个属性

    • className 指定现实 Context 接口的类,默认是 StandardContext 类
    • path 配置Web应用对应的URL,即跟在域名后面的内容
    • docBase 指定要执行的Web应用
    • reloadable 当项目下的 class 文件被更新时,是否重新加载Web应用
    • cookies 指定是否通过 Cookies 来支持 Session,默认为 true
    • useNaming 指定是否支持 JNDI,默认值为 ture

    在 Context 标签中的元素

    • Logger
    • Realm
    • Resource
    • ResourceParams

    2 Tomcat 启动过程

    Tomcat 从 7.0 开始增加了一个启动类 org.apache.catalina.startup.Tomcat。通过这个类的实例调用 start() 方法就可以启动 Tomcat,还可以通过这个对象增加和修改 Tomcat 的配置参数,来动态的添加 Context、Servlet 等。

    Tomcat 的启动是基于观察者模式设计的,所有的容器都继承了 Lifecycle 接口,由它来管理容器的生命周期,所有容器的修改和状态改变都会由它去通知已经注册的观察者(Listener)。

    当 Context 容器初始化状态为 init 时,添加到 Context 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口,它是在调用了 Tomcat.addWebapp 时被加入到 StandardContext 容器的,这个类将会负责整个 Web 应用的配置解析工作。ContextConfig 的 init 方法将会主要完成以下工作:

    1. 创建 ContextDigester 对象来解析 XML 配置文件
    2. 读取默认的 context.xml 配置文件,如果存在则解析它
    3. 读取默认的 Host 配置文件,如果存在则解析它
    4. 读取默认的 Context 自身的配置文件,如果存在则解析它
    5. 设置 Context 的 DocBase

    当 ContextConfig 的 init 方法完成后,Context 容器会执行 startInternal 方法,主要包括以下工作

    1. 创建读取资源文件的对象
    2. 创建 ClassLoader 对象
    3. 设置应用的工作目录
    4. 启动相关的辅助类,如 logger、realm、resources 等
    5. 修改启动状态,通知感兴趣的观察者
    6. 子容器的初始化
    7. 获取 servletContext 并设置必要参数
    8. 初始化“load on startup”的 Servlet

    Web 应用的初始化是在 ContextConfig 的 configureStart 方法中实现的,应用初始化主要是解析 web.xml 文件。web.xml 文件中的配置会被解析成 WebXml 对象,然后这些配置会放入 Context 中,并且 Servlet 配置会被包装成 StandardWrapper 并作为子容器添加到 Context 中。

    三、Servlet 生命周期

    前面我们知道 Servlet 由 Tomcat 解析,并被包装成 Wrapper 添加在 Context 容器中,下面就要进行 Servlet 的实例化。

    1 创建实例

    创建 Servlet 实例的方法是从 StandardWrapper 的 loadServlet() 方法开始的。loadServlet() 方法获取了 servletClass,然后将它交给了 InstanceManager 去创建一个基于 ServletClass.class 的对象。

    Servlet 并不是单例的,但一般只会有一个实例,即一个<servlet>标签对应一个实例。另外如果 Servlet 没有配置<servlet-mapping>标签,则无法通过请求时创建,只能配置 load-on-startup 使其在容器启动时便创建。 

    2 初始化

    初始化 Servlet 是在 StandardWrapper 对象的 initServlet() 方法中,这个方法会去调用 Servlet 的 init() 方法,同时把 StandardWrapperFacade 对象作为 ServletConfig 传递进去。

    3 处理请求

    客户端发出 Http 请求,Tomcat 接收到请求后将信息封装进了 HttpRequest 对象,接着创建一个 HttpResponse 对象,然后调用 HttpServlet 对象的 service() 方法,把 HttpRequest 对象与 HttpRespnse 对象传入进去。当执行完 service() 方法后,Tomcat 把响应传递给客户端。

    4 销毁

      

  • 相关阅读:
    linux学习(六)计划任务命令
    如何在在手机上安装linux(ubuntu )关键词:Termux
    linux学习(五)用户与组管理命令,以及用户信息文件解释
    linux学习(四)复制(cp)移动(mv)删除(rm)查找(find)文件、文件夹操作、软硬链接的区别
    Flutter中通过https post Json接收Json
    Api管家系列(一):初探
    Api管家系列(三):测试和Rest Client
    Api管家系列(二):编辑和继承Class
    JDK8 时间相关API基本使用
    windows杀端口
  • 原文地址:https://www.cnblogs.com/tengyunhao/p/7481324.html
Copyright © 2020-2023  润新知