前言
现如今,随着人们生活物质的急剧提高,人们生活场景逐渐丰富,许多的基础设施都已经与科学技术融为一体。
从最初的PC互联网时代,到现今几乎人手一台手机的移动互联网时代,科学技术的发展与丰富多样的应用程序造就了如今方便快捷的生活方式。
终端机器的增加与数据量的极大丰富,越来越多的数据会逐渐的往服务器端转移,或许在可预见的时间内,以后的所有应用程序主体都会被放置在服务端上,客户端仅仅只作为一个显示与交互。
往后的应用程序应当基本上都会归属于 Web 应用程序。绝大多数的业务逻辑与数据存储都会放到后端进行处理。
因此,我们很有必要了解一下后端开发的一些知识。
本文主要针对 Java Web 后端编程进行一些讲解,核心内容就是对 Servlet 的介绍与使用讲解。
Web 应用体系架构
Web 应用程序:指的是通过网络通信进行访问的应用程序。
Web 应用程序通常由 前端 和 后端 两部分组成。
- 前端:主要指的就是浏览器端(即 HTML,CSS 和 JavaScript 等)以及客户端编程内容。
- 后端:主要指的就是 Web 组件内容(比如 Servlet,JSP,Filter 等),Web 组件通常都交由 Web 服务器进行调用,并且通过 HTTP 进行请求与响应。
当前,Web 应用软件架构主要是 C/S架构 和 B/S架构。
C/S架构:即 Client-Server(客户端-服务器)。
B/S架构:即 Browser-Server(浏览器-服务器)。
使用 C/S架构,那么客户端程序就需要我们自己手动进行编写。
使用 B/S架构,客户端程序就是浏览器,因此客户端就无须重新编写个程序了,我们只需关注后端业务就行了。
可以看到,B/S架构 相对于 C/S架构 来说,会更加简单与通用,因此其越来越成为目前最流行的软件架构。
CGI vs Servlet
Web 资源可以分为 静态资源 和 动态资源,最开始的时候,后端响应动态资源都是采用 CGI(Common Gateway Interface)(通用网关接口)进行编程,依据 CGI 的标准,编写外部扩展程序,Web 服务器就可以新建进程调用该外部扩展程序,并传递 HTTP 请求,如下图所示:
CGI 技术对 每个请求 都会创建一个 新进程 进行响应,因此,其资源占用高,效率低。
而对于 Servlet 来说,Web 服务器对 每个请求 都是通过创建 新线程 进行响应,相对于 CGI 来说,线程比进程有更多优势,比如共享同一块内存,更加轻量,线程间通讯更加方便···如下图所示:
Servlet 相对于 CGI 来说,具备如下几大优势:
- 性能:基于线程响应请求而不是进程。
- 可移植性:Sevlet 基于 Java 语言编写,而 CGI 程序使用平台相关语言,比如 C/C++,Perl。
-
健壮性:编写 Servlet,我们无须关心内存泄露,垃圾回收等,全部交由 JVM 负责管理。
...
Servlet 简介
A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web clients, usually across HTTP, the HyperText Transfer Protocol.
从 Oracle 的官方文档中可以看到:Servlet 就是运行在 Web 服务器内的一个小型 Java 程序,可以对 Web 客户端发送的 HTTP 请求进行响应和处理。
更具体来说,JavaEE 为我们提供了一个接口:Servlet
对于任何实现了该接口的类,我们都可以将其看作是一个 Servlet。
Servlet 生命周期
首先来看下 Servlet 定义的接口方法:
与 Servlet 生命周期有关的方法为:
通常情况下,Servlet 由 Web 容器(也即 Web 服务器)进行管理,Web 容器在接收到请求时,会创建相应的 Servlet 实例进行响应,Servlet 的生命从这一刻便开启了。
具体来说,Servlet 的生命周期包含四个阶段:
- 在 Web 容器启动或者第一次接收到请求时,Web 容器将加载对应 Servlet 类并将其放入到 Servlet 实例池。
- 在 Servlet 实例化后,Web 容器将调用其
init
方法,让该 Servlet 实例可以进行一些初始化工作。 - Web 容器在 Servlet 初始化完成后,会调用其
service
方法,让该 Servlet 处理并响应当前客户端请求。 - 在 Web 容器关闭时,会调用 Servlet 的
destroy
方法,让该 Servlet 进行资源释放操作。
入门案例
下面举个简单的例子:让浏览器访问http://localhost/hello
时,后端类MyServlet
返回一个Hello Servlet
字符串给到浏览器进行显示。
具体操作如下:
- 使用 Maven 新建一个 web app工程:
- 在 pom.xml 中导入 Servlet 依赖:
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
- IDEA 默认创建的 web 工程目录配置不全,因此我们需要手动进行补全:
补全源代码目录:在 src/main/ 目录下,创建文件夹 java - 右键该文件夹 - Mark Directory as - Sources Root。
补全源代码资源目录:在 src/main/ 目录下,创建文件夹 resources - 右键该文件夹 - Mark Directory as - Resources Root。
补全测试代码目录:在 src/ 目录下,创建文件夹 test/java - 右键该文件夹 - Mark Directory as - Test Sources Root。
补全测试代码资源目录:在 src/test 目录下,创建文件夹 resources - 右键该文件夹 - Mark Directory as - Test Resources Root。
- 创建类
MyServlet
,实现Servlet
接口:
public class MyServlect implements Servlet {
...
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
res.setContentType("text/html;charset=UTF-8");
PrintWriter writer = res.getWriter();
writer.print("<h1>Hello Servlet</h1>");
}
...
}
- 在 webapp/WEB-INF/web.xml 中配置
MyServlet
及其映射地址:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<!--配置Servlet-->
<servlet>
<!--配置Servlet名称-->
<servlet-name>myServlet</servlet-name>
<!--Servlect类全限定名-->
<servlet-class>com.yn.MyServlect</servlet-class>
</servlet>
<!--配置Servlect映射-->
<servlet-mapping>
<!--映射的具体Servlet名称-->
<servlet-name>myServlet</servlet-name>
<!--映射路径-->
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
- 配置 Tomcat 服务器:
- 运行项目,此时浏览器输入:localhost:8080/hello,就可以看到输出了。
Servlet 执行模型
一个完整的网络请求与响应的过程如下图所示:
具体来说:
- 客户端发送一个请求时,Web 容器就会加载对应 Servlet 到 Servlet 容器池中,并调用其
init
方法,完成 Servlet 的初始化工作; - 完成初始化后,Web 容器就会创建一条新的线程,并调用其
service
方法,同时新建一个请求和响应对象(ServletRequest req,ServletResponse res
)作为参数; - 后续客户端再次请求该 Servlet 时,由于 Servlet 已存在于内存中,故无须进行加载与初始化,而是直接创建新的请求和响应对象,并开启一条新线程调用其
service
方法; - 当 Web 容器即将关闭时,会调用 Servlet 的
destroy
方法,让 Servlet 做一些资源释放操作。
以上,便是 Servlet 的整个执行模型。
可以看到,对于 Servlet 来说,默认情况下,Web 容器对相同类别的 Servlet ,在内存中只维持一个(即 Servlet 保持单例),且只有在第一次创建 Servlet 时,才会调用init
方法。只有在 Web 容器退出时,才会调用destroy
方法。而后续的请求都是直接在新线程中调用其service
方法,并且每次都会创建新的请求对象和响应对象作为参数传递给service
方法。
注:从 Servlet 执行模型可以看出,Servlet 内部存在线程安全问题(特指service
方法)。因此,如果存在共享资源,需要考虑下线程同步,但 Web 应用应当极力避免采用锁同步操作(如synchronized
),因为这样做,在高并发环境下,每次只能响应一个请求,这是绝对无法允许的,所以,能尽量避免共享资源就尽量避免。
Servlet 继承体系
Servlet 包含很多接口方法,在实际项目中,很多时候我们不需要对所有方法进行覆写(通常只需覆写service
方法),因此,直接实现 Servlet 接口会让代码变得臃肿冗余。
通常我们都会使用 适配器模式 空实现接口方法,后续创建真正的业务类就可以直接通过继承我们自定义的适配器类,并选择覆写所需要的方法即可。
其实这个适配工作,Servlet 文档已经为我们提供了,即:GenericServlet
和 HttpServlet
。
查看 Servlet 继承体系,如下图所示:
简单看下GenericServlet
源码:
public abstract class GenericServlet
implements Servlet, ServletConfig, java.io.Serializable
{
...
public void destroy() {
}
...
public void init() throws ServletException {
}
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
...
}
可以看出,GenericServlet
其实就是对 Servlet 的适配器类,其中大部分接口方法都进行空操作,只抽象出service
,强制子类进行覆写。
再来看下HttpServlet
的源码:
public abstract class HttpServlet extends GenericServlet
{
...
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
...
doGet(req, resp);
...
} else if (method.equals(METHOD_HEAD)) {
...
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
...
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
...
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
...
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
service(request, response);
}
}
...
}
可以看到,HttpServlet
内部主要做的就是对 HTTP 请求方法进行划分,依据具体请求方法将请求重定向到具体方法进行处理,这样的实现方式可以让我们可以更加细致地对具体请求方法进行单独处理。
这里有一点还需要注意的是:HttpServlet
的service
方法的参数为HttpServletRequest
和HttpServletResponse
,其将 Servlet 的service
方法的参数ServletRequest
和ServletResponse
进行了强转,提供了更加强大的请求处理和响应功能。
综上,后续进行 Servlet 的开发,建议直接继承 HttpServlet。
Web 组件跳转
Java Web 组件包括 Servlet,JSP,Filter 等,有时组件间需要进行通信,则可以采用组件跳转方式。
Web 组件之间的跳转方式可以分为如下 3 种:
- 请求转发(forward):又称为 直接转发方式,客户端发送一个请求,服务端直接将该请求转发到另一个 Servlet,如下图所示:
对应代码实现:
request.getRequestDispatcher(path).forward(request, response);
特点:
- 一次请求:客户端只发送一次请求,客户端网址不会改变。
- 响应结果:由 BServlet 负责响应。
-
资源共享:两个 Web 组件共享请求资源,即通过
request.setAttribute
设置的资源可以在多个 Web 组件中进行共享。 - 可以访问 WEB-INF 中的资源:WEB-INF 文件夹是 Java Web 应用默认的 安全目录,位于此目录的资源无法直接被浏览器进行请求,只能在服务器端通过请求转发进行间接访问(比如:服务器 Servlet 访问 WEB-INF 下的 JSP 资源目录,并将内容转发给浏览器)。
- 不支持跨域访问:请求转发只能在同域(协议,域名,端口均相同)间进行。
- 请求包含(include):响应包含资源(如 Servlet,JSP页面,HTML文件)内容,如下图所示:
请求包含即客户端请求的 Servlet 响应包含有另一个 Servlet 的响应内容。
对应代码实现:
request.getRequestDispatcher(path).include(request, response);
特点:
- 一次请求:客户端只发送一次请求,客户端地址不会改变。
- 响应结果:响应结果由 AServlet 负责返回,结果包含有 AServlet 和 BServlet 两部分响应内容。
-
资源共享:两个 Web 组件共享请求资源,即通过
request.setAttribute
设置的资源可以在多个 Web 组件中进行共享。 - 不支持跨域访问
- 重定向(redirect):又称为 间接转发方式,客户端第一次请求时,服务端下发重定向请求(响应携带新地址),客户端接收到响应后,再次请求新地址,如下图所示:
对应代码实现:
response.sendRedirect(String location);
特点:
- 客户端累计发送两次请求,浏览器地址栏会改变。
- 其模式为:请求 - 响应(重定向)- 请求 - 响应。
- 两次请求没有直接关系,无法进行资源共享。
- 可以进行跨域访问。
Filter(过滤器)
- 过滤器(Filter):可以对请求进行预处理和对响应进行后置处理的对象。
过滤器主要用于过滤一些任务,比如转换,日志,压缩,加密解密,输入验证等等。
过滤器是可插拔的,其入口点在web.xml
文件中配置,并且只要在web.xml
中移除其配置,无须更改其他地方,该过滤器就会自动被移除掉。
过滤器的执行模型如下图所示:
官方提供的接口为:Filter
,示例代码如下:
- 首先编写一个过滤器
MyFilter
实现过滤器接口Filter
:
public class MyFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.print("Filter is inovked before");
// 转发到过滤链的下一个Filter,若无,则转发到对应资源
chain.doFilter(request,response);
writer.print("Filter is invoked after");
}
public void destroy() {
}
}
- 然后在
web.xml
中配置我们定义的过滤器:
<web-app>
<!--配置Filter-->
<filter>
<filter-name>MyFilter</filter-name>
<filter-class>com.yn.filter.MyFilter</filter-class>
</filter>
<!--配置Filter映射-->
<filter-mapping>
<filter-name>MyFilter</filter-name>
<!--拦截所有请求-->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
现在,无论我们访问哪个资源,都会被我们自定义的过滤器MyFilter
拦截到。
注解开发
入门案例中采用 xml 配置的方式配置 Servlet 和 Servlet 路由映射,其配置还是相对繁琐的。因此,Servlet 3.0 版本为我们提供了更加方便的配置方法:注解。
下面我们主要针对 Servlet 和 Filter 的相关注解进行讲解:
-
Servlet 注解配置:
@WebServlet
比如,像上面入门案例,我们把web.xml
中的<servlet>
和<servlet-mapping>
标签去除掉,然后在源码中直接使用注解@WebServlet
进行配置:
@WebService("/hello")
public class MyServlect implements Servlet {
...
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
res.setContentType("text/html;charset=UTF-8");
PrintWriter writer = res.getWriter();
writer.print("<h1>Hello Servlet</h1>");
}
...
}
可以看到,使用注解配置 Servlet 比使用 xml 配置方便快捷了许多。
注:使用注解开发甚至连web.xml
文件都不需要了。
下面对注解WebServlet
进行讲解:
WebServlet
注解的各个属性含义如下:
Attribute | Description |
---|---|
name | Servlet 名称 |
value | URL 路由映射 |
urlPatterns | URL 路由映射 |
loadOnStartup | 启动加载配置 |
initParams | Servlet 初始参数配置 |
asyncSupported | Servlet 支持异步操作配置 |
small | 配置小图标 |
largeIcon | 配置大图标 |
description | Servlet 描述 |
displayName | Servlet 显示名称 |
其中,最重要的属性就是urlPatterns
,可以为 Servlet 配置一个或多个路由映射。
注:value
和urlPatterns
效果等同,使用value
配置更加简洁。
-
Filter 注解配置:
@WebFilter
比如,像上面过滤器例子,我们把web.xml
中的<filter>
和<filter-mapping>
标签去除掉,然后在源码中直接使用注解@WebFilter
进行配置:
@WebFilter("/*")
public class MyFilter implements Filter {
...
}
下面对WebFilter
进行讲解:
Attribute | Description |
---|---|
filterName | 过滤器名称 |
value | URL 路由映射 |
urlPatterns | URL 路由映射 |
dispatcherTypes | 指定调度器(Request/Response)类型 |
servletNames | 提供 Servlet 名称(数组) |
displayName | 过滤器名称 |
description | 过滤器描述 |
initParams | 过滤器初始参数配置 |
asyncSupported | 过滤器支持异步操作配置 |
smallIcon | 配置小图标 |
largeIcon | 配置大图标 |
WebFilter
和WebServlet
注解的相关属性几乎一致。
其他
-
中文乱码问题:tomcat 8 之前, tomcat 服务器在接收请求时,默认采用的编码方式为 ISO-8859-1,该编码向下兼容 ASCII,是单字节编码,故不支持中文(两个字节),此时:
- 对于 Get 请求,参数位于请求行,需要先将 ISO-8859-1 的字符串进行解码,再编码成 UTF-8 格式:
String name = request.getParameter("name"); byte[] data = name.getBytes("ISO-8859-1"); name = new String(data,"UTF-8");
注:在 tomcat 8 以后,统一采用 UTF-8 格式接收请求,此时就无须进行编码转换了。
- 对于 Post 请求,参数位于请求体,请求体编码由请求头 Content-Type 决定,官方提供了相关 api 可以自动根据请求体的解码方式解析出 post body 内容,解决乱码问题:
request.setCharacterEncoding("UTF-8");
注:
setCharacterEncoding
方法必须在读取请求参数(getParameter
)或者读取输入流(getReader
)之前进行调用,否则没有效果。
乱码终极解决方案:按上述分析,对于 Get 请求,tomcat 8 之后不会存在乱码(前提:请求页面使用的是 UTF-8 编码)。对于 Post 请求,使用setCharacterEncoding
即可,为了统一设置所有 Servlet 编码,新建一个过滤器 Filter 设置编码最为方便:
@WebFilter("/*")
public class EncodingFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding("utf-8");
chain.doFilter(request,response);
}
public void destroy() {
}
}