前言
开心一刻
有个同学去非洲援建,刚到工地接待他的施工员是个黑人,他就用英语跟人家交流,黑人没做声。 然后他又用法语,黑人还是没说话。 然后他用手去比划。黑人终于开口了:瞎比划嘎哈,整个工地都中国人
前提背景
在利用maven/eclipse搭建ssm(spring+spring mvc+mybatis)一文的问题反馈中,大体分两个:404和页面无数据;至于500,个人认为比较好解决,按照提示进行处理就好,本文就不讨论500了
404
主要也是两种
1、webapp未发布
相关资源未部署,例如webapp未发布部署,类似如下
不只是webapp,main下的java、resources、webapp,maven依赖都是需要部署到tomcat,不然就不完整,就会存在各种各样的少内容的问题;
2、请求URL不对
这个确实是很多新入行的小伙伴容易出现的问题
如果工程正常部署,请求URL出现404,很有可能是我们请求的URL不对;我们到tomcat的home目录下看看工程是否正常部署,类似如下
还可以看看工程发布的内容(问题1中需要发布的内容)是否都在;如果工程部署正常,而请求的URL又出现404,那不用想,就是你的URL写错了
404的解决方案就是:确认工程是否正确部署到tomcat,确认请求的URL是否正确,基本只要确认这两点也就能找到问题了;后文不会再详细的讲404,我们将重点放到下面这个问题上
页面无数据
具体的问题应该是这样的:当我们请求:http://localhost:端口/工程名/personController/showPerson时,数据正常显示如下
当我们直接请求jsp时,只有title没有数据,如下
这是为什么?
对于这个问题一开始确实没太在意,只是提示小伙伴去看servlet的四大作用域和jsp的九大内置对象,后面陆陆续续很多小伙伴都问了我,包括评论区留言、站内消息、QQ私聊等
站内信
评论区
我发现这个问题好像不是个别小伙伴的问题,很多新入门的小伙伴都存在这样的疑问,下面我们就对此次问题就行一个详细的探究;后续篇幅较长,基础铺垫较多,希望大家耐心看完!
问题探究
servlet与servlet容器
狭义上来讲,servlet指的就是接口:javax.servlet.Servlet,广义上来讲,servlet指的是servlet规范:Java Servlet API 标准;javax.servlet.Servlet与servlet容器都是servlet规范下的产物。Java Servlet API是Servlet容器和Servlet之间的接口,它定义了Servlet的各种方法,还定义了Servlet容器传送给Servlet的对象类,其中最重要的是请求对象ServletRequest和响应对象ServletResponseo这两个对象都是由Servlet容器在客户端调用Servlet时产生的,Servlet容器把客户请求信息封装在ServletRequest对象中,然后把这两个对象都传送给要调用的Servlet,Servlet处理完后把响应结果写入ServletResponse,然后由Servlet容器把响应结果发送到客户端。
Servlet与Servlet容器的关系有点像枪和子弹的关系,枪是为子弹而生,而子弹又让枪有了杀伤力。虽然它们是彼此依存的,但是又相互独立发展,这一切都是为了适应工业化生产的结果。从技术角度来说是为了解耦,通过标准化接口来相互协作。Servlet 容器作为一个独立发展的标准化产品,目前它的种类很多,包括Jetty、tomcat、resin、JBoss、WebSphere、Weblogic等,这些都是成熟的产品,有专门的公司或者组织进行维护,我们直接拿来用就好。
我们约定下,下文中的servet指的都是servlet接口:javax.servlet.Servlet,servlet容器指的是:Tomcat,Web服务器与Servlet容器是同一个内容(实际是有区别的,具体区别大家自行去查阅)
Tomcat容器模型如下
Tomcat响应客户请求过程
其中,①处表示Web服务器接收到客户端发出的HTTP请求后,转发给Servlet容器,再由Servlet容器转发给具体的Servlet实例进行请求的处理;②处表示Servlet实例将处理结果封装进ServletResponse中,再由Servlet容器把ServletResponse发给Web服务器,通知Web服务器以HTTP响应的方式把结果发送到客户端。也就是说,与客户端直接打交道的是tomcat(servlet容器),而不是我们的Servlet实例,而真正处理请求的才是我们的Servlet实例。
说的简单点,我们自定义的Servlet,其实是对servlet容器在业务层面的拓展,相当于业务定制一样;我们可以这样理解,servlet容器对servlet提供技术支持,而servlet对servlet容器提供业务拓展,两者缺一不可,缺了技术支持,业务拓展实施不起来,缺了业务拓展,技术支持没有现实意义。Servlet容器封装了底层复杂的技术实现,使我们可以专注于业务实现,而Servlet容器与业务实现之间的纽带就是Servlet接口,它是我们对Servlet容器进行业务拓展的标准,所以我们的业务需要实现Servlet接口。套用阿基米德的杠杆原理:给Servlet容器多个servlet实例,Servlet容器还你丰富的web服务。
JSP
示例代码:our-servlet
我们先来看看在jsp出现之前,servlet如何输出页面,HelloServlet如下
package com.lee.servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String businessDate = "业务数据...."; resp.setCharacterEncoding("utf-8"); resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); out.write("<html>"); out.write("<head>"); out.write("<title>Hello World</title>"); out.write("</head>"); out.write("<body>"); out.write("<h1>Hello World!</h1>"); out.write("<div><span><strong>"); out.write(businessDate); out.write("</strong></span></div>"); out.write("</body>"); out.write("</html>"); out.flush(); out.close(); } }
不仅仅是业务数据,还包括静态页面的内容,通通在servlet返回,如果页面简单,这么处理也能接受,但是如果页面像淘宝、京东那样非常复杂,你能想象吗?太容易出错了,一旦静态页面的元素少了或者多了内容,都不知道如何排查,面对茫茫多的out.write,就只有哭的份了。所以jsp就应运而生了。
JSP全称:Java Server Pages,允许在传统静态网页HTML中插入Java代码片段(Scriptlet)和JSP标签,以简化页面静态内容的开发。但需要注意的是,JSP文件的本质还是Servlet,只不过与Servlet不同的是,JSP是专门用于进行数据展示的Servlet;JSP最终会被Tomcat解析成Servlet,在Tomcat内置了一个JSP解析引擎,当第一次访问该JSP页面时,解析引擎会将JSP页面解析成Servlet,然后再由Servlet将动态数据、静态内容全部输出到浏览器供展示。我们来看看jsp解析后的文件在哪里、内容是什么,以示例中的index.jsp为例。路径如下图
index_jsp.java
/* * Generated by the Jasper component of Apache Tomcat * Version: Apache Tomcat/7.0.47 * Generated at: 2019-04-08 13:14:31 UTC * Note: The last modified time of this file was set to * the last modified time of the source file after * generation to assist with modification tracking. */ package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent { private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory.getDefaultFactory(); private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants; private javax.el.ExpressionFactory _el_expressionfactory; private org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map<java.lang.String,java.lang.Long> getDependants() { return _jspx_dependants; } public void _jspInit() { _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory(); _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig()); } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html; charset=utf-8"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; out.write("\n"); out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n"); out.write("<html>\n"); out.write("<head>\n"); out.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n"); out.write("<title>index</title>\n"); out.write("</head>\n"); out.write("<body>\n"); out.write(" <div>\n"); out.write(" Welcome, my friend!\n"); out.write(" </div>\n"); out.write("</body>\n"); out.write("</html>"); } catch (java.lang.Throwable t) { if (!(t instanceof javax.servlet.jsp.SkipPageException)){ out = _jspx_out; if (out != null && out.getBufferSize() != 0) try { out.clearBuffer(); } catch (java.io.IOException e) {} if (_jspx_page_context != null) _jspx_page_context.handlePageException(t); else throw new ServletException(t); } } finally { _jspxFactory.releasePageContext(_jspx_page_context); } } }
发现熟悉的out.write又回来了,只是此时的out.write不是我们手动写的,而是Tomcat解析jsp后生成的;如果jsp没变动,jsp只会在第一次被调用时解析、编译一次,后续的请求都会由编译后的servlet处理,我们来验证下,如何验证了? 不变index.jsp内容再请求index.jsp,看看上图中文件的修改时间会不会变
发现文件的修改时间没有变动,也就是说上面的的结论:如果jsp没变动,jsp只会在第一次被调用时解析、编译一次是对的。感兴趣的朋友可以去看下Tomcat的源码,看看具体的实现细节。
有人可能会问:为什么不将jsp的内容直接返回给浏览器?我们要明白一点:浏览器只能解析html、css、js,除此之外的内容它解析不了,那么我们能直接将jsp的内容返回给浏览器吗?所以中间有处理过程,最终由servlet将静态内容返回给浏览器。有些爱问的小伙伴可能又会问了:浏览器为什么只能解析:html、css、js,这涉及到浏览器规范的问题,除非你有能力改变这个规范,让浏览器支持你想要的内容,这个问题不做过深的讨论,我们姑且认为这是浏览器的限制,既然我们改变不了这个限制,那就适应这个限制。
Servlet四大作用域与JSP九大内置对象
Servlet四大作用域包括:page域、request域、session域、application域,作用域指的是变量的有效期限,具体如下
当变量的作用域是page,它的有效范围只在当前jsp页面里有效;
当变量的作用域是request,它的有效范围是当前请求周期,所谓请求周期,就是指从http请求发起,到服务器处理结束,返回响应的整个过程,在这个过程中可能使用forward的方式跳转了多个jsp页面,在这些页面里你都可以使用这个变量;
当变量的作用域是session,它的有效范围是当前会话,何为当前会话,就是指从用户打开浏览器开始,到用户关闭浏览器的整个过程,这个过程可能包含多个请求响应;
当变量的作用域是application,它的有效范围是整个应用,何为整个应用,就是指从应用启动,到应用结束;
JSP九大内置对象包括:page、request 、response、pageContext、session、application、out、config、exception,内置对象指的是Servlet容器创建的一组对象,不需使用new关键字就可以直接使用的内置对象。
四大作用域与九大内置对象对应关系如下
更多详情需要大家自己去查阅资料了
EL表达式与JSTL标签
我们知道jsp中可以插入Java代码片段,类似如下
<%pageContext.setAttribute("sex", "男"); %> <!-- 设置值,作用域是当前jsp页面 -->
<div>
<%=pageContext.getAttribute("sex") %> <!-- 注意去看解析后的el_jsp.java,被解析成了out.print(pageContext.getAttribute("sex") ); -->
Welcome, my friend!
</div>
其中<% %>包裹的就是java片段,<%= %>输出表达式值到页面;可以看到不够简洁,阅读性也不太友好,所以EL表达式就应运而生了,上述代码可以替换成如下代码
<%pageContext.setAttribute("sex", "男"); %> <!-- 设置值,作用域是当前jsp页面 -->
<div>
${sex} Welcome, my friend! <!-- ${expression} EL的语法结构 -->
</div>
EL能够访问页面的上下文以及不同作用域中的对象 ,取得对象属性的值,或执行简单的运算或判断操作,用来简化JSP中的java代码。EL表达式是JSP1.2之后内置支持的,可以直接在JSP中使用,它从servlet四大作用域(范围servletContext > session > request > pageContext)中取值,这四个域都有setAttribute("",object)方法和getAttribute("")方法, EL表达式会自动按作用范围从小到大的顺序从四大作用域中寻找对应名字的值,找到了就立即返回不再继续寻找,其内部调用的就是pageContext的findAttribute("")方法。
EL固然能简化JSP中的java代码,但是它功能非常简单,不能满足一些复杂的代码逻辑,所以就诞生了JSTL。JSP标准标签库(JSTL)是一个JSP标签集合,它封装了JSP应用的通用核心功能,支持通用的、结构化的任务,比如迭代,条件判断,XML文档操作,国际化标签,SQL标签,另外还支持自定义标签,它实现了JSP页面中的代码复用、简化了代码的书写,同时也保证了JSP的可读性更强。JSTL功能比较丰富,但它不是JSP内置支持的,所以需要导入标签库到JSP页面(还要添加jstl的jar包依赖)。JSTL往往会集合EL表达式来使用,简单示例如下
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <!-- 引入JSTL标签库,c表示标签库别名,可以任意命名,一般而言用c --> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>index</title> </head> <body> <c:set var="sex" value="女" scope="page"></c:set> <!-- scope指定作用域,page/request/session/application --> <c:if test="${sex == '男' }"> Hello, ${sex} </c:if> <c:if test="${sex != '男' }"> Hi, girl </c:if> </body> </html>
这代码看起来就清爽多了,没有java代码,前端开发者也很容易看懂;关于EL表达式与JSTL标签更详细信息,需要大家自行去查阅资料了,本文篇幅有限,不做过多的讲解了。
重定向与请求转发
那么可想而知,重定向的request作用域的变量是会失效的,而转发则不会
Spring MVC
还记得我们是如何配置Spring MVC的吗, 我们会在web.xml中配置如下代码
<servlet> <servlet-name>springDispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springDispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
这样就配置了Spring MVC;大家可以留意下DispatcherServlet,去看他的类图会发现,他就是一个Servlet的实现,也就是说Sprinv MVC就是基于Servlet的拓展。
我们在Spring MVC基础上进行开发的时候,将数据绑定到作用域的时候,一般用的是SpringMVC的数据模型:Model或者ModelMap,例如这样
@RequestMapping("/showPerson") public String showPersons(Model model){ List<Person> persons = personService.loadPersons(); model.addAttribute("persons", persons); // 绑定数据到视图 return "showperson"; }
而不是显示的直接绑定到Servlet四大作用域,数据难道没有绑定到四大作用域? 我们说过,EL表达式只能在四大作用域中取值,否则取不到,所以SpringMVC中的数据绑定最终还是会到四大作用域的某一个中,至于是何时、何地、如何将Model中的属性绑定到哪个作用域,这个不是本文要说的了,篇幅太大了,有兴趣的可以去看看这篇博客:springmvc的工作原理,我们来看看其源码实现。这里给个结论:在默认情况下,Model中的属性作用域是request级别。
问题解答
有些小伙伴会抱怨了:上面哔哔了那么多,怎么就是不讲答案,净说一些没用的
如果大家坚持看到这了,再坚持会,答案马上揭晓,上面铺垫了那么多,绝对是有用的。
我们回到问题:当我们请求http://localhost:端口/工程名/personController/showPerson时,数据正常显示,而当我们直接请求jsp时,只有title却没有数据,这是为什么?title是静态页面内容,这个不用管,那为什么直接请求jsp为什么没有数据库的person列表呢? jsp源代码如下
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>person list</title>
</head>
<body>
<table>
<tr>
<th>姓名</th>
<th>年龄</th>
</tr>
<c:forEach items="${persons}" var="person">
<tr>
<td>${person.name }</td>
<td>${person.age }</td>
</tr>
</c:forEach>
</table>
</body>
</html>
里面用到了<c:forEach>和EL表达式,解释下这个流程:EL表达式先从四大作用域获取名为persons的集合,然后<c:forEach>遍历该集合,每次遍历的结果放到page作用域,并取名叫person,最后通过EL表达式输出person的name和age到页面。那么请问:直接访问JSP,四大作用域中有名叫persons的属性吗?很显然没有,persons不存在,遍历它会有结果输出吗?这就是为什么直接访问jsp没有数据的答案。
我们再回到Controller层
@RequestMapping("/showPerson") public String showPersons(Model model){ List<Person> persons = personService.loadPersons(); // 从数据获取person列表,并存放到了persons集合中 model.addAttribute("persons", persons); // 将persons集合添加到model的persons属性中 return "showperson"; // 转发到showperson.jsp }
代码也非常简单,先从数据库获取person集合,然后将该集合设置到了model的属性persons中,我们知道model的属性默认情况下会设置到request作用域;然后将请求转发到showperson.jsp,转发过程中,request作用域的变量仍然有效,所以jsp中EL表达式能够读取到persons变量,所以就有数据输出到页面了。
总结
1、Servlet与Servlet容器的关系比较暧昧,两者相互作用,实现web服务;简单点说,我们自定义的Servlet就是对Servlet容器的业务拓展,而Servlet容器是对Servlet的支撑;
2、JSP的出现时为了简化静态页面的开发,EL表达式与JSTL的出现则是为了简化JSP页面的Java代码;JSP本质还是Servlet,在第一次被访问的时候会被Servlet容器解析成Servlet、编译Servlet,最终还是有Servlet将页面内容out.write到浏览器;
3、Spring MVC本质还是Servlet,它的出现是为了简化web开发,同时可以与spring无缝对接,享受spring带来的好处;Spring MVC的数据绑定,依托的还是Servlet的的四大作用域,只是中间存在转换过程;
4、EL表达式的取值必须存在于四大作用域中,在jsp中用EL表达式时,一定要保证数据正确地添加到了四大作用域中,不然,EL表达式会取不到值;
参考
《深入分析JavaWeb技术内幕》
《Tomcat 系统架构与模式设计分析》