通过上面的例子,我们可以看到servlet与jsp的功能差不多,都是处理请求并返回响应。甚至在操作时使用到的变量都极为相似,request,response,out这些在我们使用jsp的时候也遇到过。不同的时是,jsp中这些变量拿来即用,servlet中还要通过参数来获得。
jsp和servlet如此相似,它们之间难道有什么联系吗?为什么sun创造了两种功能相似的东西呢?
其实jsp就是servlet,你所写的jsp页面最终都会被服务器转换成servlet。为了验证这一说法,我们编写一个效果与HelloServlet完全一样的hello.jsp,它的内容就只有这么点儿。
hello
访问http://localhost:8080/06-01/hello.jsp,会看到与刚刚一样的效果。
现在打开tomcat的work目录。
在Catalina/localhost/06-01/org/apache/jsp目录下,可以看到两个文件,分别是hello_jsp.class和hello_jsp.java。
打开hello_jsp.java可以看到里边的源代码,我们只挑出其中一部分来看。
out = pageContext.getOut();
_jspx_out = out;
out.write("hello");
获得pageContext的输出流并将"hello"写入,于是浏览器上就看到了hello的字样。
是的,这里的hello_jsp.java就是由hello.jsp生成的。服务器在获得请求的时候会先根据jsp页面生成一个java文件,然后使用jdk的编译器将此文件编译,最后运行得到的class文件处理用户的请求返回响应。如果再有请求访问这jsp页面,服务器会先检查jsp文件是否被修改过,如果被修改过,则重新生成java重新编译,如果没有,就直接运行上次得到的class。
为什么第一次访问jsp的时候速度会那么慢?就是因为要经过生成java和编译class的步骤。以后再次访问同一页面就会感觉到速度明显变快,也是因为class文件已经生成的原因。
为什么jsp要经过这些步骤转换成servlet再去执行呢?因为java起初做网站的时候就只有servlet可以使用,为此还专门指定了一套servlet标准,就是我们在代码中看到的javax.servlet包下的类。但是人们马上就发现,使用servlet显示复杂页面太费力气了,使用servlet里的输出方式简直让人写到手抽筋,于是就有了仿效asp和php的jsp出现,开发人员可以在美工做好的页面上直接嵌入代码,然后让服务器将jsp转换成servlet执行。
有的朋友可能迷糊了,既然jsp是为了简化servlet开发,那么为什么我们现在又要去学习servlet?既然servlet那么麻烦为什么不直接使用jsp就好了呢?
这是因为jsp虽然比servlet灵活,却容易出错,你找不到良好的方式来测试jsp中代码,尤其在需要进行复杂的业务逻辑时,这一点儿很可能成为致命伤。所以一般都不允许在jsp里出现业务操作有关的代码,从这点来看,我们上一章中举的例子就严重违反了这一标准,CRUD的操作都写在了jsp这种,一旦出现问题就会让维护人员头大如斗。
servlet是一个java类,需要编译之后才能使用,虽然显示页面的时候会让人头疼,不过在进行业务操作和数据运算方面就比jsp稳健太多了。因此我们就要结合两者的优点,在servlet进行业务操作和请求转发,jsp全面负责页面显示,这也是目前公司企业里常用的开发方式。
既然jsp就是servlet,jsp中的那些功能在servlet中也就都可以实现,当然咱们不能再使用jsp指令(directive)和jsp动作(action)了,不过它们也都有替代方法,我们以后慢慢介绍。
现在我们使用servlet改写第五章中联系簿的例子,将CRUD操作都转移到servlet中,让jsp只负责页面显示。
新建一个ContactServlet.java,让它负责处理那些CRUD操作,它会直接引用ContactDao操作数据,现在我们可以把jsp中对ContactDao的引用删除了,所有数据都将由ContactServlet提供,现在jsp只管从request里取出数据显示出来即可。
为了让ContactServlet起作用,在web.xml中添加处理请求的配置。
<servlet>
<servlet-name>ContactServlet</servlet-name>
<servlet-class>anni.ContactServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ContactServlet</servlet-name>
<url-pattern>/contact.do</url-pattern>
</servlet-mapping>
servlet标签指定使用anni.ContactServlet,servlet-mapping将它绑定到/contact.do请求上,当我们看到浏览器上出现http://localhost:8080/06-02/contact.do的时候,就表明ContactServlet在起作用了。
请别06-02目录下去找contact.do这个文件,它是不存在的。与之前提到的forward()情况类似,虽然浏览器指定要contact.do这个资源,但服务器暗地里把这个请求交给ContactServlet处理,你可以把contact.do当作一个地址,实际上你要找的人是ContactServlet。
不管怎样,只要是对contact.do发起的请求,最后都是由ContactServlet处理。我们依然继承HttpServlet,不过这次实现两个方法doGet()和doPost()分别处理http的GET和POST方式的请求。使用GET方式的请求会交由doGet()方法处理,使用POST方法的请求会交给doPost()处理,这些都是由HttpServlet控制的,我们可以直接使用。
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.process(request, response);
}
public void doPost(HttpServletRequest request,HttpServletResponse response)
throws ServletException, IOException {
this.process(request, response);
}
为了方便,我们把GET和POST请求都交给process()方法处理,在process()中根据不同的请求进行不同的操作。
public void process(HttpServletRequest request,HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("gb2312");
String method = request.getParameter("method");
if (method == null) {
method = "list";
}
try {
if ("list".equals(method)) {
this.list(request, response);
} else if ("save".equals(method)) {
this.save(request, response);
} else if ("edit".equals(method)) {
this.edit(request, response);
} else if ("update".equals(method)) {
this.update(request, response);
} else if ("remove".equals(method)) {
this.remove(request, response);
}
} catch(Exception ex) {
System.err.println(ex);
}
}
为了解决中文编码问题,记得先要给request设置gb2312编码格式,第二步就属于我们自己的设计方案了,为了分辨不同的操作,我们为每个请求都附加一个method参数,默认下是method等于list,显示所有联系信息的列表。
一大堆的if else虽然比较丑,但流程非常直观,根据method的值执行对应的方法。
method=list的情况。
默认的索引页面index.jsp中,将list.jsp改成contact.do?method=list,把请求转发到contact.do顺便再带上操作参数。ContactServlet里的list()方法如下。
public void list(HttpServletRequest request,HttpServletResponse response)
throws Exception {
List list = contactDao.getAll();
request.setAttribute("list", list);
request.getRequestDispatcher("/list.jsp").forward(request, response);
}
调用contactDao的getAll()方法获得联系信息列表,然后把list放到request里,因为pageContext是与jsp页面对应的,servlet里要把变量放到request作用域里,保证使用forward转发请求之后在jsp里也可以使用这个变量。
进行操作之后即刻使用forward跳转到list.jsp,记得这里要用forward才能保证request中的变量不会消失。这样依赖list.jsp中改成从request中获得我们需要的数据。
List list = (List) request.getAttribute("list");
其他的操作都与这个类似,比如把save.jsp改成contact.do?method=save,把edit.jsp?id=1改成contact.do?method=edit&id=1,把update.jsp?id=1改成contact.do?method=update&id=1,把remove.jsp?id=1改成contact.do?method=remove&id=1。
经过一轮改造,原来的save.jsp, update.jsp, remove.jsp的内容都归入了ContactServlet。06-02目录下只剩下index.jsp, list.jsp, create.jsp, edit.jsp四个页面,看页面中的链接全部指向了contact.do,这下是不是觉得清爽多了?
ContactServlet作为统一的请求转发器发挥着强大的作用,基本所有的请求都是由它接收并中转的,正因为有它的存在,我们才得以把进行数据库操作的ContactDao与表现层的jsp隔离开,让处理业务的代码更加集中。
这种分层方式通常被称为MVC,Model View Controller三层结构。请求由Controller(控制器)开始,分发给对应业务代码,Model(模型)代表的数据模型承担业务操作,最后将得到的结果送到View(视图)层渲染显示。
这里ContactServlet对应的就是Controller(控制器),用来做请求的分发。Model(模型)就值得ContactDao和数据库了,它提供我们需要的各种数据信息。几个jsp构成了View(视图)这一层,用来显示结果数据。
完整的例子在lingo-sample/06-02下,对应的源代码在WEB-INF/src下,将06-02复制到tomcat的webapps下就可以执行compile.bat进行编译。