Servlet概念
Servlet(Server Applet),全称Java Servlet。是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。
Servlet运行于支持Java的应用服务器中。从实现上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。
最早支持Servlet标准的是JavaSoft的Java Web Server。此后,一些其它的基于Java的Web服务器开始支持标准的Servlet。
Servlet生命周期
Servlet 初始化后调用 init () 方法。
Servlet 调用 service() 方法来处理客户端的请求。
Servlet 销毁前调用 destroy() 方法。
最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。
init()方法
服务器第一次被访问时,加载一个Servlet容器,即一个实例,而且只会被加载一次。
init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}Service()方法
service() 方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。
每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用service()方法,而实例只有一个。
service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,一般来说只需要重写doGet、doPost等方法就行了。
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException
{
}destroy()方法
destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。
在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。d
public void destroy() {
}架构图
一个简单的Servlet:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 设置响应类型:
resp.setContentType("text/html");
// 获取输出流:
PrintWriter pw = resp.getWriter();
// 写入响应:
pw.write("<h1>Hello, world!</h1>");
// 最后不要忘记flush强制输出:
pw.flush();
}
}在pom.xml文件中将打包类型设置为war,运行maven命令(可能要添加打包插件),将项目打包成.war文件,放入tomcat的webapps文件夹中,执行startup.bat文件启动tomcat服务器,tomcat会加载之前放入的.war文件,最后在浏览器中输入localhost:8080+对应的路径就可以访问了。
使用嵌入式Tomcat运行Servlet
上述方法不方便调试程序,我们可以自己写一个main方法来启动tomcat。
1.首先在pom.xml中添加相应的依赖
2.编写Servlet程序
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
String name = req.getParameter("name");
if (name == null) {
name = "world";
}
PrintWriter pw = resp.getWriter();
pw.write("<h1>Hello, " + name + "!</h1>");
pw.flush();
}
}3.编写main方法启动Tomcat服务器
public class Main {
public static void main(String[] args) throws Exception {
// 启动Tomcat:
Tomcat tomcat = new Tomcat();
tomcat.setPort(Integer.getInteger("port", 8080));
tomcat.getConnector();
// 创建webapp:
//通过预设的tomcat.addWebapp("", new File("src/main/webapp"),Tomcat会自动加载当前工程作为根webapp
Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes",
new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);
tomcat.start();
tomcat.getServer().await();
}
}在浏览器中输出localhost:8080/hello?name=qinfeng
浏览器显示:
映射路径
一个Web App就是由一个或多个Servlet组成的,每个Servlet通过注解说明自己能处理的路径。例如:
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
...
}浏览器发出的HTTP请求总是由Web Server先接收,然后,根据Servlet配置的映射,不同的路径转发到不同的Servlet:
/hello ┌───────────────┐
┌──────────>│ HelloServlet │
│ └───────────────┘
┌───────┐ ┌──────────┐ │ /signin ┌───────────────┐
│Browser│───>│Dispatcher│─┼──────────>│ SignInServlet │
└───────┘ └──────────┘ │ └───────────────┘
│ / ┌───────────────┐
└──────────>│ IndexServlet │
└───────────────┘
Web Server
这种根据路径转发的功能我们一般称为Dispatch。映射到/的IndexServlet比较特殊,它实际上会接收所有未匹配的路径,相当于/*。
重定向
重定向是指当浏览器请求一个URL时,服务器返回一个重定向指令,告诉浏览器地址已经变了,麻烦使用新的URL再重新发送新请求。
302临时重定向
@WebServlet(urlPatterns = "/hi") public class RedirectServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 构造重定向的路径: String name = req.getParameter("name"); String redirectToUrl = "/hello" + (name == null ? "" : "?name=" + name); // 发送重定向响应: resp.sendRedirect(redirectToUrl); } }浏览器发送get/hi请求,serlvet将处理这个请求,并发回302重定向响应,浏览器收到如下响应:
HTTP/1.1 302 Found Location: /hello当浏览器收到响应,会根据Location的指示再发送get/hello请求。
┌───────┐ GET /hi ┌───────────────┐ │Browser│ ────────────> │RedirectServlet│ │ │ <──────────── │ │ └───────┘ 302 └───────────────┘301永久重定向
如果服务器发送301永久重定向响应,浏览器会缓存/hi到/hello这个重定向的关联,下次请求/hi的时候,浏览器就直接发送/hello请求了。
resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 resp.setHeader("Location", "/hello");
转发
Forward是指内部转发。当一个Servlet处理请求的时候,它可以决定自己不继续处理,而是转发给另一个Servlet处理。
@WebServlet(urlPatterns = "/morning")
public class ForwardServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//自己不做处理,将请求和响应转发给路径为/hello的servlet
//注意:转发不会改动参数相当于再url中直接将/morning改为代码中的路径,即/hello
//如果是代码中是"/hello?name=pop",将显示pop
req.getRequestDispatcher("/hello").forward(req, resp);
}
}转发和重定向的区别在于:转发是在Web服务器内部完成的,对浏览器来说,它只发出了一个HTTP请求。
┌────────────────────────┐————————————————————————
| │ ┌───────────────┐ │
| │ ────>│ForwardServlet │ │
┌───────┐ GET /morning │ └───────────────┘ │
│Browser│ ──────────────> │ │ │
│ │ <────────────── │ ▼ │
└───────┘ 200 <html> │ ┌───────────────┐ │
│ <────│ HelloServlet │ │
│ └───────────────┘ │
│ Web Server │
└────────────────────────┘Session
用户第一次访问服务器会自动获得一个Session ID,这个ID用于跟踪用户状态,用户在后续访问中总是携带此ID,但是如果一段时间内不访问,Session就会失效。在Servlet中调用getSession方法时,会自动创建一个Session ID,并用一个名为JSESSIONID的Cookie发送给浏览器,保存在客户端。
@WebServlet(urlPatterns = "/signin")
public class SignInServlet extends HttpServlet {
private Map<String, String> users = Map.of("bob", "bob123", "alice", "alice123", "tom", "tomcat");
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
PrintWriter pw = resp.getWriter();
pw.write("<h1>Sign In</h1>");
pw.write("<form action=\"/signin\" method=\"post\">");
pw.write("<p>Username: <input name=\"username\"></p>");
pw.write("<p>Password: <input name=\"password\" type=\"password\"></p>");
pw.write("<p><button type=\"submit\">Sign In</button> <a href=\"/\">Cancel</a></p>");
pw.write("</form>");
pw.flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("username");
String password = req.getParameter("password");
String expectedPassword = users.get(name.toLowerCase());
if (expectedPassword != null && expectedPassword.equals(password)) {
req.getSession().setAttribute("user", name);
resp.sendRedirect("/");
} else {
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
}Cookie
Cookie,又称“小甜饼”。类型为“小型文本文件”,指某些网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密。
Cookie除了发送Session ID,还可以用来设置用户偏好,比如记录用户选择的语言。
创建:
@WebServlet(urlPatterns = "/pref") public class LanguageServlet extends HttpServlet { private static final Set<String> LANGUAGES = Set.of("en", "zh"); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String lang = req.getParameter("lang"); if (LANGUAGES.contains(lang)) { Cookie cookie = new Cookie("lang", lang); cookie.setPath("/"); cookie.setMaxAge(8640000); // 100 days resp.addCookie(cookie); } resp.sendRedirect("/"); } }创建一个新Cookie时,除了指定名称和值以外,通常需要设置setPath(“/“),浏览器根据此前缀决定是否发送Cookie。如果一个Cookie调用了setPath(“/user/“),那么浏览器只有在请求以/user/开头的路径时才会附加此Cookie。通过setMaxAge()设置Cookie的有效期,单位为秒,最后通过resp.addCookie()把它添加到响应。
如果访问的是https网页,还需要调用setSecure(true),否则浏览器不会发送该Cookie。反正,如果设置了Secure,就必须以https来访问。
读取:
private String parseLanguageFromCookie(HttpServletRequest req) { // 获取请求附带的所有Cookie: Cookie[] cookies = req.getCookies(); // 如果获取到Cookie: if (cookies != null) { // 循环每个Cookie: for (Cookie cookie : cookies) { // 如果Cookie名称为lang: if (cookie.getName().equals("lang")) { // 返回Cookie的值: return cookie.getValue(); } } } // 返回默认值: return "en"; }可见,读取Cookie主要依靠遍历HttpServletRequest附带的所有Cookie。
JSP
用PrintWriter输出HTML比较痛苦,因为不但要正确编写HTML,还需要插入各种变量。我们可以使用JSP。
JSP是Java Server Pages的缩写,它的文件必须放到/src/main/webapp下,文件名必须以.jsp结尾,整个文件与HTML并无太大区别,但需要插入变量,或者动态输出的地方,使用特殊指令<% … %>。
<html>
<head>
<title>Hello World - JSP</title>
</head>
<body>
<%-- JSP Comment --%>
<h1>Hello World!</h1>
<p>
<%
out.println("Your IP address is ");
%>
<span style="color:red">
<%= request.getRemoteAddr() %>
</span>
</p>
</body>
</html>特殊指令<%…%>:
- 包含在<%–和–%>之间的是JSP的注释,它们会被完全忽略;
- 包含在<%和%>之间的是Java代码,可以编写任意Java代码;
- 如果使用<%= xxx %>则可以快捷输出一个变量的值。
JSP页面内置了几个变量:
- out:表示HttpServletResponse的PrintWriter;
- session:表示当前HttpSession对象
- request:表示HttpServletRequest对象。
JSP与Servlet:
JSP在执行前首先被编译成一个Servlet。在Tomcat的临时目录下,可以找到一个hello_jsp.java的源文件。
package org.apache.jsp; import ... public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { ... public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { ... out.write("<html>\n"); out.write("<head>\n"); out.write(" <title>Hello World - JSP</title>\n"); out.write("</head>\n"); out.write("<body>\n"); ... } ... }可见JSP本质上就是一个Servlet,只不过无需配置映射路径,Web Server会根据路径查找对应的.jsp文件,如果找到了,就自动编译成Servlet再执行。在服务器运行过程中,如果修改了JSP的内容,那么服务器会自动重新编译。
JSP高级功能
通过page指令引入Java类:
<%@ page import="java.io.*" %> <%@ page import="java.util.*" %>使用include指令可以引入另一个JSP文件:
<html> <body> <%@ include file="header.jsp"%> <h1>Index Page</h1> <%@ include file="footer.jsp"%> </body>
一个简单的MVC模型
model:
public class User { public long id; public String name; public School school; public User(long id, String name, School school) { this.id = id; this.name = name; this.school = school; } }public class School { public String name; public String address; public School(String name, String address) { this.name = name; this.address = address; } }controller:
public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { School school = new School("No.1 Middle School", "101 North Street"); User user = new User(123, "Bob", school); req.setAttribute("user", user); req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp); } }view:
<% User user = (User) request.getAttribute("user"); %> <html> <head> <title>Hello World - JSP</title> </head> <body> <h1>Hello <%= user.name %>!</h1> <p>School Name: <span style="color:red"> <%= user.school.name %> </span> </p> <p>School Address: <span style="color:red"> <%= user.school.address %> </span> </p> </body> </html>
使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。Model可以是一个JavaBean,也可以是一个包含多个对象的Map,Controller只负责把Model传递给View,View只负责把Model给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model。
Filter
Filter是一种对HTTP请求进行预处理的组件,它可以构成一个处理链,使得公共处理代码能集中到一起。Filter适用于日志、登录检查、全局设置等。
例如,我们编写一个最简单的EncodingFilter,它强制把输入和输出的编码设置为UTF-8:
@WebFilter(urlPatterns = "/*")
public class EncodingFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("EncodingFilter:doFilter");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}编写Filter时,必须实现Filter接口,在doFilter()方法内部,要继续处理请求,必须调用chain.doFilter()(其实是递归调用)。最后,用@WebFilter注解标注该Filter需要过滤的URL。这里的/*表示所有路径。
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
|s / ┌──────────────┐
│ ┌─────────────>│ IndexServlet │ │
│ └──────────────┘
│ │/signin ┌──────────────┐ │
├─────────────>│SignInServlet │
│ │ └──────────────┘ │
│/signout ┌──────────────┐
┌───────┐ │ ┌──────────────┐ ├─────────────>│SignOutServlet│ │
│Browser│──────>│EncodingFilter├──┤ └──────────────┘
└───────┘ │ └──────────────┘ │/user/profile ┌──────────────┐ │
├─────────────>│ProfileServlet│
│ │ └──────────────┘ │
│/user/post ┌──────────────┐
│ ├─────────────>│ PostServlet │ │
│ └──────────────┘
│ │/user/reply ┌──────────────┐ │
└─────────────>│ ReplyServlet │
│ └──────────────┘ │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─注意:
- Servlet规范并没有对@WebFilter注解标注的Filter规定顺序。如果一定要给每个Filter指定顺序,就必须在web.xml文件中对这些Filter再配置一遍。
- 如果Filter要使请求继续被处理,就一定要调用chain.doFilter()!

