准备工作
Tomcat
Tomcat 就是一个典型的 Web 应用服务器软件,通过运行 Tomcat 服务器,我们就可以快速部署我们的 Web 项目,并交由 Tomcat 进行管理,我们只需要直接通过浏览器访问我们的项目即可。
安装Tomcat
下载地址:https://tomcat.apache.org/download-10.cgi
- 点击左侧的downloads,选择对应的版本
- 下载完成后,解压,我放到了
opt/
目录下
启动Tomcat
- 打开终端,执行命令
cd /opt/apache-tomcat-10.0.20/bin
,进入到tomcat的bin目录下 - 输入:
./startup.sh
回车- 如出现错误:“Permission denied” ,赋予超级管理员权限
sudo chmod 755 *.sh
- 如果出现乱码,说明编码格式配置有问题,打开
conf
文件夹,找到logging.properties
文件,将ConsoleHandler的默认编码格式修改为GBK编码格式:java.util.logging.ConsoleHandler.encoding = GBK
- 如出现错误:“Permission denied” ,赋予超级管理员权限
- 打开浏览器,输入网址 http://localhost:8080/,如果出现一只三角猫,表示tomcat安装成功
- 关闭Tomcat:在bin目录下,终端输入命令:
./shutdown.sh
Tomcat目录
代码语言:javascript复制$ tree -L 1
.
├── bin
├── conf
├── lib
├── logs
├── temp
├── webapps
└── work
- bin目录:所有可执行文件,包括启动和关闭服务器的脚本
- conf目录:服务器配置文件目录
- lib目录:Tomcat服务端运行的一些依赖
- logs目录:所有的日志信息都在这里
- temp目录:存放运行时产生的一些临时文件,不用关心
- work目录:工作目录,Tomcat会将jsp文件转换为java文件
- webapps目录:所有的Web项目都在这里,每个文件夹都是一个Web应用程序:
我们发现,官方已经给我们预设了一些项目了,访问后默认使用的项目为ROOT项目,也就是我们默认打开的网站。
Tomcat还自带管理页面,我们打开:http://localhost:8080/manager,提示需要用户名和密码,
需要先去conf/tomcat-users.xml
配置用户名和密码
<role rolename="manager-gui"/>
<user username="admin" password="password" roles="manager-gui"/>
现在再次打开管理页面,已经可以成功使用此用户进行登陆了。登录后,展示给我们的是一个图形化界面,我们可以快速预览当前服务器的一些信息,包括已经在运行的Web应用程序,甚至还可以查看当前的Web应用程序有没有出现内存泄露。还有一个虚拟主机管理页面,用于一台主机搭建多个Web站点
Maven创建Web项目
- 1、打开IDEA,新建一个项目,选择 Java Enterprise(社区版没有此选项)
- 2、项目模板选择Web应用程序
- 3、然后需要配置Web应用程序服务器,将前面下载的Tomcat服务器集成到IDEA中。
- 首先点击新建,然后设置Tomcat主目录即可,配置完成后,点击下一步即可,依赖项使用默认即可,然后点击完成,之后IDEA会自动帮助我们创建Maven项目。
- 4、创建完成后,直接点击右上角即可运行此项目了,但是我们发现,有一个Servlet页面不生效。因为 Tomcat 10 以上的版本比较新,Servlet API包名发生了一些变化
- 因此我们需要修改一下依赖
- 包名也需要全部从
javax
改为jakarta
,我们需要手动修改一下。
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
我们可以使用Maven 的 package 命令将项目直接打包为war包(默认),默认在项目的target目录下,然后放入webapp文件夹,就可以直接运行我们通过Java编写的Web应用程序了,访问路径为文件的名称。
Servlet 简介
Servlet 是 Server Applet 的缩写,译为“服务器端小程序”,是一种使用 Java 语言来开发动态网站的技术。
Servlet 是 Java EE 的一个标准,大部分的 Web 服务器都支持此标准,包括 Tomcat,就像之前的JDBC一样,由官方定义了一系列接口,而具体实现由我们来编写,最后交给Web服务器(如Tomcat)来运行我们编写的Servlet。
创建Servlet
使用注解配置
如何创建一个Servlet呢,只需要实现Servlet
类即可,并添加注解@WebServlet
来进行注册。
@WebServlet("/test")
public class TestServlet implements Servlet {
...//实现接口方法
}
现在就可以访问一下我们的页面:http://localhost:8080/xxx/test
使用web.xml配置
除了直接编写一个类,我们也可以在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,但是显然直接使用注解更加方便,因此之后我们一律使用注解进行开发。
Servlet生命周期
接着来看看,一个Servlet是如何运行的。首先我们需要了解,Servlet中的方法各自是在什么时候被调用的,我们先编写一个打印语句来看看
代码语言:javascript复制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");
}
}
我们首先启动一次服务器,然后访问我们定义的页面,然后再关闭服务器,得到如下的顺序:
代码语言:javascript复制我是构造方法!
我是init
我是service
我是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响应报文实体类封装。
那么我们来看看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
代码语言:javascript复制public abstract class GenericServlet implements Servlet {}
public abstract class HttpServlet extends GenericServlet {}
- HttpServlet 继承自 GenericServlet 类
- GenericServlet 实现了 Servlet 接口
Servlet
有一个直接实现抽象类GenericServlet
,这个类完善了配置文件读取和Servlet信息相关的的操作,但是依然没有去实现service方法
HttpServlet
,它是遵循HTTP协议的一种Servlet,继承自GenericServlet
,它根据HTTP协议的规则,完善了service方法。
因此只需要继承 HttpServlet 就可以编写我们的 Servlet 了,并且它已经帮助我们提前实现了一些操作,这样就会给我们省去很多的时间。
代码语言:javascript复制@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>");
}
}
@WebServlet注解
可以直接使用 WebServlet 注解来快速注册一个 Servlet
属性名 | 类型 | 描述 | 必需 |
---|---|---|---|
name | String | 指定 Servlet 的 name 属性。 如果没有显式指定,则取值为该 Servlet 的完全限定名,即包名 类名。 | 否 |
value | String[ ] | 该属性等价于 urlPatterns 属性,两者不能同时指定。 如果同时指定,通常是忽略 value 的取值。 | 是 |
urlPatterns | String[ ] | 代表当前Servlet的访问路径。 | 是 |
loadOnStartup | int | 指定 Servlet 的加载顺序。 | 否 |
initParams | WebInitParam[ ] | 指定一组 Servlet 初始化参数。 | 否 |
asyncSupported | boolean | 声明 Servlet 是否支持异步操作模式。 | 否 |
description | String | 指定该 Servlet 的描述信息。 | 否 |
displayName | String | 指定该 Servlet 的显示名。 | 否 |
@WebServlet(urlPatterns = "/test/*")
//可省略urlPatterns
@WebServlet("/test/*")
上面的路径表示,所有匹配/test/随便什么
的路径名称,都可以访问此Servlet
也可以进行某个扩展名称的匹配:
代码语言:javascript复制@WebServlet("*.js")
这样的话,获取任何以js结尾的文件,都会由我们自己定义的Servlet处理。
还可以为一个Servlet配置多个访问路径:
代码语言:javascript复制@WebServlet({"/test1", "/test2"})
接着看 loadOnStartup 属性,此属性决定了是否在Tomcat启动时就加载此Servlet,默认情况下,Servlet只有在被访问时才会加载,它的默认值为-1,表示不在启动时加载,我们可以将其修改为大于等于0的数,来开启启动时加载。并且数字的大小决定了此Servlet的启动优先级。
代码语言:javascript复制@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>");
}
}
POST请求完成登录
创建一个 Servlet,让其能够接收一个 POST 请求:
代码语言:javascript复制@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请求所携带的表单数据,我们可以直接将其遍历查看,浏览器发送了什么数据。
现在我们再来修改一下前端:
代码语言:javascript复制<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>
现在我们点击登录按钮,会自动向后台发送一个POST请求,请求地址为当前地址 /login,也就是我们上面编写的Servlet路径。
上传和下载文件
首先将icon.png放入到resource文件夹中,接着我们编写一个Servlet用于处理文件下载:
代码语言:javascript复制@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代码,我们可以引入一个工具库:
代码语言:javascript复制<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
使用此类库可以快速完成IO操作:
代码语言:javascript复制resp.setContentType("image/png");
OutputStream outputStream = resp.getOutputStream();
InputStream inputStream = Resources.getResourceAsStream("icon.png");
//直接使用copy方法完成转换
IOUtils.copy(inputStream, outputStream);
现在我们在前端页面添加一个链接,用于下载此文件:
代码语言:javascript复制<hr>
<a href="file" download="icon.png">点我下载高清资源</a>
下载文件搞定,那么如何上传一个文件呢?
首先我们编写前端部分:
代码语言:javascript复制<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代码:
代码语言:javascript复制@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请求,并在请求中完成动态更新:
代码语言:javascript复制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();
}
接着修改一下前端页面,添加一个时间显示区域:
代码语言:javascript复制<hr>
<div id="time"></div>
<br>
<button onclick="updateTime()">更新数据</button>
<script>
updateTime()
</script>
最后创建一个Servlet用于处理时间更新请求:
代码语言:javascript复制@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请求也能传递参数,这里做一下演示。
重定向与请求转发
当我们希望用户登录完成之后,直接跳转到网站的首页,那么这个时候,我们就可以使用重定向来完成。当浏览器收到一个重定向的响应时,会按照重定向响应给出的地址,再次向此地址发出请求。
实现重定向很简单,只需要调用一个方法即可,我们修改一下登陆成功后执行的代码:
代码语言:javascript复制resp.sendRedirect("time");
调用后,响应的状态码会被设置为302,并且响应头中添加了一个Location属性,此属性表示,需要重定向到哪一个网址。
接着来看请求转发,请求转发其实是一种服务器内部的跳转机制,我们知道,重定向会使得浏览器去重新请求一个页面,而请求转发则是服务器内部进行跳转,它的目的是,直接将本次请求转发给其他Servlet进行处理,并由其他Servlet来返回结果,因此它是在进行内部的转发。
代码语言:javascript复制req.getRequestDispatcher("/time").forward(req, resp);
现在,在登陆成功的时候,我们将请求转发给处理时间的Servlet,注意这里的路径规则和之前的不同,我们需要填写Servlet上指明的路径,并且请求转发只能转发到此应用程序内部的Servlet,不能转发给其他站点或是其他Web应用程序。
现在再次进行登陆操作,我们发现,返回结果为一个405页面,证明了,我们的请求现在是被另一个Servlet进行处理,并且请求的信息全部被转交给另一个Servlet,由于此Servlet不支持POST请求,因此返回405状态码。
那么也就是说,该请求包括请求参数也一起被传递了,那么我们可以尝试获取以下POST请求的参数。
现在我们给此Servlet添加POST请求处理,直接转交给Get请求处理:
代码语言:javascript复制@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
再次访问,成功得到结果,但是我们发现,浏览器只发起了一次请求,并没有再次请求新的URL,也就是说,这一次请求直接返回了请求转发后的处理结果。
那么,请求转发有什么好处呢?它可以携带数据!
代码语言:javascript复制req.setAttribute("test", "我是请求转发前的数据");
req.getRequestDispatcher("/time").forward(req, resp);
代码语言:javascript复制System.out.println(req.getAttribute("test"));
通过setAttribute
方法来给当前请求添加一个附加数据,在请求转发后,我们可以直接获取到该数据。
重定向属于2次请求,因此无法使用这种方式来传递数据,那么,如何在重定向之间传递数据呢?我们可以使用即将要介绍的ServletContext对象。
最后总结,两者的区别为:
- 请求转发是一次请求,重定向是两次请求
- 请求转发地址栏不会发生改变, 重定向地址栏会发生改变
- 请求转发可以共享请求参数 ,重定向之后,就获取不了共享参数了
- 请求转发只能转发给内部的Servlet
ServletContext对象
ServletContext全局唯一,它是属于整个Web应用程序的,我们可以通过getServletContext()
来获取到此对象。
此对象也能设置附加值:
代码语言:javascript复制ServletContext context = getServletContext();
context.setAttribute("test", "我是重定向之前的数据");
resp.sendRedirect("time");
代码语言:javascript复制System.out.println(getServletContext().getAttribute("test"));
因为无论在哪里,无论什么时间,获取到的ServletContext始终是同一个对象,因此我们可以随时随地获取我们添加的属性。
它不仅仅可以用来进行数据传递,还可以做一些其他的事情,比如请求转发:
代码语言:javascript复制context.getRequestDispatcher("/time").forward(req, resp);
它还可以获取根目录下的资源文件(注意是webapp根目录下的,不是resource中的资源)
初始化参数
初始化参数类似于初始化配置需要的一些值,比如我们的数据库连接相关信息,就可以通过初始化参数来给予Servlet,或是一些其他的配置项,也可以使用初始化参数来实现。
我们可以给一个Servlet添加一些初始化参数:
代码语言:javascript复制@WebServlet(value = "/login", initParams = {
@WebInitParam(name = "test", value = "我是一个默认的初始化参数")
})
它也是以键值对形式保存的,我们可以直接通过Servlet的getInitParameter
方法获取:
System.out.println(getInitParameter("test"));
但是,这里的初始化参数仅仅是针对于此Servlet,我们也可以定义全局初始化参数,只需要在web.xml编写即可:
代码语言:javascript复制<context-param>
<param-name>lbwnb</param-name>
<param-value>我是全局初始化参数</param-value>
</context-param>
我们需要使用ServletContext来读取全局初始化参数:
代码语言:javascript复制ServletContext context = getServletContext();
System.out.println(context.getInitParameter("lbwnb"));
参考
- 重温Servlet,2020年了,它还有必要学吗?
- Servlet教程 (biancheng.net)