# 前言
以前的 Web 开发会用到 Servlet,在后端使用 Cookie,Session,Fliter,HttpServletRequest 之类的,当然也不代表现在开发 / 维护的项目就没有这些。只是说在 Springboot+vue 开发中,这些东西越来越简化,使用起来越来越便利,前端传过来的数据可以直接用 @RequestMapping
参数接收,界面跳转,设置 cookie 之类的在 vue 就可以完成。
为了完整性,还是将这些在本文写一下。
# 传统 Web 开发
现在的 web 开发大部分都用不到了,但是出于兴趣或者了解知识,我们可以学习一下。tomcat,maven 我就没讲了,可以自己网上搜一下配置(像 springboot 就不需要我们自己配置 tomcat 了)。
创建项目:
在
New Project
左侧选择Java Enterprise
创建项目。如果
main
包下没有webappp
文件夹,就手动创建一个,其中 html 文件就放在里面。配置项目在 Servlet 里面会讲。
# Servlet
我们先看一下 Web 的执行流程:浏览器输入 URL 发出请求 — 后端 Servlet 处理对应请求 —Servlet 返回请求结果(如重定向到另一界面)。
可以通过实现 Servlet 来进行动态网页响应,使用 Servlet,不再是直接由 Tomcat 服务器发送我们编写好的静态网页内容(HTML 文件),而是由我们通过 Java 代码进行动态拼接的结果,它能够很好地实现动态网页的返回。
先导入依赖:
<dependency> | |
<groupId>javax.servlet</groupId> | |
<artifactId>servlet-api</artifactId> | |
<version>2.5</version> | |
</dependency> |
需要注意的是,Tomcat10 以上的版本比较新,Servlet API 包名发生了一些变化,因此我们需要修改一下依赖:
<dependency> | |
<groupId>jakarta.servlet</groupId> | |
<artifactId>jakarta.servlet-api</artifactId> | |
<version>5.0.0</version> | |
</dependency> |
Jakarta 9(2019 及以后)使用 jakarta 命名空间。
Java EE 5(2005)到 Java EE 8(2017)使用 javax 命名空间。
Java EE 4 使用 javax 命名空间。
然后写一个 Servlet
:
@WebServlet("/test") | |
public class TestServlet implements Servlet { | |
// 其实直接继承 HttpServlet 也可以 | |
...实现接口方法 | |
} |
项目名为 demo,所以运行,我们就可以访问:http://localhost:8080/demo/test IDEA 里面需要修改一下配置:
再改一下 Deployment:
我们这里统一下, /demo_war_exploded
都改成 /demo
。
写一个 login.html
放在 webappp 里,就可以访问 http://localhost:8080/demo/login.html 了。
这个着重说一下,访问 html,相当于直接向服务器请求一个静态资源,服务器给的响应(包含)就是 html 资源。之前我们写的直接在浏览器访问 URL
.../demo/test
也是一个请求,只不过请求的不是静态资源。
# @WebServlet 注解
可以是通配符匹配:
@WebServlet("/test/*") |
上面的路径表示,所有匹配 /test/随便什么
的路径名称,都可以访问此 Servlet。
该注解甚至可以配置多个路径,配置该 servlet 是否在服务器启动时加载此 Servlet 等,这里不再深究。
# 生命周期
运行服务器,发送请求,再关闭服务器,servlet 里面执行的方法顺序是:
- 先执行构造方法完成 Sevlet 初始化。
- 调用
init()
方法。 - Servlet 调用 service () 方法处理请求(有时调用两次是因为浏览器请求
favicon.ico
。 - Servlet 销毁前调用 destory 方法。
每当浏览器向服务器发起一个请求时,都会创建一个线程执行一次 service
方法,来让我们处理用户的请求,并将结果响应给用户。
service
方法中,还有两个参数, ServletRequest
和 ServletResponse
,实际上,用户发起的 HTTP 请求,就被 Tomcat 服务器封装为了一个 ServletRequest
对象,我们得到是其实是 Tomcat 服务器帮助我们创建的一个实现类,HTTP 请求报文中的所有内容,都可以从 ServletRequest
对象中获取,同理, ServletResponse
就是我们需要返回给浏览器的 HTTP 响应报文实体类封装。
@Override | |
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { | |
// 首先将其转换为 HttpServletRequest(继承自 ServletRequest,一般是此接口实现) | |
HttpServletRequest request = (HttpServletRequest) servletRequest; | |
System.out.println(request.getProtocol()); // 获取协议版本 | |
System.out.println(request.getRemoteAddr()); // 获取访问者的 IP 地址 | |
System.out.println(request.getMethod()); // 获取请求方法 | |
// 转换为 HttpServletResponse(同上) | |
HttpServletResponse response = (HttpServletResponse) servletResponse; | |
// 设定内容类型以及编码格式(普通 HTML 文本使用 text/html,之后会讲解文件传输) | |
response.setHeader("Content-type", "text/html;charset=UTF-8"); | |
// 获取 Writer 直接写入内容 | |
response.getWriter().write("我是响应内容!"); | |
// 所有内容写入完成之后,再发送给浏览器 | |
} |
# Post 请求实现登录
这里我们看一下登录实例:
前端 login.html
:
<body> | |
<h1>登录到系统</h1> | |
<form method="post" action="login"> <!-- 发送 login 请求,不要加 /,不然就会当成根目录 --> | |
<hr> | |
<div> | |
<label> | |
<input type="text" placeholder="用户名" name="username"> | |
</label> | |
</div> | |
<div> | |
<label> | |
<input type="password" placeholder="密码" name="password"> | |
</label> | |
</div> | |
<div> | |
<button>登录</button> | |
</div> | |
</form> | |
</body> |
通过修改 form 标签的属性,现在我们点击登录按钮,会自动向后台发送一个 POST 请求,请求地址为当前地址 +/login(注意不同路径的写法,加 /
就是根目录 +/login),也就是我们上面编写的 Servlet 路径。
后端 LoginServlet
:
@Log | |
@WebServlet("/login") | |
public class LoginServlet extends HttpServlet { | |
@Override | |
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { | |
// 首先设置一下响应类型 | |
resp.setContentType("text/html;charset=UTF-8"); | |
// 获取 POST 请求携带的表单数据 | |
Map<String, String[]> map = req.getParameterMap(); | |
// 判断表单是否完整 | |
if(map.containsKey("username") && map.containsKey("password")) { | |
String username = req.getParameter("username"); | |
String password = req.getParameter("password"); | |
// 假设有数据库的数据校验 | |
resp.getWriter().write("登陆成功!"); | |
// 之后学了请求转发和重定向就可以跳转到其他界面 | |
}else { | |
resp.getWriter().write("错误,您的表单数据不完整!"); | |
} | |
} | |
} |
现在我们请求一下 http://localhost:8888/demo/login.html ,写点东西提交上去,看看访问的 URL。
那么我希望登录成功是跳转到另一个界面,就可以使用重定向:
if(map.containsKey("username") && map.containsKey("password")) { | |
String username = req.getParameter("username"); | |
String password = req.getParameter("password"); | |
// 假设有数据库的数据校验 | |
resp.getWriter().write("登陆成功!"); | |
resp.sendRedirect("success.html");// 记得在 webapp 下创建一个 success.html | |
// 之后学了请求转发和重定向就可以跳转到其他界面 | |
}else { | |
resp.getWriter().write("错误,您的表单数据不完整!"); | |
} |
调用后,响应的状态码会被设置为 302,并且响应头中添加了一个 Location 属性,此属性表示,需要重定向到哪一个网址。
请求转发是在服务器内部跳转,直接将本次请求转发给其他 Servlet 进行处理,并由其他 Servlet 来返回结果,因此它是在进行内部的转发。
比如登陆后,我希望交给关于时间的 servlet 来处理:
req.getRequestDispatcher("/time").forward(req, resp); |
这是在服务器内部跳转的,浏览器是只请求了一次。请求转发的有点就在于可以传输数据。
最后总结,两者的区别为:
请求转发是一次请求,重定向是两次请求
请求转发地址栏不会发生改变, 重定向地址栏会发生改变
请求转发可以共享请求参数 ,重定向之后,就获取不了共享参数了
请求转发只能转发给内部的 Servlet
# ServletContext 对象
这是 Servlet 最后一部分,重定向就可以通过这个传输数据。
ServletContext 全局唯一,它是属于整个 Web 应用程序的,我们可以通过 getServletContext()
来获取到此对象。
ServletContext context = getServletContext(); | |
context.setAttribute("test", "我是重定向之前的数据"); | |
resp.sendRedirect("time"); |
因为无论在哪里,无论什么时间,获取到的 ServletContext 始终是同一个对象,因此我们可以随时随地获取我们添加的属性。还可以获取根目录的资源文件,注意是 webapp 目录下的文件。
# Cookie
在浏览器中保存一些信息,并且在下次请求时,请求头中会携带这些信息。
- 从
HttpServletRequest
请求获得cookie
:request.getCookie()
。 - 将
cookie
写入HttpServletResponse
响应:response.addCookie(cookie)
。
Cookie
包含的重要信息:
name - Cookie 的名称,Cookie 一旦创建,名称便不可更改。
value - Cookie 的值,如果值为 Unicode 字符,需要为字符编码。如果为二进制数据,则需要使用 BASE64 编码。
maxAge - Cookie 失效的时间,单位秒。如果为正数,则该 Cookie 在 maxAge 秒后失效。如果为负数,该 Cookie 为临时 Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该 Cookie。如果为 0,表示删除该 Cookie。默认为 - 1。
domain - 可以访问该 Cookie 的域名。如果设置为 “.google.com”,则所有以 “google.com” 结尾的域名都可以访问该 Cookie。注意第一个字符必须为 “.”。
通过 Cookie 来校验登录信息(之前登录过一次,在有效时间内,浏览器再次请求该服务器,会将之前的 Cookie 一起发送过去)
@Override | |
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { | |
Cookie[] cookies = req.getCookies(); | |
if(cookies != null){ | |
String username = null; | |
String password = null; | |
for (Cookie cookie : cookies) { | |
if(cookie.getName().equals("username")) username = cookie.getValue(); | |
if(cookie.getName().equals("password")) password = cookie.getValue(); | |
} | |
if(username != null && password != null){ | |
// 登陆校验 | |
try (SqlSession sqlSession = factory.openSession(true)){ | |
UserMapper mapper = sqlSession.getMapper(UserMapper.class); | |
User user = mapper.getUser(username, password); | |
if(user != null){ | |
resp.sendRedirect("time"); | |
return; // 直接返回 | |
} | |
} | |
} | |
} | |
req.getRequestDispatcher("/").forward(req, resp); | |
// 正常情况还是转发给默认的 Servlet 帮我们返回静态页面 | |
} |
# Session
Session 会给浏览器设定一个叫做 JSESSIONID
的 Cookie,值是一个随机的排列组合,而此 Cookie 就对应了你属于哪一个对话,只要我们的浏览器携带此 Cookie 访问服务器,服务器就会通过 Cookie 的值进行辨别,得到对应的 Session 对象,因此,这样就可以追踪到底是哪一个浏览器在访问服务器。
其实我感觉和 Cookie 使用差不多,只是说 Session 是针对该浏览器和该服务器特殊的 Cookie。
// 设置 session | |
HttpSession session = req.getSession(); | |
session.setAttribute("user", user); | |
// 获取 session | |
HttpSession session = request.getSession(); | |
User user = (User) session.getAttribute("user"); | |
if(user == null) { | |
resp.sendRedirect("login"); | |
return; | |
} |
Session 并不是永远都存在的,它有着自己的过期时间,默认时间为 30 分钟,若超过此时间,Session 将丢失。
# Fliter
在 Session 的代码就可以控制没有登陆的请求跳转到登录界面,这种请求也可以配合过滤器使用。只有过滤器允许通过的请求,才可以顺利地到达对应的 Servlet,而过滤器不允许的通过的请求,我们可以自由地进行控制是否进行重定向或是请求转发。并且过滤器可以添加很多个,就相当于添加了很多堵墙。
@WebFilter("/*") // 路径的匹配规则和 Servlet 一致,这里表示匹配所有请求 | |
public class TestFilter implements Filter { | |
@Override | |
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { | |
} | |
} |
让请求通过该过滤器:
filterChain.doFilter(servletRequest, servletResponse); |
由于我们整个应用程序可能存在多个过滤器,那么这行代码的意思实际上是将此请求继续传递给下一个过滤器,当没有下一个过滤器时,才会到达对应的 Servlet 进行处理。过滤器的过滤顺序是按照类名的自然排序进行的,因此我们将第一个过滤器命名进行调整。
这种模式处理让我想起了 Nettty 的
Channel
🤣。
实际上,当 doFilter
方法调用时,就会一直向下直到 Servlet,在 Servlet 处理完成之后,又依次返回到最前面的 Filter,类似于递归的结构。
同 Servlet 一样,实现一个 Filter
类可以继承 HttpFilter
。下面看一下🌰,为应用程序添加一个过滤器,在用户未登录的情况下,只允许静态资源和登录页面请求通过:
@WebFilter("/*") | |
public class MainFilter extends HttpFilter { | |
@Override | |
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { | |
String url = req.getRequestURL().toString(); | |
// 判断是否为静态资源 | |
if(!url.endsWith(".js") && !url.endsWith(".css") && !url.endsWith(".png")){ | |
HttpSession session = req.getSession(); | |
User user = (User) session.getAttribute("user"); | |
// 判断是否未登陆 | |
if(user == null && !url.endsWith("login")){ | |
res.sendRedirect("login"); | |
return; | |
} | |
} | |
// 交给过滤链处理 | |
chain.doFilter(req, res); | |
} | |
} |
# JSP
JSP 并不是我们需要重点学习的内容,因为它已经过时了,使用 JSP 会导致前后端严重耦合,因此这里只做了解即可。
JSP 是模板引擎,模板需要我们填入数据才可以变成页面,我们可以直接在 JSP 中编写 Java 代码,并在页面加载的时候执行。
<h1><%= new Date() %></h1>
这样的写法相当于整个页面既要编写前端代码,也要编写后端代码,随着项目的扩大,整个页面会显得难以阅读。
实际上,Tomcat 在加载 JSP 页面时,会将其动态转换为一个 java 类并编译为 class 进行加载,而生成的 Java 类,正是一个 Servlet 的子类,而页面的内容全部被编译为输出字符串,这便是 JSP 的加载原理,因此,JSP 本质上依然是一个 Servlet!
之后介绍的百里香叶就是一种技能实现模板,又能兼顾前后端分离的模板引擎。
# 参考
https://www.yuque.com/qingkongxiaguang/javaweb/hkgbvo#Listener