Web请求处理的核心类--Spring的轮子Servlet

除了Http/Https、WebSocket,其它的应用层协议,包括私有协议,都不被Tomcat直接支持
需要自定义Servlet类扩展所需协议,基于TCP协议的应用层私有协议,更推荐使用Netty进行定制
下文中,Web请求,指代Http/Https请求
篇幅有限,所有Http请求外的话题均不在下文讨论范围内
Spring中Servlet相关的逻辑太过广泛,本文仅简述DispatcherServlet的基本作用

Web请求处理基本过程:Servlet类和Servlet容器

Web请求从客户端(即某个网络应用程序,通常是远程程序,如远程浏览器、PostMan、或者是另一台JVM等)发起,
忽略网络路由细节,请求到达服务器程序,①经过过滤和分发,②到达请求处理逻辑,③处理完成后返回结果到客户端

Java诞生以来处理Web请求的技术有很多,如CGI和Servlet,CGI对每一个Http请求创建一个处理进程,根本不考虑性能,基本已被淘汰,当前绝大多数Java网络程序使用Servlet来处理网络请求
Servlet是Java中处理Web请求的类的顶层接口,该接口及相关类位于javax.servlet包下
需要特别注意的是,JDK虽然提供了javax扩展包,但并不提供javax.servlet包,该包位于Tomcat等Web容器程序的servlet-api.jar
Servlet类在JVM中驻存单例对象,对每一个到达的请求另起线程(而不是进程)处理,因此比CGI更高效
Servlet对象是多线程的,一个会话同一时刻只有一个请求需要目标Servlet处理(不考虑黑客攻击),多个会话可能同时请求一个目标Servlet,即存在并发请求
多线程安全由开发者保证,通常,除非进行请求次数统计等多线程共享操作,Servlet类不应该包括全局可变变量以保证线程安全性
关于线程安全此处不展开叙述,但在编写Servlet等任何多线程程序时,需要特别注意

步骤②由Servlet类完成,步骤①和③则由Web容器(如Tomcat、Jetty等)来完成
由于容器中的核心处理类为Servlet类,这些Web容器因此又称为Servlet容器

正如开篇已经提到的,常用的Servlet容器大多只实现了http协议,优点是这些Web容器都是轻量级的,类文件很少,缺点则是不支持更广泛的通信协议
付费或开源的Web容器中,RedHat JBOSS、Oracle GlassFish、Oracle WebLogic、IBM WebSphere等,除了Servlet以外,都还支持更广泛的通信协议,如JMS等
这些重型Web容器,不在本文讨论范围内

除了原生的Servlet编程,Java Web框架如Spring虽然不会直接使用Servlet编程,但其分发请求的入口类DispatcherServlet就是间接继承自HttpServlet
HttpServlet抽象类基于Servlet接口,Servlet接口基于Socket接口,Socket接口则基于TCP协议,TCP通信的基本操作则由操作系统native实现
除了HttpServlet的业务逻辑,其余所有的操作均由Tomcat或JDK封装
Servlet类和Servlet容器的出现,使得开发者不必关心网络通信以及协议解析的大量繁琐复杂困难的操作,只需要关注于自身的业务逻辑
了解请求处理的基本过程,可以简单而又轻松地实现基本的HTTP协议网络开发

本文在业务逻辑之外,延伸了解Servlet的基本内容和使用方法
至于Tomcat如何过滤和分发请求,如何封装HttpServletRequest和HttpServletResponse,如何响应客户端等,不在本文讨论范围内

Servlet及其相关类的继承链和基本方法

所有的Servlet相关类都位于Tomcat的servlet-api.jar中
1.javax.servlet.Servlet接口
是Servlet的核心和顶层接口,用于处理Web请求,包含5个方法

  ①public abstract void init(ServletConfig paramServletConfig) throws ServletException;

  ②public abstract ServletConfig getServletConfig();

  ③public abstract void service(ServletRequest paramServletRequest, ServletResponse paramServletResponse)
    throws ServletException, IOException;

  ④public abstract String getServletInfo();

  ⑤public abstract void destroy();

其中,①③⑤属于Servlet的生命周期方法
init方法在首次请求Servlet时被Servlet容器调用,后续请求不再调用,通常用于初始化一些Servlet级全局变量,如Servlet的名称、请求计数器等
service方法为实际的请求处理逻辑,每次请求都会调用此方法,首次请求时,先调用init再调用service
destroy方法在卸载应用程序(即关闭服务器应用程序)或关闭Servlet容器(即关闭Tomcat或Jetty等)被Servlet容器调用,方法体中通常进行资源清理等操作
注:首次请求是指本Servlet的首次请求,而不是每个会话的首次请求,一个Servlet可以处理多个会话,Servlet是会话无关的

②④属于非生命周期方法
如果在init方法中定义了ServletConfig的值,则getServletConfig即可取到,ServletInfo亦然

2.javax.servlet.ServletRequest接口
对于每一个Http请求,Servlet容器都会封装为一个ServletRequest对象,并作为第一个参数传递并调用Servlet的service方法
ServletRequest对象中封装了大量请求细节,如协议类型、客户端域名/ip地址/端口、url地址、字符编码、服务端域名/ip地址/端口等
对于url中的参数,可以使用getParameter(String paramString)等方法来获取,注意,如果参数不存在,结果返回null
不详述

3.javax.servlet.ServletResponse接口
对于每一个Http请求,Servlet容器都会封装为一个ServletResponse对象,并作为第二个参数传递并调用Servlet的service方法
ServletResponse对象中封装了大量的响应方法,服务器响应客户端的所有复杂的细节都被ServletResponse简化到了几乎极致
通过ServletResponse可以设置响应信息的多媒体类型、编码字符集,可以选择响应文本或者二进制流

多媒体类型setContentType应该根据需要显式地设置为text/html或text/json等,以告知浏览器如何解析
编码字符集setCharacterEncoding默认为ISO-8859-1,对于中文来说,应该和前端约定,最好明确指定为UTF-8
响应文本getWriter获取响应通道的PrintWriter对象,通过其print方法响应文本
响应二进制流getOutputStream获取响应通道的ServletOutputStream对象,通过其print方法响应字节流

4.javax.servlet.ServletConfig接口
Servlet类可以通过getServletConfig()方法获取其ServletConfig对象,如果在init中没有对其初始化,则会获得空对象
ServletConfig对象本身没什么用处,一般并不需要对Servlet存储Servlet级别的信息,关于ServletConfig对象初始化及使用方法,不详述

5.javax.servlet.ServletContext接口
ServletConfig对象虽然是鸡肋,但可以通过其getServletContext()获取ServletContext对象
每一个JVM中可以有多个不同的Servlet对象,但这些Servlet对象共享同一个ServletContext对象
也就是说ServletContext对象可以存储JVM级别的Servlet全局变量,并对所有Servlet对象提供间接的(通过ServletConfig)访问接口

小结:Session级的变量存储在HttpSession中,Servlet级的变量存储在Servlet类或其ServletConfig属性中,JVM级别的Servlet全局变量存储在ServletContext中
JVM级别的全局变量可以存储在全局静态类中,分布式全局变量则应存储在公共服务器(如Redis)中

HttpServlet及其相关类

0.javax.servlet.GenericServlet抽象类
GenericServlet类实现了Servlet接口,除了service方法,实现了其余4个方法
  public void destroy(){}

  public ServletConfig getServletConfig(){return this.config;}

  public String getServletInfo(){return "";}

  public void init(ServletConfig config)throws ServletException
  {
    this.config = config;
    init();
  }

  public void init()throws ServletException{}
除此之外,还扩展定义了getServletContext()方法,使得通过Servlet就可以直接得到ServletContext对象,而且此对象不为空
  public ServletContext getServletContext(){return getServletConfig().getServletContext();}
  
实际使用中,自定义的Servlet实现类继承自GenericServlet类即可,不必去实现Servlet接口,从而减少大量的模板代码

1.javax.servlet.http.HttpServlet抽象类
对于Http请求,实际使用的Servlet实现类继承自更加专业的HttpServlet类,而不是GenericServlet类
HttpServlet类本身继承自GenericServlet类,虽然是抽象类,但并没有任何抽象方法,换言之,实现了最后的service方法
但是由于业务逻辑还是需要开发者实现,因此定义为抽象类,以免直接使用

HttpServlet最核心的代码是service方法,service方法做了两件事:入参类型强转和请求类型转发
在service方法中首先将入参ServletRequest和ServletResponse分别转换为HttpServletRequest和HttpServletResponse
然后根据请求的类型转发到相应的处理方法
HttpServlet中定义了7种Http请求类型:"DELETE"、"HEAD"、"GET"、"OPTIONS"、"POST"、"PUT"和"TRACE"
对应7种处理方法:doDelete/doHead/doGet/doOptions/doPost/doPut/doTrace
7种类型的处理方法默认实现本质是一样的:如果为Http/1.1协议,返回405错误,否则返回400错误
如果请求类型不属于7种之一,返回501错误

显然,开发者需要继承HttpServlet并以实际业务逻辑重写7种请求处理方法
最常用的请求类型为get和post,因此,根据需要,通常只需要重写doGet和doPost方法
doGet和doPost方法的处理逻辑可以是完全相同的,也可以不同(即同一个url请求,对get和post进行不同的处理,这种情况比较少见,但的确存在)

2.javax.servlet.http.HttpServletRequest接口
HttpServletRequest接口继承自ServletRequest接口,并扩展了多个方法
除了继承的方法之外,HttpServletRequest对象可以获取请求所属的会话对象、请求的应用程序名/http方法类型/Header/Cookie等参数

根据这些参数,可以获取请求的必要信息,并据此进行业务逻辑的处理

3.javax.servlet.http.HttpServletResponse接口
HttpServletResponse接口继承自ServletResponse接口,并扩展了多个方法
除了继承的方法之外,HttpServletResponse对象可以在响应中添加Cookie/Header、设置响应码、告知浏览器重定向url、响应错误码和错误信息等

Servlet的部署描述符、监听器和过滤器

参见《Servlet的部署描述符、监听器和过滤器》ing

Spring对请求的分发:DispatcherServlet

Spring轻易不造轮子,但封装了很多优秀的轮子,使得用户接口对于开发者更友好
org.springframework.web.servlet.DispatcherServlet是为数不多的Spring自己造的Web请求处理的轮子之一,是分发Http请求的核心类,其底层正是HttpServlet

1.Bean扫描创建、依赖注入和Map<URL,Method>映射关系创建
Spring读取扫描包路径,递归读取路径下的每一个class文件名,根据全路径反射获取类的Class对象
Class对象获取类注解,对含有@Controller、@Service、@Component、@Repository注解的类创建单例Bean
对含有@Controller注解的类创建Map映射,根据类注解@RequestMapping的值(缺省为/),映射url路径到方法名
    Class对象遍历全局变量(无论私有还是public),获取全局变量注解,对含有@AutoWired注解的变量初始化赋值,赋值名称为注解value(缺省为变量名)值的bean实例
    Class对象遍历所有方法(无论私有还是public),获取方法注解,对含有@RequestMapping注解的方法拼接Map映射

2.DispatcherServlet请求分发
request对象获取请求的URL地址,根据Map<URL,Method>映射关系查找对应的service业务逻辑处理方法,反射传参并invoke方法执行业务逻辑
若查找不到映射关系,返回404

原生Servlet编程中,存在多个Servlet类,Servlet容器对请求进行Servlet对象分发,分发到service方法,进一步再细分到具体的http请求类型处理逻辑中。web.xml中配置多个Servlet,示例如下:

    <servlet>
        <servlet-name>XxxServlet</servlet-name>
        <servlet-class>full-class-path-of-XxxServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>XxxServlet</servlet-name>
        <url-pattern>/xxx</url-pattern>
    </servlet-mapping>
    
    <servlet>
        <servlet-name>XyyServlet</servlet-name>
        <servlet-class>full-class-path-of-XyyServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>XyyServlet</servlet-name>
        <url-pattern>/yyy</url-pattern>
    </servlet-mapping> 

Spring编程中,Servlet容器中只需注册一个Servlet对象,即Spring的DispatcherServlet对象,Servlet容器将所有请求分发到DispatcherServlet对象,后续的所有分发全部由DispatcherServlet完成
业务逻辑处理类使用@Controller注解标记,类中可以有多个处理方法,每个处理方法都相当于一个Servlet类(的service方法)
Spring管理url到业务逻辑方法的映射关系,在请求到来时寻找相应的方法并代理反射执行。web.xml配置示例如下:

    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--按启动顺序使此Servlet随Servlet容器一起启动(即执行Servlet的init方法),而不是在请求到来时再启动-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <!--拦截所有URL请求-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

传统的Servlet编程,通常会使用Servlet拦截请求结尾为xxx.do、xxx.action、xxx.html等格式的url,然后将其分发到相应的Servlet中处理
在Spring中建议使用REST风格的URL请求,REST格式简洁明了,举例来说,/会拦截所有的请求,/aaa/bbb则会拦截以/aaa/bbb开头的请求
DispatcherServlet通常拦截所有请求
REST拦截会导致静态文件(jsp/html/css/jpg/gif等)也被Servlet拦截,很多项目前后端服务分离,后端开发并不需要考虑静态资源的访问
对于前后端不分离的项目,可以使用Tomcat默认的org.apache.catalina.servlets.DefaultServlet拦截处理所有的资源文件,Tomcat对Servlet的分发按照注册顺序进行,以下配置需要先于Spring的DispatcherServlet

    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.html</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.jsp</url-pattern>  
    </servlet-mapping>
    ...

以上分发过程仅做简单和简化的说明,更多更真实的细节不展开描述,关于Spring如何处理Http请求,更多详情参见《SpringMVC的请求处理逻辑》ing

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值