起初,所有的 Web 网站都是静态的,网页只是作为一个信息的发布平台,客户机上请求服务器上的某个资源,服务器将指定的资源返回给客户机。随着计算机网络的发展,显然静态网站已经无法满足需求。Servlet 是 Java 进军 Web 开发领域的第一款技术,完全基于Java实现。
Servlet 是一个运行于 Servlet 容器中的 Java 对象,具有很多的优势,例如 Servlet 初始化仅在第一次访问时进行并且只执行一次,Servlet 能够维护每个请求的状态,一旦加载了 Servlet,他就存放在内存中,具有移植性等。
不得不说,Servlet 确实是一门古老的技术了,现在很少有公司直接使用 Servlet 来写项目了,大家都在用 SpringMVC-Spring-MyBatis / SpringBoot 做开发了,那么都2023年了,Servlet 还需要学习吗?真的有必要吗?
答案是肯定的,学习了 SpringMVC 的同学一定知道,他的底层是离不开 Servlet 的,其实不止这一点,还有很多的地方用到了 Servlet 思想。所以,一个中肯的建议是不要跳过 Servlet 去学习框架,学习完 Servlet ,你将有更好的基础去面对后面的框架的知识。
说完了学习 Servlet 的必要性,下面我们将开始 Servlet 系列的学习,你正在阅读的是 Servlet 快速入门篇,让你快速了解 Servlet 技术。
Servlet 是 JavaEE 的规范之一,通俗的来说就是 Java 接口,将来我们可以定义 Java 类来实现这个接口,并由 Web 服务器运行 Servlet ,所以 TomCat 又被称作 Servlet 容器。
Servlet 提供了动态 Web 资源开发技术,一种可以将网页数据提交到 Java 代码,并且将 Java 程序的数据返回给网页的技术,使用 Servlet 技术实现了不同用户登录之后在页面上动态的显示不同内容等的功能。
我们通过一个案例来快速了解什么是 Servlet,怎么样使用 Servlet,后面的案例大多不做具体实现,只是举例的方式说明,同时我们会在快速入门的过程中多了解源码种的实现逻辑。
需求:编写一个 Servlet,部署到 Tomcat 服务器中,最终实现浏览器访问这个 Servlet 程序。
第一步:
创建一个 Web 项目 servlet-project,导入 Servlet 的依赖坐标
javax.servlet javax.servlet-api 3.1.0 provided
请把这个依赖的范围设置为 provided ,即只在编译和测试的过程中有效,最后生成的 war 包中不会加入这个依赖 jar 包,因为 TomCat 文件中本身就带有这个 jar 包,如果不使用这个范围,则会出现冲突。添加依赖范围时只需要在依赖坐标下面添加
第二步:
定义一个类,用来实现 Servlet 接口,并重写接口中的所有方法。
public class ServletDemo implements Servlet { @Override public void init(ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { System.out.println("Hello"); } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }
第三步:
通过 @WebServlet 注解,配置其访问路径,如图:
第四步:
启动 TomCat ,在浏览器中访问该 Servlet,
此时,浏览器页面展示 Hello World!因为我们项目中有一个默认的 html 文件,文件中内容被展示到浏览器。此时往浏览器的 url 路径中添加刚才使用 Java 注解配置的访问路径,idea 控制台就会打印刚才在 servlet() 方法中定义的输出内容。
我们并没有实例化这个 Servlet 类的对象,那么为什么 servlet() 方法被成功执行了呢?学习完后面的内容以后这个问题就不难理解了。
要想知道我们并没有手动创建 ServletDemo 这个类的实例化对象,为什么它的方法 servlet() 被成功执行这个问题,我们就要了解其执行流程。
在上面的例子中,我们已经写好一个 Servlet 的项目,并且将其部署到了 Web 服务器 TomCat 中,此时我们可以根据对应的 url 在浏览器中访问该服务器资源。
浏览器发出 http://localhost:8080/servlet-project/demo请求,这个请求大致分为3部分,分别是:
此时 Web 服务器软件 TomCat 将会创建一个 ServletDemo 的对象,这个对象称为 Servlet 对象,并且该对象的 service() 方法也会被服务器自动调用。
当 service() 方法被调用执行后就会向客户端浏览器发送响应数据。上面的例子中我们没有向浏览器发送具体的数据,要想实现这个功能,我们就要继续学习 ServletRequest 类和 ServletResponse 类。
其中 ServletRequest 中封装了请求数据,而 ServletResponse 中则是封装的响应数据,通过这两个类的对象就可以实现前后端的数据交互了。
在 Servlet 执行流程中,我们说到,Servlet 对象是由 Web 服务器创建的,那么具体创建时机是什么时候呢?要想了解这个问题,就要学习 Servlet 的生命周期。
对象的生命周期是指一个对象从创建到销毁经历的整个过程。Servlet 运行在 Servlet 容器(Web 服务器)中,其生命周期由容器来管理,大致分为四个阶段:
加载和实例化:默认情况下,当 Servlet 第一次被访问时,由容器创建 Servlet 对象,有时创建 Servlet 是比较耗时的,那么第一次访问就比较耗费时间,用户体验比较差。Servlet 提供了解决这个问题的方法,通过具体的配置可以实现在服务器启动的时间来创建 Servlet 对象,提高了访问速度。
只需要使用下面这个简单的配置:
@Webservlet(urlPatterns = "/demo",loadOnStartup=1)
其中 loadOnStartup 参数如果是负整数,则 Servlet 对象在第一次访问时创建,如果参数的值为 0 或者正整数的话,则会在服务器启动时创建,并且数字越小优先级越高。
初始化:在 Servlet 实例化以后,容器就会调用 init() 方法初始化这个对象,完成一些如加载配置文件,创建连接等初始化工作,该方法只会被调用一次。
请求处理:每次请求 Servlet 时,Servlet 容器都会调用 Servlet 的 service() 方法来对请求进行处理,该方法会被多次调用。
服务终止:当需要释放内存或者是容器关闭时,容器都会调用 Servlet 的 destroy() 方法完成资源的释放,在 destroy() 方法调用之后,容器会释放实例化对象,随后被 Java 垃圾回收机制处理,该方法只会被调用一次。注意,此时的服务器关闭指的是正常关闭非强制关闭。
Servlet 是一个接口,其中一共有 5 个方法,当我们的类实现了这个接口以后,必须将这 5 个方法全部实现。这 5 个方法分别是:
其中,前三个方法我们在之前的生命周期中已经接触过了,在 Servlet 被创建时,会执行 init() 方法进行初始化操作,此方法只会执行一次,每次 Servlet 被访问时都会执行 service() 方法,而 Servlet 被销毁时,则会执行 destroy() 方法,释放对象。
我们看到在进行初始化操作时,会往 init() 方法中传入 ServletConfig 类对象,如下:
@Override public void init(ServletConfig servletConfig) throws ServletException { //... }
所以在 getServletConfig() 方法中,我们只需要将容器创建的 ServletConfig 对象返回即可,而在 getServletInfo() 方法中先返回一个空字符串处理,如下:
private ServletConfig servletConfig; public void init(ServletConfig config) throws ServletException { this.servletConfig = config; System.out.println("init..."); } public ServletConfig getServletConfig() { return servletConfig; } public String getServletInfo() { return ""; }
其中最常用的是前面三个方法,这里我们都是只做了解,后面等对整个 Servlet 体系有了认知以后,再深入学习。
小tips:这也是我们学习编程一个很重要的方法论,先广度后深度,前期不用深入的了解其底层的含义,否则容易导致我们钻牛角尖,得不偿失。
在我们编写的实现 Servlet 接口的类中,我们更加关注的是 service() 方法,有没有一种方式让我们创建 Servlet 更加简便高效呢?学习完 Servlet 的体系结构之后,这个问题就变得简单了。
我们开发 B / S 架构的 Web 项目时,其实都要对 HTTP 协议进行封装,所以我们自定义的 Servlet 要继承 HttpServlet ,Servlet 的体系结构如下:
我们想要实现的是,在客户端发送 GET 请求给 Servlet 后,执行一种逻辑,当客户端发送 POST 请求给 Servlet 后执行另外一种逻辑…
在 HttpServlet 类中就实现了这样的功能,HttpServlet 类会判断页面发送的请求方式,根据不同的请求方式定义了不同的执行方法,例如 doGet,doPOST 等,
例如下面是部分HttpServlet类的源码:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); long lastModified; if (method.equals("GET")) { lastModified = this.getLastModified(req); if (lastModified == -1L) { this.doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader("If-Modified-Since"); if (ifModifiedSince < lastModified) { this.maybeSetLastModified(resp, lastModified); this.doGet(req, resp); } else { resp.setStatus(304); } } } else if (method.equals("HEAD")) { lastModified = this.getLastModified(req); this.maybeSetLastModified(resp, lastModified); this.doHead(req, resp); } else if (method.equals("POST")) { this.doPost(req, resp); } else if (method.equals("PUT")) { this.doPut(req, resp); } else if (method.equals("DELETE")) { this.doDelete(req, resp); } else if (method.equals("OPTIONS")) { this.doOptions(req, resp); } else if (method.equals("TRACE")) { this.doTrace(req, resp); } else { String errMsg = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[]{method}; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(501, errMsg); } }
我们自己定义的实现类只需要继承自 HttpServlet 类,并且重写 HttpServlet 类中的 doGet 这样的方法,以此来实现不同的处理逻辑。例如:
@WebServlet("/demo") public class ServletDemo extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //TODO GET 请求方式处理逻辑 System.out.println("get..."); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //TODO Post 请求方式处理逻辑 System.out.println("post..."); } }
Servlet 编写好以后,要想被浏览器访问,就需要配置其访问路径,例如前面案例中我们就通过 Java 注解的方式,给 Servlet 配置了访问路径,如图:
一个 Servlet 可以配置多个访问路径,urlPattern 的配置有以下几个规则:
精确匹配:
当我们配置如下的访问路径:
@WebServlet("/user/demo")
此时我们在浏览器中访问时的 url 为:
目录匹配:
当我们配置如下的访问路径:
@WebServlet("/user/*")
此时我们在浏览器中的访问 url 为:
扩展名匹配:
当我们配置如下的访问路径:
@WebServlet("*.do")
此时我们在浏览器中的访问 url 为:
任意匹配:
当我们配置如下的访问路径:
@WebServlet("/")
@WebServlet("/*")
此时我们在浏览器中的访问 url 为:
/* 和 / 的区别:当项目中的 Servlet 配置了 / ,会覆盖 tomcat 中的 DefaultServlet 默认访问路径。DefaultSerlet 是用来处理静态资源的,当其他的 urlPattern 都匹配不上时就会访问这个 Servlet,如果我们自己定义了 / 访问路径,则请求静态资源时不会生效而会匹配自己定义的路径,最终导致静态资源无法访问,所以这种方式不建议使用。
当我们的项目中配置了 /*,意味着匹配任意访问路径。
urlPattern 共有四种匹配方式,分别是精确匹配,目录匹配,扩展名匹配和任意匹配。其具有不同的优先级,具体为精确匹配 > 目录匹配 > 扩展名匹配 > 任意匹配,任意匹配中 /* 大于 / 。
前面对应的 Servlet 访问路径我们都是使用 Java 注解的方式配置,其实在 Servlet 3.0 之前是只支持 XML 配置文件的方式配置 Servlet 的访问路径,这里只对其方法做了解。
使用 XML 配置方法大致分为两步,编写 Servlet 类和配置,下面通过案例的方式讲解。
第一步:编写 Servlet 类。
public class ServletDemo extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //TODO GET 请求方式处理逻辑 System.out.println("get..."); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //TODO Post 请求方式处理逻辑 System.out.println("post..."); } }
第二步:在 web.xml 中配置该 Servlet
demo org.chengzi.web.ServletDemo demo /demo
不得不说,Servlet 确实是一门古老的技术,现在很少有公司直接使用 Servlet 来写项目了,大家都在用 SpringMVC-Spring-MyBatis / SpringBoot 做开发了,那么 Servlet 还值得学习吗?一个中肯的建议是不要跳过 Servlet 去学习框架,学习完 Servlet ,你将有更好的基础去面对后面的框架的知识,而绝非浪费时间和精力。
本文是 Servlet 的快速入门篇,主要从执行流程,生命周期,方法的介绍,体系结构和访问路径的配置等这几个方面探讨了 Servlet 。后面我们将通过源码更加深入的学习。
下期见。