今天来继续学习JavaWeb的相关知识,之前都是都介绍一些基本知识,从今天开始我们来说一下如何在服务器编写程序,这里就需要来介绍一下Servlet的相关知识了。Servlet就是一个能够运行在服务器端的java代码,我们从他的api开始来解读吧!
Servlet是JavaEE的13门技术中的一门,所以我们需要从JavaEE的api中寻找,下面就是Servlet的api:
其实Servlet是一个接口,他有两个实现类:GenericServlet,HttpServlet,我们可以看到Servlet接口中的五个方法:
init(ServletConfig config):是个初始化方法,这个方法是在Servlet被初始化的时候被调用,而且会有一个ServletConfig对象传递进来,至于ServletConfig对象我们后面会继续说的
getServletInfo():方法是获取这个Servlet的相关信息的。此方法用到的地方很少
getServletConfig():方法是获取一个ServletConfig对象,和init方法中传递进来的ServletConfig是同一对象
service(ServletRequest req,ServletResponse res):方法是用户处理客户机的请求的,这个方法的两个参数对应的是请求参数:req;和响应参数res。
destroy():是在servlet被销毁的时候调用。
从上面的方法中我们可以看到有三个方法是和servlet的生命周期相关的方法:init(...),service(..),destroy();
下面就来说一下servlet的生命周期的吧:
当服务器启动的时候,服务器会加载所有的web应用,当用户在浏览器中第一次请求servlet的时候,init方法就会被调用,这时候servlet就会被创建,因为servlet是用的单例模式。所以只要servlet所在的应用没有关闭或者服务器没有关闭,这个servlet始终都是在服务器的内存中的,所以当你在一次请求这个servlet的时候init方法是不会再调用的。
当用户每次请求servlet的时候,这个servlet中的service的方法都会被调用,因为他是用来处理客户机的请求的。
当该web应用被关闭或者服务器关闭了,这个servlet才会被销毁,此时destroy方法会被调用。同时这个单例的servlet也会从内存中消失。
上面介绍了servlet的接口和servlet的相关信息,下面就来看一下他的子类吧:
GenericServlet:
他是实现Servlet接口中的所有方法,同时自己也是添加了几个方法:
getInitParameter(String name):这个方法是和ServletConfig对象相关的,通过name来从ServletConfig对象中获取值value
getInitParameterNames():这个方法也是和ServletConfig对象相关的,是获取ServletConfig对象中所有的name的枚举集合
getServletContext():这个方法很重要的,是获取ServletContext对象,这个ServletContext对象我们会在后面说到
下面就来手动的书写一个Servlet,这样我们就能够深入的了解到Servlet的运行原理:
第一步:在tomcat中的webapp目录中新建一个web应用:FirstServlet,然后在应用中新建一个WEB-INF文件夹,在WEB-INF文件夹下面新建一个classes文件夹,之前说过,服务器端的程序代码文件就是放在这个文件夹下面的,然后我们就在classes文件夹下面新建一个FirstServlet.java文件:代码如下:
注意:-d参数是编译包名;.点号是指编译后的包名文件存在当前目录
编译发生错误,提示找不到javax.servlet.*这样的包,其实很简单,因为我们使用javac命令编译程序的话只导入了JavaSE的jar包,所以我们需要导入JavaEE的jar包到路径:set classpath=%classpath%;C:\Program Files\Apache Software Foundation\Tomcat 6.0\lib\servlet-api.jar
因为tomcat目录中肯定用到了javaee中的jar包,所以将这个jar包添加到classpath中即可。
这时候在执行javac命令编译没有问题了,在当前目录中产生文件了,就是包名对应的文件目录
程序编译好了,下面还需要进行配置这个Servlet的对外访问路径,这个我们之前也说过了,关于web应用的所有配置都是在web.xml文件中配置的,所以我们需要在应用FirstServlet应用文件夹中新建一个web.xml文件,这里面来配置Servlet,但是我们该如何书写呢?
这时候我们再去tomcat目录中找到web.xml文件,从这个文件中抄过来,配置如下:
在浏览器中输入:http://localhost:8080/FirstServlet/FirstServlet
效果如下:
这样我们就手动的编写一个Servlet,而且可以看到我们是继承GenericServlet的。并且将逻辑都写在了service方法中,其实GenericServlet是一个抽象类,service也是一个抽象的方法需要子类实现的。
那下面就来看看HttpServlet,其实HttpServlet是实现了GenericServlet类的。
上面就是HttpServlet的api了,实现了GenericServlet中的抽象方法,一起我们注意的是:他有好多doXXX这样的方法,其实这个和我们之前介绍Http协议的时候相联系的,HttpServlet就是针对于Http协议的Servlet,我们在之前介绍Http协议的时候说过他的请求方式其实是有七种方式的,只是get/post方式现在最常用,其他的方式被弃用了,所以我们在这里看一看到以do开头的方法其实是专门用来处理不同方式的请求的,但是我们之前说过所有的请求都会调用service方法,所以这时候我们可以看一下service中的源代码:
下面我就是用MyEclipse来开发一个HttpServlet:
首先在MyEclipse中新建一个web应用,在应用中新建包名(web应用中的程序是必须要有包名的),在包下面新建一个Servlet(这里不要在新建一个java类了,不然我们还需要手动的继承HttpServlet,配置这个类的对外访问路径,步骤是很麻烦的),如果是新建一个Servlet的话,MyEclipse会自动帮我们将这个Servlet配置好对外访问路径,而且会自动的继承HttpServlet类。这样我们就可以在doGet和doPost方法中处理客户机的请求了。
上面的内容就介绍了Servlet的相关知识,下面就来开始介绍和Servlet相关的对象:
首先来看一下ServletConfig:
之前说过:Servlet接口中有一个getServletConfig()方法,而且在init(ServletConfig config)方法中也会回传一个ServletConfig对象,那么这个对象到底有什么用呢?见名知意,是Servlet的配置信息相关的,没错,他就是将ServletConfig的配置信息封装成对象,那么Servlet的配置信息是在哪里配置,配置后之后怎么读取,什么样的信息才需要进行配置呢?
其实有很多信息我们都是需要进行配置的,比如:这个Servlet所需要的连接数据库的信息,所使用的字符集码表,下面就来个例子吧:
首先看一下怎么配置,在webx.xml文件中找到对应的Servlet的
下面在来看一下ServletContext对象,关于这个对象我们要详细的了解他,我们在之前介绍ServletConfig对象的时候,发现ServletConfig对象有一个方法:getServletContext(),通过这个方法可以获取一个ServletContext对象。当然也可以通过this.getServletContext()方法得到这个对象。ServletContext是很重要的一个对象,他同时也是四大域对象之一的context域对象,从他的名字我们就知道context是上下文的意思,(在Android中也有这样的一个概念,getApplicationContext()可以获取当前的Android应用的上下文对象)。那么这里的context就代表这个整个web应用,所以他的生命周期是:当服务器启动的时候会为每一个web应用创建一个ServletContext对象,当web应用销毁了或者关闭服务器这个对象也就随之销毁。这里我们也可以看到他的生命周期是最长的,贯彻整个web应用。下面就来看一下他的相关api(只说一些重要的,常用的方法)
getAttribute(String name)/setAttribute(String name,String value)/removeAttribute(String):是将值保存到context域中的,这些值将可以被web应用中所有的servlet和jsp访问
getInitParameter(String name)/getInitParameterNames():可以获取web应用的初始化信息
getMajorVersion()/getMinorVersion():获取Servlet的最大和最小版本
getMimeType(String fileName):通过文件名判断这个文件的mime类型
getRequestDispatcher(String path):通过path路径获取一个转发对象RequestDispatcher,可以实现转发机制
getResourceAsStream(String path):通过给定的path获取一个资源Inpustream
getRealPath(String path):通过给定的path返回这个文件所在磁盘的真实路径
下面就来通过实例来看一下上面方法的使用规则
首先来看一下getAttribute(String name)和setAttribute(String name,Object value),以及getRequestDispatcher(String path):
场景:我们在Servlet1中存入一个值,然后转发到Servlet2,取出这个值打印出来:
Servlet1:
从Servlet1中存入的值通过转发到Servlet2中取出来进行读取显示。
下面在来看一下getInitParameter(String name)和getInitParameterNames(),我们读取全局配置参数,其实这个配置是对每个Servlet都是有效的,所以他是全局的效果,我们之前使用ServletConfig进行对某个Servlet进行初始化参数配置,数据库连接的信息这个东东是最每个Servlet都是有效的而且都是一样的配置,所以我们应该将这些信息配置到全局中ServletContext,上面配置到ServletConfig中只是为了演示ServletConfig的作用。这种全局性的信息肯定是要配置到ServletContext中的。
结果:
下面在来看一下getResouceAsStream(String path)和getReal(String path)方法
这两个方法主要是用来读取web应用中的资源文件的,这个用处很大的,首先我们来看一下web应用的项目结构:
我们分别src目录中新建一个db.properties属性文件和在com.weijia.serlvetcontext包中新建一个db.properties属性文件:
先来看一下怎么读取包中的属性文件
使用getResourceAsStream方法获取一个InputStream流,我们需要填写属性文件的路径path:
重点在于这个path:这个path的书写是有规则的:
/WEB-INF/classes/com/weijia/servletcontext/db.properties
首先第一个"/"代表当前的web应用(这个和JavaSE中读取文件不同,JavaSE中读取文件的话是一定不能以斜杠开头的,不然就报错),然后是WEB-INF/classes,这个路径我们之前说过,一个web应用发布之后是没有src这样的目录的,我们在第一个手写Servlet的例子中我们也看到了,我们并没有新建一个src文件夹,而是新建一个classes文件夹,在这个文件下面新建一个Servlet的,这点一定要注意;然后就是包名了路径了,因为包名会被映射到文件目录。这样我们就可以读取到了属性文件。
那么读取src目录中的属性文件就更简单了:
/WEB-INF/classes/db.properties
这里千万不要写成:
/src/db.properties
一定要记得web应用发布之后是没有src目录的,这里的src目录就相当于是classes目录
上面的这个方法是返回一个InputStream流的,但是如果我们现在想通过FileInputStream文件流来读取文件我们该怎么做呢?
那么我就必须要获取到文件的路径,那么我们能这样写吗:
FileInputStream fis = new FileInputStream("classes/db.properties")
这样写行不行呢?我们运行一下就知道了,我发现运行时报异常的,下面我们就来分析一下这个问题:
首先我们都知道上面的那样写法是相对路径,那么这个相对路径是相对于谁呢?
下面的代码是JavaSE:
运行结果:
我们可以看到JVM是会使用user.dir这样的系统属性来拼接相对路径得到绝对路径的。这个user.dir存储的就是当前的工作目录
所以我们在Servlet中打印一下:
System.getProperty("user.dir"):可以得到当前的工作目录:
打印结果如下:
C:\Program Files\Apache Software Foundation\Tomcat 6.0\bin
可以看到是tomcat的bin目录,那么我们使用相对路径:classes/db.properties这样就相当于绝对路径:
C:\Program Files\Apache Software Foundation\Tomcat 6.0\bin\classes\db.properties
所以我们可以到tomcat目录中的bin文件夹下面新建一个classes文件夹,里面新建一个db.properties文件,这时候在运行程序,就不会在报异常了,这样貌似我们想使用FileInputStream来读取文件的话,很是麻烦呀!
这时候我们就可以使用了getRealPath(String path)方法来获取资源的本地磁盘中的绝对路径:
打印属性地址:
打印结果:
AbsolutePath:C:\Program Files\Apache Software Foundation\Tomcat 6.0\webapps\ServletDemo\WEB-INF\classes\db.properties
那么这时候我们就可以通过FileInputStream来读取文件了。
现在还有一个问题就是以后我们在编写Web应用的时候是要遵从多层设计思想的,而且层与层之间不能有侵入行为,层与层之间使用接口进行访问的。比如Service层和Dao层是不能有感染的,就是耦合度要很低的,Servlet就是Service层,那么现在如果想在Dao层中读取文件资源我们该怎么办呢?因为Dao层中没有Servlet,所以没有ServletContext了,那么我们该怎么获取路径呢?有人说可以通过方法的参数形式将ServletContext对象从service层传递到Dao层中,这个方法是可以的,但是这就违背了我们开始说的层与层之间的耦合和互不感染的原则了。那这时候我们该怎么办呀?这时候我们就需要另外一种读取资源的一种方式了,而且这种方式也是可以用于JavaSE中读取文件的。也是一种非常经典的读取资源的方式,这就使用类加载器来读取资源:
这样我们就可以不使用ServletContext对象也可以读取到com.weijia.servletcontext包底下的db.properties文件资源了,同样的他也是有一个getRealPath(String path)方法的可以得到本地磁盘的绝对路径,这样我们也可以使用FileInputStream来读取资源了。
其实这种读取资源的方式是:类加载器会把该资源文件和class文件等同一样加载到内存中的,所以问题就出现了:
1.首先这个资源文件肯定不能太大,因为他是和class文件一起加载到内存中,太大的话,内存就爆了
2.只要当类加载的时候这个文件才会被加载到内存中,现在假如我们修改了这个db.properties资源文件,保存,但是我们在读取的时候还是之前的资源文件中的内容,原因很简单,因为我们修改的是db.properties资源文件,而没有修改类文件,所以类加载器并不会再次加载类,那么就不会在加载这个修改过的资源文件了,那么这次修改是无效的,所以说这点我们在使用类加载器读取资源的时候一定要注意了。
下面来看一下Servlet的线程安全问题:
当多个客户端并发访问同一个servlet时,web服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用servlet的service方法,因此service方法如果访问了同一个资源的话,就有可能引发线程安全的问题,如果某个servlet实现了SingleThreadModel(标记接口)接口,那么servlet引擎将以单线程模式来调用其service方法。SingleThreadModel接口中没有定义任何方法,只要在servlet类的定义中增加实现SingleThreadModel接口的声明即可.对于实现了SingleThreadModel接口的servlet,servlet引擎仍然支持对该servlet的多线程并发访问,其采用的方式是产生多个servlet实例对象,并发的每个线程分别条用一个独立的servlet实例对象。实现SingleThreadModel接口并不能真正解决servlet的线程安全问题,因为servlet的引擎会创建多个Servlet实例对象,而真正意义上解决多线程安全问题是指一个servlet实例对象被多个线程同时调用的问题,事实上,在servlet api2.4中,已经将SingleThreadModel标记为Deprecated(过时的).标准的解决方案是同步方式sychronized
如果线程向静态list集合中加入了数据(aaa),数据用完后,一般要移除静态集合中的数据(aaa),否则集合中的数据越来越多,就会导致内存溢出。对象销毁了,静态资源的字节码仍然驻留在内存中.
下面来看一下Servlet的对外访问路径的配置:
我们一个Servlet编写完成之后是需要对其进行配置对外访问路径的,如果我们是新建一个Servlet的话,IDE会自动的帮我们配置好这个对外访问路径,但是有时候我们需要自己进行配置已达到我们需要的效果,这时候我们知道配置应用的web.xml文件中的内容即可,首先要知道一个Servlet是可以配置多个对外访问路径的。下面就直接来一下配置路径的案例吧:
以下是Servlet对外访问路径的配置规则案例:
servlet1 映射到 /abc/*
servlet2 映射到 /*
servlet3 映射到 /abc
servlet4 映射到 *.do
问题:
1.当请求URL为"/abc/a.html","/abc/*"和"/*"都匹配,但是servlet引擎会调用servlet1
2.当请求URL为"/abc"时,"/abc/*"和"/abc"都匹配,但是servlet引擎会调用servlet3
3.当请求URL为"/abc/a.do"时,"/abc/*"和"*.do"都匹配,但是servlet引擎会调用servlet1
4.当请求URL为"/a.do"时,"/*"和"*.do"都匹配,但是servlet引擎会调用servlet2
5.当请求URL为"/xxx/yyy/a.do"时,"/*"和"*.do"都匹配,但是servlet引擎会调用servlet2
总结:谁长的最像,谁先匹配,同时*的优先级最低.
当然我们还可以配置一个默认的访问对外访问路径就是直接一个斜杠:/
这个默认的对外访问路径的作用就是当一个Servlet找不到其对应的映射路径的时候回去找打这个默认的对外访问路径:
上面的图片就是tomcat中的web.xml文件中配置的默认对外访问路径,当我们访问的路径找不到都会去请求DefaultServlet。
总结:上面我们就介绍了Servlet的相关知识,以及ServletConfig和ServletContext对象,下面总结一下在开发web的时候的问题的解决:
1.首先来看怎么修改Servlet的模板,因为我们在开发一个Servlet的时候,发现在doGet/doPost方法中发现很多没有用的代码,每次都需要删除很是麻烦的,我们进入到MyEclispe的安装目录中全局搜索一个servlet.java文件,打开之后我们将doGet/doPost方法中无效的内容删除了即可(最后在修改之前备份一下这个servlet.java)
2.我们有时候在将一个web应用导入到MyEclipse中的时候项目的名称可能会修改,但是他的Context Path并没有修改,所以在通过浏览器访问的时候还是之前项目的名称路径,这时候我们要修改的话就点击项目的properties如下界面修改:
我们只需要将WebRoot从新映射到一个Context-path就可以了。
3.有时候我们在发布应用的时候会发现以下的问题:
这个问题我们知道是版本的问题,就是使用高版本的JDK去编译web应用,然后在用低版本的JVM(tomcat)去运行,这个问题发生在我们之前手动编写一个web应用FirstServlet,那时候我们是用javac命令进行编译的,这个javac命令是使用了JDK7.0版本的(我自己安装了JDK7.0),当我们打开MyEclipse的时候,我在MyEclipse中配置的是Tomcat6.0的,tomcat6.0是运行在JVM6.0的,所以会报错了,那么我们可以修改MyEclipse中的tomcat运行所依赖的JVM:
点击add可以添加我们JDK7.0版本的JVM,保存在运行就没有问题了。
Servlet监听器
有时候,知道应用服务器容器(the application server container)里某些事件发生的时间是很有用的。这个概念适用于很多情况,但它通常用在开启应用时初始化应用或者关闭应用时清理应用。可以在应用里 注册一个监听器(listener)来显示应用什么时候开启或者关闭。因此,通过监听这些事件,Servlet可以在一些事件发生时执行相应的动作。
为了创建一个基于容器事件执行动作的监听器,你必须创建一个实现 ServletContextListener 接口的类。这个类必须实现的方法有 contextInitialized() 和 contextDestroyed()。这两个方法都需要 ServletContextEvent 作为参数,并且在每次初始化或者关闭Servlet容器时都会被自动调用。
为了在容器注册监听器,你可以使用下面其中一个方法:
1) 利用 @WebListener 注解。
2) 在web.xml应用部署文件里注册监听器。
3) 使用 ServletContext 里定义的 addListener() 方法
请注意,ServletContextListener 不是Servlet API里唯一的监听器。这里还有一些其他的监听器,比如
1
2
3
4
5
6
|
javax.servlet.ServletRequestListener
javax.servlet.ServletRequestAttrbiteListener
javax.servlet.ServletContextListener
javax.servlet.ServletContextAttributeListener
javax.servlet.HttpSessionListener
javax.servlet.HttpSessionAttributeListener
|
根据你要监听的事件选择他们来实现你的监听器类。比如,每当创建或销毁一个用户session时,HttpSessionListener 就会发出通知。
Servlet过滤器
Web过滤器在给定的URL被访问时对请求进行预处理并调用相应的功能是很有用的。相 比于直接调用给定URL请求的Servlet,包含相同URL模式的过滤器(filter)会在Servlet调用前被调用。这在很多情况下是很有用的。 或许最大的用处就是执行日志,验证或者其他不需要与用户交互的后台服务。
过滤器必须要实现 javax.servlet.Filter 接口。这个接口包含了init(),descriptor()和doFilter()这些方法。init()和destroy()方法会被容器调用。 doFilter()方法用来在过滤器类里实现逻辑任务。如果你想把过滤器组成过滤链(chain filter)或者存在多匹配给定URL模式的个过滤器,它们就会根据web.xml里的配置顺序被调用。
为了在web.xml里配置过滤器,需要使用
1
2
3
4
5
6
7
8
|
|
Servlet下载文件如果你要使用注解来为特定的servlet配置过滤器,你可以使用@WebFilter注解。
几乎所有的web应用都必须有下载文件的功能。为了下载一个文件,Servlet必须提供一个和下载文件类型匹配的响应类型。同样,必须在响应头里指出该响应包含附件。就像下面的代码。
1
2
3
|
String mimeType = context.getMimeType( fileToDownload );
response.setContentType( mimeType != null ? mimeType : "text/plain" );
response.setHeader( "Content-Disposition" , "attachment; filename=" " + fileToDownload + " "" );
|
通过调用 ServletContext.getResourceAsStream() 方法并传递文件路径给该方法,你可以获取要下载的文件(文件保存在文件系统)的引用。这个方法会返回一个输入流(InputStream)对 象,我们可以用这个对象来读取文件内容。当读取文件时,我们创建一个字节缓存区(byte buffer)从文件里获取数据块。最后的工作就是读取文件内容并且把它们复制到输出流。我们使用while循环来完成文件的读取,这个循环直到读取了文 件的所有内容才会跳出循环。我们使用循环来读进数据块并把它写进输出流。把所有数据写进输出流后,ServletOutputStream 对象的flush方法就会被调用并且清空内容和释放资源。
看这段简单的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private void downloadFile(HttpServletRequest request, HttpServletResponse response, String fileToDownload) throws IOException
{
final int BYTES = 1024 ;
int length = 0 ;
ServletOutputStream outStream = response.getOutputStream();
ServletContext context = getServletConfig().getServletContext();
String mimeType = context.getMimeType( fileToDownload );
response.setContentType( mimeType != null ? mimeType : "text/plain" );
response.setHeader( "Content-Disposition" , "attachment; filename=" " + fileToDownload + " "" );
InputStream in = context.getResourceAsStream( "/" + fileToDownload);
byte [] bbuf = new byte [BYTES];
while ((in != null ) && ((length = in.read(bbuf)) != - 1 )) {
outStream.write(bbuf, 0 , length);
}
outStream.flush();
outStream.close();
}
|
使用RequestDispatcher.forward()转发请求到另一个Servlet
有时候,你的应用需要把一个Servlet要处理的请求转让给另外的Servlet来处理并完成任务。而且,转让请求时不能重定向客户端的URL。即浏览器地址栏上的URL不会改变。
在 ServletContext 里已经内置了实现上面需求的方法。所以,当你获取了 ServletContext 的引用,你就可以简单地调用getRequestDispatcher() 方法去获取用来转发请求的 RequestDispatcher 对象。当调用 getRequestDispatcher() 方法时,需要传递包含servlet名的字符串,这个Servlet就是你用来处理转让请求的Servlet。获取 RequestDispatcher 对象后,通过传递 HttpServletRequest 和HttpServletResponse 对象给它来调用转发方法。转发方法负责对请求进行转发。
1
2
|
RequestDispatcher rd = servletContext.getRequestDispatcher( "/NextServlet" );
rd.forward(request, response);
|