Servlet
Java Servlet是运行在HTTP服务器中的java程序,它可以处理浏览器客户端对该服务器发起的请求。
1. Tomcat
直接在idea中创建javaee项目即可根据提示直接集成tomcat。Tomcat的特点是运行时占用的系统资源少,扩展性好,开源,一般用于开发或者调试,或者在中小型系统和并发访问用户不多的场景下使用。
抽象类的作用是降低实现类的难度,如果没有抽象类做中间层,那么每次实现一个接口都要重写里面的所有方法。这一点在Servlet源码中有体现。
2. 初步使用Servlet
在写项目之前,先进行一些配置。比如每次更新文件内容后都要手动重启tomcat服务器才能生效,非常麻烦,可以进行如下设置:
设置完成后,每次以debug的方式运行项目,就不用每次手动重启服务器了。
【初步使用servlet】
创建javaee项目,选择web application。
创建类(注意servlet类一般放在com.companyName.controller包下),实现HttpServlet接口,重写service方法
将servlet接口实现类信息注册到tomcat服务器
在WEB-INF目录下,找到web.xml文件,向其中添加:
1
2
3
4
5<!--将servlet接口实现类路径提交给tomcat-->
<servlet>
<servlet-name> var </servlet-name> <!--用变量var存储servlet实现类路径-->
<servlet-class> com.google.controller.MyServlet </servlet-class> <!--声明servlet实现类路径-->
</servlet>这样当tomcat创建MyServlet实例时,就会执行:
String var = "com.google.controller.MyServlet"
1
2
3
4
5<!--这时可以通过:网站根目录/com/google/controller/MyServlet 访问到MyServlet这个资源,但是这样太麻烦了,因此给这个路径起个别名-->
<servlet-mapping>
<servlet-name> var </servlet-name>
<url-pattern> /alias </url-pattern> <!--这时就可以通过:网站根目录/alias 来访问MyServlet这个资源了。注意别名开头必须有“/”-->
</servlet-mapping>【上述步骤也直接可以通过注解直接完成,不用去xml文件中配置,如下图】
现在可以通过http://localhost:8080/javaeeTest_war_exploded/t02这个网址访问到该类中的资源了。注意t02左侧的目录为本项目的根目录,可以更改
3. Servlet生命周期
图片来自网络,侵删。
开发人员实现的Servlet接口实现类,它的实例由http服务器负责创建。默认情况下,http服务器第一次接收到对某一servlet对象的请求时,创建该对象实例。
也可以手动配置当http服务器启动时直接就创建某servlet对象的实例:
1
2
3
4
5<servlet>
<servlet-name> var </servlet-name>
<servlet-class> com.google.contoller.MyServlet </servlet-class>
<load-on-startup> 1 <load-on-startup> <!--其中填写一个大于0的整数即让http服务器启动时就创建MyServlet对象的实例,有多个servlet实现类时,该数值大的优先启动-->
</servlet>http服务器运行期间,对任一个servlet实现类只会实例化它的一个对象
http服务器关闭时,将所有servlet实例销毁
其实init之后可以调用doGet或者doPost方法,分别处理get和post的数据,但service集成了这两个方法,更加方便。
4. HttpServlet的两个接口
当http服务器接收到请求报文时,会自动创建与之关联的HttpServletResponse对象和HttpServletRequest对象,并将它们以参数的形式传递给servlet实现类的service方法。service方法执行完毕后,在http服务器生成回复报文之前,这两个对象将被销毁。
因此,HttpServletResponse对象和HttpServletRequest对象的声明周期就是【一次请求】。
4.1 HttpServletResponse
这个接口负责将service方法的执行结果以二进制的形式写入到【HTTP应答报文】中。它可以设置应答报文中的contect-type属性值,来标识本报文携带的数据类型。
4.1.1 输出流
1 | PrintWriter pw = response.getWriter(); //字符输出流 |
4.1.2 重定向
response.sendRedirect("https://www.baidu.com");
这句话相当于设置http回应报文中location字段的值,浏览器接收到http回应报文后就会立即跳转到location指定的url。(注意location字段是不显示的,而且浏览器接收到location字段不为空的响应报文(状态码302)后不会读取该报文中的内容,而是直接跳转到location指定的url).
重定向后默认用get方式发起请求
4.2 HttpServletRequest
该接口负责读取HTTP请求报文的数据,读取其中的请求,将该请求转发给服务器。
URI是URL的子串,它截取了URL中com之后的路径
1 | /*获取请求报文头中所有请求参数*/ |
获取请求头和请求体中的参数信息都是使用该方法,区别在于当用户以get方式提交数据时,这些数据都存在请求头中,请求头的数据由tomcat负责解析,默认编码格式为utf-8;而当用户以post方式提交数据时,其中数据存在请求体中,请求体中的数据由request对象负责解析,其默认编码格式不支持中文,这时就会出现乱码,显式地设置编码格式即可解决:
request.setCharacterEncoding("UTF-8");
5. 前后端交互
- 在后端通过
request.setAttribute("key","value");
将key和value存储到request作用域中 - 在前端通过
<%=request.getAttribute("key")%>
从request作用域中获取key的值
6. 多个servlet相互关联
【请求转发】我去鞋店买衣服,鞋店没有衣服,老板去隔壁衣服店取了一件卖给我。
【重定向】我去鞋店买衣服,鞋店没有衣服,老板告诉我隔壁有家衣服店,我去隔壁衣服店买。
请求转发的根本是【转发】,就是将传输到这个servlet中的response和request对象转发给另外一个servlet,因此在整个转发的过程中,所有servlet获取到的response和request对象信息都是相同的。而重定向本质上是服务器让浏览器去请求另外一个servlet对象,浏览器重新发起请求那就是全新的response和request对象了。
另外请求转发只能定位到本项目中的资源,因为请求转发是服务器内部的转发,不能定位到外部资源比如www.baidu.com,但是重定向可以。
6.1 请求转发
如下后台逻辑
1 | protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { |
当前端传来的username=”root”且password=”123”时,验证成功,在页面上显示success;而如果验证不成功,将验证失败的消息传输到前端,并且利用请求转发将页面定位到login.jsp(否则验证失败就会一直停留在空白页面),可以观察到,验证失败跳转到login.jsp后,url并没有发生改变,这就是请求转发的特性。
6.2 重定向
参照4.1.2
7. 多个servlet之间共享数据的方案
- ServletContext接口
- Cookie类
- HttpSession接口
- HttpServletRequest接口
8. 解决HTTP无状态特性的问题
由于HTTP是无状态协议,这个问题如果不解决,那么网页就无法记录用户的行为,也无法存储用户的信息来对用户加以区分(比如会员和非会员)。Cookie和Session就是用来解决这个问题的。
8.1 Cookie
当同一个浏览器/用户向某网站的多个servlet发起请求时,Cookie使得这些servlet之间可以共享该用户的信息数据。
【原理】
用户第一次访问某网站,假如它访问的是servlet01,那么servlet01在运行期间会创建一个cookie来存储当前用户的相关数据(例如用户刚刚注册的用户名密码,或者用户在网站中的行为数据),当该servlet运行完毕后,会将cookie信息打包在HTTP响应报文中发送给用户浏览器
浏览器收到HTTP响应报文以及其中的cookie,将该cookie保存(或更新)到自己本地。
过了两天,用户通过【同一个浏览器】又来访问【该】网站,这次它访问的是servlet02。(注意只要浏览器本地有被访网站的cookie,那么之后该浏览器向该网站发起的所有HTTP请求报文中都会自动携带该cookie)。网站服务器接收到请求后,读取其携带的cookie信息,直接帮用户登陆网站,【提升了用户体验】,【同时也降低了服务器的压力】,但是因为cookie保存在用户本地,因此安全性较差。
Cookie的形式是键值对,键值对之间用“=”连接,多个键值对通过“ ; ”隔开,Cookie存储的数据类型只能是String,而HttpSession可以是任意类型Object。
cookie是网站和浏览器绑定的
8.1.1 创建Cookie并将其添加到浏览器
1 | protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { |
8.1.2 获取Cookie
只能一次获取所有Cookie,以数组的形式返回。
1 | protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { |
注意,如果(比如在/login这个servlet中)加入了新的cookie,那么必须要重新访问一次/login才能将新的cookie存到浏览器中,因为必须重新访问才能调用到/login下的service函数。重新访问了/login,更新了本地浏览器的cookie后,如果把浏览器关了再开,就会发现cookie已经被清空,这是因为cookie的默认失效时间就是当浏览器关闭时失效。
8.1.3 设置Cookie的失效时间
通过改变Cookie的maxAge属性可以设置该Cookie的失效时间,它的默认值是-1,代表关闭浏览器时失效;正整数代表该cookie可以存活多少秒。
0代表删除该Cookie,不过直接设置为0并不能删除cookie,能够删除的前提是该cookie的path和domain属性都设置了。
8.1.4 Cookie的path
设置cookie的path就是设置可以访问cookie的路径。
- 默认在当前项目中可以访问
setPath("/path")
指定可以访问该cookie的路径setPath("/")
允许当前服务器中所有资源访问cookiesetPath("/path/resource")
指定可以访问cookie的某一资源
总的来看,只有访问的项目路径中包含某一个cookie的path才能访问到该cookie。
8.2 Session
与cookie相同,当同一个浏览器/用户向某网站的多个servlet发起请求时,Session也可以使得这些servlet之间可以共享该用户的信息数据。
不同之处在于,一个Cookie对象只能存储一个键值对(且值只能是String),而一个session对象可以存储集合,这意味着session对象可以存储任意数量的键值对。
且对于服务器来说,每一个客户端连接到它之后,它们之间都会形成一个session,每一个session会维持一段时间,在session有效期间服务器和客户端可以相互“有状态地”通信(比如客户端在一个页面上通过session登陆了,然后通过超链接跳转到另外一个属于该服务器 的页面,那么session信息可以传递到这个页面,所以该用户在这个页面上也是登陆状态)。session的实现依赖cookie,且客户端的session信息保存在服务器端,这个特点最明显的体现就是:客户端发出的HTTP请求携带的cookie到达服务端,服务端的service方法调用完毕后,会返回一个新的cookie给客户端(无论cookie中的信息有没有发生改变),客户端收到后新的cookie后会用它覆盖掉本地旧的cookie,也就是说,cookie的存活时间是一次请求。而session是将用户的数据保存在服务端,只要服务端不删除用户的session数据,它就会一直存活(一般会限时,tomcat默认为30分钟),如果存活时间没到,甚至服务器关闭了它也还存活(服务器关闭时将session数据写到本地硬盘上,下次开启时自动加载,可以手动设置)。
因为session信息存储在服务器上,所以比cookie安全
【session可以理解成用户在网站服务器中的私人储物柜,用户申请私人储物柜,服务器给它钥匙,用户下次带着钥匙来直接开自己的柜子无需申请,柜子里存放着用户的私人信息】
8.2.1 会话标识JSESSIONID
http服务器创建一个HttpSession对象时,会用JsessionId来唯一的标识它,并在当前service方法执行完毕后,将其作为cookie(key=”JSESSIONID”:value=”597d…”)通过HTTP应答报文返回给客户端。下一次该客户再连接该服务器时,服务器获取其请求报文中的cookie中的JsessionId,然后通过这个JsessionId去存储session的容器中寻找之前为该用户创建的session,找到后,用这个session中的信息接着上次的进度继续为用户提供服务,这样就用session标识了一个用户。
也就是说,http服务器利用cookie将HttpSession与用户关联起来。
8.2.2 session操作实例
1 | /*添加session*/ |
真实项目中,为了防止用户恶意登陆(比如用户知道网站的资源路径,不登陆直接访问),一般用令牌机制,这个令牌(用户身份标识)一般就用session来充当。request.getSession()
这种无参版本,一般只会出现在loginServlet中,即服务器未知session的用户只被允许访问该页面。其他所有servlet中全部使用request.getSession(false),这样所有服务器未知session的用户的请求就会全部被拒绝。
但是这种方案也有一些缺点:
- 太多servlet都要做一个令牌验证,太麻烦
- 无法保护静态资源文件
这两个缺点都可以被Filter解决,对loginServlet之外的其他所有servlets加过滤器,过滤器中做令牌验证,以下为示例:
1 | public class MyFilter implements Filter { |
8.2.3 session对象的销毁时机
用户浏览器中的JSESSIONID cookie缓存被清除时,它与http服务器中与之相关的session连接也就中断了。但是http服务器不可能去监测每个用户是否中断了连接(用户太多),不过它可以计算用户空闲时间(多长时间没有向服务器发起请求),因此可采用设置session的存活时间的办法,用户空闲一定时间后session自动销毁,以此腾出服务器空间资源。
tomcat默认用户空闲30分钟销毁session,我们可以手动更改某个session的空闲存活时间
设置多少秒后失效: hs.setMaxInactiveInterval();
立即失效: hs.invalidate();
也可以直接修改http服务器的默认参数
1 | <session-config> |
9. SevletContext
每一个java web应用都有且仅有一个SevletContext对象(一般也叫做全局作用域对象),也叫做application对象。当web服务器(容器)启动时,它会为其中的每一个web应用程序创建一个SevletContext对象,只要服务器不关闭,这个对象就会一直存在。ServletContext对象相当于一个map,比如一个servlet A把数据放到ServletContext对象中(以key-value的形式),然后另外一个Servlet B就可以获取ServletContext对象,然后通过其中存储的key找到对应的value。
SevletContext对象有两个作用:1. 属于同一个网站的所有servlet可以通过ServletContext对象实现数据共享 2. 该对象中保存了一些当前应用的信息,比如获取当前服务器信息或者获取某个资源在服务器中的路径。
SevletContext对象使用示例(注意,规范是将ServletContext对象命名为application):
通过request对象获取(获取ServletContext对象的方法很多,这是其一)
ServletContext application = request.getServletContext();
1 | /*往全局作用域中存数据*/ |
因为全局容量是有限的,因此基层的程序员是没有权限往里面存数据的,只能取。
10. 通过HttpServletRequest接口实现数据共享
1 |
|
11. 监听器接口
监听器接口不像其他servlet规范下的接口有一个承接的抽象类,程序员需要自己实现监听器接口。
监听器接口的作用就是监控【作用域对象生命周期发生变化的时刻】以及【作用域对象共享数据变化的时刻】。
作用域对象:该对象中存储的数据被服务端的多个servlet共享。Servlet规范下的几个作用于对象:
- ServletContext:全局作用域对象
- HttpSession:会话作用域对象
- HttpServletRequest:请求作用域对象
注意cookie虽然也可以用作servlet之间的数据共享,但是因为它本身存储在客户端,因此不算做作用域对象。
11.1 监听器接口开发规范
有三步。(现在一般认为某个步骤的开发规范需要三步或者三步以上是严重影响效率的因素,一般都会被框架抽象出来,就像之前学的JDBC到后面会直接被框架代替)
- 根据监听的实际情况,选择对应的监听器接口进行实现(选择三个作用域对象中的一个)
- 重写监听器接口中的监听事件处理方法
- 在web.xml文件中将监听器接口实现类注册到http服务器
11.2 使用示例
ServletContextListener接口可以监听全局作用域对象。
1 | //在服务器开启和关闭时会分别在控制台打印initiallized和destroyed |
然后将该监听器对象注册到http服务器中
ServletContextAttributeListener接口监听全局作用域对象中共享数据变化的时刻
1 | public class HelloServlet implements ServletContextAttributeListener { |
同样要将其注册到http服务器中
可以通过如下代码让全局作用域中的数据发生变化来观察实验现象
1 |
|
11. 3 使用监听器接口提高程序运行速度
Dao层中,最消耗时间的是建立JDBC connection的过程,如果我们每一次访问数据库都要建立一次connection就太慢了。
ServletContextListener接口刚好可以监听服务器开启和关闭,如果我们将connection在服务器开始时创建,关闭时销毁,就可以节省大量的时间。
【实例】
1 | public class MyServlet implements ServletContextListener { |
然后,把原来的getConnection方法重载一下即可,使得该方法每次可以从全局作用域中获取Connection而非新建一个,如public Connection getConnection(HttpServletRequest request)
。 然后再把closeConnection方法重载一下,让它不是直接关闭连接,而是在全局作用域的Connection map中找到想要关闭的Connection,将其在map中的value置为false,代表该连接现在处于空闲状态。
12. 过滤器接口
Filter接口对发给HTTP服务器的请求进行拦截,然后可以进行如下两种操作:
- 检查该请求的合法性,如果通过才将其进一步执行。
- 对当前请求进行增强辅助操作(比如我要去国外读书,走的时候碰到一个朋友,他给了我200块钱让我路上买点好吃的,他对我去国外这个请求做了增强辅助)
12.1 过滤器接口开发规范
三步。
- 创建类实现Filter接口
- 重写doFilter方法
- 将过滤器注册到服务器
12.2 Filter对客户端向HTTP服务器发起的请求进行过滤
实现过滤器的servlet按照规范一般放在com.google.filter包下
1 | public class MyServlet implements Filter { |
然后将该过滤器注册到HTTP服务器,在web.xml下
1 |
|
12.3 Filter对客户端向HTTP服务器发起的请求进行辅助增强
比如用户以post方式提交数据时,我们在服务端必须request.setCharacterEncoding("UTF-8")
将请求体中的编码格式转为UTF-8,否则读中文会乱码,以前我们必须在每一个servlet中加上这句话,现在可以用Filter来一劳永逸。
1 |
|
然后将其注册到HTTP服务器,注意要【将其生效范围应用到所有servlet】
1 |
|