JSP即Java Server Page,本质是Servlet。要讲JSP还是要先说Servlet,要说Servlet则离不开Tomcat,要谈Tomcat还是要从HTTP协议说起。
一、HTTP协议
1、简述与特点
HTTP协议是基于TCP/IP的应用层协议。
HTTP协议是无状态协议,协议本身并不保留之前一切的请求或响应报文的信息,为了保存一些信息,引入了Cookie。
HTTP协议是无连接协议。无连接又分为两种,HTTP/1.0和HTTP/1.1。1.0的时候,每一次请求都会建立一个连接,传完就关闭,下次重新开启个新的连接。现在的HTTP/1.1版本不是直接就断开了,而是等几秒钟,如果用户在这几秒钟之内有新的请求,那么还是通过之前的连接通道来收发消息,如果过了这几秒钟用户没有发送新的请求,那么就会断开连接。其实一般的web网站都是短连接,实时视频这种肯定是长连接了。
2、流程梳理
用户在浏览器上输入URL,进而DNS域名解析器会工作,将域名解析成对应的IP地址,客户端与Web服务器建立TCP的套接字连接(三次握手)。服务端接收并建立连接后,客户端生成HTTP格式的数据包作为请求报文发给服务端,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据等,服务端解析HTTP格式的数据包。之后在Servlet容器中找到对应的Servlet并执行业务方法,执行完后生成为HTTP格式的数据传回给客户端浏览器,浏览器再去解析HTTP格式的数据,之后浏览器再去渲染页面呈现给用户。
3、HTTP状态码
状态代码的第一个数字代表当前响应的类型:
1xx消息——请求已被服务器接收,继续处理
2xx成功——请求已成功被服务器接收、理解、并接受
3xx重定向——需要后续操作才能完成这一请求
4xx客户端请求错误——客户端请求错误或者无法被执行
5xx服务器错误——服务器在处理某个正确请求时发生错误
二、Tomcat详解
1、流程梳理
HTTP请求进来不是直接调业务方法,而是先从Servlet容器中找是否有请求所对应的Servlet,没找到就反射new一个出来加到容器中,然后再调用此Servlet的业务方法。和Spring容器一样,这样搞最大的好处就是解耦!
tomcat的主启动类是BootStrap的main方法。
2、整体结构
连接器即Coyoto封装了最底层的网络通信(Socket),还负责网络字节流传输,并将Request对象转化为ServletRequest对象后发给容器即Catalina。
Engine:一个service最多只有一个引擎
Host:对应于一个主机
Context:对应一个应用
Wrapper:对应一个Servlet
看了上图再看tomcat的配置文件server.xml就豁然开朗了。
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
keystoreFile="E:\apache-tomcat_key\tomcat.keystore" keystorePass="123456"
clientAuth="false" sslProtocol="TLS" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
</Service>
</Server>
另外上面的容器结构图中Host下面还可以配置Context。看到tomcat的conf文件夹下面还有context.xml配置文件。
<Context>
<!-- Default set of monitored resources. If one of these changes, the -->
<!-- web application will be reloaded. -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
</Context>
其实可以看出所有发布的应用都会归当前应用下的WEB-INF/web.xml
和E:\apache-tomcat-8.5.41\conf\web.xml
管理?
通常一个Catalina容器可以有多个服务,可以在Host标签中的appBase属性中设置发布路径。
IDEA这种IDE上配置的端口其实只是暂时让我能通过这个IP地址+端口号访问我们的应用,最后打成的war包还是要放到linux服务器上面的,最后还是要靠最终服务器上的配置文件来进行管理。war包放到webapps下面会自动解压缩,而且一旦删除war包连带的文件夹也会被删除掉。
三、JSP
1、原理分析
jsp本质就是Servlet。
tomcat下的web.xml中配置了映射关系,只要是.jsp以及.jspx结尾的URL都会交给JspServlet处理该请求。
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- The mappings for the JSP servlet -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
2、看下源码咯
JspServlet的service方法:
public void service (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// jspFile may be configured as an init-param for this servlet instance
String jspUri = jspFile;
if (jspUri == null) {
/*
* Check to see if the requested JSP has been the target of a
* RequestDispatcher.include()
*/
jspUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_SERVLET_PATH);
if (jspUri != null) {
/*
* Requested JSP has been target of
* RequestDispatcher.include(). Its path is assembled from the
* relevant javax.servlet.include.* request attributes
*/
String pathInfo = (String) request.getAttribute(
RequestDispatcher.INCLUDE_PATH_INFO);
if (pathInfo != null) {
jspUri += pathInfo;
}
} else {
/*
* Requested JSP has not been the target of a
* RequestDispatcher.include(). Reconstruct its path from the
* request's getServletPath() and getPathInfo()
*/
jspUri = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
jspUri += pathInfo;
}
}
}
if (log.isDebugEnabled()) {
log.debug("JspEngine --> " + jspUri);
log.debug("\t ServletPath: " + request.getServletPath());
log.debug("\t PathInfo: " + request.getPathInfo());
log.debug("\t RealPath: " + context.getRealPath(jspUri));
log.debug("\t RequestURI: " + request.getRequestURI());
log.debug("\t QueryString: " + request.getQueryString());
}
try {
boolean precompile = preCompile(request); //只是预编译
//主要是这个方法
serviceJspFile(request, response, jspUri, precompile);
} catch (RuntimeException e) {
throw e;
} catch (ServletException e) {
throw e;
} catch (IOException e) {
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
throw new ServletException(e);
}
}
JspServlet的serviceJspFile方法:
private void serviceJspFile(HttpServletRequest request,
HttpServletResponse response, String jspUri,
boolean precompile)
throws ServletException, IOException {
JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
if (wrapper == null) {
synchronized(this) {
wrapper = rctxt.getWrapper(jspUri);
if (wrapper == null) {
// Check if the requested JSP page exists, to avoid
// creating unnecessary directories and files.
if (null == context.getResource(jspUri)) {
handleMissingResource(request, response, jspUri);
return;
}
wrapper = new JspServletWrapper(config, options, jspUri,
rctxt);
rctxt.addWrapper(jspUri,wrapper);
}
}
}
try {
//主要是这个方法
wrapper.service(request, response, precompile);
} catch (FileNotFoundException fnfe) {
handleMissingResource(request, response, jspUri);
}
}
JspServletWrapper的service方法:
public void service(HttpServletRequest request,
HttpServletResponse response,
boolean precompile)
throws ServletException, IOException, FileNotFoundException {
Servlet servlet;
...
try {
if (options.getDevelopment() || mustCompile) {
synchronized (this) {
if (options.getDevelopment() || mustCompile) {
// The following sets reload to true, if necessary
ctxt.compile(); //生成对应的java文件和字节码文件
mustCompile = false;
}
}
} else {
if (compileException != null) {
// Throw cached compilation exception
throw compileException;
}
}
/*
* (2) (Re)load servlet class file
*/
servlet = getServlet(); //重新加载路径下编译好的字节码文件
...
if (servlet instanceof SingleThreadModel) {
// sync on the wrapper so that the freshness
// of the page is determined right before servicing
synchronized (this) {
servlet.service(request, response); //最终用PrintOut对象输出到浏览器
}
} else {
servlet.service(request, response);
}
} catch(Eeception e) {
...
}
虽然很绕,但问题不是很大,快要绕出来了!
JspCompilationContext的compile方法:
public void compile() throws JasperException, FileNotFoundException {
createCompiler();
if (jspCompiler.isOutDated()) {
if (isRemoved()) {
throw new FileNotFoundException(jspUri);
}
try {
jspCompiler.removeGeneratedFiles();
jspLoader = null;
jspCompiler.compile();
jsw.setReload(true);
jsw.setCompilationException(null);
} catch (JasperException ex) {
// Cache compilation exception
jsw.setCompilationException(ex);
if (options.getDevelopment() && options.getRecompileOnFail()) {
// Force a recompilation attempt on next access
jsw.setLastModificationTest(-1);
}
throw ex;
} catch (FileNotFoundException fnfe) {
// Re-throw to let caller handle this - will result in a 404
throw fnfe;
} catch (Exception ex) {
JasperException je = new JasperException(
Localizer.getMessage("jsp.error.unable.compile"),
ex);
// Cache compilation exception
jsw.setCompilationException(je);
throw je;
}
}
}
Compiler的compile方法:
public void compile(boolean compileClass, boolean jspcMode)
throws FileNotFoundException, JasperException, Exception {
if (errDispatcher == null) {
this.errDispatcher = new ErrorDispatcher(jspcMode);
}
try {
final Long jspLastModified = ctxt.getLastModified(ctxt.getJspFile());
//生成java文件
String[] smap = generateJava();
File javaFile = new File(ctxt.getServletJavaFileName());
if (!javaFile.setLastModified(jspLastModified.longValue())) {
throw new JasperException(Localizer.getMessage("jsp.error.setLastModified", javaFile));
}
if (compileClass) {
//生成字节码文件
generateClass(smap);
// Fix for bugzilla 41606
// Set JspServletWrapper.servletClassLastModifiedTime after successful compile
File targetFile = new File(ctxt.getClassFileName());
if (targetFile.exists()) {
if (!targetFile.setLastModified(jspLastModified.longValue())) {
throw new JasperException(
Localizer.getMessage("jsp.error.setLastModified", targetFile));
}
if (jsw != null) {
jsw.setServletClassLastModifiedTime(
jspLastModified.longValue());
}
}
}
} finally {
if (tfp != null && ctxt.isPrototypeMode()) {
tfp.removeProtoTypeFiles(null);
}
// Make sure these object which are only used during the
// generation and compilation of the JSP page get
// dereferenced so that they can be GC'd and reduce the
// memory footprint.
tfp = null;
errDispatcher = null;
pageInfo = null;
// Only get rid of the pageNodes if in production.
// In development mode, they are used for detailed
// error messages.
// http://bz.apache.org/bugzilla/show_bug.cgi?id=37062
if (!this.options.getDevelopment()) {
pageNodes = null;
}
if (ctxt.getWriter() != null) {
ctxt.getWriter().close();
ctxt.setWriter(null);
}
}
}
找到IDEA对于对应路径下面的文件打开查看。果然是PrintOut对象调用write()方法直接写到浏览器上
。
index_jsp的__jspService方法:
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
final java.lang.String _jspx_method = request.getMethod();
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET POST or HEAD");
//看源码终于明白了为啥SpringMVC的Rest风格会在tomcat7以上报错, 需要在头部加上isErrorPage="true"
return;
}
...
out.write("\n");
out.write("\n");
out.write("<html>\n");
out.write(" <head>\n");
out.write(" <title>$Title$</title>\n");
out.write(" </head>\n");
out.write(" <body>\n");
out.write(" $END$\n");
out.write(" </body>\n");
out.write("</html>\n");
...
//说到底最后就是PrintOut对象输出
3、JSP的九大内置对象
内置对象 | 作用 |
---|---|
request | 传递数据, 作用域为一次请求 |
response | 传递数据, 作用域仅在JSP页面 |
session | 服务器为每个用户都生成一个对象(默认过期时间为30分钟) |
application | 服务器为每个应用都生成一个对象 |
out | 用于在Web浏览器内输出信息 |
pageContext | 初始化由容器完成, JSP页面可直接用该对象。通过它可以获取 JSP页面的out、request、reponse、session、application 等对象 |
config | 取得服务器的配置信息, 通过pageConext.getServletConfig()可以获取该对象 |
page | page 对象代表JSP本身, 类似于this指针 |
exception | 用于显示异常信息, 包含 isErrorPage=“true” 的页面中才可以被使用 |
四、Thymeleaf模板引擎
1、简介
就是jsp的升级版,是一个模板引擎(java语言编写),可以处理html、xml、js、css等等,特别是html5。既可以展示静态数据也可以用后端传的数据直接展示,也可以实现前后端分离。不过也有缺点,Thymeleaf都是后端渲染的数据,渲染整个html文档,服务器压力大,Vue的只需要ajax请求数据,后端只需要渲染json数据就好了,服务器压力小很多。
这种模板引擎底层绝对逃不开Servlet。
需要引入名称空间:<html lang="en" xmlns:th="http://www.thymeleaf.org">
。
2、源码(只看前后缀)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/"; //前缀
public static final String DEFAULT_SUFFIX = ".html"; //后缀
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/"; //前缀
private String suffix = ".html"; //后缀
private String mode = "HTML";
private Charset encoding;
private boolean cache;
private Integer templateResolverOrder;
private String[] viewNames;
private String[] excludedViewNames;
private boolean enableSpringElCompiler;
private boolean renderHiddenMarkersBeforeCheckboxes;
private boolean enabled;
private final ThymeleafProperties.Servlet servlet;
private final ThymeleafProperties.Reactive reactive;
...
}
3、基本语法及内置对象
基本语法:
语法 | 描述 | 作用 |
---|---|---|
${...} | 变量表达式 | 取出上下文变量的值 |
*{...} | 选择变量表达式 | 取出选择对象的属性值 |
#{...} | 消息表达式 | 取出国际化消息 |
@{...} | 链接表达式 | 表示超链接地址 |
对象 | 描述 |
---|---|
#ctx | 上下文对象 |
#var | 上下文变量 |
#locale | 上下文地理区域对象 |
#request | HttpServletRequest对象 |
#response | HttpServletResponse 对象 |
#session | HttpSession 对象 |
#servletContext | ServletContext 对象 |
3、简单demo
html界面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<div th:text="${msg}"></div>
<div>[(${msg})]</div>
<hr>
<h2 th:each="user:${users}">[(${user})]</h2>
<hr>
<h2>
<span th:each="user:${users}">[[${user}]]</span>
</h2>
<hr>
<div th:object="${person}">
<h2 th:text="*{name}"></h2>
<h2>[[*{age}]]</h2>
</div>
<hr>
<div th:text="${#request.getAttribute('msg')}"></div>
</body>
</html>
controller:
@RequestMapping("/hello")
public String hello(Model model) {
model.addAttribute("msg", "<h2>哈哈哈</h2>");
List<String> users = new CopyOnWriteArrayList<String>();
users.add("张三");
users.add("李四");
users.add("王五");
model.addAttribute("users", users);
Person p = new Person();
p.setAge(18);
p.setName("xxx");
model.addAttribute("person", p);
return "hello";
}
运行结果:
都是用th作为前缀,text表示对特殊字符转译并写入文本(转译后可以原样输出),utext是不对特殊字符进行转译并写入文本。
属性上的th:text等价于行内写法[[${...}]]
。属性上的th:utext等价于行内写法[(${...})]
。
SpringBoot可以集成Thymeleaf、Freemaker等等,但不支持JSP。