Java Web(一)web容器和Servlet
在聊 Servlet 之前,先讲讲什么是 Web服务器 和 应用服务器。
Web服务器
无论何种 Web 资源,想被远程计算机访问,都必须有一个与之对应的网络通信程序,当用户来访问时,这个网络通信程序读取 Web 资源数据,并把数据发送给来访者。
Web服务器就是一个网络通信程序,它用于完成底层网络通迅。具体来说,它将某个主机上的资源映射为一个URL供外界访问。
使用 Web服务器,Web 应用的开发者只需要关注 Web 资源怎么编写,而不需要关心资源如何发送到客户端手中,从而极大的减轻了开发者的开发工作量。
应用服务器 - Tomcat
我们的 Web 应用要运行起来,是需要部署在应用服务器上而不是Web服务器。因为web服务器只负责资源映射,而程序业务逻辑需要另外的容器来处理。
应用服务器一般装载着我们的后端应用程序,帮助我们接收请求、处理请求、响应请求。Tomcat 就是一种常见的应用服务器,但也具有web服务器的功能,所以直接访问也可以。
通常,Tomcat 装载着我们的 Servlet 对象。那什么是 Servlet 呢?下文会讲到。
在实际的生产环境中,由于负载均衡,cdn加速等原因,我们还是需要在应用服务器的前端再加一个web服务器来提高访问效率,常用的有 Nginx, Apache 这样的服务器。
Servlet
Servlet 是什么
简而言之,Servlet 就是一个接口,它规定了一个后端逻辑初始化的时候做什么、业务逻辑是什么、销毁的时候做什么。
1 | public interface Servlet { |
如果想开发一个Java程序向浏览器输出数据,需要完成以下2个步骤:
- 编写一个Java类,实现servlet接口(然而现实情况是,servlet开发者已经帮我们实现了一个httpServlet的抽象类,我们只需继承httpServlet并重写doGet和doPost方法)。
- 把开发好的Java类部署到web服务器(tomcat)中。
按照一种约定俗成的称呼习惯,通常我们也把实现了Servlet接口的java程序,称之为Servlet。
Servlet的运行过程
Servlet程序由Web服务器调用,web服务器收到客户端的Servlet访问请求后:
- Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第4步,否则,执行第2步
- 装载并创建该Servlet的一个实例对象
- 调用Servlet实例对象的init()方法
- 创建一个用于封装HTTP请求消息的
HttpServletRequest对象
和一个代表HTTP响应消息的HttpServletResponse对象
,然后调用Servlet的service()
方法并将请求和响应对象作为参数传递进去。 - Web应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的
destroy()
方法。
也就是说,web容器只有在首次访问时才创建Servlet,然后调用Servlet的init()
方法。之后web容器创建请求(request)对象和响应(response)对象(响应对象此时为空),并将这两个对象作为参数,传入Servlet的service()
方法。经service()
方法处理后,将结果写入响应信息(此时响应对象已有内容)。最后,由web容器取出响应信息回送给浏览器。
实际上,在执行doGet()
或者doPost()
之前,都会先执行service()
,由service()
方法进行判断,到底该调用doGet()
还是doPost()
Servlet与普通Java类的区别
Servlet是一个供其他Java程序(Servlet引擎)调用的Java类,它不能独立运行,它的运行完全由Servlet引擎来控制和调度。
针对客户端的多次Servlet请求,通常情况下,服务器只会创建一个Servlet实例对象,也就是说Servlet实例对象一旦创建,它就会驻留在内存中,为后续的其它请求服务,直至web容器退出,servlet实例对象才会销毁。
在Servlet的整个生命周期内,Servlet的 init方法只被调用一次。而对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。对于每次访问请求,Servlet引擎都会创建一个新的HttpServletRequest
请求对象和一个新的HttpServletResponse
响应对象,然后将这两个对象作为参数传递给它调用的Servlet的·方法,service方法再根据请求方式分别调用doXXX方法。
如果在<servlet>
元素中配置了一个<load-on-startup>
元素,那么 Web 应用程序在启动时,就会装载并创建Servlet的实例对象、以及调用Servlet实例对象的init()
方法。
实战
配置
新建IDEA工程
- 新建一个IDEA Maven工程
- 在 pom.xml 添加 servlet-api 依赖 (依赖到 mvnrepository 或 search.maven.org 找)
- 右键工程名字,Add Framework Support,选择 Web
Application - Edit Configurations,配置 Tomcat 服务器
- Deployment 选择 Artifacts
遇到问题可参考:
后端:GET
src/main/java 下,new 一个 servlet
helloServlet.java
1 | import javax.servlet.annotation.WebServlet; |
访问 localhost:8080/hello
,能看到 hello world!!
@WebServlet("/hello")
是一个注解,我们用这种方式来表示该 Servlet 的路径是 ./hello 。更原始的,我们可以在项目工程下找到 web.xml 文件,在这里面配置映射路径。(参考:WEB PROJECT)
后端:POST
src/main/java 下,new 一个 servlet
postServlet.java
1 |
|
- 使用
request.setCharacterEncoding("UTF-8");
和response.setContentType("text/html; charset=UTF-8");
解决中文问题 "字面量".equals(str)
是个好习惯- 用
PrintWriter
类来写html
前端:HTML
web目录下,new 一个 login.html
login.html
1 |
|
<meta charset="UTF-8">
解决中文问题action
属性表示要提交的服务器页面地址为 ./loginmethod
属性表示HTTP方法(get or post)
访问 localhost:8080/login.html
,能看到登录表单
输入admin,123456,可以看到网页跳转到 success
跳转
登录成功或是失败后,分别会跳转到不同的页面。 跳转分为服务端跳转
和客户端跳转
。
服务端跳转(forward)
forward 是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。 因此,用户看到的网址没有变化 。 在这个过程中,控制权并没有转交给另一服务器对象。
1 | request.getRequestDispatcher("success.html").forward(request, response); |
客户端跳转(redirect)
redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL.
1 | response.sendRedirect("fail.html"); |
- 用户看到的网址变为 127.0.0.1:8080/fail.html
Web.xml
在项目中有一个 Web.xml 文件,这个文件是一些配置参数。
1 |
|
然后我们可以在 Java 中取出这些参数
1 | this.driver = request.getSession().getServletContext().getInitParameter("database_driver"); |
Request常用方法
获取信息
方法 | 释义 |
---|---|
request.getRequestURL() | 浏览器发出请求时的完整URL,包括协议 主机名 端口(如果有)” |
request.getRequestURI() | 浏览器发出请求的资源名部分,去掉了协议和主机名” |
request.getQueryString() | 请求行中的参数部分,只能显示以get方式发出的参数,post方式的看不到 |
request.getRemoteAddr() | 浏览器所处于的客户机的IP地址 |
request.getRemoteHost() | 浏览器所处于的客户机的主机名 |
request.getRemotePort() | 浏览器所处于的客户机使用的网络端口 |
request.getLocalAddr() | 服务器的IP地址 |
request.getLocalName() | 服务器的主机名 |
request.getMethod() | 得到客户机请求方式,一般是GET或者POST |
request.getHeader() | 获取头信息 |
request.getHeaderNames() | 获取所有头信息 |
获取参数
方法 | 释义 |
---|---|
request.getParameter() | 是常见的方法,用于获取单值的参数 |
request.getParameterValues() | 用于获取具有多值得参数,比如注册的时候提交的爱好,可以使多选的。 |
request.getParameterMap() | 用于遍历所有的参数,并返回Map类型。 |
respoonse 常用方法
方法 | 释义 |
---|---|
response.setContentType(“text/html”); | 设置相应格式 |
response.setContentType(“text/html; charset=UTF-8”); | 设置编码格式,服务器端使用 UTF-8 编码,同时通知浏览器使用UTF-8 |
response.setCharacterEncoding(“UTF-8”); | 仅仅是服务器端设置UTF-8,浏览器编码由浏览器自己决定 |
response.sendRedirect(“fail.html”); | 302 客户端跳转 |
可以用以下方法设置不使用缓存
1 | response.setDateHeader("Expires",0 ); |
Servlet的线程安全问题
当多个客户端并发访问同一个Servlet时,web服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用Servlet的service方法,因此service方法内如果访问了同一个资源的话,就有可能引发线程安全问题。
线程安全问题只存在多个线程并发操作同一个资源的情况下,所以在编写Servlet的时候,如果并发访问某一个资源(变量,集合等),就会存在线程安全问题。
解决方案:让Servlet去实现一个SingleThreadModel
接口,如果某个Servlet实现了SingleThreadModel
标记接口,那么Servlet引擎将以单线程模式来调用其service方法。
对于实现了SingleThreadModel
接口的Servlet,Servlet引擎仍然支持对该Servlet的多线程并发访问,其采用的方式是产生多个Servlet实例对象,并发的每个线程分别调用一个独立的Servlet实例对象。
实现SingleThreadModel接口并不能真正解决Servlet的线程安全问题,因为Servlet引擎会创建多个Servlet实例对象,而真正意义上解决多线程安全问题是指一个Servlet实例对象被多个线程同时调用的问题。
事实上,在Servlet API 2.4中,已经将SingleThreadModel
标记为Deprecated(过时的)。
标记接口
在Java中,把没有定义任何方法和常量的接口称之为标记接口,经常看到的一个最典型的标记接口就是”Serializable”,这个接口也是没有定义任何方法和常量的,标记接口在Java中有什么用呢?主要作用就是给某个对象打上一个标志,告诉JVM,这个对象可以做什么,比如实现了”Serializable”接口的类的对象就可以被序列化,还有一个”Cloneable”接口,这个也是一个标记接口,在默认情况下,Java中的对象是不允许被克隆的,就像现实生活中的人一样,不允许克隆,但是只要实现了”Cloneable”接口,那么对象就可以被克隆了。