Servlet
文章目录
一、C/S和B/S架构
1.1 C/S
是指客户端/服务器。
优点:显示效果比较好,不用下载界面内容等。
缺点:每次更新需要下载客户端才能使用。
应用场景:一般应用于大型软件、游戏等。
1.2 B/S
是指浏览器/服务器。
优点:打开浏览器即可访问,更新只是在服务器上进行。
缺点:显示效果没有C/S好。
应用场景:基本上企业应用都会使用此架构。
二、服务器
注意:web服务器的作用是将程序员编写的web项目加载并运行。由于编写的web项目不能独立运行,而需要放入到web服务器加载运行,所以web项目中没有main方法。
三、Tomcat服务器
3.1 目录结构
许可证:表示当前软件是否可以使用,是否可以商用的许可。
常见的有Apache License。
bin:可执行文件。
conf:配置文件。server.xml是服务器的配置,用得最多。
lib:tomcat运行需要的jar包。
logs:日志文件。
webapps:可运行的web项目。(自己写的项目也是放到此处)ROOT是默认项目,如果把自己的项目放入ROOT中,那么自己项目也可以成为默认项目。实际在企业开发中,会删除掉ROOT里面的内容,或者将自己的项目放入其中。
work:主要是将JSP转换成servlet代码放入其中。
3.2启动和停止
tomcat的启动有两种方式:
- catalina.bat run 是通过控制台(命令行)直接启动,会显示日志和错误信息,关掉该窗口(也可以使用ctrl+C)即为停止。
- startup.bat 是通过后台运行的方式启动,需要通过shutdown.bat停止。(如果停止不了,需要关闭进程)
访问方式:本机访问可以通过打开浏览器输入:http://localhost:8080或者http://127.0.0.1:8080,如果其他人要访问,可以使用服务器ip:8080访问。
修改端口号可以在conf/server.xml中修改。默认为8080
#####3.3 HTTP协议
HTTP超文本传输协议。
基于请求和响应模式,无状态
三次握手和四次挥手
三次握手(three-way handshaking)
1.背景:TCP位于传输层,作用是提供可靠的字节流服务,为了准确无误地将数据送达目的地,TCP协议采纳三次握手策略。
2.原理:
1)发送端首先发送一个带有SYN(synchronize)标志地数据包给接收方。
2)接收方接收后,回传一个带有SYN/ACK标志的数据包传递确认信息,表示我收到了。
3)最后,发送方再回传一个带有ACK标志的数据包,代表我知道了,表示’握手‘结束。
四次挥手(Four-Way-Wavehand)
**1.意义:**当被动方收到主动方的FIN报文通知时,它仅仅表示主动方没有数据再发送给被动方了。但未必被动方所有的数据都完整的发送给了主动方,所以被动方不会马上关闭SOCKET,它可能还需要发送一些数据给主动方后,再发送FIN报文给主动方,告诉主动方同意关闭连接,所以这里的ACK报文和FIN报文多数情况下都是分开发送的。
2.原理:
1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手
长连接和短连接:
短连接是每次请求和响应都会建立连接和断开连接。
长连接是指多次请求和响应基于一次连接。在HTTP协议中,1.1版本使用的长连接,在规定时间内,可以完成多次请求和响应,超时断开。
四、Servlet的使用
4.1 Servlet接口、GenericServlet、HTTPServlet的关系
Servlet接口中包含5个方法,关键方法是init、destory、service。如果实现此接口需要实现5个方法,比较麻烦。
GenericServlet实现了Servlet接口,并对init、destory等方法提供了简单实现。如果继承此类只需要实现service方法即可。
HTTPServlet专注于HTTP协议。继承了GenericServlet,并重写了service方法。将其请求和响应强转成HTTP协议类型。并且对service方法进行了分发处理。然后定义了doGet、doPost等一系列的分发处理方法。这些方法内容都是直接报错,强制要求子类继承时必须要重写这些方法。(一般重写doGet和doPost方法即可)
4.2 Servlet创建和配置
继承HttpServlet并实现方法。
public class FirstServlet extends HttpServlet{
public FirstServlet(){
System.out.println("create FirstServlet");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("Hello, doGet");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("Hello, doPost");
}
}
Servlet 2.5版本以及之前的配置,主要配置在web.xml中。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 给Servlet类配置名字-->
<servlet>
<servlet-name>FirstServlet</servlet-name>
<servlet-class>com.qf.day10.servlet.FirstServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<!-- 给Servlet名字绑定路径-->
<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/fs</url-pattern>
</servlet-mapping>
</web-app>
Servlet3.0之后简化了配置,可以使用注解配置。
@WebServlet(value = "/ts", loadOnStartup = 2) // Servlet3.0以上的版本才支持
public class ThirdServlet extends HttpServlet{
public ThirdServlet(){
System.out.println("create ThirdServlet");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("ThirdServlet, doGet");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("ThirdServlet, doPost");
}
}
Servlet默认是在第一次访问的时候创建并初始化对象。
但是可以通过配置loadOnStartup来改变此设定,以达到启动服务器就创建的目的。
默认是-1,如果配置为0或者正数,按照数字从小到大顺序来创建对象。
五、request和response的使用
5.1 request的使用
request是封装用户请求的对象。包含请求头和请求正文(具体内容参考请求报文)
常用的方法有两个:
getParameter(String name);通过表单提交的name属性名称获取对应的值。
setCharacterEncoding(String charset);设置POST请求的编码格式,因为默认是使用ISO-8859-1接收数据。
注意:GET方法在tomcat8.0后默认使用UTF-8编码,如果是8.0之前还是需要转码,如果不是使用 tomcat服务器,根据具体情况确定是否需要转码。
@WebServlet("/login.do")
public class LoginServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("正在接收数据:");
String username = request.getParameter("username"); //接收表单name为username的数据
// GET方式的转码
// Tomcat8.0以后默认设置本来就是UTF-8,不需要转码,所以一般针对8.0之前的版本
// username = new String(username.getBytes("ISO-8859-1"), "UTF-8");
System.out.println(username);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("正在接收数据:");
request.setCharacterEncoding("utf-8"); // 转码
String username = request.getParameter("username"); //接收表单name为username的数据
System.out.println(username);
}
}
index.html
注意:此处action属性中的路径应该写login.do而不能写/login.do,前面有/表示绝对路径,需要加上工程名称,正确的写法应该是/day11/login.do,但是并不能保证工程名称一定叫day11,所以应该使用动态方式(后面讲解)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<form method="get" action="login.do">
<input type="text" name="username" placeholder="用户名"/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
5.2 response的使用
response用来封装服务器对客户端的响应。通常会输出html页面内容。
常见的方法有两个:
getWriter():得到向页面输出的输出对象。
setContentType(String type):设置响应的内容类型和文本字符集。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("正在接收数据:");
request.setCharacterEncoding("utf-8"); // 转码
String username = request.getParameter("username"); //接收表单name为username的数据
// 设置响应的类型和字符集
response.setContentType("text/html;charset=utf-8");
// 得到向页面响应输出的对象
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<meta charset=\"UTF-8\"/>");
out.println("</head>");
out.println("<body>");
out.println("<h1>当前用户为:"+username+"</h1>");
out.println("</body>");
out.println("</html>");
}
六、重定向和请求转发【重点】
6.1 重定向
在servlet中需要跳转到其他的页面或者servlet中,可以使用重定向方式。
该方式特点如下:
- 地址栏地址会变化。
- 至少有两次请求。
- 多次请求直接request对象不会共享,所以不会共享request对象中的数据。
- 可以跳转到站外。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("正在接收数据:");
request.setCharacterEncoding("utf-8"); // 转码
String username = request.getParameter("username"); //接收表单name为username的数据
String password = request.getParameter("password");
// 登录的逻辑
if (username.equals("zhangsan")&&password.equals("123456")){
// 登录成功
// 重定向到成功servlet(相当于页面上的<a>标签被点击)
response.sendRedirect("success.html");
}else{
// 登录失败
response.sendRedirect("fail.html");
}
}
6.2 请求转发
在servlet中需要跳转到其他的页面或者servlet中,可以使用请求转发方式。
该方式特点如下:
- 属于服务器的行为。
- 只是一次请求。
- 地址栏地址不会发生改变。
- 请求对象会进行多次传递,所以可以使用request对象共享数据。
- 不能转发给站外。
- 可以手动存储数据以传递下一个servlet或页面。
注意:手动存储数据使用setAttribute(String key, Object obj);获取数据使用getAttribute(String key);返回Object类型,需要强制转换类型。
getAttribute(String key)和getParameter(String name)区别:
attribute是程序手动存储数据。所以有setAttribute方法和getAttribute方法。
parameter是容器封装请求对象时存储的数据,所以只有getParameter()方法。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("正在接收数据:");
request.setCharacterEncoding("utf-8"); // 转码
String username = request.getParameter("username"); //接收表单name为username的数据
String password = request.getParameter("password");
// 登录的逻辑
if (username.equals("zhangsan")&&password.equals("123456")){
// 登录成功
// 请求转发(将当前servlet的request和response对象传递下一个页面或者servlet)
// 获得当前用户的余额(通常是查询数据库得到)
String money = "3000元宝";
// 将信息存入到request对象中,以键值对的形式
request.setAttribute("money", money);
request.getRequestDispatcher("success.html").forward(request, response);
}else{
// 登录失败
response.sendRedirect("fail.html");
}
}
七、综合案例
见day11_demo
八、Servlet生命周期
Servlet生命周期是指Servlet对象从创建到销毁的过程以及过程中所经历的内容。
创建(实例化new)-> 初始化(init) -> 服务(service)-> 销毁(destroy)
注意:初始化只会调用一次。服务是指Servlet接口中的方法。destroy方法是指在将要销毁之前调用的方法,一般用来释放资源等。
九、Servlet线程安全问题
Servlet是单实例,多线程。
要解决线程安全问题,一般有以下方案:
实现SingleThreadModel接口,使Servlet在访问时变为单线程,那么必然安全,但是性能太低,
不推荐
。使用Synchronized代码块。
使用技巧:尽量能不能使用全局变量就不要使用(推荐使用局部变量),或者仅使用只读的全局变量。从源头断绝线程安全问题出现。
十、状态管理
HTTP协议是无状态的,即服务器不会记录客户端的信息。每一次发送请求和响应都不会记录相关的客户端状态,因为这样可以提升访问速度。
如果需要进行状态管理,可以采用cookie或者session来处理。
cookie是指在客户端记录与服务器的交互信息,下一次再访问服务器,服务器会读取cookie中信息。cookie是一个txt文本,不安全。
session是指服务器会生成一个唯一的标识(sessionID),并将此表示发送到客户端,客户端会将其记录,下一次访问时服务器会访问此记录来确认是否访问过。相对安全。
10.1 编码和解码
cookie中不能保存中文,可以通过java.net.URLEncoder进行编码。然后使用java.net.URLDecoder中的decoder方法进行解码。
例如:张三,编码后会变成%E5%BC%A0%E4%B8%89,解码后又变成张三
public static void main(String[] args) throws Exception{
String name = "张三";
System.out.println("原来名称:" + name);
String encodeString = URLEncoder.encode(name, "utf-8");
System.out.println("编码后:" + encodeString);
String decodeString = URLDecoder.decode(encodeString, "utf-8");
System.out.println("解码后:" + decodeString);
}
10.2cookie的使用
首先,进入到IndexServlet来判断用户是否曾经登录并写入了cookie信息,如果没有则跳转到登录界面,如果写入了则跳转到首页。
@WebServlet("/index.html")
public class IndexServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 以发会员卡案例为示例
// 获取用户请求中的cookies对象(让用户出示会员卡)
Cookie[] cookies = request.getCookies();
// 没有找到响应的cookies对象(用户告知没有会员卡)
if (cookies == null){
// 去登录,登录后保存cookie信息(去办理会员卡)
response.sendRedirect("login.html");
}else{ // 找到一些cookies信息(用户出示了一堆会员卡《五花八门》)
String value = null;
// 循环查找有没有正确的cookie信息(将用户出示的一堆会员卡进行翻找,看是否有我们下发的会员卡)
for (Cookie cookie : cookies) {
// 如果找到了对应的cookie信息(找到了我们下发的会员卡)
if (cookie.getName().equals("day12_username")){
value = cookie.getValue(); // 记录该cookie信息(记录该会员卡信息)
break; // 跳出循环(不再翻找剩下的会员卡)
}
}
// 如果最终没有记录正确的cookie信息,说明没有登录过(如果没有找到我们下发的会员卡)
if (value == null){
// 去登录,登录后保存cookie信息(去办理会员卡)
response.sendRedirect("login.html");
}else{
// 如果找到了cookie信息,就跳转到首页并显示对应的信息(如果找到了会员卡,就允许直接进)
request.setAttribute("currentUser", value); // 保存信息
request.getRequestDispatcher("home.html").forward(request, response);
}
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
登录时,如果登录成功需要将用户信息写入到cookie。
@WebServlet("/login.do")
public class LoginServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 接收用户登录信息
String name = request.getParameter("name");// 用户名
String password = request.getParameter("password");// 密码
if (name.equals("zhangsan") && password.equals("123456")){
// 登录成功
// 将登录成功的用户信息保存到cookie中
Cookie cookie = new Cookie("day12_username", name); // 创建对象(注意不要中文)
cookie.setPath("/day12"); //设置路径
cookie.setMaxAge(60*60*12); // 设置生命周期,单位是秒,此处设置12小时
response.addCookie(cookie); // 将cookie写入到响应对象中
request.setAttribute("currentUser", name); // 保存信息
request.getRequestDispatcher("home.html").forward(request, response);
}else{
response.sendRedirect("fail.html");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
cookie的优点:可以设置到期规则,相对简单。
cookie的缺点:不安全;数据大小有限制,一般默认大小不能超过4kb;容易被安全工具禁用了浏览器的cookie。
#####10.3 session的使用
session一般指一次会话。(一个浏览器与服务器之间的多次请求和响应的过程叫一次会话)
session的原理:由于cookie解决HTTP无状态问题时,会有三个缺点。通过session能够解决这三个问题。
使用session,会将数据保存在服务器,大小能够超出4kb的cookie的范围;将sessionID发送到客户端保存在cookie中,安全问题大大减低;即使浏览器禁用了cookie,也能够通过url重写(将sessionid以参数?的形式传递到服务器)的办法来解决。
session作为一个作用域,也具备setAttribute,getAttribute,removeAttribute三个常规操作方法。有效范围是一个会话范围。
会话的注销(销毁)的方法是session.invalidate();
LoginController
@WebServlet("/login.do")
public class LoginController extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 接收用户参数
String username = request.getParameter("username");
String password = request.getParameter("password");
if (username.equals("zhangsan") && password.equals("123456")){
// 登录成功
HttpSession session = request.getSession(); // 获取当前session对象(类似于map)
System.out.println("JSESSIONID=====" + session.getId());// 输出sessionID
session.setAttribute("user", username); // 将登录成功的用户信息存入到session中
response.sendRedirect("home.html");
}else {
// 登录失败
response.sendRedirect("fail.html");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
LogoutController
@WebServlet("/logout.do")
public class LogoutController extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.getSession().invalidate(); // 注销当前用户
response.sendRedirect("index.html"); // 跳转到登录界面
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
HomeJSP.java
@WebServlet("/home.html")
public class HomeJSP extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 判断用户是否登录,如果没有登录,则跳转到登录页面
HttpSession session = request.getSession(); // 得到session
String user = (String)session.getAttribute("user"); // 得到user信息
// 如果没有得到信息,则未登录
if (user == null){
// 跳转登录页面
response.sendRedirect("index.html");
}else{
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<h1>欢迎你,"+user+"</h1>");
out.println("<a href=\"first.html\">First</a>");
out.println("<a href=\"second.html\">Second</a>");
out.println("<a href=\"logout.do\">注销</a>");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
10.4 ServletContext
ServletContext指Servlet上下文,通常是指一个服务器(容器)对应的上下文。
作为一个作用域,范围是全局范围,即所有的用户皆可访问。
与前面学习的作用域比较范围大小如下:ServletContext > HttpSession > HttpServletRequest > PageContext
常见的方法:
getContextPath()得到上下文的名称,即工程的名称。
getRealPath("/")得到项目实际的完整路径。
十一、过滤器
过滤器是指在客户端与请求资源之间建立的一个拦截组件。它能够拦截用户的请求,并对请求进行一定的过滤分析,如果满足需求则放行,否则会被拦截并要求转向。
11.1 创建和配置
需要实现一个Filter接口。并将过滤逻辑写在doFilter方法中。
注意:doFiter方法中最后一定应该有过滤器链处理的代码。
@WebFilter("/*") // 配置过滤的路径
public class MyFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("过滤器开始过滤...");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 简单逻辑
System.out.println("uri===" + request.getRequestURI());
System.out.println("url===" + request.getRequestURL());
String url = request.getRequestURI(); // 得到请求的路径
// 判断请求路径是否合法
if (url.endsWith("/first.do") ||
url.endsWith("/first.html") ||
url.endsWith("/index.html") ||
url.endsWith("/") )
{
// 什么都不做(放行)
}else{
// 跳转到index页面
response.sendRedirect("index.html");
System.out.println("hello, world");
return;
}
// 放过请求
filterChain.doFilter(servletRequest, servletResponse); // 让过滤器链继续执行下一步过滤
}
@Override
public void destroy() {
}
}
Filter的配置一般有两种方式:
1.使用web.xml中配置
2.使用@WebFilter()注解配置
web.xml配置
<filter>
<filter-name>myFilter</filter-name>
<filter-class>com.qf.day13.utils.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
注解配置见上面的案例代码
11.2 过滤器链
过滤器链是指在项目如果存在多个过滤器,则构成过滤器链,当发送请求时会依次通过过滤器链上的所有过滤的过滤。
当某一个过滤器禁止了请求的继续过滤时,一定要该代码后加上return。否则会引发Cannot call sendError…异常。
过滤器链优先级:
如果都在web.xml中配置,则按照配置的filter-mapping的顺序。
如果都使用注解配置,则按照类名称的字母顺序。
如果既有web.xml配置也有注解配置,web.xml配置的优先。
11.3 过滤器实际应用
在项目中过滤器一般应用在转码、认证、权限等场景。
下面代码演示了转码和认证的场景:
转码过滤器
@WebFilter("/*")
public class CharacterEncodingFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("开始转码...");
servletRequest.setCharacterEncoding("utf-8"); // POST转码
filterChain.doFilter(servletRequest, servletResponse); // 让过滤器链继续执行过滤
}
@Override
public void destroy() {
}
}
认证过滤器
@WebFilter("/*")
public class AuthFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("AuthFilter开始过滤...");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String uri = request.getRequestURI(); // 得到请求的路径
if (uri.endsWith("/login.do") ||
uri.endsWith("/index.html")||
uri.endsWith("/fail.html")){
// url放行
}else{
HttpSession session = request.getSession();
String user = (String) session.getAttribute("user");
if (user == null){
response.sendRedirect("index.html");
return;
}
}
filterChain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
关于过滤器的想法和总结:
因为过滤器起到拦截和放行的作用,所以在跳转时需要慎重,因为通过getURI获取的路径是request 携带的目标地址,过滤器是横在浏览器和目标地址之间的拦路虎,你需要符合你所写的过滤逻辑,搞清楚怎么样可以放过,怎么样会被拦截,发送到其他的界面。