Servlet入门
使用教程为廖雪峰老师的Java教程
最简单的Servlet实现:
将Servlet API导入Maven
编辑pox.xml
,添加打包类型,设置为war
,表示为Java Web Application Archive
1
| <packaging>war</packaging>
|
引入项目依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> </plugin> </plugins> </build>
|
注意<scope>
指定为provided
,表示编译时使用,不会打包进war
文件中,因为运行期Web服务器本身已经提供了Servlet API相关的jar包。
基本代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import jakarta.servlet.*; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.*;
import java.io.*;
@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>"); pw.flush(); } }
|
项目工程结构为:
1 2 3 4 5 6 7 8
| JavaEELearn ├── pom.xml └── src └── main ├── java │ └── HelloServlet.java ├── resources ##存放网站的资源文件 └── webapp
|
下载Tomcat并安装
- 使用Servlet<=4.0时,使用Tomcat 9.x
- 使用Servlet>=5.0时,使用Tomcat 10.x
项目构建运行
使用mvn clean package
生成war文件,将war文件重命名为Root.war
,放入%Tomcat%/webapps
中,运行%Tomcat%/bin/startup.bat
或者%Tomcat%/bin/startup.sh
启动Tomcat服务器,默认监听端口为8080,访问http://localhost:8080/
即可。
Servlet进阶
一个Web App就是由一个或多个Servlet组成的,每个Servlet通过注解设置自己处理的路径。
浏览器发送请求时,有多种不同类型的请求,比如GET,POST,PUT。为了处理这些不同的请求,我们需要覆写这些请求的方法,例如doGet()
doPost()
。
HttpServletRequest
HttpServletRequest
封装了一个Http请求,实际上时从ServletRequest
继承的,可以通过HttpServletRequest
提供的接口方法获得Http请求的几乎全部信息,常用的方法有:
- getMethod() 获取请求方法,例如
GET
POST
- getRequestURI() 获取请求路径,但不包括请求的参数,例如
"/index"
- getQuerryString() 获取请求参数,例如
"name=123&age=11&sex=1"
- getParameter(name) 获取指定的参数,GET从URL读取,POST从Body中读取
- getContentType() 获取请求Body的类型,例如
"application/x-www-form-urlencoded"
- getContextPath() 获取当前Webapp挂载的路径
- getCookies() 获取请求携带的所有Cookie
- getHeader(name) 获取指定的Header
- getHeaderNames() 获取所有的Header
- getInputStream() 打开一个输入流读取Body
- getReader() 打开一个Reader读取Body
- getRemoteAddr() 获取客户端的IP
- getScheme() 获取协议类型,例如
http
https
HttpServletResponse
HttpServletResponse
封装了一个Http响应,由于Http响应必须先发送Header,再发送Body,所以操作HttpServletResponse
对象时,必须先调用设置Header的方法,最后调用发送Body的方法
常用设置Header的方法有:
- setStatus(sc) 设置响应代码,默认为200
- setContentType(type) 设置Body的类型,例如
"text/html"
- setCharacterEncoding(charset) 设置字符编码,例如
"UTF-8"
- setHeader(name,value) 设置一个Header的值
- addCookie(cookie) 给响应添加一个Cookie
- addHeader(name,value) 添加一个Header,因为Http协议允许多个相同的Header
写入响应时,需要通过getOutputStream()
获取写入流,或者通过getWriter()
获取字符流,二者只能获取其中的一个。
写入响应前,无需setContentLength()
,底层服务器会自动设置好。
写入完毕后,必须调用flush()
刷新缓冲区,将缓冲区的内容发送到客户端,并且,禁止调用close()
,因为这样会关闭TCP连接,使Web服务器无法复用此连接
Servlet多线程模型
一个Servlet类在服务器中只有一个实例,但对于每个Http请求,服务器会使用多线程执行请求,因此,一个Servlet的处理请求的方法是多线程并发执行的,要注意多线程并发访问的问题。
对于每个请求,Web服务器会创建唯一的HttpServletRequest
和HttpServletResponse
实例,他们总是局部变量,不存在多线程共享的问题。
重定向与转发
Redirect
重定向是指当浏览器请求一个URL时,服务器返回一个重定向指令,告诉浏览器地址已经变了,麻烦使用新的URL再重新发送新请求。
比如:
1 2 3 4 5 6 7 8
| @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
请求,会收到服务器的302回应,要求访问新路径。产生两次Http请求,浏览器地址栏自动更新为重定向后的地址。
302响应为临时重定向,301响应为永久重定向
永久重定向写法为:
1 2
| resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); resp.setHeader("Location", "/hello");
|
Forward
Forward是指内部转发,当一个Servlet处理请求时,它可以决定自己不继续处理,而是转发给另一个Servlet处理,例如:
1 2 3 4 5 6
| @WebServlet(urlPatterns = "/morning") public class ForwardServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher("/hello").forward(req, resp); } }
|
ForwardServlet
在收到请求后,它并不自己发送请求,而是把请求和响应都转发给路径为/hello
的Servlet,后续的请求实际上是由HelloServlet
完成的。
转发与重定向的区别在于,转发是在Web服务器内部完成的,对于浏览器来说,他只是发出了一个Http请求,浏览器并不知道该请求在Web服务器内实际做了一次转发。
Session和Cookie
Http协议是一个无状态协议,Web服务器无法区分收到的两个请求是否是同一个浏览器发出的,为了跟踪用户状态,服务器可以向浏览器分配一个唯一ID,以Cookie的形式发送到浏览器,浏览器在后续访问中总是附带此Cookie,这样,服务器就可以识别出来用户的身份。
Session
我们把这种基于唯一ID识别用户身份的机制称为Session,每个用户第一次访问服务器后,会自动后将获得一个Session ID。若用户一段时间未访问服务器,Session会自动失效,即使下次访问带着上次分配的Session ID访问,服务器也会认作新用户,分配一个新的Session ID。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @WebServlet(urlPatterns = "/signin") public class SignInServlet extends HttpServlet { private Map<String, String> users = Map.of("bob", "bob123", "alice", "alice123", "tom", "tomcat");
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(); }
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); } } }
|
在其他页面获取用户名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @WebServlet(urlPatterns = "/") public class IndexServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String user = (String) req.getSession().getAttribute("user"); resp.setContentType("text/html"); resp.setCharacterEncoding("UTF-8"); resp.setHeader("X-Powered-By", "JavaEE Servlet"); PrintWriter pw = resp.getWriter(); pw.write("<h1>Welcome, " + (user != null ? user : "Guest") + "</h1>"); if (user == null) { pw.write("<p><a href=\"/signin\">Sign In</a></p>"); } else { pw.write("<p><a href=\"/signout\">Sign Out</a></p>"); } pw.flush(); } }
|
移除Session:
1 2 3 4 5 6 7 8
| @WebServlet(urlPatterns = "/signout") public class SignOutServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getSession().removeAttribute("user"); resp.sendRedirect("/"); } }
|
对于Web服务器来说,我们通过HttpSession
接口访问当前Session,可以理解为Web服务器在内存中自动维护了一个ID到HttpSession的映射表。
服务器识别Session的关键就是依靠一个名为JSESSIONID的Cookie。在第一次调用req.getSession()
时,服务器自动创建一个SessionID,然后通过名为JSESSIONID的Cookie发送给浏览器。
使用Session时,由于服务器把所有用户的Session都存储在内存中,如果遇到内存不足的情况,就需要把部分不活动的Session序列化到磁盘上,这会大大降低服务器的运行效率,因此,放入Session的对象要小,通常我们放入一个简单的User
对象就足够了。
在使用多台服务器构成集群时,使用Session会遇到一些额外的问题。Session适用于中小型Web应用程序,对于大型应用程序来说,通常需要避免使用Session机制.
Cookie
Servlet提供的HttpSession
本质上是通过一个名为JSESSIONID
的Cookie来跟踪用户对话,除这个名称外,其他名称的Cookie我们可以任意使用。
比如可以编写一个LanguageServlet
设置Cookie记录用户选择的语言。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @WebServlet(urlPatterns = "/pref") public class LanguageServlet extends HttpServlet {
private static final Set<String> LANGUAGES = Set.of("en", "zh");
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); resp.addCookie(cookie); } resp.sendRedirect("/"); } }
|
创建Cookie时,除了指定名称和值以外,通常还要设置setPath("<Path>")
,浏览器根据此前缀决定是否发送Cookie,如果网页是Https,还需要调用setSecure(true),否则浏览器不会发送该Cookie。
Cookie的读取主要靠遍历HttpServletRequest
实现,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private String parseLanguageFromCookie(HttpServletRequest req) { Cookie[] cookies = req.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals("lang")) { return cookie.getValue(); } } } return "en"; }
|
JSP
JSP文件的内容其实是一个HTML,必须放在/src/main/webapp
下,文件名以.jsp
结尾,在需要动态输出的地方,使用特殊指令<% ... %>
。
1 2 3
| <%-- 注释 --%> <% java代码 %> <%-- 可以编写任意java代码 --%> <%= xxx %> <%-- 可以快捷输出一个变量的值 --%>
|
JSP页面内置了几个变量:
- out:表示
HttpServletResponse
的PrintWriter
- session:表示当前
HttpSession
对象
- request:表示
HttpServletRequest
对象
JSP本质上就是一个Servlet,只是无需配置映射路径。在服务器运行过程中,如果修改了JSP的内容,服务器会自动重新编译。
MVC开发
MVC开发就是将Servlet和JSP相结合,Servlet负责处理数据,JSP负责展示页面。
比如:
1 2 3 4 5 6 7 8 9 10
| public class User { public long id; public String name; public School school; }
public class School { public String name; public String address; }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @WebServlet(urlPatterns = "/user") public class UserServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { School school = new School("No.1 Middle School", "101 South Street"); User user = new User(123, "Bob", school); req.setAttribute("user", user); req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <%@ page import="com.itranswarp.learnjava.bean.*"%> <% 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>
|
需要注意的是:
- 要把
user.jsp
放到/WEB-INF/
目录下,阻止用户通过/user.jsp
绕过Servlet直接访问JSP页面。
- JSP直接从
request
获取User
实例,然后直接输出,此处未考虑html的转义,有潜在安全风险。
简介
我们把UserServlet
看作业务逻辑处理,把User
看作模型,把user.jsp
看作渲染,这种设计模式通常被称为MVC:Model-View-Controller,即UserServlet
作为控制器(Controller),User
作为模型(Model),user.jsp
作为视图(View)
使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。Model可以是一个JavaBean,也可以是一个包含多个对象的Map,Controller只负责把Model传递给View,View只负责把Model给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model。
MVC的设计
MVC框架有一个接收所有请求的Servlet
,通常我们把它命名为DispatcherServlet
,映射到/
,然后根据不同Controller方法定义的@Get
或@Post
的Path决定调用哪个方法,最后获得方法返回的ModeAndView
渲染模板,写入HttpServletResponse
。
其中,DispatcherServlet
以及如何渲染均由MVC框架实现,在MVC框架之上只需要编写每一个Controller。
Filter
Filter可以在Http请求到达Servlet前,进行预处理,比如:
1 2 3 4 5 6 7 8 9 10
| @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。这里的/*
表示所有路径。
如果编写了多个Filter,Filter会组成一个链,依次进行处理(无序,若需有序,在web.xml
中配置)
Filter可以有针对性地拦截或放行Http请求。如果Filter没有调用chain.doFilter()
,后续的Filter和Servlet都没有机会处理该请求,如果一个Filter在请求中生效,但什么都没做,用户将看到一个空白页,因为请求没有继续处理,默认响应200和空白输出。
Filter可以进行预处理,因此,可以把很多公共预处理逻辑放到Filter中完成,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @WebServlet(urlPatterns = "/upload/file") public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { InputStream input = req.getInputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; for (;;) { int len = input.read(buffer); if (len == -1) { break; } output.write(buffer, 0, len); } String uploadedText = output.toString(StandardCharsets.UTF_8); PrintWriter pw = resp.getWriter(); pw.write("<h1>Uploaded:</h1>"); pw.write("<pre><code>"); pw.write(uploadedText); pw.write("</code></pre>"); pw.flush(); } }
|
此为上传文件的Servlet,为了保证文件的完整性,可以在服务端做一个验证,让客户端把文件的哈希也传过来,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| @WebFilter("/upload/*") public class ValidateUploadFilter implements Filter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String digest = req.getHeader("Signature-Method"); String signature = req.getHeader("Signature"); if (digest == null || digest.isEmpty() || signature == null || signature.isEmpty()) { sendErrorPage(resp, "Missing signature."); return; } MessageDigest md = getMessageDigest(digest); InputStream input = new DigestInputStream(request.getInputStream(), md); byte[] buffer = new byte[1024]; for (;;) { int len = input.read(buffer); if (len == -1) { break; } } String actual = toHexString(md.digest()); if (!signature.equals(actual)) { sendErrorPage(resp, "Invalid signature."); return; } chain.doFilter(request, response); }
private String toHexString(byte[] digest) { StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); }
private MessageDigest getMessageDigest(String name) throws ServletException { try { return MessageDigest.getInstance(name); } catch (NoSuchAlgorithmException e) { throw new ServletException(e); } }
private void sendErrorPage(HttpServletResponse resp, String errorMessage) throws IOException { resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); PrintWriter pw = resp.getWriter(); pw.write("<html><body><h1>"); pw.write(errorMessage); pw.write("</h1></body></html>"); pw.flush(); } }
|
以上这个Filter队签名进行验证是没问题的,但UploadServlet将读取不到任何信息,因为HttpServletRequest
读取时,只能读取一次,后续无法读取任何信息,此时需要伪造一个HttpServletRequest
,获得一个新的流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| class ReReadableHttpServletRequest extends HttpServletRequestWrapper { private byte[] body; private boolean open = false;
public ReReadableHttpServletRequest(HttpServletRequest request, byte[] body) { super(request); this.body = body; }
public ServletInputStream getInputStream() throws IOException { if (open) { throw new IllegalStateException("Cannot re-open input stream!"); } open = true; return new ServletInputStream() { private int offset = 0;
public boolean isFinished() { return offset >= body.length; }
public boolean isReady() { return true; }
public void setReadListener(ReadListener listener) { }
public int read() throws IOException { if (offset >= body.length) { return -1; } int n = body[offset] & 0xff; offset++; return n; } }; }
public BufferedReader getReader() throws IOException { if (open) { throw new IllegalStateException("Cannot re-open reader!"); } open = true; return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), "UTF-8")); } }
|
并将Filter的doFilter()
更改为chain.doFilter(new ReReadableHttpServletRequest(req, output.toByteArray()), response);
虽然整个Filter的代码比较复杂,但这个Filter在整个处理链中实现了灵活的可插拔性,是否启用对Web应用程序的其他组件完全没有影响。
Listener
任何标注为@WebListener
且内部实现了特定接口的类都会被Web服务器自动初始化,ServletContextListener
接口会在整个Web应用程序初始化完毕后以及Web应用程序关闭后得到回调通知,可以把初始化数据库等工作放到contextInitialized()
中,把清理资源的工作放到contextDestory()
中。
ServletContextListener写法:
1 2 3 4 5 6 7 8 9 10 11
| @WebListener public class AppListener implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { System.out.println("WebApp initialized."); } public void contextDestroyed(ServletContextEvent sce) { System.out.println("WebApp destroyed."); } }
|
除了ServletContextListener
外还有几种Listener:
HttpSessionListener
:监听HttpSession的创建和销毁
ServletRequestListener
:监听ServletRequest的创建和销毁
ServletRequestAttributeListener
:监听ServletRequest请求的属性变化(ServletQuest.setAttribute()
)
ServletContextAttributeListener
:监听ServletContext的属性变化(ServletContext.setAttribute()
)