Servlet
前面我们已经完成了基本的环境搭建,那么现在我们就可以开始来了解我们的第一个重要类——Servlet。
它是Java EE的一个标准,大部分的Web服务器都支持此标准,包括Tomcat,就像之前的JDBC一样,由官方定义了一系列接口,而具体实现由我们来编写,最后交给Web服务器(如Tomcat)来运行我们编写的Servlet。
那么,它能做什么呢?我们可以通过实现Servlet来进行动态网页响应,使用Servlet,不再是直接由Tomcat服务器发送我们编写好的静态网页内容(HTML文件),而是由我们通过Java代码进行动态拼接的结果,它能够很好地实现动态网页的返回。
当然,Servlet并不是专用于HTTP协议通信,也可以用于其他的通信,但是一般都是用于HTTP。
创建Servlet
那么如何创建一个Servlet呢,非常简单,我们只需要实现Servlet
类即可,并添加注解@WebServlet
来进行注册。
我们现在就可以去访问一下我们的页面:http://localhost:8080/test/test
我们发现,直接访问此页面是没有任何内容的,这是因为我们还没有为该请求方法编写实现,这里先不做讲解,后面我们会对浏览器的请求处理做详细的介绍。
除了直接编写一个类,我们也可以在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目录下,来测试一下是否能直接访问。
探究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 的垃圾回收器进行垃圾回收的。
现在我们发现,实际上在Web应用程序运行时,每当浏览器向服务器发起一个请求时,都会创建一个线程执行一次service
方法,来让我们处理用户的请求,并将结果响应给用户。
我们发现service
方法中,还有两个参数,ServletRequest
和ServletResponse
,实际上,用户发起的HTTP请求,就被Tomcat服务器封装为了一个ServletRequest
对象,我们得到是其实是Tomcat服务器帮助我们创建的一个实现类,HTTP请求报文中的所有内容,都可以从ServletRequest
对象中获取,同理,ServletResponse
就是我们需要返回给浏览器的HTTP响应报文实体类封装。
先把lombok的日志导入下
那么我们来看看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));
}
}
我们发现,整个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帮助我们生成的一个默认响应头。
因此,实际上整个流程就已经很清晰明了了。
解读和使用HttpServlet
前面我们已经学习了如何创建、注册和使用Servlet,那么我们继续来深入学习Servlet接口的一些实现类。
首先Servlet
有一个直接实现抽象类GenericServlet
,那么我们来看看此类做了什么事情。
我们发现,这个类完善了配置文件读取和Servlet信息相关的的操作,但是依然没有去实现service方法,因此此类仅仅是用于完善一个Servlet的基本操作,那么我们接着来看HttpServlet
,它是遵循HTTP协议的一种Servlet,继承自GenericServlet
,它根据HTTP协议的规则,完善了service方法。
在阅读了HttpServlet源码之后,我们发现,其实我们只需要继承HttpServlet来编写我们的Servlet就可以了,并且它已经帮助我们提前实现了一些操作,这样就会给我们省去很多的时间。
@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>");
}
}
现在,我们只需要重写对应的请求方式,就可以快速完成Servlet的编写。
doget 方法需要重写
@WebServlet注解详解
我们接着来看WebServlet注解,我们前面已经得知,可以直接使用此注解来快速注册一个Servlet,那么我们来想细看看此注解还有什么其他的玩法。
首先name属性就是Servlet名称,而urlPatterns和value实际上是同样功能,就是代表当前Servlet的访问路径,它不仅仅可以是一个固定值,还可以进行通配符匹配:
上面的路径表示,所有匹配/test/随便什么
的路径名称,都可以访问此Servlet,我们可以在浏览器中尝试一下。
也可以进行某个扩展名称的匹配:
这样的话,获取任何以js结尾的文件,都会由我们自己定义的Servlet处理。
那么如果我们的路径为/
呢?
此路径和Tomcat默认为我们提供的Servlet冲突,会直接替换掉默认的,而使用我们的,此路径的意思为,如果没有找到匹配当前访问路径的Servlet,那么久会使用此Servlet进行处理。
我们还可以为一个Servlet配置多个访问路径:
我们接着来看loadOnStartup属性,此属性决定了是否在Tomcat启动时就加载此Servlet,默认情况下,Servlet只有在被访问时才会加载,它的默认值为-1,表示不在启动时加载,我们可以将其修改为大于等于0的数,来开启启动时加载。并且数字的大小决定了此Servlet的启动优先级。
@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的一些基本配置
使用POST请求完成登陆
我们前面已经了解了如何使用Servlet来处理HTTP请求,那么现在,我们就结合前端,来实现一下登陆操作。
我们需要修改一下我们的Servlet,现在我们要让其能够接收一个POST请求:
@Log
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@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>
通过修改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的依赖和配置文件,创建一个表,用于存放我们用户的账号和密码。
<?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来进行用户信息查询:
public interface UserMapper {
@Select("select * from users where username = #{username} and password = #{password}")
User getUser(@Param("username") String username, @Param("password") String 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("登陆失败,请验证您的用户名或密码!");
}
}
现在再去浏览器上进行测试吧!
注册界面其实是同理的,这里就不多做讲解了。
有问题的话 可以先 clean 再 install , 再重启服务器
上传和下载文件
首先我们来看看比较简单的下载文件,首先将我们的icon.png放入到resource文件夹中,接着我们编写一个Servlet用于处理文件下载:
@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);
现在我们在前端页面添加一个链接,用于下载此文件:
下载文件搞定,那么如何上传一个文件呢?
首先我们编写前端部分:
<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代码:
@MultipartConfig
@WebServlet("/file")
public class FileServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try(FileOutputStream stream = new FileOutputStream("/Users/nagocoler/Documents/IdeaProjects/WebTest/test.png")){
Part part = req.getPart("test-file");
IOUtils.copy(part.getInputStream(), stream);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("文件上传成功!");
}
}
}
注意,必须添加@MultipartConfig
注解来表示此Servlet用于处理文件上传请求。
现在我们再运行服务器,并将我们刚才下载的文件又上传给服务端。
使用XHR请求数据
现在我们希望,网页中的部分内容,可以动态显示,比如网页上有一个时间,旁边有一个按钮,点击按钮就可以刷新当前时间。
这个时候就需要我们在网页展示时向后端发起请求了,并根据后端响应的结果,动态地更新页面中的内容,要实现此功能,就需要用到JavaScript来帮助我们,首先在js中编写我们的XHR请求,并在请求中完成动态更新:
function updateTime() {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
document.getElementById("time").innerText = xhr.responseText
}
};
xhr.open('GET', 'time', true);
xhr.send();
}
接着修改一下前端页面,添加一个时间显示区域:
<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);
}
}
现在点击按钮就可以更新了。
GET请求也能传递参数,这里做一下演示。
清理缓存
重定向与请求转发
当我们希望用户登录完成之后,直接跳转到网站的首页,那么这个时候,我们就可以使用重定向来完成。当浏览器收到一个重定向的响应时,会按照重定向响应给出的地址,再次向此地址发出请求。
实现重定向很简单,只需要调用一个方法即可,我们修改一下登陆成功后执行的代码:
调用后,响应的状态码会被设置为302,并且响应头中添加了一个Location属性,此属性表示,需要重定向到哪一个网址。
现在,如果我们成功登陆,那么服务器会发送给我们一个重定向响应,这时,我们的浏览器会去重新请求另一个网址。这样,我们在登陆成功之后,就可以直接帮助用户跳转到用户首页了。
那么我们接着来看请求转发,请求转发其实是一种服务器内部的跳转机制,我们知道,重定向会使得浏览器去重新请求一个页面,而请求转发则是服务器内部进行跳转,它的目的是,直接将本次请求转发给其他Servlet进行处理,并由其他Servlet来返回结果,因此它是在进行内部的转发。
现在,在登陆成功的时候,我们将请求转发给处理时间的Servlet,注意这里的路径规则和之前的不同,我们需要填写Servlet上指明的路径,并且请求转发只能转发到此应用程序内部的Servlet,不能转发给其他站点或是其他Web应用程序。
现在再次进行登陆操作,我们发现,返回结果为一个405页面,证明了,我们的请求现在是被另一个Servlet进行处理,并且请求的信息全部被转交给另一个Servlet,由于此Servlet不支持POST请求,因此返回405状态码。
那么也就是说,该请求包括请求参数也一起被传递了,那么我们可以尝试获取以下POST请求的参数。
现在我们给此Servlet添加POST请求处理,直接转交给Get请求处理:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
再次访问,成功得到结果,但是我们发现,浏览器只发起了一次请求,并没有再次请求新的URL,也就是说,这一次请求直接返回了请求转发后的处理结果。
那么,请求转发有什么好处呢?它可以携带数据!
通过setAttribute
方法来给当前请求添加一个附加数据,在请求转发后,我们可以直接获取到该数据。
重定向属于2次请求,因此无法使用这种方式来传递数据,那么,如何在重定向之间传递数据呢?我们可以使用即将要介绍的ServletContext对象。
最后总结,两者的区别为:
- 请求转发是一次请求,重定向是两次请求
- 请求转发地址栏不会发生改变, 重定向地址栏会发生改变
- 请求转发可以共享请求参数 ,重定向之后,就获取不了共享参数了
- 请求转发只能转发给内部的Servlet
了解ServletContext对象
ServletContext全局唯一,它是属于整个Web应用程序的,我们可以通过getServletContext()
来获取到此对象。
此对象也能设置附加值:
ServletContext context = getServletContext();
context.setAttribute("test", "我是重定向之前的数据");
resp.sendRedirect("time");
因为无论在哪里,无论什么时间,获取到的ServletContext始终是同一个对象,因此我们可以随时随地获取我们添加的属性。
它不仅仅可以用来进行数据传递,还可以做一些其他的事情,比如请求转发:
它还可以获取根目录下的资源文件(注意是webapp根目录下的,不是resource中的资源)
初始化参数
初始化参数类似于初始化配置需要的一些值,比如我们的数据库连接相关信息,就可以通过初始化参数来给予Servlet,或是一些其他的配置项,也可以使用初始化参数来实现。
我们可以给一个Servlet添加一些初始化参数:
@WebServlet(value = "/login", initParams = {
@WebInitParam(name = "test", value = "我是一个默认的初始化参数")
})
它也是以键值对形式保存的,我们可以直接通过Servlet的getInitParameter
方法获取:
但是,这里的初始化参数仅仅是针对于此Servlet,我们也可以定义全局初始化参数,只需要在web.xml编写即可:
<context-param>
<param-name>lbwnb</param-name>
<param-value>我是全局初始化参数</param-value>
</context-param>
我们需要使用ServletContext来读取全局初始化参数:
ServletContext context = getServletContext();
System.out.println(context.getInitParameter("lbwnb"));
有关ServletContext其他的内容,其他内容的学习,才能理解。