J2EE Architecture(6)
J2EE Architecture(6)
1、企业解决方案
很多企业在发展过程中,购置或自行开发了多种应用软件,这些软件,可能基于不同的硬件,使用互不兼容的操作系统,经专用协议与其它应用程序交流信息。
为了满足这些应用程序的异步通信要求,一批中间件解决方案应运而生。如IBM的MQ系列、TIBCO公司的TIBCO、微软的MSMQ,但这些方案带来了其它的问题。因为无论是否使用了中间件,这些解决方案都缺少规范约束,企业也不愿接受这种思维。
注:MSMQ(Microsoft Message Queue,微软消息队列)是在多个不同的应用之间实现相互通信的一种异步传输模式,相互通信的应用可以分布于同一台机器上,也可以分布于相连的网络空间中的任一位置。其实现原理是:消息的发送者把自己想要发送的消息放入一个容器中(称之为Message),然后把它保存至一个系统公用空间的消息队列(Message Queue)中;本地或者是异地的消息接收程序再从该队列中取出发给它的消息进行处理。
J2EE是很好的企业解决方案。在J2EE上,可以快捷地开发和部署企业应用程序,可将应用程序部署为“组件”,并在“容器”环境中部署这些组件。
2、J2EE规范
J2EE规范本质上是文档记录,详细描述一些约束企业平台行为的规则和条件。J2EE规范主要面向一下两类专业人员:
1)J2EE平台和解决方案厂商
2)企业应用程序架构师和开发人员
3、Servlet规范
1)Servlet是运行在Web服务器上的Java程序,用于响应客户请求。
Servlet是Java组件,部署在Web服务器上,能在请求/响应通信模式下有效工作。
2)GenericServlet是独立于协议的Servlet。按照需要扩展GenericServlet后可以实现与协议相关的Servlet。HttpServlet就是一个典型例子,它扩展了GenericServlet,并将HTTP作为通信协议。
3)Servlet2.3文档的Servlet规范
4、Servlet规范详解
1)Servlet接口
可使用两种方式之一创建Servlet:
(1)实现Servlet接口
(2)扩展的GenericServlet类或javax.servlet.http.HttpServlet类
在创建Servlet后,还要描述Servlet细节,以及在运行时环境中,Servlet及Web应用程序中其它关联Web组件的附加信息。
规范将XML文档中嵌入的这些信息称为部署描述符,供Web容器在部署组件时使用。在创建和部署Servlet后,Servlet成为Web服务器“容器”中的“实例”。
Web组件设计人员可按照设计标准,选择创建一下两类Servlet:
(1)单线程Servlet
(2)多线程Servlet
多线程Servlet是Web服务器“容器”的“单个实例”,Web容器将所有客户请求转发给一个实例。规范也允许创建多个Servlet实例,以便为各个客户请求提供服务。此时的Servlet类需要实现SingleThreadModel接口。
Servlet容器厂商需要确保合理地加载和实例化Servlet。
第一种方法是在部署期间,在启动容器本身时,由Servlet引擎加载和实例化Servlet;
第二种方法是在第一个客户请求到达容器时,加载和实例化Servlet。
2)Servlet生命期
Web容器的Servlet组件的生命期可分为3个阶段:
(1)Servlet开始
Servlet会经历一个简单的生命历程。init()方法使Servlet进入“就绪”状态;之后,Servlet可接收服务请求;当容器决定销毁Servlet时,将调用destroy()方法。
在初始化过程中,init()方法使用ServletConfig对象。它包含Servlet需要的初始化参数的名-值对。通常,目录、文件路径及其它资源信息将写入这个对象。
未初始化的Servlet由容器卸载,不经历通常定义的Servlet生命期。
未初始化的Servlet不能接收客户端的任何请求,也不能对它运行destroy()方法。
(2)Servlet服务
Servlet容器可能通过Servlet的service()方法发送多个并发请求,故在设计service()方法时,需要考虑并发处理多个线程,还要注意灵活地“同步”全部或部分service()方法,尽量避免性能的大幅下降。
在Servlet生命期,服务线程的执行可能引发异常。
出现异常时,Servlet会变得临时不可用,或长期不可用。如长期不可用,则容器需要运行Servlet的destroy()方法,并释放资源。如果Servlet是临时不可用,容器将异常返回客户端,并显示消息,要求在一定时间后重试。
(3)Servlet结束
容器可在任意长的时间内使用一个Servlet实例,或SingleThreadModelServlet的多个实例。Servlet实例可一直延续到Servlet容器本身的生命期结束为止。
当容器认定不再需要某个Servlet时,可以调用Servlet的destroy()方法,释放Servlet占用的所有容器资源。
注意:容器不能调用活动的Servlet的destroy()方法,在调用Servlet的destroy()方法前,容器需要确保以下两点:
一是阻塞更多新请求;
二是完成所有待处理请求。
走进JavaWeb技术世界4:Servlet 工作原理详解
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
https://github.com/h2pl/Java-Tutorial
喜欢的话麻烦点下Star哈
文章首发于我的个人博客:
www.how2playlife.com
本文是微信公众号【Java技术江湖】的《走进JavaWeb技术世界》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。
该系列博文会告诉你如何从入门到进阶,从servlet到框架,从ssm再到SpringBoot,一步步地学习JavaWeb基础知识,并上手进行实战,接着了解JavaWeb项目中经常要使用的技术和组件,包括日志组件、Maven、Junit,等等内容,以便让你更完整地了解整个Java Web技术体系,形成自己的知识框架。
为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。
如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。
什么是Servlet
Servlet的作用是为Java程序提供一个统一的web应用的规范,方便程序员统一的使用这种规范来编写程序,应用容器可以使用提供的规范来实现自己的特性。比如tomcat的代码和jetty的代码就不一样,但作为程序员你只需要了解servlet规范就可以从request中取值,你可以操作session等等。不用在意应用服务器底层的实现的差别而影响你的开发。
HTTP 协议只是一个规范,定义服务请求和响应的大致式样。Java servlet 类将HTTP中那些低层的结构包装在 Java 类中,这些类所包含的便利方法使其在 Java 语言环境中更易于处理。
正如您正使用的特定 servlet 容器的配置文件中所定义的,当用户通过 URL 发出一个请求时,这些 Java servlet 类就将之转换成一个 HttpServletRequest,并发送给 URL 所指向的目标。当服务器端完成其工作时,Java 运行时环境(Java Runtime Environment)就将结果包装在一个 HttpServletResponse 中,然后将原 HTTP 响应送回给发出该请求的客户机。在与 Web 应用程序进行交互时,通常会发出多个请求并获得多个响应。所有这些都是在一个会话语境中,Java 语言将之包装在一个 HttpSession 对象中。在处理响应时,您可以访问该对象,并在创建响应时向其添加事件。它提供了一些跨请求的语境。
容器(如 Tomcat)将为 servlet 管理运行时环境。您可以配置该容器,定制 J2EE 服务器的工作方式,以便将 servlet 暴露给外部世界。正如我们将看到的,通过该容器中的各种配置文件,您在 URL(由用户在浏览器中输入)与服务器端组件之间搭建了一座桥梁,这些组件将处理您需要该 URL 转换的请求。在运行应用程序时,该容器将加载并初始化 servlet,管理其生命周期。
Servlet体系结构
Servlet顶级类关联图
Servlet
Servlet的框架是由两个Java包组成的:javax.servlet与javax.servlet.http。在javax.servlet包中定义了所有的Servlet类都必须实现或者扩展的通用接口和类。在javax.servlet.http包中定义了采用Http协议通信的HttpServlet类。Servlet的框架的核心是javax.servlet.Servlet接口,所有的Servlet都必须实现这个接口。
Servlet接口
在Servlet接口中定义了5个方法:
1\. init(ServletConfig)方法:负责初始化Servlet对象,在Servlet的生命周期中,该方法执行一次;该方法执行在单线程的环境下,因此开发者不用考虑线程安全的问题;
2\. service(ServletRequest req,ServletResponse res)方法:负责响应客户的请求;为了提高效率,Servlet规范要求一个Servlet实例必须能够同时服务于多个客户端请求,即service()方法运行在多线程的环境下,Servlet开发者必须保证该方法的线程安全性;
3\. destroy()方法:当Servlet对象退出生命周期时,负责释放占用的资源;
4\. getServletInfo:就是字面意思,返回Servlet的描述;
5\. getServletConfig:这个方法返回由Servlet容器传给init方法的ServletConfig。
ServletRequest & ServletResponse
对于每一个HTTP请求,servlet容器会创建一个封装了HTTP请求的ServletRequest实例传递给servlet的service方法,ServletResponse则表示一个Servlet响应,其隐藏了将响应发给浏览器的复杂性。通过ServletRequest的方法你可以获取一些请求相关的参数,而ServletResponse则可以将设置一些返回参数信息,并且设置返回内容。
ServletConfig
ServletConfig封装可以通过@WebServlet或者web.xml传给一个Servlet的配置信息,以这种方式传递的每一条信息都称做初始化信息,初始化信息就是一个个K-V键值对。为了从一个Servlet内部获取某个初始参数的值,init方法中调用ServletConfig的getinitParameter方法或getinitParameterNames方法获取,除此之外,还可以通过getServletContext获取ServletContext对象。
ServletContext
ServletContext是代表了Servlet应用程序。每个Web应用程序只有一个context。在分布式环境中,一个应用程序同时部署到多个容器中,并且每台Java虚拟机都有一个ServletContext对象。有了ServletContext对象后,就可以共享能通过应用程序的所有资源访问的信息,促进Web对象的动态注册,共享的信息通过一个内部Map中的对象保存在ServiceContext中来实现。保存在ServletContext中的对象称作属性。操作属性的方法:
GenericServlet
前面编写的Servlet应用中通过实现Servlet接口来编写Servlet,但是我们每次都必须为Servlet中的所有方法都提供实现,还需要将ServletConfig对象保存到一个类级别的变量中,GenericServlet抽象类就是为了为我们省略一些模板代码,实现了Servlet和ServletConfig,完成了一下几个工作:
将init方法中的ServletConfig赋给一个类级变量,使的可以通过getServletConfig来获取。
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
同时为避免覆盖init方法后在子类中必须调用super.init(servletConfig),GenericServlet还提供了一个不带参数的init方法,当ServletConfig赋值完成就会被第带参数的init方法调用。这样就可以通过覆盖不带参数的init方法编写初始化代码,而ServletConfig实例依然得以保存
为Servlet接口中的所有方法提供默认实现。
提供方法来包装ServletConfig中的方法。
HTTPServlet
在编写Servlet应用程序时,大多数都要用到HTTP,也就是说可以利用HTTP提供的特性,javax.servlet.http包含了编写Servlet应用程序的类和接口,其中很多覆盖了javax.servlet中的类型,我们自己在编写应用时大多时候也是继承的HttpServlet。
Servlet工作原理
当Web服务器接收到一个HTTP请求时,它会先判断请求内容——如果是静态网页数据,Web服务器将会自行处理,然后产生响应信息;如果牵涉到动态数据,Web服务器会将请求转交给Servlet容器。此时Servlet容器会找到对应的处理该请求的Servlet实例来处理,结果会送回Web服务器,再由Web服务器传回用户端。
针对同一个Servlet,Servlet容器会在第一次收到http请求时建立一个Servlet实例,然后启动一个线程。第二次收到http请求时,Servlet容器无须建立相同的Servlet实例,而是启动第二个线程来服务客户端请求。所以多线程方式不但可以提高Web应用程序的执行效率,也可以降低Web服务器的系统负担。
Web服务器工作流程
接着我们描述一下Tomcat与Servlet是如何工作的,首先看下面的时序图:
Servlet工作原理时序图
Web Client 向Servlet容器(Tomcat)发出Http请求;
Servlet容器接收Web Client的请求;
Servlet容器创建一个HttpRequest对象,将Web Client请求的信息封装到这个对象中;
Servlet容器创建一个HttpResponse对象;
Servlet容器调用HttpServlet对象的service方法,把HttpRequest对象与HttpResponse对象作为参数传给 HttpServlet对象;
HttpServlet调用HttpRequest对象的有关方法,获取Http请求信息;
HttpServlet调用HttpResponse对象的有关方法,生成响应数据;
Servlet容器把HttpServlet的响应结果传给Web Client;
Servlet生命周期
在Servlet接口中定义了5个方法,其中3个方法代表了Servlet的生命周期:
1\. init(ServletConfig)方法:负责初始化Servlet对象,在Servlet的生命周期中,该方法执行一次;该方法执行在单线程的环境下,因此开发者不用考虑线程安全的问题;
2\. service(ServletRequest req,ServletResponse res)方法:负责响应客户的请求;为了提高效率,Servlet规范要求一个Servlet实例必须能够同时服务于多个客户端请求,即service()方法运行在多线程的环境下,Servlet开发者必须保证该方法的线程安全性;
3\. destroy()方法:当Servlet对象退出生命周期时,负责释放占用的资源;
编程注意事项说明:
当Server Thread线程执行Servlet实例的init()方法时,所有的Client Service Thread线程都不能执行该实例的service()方法,更没有线程能够执行该实例的destroy()方法,因此Servlet的init()方法是工作在单线程的环境下,开发者不必考虑任何线程安全的问题。
当服务器接收到来自客户端的多个请求时,服务器会在单独的Client Service Thread线程中执行Servlet实例的service()方法服务于每个客户端。此时会有多个线程同时执行同一个Servlet实例的service()方法,因此必须考虑线程安全的问题。
虽然service()方法运行在多线程的环境下,并不一定要同步该方法。而是要看这个方法在执行过程中访问的资源类型及对资源的访问方式。分析如下:
1\. 如果service()方法没有访问Servlet的成员变量也没有访问全局的资源比如静态变量、文件、数据库连接等,而是只使用了当前线程自己的资源,比如非指向全局资源的临时变量、request和response对象等。该方法本身就是线程安全的,不必进行任何的同步控制。
2\. 如果service()方法访问了Servlet的成员变量,但是对该变量的操作是只读操作,该方法本身就是线程安全的,不必进行任何的同步控制。
3\. 如果service()方法访问了Servlet的成员变量,并且对该变量的操作既有读又有写,通常需要加上同步控制语句。
4\. 如果service()方法访问了全局的静态变量,如果同一时刻系统中也可能有其它线程访问该静态变量,如果既有读也有写的操作,通常需要加上同步控制语句。
5\. 如果service()方法访问了全局的资源,比如文件、数据库连接等,通常需要加上同步控制语句。
在创建一个 Java servlet 时,一般需要子类 HttpServlet。该类中的方法允许您访问请求和响应包装器(wrapper),您可以用这个包装器来处理请求和创建响应。Servlet的生命周期,简单的概括这就分为四步:
Servlet类加载--->实例化--->服务--->销毁;
Servlet生命周期
创建Servlet对象的时机:
默认情况下,在Servlet容器启动后:客户首次向Servlet发出请求,Servlet容器会判断内存中是否存在指定的Servlet对象,如果没有则创建它,然后根据客户的请求创建HttpRequest、HttpResponse对象,从而调用Servlet对象的service方法;
Servlet容器启动时:当web.xml文件中如果元素中指定了子元素时,Servlet容器在启动web服务器时,将按照顺序创建并初始化Servlet对象;
Servlet的类文件被更新后,重新创建Servlet。Servlet容器在启动时自动创建Servlet,这是由在web.xml文件中为Servlet设置的属性决定的。从中我们也能看到同一个类型的Servlet对象在Servlet容器中以单例的形式存在;
注意:在web.xml文件中,某些Servlet只有元素,没有元素,这样我们无法通过url的方式访问这些Servlet,这种Servlet通常会在元素中配置一个子元素,让容器在启动的时候自动加载这些Servlet并调用init(ServletConfig config)方法来初始化该Servlet。其中方法参数config中包含了Servlet的配置信息,比如初始化参数,该对象由服务器创建。
销毁Servlet对象的时机:
Servlet容器停止或者重新启动:Servlet容器调用Servlet对象的destroy方法来释放资源。以上所讲的就是Servlet对象的生命周期。那么Servlet容器如何知道创建哪一个Servlet对象?Servlet对象如何配置?实际上这些信息是通过读取web.xml配置文件来实现的。
<servlet>
<!-- Servlet对象的名称 -->
<servlet-name>action<servlet-name>
<!-- 创建Servlet对象所要调用的类 -->
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<init-param>
<!-- 参数名称 -->
<param-name>config</param-name>
<!-- 参数值 -->
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>detail</param-name>
<param-value>2</param-value>
</init-param>
<init-param>
<param-name>debug</param-name>
<param-value>2</param-value>
</init-param>
<!-- Servlet容器启动时加载Servlet对象的顺序 -->
<load-on-startup>2</load-on-startup>
</servlet>
<!-- 要与servlet中的servlet-name配置节内容对应 -->
<servlet-mapping>
<servlet-name>action</servlet-name>
<!-- 客户访问的Servlet的相对URL路径 -->
<url-pattern>*.do</url-pattern>
</servlet-mapping>
当Servlet容器启动的时候读取配置节信息,根据配置节信息创建Servlet对象,同时根据配置节信息创建HttpServletConfig对象,然后执行Servlet对象的init方法,并且根据配置节信息来决定创建Servlet对象的顺序,如果此配置节信息为负数或者没有配置,那么在Servlet容器启动时,将不加载此Servlet对象。当客户访问Servlet容器时,Servlet容器根据客户访问的URL地址,通过配置节中的配置节信息找到指定的Servlet对象,并调用此Servlet对象的service方法。
在整个Servlet的生命周期过程中,创建Servlet实例、调用实例的init()和destroy()方法都只进行一次,当初始化完成后,Servlet容器会将该实例保存在内存中,通过调用它的service()方法,为接收到的请求服务。下面给出Servlet整个生命周期过程的UML序列图,如图所示:
Servlet生命周期
如果需要让Servlet容器在启动时即加载Servlet,可以在web.xml文件中配置元素。
Servlet中的Listener
Listener 使用的非常广泛,它是基于观察者模式设计的,Listener 的设计对开发 Servlet 应用程序提供了一种快捷的手段,能够方便的从另一个纵向维度控制程序和数据。目前 Servlet 中提供了 5 种两类事件的观察者接口,它们分别是:4 个 EventListeners 类型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 个 LifecycleListeners 类型的,ServletContextListener、HttpSessionListener。如下图所示:
Servlet中的Listener
它们基本上涵盖了整个 Servlet 生命周期中,你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 标签中。当然也可以在应用程序中动态添加 Listener,需要注意的是 ServletContextListener 在容器启动之后就不能再添加新的,因为它所监听的事件已经不会再出现。掌握这些 Listener 的使用,能够让我们的程序设计的更加灵活。
Cookie与Session
Servlet 能够给我们提供两部分数据,一个是在 Servlet 初始化时调用 init 方法时设置的 ServletConfig,这个类基本上含有了 Servlet 本身和 Servlet 所运行的 Servlet 容器中的基本信息。还有一部分数据是由 ServletRequest 类提供,从提供的方法中发现主要是描述这次请求的 HTTP 协议的信息。关于这一块还有一个让很多人迷惑的 Session 与 Cookie。
Session 与 Cookie 的作用都是为了保持访问用户与后端服务器的交互状态。它们有各自的优点也有各自的缺陷。然而具有讽刺意味的是它们优点和它们的使用场景又是矛盾的,例如使用 Cookie 来传递信息时,随着 Cookie 个数的增多和访问量的增加,它占用的网络带宽也也会越来越大。所以大访问量的时候希望用 Session,但是 Session 的致命弱点是不容易在多台服务器之间共享,所以这也限制了 Session 的使用。
不管 Session 和 Cookie 有什么不足,我们还是要用它们。下面详细讲一下,Session 如何基于 Cookie 来工作。实际上有三种方式能可以让 Session 正常工作:
基于 URL Path Parameter,默认就支持
基于 Cookie,如果你没有修改 Context 容器个 cookies 标识的话,默认也是支持的
基于 SSL,默认不支持,只有 connector.getAttribute("SSLEnabled") 为 TRUE 时才支持
第一种情况下,当浏览器不支持 Cookie 功能时,浏览器会将用户的 SessionCookieName 重写到用户请求的 URL 参数中,它的传递格式如:
/path/Servlet?name=value&name2=value2&JSESSIONID=value3
接着 Request 根据这个 JSESSIONID 参数拿到 Session ID 并设置到 request.setRequestedSessionId 中。
请注意如果客户端也支持 Cookie 的话,Tomcat 仍然会解析 Cookie 中的 Session ID,并会覆盖 URL 中的 Session ID。
如果是第三种情况的话将会根据 javax.servlet.request.ssl_session 属性值设置 Session ID。
有了 Session ID 服务器端就可以创建 HttpSession 对象了,第一次触发是通过 request. getSession() 方法,如果当前的 Session ID 还没有对应的 HttpSession 对象那么就创建一个新的,并将这个对象加到 org.apache.catalina. Manager 的 sessions 容器中保存,Manager 类将管理所有 Session 的生命周期,Session 过期将被回收,服务器关闭,Session 将被序列化到磁盘等。只要这个 HttpSession 对象存在,用户就可以根据 Session ID 来获取到这个对象,也就达到了状态的保持。
Session相关类图
上从图中可以看出从 request.getSession 中获取的 HttpSession 对象实际上是 StandardSession 对象的门面对象,这与前面的 Request 和 Servlet 是一样的原理。下图是 Session 工作的时序图:
Session工作的时序图
还有一点与 Session 关联的 Cookie 与其它 Cookie 没有什么不同,这个配置的配置可以通过 web.xml 中的 session-config 配置项来指定。
参考文章
https://segmentfault.com/a/1190000009707894
https://www.cnblogs.com/hysum/p/7100874.html
http://c.biancheng.net/view/939.html
https://www.runoob.com/
https://blog.csdn.net/android_hl/article/details/53228348
微信公众号
个人公众号:程序员黄小斜
微信公众号【程序员黄小斜】新生代青年聚集地,程序员成长充电站。作者黄小斜,职业是阿里程序员,身份是斜杠青年,希望和更多的程序员交朋友,一起进步和成长!专注于分享技术、面试、职场等成长干货,这一次,我们一起出发。
关注公众号后回复“2020”领取我这两年整理的学习资料,涵盖自学编程、求职面试、算法刷题、Java技术学习、计算机基础和考研等8000G资料合集。
技术公众号:Java技术江湖
微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,专注于 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!
关注公众号后回复“PDF”即可领取200+页的《Java工程师面试指南》强烈推荐,几乎涵盖所有Java工程师必知必会的知识点。
基于SSM的Java Web应用开发原理初探
SSM开发Web的框架已经很成熟了,成熟得以至于有点落后了。虽然如今是SOA架构大行其道,微服务铺天盖地的时代,不过因为仍有大量的企业开发依赖于SSM,本文简单对基于SSM的Java开发做一快速入门,方便读者尽快把握脉络,理解Java Web开发的主轴,便于做进一步深入学习。
关于第一代MVC开发可见:JavaWeb开发之详解Servlet及Servlet容器
SpringMVC
跟许多MVC框架一样,SpringMVC底层依赖Servlet实现底层HTTP请求的处理。不久前Spring 5.0更新了基于Servlet3.1非阻塞式IO的Spring WebFlux,将另文介绍。
DispatcherServlet
DispatcherServlet的核心作用是请求分发,作为前端控制器接收所有请求,特点如下:
继承了HttpServlet,需要配置在Web应用中。
需在init-param中配置contextConfigLocation参数,DispatcherServlet会解析该文件并生成一个WebApplicationContext的容器对象,称为上下文环境。
WebApplicationContext继承ApplicationContext容器,它需要ServletContext实例。因此必须运行在Web容器中
基于Controller接口的控制器
DispatcherServlet请求转发至Handler,Handler是Controller接口的实现类,可以通过handleRequest方法访问对应请求的HttpServletRequest和HttpServletResponse对象。处理完业务请求后,必须返回一个包含模型对象和视图路径的ModelAndView对象。
Spring MVC执行流程
当web程序启动的时候,ContextLoaderServlet会把对应的配置文件信息读取出来,通过IoC去初始化控制器DispatchServlet
用户向服务器发送请求,请求被前端控制器DispatcherServlet统一截获。
DispatcherServlet解析URL得到URI,根据URI调用HandlerMapping获得该Handler配置的所有相关对象,包括Handler对象以及Handler对象对应的拦截器,这些对象会被封装到HandlerExecutionChain对象返回。
DispatcherServlet根据获得的Handler,选一个合适的HandlerAdapter。HandlerAdapter符合单一职责原则,会被用于处理多种Handler,调用Handler的实际处理方法。
提取请求中的数据模型,开始执行Handler。在处理入参过程中,根据配置,Spring会负责:
消息转换:将请求信息(如Json,xml)转换为对象,将对象转换为响应。
数据转换:将请求信息的数据进行转换,如String转为Integer、Double等。
数据格式化:对请求信息的数据进行格式化,如String转为格式化数字或日期等。
数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中。
Handler执行完毕后,向DispatcherServlet返回一个ModelAndView对象,该对象应该包含视图名,或视图名和模型。
根据返回的ModelAndView,选择一个合适的ViewResolver视图解析器返回给DispatcherServlet。
ViewResolver结合Model来渲染视图。
将视图渲染结果返回给客户端。
开发步骤
web.xml中定义前端控制器DispatcherServlet
定义使用POST提交数据的前端JSP
定义处理用户请求的Handle类,可以实现Controller接口或使用@Controller注解
配置Handle
编写视图资源
Mybatis
ORM(Object/Relation Mapping)即对象/关系数据库映射,核心作用就是把持久化对象的增删查改转为对数据库的操作。开发者可以面向对象的方式操作持久化对象,而ORM框架负责将相关操作转换成对应的SQL操作。
ORM基本映射关系:
数据表映射类:持久化类被映射到一个数据表。程序使用该持久化类来创建实例、修改属性、删除实例时,系统会自动转换为对表的CRUD操作。
数据行映射对象:持久化类所生成的对象,对应表中的行。当程序修改实例时,ORM转为对行的操作。
数据类字段映射对象的属性:程序修改程序化类的某个属性时,ORM会对应修改某个列。
Mybatis体系结构
Mybatis的常用对象有SqlSessionFactory和SqlSession。
SqlSessionFactory是线程安全的,是数据库编译后的内存镜像,通常一个应用对应一个SqlSessionFactory。SqlSessionFactory对象通过加载mybatis-config.xml配置文件生成。它生成的逻辑是:
XML配置文件/Configuration实例===>SqlSessionFactoryBuilder===>SqlSessionFactory
SqlSession是线程不安全的,是执行持久化操作的对象,底层封装了JDBC连接,由SqlSessionFactory生成。因为是线程不安全的,一般不能作为类的静态字段或实例字段,也不能放在任何类的管理范围中,比如Servlet中的HttpSession对象。
Mybatis初始化基本流程
调用SqlSessionFactoryBuilder对象的build(inputStream)方法;
SqlSessionFactoryBuilder会根据输入流创建XMLConfigBuilder对象;
SqlSessionFactoryBuilder调用XMLConfigBuilder的parse方法,解析XML配置文件返回Configuration对象;
SqlSessionFactoryBuilder根据Configuration对象创建一个DefaultSessionFactory对象,并返回给客户端。
使用MyBatis操作持久化对象接本步骤
编写持久化类POJO和持久化操作的Mapper.xml,定义要执行的SQL语句
获取SqlSessionFactory
获取SqlSession
用面向对象的方式操作数据库
关闭事务,关闭SqlSession
SpringBoot之DispatcherServlet详解及源码解析
在使用SpringBoot之后,我们表面上已经无法直接看到DispatcherServlet的使用了。本篇文章,带大家从最初DispatcherServlet的使用开始到SpringBoot源码中DispatcherServlet的自动配置进行详解。DispatcherServlet简介DispatcherServlet是前端控制器设计模式的实现,提供了Spring Web MVC的集中访问点,而且负责职责的分派,而且与Spring Ioc容器无缝集成,从而可以获得Spring的所有好处。DispatcherServlet作用DispatcherServlet主要用作职责调度工作,本身主要用于控制流程,主要职责如下:文件上传解析,如果请求类型是multipart将通过MultipartResolver进行文件上传解析;通过HandlerMapping,将请求映射到处理器(返回一个HandlerExecutionChain,它包括一个处理器、多个HandlerInterceptor拦截器);通过HandlerAdapter支持多种类型的处理器(HandlerExecutionChain中的处理器);通过ViewResolver解析逻辑视图名到具体视图实现;本地化解析;渲染具体的视图等;如果执行过程中遇到异常将交给HandlerExceptionResolver来解析。DispatcherServlet工作流程DispatcherServlet传统配置DispatcherServlet作为前置控制器,通常配置在web.xml文件中的。拦截匹配的请求,Servlet拦截匹配规则要自已定义,把拦截下来的请求,依据相应的规则分发到目标Controller来处理,是配置spring MVC的第一步。<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcherServlet-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
DospatcherServlet实际上是一个Servlet(它继承HttpServlet)。DispatcherServlet处理的请求必须在同一个web.xml文件里使用url-mapping定义映射。这是标准的J2EE servlet配置。在上述配置中:servlet-name用来定义servlet的名称,这里是dispatcherServlet。servlet-class用来定义上面定义servlet的具体实现类,这里是org.springframework.web.servlet.DispatcherServlet。init-param用来定义servlet的初始化参数,这里指定要初始化WEB-INF文件夹下的dispatcherServlet-servlet.xml。如果spring-mvc.xml的命名方式是前面定义servlet-name “-servlet”,则可以不用定义这个初始化参数,(Spring默认配置文件为“/WEB-INF/[servlet名字]-servlet.xml”),Spring会处理这个配置文件。由此可见,Spring的配置文件也可放置在其他位置,只要在这里指定就可以了。如果定义了多个配置文件,则用“,”分隔即可。servlet-mapping定义了所有以.do结尾的请求,都要经过分发器。当DispatcherServlet配置好后,一旦DispatcherServlet接受到请求,DispatcherServlet就开始处理请求了。DispatcherServlet处理流程当配置好DispatcherServlet后,DispatcherServlet接收到与其对应的请求之时,处理就开始了。处理流程如下:找到WebApplicationContext并将其绑定到请求的一个属性上,以便控制器和处理链上的其它处理器能使用WebApplicationContext。默认的属性名为DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE。将本地化解析器绑定到请求上,这样使得处理链上的处理器在处理请求(准备数据、显示视图等等)时能进行本地化处理。如果不需要本地化解析,忽略它就可以了。将主题解析器绑定到请求上,这样视图可以决定使用哪个主题。如果你不需要主题,可以忽略它。如果你指定了一个上传文件解析器,Spring会检查每个接收到的请求是否存在上传文件,如果是,这个请求将被封装成MultipartHttpServletRequest以便被处理链中的其它处理器使用。(Spring’s multipart (fileupload) support查看更详细的信息)找到合适的处理器,执行和这个处理器相关的执行链(预处理器,后处理器,控制器),以便为视图准备模型数据。如果模型数据被返回,就使用配置在WebApplicationContext中的视图解析器显示视图,否则视图不会被显示。有多种原因可以导致返回的数据模型为空,比如预处理器或后处理器可能截取了请求,这可能是出于安全原因,也可能是请求已经被处理过,没有必要再处理一次。DispatcherServlet相关源码org.springframework.web.servlet.DispatcherServlet中doService方法部分源码:protected void doService(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// ......
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE,getWebApplicationContext());
// ......
}
通过上面源码得知,DispatcherServlet会找到上下文WebApplicationContext(其指定的实现类为XmlWebApplicationContext),并将它绑定到一个属性上(默认属性名为WEB_APPLICATION_CONTEXT_ATTRIBUTE),以便控制器能够使用WebApplicationContext。initStrategies方法源码如下:protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
从如上代码可以看出,DispatcherServlet启动时会进行我们需要的Web层Bean的配置,如HandlerMapping、HandlerAdapter等,而且如果我们没有配置,还会给我们提供默认的配置。DispatcherServlet SpringBoot自动配置DispatcherServlet在Spring Boot中的自动配置是通过DispatcherServletAutoConfiguration类来完成的。先看注解部分代码:@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
...
}
@AutoConfigureOrder指定该自动配置的优先级;@Configuration指定该类为自动配置类;@ConditionalOnWebApplication指定自动配置需要满足是基于SERVLET的web应用;@ConditionalOnClass指定类路径下必须有DispatcherServlet类存在;@AutoConfigureAfter指定该自动配置必须基于ServletWebServerFactoryAutoConfiguration的自动配置。DispatcherServletAutoConfiguration中关于DispatcherServlet实例化的代码如下:@Configuration(proxyBeanMethods = false) // 实例化配置类
@Conditional(DefaultDispatcherServletCondition.class) // 实例化条件:通过该类来判断
@ConditionalOnClass(ServletRegistration.class) // 存在指定的ServletRegistration类
// 加载HttpProperties和WebMvcProperties
@EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class })
protected static class DispatcherServletConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
// 创建DispatcherServlet
DispatcherServlet dispatcherServlet = new DispatcherServlet();
// 初始化DispatcherServlet各项配置
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails());
return dispatcherServlet;
}
// 初始化上传文件的解析器
@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
}
内部类DispatcherServletConfiguration同样需要满足指定的条件才会进行初始化,具体看代码中的注释。其中的dispatcherServlet方法中实现了DispatcherServlet的实例化,并设置了基础参数。这对照传统的配置就是web.xml中DispatcherServlet的配置。另外一个方法multipartResolver,用于初始化上传文件的解析器,主要作用是当用户定义的MultipartResolver名字不为“multipartResolver”时,通过该方法将其修改为“multipartResolver”,相当于重命名。其中DispatcherServletConfiguration的注解@Conditional限定必须满足DefaultDispatcherServletCondition定义的匹配条件才会自动配置。而DefaultDispatcherServletCondition类同样为内部类。@Order(Ordered.LOWEST_PRECEDENCE - 10)
private static class DefaultDispatcherServletCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("Default DispatcherServlet");
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
List<String> dispatchServletBeans = Arrays
.asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false));
if (dispatchServletBeans.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
return ConditionOutcome
.noMatch(message.found("dispatcher servlet bean").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME));
}
if (beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
return ConditionOutcome.noMatch(
message.found("non dispatcher servlet bean").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME));
}
if (dispatchServletBeans.isEmpty()) {
return ConditionOutcome.match(message.didNotFind("dispatcher servlet beans").atAll());
}
return ConditionOutcome.match(message.found("dispatcher servlet bean", "dispatcher servlet beans")
.items(Style.QUOTE, dispatchServletBeans)
.append("and none is named " DEFAULT_DISPATCHER_SERVLET_BEAN_NAME));
}
}
该类的核心功能,总结起来就是:检验Spring容器中是否已经存在一个名字为“dispatcherServlet”的DispatcherServlet,如果不存在,则满足条件。在该自动配置类中还有用于实例化ServletRegistrationBean的内部类:@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
// 通过ServletRegistrationBean将dispatcherServlet注册为servlet,这样servlet才会生效。
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
// 设置名称为dispatcherServlet
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
// 设置加载优先级,设置值默认为-1,存在于WebMvcProperties类中
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
DispatcherServletRegistrationConfiguration类的核心功能就是注册dispatcherServlet使其生效并设置一些初始化的参数。其中,DispatcherServletRegistrationBean继承自ServletRegistrationBean,主要为DispatcherServlet提供服务。DispatcherServletRegistrationBean和DispatcherServlet都提供了注册Servlet并公开DispatcherServletPath信息的功能。Spring Boot通过上面的自动配置类就完成了之前我们在web.xml中的配置操作。这也是它的方便之处。参考文章:https://www.cnblogs.com/wql025/p/4805634.htmlhttps://juejin.im/post/5d3066736fb9a07ece6806e4
JavaWeb开发Servlet学习
博主信息:@博主: 嘟嘟的程序员铲屎官:一位爱喵咪,爱开源,爱总结,爱分享技术的Java领域新星博主,如果你想和博主做朋友,关注博主,并私聊博主(给我发一条消息我就会关注你喔),博主本人也十分喜欢解决问题,如果你有什么问题,也可以来私聊博主喔,希望能够和C站的朋友相互学习,相互进步。:关于本篇博客,最近在看廖雪峰大佬JavaWeB部分的知识,对大佬的Servlet这一篇进行个人总结,如果有什么错误的,请各位大佬能够及时提出,以免小弟误人子弟!@TOC一.Tomcat在前面我们通过自己编写的HTTP服务器代码实现了通过游览器访问网页,但是该服务器存在非常多的问题,我们编写的HTTP服务器访问的内容其实是静态资源,HTTP访问方式仅仅只有Get,网页代码就写在服务器端,没有任何的异常处理措施,并且我们仅仅只是为了编写网站,如果创建一个网站还要编写相应的服务器代码,成本负担就实在太大了,有没有现成的服务器,我们只需要将我们编写的网站代码放到服务器中进行部署,无需关心服务端代码怎么实现,这就是Tomcat服务器。1.关于什么是Tomcat服务器?Tomcat 服务器是一个免费的开放源代码的Web应用服务器Tomcat 实际上运行JSP页面和ServletTomcat处理静态HTML的能力不如Apache服务器搜狗百科:Tomcat 服务器是一个免费的开放源代码的Web应用服务器,[1]属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。诀窍是,当配置正确时,Apache 为HTML页面服务,而Tomcat 实际上运行JSP页面和Servlet。另外,Tomcat和IIS等Web服务器一样,具有处理HTML页面的功能,另外它还是一个Servlet和JSP容器,独立的Servlet容器是Tomcat的默认模式。不过,Tomcat处理静态HTML的能力不如Apache服务器。目前Tomcat最新版本为9.0(下图就是代表Tomcat服务器的图标)。游览器怎么和Tomcat服务器进行交互的?游览器发起请求Tomcat服务器通过Servlet获得用户的请求Tomcat服务器通过Servlet响应用户的请求2.Tomcat的安装在网上找了一篇安装Tomcat的博客,可以跟着进行安装!相关学习链接:TOMCAT原理详解及请求过程二.Servlet搜狗百科:servlet是Java Servlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。关于什么是动态Web内容(和数据库有交互),如下图:1.Servlet ApI 4个Java包该API的jar包文件位于Tomcat的lib路径下Javax.servlet:包含定义Servlet与Servlet容器之间契约的类和接口。Javax.servlet.http:包含定义HTTP Servlet与Servlet容器之间契约的类和接口javax.servlet.annotaion:包含对Servlet,Filter和Listener进行标注的注解,它还为标注元件指定元数据。javax.servlet.descriptor:包含为Web应用程序的配置信息提供编程式访问的类型。2.游览器怎么和Tomcat服务器进行交互的(进一步了解)?客户端(通常都是浏览器)访问Web服务器,发送HTTP请求Web服务器接收到请求后,传递给Servlet容器Servlet容器加载Servlet,产生Servlet实例后,向其传递表示请求和响应的对象Servlet实例使用请求对象得到客户端的请求信息,然后进行相应的处理Servlet实例将处理结果通过响应对象发送回客户端,容器负责确保响应送出去,同时将控制返回给Web服务器3.Servlet的生命周期Sun 公司提供了一系列的接口和类用于 Servlet 技术的开发,其中最重要的接口是 javax.servlet.Servlet。在 Servlet 接口中定义了 5 个抽象方法,如下表所示。方法声明功能描述void init(ServletConfig config)容器在创建好 Servlet 对象后,就会调用此方法。该方法接收一个 ServletConfig 类型的参数,Servlet 容器通过该参数向 Servlet 传递初始化配置信息 ServletConfig getSendetConfig()用于获取 Servlet 对象的配置信息,返回 Servlet 的 ServletConfig 对象 String getServletInfo()返回一个字符串,其中包含关于 Servlet 的信息,如作者、版本和版权等信息 void service (ServletRequest request,ServletResponse response)负责响应用户的请求,当容器接收到客户端访问 Servlet 对象的请求时,就会调用此方法。容器会构造一个表示客户端请求信息的 ServletRequest 对象和一个用于响应客户端的 ServletResponse 对象作为参数传递给 service() 方法。在 service() 方法中,可以通过 ServletRequest 对象得到客户端的相关信息和请求信息,在对请求进行处理后,调用 ServletResponse 对象的方法设置响应信息void destroy()负责释放 Servlet 对象占用的资源。当服务器关闭或者 Servlet 对象被移除时,Servlet 对象会被销毁,容器会调用此方法 在 Java 中,任何对象都有生命周期,Servlet 也不例外。Servlet 的生命周期如图下图所示。4.ServletRequest和ServletResponse(1) ServletRequest当浏览器请求网页时,它会向 Web 服务器发送特定信息,这些信息不能被直接读取,因为这些信息是作为 HTTP 请求的头的一部分进行传输的,对于每一个HTTP请求,Servlet容器都会创建一个ServletRequest实例,并将它传给Servlet的service方法。以下是来自于浏览器端的重要头信息,您可以在 Web 编程中频繁使用:下面的方法可用在 Servlet 程序中读取 HTTP 头。这些方法通HttpServletRequest 对象可用。(1) ServletResponsejavax.servlet.ServletResponse接口表示一个Servlet响应。在调用一个Servlet的service方法之前,Servlet 容器会先创建一个ServletResponse,并将它作为第二个参数传给service方法。ServletResponse隐藏了将响应发给浏览器的复杂性。当一个 Web 服务器响应一个 HTTP 请求时,响应通常包括一个状态行、一些响应报头、一个空行和文档。一个典型的响应如下所示:HTTP/1.1 200 OK
Content-Type: text/html
Header2: ...
...
HeaderN: ...
(Blank Line)
<!doctype ...>
<html>
<head>...</head>
<body>
...
</body>
</html>状态行包括 HTTP 版本(在本例中为 HTTP/1.1)、一个状态码(在本例中为 200)和一个对应于状态码的短消息(在本例中为 OK)。下表总结了从 Web 服务器端返回到浏览器的最有用的 HTTP 1.1 响应报头,您会在 Web 编程中频繁地使用它们:设置 HTTP 响应报头的方法下面的方法可用于在 Servlet 程序中设置 HTTP 响应报头。这些方法通过 HttpServletResponse 对象可用。编写传递给用户的文档(HTML)通过HttpServletResponse .getWriter()获得PrintWWriter对象,该对象采用ISO-8859-1编码,该对象通过print("")方法输出文本内容。上面的内容来源于(菜鸟编程):Servlet 客户端 HTTP 请求Servlet 服务器 HTTP 响应5.ServletConfig和ServletContext接口及其使用方法详解(1) ServletConfig当 Tomcat 初始化一个 Servlet时,会将该 Servlet 的配置信息封装到 ServletConfig 对象中,此时可以通过调用 init(ServletConfig config)方法将 ServletConfig 对象传递给 Servlet。ServletConfig 接口中定义了一系列获取配置信息的方法,如下表所示。方法说明功能描述String getInitParameter(String name)根据初始化参数名返回对应的初始化参数值 Enumeration getInitParameterNames()返回一个 Enumeration 对象,其中包含了所有的初始化参数名 ServletContext getServletContext()返回一个代表当前 Web 应用的 ServletContext 对象 String getServletName()返回 Servlet 的名字,即在 web.xml 中 元素的值,在@WebServlet种name的值 配置Servlet的二种方式@WebServlet注解:给Servlet映射了一条路径@WebServlet(name="Myservlet" , urlPatterns={ "/my" })name:该Servlet的名称为Myservlet,可通过getServletName()获取urlPatterns:映射的路径使用部署描述符(在WEB-INF目录下web.xml 文件)在运行 Servlet 程序时,可能需要一些辅助信息,例如,文件使用的编码、使用 Servlet 程序的共享信息等,这些信息可以在 web.xml 文件中使用一个或多个 元素进行配置。可以使用@WebServlet中没有的元素如果需要修改配置值,如Servlet路径,就不需要重新编译Servlet类允许覆盖Servlet中@WebServlet注解中的值配置信息如下:<servlet>
<servlet-name>Myservlet</servlet-name>
<servlet-class>app01.MyServlet</servlet-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Myservlet</servlet-name>
<url-pattern>/My01</url-pattern>
</servlet-mapping>备注:ServletConfig的配置信息通过web.xml 文件进行配置,在 web.xml 文件中,不仅可以配置 Servlet 的映射信息,还可以配置整个 Web 应用的初始化信息。(2) ServletContext接口当 Tomcat 启动时,Tomcat 会为每个 Web 应用创建一个唯一的 ServletContext 对象代表当前的 Web应用,该对象封装了当前 Web 应用的所有信息。可以利用该对象获取 Web 应用程序的初始化信息、读取资源文件等。下面ServletContext 接口的不同作用分别进行讲解。获取 Web 应用程序的初始化参数在web.xml中定义:<context-param>
<param-name>XXX</param-name>
<param-value>xxx</param-value>
</context-param>
<context-param>
<param-name>AAA</param-name>
<param-value>aaa</param-value>
</context-param>在上面的示例中, 元素位于根元素 中,它的子元素 和 分别用于指定参数的名字和参数值。要想获取这些参数名和参数值的信息,可以使用 ServletContext 接口中定义的 getInitParameterNames() 和 getInitParameter(String name)方法分别获取。在web.xml中定义:<context-param>
<param-name>username</param-name>
<param-value>admin</param-value>
</context-param>
<context-param>
<param-name>password</param-name>
<param-value>1234</param-value>
</context-param>在Servlet中获取web.xml中的参数,并输出读取 Web 应用下的资源文件在实际开发中,有时会需要读取 Web 应用中的一些资源文件,如配置文件和日志文件等。为此,在 ServletContext 接口中定义了一些读取 Web 资源的方法,这些方法是依靠 Servlet 容器实现的。Servlet 容器根据资源文件相对于 Web 应用的路径,返回关联资源文件的 I/O 流或资源文件在系统的绝对路径等。ServletContext接口的常用方法:方法说明功能描述Set getResourcePaths(String path)返回一个 Set 集合,集合中包含资源目录中子目录和文件的路径名 称。参数 path 必须以正斜线(/)开始,指定匹配资源的部分路径 String getRealPath(String path)返回资源文件在服务器文件系统上的真实路径(文件的绝对路径)。参数 path 代表资源文件的虚拟路径,它应该以正斜线(/)开始,/ 表示当前 Web 应用的根目录,如果 Servlet 容器不能将虚拟路径转换为文 件系统的真实路径,则返回 null URL getResource(String path)返回映射到某个资源文件的 URL 对象。参数 path 必须以正斜线(/)开始,/ 表示当前 Web 应用的根目录 InputStream getResourceAsStream(String path)返回映射到某个资源文件的 InputStream 输入流对象。参数 path 的传递规则和 getResource() 方法完全一致 如何使用 ServletContext 对象读取资源文件在 servletDemo01 项目的 src 目录中创建一个名称为 itcast.properties 的文件,在创建好的文件中输入如下所示的配置信息: username=admin password=1234在Servlet下如何读取 itcast.properties 资源文件
相关链接(上面的内容来源于):ServletConfig和ServletContext接口及其使用方法详解(附带实例)6.通过实现servlet接口的方式创建一个Servlet实例File→New→Other,在 Select a wizard 窗口中选择 Dynamic Web Project 选项输入项目名app01点击Next勾选Generate web.xml deployment descriptor,不然不能生成web.xml文件点击finish由于我是通过Java视图进行创建,所以会提示是否切换到JavaEE视图(JavaWeb开发的视图),点击Yes点击Yes之后该视图就切换到Java EE视图项目结构如下:右击项目,创建一个app01的包在该包下创建一个名为MyServlet的类,该类实现Servlet接口,并重写该类下的所有抽象方法。package app01;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
@WebServlet(name="Myservlet" , urlPatterns={ "/my" })
public class MyServlet implements Servlet{
private transient ServletConfig servletConfig;
@Override
public void init(ServletConfig servletConfig) throws ServletException {
// TODO Auto-generated method stub
this.servletConfig=servletConfig;
System.out.println("init()通过servletConfig进行初始化操作");
}
@Override
public ServletConfig getServletConfig() {
// TODO Auto-generated method stub
return servletConfig;
}
@Override
public String getServletInfo() {
// TODO Auto-generated method stub
return "Myservlet";
}
@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
System.out.println("service()获取请求,并进行响应!");
String name = servletConfig.getServletName();
response.setContentType("text/html");
PrintWriter writer=response.getWriter();
writer.print("<html><head></head>");
writer.print("<body>hello from"+name+"</body></html>");
}
@Override
public void destroy() {
// TODO Auto-generated method stub
System.out.println("销毁!");
}
}
运行项目:运行效果:运行之后我们发现无法访问,这是因为没有在web.xml中设置网站的首页,并且我们也没有创建任何的静态HTML或者Jsp等网页,给用户进行展示,要响应对应的Servlet可以通过/my进行响应(我们给Servlet映射了一条路径为/my,必须通过该路径才能进行响应)。在网址后面输入my,并进行跳转即可访问获取内容:上面的内容其实就是我们在MyServlet类下,service()方法所写的响应内容从该实例中看看servlet的生命周期init():通过servletconfig进行初始化,在servlet生命周期中只执行一次service():获取用户的请求并发送服务器端的响应,网页刷新一次(用户请求一次)就调用一次service().destroy():当服务器关闭或者 Servlet 对象被移除时,Servlet 对象会被销毁,容器会调用此方法7.servlet实例(app01)进行优化Sun 公司其实已经提供了一个Servlet的实现类GenericServlet抽象类,但是该抽象类并没有实现HTTP 请求处理HttpServlet 是 GenericServlet 的子类,它继承了 GenericServlet 的所有方法,并且为 HTTP 请求中的 GET 和 POST 等类型提供了具体的操作方法。通常情况下,编写的 Servlet 类都继承自 HttpServlet,在开发中使用的也是 HttpServlet 对象。HttpServlet 类中包含两个常用方法,这两个方法的说明如下表所示:方法声明功能描述protected void doGet (HttpServletRequest req, HttpServletResponse resp)用于处理 GET 类型的 HTTP 请求的方法 protected void doPost(HttpServletRequest req, HttpServletResponse resp)用于处理 POST 类型的 HTTP 请求的方法 优化1:通过继承GenericServlet类实现Servlet创建一个MyServlet02类,继承GenericServlet抽象类,并重写该抽象类的所有抽象方法(抽象方法只有一个service())运行效果:优化2:通过继承HttpServlet类实现Servlet优化1的方式虽然可以通过简化实现servlet但是该方式并没有实现HTTP 请求处理创建一个MyServlet03类,继承HttpServlet类通过重写对应的请求方式,实现对应请求方式的响应(重写doGet(),实现get请求的响应;重写doPost(),实现post请求的响应)doGet()和doPost()并不是抽象方法,不需要强制进行重写,由于这二种请求方式使用的最多所以一般都将其重写运行效果:其实在eclipse中,JavaEE视图下,可以直接创建Servlet通过eclipse创建的Servlet代码如下:优化三:ServletConfig的配置通过web.xml进行配置在Web.xml中定义Servlet部署描述符<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<display-name>app01</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>My01</servlet-name>
<servlet-class>app01.MyServlet</servlet-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>My01</servlet-name>
<url-pattern>/My01</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>My02</servlet-name>
<servlet-class>app01.MyServlet02</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>My02</servlet-name>
<url-pattern>/My02</url-pattern>
</servlet-mapping>
</web-app>
备注: servlet-mapping: 如果url-pattern定义的是路径,那么以后所有对这个路径下资源的请求都会由servlet-name中定义的servlet处理; 注意:如果在web.xml和@WebServlet("/my")都定义了映射路径,二者的映射路径不能一致,不然会出现异常在web.xml和@WebServlet("/my")都定义了映射路径,二个路径都可以进行访问关于<welcome-file-list >:<welcome-file-list>:网站的首页列表(访问网站第一次见到的网页)前面的app01项目,当访问网站的时候,会显示404(404表示没有找到<welcome-file-list >里面的网页),这是因为我们网站没有创建任何的网页(HTML,JSP等),也没有设置网站的首页.优化四:给app01网站创建首页我们只要创建web.xml中 <welcome-file-list>中任何一个网页,第一次打开网站就会显示该网页在Webcontent中创建index.htmlindex.html代码如下:运行效果:优化五:在Tomcat中部署app01网站War文件(扩展名为.War,Web Application Archive)包含全部Web应用程序。在这种情形下,一个Web应用程序被定义为单独的一组文件、类和资源,用户可以对jar文件进行封装,并把它作为小型服务程序(servlet)来访问。部署:将项目导包为*.war后缀的文件,并复制文件到Tomcat服务器下的webapps目录下即可,具体步骤如下:将app01导出为*.war文件下面的路径可以直接选择Tomcat服务器下的webapps目录下或者复制app01.war到Tomcat服务器下的webapps目录下当Tomcat运行的时候,会自动解压(手动启动Tomcat服务器)在游览器地址栏输入:http://localhost:8085/app01/即可访问(我的Tomcat的端口号是8085,有的是8080)在Tomcat服务器中应用程序的目录结构如下:我们编写的app01的应用结构是如何的?Tomcat服务器的webapps目录下的app01app01应用目录结构META-INF目录下:WEB-INF目录下:classes目录下:app01目录下:8.处理HTML表单每个Web应用程序中几乎都会包含一个或者多个HTML表单,用来接收用户输入。你可以轻松地将一个HTML表单从Servlet发送到浏览器。当用户提交表单时,在表单元素中输入的值会被当作请求参数发送到服务器。HTML输入域(文本域、隐藏域或密码域)或者文本域的值被当作一个字符串发送到服务器。对于空白的输入域或者文本域将发送一条空白的字符串。因此,带有一个输入域名称的ServletRequest.getParameter()将永远不会返回null。将复选框数据传递到 Servlet 程序被选中的复选框将字符串“on”发送到服务器。没有被选中的复选框则不发送任何内容到服务器,并且ServletRequest.getParameter(fieldname)返回null。项目结构:index.html<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="indexServlet" method="POST" target="_blank">
<input type="checkbox" name="runoob" checked="checked" /> 菜鸟教程
<input type="checkbox" name="google" /> Google
<input type="checkbox" name="taobao" checked="checked" /> 淘宝
<input type="submit" value="选择站点" />
</form>
</body>
</html>IndexServlet.java
@WebServlet(urlPatterns={"/indexServlet"})
public class IndexServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// TODO Auto-generated method stub
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// TODO Auto-generated method stub
// 设置响应内容类型
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
String title = "读取复选框数据";
String docType = "<!DOCTYPE html> \n";
out.println(docType +
"<html>\n" +
"<head><title>" + title + "</title></head>\n" +
"<body bgcolor=\"#f0f0f0\">\n" +
"<h1 align=\"center\">" + title + "</h1>\n" +
"<ul>\n" +
" <li><b>菜鸟按教程标识:</b>: "
+ req.getParameter("runoob") + "\n" +
" <li><b>Google 标识:</b>: "
+ req.getParameter("google") + "\n" +
" <li><b>淘宝标识:</b>: "
+ req.getParameter("taobao") + "\n" +
"</ul>\n" +
"</body></html>");
}
}
运行效果:将单选框数据传递到 Servlet 程序单选按钮将被选按钮的值发送到服务器。如果没有选中任何按钮,则不发送任何内容到服务器,并且ServletRequest.getParameter(fieldname)返回null。selectgender.htmlGenderServlet.Java运行效果:在地址栏输入:http://localhost:8085/app01_form/selectgender.html,并跳转读取所有的表单参数(getParameterNames() 方法读取所有可用的表单参数)以下是通用的实例,使用 HttpServletRequest 的 getParameterNames()方法读取所有可用的表单参数。该方法返回一个枚举,其中包含未指定顺序的参数名。 一旦我们有一个枚举,我们可以以标准方式循环枚举,使用hasMoreElements() 方法来确定何时停止,使用 nextElement() 方法来获取每个参数的名称。course.htmlReadParamsServlet.java@WebServlet("/ReadParams")
public class ReadParamsServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 设置响应内容类型
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
String title = "读取所有的表单数据";
String docType =
"<!doctype html public \"-//w3c//dtd html 4.0 " +
"transitional//en\">\n";
out.println(docType +
"<html>\n" +
"<head><meta charset=\"utf-8\"><title>" + title + "</title></head>\n" +
"<body bgcolor=\"#f0f0f0\">\n" +
"<h1 align=\"center\">" + title + "</h1>\n" +
"<table width=\"100%\" border=\"1\" align=\"center\">\n" +
"<tr bgcolor=\"#949494\">\n" +
"<th>参数名称</th><th>参数值</th>\n"+
"</tr>\n");
Enumeration paramNames = request.getParameterNames();
while(paramNames.hasMoreElements()) {
String paramName = (String)paramNames.nextElement();
out.print("<tr><td>" + paramName + "</td>\n");
String[] paramValues =
request.getParameterValues(paramName);
// 读取单个值的数据
if (paramValues.length == 1) {
String paramValue = paramValues[0];
if (paramValue.length() == 0)
out.println("<td><i>没有值</i></td>");
else
out.println("<td>" + paramValue + "</td>");
} else {
// 读取多个值的数据
out.println("<td><ul>");
for(int i=0; i < paramValues.length; i++) {
out.println("<li>" + paramValues[i]);
}
out.println("</ul></td>");
}
out.print("</tr>");
}
out.println("\n</table>\n</body></html>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}运行效果:在地址栏后面输入course.html,并跳转
Servlet过滤器详解
日常生活里,自来水都是经过一层层的过滤处理才达到食用标准的,每一层过滤都起到一种净化的作用,今天所讲Servlet过滤器与自来水被过滤的原理相似,1 定义Servlet过滤器和 Servlet 很类似,注意的是它有拦截客户端请求的用法,Servlet过滤器可以改变请求中的内容,满足实际开发的需要。开发来讲,过滤器实质上是在Web应用服务器里面的一个Web组件,目的用在拦截客户端(浏览器)与目标资源的请求,并把这些请求进行一定过滤处理在发给目标资源:看出,Web容器部署过滤器后,不仅客户端发送的请求会经过过滤器的处理,要不要返回呢?请求的回应信息也同样要经过过滤器。如果一个Web应用中使用一个过滤器不能解决实际的业务需求,那可以部署多个过滤器对业务请求多次处理,这样做就组成一个过滤器链,Web容器在处理过滤器时,将被过滤器的先后顺序对请求进行处理:Web窗口中部署过滤器链,那么请求会一次按照过滤器顺序进行处理,在第一个过滤器处理一请求后,会传递给第二个过滤器进行处理,依次类推,传递到最后一个过滤器为止,再将请求交给目标资源进行处理,目标资源在处理经过过滤的请求后,其回应信息再从最后一个过滤器依次传递到第一个过滤器,最后传送到客户端;2 过滤器核心对象过滤器对象放置在 javax.servlet 包中,叫做 Filter, 是个接口,与过滤器相关的对象还有 FilterConfig对象 与 FilterChain对象,这两个也同样是接口对象,位于 javax.servlet 包中,分别作为过滤器的配置对象与过滤器的传递工具。在实际开发,定义过滤器对象只需要直接或间接地实现 Filter接口即可。下图的MyFilter1过滤器与MyFilter2过滤器,而 FilterConfig对象 与 FilterChain对象 用在过滤器的相关操作:Filter接口每个过滤器对象只直接或间接地实现 Filter接口,在Filter接口中定义了3个方法,分别是 init(),doFilter()和destroy(),方法及说明如表:方法声明public void init(FilterConfig filterConfig) throws ServletException过滤器初始化方法,该方法在过滤器初始化时调用public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException对请求进行过滤处理public void destroy()销毁方法,以便释放资源FilterConfig接口Servlet容器进行实现,主要是获取过滤器中的配置信息,其方明及说明如表:方法说明public String getFilterName()用于获取过滤器的名字public ServletContext getServletContext()获取Servlet上下文public String getInitParameter(String name)获取过滤器的初始化参数值public Enumeration getInitParameterNames()获取过滤器的所有初始化参数FilterChain接口它由Servlet容器进行实现,在这个接口中一个方法,其方法声明:public void doFilter(ServletRequest request, ServletResponse response)throws IOException,ServletException 该方法将过滤的请求传给下一个过滤器,如果此过滤器已经是过滤器链中的最后一个过滤器,那么,请求传送给目标资源。过滤器创建于配置创建一个过滤器对象需要实现 javax.serjavax.servlet.Filter接口,下面演示过滤器的创建:创建名称为 MyFilter的过滤器对象,其代码如下:import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servletFilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* 过滤器
*/
public class MyFilter implements Filter{
//初始化方法
public void init(FilterConfig fConfig) throws ServletException{
//初始化处理
}
//过滤处理方法
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletEXception {
//过滤处理
chain.doFilter(request, response);
}
//销毁方法
public void destroy(){
//释放资源
}
}
过滤器的init()对过滤器的初始化进行处理,destroy()是过滤器的销毁方法,主要是释放资源,对于过滤器处理的业务需要编写到doFilter()里面,在请求过滤需调用chain参数的doFilter()将请求向下传给下一过滤器或目标资源:使用过滤器并不一定要将请求向下传递到下一过滤器或目标资源,如果业务逻辑需要,也可以在过滤处理后,直接回应客户端;过滤器与Servlet十分相似,在创建后同样对其配置,过滤器的配置主要分为两个步骤,分别在声明过滤器对象和创建过滤器映射,创建名为MyFilter的过滤器对象:<!- 过滤器声明 ->
<filter>
<!- 过滤器的名称 ->
<filter-name>MyFilter</filter-name>
<!- 过滤器的完整类名 ->
<filter-class>com.lyq.Myilter</filter-class>
</filter>
<!- 过滤器映射 ->
<filter-mapping>
<!- 过滤器名称 ->
<filter-name>MyFilter</filter-name>
<!-- 过滤器 URL映射 ->
<url-pattern>/MyFilter</filter-name>
<!- 过滤器URL映射 ->
<url-pattern>/MyFilter</url-pattern>
</filter-mapping>
< filter>标签用在声明过滤器对象,这个标签中必须配置两个子元素,分别是过滤器的名称与过滤器完整类名,其中< filter-name>定义过滤器的名称,< filter-class>指定过滤器的完整类名。< filter-mapping>标签用于创建过滤器的映射,它的主要作用是指定Web应用中,哪些URL应用哪一个过滤器进行处理,在< filter-mapping>标签里面需要你自己指定过滤器应用的URL。< filter>标签中的< filter-name>可以是自定义的名称,而< filter-mapping>标签中的< filter-name>是指定已定义的过滤器名称,没法改变。它需要和< filter>标签中的< filter-name>一一对应;我们试着创建一个过滤器,完成网站访问计数器的功能,在web.xml文件的配置里,将网站访问量的初始值设置为5000。新建名称是CountFilter的类,该类实现 javax.servlet.Filter接口,它是个过滤器对象,通过该过滤器实现统计网站访问人数,关键代码:
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class CountFilter implements Filter{
//来访数量
private int count;
@Override
public void init(FilterConfig filterConfig) throws ServletException{
String param = filterConfig.getInitParameter("count");
count++;
//将ServletRequest 转换成 HttpServletRequest
HttpServletRequest req = (HttpServletRequest)request;
//获取ServletContext
ServletContext context = req.getSession.getServletContext();
context.setAttribute("count", count);
chain.doFilter(request, response);
}
@Override
public void destroy(){
}
}
在ComFiler类中,包含一个成员变量 count,用于记录网站访问人数,该变量在过滤器的初始化方法init() 中被赋值,它的初始值通过FilterConfig对象读取配置文件中的初始化参数就可以得到;计数器 count变量的值在CountFilter类的 doFilter()中递增, 因为客户端在请求服务器中的Web应用时侯,过滤器拦截请求通过 doFilter()方法进行过滤处理,所以当client请求Web应用时,计数器 ount的值将加1, 为了能够访问计数器中的值,实例中我们把它放置在 Servlet 上下文中,Servlet 上下文对象通过将 ServletRequest 转换为 HttpServletRequest对象后获取。写过滤器对象需要我们实现javax.servlet.Filter接口,实现该接口后需要对 Filter对象的3个方法进行实现,在这3个方法中,除了 doFilter()方法外,如果在业务逻辑中不涉及初始化方法 init()与销毁方法 destroy(), 可以不编写任何代码对其进行空实现,如实例中的 destroy()方法。配置已创建的 CountFilter对象,此操作通过配置web.xml文件进行实现。<filter>
<filter-name>CountFilter</filter-name>
<filter-class>com.lyq.CountFilter</filter-class>
<init-param>
<param-name>count</param-name>
<param-value>5000</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CountFilter</filter-name>
<url-pattern>/index.jsp</url-pattern>
</filter-mapping>
CountFilter对象的配置主要通过声明过滤器及创建过滤器的映射进行实现,其中声明过滤器要通过 < filter>标签进行实现。在声明过程中,实例用< init-param>标签配置过滤器的初始化参数,初始化参数的名称为count,参数值为5000;如果直接对过滤器对象的成员变量进行赋值,那在过滤器被编译后将不可修改,所以,实例中将过滤器对象中的成员变量定义为过滤器的初始化参数,提高了代码的灵活性;创建程序中的首页index.jsp页面,在该页面中通过JSP内置对象Application获取计数器的值。关键代码如下:
<body>
<h2>
欢迎光临, <br>
你是本站的第【
<%=application.getAttribute("count") %>
】位访客!
</h2>
</body>
因为在web.xml中将计数器的初始值设置为 5000,所以实例运行后,计数器的数值变为大于5000的数,在多次刷新页面后,实例运行结果:
走进JavaWeb技术世界4:Servlet 工作原理详解
微信公众号【黄小斜】大厂程序员,互联网行业新知,终身学习践行者。关注后回复「Java」、「Python」、「C++」、「大数据」、「机器学习」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「笔试」、「面试」、「面经」、「计算机基础」、「LeetCode」 等关键字可以获取对应的免费学习资料。
从本篇开始,正式进入Java核心技术内容的学习,首先介绍的就是Java web应用的核心规范servlet
转自:https://www.ibm.com/developerworks/cn/java/j-lo-servlet/
Servlet 容器的启动过程
Tomcat7 也开始支持嵌入式功能,增加了一个启动类 org.apache.catalina.startup.Tomcat。创建一个实例对象并调用 start 方法就可以很容易启动 Tomcat,我们还可以通过这个对象来增加和修改 Tomcat 的配置参数,如可以动态增加 Context、Servlet 等。下面我们就利用这个 Tomcat 类来管理新增的一个 Context 容器,我们就选择 Tomcat7 自带的 examples Web 工程,并看看它是如何加到这个 Context 容器中的。
清单 2 . 给 Tomcat 增加一个 Web 工程
1
2
3
4
5
6
7
Tomcat tomcat = getTomcatInstance();
File appDir = new File(getBuildDirectory(), "webapps/examples");
tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath());
tomcat.start();
ByteChunk res = getUrl("http://localhost:" + getPort() +
"/examples/servlets/servlet/HelloWorldExample");
assertTrue(res.toString().indexOf("<h1>Hello World!</h1>") > 0);
清单 1 的代码是创建一个 Tomcat 实例并新增一个 Web 应用,然后启动 Tomcat 并调用其中的一个 HelloWorldExample Servlet,看有没有正确返回预期的数据。
Tomcat 的 addWebapp 方法的代码如下:
清单 3 .Tomcat.addWebapp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Context addWebapp(Host host, String url, String path) {
silence(url);
Context ctx = new StandardContext();
ctx.setPath( url );
ctx.setDocBase(path);
if (defaultRealm == null) {
initSimpleAuth();
}
ctx.setRealm(defaultRealm);
ctx.addLifecycleListener(new DefaultWebXmlListener());
ContextConfig ctxCfg = new ContextConfig();
ctx.addLifecycleListener(ctxCfg);
ctxCfg.setDefaultWebXml("org/apache/catalin/startup/NO_DEFAULT_XML");
if (host == null) {
getHost().addChild(ctx);
} else {
host.addChild(ctx);
}
return ctx;
}
前面已经介绍了一个 Web 应用对应一个 Context 容器,也就是 Servlet 运行时的 Servlet 容器,添加一个 Web 应用时将会创建一个 StandardContext 容器,并且给这个 Context 容器设置必要的参数,url 和 path 分别代表这个应用在 Tomcat 中的访问路径和这个应用实际的物理路径,这个两个参数与清单 1 中的两个参数是一致的。其中最重要的一个配置是 ContextConfig,这个类将会负责整个 Web 应用配置的解析工作,后面将会详细介绍。最后将这个 Context 容器加到父容器 Host 中。
接下去将会调用 Tomcat 的 start 方法启动 Tomcat,如果你清楚 Tomcat 的系统架构,你会容易理解 Tomcat 的启动逻辑,Tomcat 的启动逻辑是基于观察者模式设计的,所有的容器都会继承 Lifecycle 接口,它管理者容器的整个生命周期,所有容器的的修改和状态的改变都会由它去通知已经注册的观察者(Listener),关于这个设计模式可以参考《 Tomcat 的系统架构与设计模式,第二部分:设计模式》。Tomcat 启动的时序图可以用图 2 表示。
图 2. Tomcat 主要类的启动时序图(查看大图)
上图描述了 Tomcat 启动过程中,主要类之间的时序关系,下面我们将会重点关注添加 examples 应用所对应的 StandardContext 容器的启动过程。
当 Context 容器初始化状态设为 init 时,添加在 Contex 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口,它是在调用清单 3 时被加入到 StandardContext 容器中。ContextConfig 类会负责整个 Web 应用的配置文件的解析工作。
ContextConfig 的 init 方法将会主要完成以下工作:
创建用于解析 xml 配置文件的 contextDigester 对象
读取默认 context.xml 配置文件,如果存在解析它
读取默认 Host 配置文件,如果存在解析它
读取默认 Context 自身的配置文件,如果存在解析它
设置 Context 的 DocBase
ContextConfig 的 init 方法完成后,Context 容器的会执行 startInternal 方法,这个方法启动逻辑比较复杂,主要包括如下几个部分:
创建读取资源文件的对象
创建 ClassLoader 对象
设置应用的工作目录
启动相关的辅助类如:logger、realm、resources 等
修改启动状态,通知感兴趣的观察者(Web 应用的配置)
子容器的初始化
获取 ServletContext 并设置必要的参数
初始化“load on startup”的 Servlet
Web 应用的初始化工作
Web 应用的初始化工作是在 ContextConfig 的 configureStart 方法中实现的,应用的初始化主要是要解析 web.xml 文件,这个文件描述了一个 Web 应用的关键信息,也是一个 Web 应用的入口。
Tomcat 首先会找 globalWebXml 这个文件的搜索路径是在 engine 的工作目录下寻找以下两个文件中的任一个 org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。接着会找 hostWebXml 这个文件可能会在 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default,接着寻找应用的配置文件 examples/WEB-INF/web.xml。web.xml 文件中的各个配置项将会被解析成相应的属性保存在 WebXml 对象中。如果当前应用支持 Servlet3.0,解析还将完成额外 9 项工作,这个额外的 9 项工作主要是为 Servlet3.0 新增的特性,包括 jar 包中的 META-INF/web-fragment.xml 的解析以及对 annotations 的支持。
接下去将会将 WebXml 对象中的属性设置到 Context 容器中,这里包括创建 Servlet 对象、filter、listener 等等。这段代码在 WebXml 的 configureContext 方法中。下面是解析 Servlet 的代码片段:
清单 4. 创建 Wrapper 实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
for (ServletDef servlet : servlets.values()) {
Wrapper wrapper = context.createWrapper();
String jspFile = servlet.getJspFile();
if (jspFile != null) {
wrapper.setJspFile(jspFile);
}
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
if (multipartdef.getMaxFileSize() != null &&
multipartdef.getMaxRequestSize()!= null &&
multipartdef.getFileSizeThreshold() != null) {
wrapper.setMultipartConfigElement(new
MultipartConfigElement(
multipartdef.getLocation(),
Long.parseLong(multipartdef.getMaxFileSize()),
Long.parseLong(multipartdef.getMaxRequestSize()),
Integer.parseInt(
multipartdef.getFileSizeThreshold())));
} else {
wrapper.setMultipartConfigElement(new
MultipartConfigElement(
multipartdef.getLocation()));
}
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
context.addChild(wrapper);
}
这段代码清楚的描述了如何将 Servlet 包装成 Context 容器中的 StandardWrapper,这里有个疑问,为什么要将 Servlet 包装成 StandardWrapper 而不直接是 Servlet 对象。这里 StandardWrapper 是 Tomcat 容器中的一部分,它具有容器的特征,而 Servlet 为了一个独立的 web 开发标准,不应该强耦合在 Tomcat 中。
除了将 Servlet 包装成 StandardWrapper 并作为子容器添加到 Context 中,其它的所有 web.xml 属性都被解析到 Context 中,所以说 Context 容器才是真正运行 Servlet 的 Servlet 容器。一个 Web 应用对应一个 Context 容器,容器的配置属性由应用的 web.xml 指定,这样我们就能理解 web.xml 到底起到什么作用了。
创建 Servlet 实例
前面已经完成了 Servlet 的解析工作,并且被包装成 StandardWrapper 添加在 Context 容器中,但是它仍然不能为我们工作,它还没有被实例化。下面我们将介绍 Servlet 对象是如何创建的,以及如何被初始化的。
创建 Servlet 对象
如果 Servlet 的 load-on-startup 配置项大于 0,那么在 Context 容器启动的时候就会被实例化,前面提到在解析配置文件时会读取默认的 globalWebXml,在 conf 下的 web.xml 文件中定义了一些默认的配置项,其定义了两个 Servlet,分别是:org.apache.catalina.servlets.DefaultServlet 和 org.apache.jasper.servlet.JspServlet 它们的 load-on-startup 分别是 1 和 3,也就是当 Tomcat 启动时这两个 Servlet 就会被启动。
创建 Servlet 实例的方法是从 Wrapper. loadServlet 开始的。loadServlet 方法要完成的就是获取 servletClass 然后把它交给 InstanceManager 去创建一个基于 servletClass.class 的对象。如果这个 Servlet 配置了 jsp-file,那么这个 servletClass 就是 conf/web.xml 中定义的 org.apache.jasper.servlet.JspServlet 了。
创建 Servlet 对象的相关类结构图如下:
图 3. 创建 Servlet 对象的相关类结构
初始化 Servlet
初始化 Servlet 在 StandardWrapper 的 initServlet 方法中,这个方法很简单就是调用 Servlet 的 init 的方法,同时把包装了 StandardWrapper 对象的 StandardWrapperFacade 作为 ServletConfig 传给 Servlet。Tomcat 容器为何要传 StandardWrapperFacade 给 Servlet 对象将在后面做详细解析。
如果该 Servlet 关联的是一个 jsp 文件,那么前面初始化的就是 JspServlet,接下去会模拟一次简单请求,请求调用这个 jsp 文件,以便编译这个 jsp 文件为 class,并初始化这个 class。
这样 Servlet 对象就初始化完成了,事实上 Servlet 从被 web.xml 中解析到完成初始化,这个过程非常复杂,中间有很多过程,包括各种容器状态的转化引起的监听事件的触发、各种访问权限的控制和一些不可预料的错误发生的判断行为等等。我们这里只抓了一些关键环节进行阐述,试图让大家有个总体脉络。
下面是这个过程的一个完整的时序图,其中也省略了一些细节。
图 4. 初始化 Servlet 的时序图(查看大图)
Servlet 体系结构
我们知道 Java Web 应用是基于 Servlet 规范运转的,那么 Servlet 本身又是如何运转的呢?为何要设计这样的体系结构。
图 5.Servlet 顶层类关联图
从上图可以看出 Servlet 规范就是基于这几个类运转的,与 Servlet 主动关联的是三个类,分别是 ServletConfig、ServletRequest 和 ServletResponse。这三个类都是通过容器传递给 Servlet 的,其中 ServletConfig 是在 Servlet 初始化时就传给 Servlet 了,而后两个是在请求达到时调用 Servlet 时传递过来的。我们很清楚 ServletRequest 和 ServletResponse 在 Servlet 运行的意义,但是 ServletConfig 和 ServletContext 对 Servlet 有何价值?仔细查看 ServletConfig 接口中声明的方法发现,这些方法都是为了获取这个 Servlet 的一些配置属性,而这些配置属性可能在 Servlet 运行时被用到。而 ServletContext 又是干什么的呢? Servlet 的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”就是两个模块为了交换数据通常都会准备一个交易场景,这个场景一直跟随个这个交易过程直到这个交易完成为止。这个交易场景的初始化是根据这次交易对象指定的参数来定制的,这些指定参数通常就会是一个配置类。所以对号入座,交易场景就由 ServletContext 来描述,而定制的参数集合就由 ServletConfig 来描述。而 ServletRequest 和 ServletResponse 就是要交互的具体对象了,它们通常都是作为运输工具来传递交互结果。
ServletConfig 是在 Servlet init 时由容器传过来的,那么 ServletConfig 到底是个什么对象呢?
下图是 ServletConfig 和 ServletContext 在 Tomcat 容器中的类关系图。
图 6. ServletConfig 在容器中的类关联图
上图可以看出 StandardWrapper 和 StandardWrapperFacade 都实现了 ServletConfig 接口,而 StandardWrapperFacade 是 StandardWrapper 门面类。所以传给 Servlet 的是 StandardWrapperFacade 对象,这个类能够保证从 StandardWrapper 中拿到 ServletConfig 所规定的数据,而又不把 ServletConfig 不关心的数据暴露给 Servlet。
同样 ServletContext 也与 ServletConfig 有类似的结构,Servlet 中能拿到的 ServletContext 的实际对象也是 ApplicationContextFacade 对象。ApplicationContextFacade 同样保证 ServletContex 只能从容器中拿到它该拿的数据,它们都起到对数据的封装作用,它们使用的都是门面设计模式。
通过 ServletContext 可以拿到 Context 容器中一些必要信息,比如应用的工作路径,容器支持的 Servlet 最小版本等。
Servlet 中定义的两个 ServletRequest 和 ServletResponse 它们实际的对象又是什么呢?,我们在创建自己的 Servlet 类时通常使用的都是 HttpServletRequest 和 HttpServletResponse,它们继承了 ServletRequest 和 ServletResponse。为何 Context 容器传过来的 ServletRequest、ServletResponse 可以被转化为 HttpServletRequest 和 HttpServletResponse 呢?
图 7.Request 相关类结构图
上图是 Tomcat 创建的 Request 和 Response 的类结构图。Tomcat 一接受到请求首先将会创建 org.apache.coyote.Request 和 org.apache.coyote.Response,这两个类是 Tomcat 内部使用的描述一次请求和相应的信息类它们是一个轻量级的类,它们作用就是在服务器接收到请求后,经过简单解析将这个请求快速的分配给后续线程去处理,所以它们的对象很小,很容易被 JVM 回收。接下去当交给一个用户线程去处理这个请求时又创建 org.apache.catalina.connector. Request 和 org.apache.catalina.connector. Response 对象。这两个对象一直穿越整个 Servlet 容器直到要传给 Servlet,传给 Servlet 的是 Request 和 Response 的门面类 RequestFacade 和 RequestFacade,这里使用门面模式与前面一样都是基于同样的目的——封装容器中的数据。一次请求对应的 Request 和 Response 的类转化如下图所示:
图 8.Request 和 Response 的转变过程
Servlet 如何工作
我们已经清楚了 Servlet 是如何被加载的、Servlet 是如何被初始化的,以及 Servlet 的体系结构,现在的问题就是它是如何被调用的。
当用户从浏览器向服务器发起一个请求,通常会包含如下信息:http://hostname: port /contextpath/servletpath,hostname 和 port 是用来与服务器建立 TCP 连接,而后面的 URL 才是用来选择服务器中那个子容器服务用户的请求。那服务器是如何根据这个 URL 来达到正确的 Servlet 容器中的呢?
Tomcat7.0 中这件事很容易解决,因为这种映射工作有专门一个类来完成的,这个就是 org.apache.tomcat.util.http.mapper,这个类保存了 Tomcat 的 Container 容器中的所有子容器的信息,当 org.apache.catalina.connector. Request 类在进入 Container 容器之前,mapper 将会根据这次请求的 hostnane 和 contextpath 将 host 和 context 容器设置到 Request 的 mappingData 属性中。所以当 Request 进入 Container 容器之前,它要访问那个子容器这时就已经确定了。
图 9.Request 的 Mapper 类关系图
可能你有疑问,mapper 中怎么会有容器的完整关系,这要回到图 2 中 19 步 MapperListener 类的初始化过程,下面是 MapperListener 的 init 方法代码 :
清单 5. MapperListener.init
1
2
3
4
5
6
7
8
9
10
11
12
13
public void init() {
findDefaultHost();
Engine engine = (Engine) connector.getService().getContainer();
engine.addContainerListener(this);
Container[] conHosts = engine.findChildren();
for (Container conHost : conHosts) {
Host host = (Host) conHost;
if (!LifecycleState.NEW.equals(host.getState())) {
host.addLifecycleListener(this);
registerHost(host);
}
}
}
这段代码的作用就是将 MapperListener 类作为一个监听者加到整个 Container 容器中的每个子容器中,这样只要任何一个容器发生变化,MapperListener 都将会被通知,相应的保存容器关系的 MapperListener 的 mapper 属性也会修改。for 循环中就是将 host 及下面的子容器注册到 mapper 中。
图 10.Request 在容器中的路由图
上图描述了一次 Request 请求是如何达到最终的 Wrapper 容器的,我们现正知道了请求是如何达到正确的 Wrapper 容器,但是请求到达最终的 Servlet 还要完成一些步骤,必须要执行 Filter 链,以及要通知你在 web.xml 中定义的 listener。
接下去就要执行 Servlet 的 service 方法了,通常情况下,我们自己定义的 servlet 并不是直接去实现 javax.servlet.servlet 接口,而是去继承更简单的 HttpServlet 类或者 GenericServlet 类,我们可以有选择的覆盖相应方法去实现我们要完成的工作。
Servlet 的确已经能够帮我们完成所有的工作了,但是现在的 web 应用很少有直接将交互全部页面都用 servlet 来实现,而是采用更加高效的 MVC 框架来实现。这些 MVC 框架基本的原理都是将所有的请求都映射到一个 Servlet,然后去实现 service 方法,这个方法也就是 MVC 框架的入口。
当 Servlet 从 Servlet 容器中移除时,也就表明该 Servlet 的生命周期结束了,这时 Servlet 的 destroy 方法将被调用,做一些扫尾工作。
Session 与 Cookie
前面我们已经说明了 Servlet 如何被调用,我们基于 Servlet 来构建应用程序,那么我们能从 Servlet 获得哪些数据信息呢?
Servlet 能够给我们提供两部分数据,一个是在 Servlet 初始化时调用 init 方法时设置的 ServletConfig,这个类基本上含有了 Servlet 本身和 Servlet 所运行的 Servlet 容器中的基本信息。根据前面的介绍 ServletConfig 的实际对象是 StandardWrapperFacade,到底能获得哪些容器信息可以看看这类提供了哪些接口。还有一部分数据是由 ServletRequest 类提供,它的实际对象是 RequestFacade,从提供的方法中发现主要是描述这次请求的 HTTP 协议的信息。所以要掌握 Servlet 的工作方式必须要很清楚 HTTP 协议,如果你还不清楚赶紧去找一些参考资料。关于这一块还有一个让很多人迷惑的 Session 与 Cookie。
Session 与 Cookie 不管是对 Java Web 的熟练使用者还是初学者来说都是一个令人头疼的东西。Session 与 Cookie 的作用都是为了保持访问用户与后端服务器的交互状态。它们有各自的优点也有各自的缺陷。然而具有讽刺意味的是它们优点和它们的使用场景又是矛盾的,例如使用 Cookie 来传递信息时,随着 Cookie 个数的增多和访问量的增加,它占用的网络带宽也很大,试想假如 Cookie 占用 200 个字节,如果一天的 PV 有几亿的时候,它要占用多少带宽。所以大访问量的时候希望用 Session,但是 Session 的致命弱点是不容易在多台服务器之间共享,所以这也限制了 Session 的使用。
不管 Session 和 Cookie 有什么不足,我们还是要用它们。下面详细讲一下,Session 如何基于 Cookie 来工作。实际上有三种方式能可以让 Session 正常工作:
基于 URL Path Parameter,默认就支持
基于 Cookie,如果你没有修改 Context 容器个 cookies 标识的话,默认也是支持的
基于 SSL,默认不支持,只有 connector.getAttribute("SSLEnabled") 为 TRUE 时才支持
第一种情况下,当浏览器不支持 Cookie 功能时,浏览器会将用户的 SessionCookieName 重写到用户请求的 URL 参数中,它的传递格式如 /path/Servlet;name=value;name2=value2? Name3=value3,其中“Servlet;”后面的 K-V 对就是要传递的 Path Parameters,服务器会从这个 Path Parameters 中拿到用户配置的 SessionCookieName。关于这个 SessionCookieName,如果你在 web.xml 中配置 session-config 配置项的话,其 cookie-config 下的 name 属性就是这个 SessionCookieName 值,如果你没有配置 session-config 配置项,默认的 SessionCookieName 就是大家熟悉的“JSESSIONID”。接着 Request 根据这个 SessionCookieName 到 Parameters 拿到 Session ID 并设置到 request.setRequestedSessionId 中。
请注意如果客户端也支持 Cookie 的话,Tomcat 仍然会解析 Cookie 中的 Session ID,并会覆盖 URL 中的 Session ID。
如果是第三种情况的话将会根据 javax.servlet.request.ssl_session 属性值设置 Session ID。
有了 Session ID 服务器端就可以创建 HttpSession 对象了,第一次触发是通过 request. getSession() 方法,如果当前的 Session ID 还没有对应的 HttpSession 对象那么就创建一个新的,并将这个对象加到 org.apache.catalina. Manager 的 sessions 容器中保存,Manager 类将管理所有 Session 的生命周期,Session 过期将被回收,服务器关闭,Session 将被序列化到磁盘等。只要这个 HttpSession 对象存在,用户就可以根据 Session ID 来获取到这个对象,也就达到了状态的保持。
图 11.Session 相关类图
上从图中可以看出从 request.getSession 中获取的 HttpSession 对象实际上是 StandardSession 对象的门面对象,这与前面的 Request 和 Servlet 是一样的原理。下图是 Session 工作的时序图:
图 12.Session 工作的时序图(查看大图)
还有一点与 Session 关联的 Cookie 与其它 Cookie 没有什么不同,这个配置的配置可以通过 web.xml 中的 session-config 配置项来指定。
Servlet 中的 Listener
整个 Tomcat 服务器中 Listener 使用的非常广泛,它是基于观察者模式设计的,Listener 的设计对开发 Servlet 应用程序提供了一种快捷的手段,能够方便的从另一个纵向维度控制程序和数据。目前 Servlet 中提供了 5 种两类事件的观察者接口,它们分别是:4 个 EventListeners 类型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 个 LifecycleListeners 类型的,ServletContextListener、HttpSessionListener。如下图所示:
图 13.Servlet 中的 Listener(查看大图)
它们基本上涵盖了整个 Servlet 生命周期中,你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 <listener> 标签中。当然也可以在应用程序中动态添加 Listener,需要注意的是 ServletContextListener 在容器启动之后就不能再添加新的,因为它所监听的事件已经不会再出现。掌握这些 Listener 的使用,能够让我们的程序设计的更加灵活。
实战Servlet、Filter和Listener
一个例子
我们先看一段简单的servlet请求响应的代码片段
//HelloServlet类
public class HelloServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
resp.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = resp.getWriter();
printWriter.write("<html><head><head><body><h1>");
printWriter.write("Hello "+name);
printWriter.write("</h1></body></html>");
printWriter.close();
}
}
然后在web.xml注册此servlet
<?xml version="1.0" encoding="utf-8"?>
<web-app version="3.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<display-name>wthfeng 的mvc练习项目</display-name>
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.wthfeng.mymvc.servlet.HelloServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
启动项目,效果如下图所示
这样我们就写完了一个简单的servlet,有关这个例子,我们需要知道:
编写Servlet必须继承HttpServlet或GenericServlet或直接实现Servlet接口。HttpServlet已经为我们封装了有关http请求的相关参数,一般情况下我们直接继承此类即可。
此servlet重写了doGet方法,也就是它可以响应/myprojectName/hello?name=xxx 的请求,并在页面打印xxx的内容。
Servlet的创建和初始化
好了,这些逻辑符合上面所说的,浏览器发出请求/hello,一个名为hello的 servlet被命中,接受请求处理后返回。这里我们再深入一些,为什么要把servlet的定义在web.xml中?web.xml又和servlet容器有什么关系?
web.xml是web项目的入口文件。在项目启动过程中,servlet容器(如tomcat)会读取web.xml并进行相应配置以初始化该项目。
具体容器启动流程如下(以tomcat为例):
先解析tomcat路径下的conf/web.xml等web.xml文件,它们是全局web配置文件,做一些基本配置工作。如注册了default、jsp等servlet。
解析web.xml,将其各个配置项(包括servlet、filter、listener)经处理包装后设在Tomcat的Context容器中,一个 Web 应用对应一个 Context 容器。
创建servlet并初始化。load-on-startup大于1的servlet会在此时初始化。初始化servlet就是调用servlet的init方法。注意:若不设置 此 值,init() 方法只在第一次 HTTP 请求命中时才被调用。此时servlet容器就算启动了。
简而言之就是,web.xml是项目和servlet容器(服务器)关联的桥梁。通过在web.xml注册servlet、filter、listener等,使得项目具有处理特定请求的功能。
2. filter和listener
在有关web servlet的配置中,常能在web.xml看到filter、listener的配置。实际上,filter是过滤器,listener是监听器。这两项配置都是servlet中的重要部分。我们一一来看。
fliter(过滤器)
filter可用于拦截请求,在请求处理前进行一些预处理工作,一般用于处理编码问题,记录日志等。Filter类需实现javax.servlet.Filter接口。其中有3个方法init()、、dofilter()、destroy(),分别用于过滤器的初始化、过滤处理、销毁。
先来个例子
public class MyFilter implements Filter {
private String param;
//初始化方法,在容器启动时调用
public void init(FilterConfig filterConfig) throws ServletException {
//做一些初始化操作
param = filterConfig.getInitParameter("myParam");
System.out.println("filter:"+param);
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//处理请求
chain.doFilter(request,response); //调用下一个过滤器
//处理响应
}
@Override
public void destroy() {
//在servlet销毁后销毁
//做一些销毁后的善后工作
}
}
在 web.xml中添加
<filter>
<filter-name>myFilter</filter-name>
<filter-class>com.wthfeng.mymvc.filter.MyFilter</filter-class>
<init-param>
<param-name>myParam</param-name>
<param-value>myValue</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
配置后filter就会拦截所有servlet请求,需注意:
所有的filter在容器启动时即初始化。
filter的调用顺序为在web.xml中的定义顺序。若多余一个会形成过滤链依次处理。
Listener(监听器)
servlet 监听器用于监听servlet容器中事件变化,当指定事件变化时,会触发注册该事件的监听器。监听器基于观察者模式。
Servlet监听器分为3类,分别用于监听ServletContent(Servlet上下文)、HttpSession(Session),HttpRequest(Request)。
主要有以下几个类:
ServletContextListener //监听Servlet容器创建销毁
ServletContextAttributeListener //监听Servlet容器级别属性的添加及删除
HttpSessionListener //监听Session创建销毁
HttpSessionAttributeListener //监听Session属性创建删除
ServletRequestListener //监听请求创建及销毁
ServletRequestAttributeListener //监听请求属性变化
如我们常见的Spring项目中的如下片段,就是Spring的监听器。其实现了ServletContextListener,用于监听Servlet上下文,以便在项目初始化时加载Spring的必要配置。
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
Servlet 、Filter、Listerer 执行顺序
分别了解了servlet、filter、listener后,来确定一下他们之间的执行顺序。我们已经知道,filter在servlet之前,那listener呢?
应该想到,监听器负责监听事件变化,应该有最先执行的权限,测试一下,我把上述三种都加了日志,打印结果如下:
//开启服务器并请求
listener 容器初始化开始 //servletContent 监听器 contextInitialized()
filter初始化 //filter init()
初始化servlet //servlet init()
listener request初始化 // request 监听器requestInitialized()
开始filter //fiter doFilter() chain.doFilter前
执行servlet //servlet service()
结束filter //fiter doFilter() chain.doFilter后
listener request销毁 //request 监听器 requestInitialized()
//关闭服务器
servlet销毁
filter销毁
listener 容器销毁
从此我们可以得出结论:
对于涉及3者的部分,顺序为 listener - filter - servlet
filter和servlet的初始化部分,先filter后servlet
销毁或结束顺序为加载顺序的反序
##3. 有关Servlet的注解
Servlet3后,添加了若干关于servlet,filter,listener的注解支持。分别为@WebServlet,@WebFilter,@Webistener。也就是说,可以不用web.xml 配置文件了。注册相关类型可直接在类上添加相应注解。
如,添加一个servlet上下文的监听器。可使用如下方式。
@WebListener
public class ContentListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
System.out.println("listener 容器初始化开始");
}
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("listener 容器销毁");
}
}
相当方便有木有。。。
总结
本文涉及到内容有点多,要把每个细节都说清楚,似乎不可能,本文试着从 Servlet 容器的启动到 Servlet 的初始化,以及 Servlet 的体系结构等这些环节中找出一些重点来讲述,目的是能读者有一个总体的完整的结构图,同时也详细分析了其中的一些难点问题,希望对大家有所帮助。
Javaweb复习资料(二)(上)
一、Tomcat1.1 Tomcat介绍1.1.1 关于服务器服务器的概念非常的广泛,它可以指代一台特殊的计算机(相比普通计算机运行更快、负载更高、价格更贵),也可以指代用于部署网站的应用。我们这里说的服务器,其实是web服务器,或者应用服务器。它本质就是一个软件,一个应用。作用就是发布我们的应用(工程),让用户可以通过浏览器访问我们的应用。常见的应用服务器,请看下表:1.1.2 Tomcat下载与安装Tomcat官网下载地址详细安装与配置可以参考下面的博文:https://yangyongli.blog.csdn.net/article/details/1178224181.1.3 Tomcat各版本所需支持1.1.4 Tomcat目录结构详解1.2 Tomcat基本使用1.2.1 Tomcat启动和停止及问题分析解决1)启动和停止Tomcat服务器的启动文件在二进制文件目录中:,这两个文件就是Tomcat的启动文件。Tomcat服务器的停止文件也在二进制文件目录中:,这两个文件就是Tomcat的停止文件。2)启动时问题第一个问题:启动一闪而过原因:没有配置环境变量。解决办法:配置上JAVA_HOME环境变量第二个:Address already in use : JVM_Bind原因:端口被占用解决办法:找到占用该端口的应用 进程不重要:使用cmd命令:netstat -a -o 查看pid 在任务管理器中结束占用端口的进程。 进程很重要:修改自己的端口号。修改的是Tomcat目录下\conf\server.xml中的配置。第三个:启动产生很多异常,但能正常启动原因:Tomcat中部署着很多项目,每次启动这些项目都会启动。而这些项目中有启动报异常的。解决办法: 能找到报异常的项目,就把它从发布目录中移除。 不能确定报异常的项目,就重新解压一个新的Tomcat。第四个:其它问题例如:启动产生异常,但是不能正常启动。此时就需要解压一个新的Tomcat启动,来确定是系统问题,还是Tomcat的问题。所以,此时就需要具体问题,具体分析,然后再对症解决。1.2.2 IDEA集成Tomcat服务器第一步第二步第三步第四步1.1 Servlet概述Servlet是SUN公司提供的一套规范,名称就叫Servlet规范,它也是JavaEE规范之一。我们可以像学习Java基础一样,通过API来学习Servlet。这里需要注意的是,在我们之前JDK的API中是没有Servlet规范的相关内容,需要使用JavaEE的API。目前在Oracle官网中的最新版本是JavaEE8,该网址中介绍了JavaEE8的一些新特性。当然,我们可以通过访问官方API,学习和查阅里面的内容。打开官方API网址,在左上部分找到javax.servlet包,在左下部分找到Servlet,如下图显示:通过阅读API,我们得到如下信息:第一:Servlet是一个运行在web服务端的java小程序第二:它可以用于接收和响应客户端的请求第三:要想实现Servlet功能,可以实现Servlet接口,继承GenericServlet或者HttpServlet第四:每次请求都会执行service方法第五:Servlet还支持配置具体请看下图:二、servlet基础1.2 Servlet入门1.2.1 Servlet编写步骤1.2.1.1 编码步骤第一步:前期准备-创建JavaWeb工程第二步:编写一个普通类继承GenericServlet并重写service方法第三步:在web.xml配置Servlet1.2.1.2 测试在Tomcat中部署项目,然后在浏览器访问Servlet出现下面这就成功了。1.2.2 Servlet执行过程分析我们通过浏览器发送请求,请求首先到达Tomcat服务器,由服务器解析请求URL,然后在部署的应用列表中找到我们的应用。接下来,在我们的应用中找应用里的web.xml配置文件,在web.xml中找到FirstServlet的配置,找到后执行service方法,最后由FirstServlet响应客户浏览器。整个过程如下图所示:一句话总结执行过程:浏览器——>Tomcat服务器——>我们的应用——>应用中的web.xml——>FirstServlet——>响应浏览器我们可以看到,在编译器里的部分只是后面web.xml到Servlet的部分。1.2.3 Servlet类视图在《Tomcat和Http协议》这天课程和刚才的入门案例中,我们都定义了自己的Servlet,实现的方式都是选择继承GenericServlet,在Servlet的API介绍中,它提出了我们除了继承GenericServlet外还可以继承HttpServlet,通过查阅servlet的类视图,我们看到GenericServlet还有一个子类HttpServlet。同时,在service方法中还有参数ServletRequest和ServletResponse,它们的关系如下图所示:1.2.4 Servlet编写方式1.2.4.1 编写方式说明(※)https://yangyongli.blog.csdn.net/article/details/1178717871.2.4.2 HttpServlet的使用细节第一步:在入门案例的工程中创建一个Servlet继承HttpServlet注意:不要重写任何方法,如下图所示:第二步:部署项目并测试访问当我们在地址栏输入ServletDemo2的访问URL时,出现了访问错误,状态码是405。提示信息是:方法不允许。第三步:分析原因得出HttpServlet的使用结论: 我们继承了HttpServlet,需要重写里面的doGet和doPost方法来接收get方式和post方式的请求。为了实现代码的可重用性,我们只需要在doGet或者doPost方法中一个里面提供具体功能即可,而另外的那个方法只需要调用提供了功能的方法。三、Servlet细节进阶(能懂就懂)Servlet使用细节1.1 Servlet的生命周期对象的生命周期,就是对象从生到死的过程,即:出生——活着——死亡。用更偏向 于开发的官方说法就是对象创建到销毁的过程。出生(对象的建立):请求第一次到达Servlet时,对象就创建出来,并且初始化成功。只出生一次,就放到内存中。活着(对象的应用):服务器提供服务的整个过程中,该对象一直存在,每次只是执行service方法。死亡(对象的销毁):当服务停止时,或者服务器宕机时,对象消亡。通过分析Servlet的生命周期我们发现,它的实例化和初始化只会在请求第一次到达Servlet时执行,而销毁只会在Tomcat服务器停止时执行。由此我们得出一个结论,Servlet对象只会创建一次,销毁一次。所以,Servlet对象只有一个实例。如果一个对象实例在应用中是唯一的存在,那么我们就说它是单实例的,即运用了单例模式1.2 Servlet的线程安全由于Servlet运用了单例模式,即整个应用中只有一个实例对象,所以我们需要分析这个唯一的实例中的类成员是否线程安全。接下来,我们来看下面的的示例:/*
Servlet线程安全
*/
public class ServletDemo04 extends HttpServlet{
//1.定义用户名成员变量:定义在类里面,这样就会发生线程安全问题,造成数据共享
private String username = null;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//2.获取用户名
username = req.getParameter("username");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//3.获取输出流对象
PrintWriter pw = resp.getWriter();
//4.响应给客户端浏览器
pw.print("welcome:" + username);
//5.关流
pw.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
启动两个浏览器,输入不同的参数,访问之后发现输出的结果都是一样,所以出现线程安全问题例如下图是上面案例的测试结果通过上面的测试我们发现,在Servlet中定义了类成员之后,多个浏览器都会共享类成员的数据。其实每一个浏览器端发送请求,就代表是一个线程,那么多个浏览器就是多个线程,所以测试的结果说明了多个线程会共享Servlet类成员中的数据,其中任何一个线程修改了数据,都会影响其他线程。因此,我们可以认为Servlet它不是线程安全的。分析产生这个问题的根本原因,其实就是因为Servlet是单例,单例对象的类成员只会随类实例化时初始化一次,之后的操作都是改变,而不会重新初始化。解决方法解决这个问题也非常简单,就是在Servlet中定义类成员要慎重。如果类成员是共用的,并且只会在初始化时赋值,其余时间都是获取的话,那么是没问题。如果类成员并非共用,或者每次使用都有可能对其赋值,那么就要考虑线程安全问题了,把它定义到doGet或者doPost方法里面去就可以了。还可以再给他进一步加上线程锁,对数据进行保护。可以参考下面改进后的代码/*
Servlet线程安全
*/
public class ServletDemo04 extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.定义用户名成员变量:放在doGet里,作为临时变量
String username = null;
// 加上线程同步锁,进一步保护数据
synchronized (this) {
//2.获取用户名
username = req.getParameter("username");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//3.获取输出流对象
PrintWriter pw = resp.getWriter();
//4.响应给客户端浏览器
pw.print("welcome:" + username);
//5.关流
pw.close();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
1.3 Servlet的注意事项1.3.1 映射Servlet的细节Servlet支持三种映射方式,以达到灵活配置的目的。首先编写一个Servlet,代码如下:/**
* 演示Servlet的映射方式
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public class ServletDemo5 extends HttpServlet {
/**
* doGet方法输出一句话
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("ServletDemo5接收到了请求");
}
/**
* 调用doGet方法
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
第一种:指名道姓的方式 此种方式,只有和映射配置一模一样时,Servlet才会接收和响应来自客户端的请求。 例如:映射为:/servletDemo5 访问URL:http://localhost:8585/servlet_demo/servletDemo5第二种:/开头+通配符的方式 此种方式,只要符合目录结构即可,不用考虑结尾是什么。 例如:映射为:/servlet/* 访问URL:http://localhost:8585/servlet/itheima http://localhost:8585/servlet/itcast.do 这两个URL都可以。因为用的*,表示/servlet/后面的内容是什么都可以。第三种:通配符+固定格式结尾 此种方式,只要符合固定结尾格式即可,其前面的访问URI无须关心(注意协议,主机和端口必须正确) 例如:映射为:*.do 访问URL:http://localhost:8585/servlet/itcast.do http://localhost:8585/itheima.do 这两个URL都可以方法。因为都是以.do作为结尾,而前面用*号通配符配置的映射,所有无须关心。第四种:多种映射顺序通过测试我们发现,Servlet支持多种配置方式,但是由此也引出了一个问题,当有两个及以上的Servlet映射都符合请求URL时,由谁来响应呢?注意:HTTP协议的特征是一请求一响应的规则。那么有一个请求,必然有且只有一个响应。所以,我们接下来明确一下,多种映射规则的优先级。先说结论:指名道姓的方式优先级最高,带有通配符的映射方式,有/的比没/的优先级高所以,我们前面讲解的三种映射方式的优先级为:第一种>第二种>第三种。演示代码如下:/**
* 它和ServletDemo5组合演示Servlet的访问优先级问题
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public class ServletDemo6 extends HttpServlet {
/**
* doGet方法输出一句话
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("ServletDemo6接收到了请求");
}
/**
* 调用doGet方法
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
<!--配置ServletDemo6-->
<servlet>
<servlet-name>servletDemo6</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo6</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo6</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
运行结果如下:1.3.2 多路径映射Servlet上面我们讲解了Servlet的多种映射方式,这一小节我们来介绍一下,一个Servlet的多种路径配置的支持。它其实就是给一个Servlet配置多个访问映射,从而可以根据不同请求URL实现不同的功能。首先,创建一个Servlet:/**
* 演示Servlet的多路径映射
*/
public class ServletDemo7 extends HttpServlet {
/**
* 根据不同的请求URL,做不同的处理规则
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取当前请求的URI
String uri = req.getRequestURI();
uri = uri.substring(uri.lastIndexOf("/"),uri.length());
//2.判断是1号请求还是2号请求
if("/servletDemo7".equals(uri)){
System.out.println("ServletDemo7执行1号请求的业务逻辑:商品单价7折显示");
}else if("/demo7".equals(uri)){
System.out.println("ServletDemo7执行2号请求的业务逻辑:商品单价8折显示");
}else {
System.out.println("ServletDemo7执行基本业务逻辑:商品单价原价显示");
}
}
/**
* 调用doGet方法
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
接下来,在web.xml配置Servlet:<!--配置ServletDemo7-->
<servlet>
<servlet-name>servletDemo7</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo7</servlet-class>
</servlet>
<!--映射路径1-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/demo7</url-pattern>
</servlet-mapping>
<!--映射路径2-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/servletDemo7</url-pattern>
</servlet-mapping>
<!--映射路径3-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/servlet/*</url-pattern>
</servlet-mapping>
最后,启动服务测试运行结果:1.3.3 启动时创建Servlet我们前面讲解了Servlet的生命周期,Servlet的创建默认情况下是请求第一次到达Servlet时创建的。但是我们都知道,Servlet是单例的,也就是说在应用中只有唯一的一个实例,所以在Tomcat启动加载应用的时候就创建也是一个很好的选择。那么两者有什么区别呢?第一种:应用加载时创建Servlet,它的优势是在服务器启动时,就把需要的对象都创建完成了,从而在使用的时候减少了创建对象的时间,提高了首次执行的效率。它的弊端也同样明显,因为在应用加载时就创建了Servlet对象,因此,导致内存中充斥着大量用不上的Servlet对象,造成了内存的浪费。第二种:请求第一次访问是创建Servlet,它的优势就是减少了对服务器内存的浪费,因为那些一直没有被访问过的Servlet对象都没有创建,因此也提高了服务器的启动时间。而它的弊端就是,如果有一些要在应用加载时就做的初始化操作,它都没法完成,从而要考虑其他技术实现。方式的选择通过上面的描述,相信同学们都能分析得出何时采用第一种方式,何时采用第二种方式。就是当需要在应用加载就要完成一些工作时,就需要选择第一种方式。当有很多Servlet的使用时机并不确定是,就选择第二种方式。在web.xml中是支持对Servlet的创建时机进行配置的,配置的方式如下:我们就以ServletDemo3为例。<!--配置ServletDemo3-->
<servlet>
<servlet-name>servletDemo3</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo3</servlet-class>
<!--配置Servlet的创建顺序,当配置此标签时,Servlet就会改为应用加载时创建
配置项的取值只能是正整数(包括0),数值越小,表明创建的优先级越高
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo3</servlet-name>
<url-pattern>/servletDemo3</url-pattern>
</servlet-mapping>
1.3.4 默认Servlet默认Servlet是由服务器提供的一个Servlet,它配置在Tomcat的conf目录下的web.xml中。如下图所示:它的映射路径是<url-pattern>/<url-pattern>,我们在发送请求时,首先会在我们应用中的web.xml中查找映射配置,找到就执行,这块没有问题。但是当找不到对应的Servlet路径时,就去找默认的Servlet,由默认Servlet处理。所以,一切都是Servlet。1.4 Servlet关系总图
后台开发常问面试题集锦(问题搬运工,附链接)
Java基础问题
String的’+’的性能及原理
java之yield(),sleep(),wait()区别详解-备忘笔记
深入理解Java Stream流水线
抽象 & abstract关键字
Java final 修饰符知识点总结(必看篇)
Java中的static关键字解析
Java 回调机制解读
Java抽象类与oop三大特征
Java时间和时间戳的相互转换
Java为什么要使用内部类?
Synchronized(对象锁)和Static Synchronized(类锁)的区别
select,poll,epoll优缺点及比较
java提高篇(八)—-详解内部类
单例模式和双重检查锁定
java中实现多态的机制是什么?
java提高篇(四)—–理解java的三大特性之多态
java类静态域、块,非静态域、块,构造函数的初始化顺序
Java 中的 String 为什么是不可变的?
Java的string类为什么是不可变的
Java泛型深入理解
静态内部类和非静态内部类的区别
十分钟理解Java中的弱引用
底层原理(JVM、JMM、Java源码、并发等)
JVM如何操作缓冲区
成为Java GC专家(3)—如何优化Java垃圾回收机制
JVM性能调优(重要)
Java8系列之重新认识HashMap
解读克隆
如何正确遍历删除List中的元素,你会吗?
keySet和entrySet效率比较
Thread的run()与start()的区别
JAVA并发编程学习笔记之ReentrantLock
百度笔试题:malloc/free与new/delete的区别
JAVA集合框架中的常用集合及其特点、适用场景、实现原理简介
JAVA并发编程学习笔记之ReentrantLock
详解Java中LinkedHashMap
请求Servlet流程以及Servlet生命周期
java finalize方法总结、GC执行finalize的过程
JVM系列三:JVM参数设置、分析
Java泛型总结
Java并发之CountDownLatch、CyclicBarrier和Semaphore
使用LinkedHashMap构建LRU的Cache
谈谈ConcurrentHashMap1.7和1.8的不同实现
HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别
Java 四种线程池
JDK与设计模式:命令模式
细数JDK里的设计模式
java多线程(二)——用到的设计模式
Java并发核心基础——线程池使用及底层实现机制详解
单例模式的优缺点和使用场景
Java 8 中的ConcurrentHashMap源码分析
线程池
框架类问题(Spring、SpringMVC、Mybatis等)
Spring AOP的实现原理
git干货系列:(三)我提交错了我想撤销或者回退版本
Java Web应用中支持跨域请求
69道Spring面试题和答案
深入理解 Spring 事务原理
Spring事务管理(详解+实例)
MyBatis动态SQL底层原理分析
简述Spring容器与SpringMVC的容器的联系与区别
Spring AOP 实现原理
jsp 九大内置对象和其作用详解
浅谈 SOAP
Maven如何解决包冲突问题
maven中如何将所有引用的jar包打包到一个jar中
MyBatis 拦截器原理探究
Tomcat负载均衡原理详解及配置
SpringMVC一个Controller处理所有用户请求的并发问题
算法问题
AVL树、红黑树、B/B+树和Trie树的比较
Bloom Filter概念和原理
双向链表的插入及删除图解
搜索算法集锦
关于B树的一些总结
Redis中5种数据结构的使用场景介绍
字典树(Trie树)实现与应用
实现一个 能在O(1)时间复杂度 完成 Push、Pop、Min操作的栈
常见的安全算法
面试题:支持O(1)时间内完成pop,push和max的栈
教你如何迅速秒杀掉:99%的海量数据处理面试题
单例模式应用场景
每天进步一点点——五分钟理解一致性哈希算法(consistent hashing)
为什么说B+-tree比B 树更适合实际应用中操作系统的文件索引和数据库索引?
B+树
基础课程(数据库、计算机网络、计算机组成原理、操作系统等)
高并发访问数据库优化方法
简单理解Socket及TCP/IP、Http、Socket的区别
虚拟内存机制
几种网络I/O模型
从输入url到显示网页,后台发生了什么?
网页打开时都发生了什么?我被吓着了
线程返回值的方式介绍
SSL交互和握手过程
SQL学习笔记——之SQL优化经验总结34条
HTTP长连接和短连接
进程?线程?到底共享了什么私有了什么
MySQL 慢查询日志(Slow Query Log)
TCP/IP详解–TIME_WAIT状态存在的原因
操作系统:内存管理(概念)
从关系型数据库到非关系型数据库
drop、truncate和delete的区别
线程的几种状态转换
MySQL的进阶实战篇
关于 MySQL 你可能不知道的 SQL 使用技巧
MySQL优化必须调整的10项配置
浏览器缓存机制剖析
MySQL数据的主从复制、半同步复制和主主复制详解-转
8 种 NoSQL 数据库系统对比
mysql 主从复制原理
Java里如何实现线程间通信?
理解多线程设计模式
其他
getAttribute与getParame的区别
大型网站架构之分布式消息队列
消息队列的两种模式
初窥Linux 之 我最常用的20条命令
常见性能优化策略总结
缓存、缓存算法和缓存框架简介
如何设计一个秒杀系统
Redis初级介绍
大型网站系统架构的演化
浅谈负载均衡技术与分类
一分钟了解负载均衡的一切
保证分布式系统数据一致性的6种方案
欢迎大家关注我的个人公众号
欢迎加入QQ学习交流群(内有干货):
Spring经典高频面试题,原来是长这个样子
本文选自《Spring 5核心原理与30个类手写实战》一书,文末参与互动有机会赢取本书。本文题目目录:
1 什么是Spring框架,Spring框架有哪些主要模块2 使用Spring框架能带来哪些好处3 什么是控制反转(IoC),什么是依赖注入4 在Java中依赖注入有哪些方式5 BeanFactory和ApplicationContext有什么区别6 Spring提供几种配置方式来设置元数据7 如何使用XML配置方式配置Spring8 Spring提供哪些配置形式9 怎样用注解的方式配置Spring10 请解释Spring Bean的生命周期11 更多经典高频面试题
1 什么是Spring框架,Spring框架有哪些主要模块Spring框架是一个为Java应用程序开发提供综合、广泛的基础性支持的Java平台。Spring帮助开发者解决了开发中基础性的问题,使得开发人员可以专注于应用程序的开发。Spring框架本身也是按照设计模式精心打造的,这使得我们可以在开发环境中安心地集成Spring框架,不必担心Spring是如何在后台工作的。
2 使用Spring框架能带来哪些好处下面列举了一些使用Spring框架带来的主要好处。(1)Dependency Injection(DI)使得构造器和JavaBean properties文件中的依赖关系一目了然。(2)与EJB容器相比较,IoC容器更加趋向于轻量级。这样一来使用IoC容器在有限的内存和CPU资源的情况下进行应用程序的开发和发布就变得十分有利。(3)Spring并没有闭门造车,Spring利用了已有的技术,比如ORM框架、logging框架、J2EE、Quartz和JDK Timer,以及其他视图技术。(4)Spring框架是按照模块的形式来组织的。由包和类的编号就可以看出其所属的模块,开发者只需选用需要的模块即可。(5)要测试一个用Spring开发的应用程序十分简单,因为测试相关的环境代码都已经囊括在框架中了。更加简单的是,利用JavaBean形式的POJO类,可以很方便地利用依赖注入来写入测试数据。(6)Spring的Web框架也是一个精心设计的Web MVC框架,为开发者在Web框架的选择上提供了一个除主流框架(比如Struts)和过度设计的、不流行Web框架以外的选择。(7)Spring提供了一个便捷的事务管理接口,适用于小型的本地事务处理(比如在单DB的环境下)和复杂的共同事务处理(比如利用JTA的复杂DB环境)。
3 什么是控制反转(IoC),什么是依赖注入(1)控制反转是应用于软件工程领域的,在运行时被装配器对象用来绑定耦合对象的一种编程技巧,对象之间的耦合关系在编译时通常是未知的。在传统的编程方式中,业务逻辑的流程是由应用程序中早已被设定好关联关系的对象来决定的。在使用控制反转的情况下,业务逻辑的流程是由对象关系图来决定的,该对象关系图由装配器负责实例化,这种实现方式还可以将对象之间的关联关系的定义抽象化。绑定的过程是通过“依赖注入”实现的。(2)控制反转是一种以给予应用程序中目标组件更多控制为目的设计范式,并在实际工作中起到了有效的作用。(3)依赖注入是在编译阶段尚未知所需的功能是来自哪个的类的情况下,将其他对象所依赖的功能对象实例化的模式。这就需要一种机制来激活相应的组件以提供特定的功能,所以依赖注入是控制反转的基础。否则如果在组件不受框架控制的情况下,框架又怎么知道要创建哪个组件呢?
4 在Java中依赖注入有哪些方式(1)构造器注入。(2)Setter方法注入。(3)接口注入。
5 BeanFactory和ApplicationContext有什么区别BeanFactory 可以理解为含有Bean集合的工厂类。BeanFactory 包含了Bean的定义,以便在接收到客户端请求时将对应的Bean实例化。BeanFactory还能在实例化对象时生成协作类之间的关系。此举将Bean自身从Bean客户端的配置中解放出来。BeanFactory还包含Bean生命周期的控制,调用客户端的初始化方法(Initialization Method)和销毁方法(Destruction Method)。从表面上看,ApplicationContext如同BeanFactory一样具有Bean定义、Bean关联关系的设置及根据请求分发Bean的功能。但ApplicationContext在此基础上还提供了其他功能。(1)提供了支持国际化的文本消息。(2)统一的资源文件读取方式。(3)已在监听器中注册的Bean的事件。以下是三种较常见的 ApplicationContext 实现方式。(1) ClassPathXmlApplicationContext: 从ClassPath的XML配置文件中读取上下文,并生成上下文定义。应用程序上下文从程序环境变量中取得。ApplicationContext context = new ClassPathXmlApplicationContext(“application.xml”);
(2)FileSystemXmlApplicationContext :由文件系统中的XML配置文件读取上下文。
ApplicationContext context = new FileSystemXmlApplicationContext(“application.xml”);
(3)XmlWebApplicationContext:由Web应用的XML文件读取上下文。
6 Spring提供几种配置方式来设置元数据Spring提供以下三种配置方式来设置元数据:(1)基于XML的配置。(2)基于注解的配置。(3)基于Java的配置。
7 如何使用XML配置方式配置Spring在Spring框架中,依赖和服务需要专门的配置文件实现,一般用XML格式的配置文件。这些配置文件的格式采用公共的模板,由一系列的Bean定义和专门的应用配置选项组成。 Spring XML配置的主要目的是使所有的Spring组件都可以用XML文件的形式来进行配置。这意味着不会出现其他的Spring配置类型(比如声明配置方式或基于Java Class的配置方式)。Spring的XML配置方式是使用被Spring命名空间所支持的一系列的XML标签来实现的。Spring主要的命名空间有context、beans、jdbc、tx、aop、mvc和aso。例如:<beans>
<!-- JSON Support -->
<bean name="viewResolver"
class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<bean name="jsonTemplate"
class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
<bean id="restTemplate" class="org.springframework.web.client.RestTemplate"/>
</beans>
下面这个web.xml仅配置了DispatcherServlet,最简单的配置便能满足应用程序配置运行时组件的需求。
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
8 Spring提供哪些配置形式Spring对Java配置的支持是由@Configuration注解和@Bean注解来实现的。由@Bean注解的方法将会实例化、配置和初始化一个新对象,这个对象将由Spring的IoC容器来管理。@Bean声明所起到的作用与元素类似。被@Configuration所注解的类则表示这个类的主要目的是作为Bean定义的资源。被@Configuration声明的类可以通过在同一个类内部调用@bean方法来设置嵌入Bean的依赖关系。最简单的@Configuration 声明类请参考下面的代码:@Configuration
public class AppConfig{
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
与上面的@Beans配置文件相同的XML配置文件如下:
<beans>
<bean id="myService" class="com.gupaoedu.services.MyServiceImpl"/>
</beans>
上述配置方式的实例化方式如下:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
要使用组件扫描,仅需用@Configuration进行注解即可:
@Configuration
@ComponentScan(basePackages = "com.gupaoedu")
public class AppConfig {
}
在上面的例子中,com.gupaoedu包首先会被扫描到,然后在容器内查找被@Component 声明的类,找到后将这些类按照Spring Bean定义进行注册。 如果你要在Web应用开发中选用上述配置方式,需要用AnnotationConfigWebApplicationContext类来读取配置文件,可以用来配置Spring的Servlet监听器ContrextLoaderListener或者Spring MVC的DispatcherServlet。例如:
<web-app>
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.gupaoedu.AppConfig</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.gupaoedu.web.MVCConfig</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/web/*</url-pattern>
</servlet-mapping>
</web-app>
9 怎样用注解的方式配置SpringSpring在2.5版本以后开始支持用注解的方式配置依赖注入。可以用注解的方式来替代XML方式的Bean描述,可以将Bean描述转移到组件类的内部,只需要在相关类上、方法上或者字段声明上使用注解即可。注解注入将会被容器在XML注入之前处理,所以后者会覆盖前者对于同一个属性的处理结果。注解装配在Spring中是默认关闭的,需要在Spring文件中进行配置才能使用基于注解的装配模式。如果你想要在应用程序中使用注解的方式,请参考如下配置:<beans>
<context:annotation-config/>
</beans>
配置完成以后,就可以用注解的方式在Spring中向属性、方法和构造方法中自动装配变量。下面是几种比较重要的注解类型。(1)@Required:该注解应用于设值方法。 (2)@Autowired:该注解应用于设值方法、非设值方法、构造方法和变量。 (3)@Qualifier:该注解和@Autowired注解搭配使用,用于消除特定Bean自动装配的歧义。 (4)JSR-250 Annotations:Spring支持基于JSR-250 注解的注解,即@Resource、@PostConstruct和@PreDestroy。
10 请解释Spring Bean的生命周期Spring Bean的生命周期简单易懂。在一个Bean实例被初始化时,需要执行一系列初始化操作以使其达到可用的状态。同样,当一个Bean不再被调用时需要进行相关的析构操作,并从Bean容器中移除。Spring Bean Factory 负责管理在Spring容器中被创建的Bean的生命周期。Bean的生命周期由两组回调方法组成。 (1)初始化之后调用的回调方法。 (2)销毁之前调用的回调方法。 Spring提供了以下4种方式来管理Bean的生命周期事件:(1)InitializingBean和DisposableBean回调接口。(2)针对特殊行为的其他Aware接口。(3)Bean配置文件中的 customInit() 方法和 customDestroy() 方法。(4)@PostConstruct和@PreDestroy注解方式。使用customInit()和 customDestroy()方法管理Bean生命周期的代码样例如下:<beans>
<bean id="demoBean" class="com.gupaoedu.task.DemoBean"
init-Method="customInit" destroy-Method="customDestroy">
</bean>
</beans>
更多经典高频面试题
11 Spring Bean作用域的区别是什么
12 什么是Spring Inner Bean
13 Spring中的单例Bean是线程安全的吗
14 请举例说明如何在Spring中注入一个Java集合
15 如何向Spring Bean中注入java.util.Properties
16 请解释Spring Bean的自动装配
17 自动装配有哪些局限性
18 请解释各种自动装配模式的区别
19 请举例解释@Required注解
20 请举例说明@Qualifier注解
21 构造方法注入和设值注入有什么区别
22 Spring中有哪些不同类型的事件
23 FileSystemResource和ClassPathResource有什么区别
24 Spring中用到了哪些设计模式
25 在Spring中如何更有效地使用JDBC
26 请解释Spring中的IoC容器
27 在Spring中可以注入null或空字符串吗
题目详解请见《Spring 5核心原理与30个类手写实战》一书。
《Spring 5核心原理与30个类手写实战》一书基于编程开发实践,不仅深度解析Spring 5的原理与新特性,更从环境准备、顶层结构设计、数据访问等方面一步步地推导出Spring的设计原理。在每个知识点上,均以大量的经典代码案例辅助讲解,使理论紧密联系实际。最后手写30个类,以体会Spring作者的创作过程,让每一位读者学以致用。
对于立志成为Java架构师的技术人员,以及对以Spring为核心的Java Web开发感兴趣的计算机专业高校生、在职Java开发人员来说,本书是一本具备超强实战意义的技术升级指南。读者通过本书可以看源码不再“晕车”,轻松找到入口;系统学习设计思想,提高解决问题的效率;培养架构思维能力,以及自驱学习能力。
Spring 5的首要特性是新的反应式编程模型,这代表着对提供可无缝扩展、基于Spring的响应式服务的重大保障。随着人们对Spring 5的采用,反应式编程有望成为使用Java语言的Web和企业应用程序开发的未来。未来的Spring将继续体现这一承诺,因为Spring Security、Spring Data和Spring Integration有望采用反应式编程的特征和优势。总之,Spring 5代表着一次大受Spring开发人员欢迎的华丽转变,同时也为其他框架指出了一条发展之路。Spring 5的升级也为Spring Boot、Spring Cloud提供了非常丰富的经验,Spring不只是一个框架,已然成了一个编程生态。