Web基础
BS架构
访问网站、使用App时,都是基于Web的
Browser/Server模式,简称BS架构
- 客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端
- 浏览器只需要请求服务器,获取Web页面并展示给用户即可
- BS架构下,服务器端升级后,客户端无需任何部署就可以使用到新的版本
Servlet接口
Servlet基础
- 编写Web应用程序就是编写Servlet处理HTTP请求;
- Servlet API提供
HttpServletRequest和HttpServletResponse两个高级接口封装HTTP请求和响应; - Web应用程序必须按固定结构组织并打包为
.war文件; - 需要启动Web服务器来加载webapps中的war包并运行Servlet
完整web开发流程
- 编写Servlet;
- 打包为war文件;
- 复制到Tomcat的webapps目录下;
- 启动Tomcat
使用web服务器提供的Servlet API编写Servlet来处理HTTP请求,从而将处理TCP连接、解析HTTP协议等底层工作都交给现成的Web服务器去实现,以实现高效、可靠的开发。因此,编写Web应用程序就是编写Servlet处理HTTP请求。
如下图,浏览器与web server通过HTTP协议建立连接,web server提供Servlet API,web应用程序利用这些接口来处理HTTP请求!
简单的Servlet
// WebServlet注解:这是一个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(); } }
- Servlet类总是继承自
HttpServlet,并覆写doGet()或doPost()方法doGet()方法传入HttpServletRequest和HttpServletResponse两个对象代表HTTP请求和响应- 使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为
HttpServletRequest和HttpServletResponse已封装好了请求和响应。以发送响应为例,只需要设置正确的响应类型,然后获取PrintWriter输出流,写入响应即可。
Servlet API获取
Servlet API是一个jar包,需要通过Maven引入才能正常编译。jar包的引入方式编写在
pom.xml文件中:<packaging>war</packaging> //打包类型:war包(Java Web Application Archive) <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.0</version> <scope>provided</scope> //provided表示编译时使用,不会打包到.war文件中 </dependency> //因为运行期Web服务器已提供Servlet API相关的jar包
执行war包
- 运行maven命令得到war文件,即打包后的web应用程序
- 将war包放入
web server的webapps目录下- 启动web服务器并加载自编Servlet类
- Servlet类处理浏览器请求并响应
支持Servlet API的常用Web服务器有:
Servlet容器
实际上,类似Tomcat这样的服务器也是Java编写的,启动Tomcat服务器实际上是启动Java虚拟机,执行Tomcat的
main()方法,然后由Tomcat负责加载我们的.war文件,并创建一个HelloServlet实例,最后以多线程的模式来处理HTTP请求。如果Tomcat服务器收到的请求路径是/(假定部署文件为ROOT.war),就转发到HelloServlet并传入HttpServletRequest和HttpServletResponse两个对象。编写的Servlet并不直接运行,而是由Web服务器加载后创建实例运行,所以Web服务器也称Servlet容器。
Servlet容器中运行的Servlet具有如下特点:
- 无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例;
- Servlet容器只会给每个Servlet类创建唯一实例;
- Servlet容器会使用多线程执行
doGet()或doPost()方法。复习Java多线程的内容,可以得出结论:
- 在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全;
HttpServletRequest和HttpServletResponse实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;- 在
doGet()或doPost()方法中,如果使用了ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用。因此,正确编写Servlet,要清晰理解Java的多线程模型,需要同步访问的必须同步。
Servlet开发
Servlet开发时通过main()方法启动Tomcat服务器并加载webapp有如下好处:
- 启动简单,无需下载Tomcat或安装任何IDE插件;
- 调试方便,可在IDE中使用断点调试;
- 使用Maven创建war包后,也可以正常部署到独立的Tomcat服务器中
引入嵌入式Tomcat服务器
开发Servlet时,可使用
main()方法启动嵌入式Tomcat服务器并加载当前工程的webapp,便于开发调试,且不影响打包部署,能极大地提升开发效率。
pom.xml中仍定义<packaging>类型仍然为war,需要引入依赖tomcat-embed-core和tomcat-embed-jasper,并定义引入的Tomcat版本<tomcat.version>为9.0.26。不必引入Servlet API,因为引入Tomcat依赖后自动引入了Servlet API。
编写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: 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(); } }直接运行
main()方法,无需用mvn打包成war包,即可启动嵌入式Tomcat服务器,然后,通过预设的tomcat.addWebapp("", new File("src/main/webapp"),Tomcat会自动加载当前工程作为根webapp,可直接在浏览器访问http://localhost:8080/
Servlet进阶
1. 请求响应处理
- 一个Webapp可以有多个Servlet,分别映射不同的路径,从而处理不同的请求;
- 映射为
/的Servlet可处理所有“未匹配”的请求; - 如何处理请求取决于Servlet覆写的对应方法;
- Web服务器通过多线程处理HTTP请求,一个Servlet的处理方法可以由多线程并发执行。
路径转发Dispatcher
一个Webapp完全可以有多个Servlet,分别映射不同的路径:
@WebServlet(urlPatterns = "/hello") //路径hello转发到HelloServlet进行处理 public class HelloServlet extends HttpServlet { ... } @WebServlet(urlPatterns = "/signin") public class SignInServlet extends HttpServlet { ... } @WebServlet(urlPatterns = "/") public class IndexServlet extends HttpServlet { ... }浏览器发出的HTTP请求总是由Web Server先接收,然后根据Servlet配置的映射,不同的路径转发到不同的Servlet
Dispatcher的映射逻辑描述
String path = ... if (path.equals("/hello")) { dispatchTo(helloServlet); } else if (path.equals("/signin")) { dispatchTo(signinServlet); } else { // 所有未匹配的路径均转发到"/" dispatchTo(indexServlet); }所以在浏览器输入
http://localhost:8080/abc也会看到IndexServlet生成的页面
HttpServletRequest
HttpServletRequest封装了一个HTTP请求。它从ServletRequest继承而来。从其提供的接口方法可以拿到HTTP请求的几乎全部信息,常用的方法有:
HttpServletRequest还有两个方法:setAttribute()和getAttribute(),可以给当前HttpServletRequest对象附加多个Key-Value,相当于把HttpServletRequest当作一个Map<String, Object>使用。
HttpServletResponse
HttpServletResponse封装了一个HTTP响应。HTTP响应必须先发送Header,再发送Body,所以操作HttpServletResponse对象时,必须先调用设置Header的方法,最后调用发送Body的方法。写入响应前,无需设置
setContentLength(),因为底层服务器会根据写入的字节数自动设置写入响应时,需要通过
getOutputStream()获取写入流,或者通过getWriter()获取字符流,二者只能获取其中一个。写入完毕后,对输出流调用
flush()而不是close()方法
- 必须调用
flush(),因为大部分Web服务器都基于HTTP/1.1协议,会复用TCP连接。如果没有调用flush(),将导致缓冲区的内容无法及时发送到客户端。- 不能调用
close(),因为大部分Web服务器都基于HTTP/1.1协议,如果关闭写入流,TCP将被关闭,导致无法复用
HttpServletRequest和HttpServletResponse两个高级接口使得编程时无需直接处理HTTP协议,只需关心接口方法,具体实现类由各服务器提供,从而降低编程难度、增加效率。
Servlet多线程模型
一个Servlet类在服务器中只有一个实例,但对于每个HTTP请求,Web服务器会使用多线程执行请求。因此,一个Servlet的
doGet()、doPost()等处理请求的方法是多线程并发执行的。如果Servlet中定义了字段,要注意多线程并发访问的问题:public class HelloServlet extends HttpServlet { private Map<String, String> map = new ConcurrentHashMap<>(); protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 注意读写map字段是多线程并发的: this.map.put(key, value); } }对于每个请求,Web服务器会创建唯一的
HttpServletRequest和HttpServletResponse实例,因此,HttpServletRequest和HttpServletResponse实例只有在当前处理线程中有效,它们总是局部变量,不存在多线程共享的问题。