平时我们在写一般的应用程序的时候,无论如何都会有一个main函数入口。而在进行web开发的时候,从头到尾我们都没有写过一个main函数。最后部署时,打了一个war包,传到web容器下面就可以了。 到底这后面发生了什么,带着疑问让我开始吧。
先了解一下java web应用目录组织结构:
在看看一个浏览器与服务器交互的流程:
在java web项目中,我们无论是用什么框架,Spring MVC也好,Spring Boot也好,最后都是Servlet在起着决定性的作用。
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。
Servlet在Web应用程序的位置如下图所示:
Servlet主要作用有:
web容器收到客户端的访问请求后将进行如下处理:
具体来讲可以用下面的图说明:
其时序图可以用下图表示:
有了上面的理论基础我们就可以写一个例子加深理解。最终实现一个现实header的效果。
源码如下:
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class DisplayHeaderServletextends HttpServlet{
// 处理 GET 方法请求的方法
public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException
{
// 设置响应内容类型
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
String title = "HTTP Header 请求";
String docType =
"<!DOCTYPE html> /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>Header 名称</th><th>Header 值</th>/n"+
"</tr>/n");
Enumeration headerNames = request.getHeaderNames();
while(headerNames.hasMoreElements()) {
String paramName = (String)headerNames.nextElement();
out.print("<tr><td>" + paramName + "</td>/n");
String paramValue = request.getHeader(paramName);
out.println("<td> " + paramValue + "</td></tr>/n");
}
out.println("</table>/n</body></html>");
}
// 处理 POST 方法请求的方法
public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
doGet(request, response);
}
}
写完之后,还需要将文件便以为class文件,项目部署到tomcat目录下。启动tomcat Web容器访问对应路径即可。
在web.xml中配置一下加载的Servlet
<servlet>
<!-- servlet名,一般写成类名,并不一定严格是类名 -->
<servlet-name>DisplayHeaderServlet</servlet-name>
<!-- 所在的包 -->
<servlet-class>com.xxx.xxx.DisplayHeaderServlet</servlet-class>
</servlet>
<servlet-mapping>
<!-- 与上面的servlet-name相对应-->
<servlet-name>DisplayHeaderServlet</servlet-name>
<!-- 访问的网址 -->
<url-pattern>/TomcatTest/DisplayHeaderServlet</url-pattern>
</servlet-mapping>
由于客户端是通过URL地址访问web服务器中的资源,所以Servlet程序若想被外界访问,必须把servlet程序映射到一个URL地址上。
这个工作在web.xml文件中使用 <servlet> 元素和 <servlet-mapping> 元素完成。 <servlet> 元素用于注册Servlet,它包含有两个主要的子元素: <servlet-name> 和 <servlet-class> ,分别用于设置Servlet的注册名称和Servlet的完整类名。
一个 <servlet-mapping> 元素用于映射一个已注册的Servlet的一个对外访问路径,它包含有两个子元素: <servlet-name> 和 <url-pattern> ,分别用于指定Servlet的注册名称和Servlet的对外访问路径。
<servlet-mapping> 元素的 <servlet-name> 子元素的设置值可以是同一个Servlet的注册名。 Caused by: java.lang.IllegalArgumentException: The servlets named [xxx] and [xxx] are both mapped to the url-pattern xxx "*.扩展名" ,另一种格式是以正斜杠(/)开头并以 "/*" 结尾 但是如果存在了冲突,比如都有 * 。规则又是怎样的呢?
| 请求Url | Url1 | Url2 | 规则 |
|---|---|---|---|
| /abc/a.html | /abc/* | /* | Servlet引擎将调用/abc/* |
| /abc | /abc/* | /abc | Servlet引擎将调用/abc |
| /abc/a.do | /abc/* | *.do | Servlet引擎将调用/abc/* |
| /a.do | /* | *.do | Servlet引擎将调用/* |
| /xxx/yyy/a.do | /* | *.do | Servlet引擎将调用/* |
匹配的原则就是”谁长得更像就找谁”。但是当请求url完全匹配的时候就走完全匹配的rul。如上面的第二条。
Servlet是一个供其他Java程序(Servlet引擎,比如tomcat web容器)调用的Java类,它不能独立运行,它的运行完全由Servlet引擎来控制和调度。这也是开发Web项目没有些main方法的原因。
针对客户端的多次Servlet请求,通常情况下,服务器只会创建一个Servlet实例对象, 也就是说Servlet实例对象一旦创建,它就会驻留在内存中,为后续的其它请求服务,直至web容器退出,servlet实例对象才会销毁 。
在Servlet的整个生命周期内,Servlet的init方法只被调用一次。而对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。
如果在 <servlet> 元素中配置了一个 <load-on-startup> 元素, 那么WEB应用程序在启动时,就会装载并创建Servlet的实例对象、以及调用Servlet实例对象的init()方法。
<servlet>
<servlet-name>invoker</servlet-name>
<servlet-class>
org.apache.catalina.servlets.InvokerServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
可以用于为web应用写一个InitServlet,这个servlet配置为启动时装载,为整个web应用创建必要的数据库表和数据。 很多第三方框架也需要在应用一加载就实例化Serlvet,比如SpringMVC中的org.springframework.web.servlet.DispatcherServlet
> <!-- springMVC的核心控制器 --> > <servlet> > <servlet-name>springMVC</servlet-name> > <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> > <init-param> > <param-name>contextConfigLocation</param-name> > <param-value>classpath*:springMVC-servlet.xml</param-value> > </init-param> > <load-on-startup>1</load-on-startup> > <async-supported>true</async-supported> > </servlet> > <servlet-mapping> > <servlet-name>springMVC</servlet-name> > <url-pattern>/</url-pattern> > </servlet-mapping> >
如果某个Servlet的映射路径仅仅为一个正斜杠(/),那么这个Servlet就成为当前Web应用程序的缺省Servlet。
凡是在web.xml文件中找不到匹配的 <servlet-mapping> 元素的URL,它们的访问请求都将交给缺省Servlet处理,也就是说, 缺省Servlet用于处理所有其他Servlet都不处理的访问请求。
比如在tomcat中/conf/web.xml文件中,注册了一个名称为org.apache.catalina.servlets.DefaultServlet的Servlet,并将这个Servlet设置为了缺省Servlet。
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- The mapping for the default servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
当访问Tomcat服务器中的某个静态HTML文件和图片时,实际上是在访问这个缺省Servlet。
当多个客户端并发访问同一个Servlet时, web服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用Servlet的service方法,因此service方法内如果访问了同一个资源的话,就有可能引发线程安全问题 。
所有在Servlet中尽量 避免使用实例变量 ,最好使用局部变量。
Servlet容器默认是采用单实例多线程的方式处理多个请求的:
从上面可以看出(好处):
上面讲了可以设置Servlet对应的url,那么如何需要配置Serlvet初始化参数怎么办,这就需要用到ServletConfig。在Servlet的配置文件web.xml中,可以使用一个或多个 <init-param> 标签为servlet配置一些初始化参数。
<servlet>
<servlet-name>ServletConfigDemo1</servlet-name>
<servlet-class>com.xxx.xxx.xxx</servlet-class>
<!--配置ServletConfig的初始化参数 -->
<init-param>
<param-name>name</param-name>
<param-value>xxx</param-value>
</init-param>
<init-param>
<param-name>password</param-name>
<param-value>123</param-value>
</init-param>
<init-param>
<param-name>charset</param-name>
<param-value>UTF-8</param-value>
</init-param>
</servlet>
当servlet配置了初始化参数后,web容器在创建servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中,并在调用servlet的init方法时,将ServletConfig对象传递给servlet。进而,我们通过ServletConfig对象就可以得到当前servlet的初始化参数信息。
获取方式如下:
/**
* 定义ServletConfig对象来接收配置的初始化参数
*/
private ServletConfig config;
/**
* 当servlet配置了初始化参数后,web容器在创建servlet实例对象时,
* 会自动将这些初始化参数封装到ServletConfig对象中,并在调用servlet的init方法时,
* 将ServletConfig对象传递给servlet。进而,程序员通过ServletConfig对象就可以
* 得到当前servlet的初始化参数信息。
*/
@Override
public void init(ServletConfig config)throws ServletException {
this.config = config;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//获取在web.xml中配置的初始化参数
//获取指定的初始化参数
String paramVal = this.config.getInitParameter("name");
response.getWriter().print(paramVal);
response.getWriter().print("<hr/>");
//获取所有的初始化参数
Enumeration<String> e = config.getInitParameterNames();
while(e.hasMoreElements()){
String name = e.nextElement();
String value = config.getInitParameter(name);
response.getWriter().print(name + "=" + value + "<br/>");
}
}
比如在使用SpringMVC的时候,在web.xml就使用到了ServletConfig
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--配置ServletConfig的初始化参数 -->
<init-param>
<description>SpringMVC</description>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-mvc.xml</param-value>
</init-param>
<!--web容器一旦加载就会创建这个servlet,并且会调用init方法-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
对于ServletContext需要我们注意一下几点:
类似于ServletContext(应用上下文)这种设计思想,很多地方都有类似的应用。比如iOS中的画图CGContext 。
来一个数据共享的例子:
存储数据:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String data = "data";
/**
* ServletConfig对象中维护了ServletContext对象的引用,开发人员在编写servlet时,
* 可以通过ServletConfig.getServletContext方法获得ServletContext对象。
*/
//获得ServletContext对象
ServletContext context = this.getServletConfig().getServletContext();
//将data存储到ServletContext对象中
//将data存储到ServletContext对象中
context.setAttribute("data", data);
}
读取数据:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ServletContext context = this.getServletContext();/
//从ServletContext对象中取出数据
String data = (String) context.getAttribute("data");
response.getWriter().print("data="+data);
}
更多的情况下是通过web.xml中读取初始化参数。如下:
<!-- 配置WEB应用的初始化参数 -->
<context-param>
<param-name>url</param-name>
<param-value>xxx.xxx.xxxx</param-value>
</context-param>
读取web.xml中的初始化参数
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ServletContext context = this.getServletContext();
//获取整个web站点的初始化参数
String contextInitParam = context.getInitParameter("url");
response.getWriter().print(contextInitParam);
}
除了读取web.xml中的初始化参数,还可以通过ServletContext读取对象文件。比如常见在项目中使用的xxx.properties文件。很多第三方也是用的这种方式来读取对应的配置文件
/**
* 读取src目录下的com.xxx.xxx包中的xxx.properties配置文件
*@paramresponse
*@throwsIOException
*/
private void readPropCfgFile2(HttpServletResponse response)
throws IOException {
InputStream in = this.getServletContext().getResourceAsStream("/WEB-INF/classes/com/xxx/xxx/xxx.properties");
Properties prop = new Properties();
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
response.getWriter().println("读取src目录下的com.xxx.xxx包中的xxx.properties配置文件:");
response.getWriter().println(
MessageFormat.format(
"driver={0},url={1},username={2},password={3}",
driver,url, username, password));
}
除了通过ServletContext读取资源文件,还可以通过类装载器读取资源文件。也有很多第三方是通过这样的方式读取资源文件
/**
* 读取类路径下的资源文件
*@paramresponse
*@throwsIOException
*/
private void test1(HttpServletResponse response)throws IOException {
//获取到装载当前类的类装载器
ClassLoader loader = ServletContextDemo7.class.getClassLoader();
//用类装载器读取src目录下的db1.properties配置文件
InputStream in = loader.getResourceAsStream("db1.properties");
Properties prop = new Properties();
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
response.getWriter().println("用类装载器读取src目录下的db1.properties配置文件:");
response.getWriter().println(
MessageFormat.format(
"driver={0},url={1},username={2},password={3}",
driver,url, username, password));
}
上一篇讲了Servlet,提到Servlet由容器调用。拿么Web容器是干嘛的呢。
比较权威的解释:
web容器是一种服务程序,在服务器一个端口就有一个提供相应服务的程序,而这个程序就是处理从客户端发出的请求,如JAVA中的Tomcat容器,ASP的IIS或PWS都是这样的容器。比如tomcat容器,它提供的接口严格遵守 J2EE 规范中的 WEB APPLICATION 标准。我们把遵守该标准的 WEB 服务器就叫做 J2EE 中的 WEB 容器。 这套标准就是当有客户端请求,自动调用对应的Servlet的标准
为了加强理解这里再次说明一下:
servlet没有main方法,那我们如何启动一个servlet,如何结束一个servlet,如何寻找一个servlet等等,都受控于另一个java应用,这个应用我们就称之为web容器。
比如tomcat就是这样一个容器。如果web服务器应用得到一个指向某个servlet的请求,此时服务器不是把servlet交给servlet本身,而是交给部署该servlet的容器。要有容器向servlet提供http请求和响应,而且要由容器调用servlet的方法,如doPost或者doGet。
接下来就以Tomcat为例子
Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。由于有了Sun 的参与和支持,最新的Servlet 和JSP 规范总是能在Tomcat 中得到体现,Tomcat 5支持最新的Servlet 2.4 和JSP 2.0 规范。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。
一个简单的配置例子, 对照着上面的体系结构来看 :
<?xml version='1.0' encoding='utf-8'?>
<Serverport="8005"shutdown="SHUTDOWN">
<Servicename="Catalina">
<Connectorport="8080"protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"/>
<Connectorport="8443"protocol="org.apache.coyote.http11.Http11Protocol"
maxThreads="150"SSLEnabled="true"scheme="https"secure="true"
clientAuth="false"sslProtocol="TLS"
keystoreFile="conf/.keystore"keystorePass="123456"/>
<Connectorport="8009"protocol="AJP/1.3"redirectPort="8443"/>
<Enginename="Catalina"defaultHost="localhost">
<Hostname="localhost"appBase="webapps"
unpackWARs="true"autoDeploy="true">
<ValveclassName="org.apache.catalina.valves.AccessLogValve"directory="logs"
prefix="localhost_access_log."suffix=".txt"
pattern="%h %l %u %t "%r" %s %b"/>
</Host>
<!-- host对应的根目录 -->
<Hostname="www.xxx.xxx"appBase="F:/JavaWebApps">
<!-- 具体context对应的目录 -->
<Contextpath=""docBase="F:/JavaWebApps/xxxx"/>
</Host>
</Engine>
</Service>
</Server>
应用服务器启动时web.xml加载过程,至于这些节点在xml文件中的前后顺序没有关系,不过有些应用服务器,比如websphere就严格要求web.xml的节点顺序,否则部署不成功,所以还是最好按照web.xml标准格式写 :
具体的配置和加载顺序:content-param –> listener –> filter –> servlet
结合上面所讲的ServletConfig及ServletContext。
<context-param></context-param> 和 <listener></listener> <context-param></context-param> 转化为键值对,并交给ServletContext。 <listener></listener> 中的类实例,即创建监听。 contextInitialized(ServletContextEvent args) 初始化方法,在这个方法中获得: ServletContext = ServletContextEvent.getServletContext(); context-param的值 = ServletContext.getInitParameter("context-param的键") ; context-param 的值之后,你就可以做一些操作了.注意, 这个时候你的WEB项目还没有完全启动完成,这个动作会比所有的Servlet都要早。 换句话说,这个时候,你对
以 filter 为例,web.xml 中当然可以定义多个 filter,与 filter 相关的一个配置节是 filter-mapping, 这里一定要注意,对于拥有相同 filter-name 的 filter 和 filter-mapping 配置节而言,filter-mapping 必须出现在 filter 之后,否则当解析到 filter-mapping 时,它所对应的 filter-name 还未定义 。
web 容器启动时初始化每个 filter 时,是按照 filter 配置节出现的顺序来初始化的,当请求资源匹配多个 filter-mapping 时,filter 拦截资源是按照 filter-mapping 配置节出现的顺序来依次调用 doFilter() 方法的。 servlet 同 filter 类似!
比如filter 需要用到 bean ,但加载顺序是: 先加载filter 后加载spring,则filter中初始化操作中的bean为null;所以,如果过滤器中要使用到 bean,可以将spring 的加载 改成 Listener的方式
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
根节点
<web-app></web-app>
用来设定web站台的环境参数。
它包含两个子元素:
在此设定的参数,可以在servlet中用 getServletContext().getInitParameter("my_param") 来取得。
例子:
<context-param> <param-name>log4jConfigLocation</param-name> <param-value>classpath*:/log4j.properties</param-value> </context-param>
用来设定Listener接口,它的主要子元素为:
例子:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
是用来声明filter的相关设定
例子:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>GBK</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter>
来声明一个servlet的数据,主要有以下子元素:
同样,与
<servlet> <servlet-name>DemoServlet</servlet-name> <servlet-class>com.test.DemoServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DemoServlet</servlet-name> <url-pattern>/demoServlet</url-pattern> </servlet-mapping>
是对站台的描述
例子:
<description>传道、授业、解惑</description>
定义站台的名称
例子:
<display-name>我的站点</display-name>
icon元素包含small-icon和large-icon两个子元素.用来指定web站台中小图标和大图标的路径.
small-icon元素应指向web站台中某个小图标的路径,大小为16 X 16 pixel,但是图象文件必须为GIF或JPEG格式,扩展名必须为:.gif或.jpg.
large-icon元素应指向web站台中某个大图表路径,大小为32 X 32 pixel,但是图象文件必须为GIF或JPEG的格式,扩展名必须为; gif或jpg.
例子:
<icon> <small-icon>/images/small.gif</small-icon> <large-icon>/images/large.gir</large-icon> </icon>
是指定该站台是否可分布式处理
用来定义web站台中的session参数
包含一个子元素:
定义某一个扩展名和某一个MIME Type做对应该
包含两个子元素:
例子:
<mime-mapping> <extension>doc</extension> <mime-type>application/vnd.ms-word</mime-type> </mime-mapping> <mime-mapping> <extension>xls</extension> <mime-type>application/vnd.ms-excel</mime-type> </mime-mapping>
例子
<error-page> <error-page>
<error-code>500</error-code>
<location>/message.jsp</location>
</error-page>
<error-page>
<error-code>400</error-code>
<location>/message.jsp</location>
</error-page>
<error-page>
<error-code>403</error-code>
<location>/message.jsp</location>
</error-page>
<error-page>
<error-code>404</error-code>
<location>/message.jsp</location>
</error-page>
<error-page>
<error-code>502</error-code>
<location>/index.jsp</location>
</error-page>
例子
<jsp-config> <taglib> <taglib-uri>/struts-tags</taglib-uri> <taglib-location>/WEB-INF/struts-tags.tld</taglib-location> </taglib> <taglib> <taglib-uri>/struts-dojo-tags</taglib-uri> <taglib-location>/WEB-INF/struts-dojo-tags.tld</taglib-location> </taglib> <taglib> <taglib-uri>/s</taglib-uri> <taglib-location>/WEB-INF/struts-tags.tld</taglib-location> </taglib> </jsp-config>
####welcome-file-list
<welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list>
定义利用JNDI取得站台可利用的资源
有五个子元素:
比如,配置数据库连接池就可在此配置
<resource-ref> <description>JNDI JDBC DataSource of shop</description> <res-ref-name>jdbc/sample_db</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>