好了,通过四篇blog我们已经充分了解了JSP的基本语法、内置对象、JavaBean以及EL&JSTL了。从今天开始学习下Servlet技术,也就是我们MVC架构中的C,controller层。
Servlet基本概念
Servlet是什么?官方定义:Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。这个中间层就是对请求进行分发和控制。
Servlet解决什么问题
JSP是JavaWeb应用中处理和显示动态网页的技术,而JSP页面中出现了大量逻辑处理并且和HTML元素混在了一起,这使得JSP页面显得混乱、不易维护,职责不清晰。
- JSP是建立在静态Html基础之上,使得数据显示时具有动态特点,所以JSP的职责就是显示数据,而不是逻辑处理
- Servlet的出现很好的规避了这一点,将复杂的逻辑处理、请求响应交给Servlet处理,JSP则只负责渲染显示页面
我们之前反复提到JSP优化和发展历史的一切努力就是简化脚本代码,让JSP专注于页面显示,控制逻辑则交给Servlet去做,而事实上,JSP编译以后也就成为了Servlet。
JSP和Servlet区别和用途
分别了解下JSP和Servlet区别和用途,明确它们的使用范围。
JSP和Servlet区别
了解了JSP和Servlet的区别后才能更好的理解它们各自的用途:
- JSP经编译后就变成了Servlet,JSP的本质就是Servlet,JVM只能识别Java的类,不能识别JSP的代码,Web容器将JSP的代码编译成JVM能够识别的Java类
- JSP更擅长表现于页面显示,Servlet更擅长于逻辑控制
- Servlet能够很好地组织业务逻辑代码,但是在Java源文件中通过字符串拼接的方式生成动态HTML内容会导致代码维护困难、可读性差
- JSP虽然规避了Servlet在生成HTML内容方面的劣势,但是在HTML中混入大量、复杂的业务逻辑同样也是不可取的
- Servlet中没有内置对象,JSP中的内置对象有些是通过Servlet相关对象得到的,例如:HttpServletRequest对象,HttpServletResponse对象
JSP是Servlet的一种简化,使用JSP只需要完成程序员需要输出到客户端的内容,JSP中的Java脚本如何镶嵌到一个类中,由JSP容器完成。而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应
JSP和Servlet用途
依据它们的不同之处来进行分析,Servlet的短板在展示,如果在Servlet里描述html元素:
@Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html"); // Hello PrintWriter out = response.getWriter(); out.println("<html><body>"); out.println("<h1>" + message + "</h1>"); out.println("</body></html>"); }
那么会产生大量的html的输出代码,影响业务逻辑梳理。而JSP的短板在逻辑,如果在JSP里加大量逻辑:
<body> <% String print = "我是脚本语句内容"; List<String> strList = new ArrayList<>(); strList.add("脚本语句1"); strList.add("脚本语句2"); strList.add("脚本语句3"); // strList.add("脚本语句4"); //这是一个Java注释,用来注释Java代码,查看源码时不可见 %> <a href="hello-servlet">我的第一个JavaWeb项目</a> <!-- 这是一个html注释,可见<%=new Date().getTime() %> --> <br/> <%--这是一个JSP注释,查看源码时不可见--%> 声明语句示例:这是第 <%=count()%> 次访问该页面 <br/> 表达式语句示例: <%="我是一个表达式语句"%> <br/> 脚本语句示例: <%=print%>, 打印列表 <%=strList%> <jsp:forward page="jsp/result.jsp"></jsp:forward> 跳转语句之后,代码不会被执行:<%="我是一个跳转后的表达式语句"%> </body>
也会使页面看起来比较杂乱。所以双剑合璧,在加上我们之前提到的JavaBean就构成了简易版的MVC模式:
在这种模式【模型(Model)、视图(View)和控制器(Controller)】下
- Web浏览器发送HTTP请求到服务端,被Controller(Servlet)获取并进行处理(例如参数解析、请求转发)
- Controller(Servlet)调用核心业务逻辑——Model部分,获得结果
- Controller(Servlet)将逻辑处理结果交给View(JSP),动态输出HTML内容
MVC模式在Web开发中的好处是非常明显,它规避了JSP与Servlet各自的短板,Servlet只负责业务逻辑而不会通过out.append()动态生成HTML代码;JSP中也不会充斥着大量的业务代码,如果JSP使用标签去显示甚至不需要业务代码。
创建第一个Servlet
创建和请求第一个Servlet的流程如下,通过IDEA非常简单:
创建Servlet类
通过idea创建Servlet非常简单,只需要创建选项时选择Servlet即可:
重命名Servlet
创建好后重命名为我们包含逻辑的Servlet,例如重命名为FirstServlet,IDEA默认为我们生成了代码:
package com.example.myfirstweb.controller; /** * * @Name ${NAME} * * @Description * * @author tianmaolin * * @Data 2021/7/19 */ import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.*; import java.io.IOException; import java.io.PrintWriter; @WebServlet(name = "Servlet", value = "/Servlet") public class FirstServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } }
需要注意的是在servlet3.0以后,我们可以不用再web.xml里面配置servlet,只需要加上@WebServlet
注解就可以修改该servlet的属性了:
这个也算是注解的一个应用,解放配置化的东西,让参数可配置,关于注解可以参照我这篇Blog注解的基本概念和使用
修改注解并访问
注解的默认属性值如下:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package javax.servlet.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface WebServlet { String name() default ""; String[] value() default {}; String[] urlPatterns() default {}; int loadOnStartup() default -1; WebInitParam[] initParams() default {}; boolean asyncSupported() default false; String smallIcon() default ""; String largeIcon() default ""; String description() default ""; String displayName() default ""; }
调整注解的value值为我们想要的url地址:
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html><body>"); out.println("<h1>" + "这是第一个Servlet页面" + "</h1>"); out.println("</body></html>"); }
启动项目后输入该地址查看:
Servlet请求流程
当我们请求一个Servlet的时候发生了什么呢,完整的请求流程如下
- 用户在浏览器地址栏中输入请求URL地址:
http://localhost:8080/myfirstweb/myFirstServlet
- Tomcat 服务器根据请求获取 URL 中最后需要访问的资源,即
/myFirstServlet
- Tomcat 服务器根据获取的
/myFirstServlet
的url地址找到注解中url属性为该路径的Servlet - 找到之后服务器实例化Servlet
- 执行Servlet对应的请求处理方法,例如doGet,完成给用户响应
以上就是一个完整的查找并响应的Servlet流程。如果没有匹配到对应路径,则界面会出现404异常
Servlet生命周期
Servlet 生命周期是由 Servlet 的容器来控制的,Servlet 生命周期分为 4 个阶段:
- 第一阶段:加载并实例化,Servlet 容器负责加载和实例化,在服务器启动或第一次请求时执行,如果需要服务器启动时加载,则需在注解中加入loadOnStartup配置(默认为-1总是启动时加载),数值越小优先级越高,加载后该Servlet实例将常驻内存
- 第二阶段:初始化,在 Servlet 实例化之后,容器将调用
init()
方法,并传递实现 ServletConfig 接口的对象,在 Servlet 的整个生命周期内,init()
方法只被调用一次,一般根据需要是否添加init方法,如果Servlet需要初始化一些数据,可以在该方法中完成代码 - 第三阶段:请求处理,当容器收到对这一 Servlet 的请求,就调用 Servlet 的
service()
方法,并把请求和响应对象作为参数传递,service()
方法检查 HTTP 请求类型(GET、POST等),并在适当的时候调用doGet()、doPost()
等方法,每次请求都会调用对应的doGet或doPost方法 - 第四阶段:销毁,一旦 Web服务器停止服务,Servlet会自动执行其
destroy()
方法,以释放资源
我们可以看一下上述提到的一些方法的源码,从类继承关系的角度去看:
我们可以看到其实是从最顶层的Servlet接口来定义生命周期的,它定义了生命周期的几个默认方法:
同时我们可以看到,init方法在GenericServlet有两个重载方法,我认为tomcat使用了带有配合的init方法。
public void init(ServletConfig config) throws ServletException { this.config = config; this.init(); } public void init() throws ServletException { }
同时我们可以看到service方法是在HttpServlet类中具体实现的,就是来匹配请求方法的:
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; this.service(request, response); } else { throw new ServletException("non-HTTP request or response"); } } 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); } }
总结一下
JSP其实就是特殊的Servlet,创建一个Servlet也非常简单,需要注意的是要重新doGet或doPost方法,还有就是Servlet的继承结构和生命周期也需要详细理解下,总之Servlet是tomcat操作的对象,tomcat创建Servlet实例并接管它的全生命周期,而我们需要做的就是重写业务逻辑达成我们的目标