经过前面的学习,现在终于可以正式进入到后端的学习当中,不过,我们还是需要再系统地讲解一下HTTP通信基础知识,它是我们学习JavaWeb的基础知识,我们之前已经学习过TCP通信,而HTTP实际上是基于TCP协议之上的应用层协议,因此理解它并不难理解。
计算机网络基础
万维网的工作方式:客户程序(电脑上的浏览器)向服务器程序(Web服务器)发出请求,服务器程序向客户程序送回客户所要的万维网文档,在一个客户程序主窗口上显示出的万维网文档称为页面。
我们的服务器可能不止一个页面,可能会有很多个页面,那么客户端如何知道该去访问哪个服务器的哪个页面呢?这个时候就需要用到URL统一资源定位符。互联网上所有的资源,都有一个唯一确定的URL,比如http://www.baidu.com
URL的格式为:
<协议>://<主机>:<端口>/<路径>
协议是指采用什么协议来访问服务器,不同的协议决定了服务器返回信息的格式,我们一般使用HTTP协议。
主机可以是一个域名,也可以是一个IP地址(实际上域名最后会被解析为IP地址进行访问)
端口是当前服务器上Web应用程序开启的端口,我们前面学习TCP通信的时候已经介绍过了,HTTP协议默认使用80端口,因此有时候可以省略。
路径就是我们希望去访问此服务器上的某个文件,不同的路径代表访问不同的资源。
认识Tomcat服务器
1、Tomcat目录
- bin目录:所有可执行文件,包括启动和关闭服务器的脚本
- conf目录:服务器配置文件目录
- lib目录:Tomcat服务端运行的一些依赖,不用关心
- logs目录:所有的日志信息都在这里
- temp目录:存放运行时产生的一些临时文件,不用关心
- work目录:工作目录,Tomcat会将jsp文件转换为java文件(我们后面会讲到,这里暂时不提及)
- webapp目录:所有的Web项目都在这里,每个文件夹都是一个Web应用程序
我们发现,官方已经给我们预设了一些项目了,访问后默认使用的项目为ROOT项目,也就是我们默认打开的网站。
我们也可以访问example项目,只需要在后面填写路径即可:http://localhost:8080/examples/,或是docs项目(这个是Tomcat的一些文档)http://localhost:8080/docs/
使用Maven创建Web项目
1、创建项目
需要注意的是,Tomcat10以上的版本比较新,Servlet API包名发生了一些变化,因此我们需要修改一下依赖:
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
注意包名全部从javax改为jakarta,我们需要手动修改一下。
我们可以将项目直接打包为war包(默认),打包好之后,放入webapp文件夹,就可以直接运行我们通过Java编写的Web应用程序了,访问路径为文件的名称。
(先关闭tomcat服务器)点击这个package后就可以在target目录下得到war包:
然后我们将这个war包放到tomcat下的bin下的webapps下,再启动tomcat访问这个项目即可
Servlet
前面我们已经完成了基本的环境搭建,那么现在我们就可以开始来了解我们的第一个重要类——Servlet。
它是Java EE的一个标准,大部分的Web服务器都支持此标准,包括Tomcat,就像之前的JDBC一样,由官方定义了一系列接口,而具体实现由我们来编写,最后交给Web服务器(如Tomcat)来运行我们编写的Servlet。
那么,它能做什么呢?我们可以通过实现Servlet来进行动态网页响应,使用Servlet,不再是直接由Tomcat服务器发送我们编写好的静态网页内容(HTML文件),而是由我们通过Java代码进行动态拼接的结果,它能够很好地实现动态网页的返回。
当然,Servlet并不是专用于HTTP协议通信,也可以用于其他的通信,但是一般都是用于HTTP。
1、创建Servlet
那么如何创建一个Servlet呢,非常简单,我们只需要实现Servlet
类即可,并添加注解@WebServlet
来进行注册。
@WebServlet("/test")
public class TestServlet implements Servlet {
...实现接口方法
}
然后将tomcat编辑配置,如果要改路径,不仅要改“服务器”页面下的路径,还要将“部署“中的应用程序上下文也改了;两处必须都更改
除了直接编写一个类,我们也可以在web.xml
中进行注册,现将类上@WebServlet
的注解去掉:
<servlet>
<servlet-name>test</servlet-name>
<servlet-class>com.example.webtest.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>test</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
这样的方式也能注册Servlet,但是显然直接使用注解更加方便,因此之后我们一律使用注解进行开发。只有比较新的版本才支持此注解,老的版本是不支持的哦。
实际上,Tomcat服务器会为我们提供一些默认的Servlet,也就是说在服务器启动后,即使我们什么都不编写,Tomcat也自带了几个默认的Servlet,他们编写在conf目录下的web.xml中:
<!-- The mapping for the default servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- The mappings for the JSP servlet -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
我们发现,默认的Servlet实际上可以帮助我们去访问一些静态资源,这也是为什么我们启动Tomcat服务器之后,能够直接访问webapp
目录下的静态页面。
我们可以将之前编写的页面放入到webapp
目录下,来测试一下是否能直接访问。
2、探究Servlet的生命周期
我们已经了解了如何注册一个Servlet,那么我们接着来看看,一个Servlet是如何运行的。
首先我们需要了解,Servlet中的方法各自是在什么时候被调用的,我们先编写一个打印语句来看看:
(这里是 实现 Servlet 接口 的方式)
public class TestServlet implements Servlet {
public TestServlet(){
System.out.println("我是构造方法!");
}
@Override
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("我是init");
}
@Override
public ServletConfig getServletConfig() {
System.out.println("我是getServletConfig");
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("我是service");
}
@Override
public String getServletInfo() {
System.out.println("我是getServletInfo");
return null;
}
@Override
public void destroy() {
System.out.println("我是destroy");
}
}
我们首先启动一次服务器,然后访问我们定义的页面,然后再关闭服务器,得到如下的顺序:
我是构造方法!
我是init
我是service
我是service(出现两次是因为浏览器请求了2次,是因为有一次是请求favicon.ico,浏览器通病)
我是destroy
我们可以多次尝试去访问此页面,但是init和构造方法只会执行一次,而每次访问都会执行的是service
方法,因此,一个Servlet的生命周期为:
- 首先执行构造方法完成 Servlet 初始化
- Servlet 初始化后调用 init () 方法。
- Servlet 调用 service() 方法来处理客户端的请求。
- Servlet 销毁前调用 destroy() 方法。
- 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。
我们发现service
方法中,还有两个参数,ServletRequest
和ServletResponse
,实际上,用户发起的HTTP请求,就被Tomcat服务器封装为了一个ServletRequest
对象,我们得到是其实是Tomcat服务器帮助我们创建的一个实现类,HTTP请求报文中的所有内容,都可以从ServletRequest
对象中获取,同理,ServletResponse
就是我们需要返回给浏览器的HTTP响应报文实体类封装。
(在开发者工具下的“网络”下,run后可以看到请求头等)
那么我们来看看ServletRequest
中有哪些内容,我们可以获取请求的一些信息:
@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()); //获取请求方法
//获取头部信息
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()){
String name = enumeration.nextElement();
System.out.println(name + ": " + request.getHeader(name));
}
}
(get方法一般用于请求页面,post方法一般用于提交数据,(比如提交表单、发起登录请求)
即一般通俗来讲,get请求数据,post提交数据)
我们发现,整个HTTP请求报文中的所有内容,都可以通过HttpServletRequest
对象来获取,当然,它的作用肯定不仅仅是获取头部信息,我们还可以使用它来完成更多操作,后面会一一讲解。
那么我们再来看看ServletResponse
,这个是服务端的响应内容,我们可以在这里填写我们想要发送给浏览器显示的内容:
//转换为HttpServletResponse(同上)
HttpServletResponse response = (HttpServletResponse) servletResponse;
//设定内容类型以及编码格式(普通HTML文本使用text/html,之后会讲解文件传输)
response.setHeader("Content-type", "text/html;charset=UTF-8");
//获取Writer直接写入内容
response.getWriter().write("我是响应内容!");
//所有内容写入完成之后,再发送给浏览器
现在我们在浏览器中打开此页面,就能够收到服务器发来的响应内容了。其中,响应头部分,是由Tomcat帮助我们生成的一个默认响应头。
3、解读和使用HttpServlet
前面我们已经学习了如何创建、注册和使用Servlet,那么我们继续来深入学习Servlet接口的一些实现类。
首先Servlet有一个直接实现抽象类GenericServlet
,那么我们来看看此类做了什么事情。
我们发现,这个类完善了配置文件读取和Servlet信息相关的的操作,但是依然没有去实现service
方法,因此此类仅仅是用于完善一个Servlet的基本操作,那么我们接着来看HttpServlet
,它是遵循HTTP协议的一种Servlet,继承自GenericServlet
,它根据HTTP协议的规则,完善了service
方法。
在阅读了HttpServlet源码之后,我们发现,其实我们只需要继承HttpServlet来编写我们的Servlet就可以了,并且它已经帮助我们提前实现了一些操作,这样就会给我们省去很多的时间。
(这里是 继承 HttpServlet 类 的方式)
@Log
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h1>恭喜你解锁了全新玩法</h1>");
}
}
(重启服务器后,进到/test页面,就可以看到这行字了,并且h1凸显效果)
现在,我们只需要重写对应的请求方式(必须),就可以快速完成Servlet的编写。
4、@WebServlet注解详解
我们接着来看WebServlet注解,我们前面已经得知,可以直接使用此注解来快速注册一个Servlet,那么我们来想细看看此注解还有什么其他的玩法。
首先name
属性就是Servlet名称,而urlPatterns
和value
实际上是同样功能,就是代表当前Servlet的访问路径,它不仅仅可以是一个固定值,还可以进行通配符匹配:
@WebServlet("/test/*")
上面的路径表示,所有匹配/test/随便什么的路径名称,都可以访问此Servlet,我们可以在浏览器中尝试一下。
也可以进行某个扩展名称的匹配:
@WebServlet("*.js")
这样的话,获取任何以js结尾的文件,都会由我们自己定义的Servlet处理。
那么如果我们的路径为/呢?
@WebServlet("/")
此路径和Tomcat默认为我们提供的Servlet冲突,会直接替换掉默认的,而使用我们的,此路径的意思为,如果没有找到匹配当前访问路径的Servlet,那么就会使用此Servlet进行处理。
我们还可以为一个Servlet配置多个访问路径:
@WebServlet({"/test1", "/test2"})
我们接着来看loadOnStartup
属性,此属性决定了是否在Tomcat启动时就加载(init)此Servlet,默认情况下,Servlet只有在被访问时才会加载,它的默认值为-1
,表示不在启动时加载,我们可以将其修改为大于等于0的数,来开启启动时加载。并且数字的大小决定了此Servlet的启动优先级。
public @interface WebServlet {
String name() default "";
String[] value() default {};
String[] urlPatterns() default {};
int loadOnStartup() default -1;
@Log
@WebServlet(value = "/test", loadOnStartup = 1)
public class TestServlet extends HttpServlet {
@Override
public void init() throws ServletException {
super.init();
log.info("我被初始化了!");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h1>恭喜你解锁了全新玩法</h1>");
}
}
其他内容都是Servlet的一些基本配置,这里就不详细讲解了。
5、(Servlet应用(一))使用POST请求完成登陆
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>demo</name>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<junit.version>5.8.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/book_manage"/>
<property name="username" value="root"/>
<property name="password" value="root123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.example.mapper.UserMapper"/>
</mappers>
</configuration>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录到系统</h1>
<form method="post" action="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>
</html>
User
@Data
public class User {
String username;
String password;
}
UserMapper
public interface UserMapper {
@Select("select * from users where username = #{username} and password = #{password}")
User getUser(@Param("username") String username, @Param("password") String password);
}
LoginServlet
@Log
@WebServlet(value = "/login", loadOnStartup = 1)
public class LoginServlet extends HttpServlet {
SqlSessionFactory factory;
@SneakyThrows
@Override
public void init() throws ServletException {
factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
}
@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");
//登陆校验(待完善)
try (SqlSession sqlSession = factory.openSession(true)){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUser(username, password);
//判断用户是否登陆成功,若查询到信息则表示存在此用户
if(user != null){
resp.getWriter().write("登陆成功!");
}else {
resp.getWriter().write("登陆失败,请验证您的用户名或密码!");
}
}
} else {
resp.getWriter().write("错误,您的表单数据不完整!");
}
}
}
我们前面已经了解了如何使用Servlet来处理HTTP请求,那么现在,我们就结合前端,来实现一下登陆操作。
我们需要修改一下我们的Servlet,现在我们要让其能够接收一个POST请求:
(在com.example.servlet下新建一个LoginServlet类)
(用继承更方便一些)
@Log
@WebServlet("/login", loadOnStartup = 1)
public class LoginServlet extends HttpServlet {
@Override
public void init() throws ServletException {
super.init();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getParameterMap().forEach((k, v) -> {
System.out.println(k + ": " + Arrays.toString(v));
});
}
}
ParameterMap
存储了我们发送的POST请求所携带的表单数据,我们可以直接将其遍历查看,浏览器发送了什么数据。
现在我们再来修改一下前端:
<body>
<h1>登录到系统</h1>
<form method="post" action="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>
(注意这个login前面没有/,加了以后就直接从根目录开始走了也就是变成localhost:8080/login)
通过修改form标签的属性,现在我们点击登录按钮,会自动向后台发送一个POST请求,请求地址为当前地址+/login
(注意不同路径的写法),也就是我们上面编写的Servlet路径。
运行服务器,测试后发现,在点击按钮后,确实向服务器发起了一个POST请求,并且携带了表单中文本框的数据。
现在,我们根据已有的基础,将其与数据库打通,我们进行一个真正的用户登录操作,首先修改一下Servlet的逻辑:
@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");
//权限校验(待完善)
}else {
resp.getWriter().write("错误,您的表单数据不完整!");
}
}
接下来我们再去编写Mybatis的依赖和配置文件,创建一个表,用于存放我们用户的账号和密码。
在之前图书管理系统的book_manage数据库中新建一个表users
我们先随便弄一个数据
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${驱动类(含包名)}"/>
<property name="url" value="${数据库连接URL}"/>
<property name="username" value="${用户名}"/>
<property name="password" value="${密码}"/>
</dataSource>
</environment>
</environments>
</configuration>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
配置完成后,在我们的Servlet
的init
方法中编写Mybatis初始化代码,因为它只需要初始化一次。
SqlSessionFactory factory;
@SneakyThrows
@Override
public void init() throws ServletException {
factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
}
现在我们创建一个实体类
以及Mapper
来进行用户信息查询:
com.example.entity.User 类
@Data
public class User {
String username;
String password;
}
com.example.mapper.UserMapper 接口
public interface UserMapper {
@Select("select * from users where username = #{username} and password = #{password}")
User getUser(@Param("username") String username, @Param("password") String password);
}
<mappers>
<mapper class="com.example.mapper.UserMapper"/>
</mappers>
好了,现在完事具备,只欠东风了,我们来完善一下登陆验证逻辑:
//登陆校验(待完善)
try (SqlSession sqlSession = factory.openSession(true)){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUser(username, password);
//判断用户是否登陆成功,若查询到信息则表示存在此用户
if(user != null){
resp.getWriter().write("登陆成功!");
}else {
resp.getWriter().write("登陆失败,请验证您的用户名或密码!");
}
}
现在再去浏览器上进行测试吧!
注册界面其实是同理的,这里就不多做讲解了。
6、(Servlet应用(二))上传和下载文件
上传文件
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>demo</name>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<junit.version>5.8.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录到系统</h1>
<form method="post" action="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>
<hr>
<form method="post" action="file" enctype="multipart/form-data">
<div>
<input type="file" name="test-file">
</div>
<div>
<button>上传文件</button>
</div>
</form>
<hr>
<a href="file" download="icon.png">点我下载高清资源</a>
</body>
</html>
FileServlet
@WebServlet("/file")
@MultipartConfig
public class FileServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("image/png");
OutputStream outputStream = resp.getOutputStream();
InputStream inputStream = Resources.getResourceAsStream("icon.png");
//直接使用copy方法完成转换
IOUtils.copy(inputStream, outputStream);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try(FileOutputStream stream = new FileOutputStream("/Users/fieldxia/Documents/JAVA/learnServlet/test.png")){
Part part = req.getPart("test-file"); // 获取文件;参数为 表单中对应的名称name
IOUtils.copy(part.getInputStream(), stream);
resp.setContentType("text/html;charset=UTF-8"); // 因为还要返回数据来表示文件上传成功
resp.getWriter().write("文件上传成功!"); // 然后就可以往里面输入数据
}
}
}
首先我们来看看比较简单的下载文件,首先将我们的icon.png放入到resource
文件夹(也可以是其他地方,但是要转换输入流,最后只要是InputStream即可)中,接着我们编写一个Servlet
用于处理文件下载:
(这里还是用mybatis的工具类Resources.getResourceAsStream)
(下载文件就是直接获取资源,不需要提交什么,所以这里get
即可)
(image/png表示下载png格式的图片)
@WebServlet("/file")
public class FileServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("image/png");
OutputStream outputStream = resp.getOutputStream();
InputStream inputStream = Resources.getResourceAsStream("icon.png");
}
}
为了更加快速地编写IO代码,我们可以引入一个工具库:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
使用此类库可以快速完成IO操作:
resp.setContentType("image/png");
OutputStream outputStream = resp.getOutputStream();
InputStream inputStream = Resources.getResourceAsStream("icon.png");
//直接使用copy方法完成转换
IOUtils.copy(inputStream, outputStream);
现在我们在前端页面添加一个链接,用于下载此文件:
(注意这里a的href和上面表单form的action一样,这里不要添加斜杠,不加斜杠就是在当前这个路径下去寻找;这里按住command键这个file会变蓝,点进去直接跳转到file对应的Servlet)
<hr>
<a href="file" download="icon.png">点我下载高清资源</a>
下载文件搞定,那么如何上传一个文件呢?
首先我们编写前端部分:
(注意上传文件的method为post)
<form method="post" action="file" enctype="multipart/form-data">
<div>
<input type="file" name="test-file">
</div>
<div>
<button>上传文件</button>
</div>
</form>
注意必须添加enctype="multipart/form-data"
,来表示此表单用于文件传输。
现在我们来修改一下Servlet代码:
(这个FileOutputStream最好是绝对路径)
(HttpServletRequest的getParts
方法获得Part对象的集合。每个Part对象对应一个上传的文件,通过调用它的getInputStream方法可以获得输入流,然后按照正常的字节流方法处理即可。)
(运行时会提示由于没有提供multi-part配置,无法处理parts。根据servlet规约,有两种方式可以解决这个问题。一是给servlet增加@MultipartConfig注解;二是在部署描述符中为servlet配置< multipart-config > < /multipart-config>)
@WebServlet("/file")
@MultipartConfig
public class FileServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("image/png");
OutputStream outputStream = resp.getOutputStream();
InputStream inputStream = Resources.getResourceAsStream("icon.png");
//直接使用copy方法完成转换
IOUtils.copy(inputStream, outputStream);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try(FileOutputStream stream = new FileOutputStream("/Users/xxx/Documents/JAVA/learnServlet/test.png")){
Part part = req.getPart("test-file"); // 获取文件;参数为 表单中对应的名称name
IOUtils.copy(part.getInputStream(), stream);
resp.setContentType("text/html;charset=UTF-8"); // 因为还要返回数据来表示文件上传成功
resp.getWriter().write("文件上传成功!"); // 然后就可以往里面输入数据
}
}
}
注意,必须添加@MultipartConfig
注解来表示此Servlet用于处理文件上传请求。
现在我们再运行服务器,并将我们刚才下载的文件又上传给服务端。
8、(Servlet应用(三))使用XHR请求数据
现在我们希望,网页中的部分内容,可以动态显示,比如网页上有一个时间,旁边有一个按钮,点击按钮就可以刷新当前时间。
这个时候就需要我们在网页展示时向后端发起请求了,并根据后端响应的结果,动态地更新页面中的内容,要实现此功能,就需要用到JavaScript
来帮助我们,首先在js
中编写我们的XHR
请求,并在请求中完成动态更新:
(xhr请求实际上是当前这个页面向后台发起的一个动态请求,也就是说页面在展示的过程中还可以向后台发起请求)
(webapp/test.js)
(open的true表示将异步打开,如果false如果服务器网不太好会一直卡在这里等待服务器响应,而如果异步发了请求就发了,到底有没有结果我们也不关心,可以做别的事情,有结果的时候会自动调用这里写的onready里的内容)
(xhr一共有5种状态,=4是最后一种状态,是服务器成功接收到数据的状态,同时状态码必须是200)
function updateTime() {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
document.getElementById("time").innerText = xhr.responseText
}
};
// 第二个参数是url
xhr.open('GET', 'time', true);
xhr.send();
}
接着修改一下前端页面,添加一个时间显示区域:
(一开始不点按钮前也要获取一次时间的,所以下面< script >updateTime()< /script>;一开始的时候,html在加载的时候到这个script会直接调用一次)
<script src="test.js"></script>
<hr>
<div id="time"></div>
<br>
<button onclick="updateTime()">更新数据</button>
<script>
updateTime()
</script>
最后创建一个Servlet
用于处理时间更新请求:
@WebServlet("/time")
public class TimeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String date = dateFormat.format(new Date());
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(date);
}
}
(如果浏览器提示updateTime未定义,这种时候可以点击maven的生命周期中的clear清空一下,可能是缓存数据没有更新)
(前端更新缓存的方法:打开“设置”,搜索“缓存”,然后“清除浏览数据”)
现在点击按钮就可以更新了。
GET请求也能传递参数,这里做一下演示。
(GET请求是怎么带数据的呢?get请求不像post请求,它不能直接带一个form data;首先,在上面open函数中第二个参数url中"time"后面打一个问号(英文)然后后面跟上键值,这样我们就可以通过get请求传递参数)
xhr.open('GET', 'time?username=Test&password=123456', true);
同时,后台也读取到了
个体请求带参数一般是需要去请求一些资源之类的,分类,请求特殊的;依然不能改变get是专门用来请求资源的性质;而post是用来提交数据的
8、重定向与请求转发
当我们希望用户登录完成之后,直接跳转到网站的首页,那么这个时候,我们就可以使用重定向来完成。当浏览器收到一个重定向的响应时,会按照重定向响应给出的地址,再次向此地址发出请求。
实现重定向很简单,只需要调用一个方法即可,我们修改一下登陆成功后执行的代码:
(注意这里这个参数也是和之前一样,不要加斜杠,不然会从根目录开始访问)(这里是内部跳转)
(或者写“https://www.baidu.com“可以直接跳转到百度)
resp.sendRedirect("time");
调用后,响应的状态码会被设置为302
(表示让浏览器打开另一个页面),并且响应头中添加了一个Location
属性,此属性表示,需要重定向到哪一个网址。
现在,如果我们成功登陆,那么服务器会发送给我们一个重定向响应,这时,我们的浏览器会去重新请求另一个网址。这样,我们在登陆成功之后,就可以直接帮助用户跳转到用户首页了。
另外,“响应的状态码会被设置为302
“,想到我们也可以手动去设置
if(user != null){
resp.setStatus(302);
resp.setHeader("Location", "https://www.baidu.com");
} else {
(请求转发是在请求阶段,不是请求已经处理完后响应阶段的事情,所以我们还要去处理这个请求)
那么我们接着来看请求转发,请求转发其实是一种服务器内部的跳转机制,我们知道,重定向会使得浏览器去重新请求一个页面,而请求转发则是服务器内部进行跳转,它的目的是,直接将本次请求转发给其他Servlet进行处理,并由其他Servlet来返回结果,因此它是在进行内部的转发。
(这个参数就是@WebServlet上面的地址,跟之前是不一样的,有斜杠;而且和重定向不一样,它不能跨域(比如百度),只能写内部的)
req.getRequestDispatcher("/time").forward(req, resp);
现在,在登陆成功的时候,我们将请求转发给处理时间的Servlet,注意这里的路径规则和之前的不同,我们需要填写Servlet上指明的路径,并且请求转发只能转发到此应用程序内部的Servlet,不能转发给其他站点或是其他Web应用程序。
现在再次进行登陆操作,我们发现,返回结果为一个405
页面,证明了,我们的请求现在是被另一个Servlet进行处理,并且请求的信息全部被转交给另一个Servlet,由于此Servlet不支持POST请求(只写了doGet没有写doPost),因此返回405状态码。
那么也就是说,该请求包括请求参数也一起被传递了,那么我们可以尝试获取以下POST请求的参数。
现在我们给此Servlet添加POST请求处理,直接转交给Get请求处理:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
再次访问,成功得到结果,但是我们发现,浏览器只发起了一次请求,并没有再次请求新的URL,也就是说,这一次请求直接返回了请求转发后的处理结果。
那么,请求转发有什么好处呢?它可以携带数据
!
req.setAttribute("user", user);
req.getRequestDispatcher("/time").forward(req, resp);
User user = (User) req.getAttribute("user");
通过setAttribute
方法来给当前请求添加一个附加数据,在请求转发后,我们可以直接获取到该数据。
重定向属于2次请求,因此无法使用这种方式来传递数据,那么,如何在重定向之间传递数据呢?我们可以使用即将要介绍的ServletContext
对象。
最后总结,两者的区别为:
- 请求转发是一次请求,重定向是两次请求
- 请求转发地址栏不会发生改变, 重定向地址栏会发生改变
- 请求转发可以共享请求参数,重定向之后,就获取不了共享参数了
- 请求转发只能转发给内部的Servlet
9、了解ServletContext对象
ServletContext
全局唯一,它是属于整个Web应用程序的,我们可以通过getServletContext()
来获取到此对象。
此对象也能设置附加值:
(LoginServlet的doPost方法中)
ServletContext context = getServletContext();
context.setAttribute("test", "我是重定向之前的数据");
resp.sendRedirect("time");
(TimeServlet中doGet方法中;说明ServletContext是全局的)
System.out.println(getServletContext().getAttribute("test"));
结果为
null
我是重定向之前的数据
(注意上面这个null我们不用管它, 是因为页面一开始会自己去获取一次)
(不需要重定向也可以得到)
因为无论在哪里,无论什么时间,获取到的ServletContext始终是同一个对象,因此我们可以随时随地获取我们添加的属性。
它不仅仅可以用来进行数据传递,还可以做一些其他的事情,比如请求转发:
context.getRequestDispatcher("/time").forward(req, resp);
它还可以获取根目录下的资源文件(注意是webapp
根目录下的,不是resource
中的资源)
(注意这里的getResourceAsStream和mybatis提供的不一样!!)
(注意这里不需要加斜杠)
ServletContext context = getServletContext();
System.out.println(IOUtils.toString(context.getResourceAsStream("index.html"), StandardCharsets.UTF_8));
(这个IOUtils也可以获取到resource里面的东西)
(注意这个要加斜杠,表示是根目录)
System.out.println(IOUtils.resourceToString("/mybatis-config.xml", StandardCharsets.UTF_8));
10、初始化参数
初始化参数类似于初始化配置需要的一些值,比如我们的数据库连接相关信息,就可以通过初始化参数来给予Servlet,或是一些其他的配置项,也可以使用初始化参数来实现。
我们可以给一个Servlet
添加一些初始化参数:
@WebServlet(value = "/login", initParams = {
@WebInitParam(name = "test", value = "我是一个默认的初始化参数")
})
它也是以键值对形式保存的,我们可以直接通过Servlet
的getInitParameter
方法获取:
(在init方法中)
(getInitParameter表示直接根据名称获取)
System.out.println(getInitParameter("test"));
但是,这里的初始化参数仅仅是针对于此Servlet,我们也可以定义全局初始化参数,只需要在web.xml
编写即可:
<context-param>
<param-name>lbwnb</param-name>
<param-value>我是全局初始化参数</param-value>
</context-param>
然后就变成:
<?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_4_0.xsd"
version="4.0">
<context-param>
<param-name>lbwnb</param-name>
<param-value>我是全局初始化参数</param-value>
</context-param>
</web-app>
我们需要使用ServletContext
来读取全局初始化参数:
(自带了getServletContext()方法)
(比如在LoginServlet的init方法中)
ServletContext context = getServletContext();
System.out.println(context.getInitParameter("lbwnb"));
有关ServletContext其他的内容,我们需要完成后面内容的学习,才能理解。
Cookie
什么是Cookie?不是曲奇,它可以在浏览器中保存一些信息,并且在下次请求时,请求头中会携带这些信息。
我们可以编写一个测试用例来看看:
(在LoginServlet的doPost方法)
Cookie cookie = new Cookie("test", "yyds");
resp.addCookie(cookie);
resp.sendRedirect("time");
(可以看出,之后只要是访问localhost下的yyds应用程序下的任何页面,都会携带这个cookie进行请求)
(比如我们现在访问/file,会发现也携带了这个cookie去请求)
(在TimeServlet的doGet方法中)
(注意这里是req.getCookies!!!因为这里是在请求头里面的,而不是响应头里面的)
(现在这样,如果浏览器携带了cookie来访问这个网站,可以直接从请求头中获取cookie的信息)
if (req.getCookies() != null) {
for (Cookie cookie : req.getCookies()) {
System.out.println(cookie.getName() + ": " + cookie.getValue());
}
}
我们可以观察一下,在HttpServletResponse
中添加Cookie
之后,浏览器的响应头中会包含一个Set-Cookie
属性,同时,在重定向之后,我们的请求头中,会携带此Cookie作为一个属性,同时,我们可以直接通过HttpServletRequest
来快速获取有哪些Cookie信息。
还有这么神奇的事情吗?那么我们来看看,一个Cookie包含哪些信息:
- name - Cookie的名称,Cookie一旦创建,名称便不可更改
- value - Cookie的值,如果值为Unicode字符,需要为字符编码。如果为二进制数据,则需要使用BASE64编码
- maxAge - Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒后失效。如果为负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。如果为0,表示删除该Cookie。默认为-1。
- secure - 该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。
- path - Cookie的使用路径。如果设置为“/sessionWeb/”,则只有contextPath为“/sessionWeb”的程序(其他应用程序比如manager就无法访问这个Cookie了)可以访问该Cookie。如果设置为“/”,则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为“/”。
- domain - 可以访问该Cookie的域名(这里是localhost)。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”。
- comment - 该Cookie的用处说明,浏览器显示Cookie信息的时候显示该说明。
- version - Cookie使用的版本号。0表示遵循Netscape的Cookie规范,1表示遵循W3C的RFC 2109规范
我们发现,最关键的其实是name、value、maxAge、domain属性。
那么我们来尝试修改一下maxAge
来看看失效时间:
cookie.setMaxAge(20);
设定为20秒,我们可以直接看到,响应头为我们设定了20秒的过期时间。20秒内访问都会携带此Cookie,而超过20秒,Cookie消失。
既然了解了Cookie的作用,我们就可以通过使用Cookie来实现记住我功能,我们可以将用户名和密码全部保存在Cookie中,如果访问我们的首页时携带了这些Cookie,那么我们就可以直接为用户进行登陆,如果登陆成功则直接跳转到首页,如果登陆失败,则清理浏览器中的Cookie。
那么首先,我们先在前端页面的表单中添加一个勾选框:
<div>
<label>
<input type="checkbox" placeholder="记住我" name="remember-me">
记住我
</label>
</div>
(如果登录成功且勾选了记住我,在login的载荷中表单数据会显示remember-me为on)
(但我们只要判断这个表单数据里有没有remember-me这个表项即可,因为不勾选的话默认就是没有的)
接着,我们在登陆成功时进行判断,如果用户勾选了记住我,那么就讲Cookie存储到本地:
(在LoginServlet的doPost方法中)
if(map.containsKey("remember-me")){ //若勾选了勾选框,那么会此表单信息
Cookie cookie_username = new Cookie("username", username);
cookie_username.setMaxAge(30);
Cookie cookie_password = new Cookie("password", password);
cookie_password.setMaxAge(30);
resp.addCookie(cookie_username);
resp.addCookie(cookie_password);
}
resp.sendRedirect("time");
然后,我们修改一下默认的请求地址,现在一律通过http://localhost:8080/yyds**/login进行登陆**,那么我们需要添加GET请求的相关处理:
(LoginServlet中!)
(由于是直接输入地址来访问,因此是get请求)
(这样就把这两个工作给分开了,如果是get请求就给它这个页面,如果是post请求就是进行登录)
重要!!!(req.getRequestDispatcher(“/”).forward(req, resp);表示默认情况下还是转给默认的Servlet,因此请求转发,也就是说默认还是走index.html;但如果它携带了登录信息(即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; //直接返回!不然就走下面的去了!
} else {
Cookie cookie_username = new Cookie("username", username);
cookie_username.setMaxAge(0); // 登录失败,使cookie失效
Cookie cookie_password = new Cookie("password", password);
cookie_password.setMaxAge(0);
resp.addCookie(cookie_username);
resp.addCookie(cookie_password);
}
}
}
}
req.getRequestDispatcher("/").forward(req, resp); //正常情况还是转发给默认的Servlet帮我们返回静态页面
}
还要修改配置!!!
(如果不勾选记住我,登录成功后再访问/login,重新返回登录页面,需要重新登录;如果勾选记住我,登录成功后再访问/login,直接重定向跳转到/time)
现在,30秒内都不需要登陆,访问登陆页面后,会直接跳转到time页面。
现在已经离我们理想的页面越来越接近了,但是仍然有一个问题,就是我们的首页(/time),无论是否登陆,所有人都可以访问,那么,如何才可以实现只有登陆之后才能访问呢?这就需要用到 Session
了。
Filter
有了Session之后,我们就可以很好地控制用户的登陆验证了,只有授权的用户,才可以访问一些页面,但是我们需要一个一个去进行配置,还是太过复杂,能否一次性地过滤掉没有登录验证的用户呢?
过滤器相当于在所有访问前加了一堵墙,来自浏览器的所有访问请求都会首先经过过滤器,只有过滤器允许通过的请求,才可以顺利地到达对应的Servlet,而过滤器不允许的通过的请求,我们可以自由地进行控制是否进行重定向或是请求转发。并且过滤器可以添加很多个,就相当于添加了很多堵墙,我们的请求只有穿过层层阻碍,才能与Servlet相拥,像极了爱情。
添加一个过滤器非常简单,只需要实现Filter接口,并添加@WebFilter
注解即可:
@WebFilter("/*") //路径的匹配规则和Servlet一致,这里表示匹配(拦截)所有请求
public class TestFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}
}
这样我们就成功地添加了一个过滤器,那么添加一句打印语句看看,是否所有的请求都会经过此过滤器:
HttpServletRequest request = (HttpServletRequest) servletRequest;
System.out.println(request.getRequestURL());
(打开后,我们发现/login页面空白,即没有返回数据,即被拦截了;且状态为200)
我们发现,现在我们发起的所有请求,一律需要经过此过滤器,并且所有的请求都没有任何的响应内容。
那么如何让请求可以顺利地到达对应的Servlet,也就是说怎么让这个请求顺利通过呢?我们只需要在最后添加一句:
filterChain.doFilter(servletRequest, servletResponse);
那么这行代码是什么意思呢?
由于我们整个应用程序可能存在多个过滤器,那么这行代码的意思实际上是将此请求继续传递给下一个过滤器,当没有下一个过滤器时,才会到达对应的Servlet进行处理,我们可以再来创建一个过滤器看看效果:
@WebFilter("/*")
public class TestFilter2 implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("我是2号过滤器");
filterChain.doFilter(servletRequest, servletResponse);
}
}
由于过滤器的过滤顺序是按照类名的自然排序进行的,因此我们将第一个过滤器命名进行调整。
我们发现,在经过第一个过滤器之后,会继续前往第二个过滤器,只有两个过滤器全部经过之后,才会到达我们的Servlet中。
实际上,当doFilter
方法调用时,就会一直向下直到Servlet,在Servlet处理完成之后,又依次返回到最前面的Filter,类似于递归的结构,我们添加几个输出语句来判断一下:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("我是2号过滤器");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("我是2号过滤器,处理后");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("我是1号过滤器");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("我是1号过滤器,处理后");
}
最后验证我们的结论。
同Servlet一样,Filter也有对应的HttpFilter
专用类,它针对HTTP请求进行了专门处理,因此我们可以直接使用HttpFilter来编写:
(观察以下源码可以发现,我们要去重写的是下面那个方法,即以HttpServletRequest为参数的那个)
public abstract class HttpFilter extends GenericFilter {
private static final long serialVersionUID = 7478463438252262094L;
public HttpFilter() {
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
this.doFilter((HttpServletRequest)req, (HttpServletResponse)res, chain);
} else {
throw new ServletException("non-HTTP request or response");
}
}
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
chain.doFilter(req, res);
}
}
那么现在,我们就可以给我们的应用程序添加一个过滤器,用户在未登录情况下,只允许静态资源和登陆页面请求通过,登陆之后畅行无阻:
(注意我们重写的是HttpServletRequest那个)
(然后我们将之前写在TimeServlet的doGet方法中如果session中没有user就重定向到login的那段代码删掉了)
@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);
}
}
(现在,我们未登录的情况下去访问/time和/file都被直接重定向为/login)
现在,我们的页面已经基本完善为我们想要的样子了。
当然,可能跟着教程编写的项目比较乱,大家可以自己花费一点时间来重新编写一个Web应用程序,加深对之前讲解知识的理解。我们也会在之后安排一个编程实战进行深化练习。
Listener
监听器并不是我们学习的重点内容,那么什么是监听器呢?
如果我们希望,在应用程序加载的时候,或是Session创建的时候,亦或是在Request对象创建的时候进行一些操作,那么这个时候,我们就可以使用监听器来实现。
默认为我们提供了很多类型的监听器,我们这里就演示一下监听Session的创建即可:
@WebListener
public class TestListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("有一个Session被创建了");
}
}
(当我们访问网页的时候,就会有session被创建)
有关监听器相关内容,了解即可。
前面所有代码
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>demo</name>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<junit.version>5.8.2</junit.version>
</properties>
<dependencies>
<!-- <dependency>-->
<!-- <groupId>javax.servlet</groupId>-->
<!-- <artifactId>javax.servlet-api</artifactId>-->
<!-- <version>4.0.1</version>-->
<!-- <scope>provided</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
entity.User
@Data
public class User {
String username;
String password;
}
filter.MainFilter
@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);
}
}
mapper.UserMapper 接口
public interface UserMapper {
@Select("select * from users where username = #{username} and password = #{password}")
User getUser(@Param("username") String username, @Param("password") String password);
}
servlet.FileServlet
@WebServlet("/file")
@MultipartConfig
public class FileServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("image/png");
OutputStream outputStream = resp.getOutputStream();
InputStream inputStream = Resources.getResourceAsStream("icon.png");
//直接使用copy方法完成转换
IOUtils.copy(inputStream, outputStream);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try(FileOutputStream stream = new FileOutputStream("/Users/fieldxia/Documents/JAVA/learnServlet/test.png")){
Part part = req.getPart("test-file"); // 获取文件;参数为 表单中对应的名称name
IOUtils.copy(part.getInputStream(), stream);
resp.setContentType("text/html;charset=UTF-8"); // 因为还要返回数据来表示文件上传成功
resp.getWriter().write("文件上传成功!"); // 然后就可以往里面输入数据
}
}
}
servlet.LogServlet
@Log
@WebServlet(value = "/login", initParams = {
@WebInitParam(name = "test", value = "我是一个默认的初始化参数")
})
public class LoginServlet extends HttpServlet {
SqlSessionFactory factory;
@SneakyThrows
@Override
public void init() throws ServletException {
ServletContext context = getServletContext();
System.out.println(context.getInitParameter("lbwnb"));
factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
}
@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){
HttpSession session = req.getSession();
session.setAttribute("user", user);
resp.sendRedirect("time");
return; //直接返回
} else {
Cookie cookie_username = new Cookie("username", username);
cookie_username.setMaxAge(0);
Cookie cookie_password = new Cookie("password", password);
cookie_password.setMaxAge(0);
resp.addCookie(cookie_username);
resp.addCookie(cookie_password);
}
}
}
}
req.getRequestDispatcher("/").forward(req, resp); //正常情况还是转发给默认的Servlet帮我们返回静态页面
}
@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");
//登陆校验(待完善)
try (SqlSession sqlSession = factory.openSession(true)){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUser(username, password);
//判断用户是否登陆成功,若查询到信息则表示存在此用户
if(user != null){
if(map.containsKey("remember-me")){ //若勾选了勾选框,那么会此表单信息
Cookie cookie_username = new Cookie("username", username);
cookie_username.setMaxAge(30);
Cookie cookie_password = new Cookie("password", password);
cookie_password.setMaxAge(30);
resp.addCookie(cookie_username);
resp.addCookie(cookie_password);
}
HttpSession session = req.getSession();
session.setAttribute("user", user);
resp.sendRedirect("time");
} else {
resp.getWriter().write("登陆失败,请验证您的用户名或密码!");
}
}
} else {
resp.getWriter().write("错误,您的表单数据不完整!");
}
}
}
servlet.TimeServlet
@WebServlet("/time")
public class TimeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
// 接收get请求的参数
req.getParameterMap().forEach((k, v) -> {
System.out.println(k + ": " + Arrays.toString(v));
});
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String date = dateFormat.format(new Date());
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(date);
}
}
TestListener
@WebListener
public class TestListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("有一个Session被创建了");
}
}
resources/mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/book_manage"/>
<property name="username" value="root"/>
<property name="password" value="root123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.example.mapper.UserMapper"/>
</mappers>
</configuration>
webapp/WEB-INF/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_4_0.xsd"
version="4.0">
<context-param>
<param-name>lbwnb</param-name>
<param-value>我是全局初始化参数</param-value>
</context-param>
<session-config>
<session-timeout>1</session-timeout>
</session-config>
</web-app>
webapp/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="test.js"></script>
</head>
<body>
<h1>登录到系统</h1>
<form method="post" action="login">
<hr>
<div>
<label>
<input type="text" placeholder="用户名" name="username">
</label>
</div>
<div>
<label>
<input type="password" placeholder="密码" name="password">
</label>
</div>
<div>
<label>
<input type="checkbox" placeholder="记住我" name="remember-me">
记住我
</label>
</div>
<div>
<button>登录</button>
</div>
</form>
</body>
</html>
webapp/test.js
function updateTime() {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
document.getElementById("time").innerText = xhr.responseText
}
};
// 第二个参数是url
xhr.open('GET', 'time', true);
xhr.send();
}
了解JSP页面与加载规则
前面我们已经完成了整个Web应用程序生命周期中所有内容的学习,我们已经完全了解,如何编写一个Web应用程序,并放在Tomcat上部署运行,以及如何控制浏览器发来的请求,通过Session+Filter实现用户登陆验证,通过Cookie实现自动登陆等操作。到目前为止,我们已经具备编写一个完整Web网站的能力。
在之前的教程中,我们的前端静态页面并没有与后端相结合,我们前端页面所需的数据全部需要单独向后端发起请求获取,并动态进行内容填充,这是一种典型的前后端分离写法,前端只负责要数据和显示数据,后端只负责处理数据和提供数据,这也是现在更流行的一种写法,让前端开发者和后端开发者各尽其责,更加专一,这才是我们所希望的开发模式。
JSP并不是我们需要重点学习的内容,因为它已经过时了,使用JSP会导致前后端严重耦合,因此这里只做了解即可。
JSP其实就是一种模板引擎,那么何谓模板引擎呢?顾名思义,它就是一个模板,而模板需要我们填入数据,才可以变成一个页面,也就是说,我们可以直接在前端页面中直接填写数据,填写后生成一个最终的HTML页面返回给前端。
首先我们来创建一个新的项目,项目创建成功后,删除Java目录下的内容,只留下默认创建的jsp文件,我们发现,在webapp
目录中,存在一个index.jsp
文件,现在我们直接运行项目,会直接访问这个JSP页面。
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>
但是我们并没有编写对应的Servlet来解析啊,那么为什么这个JSP页面会被加载呢?
实际上,我们一开始提到的两个Tomcat默认的Servlet中,一个是用于请求静态资源(不然静态资源就不能加载了呀!),还有一个就是用于处理jsp的:
<!-- The mappings for the JSP servlet -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
那么,JSP和普通HTML页面有什么区别呢,我们发现它的语法和普通HTML页面几乎一致,我们可以直接在JSP中编写Java代码,并在页面加载的时候执行,我们随便找个地方插入:
<%
System.out.println("JSP页面被加载");
%>
我们发现,请求一次页面,页面就会加载一次,并执行我们填写的Java代码。也就是说,我们可以直接在此页面中执行Java代码来填充我们的数据,这样我们的页面就变成了一个动态页面,使用<%= %>
来填写一个值:
<h1><%= new Date() %></h1>
现在访问我们的网站,每次都会创建一个新的Date对象,因此每次访问获取的时间都不一样,我们的网站已经算是一个动态的网站的了。
虽然这样在一定程度上上为我们提供了便利,但是这样的写法相当于整个页面既要编写前端代码,也要编写后端代码,随着项目的扩大,整个页面会显得难以阅读,并且现在都是前后端开发人员职责非常明确的,如果要编写JSP页面,那就必须要招一个既会前端也会后端的程序员,这样显然会导致不必要的开销。
那么我们来研究一下,为什么JSP页面能够在加载的时候执行Java代码呢?
首先我们将此项目打包(右边maven的package),并在Tomcat服务端中运行(将war包放在tomcat的webapps目录下),生成了一个文件夹(在浏览器中访问后自动生成)并且可以正常访问(在浏览器中访问)。
我们现在看到work目录,我们发现这个里面多了一个index_jsp.java和index_jsp.class,那么这些东西是干嘛的呢,我们来反编译一下就啥都知道了:
public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase //继承自HttpServlet
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {
...
public void _jspService(final jakarta.servlet.http.HttpServletRequest request, final jakarta.servlet.http.HttpServletResponse response)
throws java.io.IOException, jakarta.servlet.ServletException {
if (!jakarta.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
final java.lang.String _jspx_method = request.getMethod();
if ("OPTIONS".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
return;
}
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSP 只允许 GET、POST 或 HEAD。Jasper 还允许 OPTIONS");
return;
}
}
final jakarta.servlet.jsp.PageContext pageContext;
jakarta.servlet.http.HttpSession session = null;
final jakarta.servlet.ServletContext application;
final jakarta.servlet.ServletConfig config;
jakarta.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
jakarta.servlet.jsp.JspWriter _jspx_out = null;
jakarta.servlet.jsp.PageContext _jspx_page_context = null;
try {
response.setContentType("text/html; charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
out.write("\n");
out.write("\n");
out.write("<!DOCTYPE html>\n");
out.write("<html>\n");
out.write("<head>\n");
out.write(" <title>JSP - Hello World</title>\n");
out.write("</head>\n");
out.write("<body>\n");
out.write("<h1>");
out.print( new Date() );
out.write("</h1>\n");
System.out.println("JSP页面被加载");
out.write("\n");
out.write("<br/>\n");
out.write("<a href=\"hello-servlet\">Hello Servlet</a>\n");
out.write("</body>\n");
out.write("</html>");
} catch (java.lang.Throwable t) {
if (!(t instanceof jakarta.servlet.jsp.SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
try {
if (response.isCommitted()) {
out.flush();
} else {
out.clearBuffer();
}
} catch (java.io.IOException e) {}
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
else throw new ServletException(t);
}
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
我们发现,它是继承自HttpJspBase类,我们可以反编译一下jasper.jar(它在tomcat的lib目录中(我们把它粘贴过来然后添加为库就可以反编译了))来看看:
package org.apache.jasper.runtime;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.jsp.HttpJspPage;
import java.io.IOException;
import org.apache.jasper.compiler.Localizer;
public abstract class HttpJspBase extends HttpServlet implements HttpJspPage {
private static final long serialVersionUID = 1L;
protected HttpJspBase() {
}
public final void init(ServletConfig config) throws ServletException {
super.init(config);
this.jspInit();
this._jspInit();
}
public String getServletInfo() {
return Localizer.getMessage("jsp.engine.info", new Object[]{"3.0"});
}
public final void destroy() {
this.jspDestroy();
this._jspDestroy();
}
public final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this._jspService(request, response);
}
public void jspInit() {
}
public void _jspInit() {
}
public void jspDestroy() {
}
protected void _jspDestroy() {
}
public abstract void _jspService(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException;
}
实际上,Tomcat在加载JSP页面时,会将其动态转换为一个java类并编译为class进行加载,而生成的Java类,正是一个Servlet的子类,而页面的内容全部被编译为输出字符串,这便是JSP的加载原理,因此,JSP本质上依然是一个Servlet!
如果同学们感兴趣的话,可以查阅一下其他相关的教程,本教程不再讲解此技术。