从0到1理解JavaWeb(更新至2017-03-25 18:35:46)

红色警戒:本文随时可能更新,其排版可能乱七八糟、一塌糊涂,其长度可能极长,请坐好小板凳,听我讲述另一个宇宙的罗曼史。
作者:世界上体型最大的吃瓜群众邱永臣

Web容器

Web容器是咩遭仔啊?

Wait a minute, man.
CS(computer science)常识告诉我们,一个吃瓜群众访问一个网站的过程中,底层通讯协议用的是HTTP(占用服务器的8080端口),让我们回想一下CS常识,嘛个瓜子是计算机进程咧?
一个身影庄严且肃穆地站立着,以帕瓦罗蒂一般的嗓音宣告:『进程是一个运行着的程序,会占据计算机的某个端口』。

这不就结了,不占据端口的进程是伪进程,占据端口的进程才是真.端口。某个进程占据着80端口,专门监听请求。用户们访问网站时,请求通过HTTP协议,穿过8080端口,被这个进程捕捉、处理。有同学问:『那这个特殊的进程是啥子咧』?它就是Web容器。

由此可见,Web容器是一个程序(用移动互联网时代的黑话来说,Web容器就是一个APP)。Web容器和和其余程序相似,最大的区别是Web容器运行稳定,不容易心情变丧,不容易崩溃。

容器,有容纳管理其内部内容的本事,Web容器有什么通天的本事?
在网络的发展初期,上网的底层实现,不过是浏览器请求一种后缀名为html的文件,请求到本地,浏览器渲染,显示给用户,OK,这个时期的Web资源,指的就是那些后缀名为html的文件。

后来,大家厌烦了一成不变的html,觉得每次访问页面,其结果均相同,不爽,绞尽脑汁,想法设法显示一些会随着时间而改变的内容。所以,程序员们在服务器的程序里写了特殊逻辑,每次访问时,页面上的内容都不一样。这个时期的Web资源,指的是那些在服务端程序动态生成的内容,不同时间点访问网站,其页面内容是不同的。

所以,吃瓜群众们上网时,浏览的内容是网站的Web资源:静态Web资源和动态Web资源。

Web容器可以管理一个或多个Web应用,Web应用可以管理大量的静态Web资源和动态Web资源。
图例如下:

Web容器种类

Web技术发展了多年,有许许多多的Web容器被造了出来,有巨硬家的IIS,有Sun家的Tomcat,有BAE家的WebLogic等等,JavaWeb领域,最常用的Web容器的Tomcat。


从零搭建webapp

使用Maven模板搭建webapp

任何一个吃瓜群众,只要他知道Maven是干什么用的,都能创建一个可以运行的webapp。

在终端粘贴以下命令并执行:

1
mvn archetype:generate -DarchetypeCatalog=internal

命令执行过程中,会弹出选项让吃瓜群众选择,你要创建哪个模板的项目就输入对应选项最前边的数字,本吃瓜群众选择了10,将会创建一个Webapp项目(原则上你选10最好,因为你选择其它的模板,创建出来的项目不一定能如你所想象那样运行起来)。创建完项目,就用IntelliJ IDEA导入该项目,项目结构是这样的:

web.xml内容如下:

1
2
3
4
5
6
7
8

<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>

pom.xml内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>test Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>test</finalName>
</build>
</project>

index.jsp内容如下:

1
2
3
4
5
<html>
<body>
<h2>Hello World!</h2>
</body>
</html>

实际上,你也可以手动创建一个项目,然后照瓢画葫芦,新建以上三个文件,分别复制内容进去,项目的基本模板就搭建好了。

注:
代表了Webapp的是web.xml,它指定了整个Webapp的运转规则(比如收到请求Url应该如何处置)。pom.xml,什么滴干活?太君,您别误会,它是Maven的配置文件,只对Maven生效,Webapp根本不知道它的存在,编译项目的结果中并没有pom.xml,如下图中IDEA的编译target:

看到了麽,pom.xml仅仅是用来配合Maven,配置包之间的依赖关系,是给编译过程服务的,编译完了也就没它事了。

运行Webapp

下载好Tomcat后,添加在IDEA里头,再给项目指定运行环境:

1
Run -> Edit Configurations -> 左上角’+’号 -> Tomcat -> Local -> Deployment -> 中间’+’号 -> Artific -> 选择后面附带war exploded的那项 -> 点击OK

然后再在界面中找到绿色的三角按钮,用力点击它,不要乱动键盘,几秒钟后,你的浏览器会自动弹出来,显示『Hello World』,快乐地玩耍吧。

在Webapp编写Servlet

在main目录下新建一个java目录,并点击右键-> 选择Mard Directory As -> 点击Sources Root,将java目录设置为源码根目录,以后写java代码都塞入源码根目录。

编写Servlet内容

按照你的喜欢,建立几个目录,随意选择一个目录,在里面新建一个类,并继承自HttpServlet,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.iloveqyc.web;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/3
* Time: 下午2:57
* Usage: xxx
*/
public class testServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello");
System.out.println("邱永臣");
PrintWriter writer = resp.getWriter();
writer.print("someone who is stupid is coming");
writer.close();
}
}

注:
如果没错的话,第一次引入HttpServlet,在编辑器里,HttpServlet会被标红,没关系,把鼠标放在HttpServlet上面点一点,等左边出现红色警告符号后,点击警告符号,选择 『add Maven…』 ,(这会在pom.xml里自动引入Servlet-api包),再按 ”alt + enter”,自动引入HttpServlet类。

注册Servlet

写好的Servlet,得到Webapp注册,并配置相应的规则,让Webapp知道在什么情况下调用Servlet。吃瓜群众可以且仅可以在web.xml中注册和配置Servlet,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>

<!--注册Servlet,WebApp才知道该Servlet的存在-->
<servlet>
<servlet-name>testServlet</servlet-name>
<servlet-class>com.iloveqyc.web.testServlet</servlet-class>
</servlet>
<!--配置Servlet的规则,WebApp才知道在什么情况下调用该Servlet-->
<servlet-mapping>
<servlet-name>testServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>

再次运行WebApp

再次运行,您作为一个伟大的吃瓜群众,将于几秒中后,在自动弹出的浏览器界面看到”someone who is stupid is coming”。


Tomcat

Tomcat来由

维基百科有言,

Tomcat是由Apache软件基金会下属的Jakarta项目开发的一个Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持,并提供了作为Web服务器的一些特有功能,如Tomcat管理和控制平台、安全域管理和Tomcat阀等。由于Tomcat本身也内含了一个HTTP服务器,它也可以被视作一个单独的Web服务器。

这说的是什么意思?
简单地说,Tomcat是个Servlet容器,同时也是个Web容器,既可以管理动态/静态Web资源,也可以管理Servlet。
(最开始时,Tomcat是个Servlet容器,随着时间的流失,诸如Spring/Struct等框架出现,风起云涌,JavaEE的开发逐渐弱化了Servlet的概念,Tomcat的身份也从一个小小的Servlet容器,逐渐转为Java语言的Web容器。)

Servlet容器?什么是Servlet容器,Servlet又是什么?答案极其简单,留到后面再讨论。

Tomcat安装

请自行谷歌。

Tomcat目录结构

  • bin目录,存放startup.sh、shutdown.sh等文件,你想强制关闭Tomcat,可以到这里来溜溜。
  • conf目录,存放server.xml等文件,你想改变Tomcat对各个Web应用的映射方式等,可以到这里来溜溜。
  • webapps目录,该目录是Tomcat之所以被称为『Web容器』的根源之一,你把你自己的Web应用放在此处,Tomcat帮你管理Web应用。

Tomcat体系结构

从原理上看Tomcat结构,如下图:

如果没看过Tomcat底层实现,吃瓜群众很难看懂上图,没关系,我们可以从实用主义者的角度看世界,看看Tomcat的运转流程,如下图:

Tomcat启动时,根据配置文件server.xml(位于conf目录下)先启动一个server,再启动一个service。server的作用是管理service,掌握着service的生杀大权。
启动完了service,再启动多个connector和多个host(如果有多个host的话)。connector只是一个对外的接口,接收用户发来的各种请求,比如http请求,再比如https请求,接收到请求,connector并不负责处理,而是转身就把请求塞给engine,告诉对方说:『engine老兄,这些请求就拜托您老人家去处理了,我可没时间没精力去管哪』。
engine接收到从connector那儿传来的请求,解析出请求对应的host,将请求转交给host。
host接收到从engine那儿传来的请求,解析出请求中的url该由哪个web应用处理,再将请求交由该web应用对应的context,也就是传说中的某个web应用的『上下文』。上下文这玩意说简单也简单,说复杂挺复杂的,简单的,就是一个应用运行期间都一直存在的资讯,诸如应用自身叫啥名,打哪儿来,到哪儿去,家里有几口人,几头牛,几亩地等等,复杂的,可以获取整个应用所有信息,包括该应用各个角落的各种信息,all of them。

Tomcat的结构中,有Server/Service/Engine/Connector/Host/Context等多种角色,但最终处理用户请求的,只有Context。

一个典型的Tomcat服务器,其server.xml配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">

<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
maxThreads="150" scheme="https" secure="true"
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">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>
</Engine>

</Service>
</Server>

吃瓜群众里边,稍微聪明的,可以看出有一个Connector的端口是8080,还有一个是8443,前者接收http请求,后者接收https请求。


Servlet

坐在凳子上,围绕着篝火,啃了N多瓜子,吃瓜群众们不仅知道了Web容器和Tomcat是什么玩意,也写了一个可以运行起来的Web应用,更是流弊地写了一个Servlet,并通过浏览器访问到了!那么Servlet到底是什么?Servlet和Tomcat之间有什么纠缠?

Servlet介绍

Servlet与Web容器

Servlet乍一听,挺厉害的,但它的实质极其简单,Servlet,就是个实现了servlet接口的Java类,重复,Servlet就是个Java类,再重复,Servlet是类。

之前我们说过Tomcat既是个Web容器,也是个Servlet容器。Tomcat根据web.xml来管理Web应用,所以Tomcat是Web容器,同时,我们写了一个Servlet,将它注册在web.xml里,声明『此Servlet属于此Web应用』,Tomcat就能根据web.xml来管理Servlet了,所以Tomcat也是Servlet容器。Tomcat/Web应用/Servlet三者的关系如下图:

可见,一个Web应用里头有多个Servlet(但SpringMvc等框架都只用一个Servlet来处理请求,弱化了Servlet的存在感),Tomcat可以同时管理多个Web应用,虽然说在实际应用时,一般Tomcat只管理一个Web应用。

我们可以得到结论:Servlet只是实现了servlet接口的类。

Servlet与Tomcat之间的生死存亡

既然Tomcat是Servlet容器,它就肯定有控制Servlet生命周期的法子。

首先,Tomcat是个运行着的Web容器,它如何知晓Servlet的存在?
答案在于web.xml,它是一个Web应用的核心,很大程度上代表了Web应用。我们在开发Servlet时,在web.xml里登记了Servlet的相关信息,比如servlet-name、servlet-class、servlet-mapping。
servlet-name的存在,让tomcat知道该Servlet的大名。
servlet-class,让tomcat知道该Servlet的本体是哪个类(Tomcat没有扫描的功能,不能仅根据servlet-name就找到对应的Servlet本体)。
servlet-mapping非常关键,某个请求抵达tomcat内部时,tomcat得拿着请求里的url参数和方法,到各个servlet的servlet-mapping中,一个个对应,看看该请求可以交给哪个servlet处理。
比如servletA的servlet-mapping值为”/service/A”,servletB的servlet-mapping值为”/service/B”,我们访问url:localhost:8080/service/A,请求会被servletA处理,访问url:localhost:8080/service/B,请求会被servletB处理。

其次Tomcat知道了Servlet的存在,也能找到Servlet的本体(即Servlet的java类),Tomcat何时初始化Servlet,何时调用Servlet,何时又销毁Servlet呢?
欲知答案,请看下图:

Tomcat启动时,只会加载Web应用的信息,并不会创建Servlet。
只有在用户想要访问某个Servlet时,Tomcat才懒洋洋地去创建用户想要访问的Servlet(调用servlet的init方法),并调用它的service方法处理用户请求。
Servlet被创建出来后,会一直存在,有用户访问该Servlet时,Tomcat自动调用它的service方法,service方法接收用户的request,返回response。
Tomcat不会无缘无故销毁Servlet,除非Web应用被停止或重启。

到这里,吃瓜群众们已经知晓Tomcat如何管理Servlet。

HttpServlet

如果我们要开发Servlet,势必要编写许多实现了Servlet接口的类,而Servlet接口仅指定了init()/service()/destroy()三个方法,如果不做进一步的包装,开发量极为巨大。

所以人们为了减少吃瓜群众的编程工作量,额外封装了一个HttpServlet类,该类实现了Servlet的三个接口,在此基础上,额外编写了多个便于处理Http请求的方法,比如doGet()和doPost()。

当HttpServlet在它的service方法中检测到Http请求的Method是Get,会自动调用HttpServlet的doGet()方法处理请求;如果请求的Method是Post,会自动调用HttpServlet的doPost()方法处理请求。
HttpServlet源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();

if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}

} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);

} else if (method.equals(METHOD_POST)) {
doPost(req, resp);

} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);

} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);

} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);

} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);

} else {

String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);

resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}

所以我们开发Servlet时,直接继承HttpServlet类即可,当然,不嫌麻烦可以直接实现Servlet接口。

Servlet的高级对象

对servlet的了解,一般的吃瓜群众只需知道它从哪儿来,到哪儿去即可。如果对servlet感兴趣,可以继续阅读下面对servlet高级对象的介绍。

何为Servlet高级对象

利用高级对象,我们可以对Servlet进行自定义,获取Servlet的信息等待,高级对象主要有ServletConfig/ServletContext。

ServletConfig

看名字就能知道,ServletConfig对象存放的是某个Servlet的配置,我们可以在注册Servlet时,使用init-param标签给Servlet加上初始配置,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>

<!--注册Servlet,WebApp才知道该Servlet的存在-->
<servlet>
<servlet-name>testServlet</servlet-name>
<servlet-class>com.iloveqyc.web.servlet.ResDefaultServlet</servlet-class>
<!--给testServlet加上初始配置,属性createdBy的值为qiuyongchen-->
<init-param>
<param-name>createdBy</param-name>
<param-value>qiuyongchen</param-value>
</init-param>
</servlet>
<!--配置Servlet的规则,WebApp才知道在什么情况下调用该Servlet-->
<servlet-mapping>
<servlet-name>testServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>

Web容器在创建Servlet时,会调用Servlet的init方法,并将ServletConfig传进来,如下可以获取ServletConfig里面的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.iloveqyc.web.servlet;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/3
* Time: 下午2:57
* Usage: xxx
*/
public class ResDefaultServlet extends HttpServlet {

private ServletConfig servletConfig;

@Override
public void init(ServletConfig config) throws ServletException {
servletConfig = config;
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = servletConfig.getInitParameter("createdBy");
System.out.println(name);

System.out.println(this.getClass().toString() + " had been visited");
PrintWriter writer = resp.getWriter();
writer.print("the init res servlet");
writer.close();
}
}

运行以上代码,你将可以在控制台看到qiuyongchen。

可以看出,我们可以利用ServletConfig给Servlet加上初始配置,并在代码里获取到配置值。

ServletContext

ServletContext,从名字可以看出,这应该是一个上下文。和ServletConfig只对应一个Servlet不同,ServletContext对应所有Servlet,也就是说,所有的Servlet都共享一个ServletContext。

从哪儿可以获取到ServletContext实例?
第一,由于一个Web应用有且仅有一个ServletContext,所以每个Servlet的ServletConfig里头都有ServletContext的引用,我们可以从ServletConfig获取。
第二,直接调用GenericServlet接口里的getServletContext方法(本质上也是从ServletConfig获取)。

利用Servlet可以干什么?
首先,既然所有Servlet共享一个ServletContext,那么就可以把ServletContext当做中转器,Servlet间就可以通信了。
其次,一个Web应用仅有一个ServletContext,ServletContext既相当于Servlet的上下文,也可以被视为Web应用的上下文,所以ServletContext有时被用来获取Web应用的初始配置值,如下:
在web.xml里配置context-param

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>

<!--Web应用的初始配置-->
<context-param>
<param-name>nameOfWebapp</param-name>
<param-value>qiuyongchen's web app</param-value>
</context-param>

<!--注册Servlet,WebApp才知道该Servlet的存在-->
<servlet>
<servlet-name>testServlet</servlet-name>
<servlet-class>com.iloveqyc.web.servlet.ResDefaultServlet</servlet-class>
</servlet>
<!--配置Servlet的规则,WebApp才知道在什么情况下调用该Servlet-->
<servlet-mapping>
<servlet-name>testServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>

在代码里获取整个Web应用的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.iloveqyc.web.servlet;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/3
* Time: 下午2:57
* Usage: xxx
*/
public class ResDefaultServlet extends HttpServlet {

private ServletConfig servletConfig;

@Override
public void init(ServletConfig config) throws ServletException {
servletConfig = config;
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = servletConfig.getServletContext().getInitParameter("nameOfWebApp");
System.out.println(name);

System.out.println(this.getClass().toString() + " had been visited");
PrintWriter writer = resp.getWriter();
writer.print("the init res servlet");
writer.close();
}
}

HttpServletRequest & HttpServletResponse

Tomcat默认监听8080端口,当它在8080端口监听到浏览器给它发来的Http请求时,它会将Http请求封装成HttpServletRequest,并另外构建一个HttpServletResponse,将两个对象同时传给Servlet的service方法处理,如下:

1
2
3
4
5
6
7
8
9
10
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = servletConfig.getServletContext().getInitParameter("nameOfWebApp");
System.out.println(name);

System.out.println(this.getClass().toString() + " had been visited");
PrintWriter writer = resp.getWriter();
writer.print("the init res servlet");
writer.close();
}

如果我们想获取浏览器传来的数据,则对HttpServletRequest进行处理,如果想给浏览器返回数据,则直接往HttpServletResponse对象中写数据即可。

HttpServletRequest

HttpServletRequest中主要封装着Http请求对象头、请求方法等。

通过HttpServletRequest,我们可以获取到客户端给服务器发的请求里边的所有内容,注意,是所有内容。比如,浏览器自身的信息、客户端的ip地址、请求是从哪个url跳转而来和请求中附带着的cookie等信息。

下面列出能从HttpServletRequest中获取的信息

Method 信息
getRequestURL() 获取请求的URL
getRequestURI() 获取请求的资源
getSession() 获取请求中附带的session
getCookies() 获取请求中附带的cookie
getMethod() 获取请求的方法类型,一般是Get或Post
getHeader() 获取请求的Header
getHeaderNames() 获取请求的Header
getRemoteAddr() 获取请求来源的地址
getRemoteHost() 获取请求来源的主机名
getRemotePost() 获取请求来源的端口
getLocalAddr() 获取webapp所在机器的ip地址
getLocalName() 获取webapp所在机器的主机名
getParameter() 获取Url中的参数的值
getContextPath() 获取上下文路径,通俗地说就是当前域名,比如www.iloveqyc.com

使用例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.iloveqyc.web.servlet;

import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/14
* Time: 下午5:43
* Usage: xxx
*/
public class TestRequsetServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");

PrintWriter writer = resp.getWriter();
writer.println("getAuthType: " + req.getAuthType());
writer.println("getContextPath: " + req.getContextPath());
writer.println("getCookies: " + req.getCookies().toString());
writer.println("getHeaderNames: " + req.getHeaderNames());
Enumeration<String> headers = req.getHeaderNames();
writer.println("头部信息如下:");
while (headers.hasMoreElements()) {
String header = headers.nextElement();
writer.println(header + " : " + req.getHeader(header));
}
writer.println("以上是头部信息");
writer.println("getMethod: " + req.getMethod());
writer.println("getPathInfo: " + req.getPathInfo());
writer.println("getServletPath: " + req.getServletPath());
writer.println("getRequestedSessionId: " + req.getRequestedSessionId());
writer.println("getCharacterEncoding: " + req.getCharacterEncoding());
}
}

运行结果如下:

借助HttpServletRequest实现转发

一个可爱的请求打到服务器上,被Servlet接收到,该Servlet心力交瘁,不想处理,想把请求转给另一个Servlet去处理,这种情况下,可以从HttpServletRequest中获取ResquestDispatcher对象,转发该请求,示例如下:

1
req.getRequestDispatcher("/").forward(req,  resp);

HttpServletResponse

HttpServletResponse中封装着给浏览器发回的『响应Data+响应Header+响应StatusCode』。

注,HttpResponse状态码有以下一些分类:

  • 2xx:成功
    200:请求已成功,数据已经返回
    202:请求被Accepted,但并未开始处理,且该请求有可能被处理也有可能不被处理(用于异步)
  • 3xx:重定向
    302:让浏览器重定向到某个url
    304:Not Modified,缓存未过期
  • 4xx:客户端出错
    400:Bad Request,请求包含了错误语法,服务器无法识别
    403:Forbidden,服务器禁止执行该请求
    404:请求所希望的资源不在服务器上
    408:Request Timeout,请求超时
    5xx:服务端出错
    500:Internal Server Error,服务器内部出错
    502:Bad Gateway,网关服务器收到服务器的无效响应
    504:Gateway Timeout,网关服务器收服务器的响应超时
    509:服务器带宽被打满
给浏览器返回数据

当客户端传来请求,我们要给客户端返回一些数据时,有两种选择,一个是使用OutputStream,另一个是使用Writer,使用前者需要将字符转换为数组,略微麻烦,所以本吃瓜群众推荐使用Writer。

使用Writer时,需要先设置response的返回格式为UTF-8,然后再从response中获取Writer,这样才能返回中文字符,比如下面这段代码:

1
2
3
4
5
6
7
8
// 设置response的返回格式为UTF-8
response.setCharacterEncoding("UTF-8");
// 从response中获取Writer
PrintWriter out = response.getWriter();
// 控制浏览器输出UTF-8格式的字符串
out.write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'/>");
// 给浏览器传回『邱永臣可爱的邱永臣』
out.write("最可爱的邱永臣");

值得注意的是,浏览器接收到的返回里,只有『最可爱的邱永臣』几个字,而没有其它的HTML标签。这可以启示我们什么?

我们可以使用JSON框架,将对象序列化成JSON,传给请求者,实现了restful框架中的一部分内容:传回JSON。

让浏览器下载文件

仔细想一想,我们平时用浏览器下载文件是怎么操作的?
是不是,在地址栏里敲完文件的url,点”回车”后浏览器自动开始下载文件?
这是怎么做到的呢?

其实原理很简单,我们平时访问网页时,浏览也也会将网页下载下来,只不过浏览器不会将网页保存起来,而是渲染之后,显示在屏幕上,如果我们想让浏览器下载文件,需要在response中告诉浏览器说:『不要渲染这个文件,要保存它』,这样做之后,浏览器可以知道,不显示文件,而是下载放在本地即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
String filePath = this.getServletContext().getRealPath("test.download");
String fileName = filePath.substring(filePath.indexOf("\\") + 1);
resp.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));

InputStream inputStream = new FileInputStream(filePath);
OutputStream outputStream = resp.getOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);;
}

inputStream.close();
让浏览器重定向

这个也简单,让浏览器,无非是服务器给浏览器发送指令说:『你滴,给我重定向到另一个地址去』,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.iloveqyc.web.servlet;

import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/13
* Time: 下午6:27
* Usage: xxx
*/
@Slf4j
public class TestRedirectServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(HttpServletResponse.SC_FOUND);
resp.setHeader("Location", "/");

log.info("redirect now.");
}
}

值得注意的是,这种重定向,只能在同一个webapp内跳转。

重定向VS转发

  • 重定向,面向浏览器,服务器在响应的response里添加上一些内容,浏览器得到response后,解析响应结果,自动转向另外一个url,看起来就像是服务器指示着浏览器的方向,服务器告诉浏览器说:『不要来我这里,我告诉你正确的方向,对,就是我告诉你的那个方向,去吧』。在浏览器的地址栏里,可以看到URL自动改变了。
  • 转发,面向服务器,服务器收到请求后,在客户端浏览器不知情的情况下,默默第去请求另外一个URL的内容,将另外一个URL的结果包装一下,返回给客户端浏览器。在浏览器看来,它以为内容来自于它请求的URL,其实是另一个URL的结果,浏览器被服务器欺骗,在地址栏里看不到URL的改变。

HttpServletResponse总结

我们使用Writer或OutputStream给HttpServletResponse写入的信息,会被Servlet容器捕捉,和响应头(Header)放在一起,传回给客户端浏览器。


JSP

JSP简介

现在(2017-03-15),互联网公司流行前后端分离,意思是『前端童鞋专注于自己的页面编写和调整,后端童鞋专注于自己的业务逻辑,前端调用后端的Ajax接口,获取JSON数据来展示』。但是,前后端分离的一个前提是『您有足够的前端人力』,如果没有人写前端,那就谈不上前后端分离了。一个小团队创立之初,不会有充足的前端开发,也就是俗称的FE,所有的页面均需后端人员编写。

我们都知道访问网站的原理是:

用户使用浏览器访问URL,请求直接发给服务器,服务器传回HTML文件,供浏览器显示。

在没有实现前后端分离之前,网页由的后端开发编写而来,问题是:Java开发童鞋他们只愿意写Java代码,不愿意写纯粹的Html代码,服务器里没有已.html作为后缀的文件,怎么给浏览器传回html文件呢?

摆在眼前的问题有两个解决思路:
1.强制后端开发写html文件
2.后端开发继续编写java代码,在java代码里”手动”输出html代码,比如 System.out.println(“

这是一个div
”)。

具有强迫症的后端开发自然不愿意写纯粹的html,那样的话,他们就成前端了,所以吃瓜群众们选择了第二个思路。于是,在很长一段时间里,你可以看到在一个Servlet中,System.out.println语句打印了所有的html代码。

久而久之,吃瓜群众又不满了,在Servlet里面写了大量重复的html代码,html代码全靠java来输出,看着不好看,烦,不爽。

Sun公司跳起巫师舞,看到隔壁家巨硬推出的ASP,灵机一动,『隆重』推出了『动态Web开发技术』JSP。后面你会看到,JSP其实是Sun公司在Servlet的基础上做了封装,将Servlet又炒了一遍。

JSP出现后,吃瓜群众不再需要在Servlet里面用System.out.println输出html,可以将html写在一种后缀名为jsp的文件里,另外,在jsp文件里面可以写java代码。

江湖经验少的吃瓜群众一看,咦,在同一个文件里面,既可以写html语句,也可以写java语句?伟大的SUN公司不去制造生化病毒,反而实现了2种语言的融合麽,好厉害。
可是,让稍微厉害一点的人一瞅,就能明白JSP的本质:伪装了一层的Servlet。

JSP编写示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page language="java" pageEncoding="UTF-8" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<body>
<%
out.print("From Jsp");
%>
<h2>Hello World!</h2>
</body>
</html>

运行结果:

JSP原理

如果吃瓜群众用的是Mac+IntelijIDEA,可以进入

1
/Users/qiuyongchen/Library/Caches/IntelliJIdea2016.3/tomcat/项目名/work/Catalina/localhost/_/org/apache/jsp

看到两个文件,分别是 index_jsp.class 和 index_jsp.java,打开index_jsp.java,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/*
* Generated by the Jasper component of Apache Tomcat
* Version: Apache Tomcat/7.0.37
* Generated at: 2017-03-16 03:50:49 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');

String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";

out.write("\n");
out.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n");
out.write("<html>\n");
out.write("<body>\n");

out.print("From Jsp");

out.write("\n");
out.write("<h2>Hello World!</h2>\n");
out.write("</body>\n");
out.write("</html>\n");
} 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);
}
}
}

看到了么,我们编写的index.jsp被翻译成了index_jsp.java,它从一个jsp文件变成了一个java类,类里面一行一行地打印出了jsp的html内容,遇到java代码则直接引用,比如上面代码里面那行

1
out.print("From Jsp");

原封不动地从jsp文件复制到了java文件。

翻译出来的java类继承了org.apache.jasper.runtime.HttpJspBase,打开目录

1
/Users/qiuyongchen/Downloads/apache-tomcat-7.0.75-src/java/org/apache/jasper/runtime

找到HttpJspBase,看它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.jasper.runtime;

import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.HttpJspPage;

import org.apache.jasper.compiler.Localizer;

/**
* This is the super class of all JSP-generated servlets.
*
* @author Anil K. Vijendran
*/
public abstract class HttpJspBase extends HttpServlet implements HttpJspPage {

private static final long serialVersionUID = 1L;

protected HttpJspBase() {
}

@Override
public final void init(ServletConfig config)
throws ServletException
{
super.init(config);
jspInit();
_jspInit();
}

@Override
public String getServletInfo() {
return Localizer.getMessage("jsp.engine.info");
}

@Override
public final void destroy() {
jspDestroy();
_jspDestroy();
}

/**
* Entry point into service.
*/
@Override
public final void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
_jspService(request, response);
}

@Override
public void jspInit() {
}

public void _jspInit() {
}

@Override
public void jspDestroy() {
}

protected void _jspDestroy() {
}

@Override
public abstract void _jspService(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException;
}

可以发现,HttpJspBase是一个Servlet,拥有标准的Servlet三方法:init()/destrory()/service()。
我们写的JSP文件被翻译成HttpJspBase,在service()方法里输出html代码和运行java代码,每次用户访问jsp文件,实际上是在执行Servlet的service()方法,所以JSP也是Servlet。

到这里,我们就可以得到结论:

JSP文件会被翻译成Servlet,作为一个Servlet运行,JSP是一种特殊的Servlet。

JSP的service()方法

仔细看一下JSP被翻译后的Servlet,它的_jspService()方法里自带了8个对象,如下:

1
2
3
4
5
6
7
8
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;

我们可以在JSP文件里直接使用这八个字段,借助这八个对象,在JSP文件里很方便地获取到Web的一些情况,比如我们可以在JSP文件里这么写:

1
2
3
<%
session.setAttribute("name", "这是一个session"); out.print(session.getAttribute("name")+"<br/>");
%>

pageContext对象

该对象是八个对象中最重要的一个,封装了其它对象的引用,是JSP开发中的重中之重。

域容器

pageContext作为域容器,可以保存内容,比如

  • setAttribute
  • getAttribute
  • findAttribute

注:我们在使用pageContext.findAttribute方法时,JSP引擎会先在pageContext域查找,再在HttpServletRequest域查找,再在HttpSession域查找,最后在ServletApplication域查找属性。

转发

pageContext里含有forward()方法,可以让我们做请求转发。

out对象

out对象和我们在Servlet里获取到的PrintWriter非常相似,你可以把out当做PrintWriter对象来使用。
实际上,我们向out对象写入内容,写满缓冲或者是jsp页面结束时,out对象会再调用PrintWriter向response写入内容。

JSP VS Servlet

虽然JSP是一种特殊的Servlet,但两者的用途不同,因为JSP更偏重于HTML页面布局,在JSP写大量JAVA代码是不现实的,Servlet更偏重于业务逻辑,在Servlet嵌套大量的HTML的代码也是不现实的。所以人们习惯上用Servlet写业务,用JSP写页面,Servlet生成数据让JSP展示出来。

第一次访问JSP时,JSP会被翻译成JAVA,再编译成class执行,耗时较长。往后的每一次访问都无需再翻译/编译JSP,速度会很快,所以JSP有一个缺点:第一次被访问时响应比较慢。

JSP语法

基础差一点的吃瓜群众可以阅读下面这一段语法教程

JSP表达式

使用以下格式:

1
<%= 表达式 %>

翻译成JAVA时,翻译器看到<%=开头的标签,会封装到out.print()语句中,比如:

1
out.print(表达式);

JSP片段

使用一下格式:

1
2
3
4
5
<%
java代码;
java代码;
java代码;
%>

翻译时,片段中的Java代码会直接放入_jspService()方法中。如果在片段中定义变量,每次访问JSP时,都会重新定义变量,相当于局部变量,比如在片段中定义:

1
2
3
4
5
<%
int i = 0;
i++;
out.print(i);
%>

每一次的访问,都只会打印一个”1”。

JSP声明

使用以下格式:

1
2
3
4
5
<%!
java代码;
java代码;
java代码;
%>

翻译时,声明中的Java代码会直接放入_jspService()方法外,而不是_jspService()中。在声明中定义的变量,是全局变量,比如:

1
2
3
4
5
6
7
<%!
int i = 0;
%>
<%
i = i + 1;
out.print(i);
%>

访问的次数越多,打印的数字越大。
上面的JSP被翻译成Java后,大致会是以下的模样:

1
2
3
4
5
6
7
8
class Index_Jsp extends HttpJspBase {
int i = 0;

void _jspService() {
i = i + 1;
out.print(i);
}
}

我们第一次访问时,初始化Servlet,初始化i,调用_jspService()方法,打印1,往后的每次访问,不再初始化Servlet,而是直接调用_jspService()方法。

JSP注释

使用以下格式:

1
<%-- 这是注释 —%>

JSP指令

顾名思义,JSP指令会指示JSP翻译引擎,如何翻译JSP文件。
主要有3种指令:page/include/taglib
使用以下格式:

1
<%@ 指令 属性="属性值" %>

如果一个指令有多个属性,可以把多个属性写在一起,比如:

1
<%@ 指令 属性1="属性1值" 属性2="属性2值" %>

page指令

该指令用来指定jsp页面的属性,比如页面的language,页面引入的java包等,大约有十多个属性

属性名 取值 说明 备注
language java 定义JSP页面所用的脚本语言,默认是Java
autoFlush 控制out对象的 缓存区
buffer 指定out对象使用缓冲区的大小
contentType 指定当前JSP页面的MIME类型和字符编码
errorPage 指定当JSP页面发生异常时需要转向的错误处理页面
isErrorPage 指定当前页面是否可以作为另一个JSP页面的错误处理页面
extends 指定servlet从哪一个类继承 如果不做改动,默认是HttpJspBase
import 导入要使用的Java类 比较常用
info JSP页面的描述信息
isThreadSafe 指定对JSP页面的访问是否为线程安全 如果指定为true,那么每次访问JSP页面,都创建一个新的Servlet对象,速度较慢
session 是否使用session
isELIgnored 是否执行EL表达式
isScriptingEnabled 脚本元素能否被使用
inclue指令

该指令会原封不动地把别的文件内容引入JSP文件中,注意,是原封不动。

taglib指令

该指令用于引入标签库,至于标签库有什么用,后面详细说明。

JSP属性范围

在JSP中,有多种域容器让我们存放属性,比如

pageContext.setAttribute
这种属性,在转发(比如使用pageContext.forward)后失效。

request.setAttribute
多次转发有效,另开请求,属性失效。

session.setAttribute
只要某个用户没关浏览器,属性就一直存在。

servletContext.setAttribute
只要WebApp还在运行,属性就一直在。

JSP标签

JSP标签存在的意义,是为了减少在JSP页面中的代码量,可以简单地理解:一个JSP标签可以顶多行JAVA代码。
主要有include/forward/param三个标签,可以使用以下格式:

1
<jsp:标签 属性="属性值" />

include标签

include标签和include指令的差别在于:include指令原封不动地复制目标源码,如果目标文件重复定义了变量就会报错,include标签稍微智能,目标文件重复定义变量也没有问题。

forward标签

使用格式:

1
<jsp:forward page="relativeURL | <%=expression%>" />

可以将请求转发

param标签

主要是配合include标签和forward标签,比如,使用include标签引入目标JSP文件时,可以用以下方法给目标JSP传参数:

1
2
3
<jsp:include page="dest.jsp">
<jsp:param name="key" value="value" />
</jsp:include>

目标JSP可以使用下面的方法获取到参数:

1
<%=request.getParameter("key")%>

useBean标签

语法:

1
<jsp:useBean id="beanName" class="package.subPackage.class" scope="page|request|session|application"/>

实例化一个JavaBean。

其原理是先尝试在pageContext中获取到目标Bean,如果没有,再实例化一个。

SetProperty / GetProperty标签

语法:

1
<jsp:setProperty name="beanName" property="Bean属性名" value="属性值"/>

设置bean的属性。

1
<jsp:getProperty name="beanName" property="Bean属性名" value="属性值"/>

获取bean的属性。

自定义标签

自定义标签最主要的目的是移除JSP文件中的JAVA代码。

尝试自定义标签

1.编写标签处理器类(实现Tag接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.iloveqyc.web.tag;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.Tag;
import java.io.IOException;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/17
* Time: 上午12:20
* Usage: xxx
*/
public class qycTag implements Tag {
private PageContext pageContext;

public void setPageContext(PageContext pageContext) {
this.pageContext = pageContext;
}

public void setParent(Tag tag) {
}

public Tag getParent() {
return null;
}

public int doStartTag() throws JspException {
JspWriter out = pageContext.getOut();
try {
out.write("邱永臣自定义的标签");
} catch (IOException e) {
e.printStackTrace();
}
return 0;
}

public int doEndTag() throws JspException {
return 0;
}

public void release() {
}
}

2.在WEB-INF目录下建立tld(tag library description,标签库描述)文件,注册标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8" ?>

<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">

<description>我的标签库</description>
<tlib-version>0.1</tlib-version>
<short-name>tagShort</short-name>

<!-- 在JSP里这样引用标签库:<%@taglib uri="/qyc" prefix="qyc" %> -->
<uri>/qyc</uri>

<!--下面是一个标签,在一个自定义标签库里,可以有多个自定义标签-->
<tag>
<description>我写的标签</description>
<name>mytag</name>
<tag-class>com.iloveqyc.web.tag.qycTag</tag-class>
<body-content>empty</body-content>
</tag>

</taglib>

3.在JSP中引入标签库,并使用标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page language="java" pageEncoding="UTF-8" %>

<%--使用taglib指令引入标签库/qyc,并使用qyc作为前缀--%>
<%@ taglib prefix="qyc" uri="/qyc" %>

<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<body>

<%--使用qyc标签库里的mytag标签--%>
<qyc:mytag/>

<h2>Hello World!</h2>
</body>
</html>

运行结果是浏览器中显示:『邱永臣自定义的标签』。

可见,我们可以将很长的一段Java代码放进一个自定义标签里,在JSP中只需引用一个自定义标签,即可省略大段的Java代码。

自定义标签原理

在翻译JSP文件的过程中,如果JSP引擎遇到了自定义标签,会在翻译出来的Servlet的service()中,依次调用自定义标签的doStartTag()/doEndTag()等方法。

如何辨别是哪个自定义标签?这得靠tld文件的存在。
因为在JSP文件里,我们已经使用taglib指令指出了标签库的存在,而标签库里有某个自定义标签的class文件信息,JSP引擎也就能顺利地找到自定义标签了。

传统标签VS简单标签

是附带流程控制的自定义标签,因JSP有被废弃的节奏,此处略过。

JSTL(Java Standard Tag Library)

Java标准标签库,里边包含了大量的标签,可以帮我们省略很多代码,而不用自己去定义标签,省去造轮子的步骤。
JSTL分为几种:核心标签、国际化标签、SQL标签、XML标签、EL(Expression Language)标签,此处只列使用最多的核心标签。

引入核心标签

1
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

表达式控制标签

有out标签(输出字符串和表达式)、
set标签(设置变量的属性或保存变量到域)、
remove标签(移出变量)、
catch标签。

流程控制标签

有if标签,格式是:

1
<c:if test="testCondition" var="varName" [scope="{page|request|session|application}"]/>


choose标签、when标签、otherwise标签,这三个标签用在一起,比如:

1
2
3
4
5
6
7
8
    <c:choose>
<c:when test="条件1">

<c:when>
<c:otherwise>
      
   </c:otherwise>
  </c:choose>

循环标签

有forEach标签、forTokens标签。

URL操作标签

有import标签、url标签、redirect标签、param标签。


JavaWeb开发模式

JSP+JavaBean开发模式

该模式的特点是不借助Servlet,使用JSP来控制逻辑、表现逻辑,JavaBean负责封装业务数据。
如下图:

这种模式的缺点是,没法承载复杂的业务逻辑。JSP既要负责处理用户请求,也要负责显示数据,JavaBean负责封装业务数据,业务逻辑太复杂的话,JavaBean会变得极其臃肿。

所以吃瓜群众们想出了另一种开发模式,这种模式,需要Servlet的参与。

Servlet+JSP+JavaBean开发模式

这种模式,也就相当于WebMVC模型,Servlet充当Controller,JSP相当于View,JavaBean做Model。

WebMVC模型

在这种模型下,Model负责封装数据,所以Model里既有『属性』,也有『行为』,说白了,就是业务数据和业务逻辑;View负责显示Model封装好的数据,并展示给用户;Controller则当调度员,收到用户的请求后,调度Model封装业务数据,收到Model返回的数据再传给View,让View渲染,并给用户返回页面,如下图所示:

JavaWeb里的WebMVC

前面说过,Servlet=Controller,JSP=View,JavaBean=Model,对应的示意图如下:

用户发来的所有请求都打在Servlet上,Servlet调用JavaBean来处理业务,再将JavaBean返回的结果传给JSP渲染,最后JSP负责给用户返回页面数据。

Servlet作为控制器

作为逻辑控制器,Servlet需要根据不同的参数来调用不同的方法,如果方法多了,Servlet的控制逻辑会变得复杂。
所以现代的WebMVC框架,比如Struts,就能做到『动态方法调用』,只需设置目标参数和不同参数值对应的方法,在请求塞入不同的参数,就能自动调用不同的方法。

下一步,在委托模型处理业务时,Servlet没法自动封装『请求』为『模型』。

再下一步,收到模型返回的数据,选择JSP时,Servlet依赖于自身的API,比如:

1
request.getDispatcher("xxx.jsp").forward(rep, resp);

最后一步,给JSP传输数据时,Servlet也依赖自身的API,比如:

1
2
request.setAttribute("modelValue", "value");
request.getDispatcher("xxx.jsp").forward(rep, resp);

JavaBean作为模型

JavaBean作为模型,既要封装数据,也要进行业务逻辑处理,一个Bean太大了。

为了解决JavaBean过于臃肿的问题,吃瓜群众又想到了分层,即所谓的『三层架构』。在一个项目里,分表现层、业务逻辑层和持久层三层,另外有domain模型一直伴随着这三层,JavaBean就是由其中的业务逻辑层、持久层和domain模型组成,如下图所示:

Domain模型专门用于封装数据,表现层JSP展示它的内容,业务层利用它封装数据,持久层把它落地到数据库。业务层和持久层则被用于处理业务逻辑。


Cookie系咩遭仔?

Cookie是一种保存在浏览器上的信息。用户访问一个网站时,浏览器可以为该网站保存一个Cookie,每次访问该网站时,都可以带上Cookie,这样,网站就可以知道:『之前那个用户又来访问啦~快跑啊』

Cookie属性

MaxAge

如果服务器没有调用Cookie的setMaxAge,那么用户关闭浏览器后,Cookie就自动失效了。
反之,如果设置了MaxAge,Cookie就可以保存一段时间,在硬盘中存活着,比如半个小时、一天或一年等。
注:MaxAge的单位为秒

Path

吃瓜群众可以设置一个Cookie的生效路径,比如可以设置Path为/User/Info,那么用户在访问/User/Info时浏览器才带上Cookie
(注意,在访问/User时浏览器不会带上Cookie。Cookie仅适用指定的目录,不适用超出指定的目录,要不然用户访问/时也带上Cookie,这违反了设置路径的本意)

Cookie使用

让浏览器保存Cookie

浏览器是种懒惰的动物,它不会主动为吃瓜群众保存Cookie,所以,吃瓜群众必须在response手动生成Cookie,返回给浏览器,浏览器才会保存,比如:

1
resp.addCookie(new Cookie("qiuyongchen", "handsome man"));

运行后,吃瓜群众会发现浏览器里保存了一个名为qiuyongchen,值为handsome man的Cookie。

Cookie限制

一般浏览器不能保存无限条Cookie,是有数量限制的,最多数百条,少的只有几十条,大小也有限制,4KB。

Session

Session介绍

Session系咩遭仔?

类似于Cookie,Session也是用来识别某个特定用户的技术。与Cookie不同的是,Session不保存在浏览器上,而是保存在服务器上。每次有一个新用户访问时,服务器为新用户生成一条Session,在服务器上保存着,同时也保存到浏览器的Cookie上,用户下一次来访问时带上Cookie里的Session号码,服务器拿到用户的Session,再和服务器内部保存的Session一对比,如果有记录,说明该用户之前已经访问过网站,不是小鲜肉了,示意图如下:

Session使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.iloveqyc.web.servlet;

import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/14
* Time: 下午7:17
* Usage: xxx
*/
public class TestCookieSessionServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.addCookie(new Cookie("isQiuyongchenHandsome", "yes"));
resp.setCharacterEncoding("UTF-8");
PrintWriter writer = resp.getWriter();

HttpSession session = req.getSession();
if (session.isNew()) {
writer.println("session.isNew() : " + session.getId());
} else {
writer.println("session.isNotNew() : " + session.getId());
}

writer.close();
}
}

第一次访问的时候,从HttpServletRequest中获取Session,因为浏览器第一次访问没有Session,服务器获取不到Session,所以显示”session.isNew()”。
往后的访问,浏览器携带着cookie,服务器就能从HttpServletRequest中获取到Session了,让浏览器显示”session.isNotNew()“。

用Session保存信息

在用户打开浏览器后,直到用户关闭浏览器,这段时间叫一个会话,Session的生命周期是一个会话。

一般一个浏览器独占一个Sessio对象,服务器可以把用户想要保存的数据保存到浏览器独占的Session对象中,比如:

1
2
session.setAttribute("myName", "qiuyongchen");
session.getAttribute("myName");

借助Session,我们可以在用户的一次会话中,为用户保存一些信息,比如该用户的识别信息。

维持Session-Url重写

上面的Session依赖于浏览器的cookie,如果浏览器禁用了cookie功能,我们可以使用Url重写功能继续使用Session。

Url重写是Servlet自带的功能,使用方法是把要重写的url塞给HttpServeltResponse.encodeURL()方法,如果浏览器禁用了cookie,那么该方法就自动给url补上sessionId,如果浏览器没有禁用cookie,该方法就不做变动。

HttpServeltResponse.encodeURL()的注释如下:

1
2
3
4
5
6
7
8
/**
* Encodes the specified URL by including the session ID,
* or, if encoding is not needed, returns the URL unchanged.
* The implementation of this method includes the logic to
* determine whether the session ID needs to be encoded in the URL.
* For example, if the browser supports cookies, or session
* tracking is turned off, URL encoding is unnecessary.
*/

Url重写示例如下:

1
2
3
4
HttpSession session = req.getSession();
writer.println("下面的URL理应附带sessionId");
String url = req.getContextPath() + "/user?name=";
url = resp.encodeURL(url);

使用Session防止表单重复提交

(前后端分离后,防止表单重复提交,属于前端的任务范畴)

为了消除用户不断地点击提交按钮的影响,我们需要识别出用户的『第一次提交』和『非第一次提交』,Session可以帮我们做到这一点。

步骤:
a.用户请求表单时,服务器生成Token,把Token保存在session中,并把Token隐藏在表单上回传给浏览器。
b.用户提交表单时,浏览器附带上隐藏在表单里的Token,服务器判断Token与服务器上session保存的Token是否相同,如果相同说明是第一次提交,处理完后服务器删除session里的Token,如果不同,说明非第一次提交。

原理:
服务器为浏览器生成的Token,在用户第一次提交表单后被删除,往后的提交没有Token,提交无效。

生成简单Token的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.iloveqyc.service.Utils;

import sun.misc.BASE64Encoder;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/15
* Time: 上午11:31
* Usage: xxx
*/
public class TokenUtils {
public static String makeToken() {
String randStr = System.currentTimeMillis() + new Random().nextInt(Integer.MAX_VALUE) + "";
try {
// MD5编码
MessageDigest messageDigest = MessageDigest.getInstance("md5");
byte[] md5 = messageDigest.digest(randStr.getBytes());
// BASE64编码
BASE64Encoder base64Encoder = new BASE64Encoder();
return base64Encoder.encode(md5);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException();
}
}
}

MD5,将任何长度数据编码为128位二进制数据,维基百科有介绍:

MD5消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由罗纳德·李维斯特设计,于1992年公开,用以替换MD4算法。这套算法的程序在 RFC 1321 中被加以规范。将数据(如一段文字)运算变为另一固定长度值,是散列算法的基础原理。

关于BASE64,用64个字符(大小写字母+数字+加号+斜杠,等号做后缀)表示二进制数据,维基百科有介绍:

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节可表示4个可打印字符。它可用来作为电子邮件的传输编码。在Base64中的可打印字符包括字母A-Z、a-z、数字0-9,这样共有62个字符,此外两个可打印符号在不同的系统中而不同。

Session的消亡

Session在浏览器第一次访问时创建,默认生命有效期为30分钟,如果30分钟内浏览器不再发出新请求,服务器删除浏览器独占的session。哪怕浏览器并没有关闭没有删除cookie,超过30分钟再请求,服务器上也不再有session了。

我们可以在web.xml中配置Session的生命时长,如下:

1
2
3
4
<!--Session生命时长,单位是分钟-->
<session-config>
<session-timeout>60</session-timeout>
</session-config>


日志框架

日志门面框架

『门面框架』只是一种规范,引入门面框架后还得引入具体的『实现框架』,门面框架主要有Slf4j。

Slf4j

Slf4j全称为Simple Logging Facade for Java,也就是所谓的『Java的简单日志门面』,facade表明它只是一个标准(只是一个接口),用来打日志的接口,你想要直接使用是不可能的,必须搭配上具体的实现。
所以说Slf4j并不具备打日志的功能,它只是告诉那些『有能力打日志的框架』打日志的准则是什么,让『有能力打日志的框架』来实现它定下的接口。

这样做有什么好处?

这样做的话,所有『有能力打日志的框架』的框架都可以按照Slf4j定下的规范进行日志输出,吃瓜群众只需默认写Slf4j格式的日志,而无需管底层到底是用log4j1还是log4j2还是logback,只需一次性编写Slf4j输出日志的代码,以后就可以肆意切换底层实现而不用更改代码,比如从log4j1升级到log4j2,只需要替换jar包即可。

Slf4j框架的主要jar包的包名如下:

  • slf4j-api
    要使用Slf4j,直接引入以上jar即可(既可直接引入jar包,也可通过Maven/Gradle引入)。

日志实现框架

Java语言中,日志实现框架有许多,比如log4j1、log4j2、logback、commons-logging等等。

各个框架的主要jar包的包名如下:

  • log4j1

    log4j:log4j1的全部内容

  • log4j2
    log4j-api:log4j2自身定义的API
    log4j-core:log4j2自身定义的API的实现

  • logback
    logback-core:logback的核心包

  • commons-logging
    commons-logging:commons-logging的全部内容

你要用哪个实现框架,就引入对应的包即可。

Log4j

从门面框架到实现框架 = 桥梁框架

由于门面框架和实现框架是分离的,要让它们能联合起来工作,必须额外使用”粘合剂”,也就是所谓的桥梁框架。虽然门面框架定下了日志的代码格式,但某些实现框架可能有自己定义的代码格式,这时就必须使用桥梁框架在它们中间做个协商,做个转换。

Slf4j vs Log4j2实现框架

有同学要使用Slf4j作为门面框架,使用Log4j2作为实现框架,可使用以下桥梁框架,引入其包即可。

  • log4j-slf4j-impl

Slf4j vs 其它实现框架

如果不想使用Log4j2,想用其它的实现框架,该使用哪些桥梁框架?

  • log4j1实现框架
    slf4j-log4j12

  • logback实现框架
    logback-classic

  • commons-logging实现框架
    slf4j-jcl

从实现框架到门面框架 = 逆桥梁框架

大部分情况下,我们都是使用Slf4j的API进行编码,底层再用具体的实现框架去输出。但如果,我们的系统在一开始时,没有使用Slf4j,没有使用桥梁框架,直接使用了实现框架的API进行编码(比如Log4j1),系统迭代到某个规模后,我们想从Log4j1迁移到Log4j2,又不想改变系统代码,仍旧保留Log4j1的API使用方式,在对RD透明的情况下做迁移,这时该怎么办呢?

这时就可以使用所谓的『逆桥梁框架』(本应该也叫桥梁框架,本吃瓜群众为了形象点,私下起名为逆桥梁框架)。

我们可以借助逆桥梁框架,将Log4j1的API调用Slf4j的API,玩个接龙游戏,让Slf4j的API调用Log4j2的实现,从而输出日志,日志架构变换图如下:

经过这种架构改变,RD们仍旧使用log4j1的API进行编程,但底下的实现框架已经从log4j1变成了log4j2。

那么都有哪些逆桥梁框架,它们的jar包又有哪些?

  • log4j1到slf4j
    log4j-over-slf4j

  • commons-logging到slf4j
    jcl-over-slf4j

新项目接入Log4j & Slf4j

在一个完全没接入过日志框架的项目里,接入Log4j2,仅需以下步骤:

1.添加相应的pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    <properties>
<log4j.version>2.8.1</log4j.version>
<slf4j.version>1.7.24</slf4j.version>
</properties>

<dependency>
<!--LomBok包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>

<!--slf4j + log4j2日志框架 begin-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<!--slf4j + log4j2日志框架 end-->
</dependency>

2.在项目『src.main.resource』目录,添加log4j2.xml文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>

<Configuration status="warn">

<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level [%c] - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console"/>
</Root>
</Loggers>

</Configuration>

3.给类打上@Slf4j注解,即可使用日志:

1
log.info("这是一条测试日志");


MVC

Struts2

SpringMVC

Spring

Spring BOOt

设计模式

Redis

MemCache

MongoDB

JDBC

JDBC, Java DataBase Connectivity,是一套用来规范数据库访问方式的接口,由数据库厂商实现,吃瓜群众调用一套接口,不需管底层的访问实现,维基百科有言,

Java数据库连接,(Java Database Connectivity,简称JDBC)是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。

JDBC使用示例

如果不借助第三方框架,我们要自行编写JDBC的代码去操作数据库,比如下面的代码,连接数据库后,使用Statement对象执行查询操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.iloveqyc.service.SQL;

import java.sql.*;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/17
* Time: 下午7:12
* Usage: xxx
*/
public class test1 {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");

// 连接数据库,并执行SQL查询
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/resWeb");
Statement statement = conn.createStatement();
ResultSet res = statement.executeQuery("SELECT * FROM Name");

// 打印结果
while (res.next()) {
System.out.println("Name : " + res.getObject("Name"));
System.out.println("Id : " + res.getObject("Id"));;
}

// 关闭资源
res.close();
statement.close();
conn.close();
}
}

在上面的示例中,我们使用到了Connection类、Statement类、ResultSet类。

Connection类

是用来和数据库建立连接的类,极其重要,我们的项目能否和数据库连接上,全靠它。
它有以下的方法:

  • Statement createStatement()
    创建一个和数据库的连接,用来执行普通sql指令

  • PreparedStatement prepareStatement(String sql)
    创建一个和数据库的连接,用来执行预编译sql指令

  • CallableStatement prepareCall(String sql)
    创建一个对象,用来执行存储过程

  • void setAutoCommit(boolean autoCommit)
    是否自动提交

  • void commit()
    提交事务

  • void rollback()
    回滚事务

  • void close()
    关闭和数据库的连接

  • Savepoint setSavepoint()
    设置存储点,可回滚至该存储点

Statement类

Connection对象和数据库保持连接,Statement对象向数据库发起指令,从而增删改查。
它有以下的方法:

  • ResultSet executeQuery(String sql)
    执行查询指令

  • int executeUpdate(String sql)
    执行更新指令

  • void close()
    关闭Statement对象

  • boolean execute(String sql)
    执行sql指令

  • int[] executeBatch()
    批量执行sql指令

  • Connection getConnection()
    获取Connection对象

ResultSet类

ResultSet对象封装Statement对象查询到的结果,内部形式是表格,我们可以用游标,也就是cursor来获取ResultSet对象内部的数据。默认情况下游标位于表格第一行之前,调用ResultSet.next()可以让游标指向第一行。
ResultSet对象内部的方法有:

  • Object getObject(String columnLabel)
    取出表格当前行的某个字段

  • boolean first()
    使游标指向第一行

  • boolean last()
    使游标指向最后一行

  • int getRow()
    返回当前是第几行

  • boolean previous()

使用Property读取db.property

一般,数据库的地址、用户和密码等信息不会硬编码写进代码,而是写在一个名为db.properties的文件上,格式如下:

1
2
3
4
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://10.1.77.106:3306/xxx
user=xxx
password=xxx

然后使用类加载器加载该文件,以Properties的格式读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.iloveqyc.service.Utils;

import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/19
* Time: 下午11:26
* Usage: xxx
*/
public class SQLUtils {

public static String driver = null;
public static String url = null;
public static String user = null;
public static String password = null;

static {
try {
InputStream inputStream = SQLUtils.class.getClassLoader().getResourceAsStream("db.properties");
Properties prop = new Properties();
prop.load(inputStream);

// 可见,你可以把Properties当做KV结构体来使用,从中取出key对应的value。
driver = prop.getProperty("driver");
url = prop.getProperty("url");
user = prop.getProperty("user");
password = prop.getProperty("password");

Class.forName(driver);
} catch (Exception e) {
e.printStackTrace();
}
}

public static String getTest() {
return driver + url + user + password;
}

public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, user, password);
}

public static void release(Connection conn, Statement st, ResultSet resultSet) {
try {
if (resultSet != null) {
resultSet.close();
resultSet = null;
}
if (st != null) {
st.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

写好这个Utils,我们就可以用它来实现CRUD了。

JBDC的CRUD

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// C
public void testInsertCreate() throws SQLException {
Connection conn = SQLUtils.getConnection();
Statement st = conn.createStatement();
int row = st.executeUpdate("INSERT INTO `OP_BackCategory` (`ID`, `Name`, `Status`, `AddTime`, `UpdateTime`)\n" +
"VALUES\n" +
"\t(12100, '邱永臣品类', 1, '2017-03-17 10:46:52', '2017-03-17 10:46:52');\n");
System.out.println("插入行数:" + row);
SQLUtils.release(conn, st, null);
}

// U
public void testUpdate() throws SQLException {
Connection conn = SQLUtils.getConnection();
Statement st = conn.createStatement();
int row = st.executeUpdate("UPDATE OP_BackCategory SET Name = '邱永臣改名' WHERE ID = 12100");
System.out.println("更改行数:" + row);
SQLUtils.release(conn, st, null);
}

// R
public void testRetrieve() throws SQLException {
Connection conn = SQLUtils.getConnection();
Statement st = conn.createStatement();
ResultSet res = st.executeQuery("SELECT * FROM OP_BackCategory");
System.out.println("数据库:");
while (res.next()) {
System.out.println(res.getObject("Name"));
}
System.out.println();
SQLUtils.release(conn, st, res);
}

// D
public void testDelet() throws SQLException {
Connection conn = SQLUtils.getConnection();
Statement st = conn.createStatement();
int row = st.executeUpdate("DELETE FROM OP_BackCategory WHERE ID = 12100");
System.out.println("删除行数:" + row);
SQLUtils.release(conn, st, null);
}

占位符版sql语句

如果像上述代码一样,将所有参数都拼凑成字符串,未免太麻烦,吃瓜群众们可以使用prepareStatement对象提交sql语句。和Statement对象相比,prepareStatement对象提交给数据库的指令是已经编译好的指令,减小数据库编译的压力。prepaerStatement还允许我们使用占位符的形式编写sql语句,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// C
public void testInsertCreate2() throws SQLException {
Connection conn = SQLUtils.getConnection();
PreparedStatement preSt = conn.prepareStatement("INSERT INTO `OP_BackCategory` (`ID`, `Name`, `Status`, `AddTime`, `UpdateTime`)" +
" VALUES (?, ?, ?, ?, ?)");
preSt.setInt(1, 12100);
preSt.setString(2, "邱永臣品类");
preSt.setInt(3, 1);
preSt.setDate(4, new Date(new java.util.Date().getTime()));
preSt.setDate(5, new Date(new java.util.Date().getTime()));
int row = preSt.executeUpdate();
System.out.println("插入行数:" + row);
SQLUtils.release(conn, preSt, null);
}

// U
public void testUpdate2() throws SQLException {
Connection conn = SQLUtils.getConnection();
PreparedStatement preSt = conn.prepareStatement("UPDATE OP_BackCategory SET Name = ? WHERE ID = ?");
preSt.setString(1, "邱永臣又改名");
preSt.setInt(2, 12100);
int row = preSt.executeUpdate();
System.out.println("更改行数:" + row);
SQLUtils.release(conn, preSt, null);
}

// R
public void testRetrieve2() throws SQLException {
Connection conn = SQLUtils.getConnection();
PreparedStatement preSt = conn.prepareStatement("SELECT * FROM OP_BackCategory WHERE ID = ?");
preSt.setInt(1, 12100);
ResultSet res = preSt.executeQuery();
while (res.next()) {
System.out.println(res.getObject("Name"));
}
SQLUtils.release(conn, preSt, res);
}

先用占位符版sql语句从connection对象中获取到prepareStatement对象,随后一个一个地替换占位符,最后执行即可。

另外,既然prepareStatement可以使用setInt的方式填充参数,也可以使用addBatch批量增加,我们就能顺便写一个批量插入,例如:

1
2
3
4
for (int i = 0; i < 100; i++) {
preSt.setInt(1, i);
preSt.addBatch();
}

JDBC事务

数据库事务是一种保证数据一致性的手段,也就是ACID(原子性Atomicity、一致性Consistency、持久性Isolation、隔离性Durability)

开启了事务后,在提交(commit)之前的所有sql更新都可以撤销掉,也就是回滚(rollback),比如:

1
2
3
4
5
6
7
8
9
开启事务()

sql更新()
sql更新()
sql更新()
sql更新()

// 事务提交前的所有'sql更新()'都可以通过回滚来撤销掉,数据库恢复到第一条'slq更新()'之前的状态
事务提交()

值得注意的是,开启了事务后,一定要提交事务,否则所做的sql更新都不生效。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void testRollback() throws SQLException {
Connection conn = SQLUtils.getConnection();
// 关闭自动提交
conn.setAutoCommit(false);
PreparedStatement preSt = conn.prepareStatement("UPDATE OP_BackCategory SET Name = ? WHERE ID = ?");
preSt.setString(1, "邱永臣第一次改名");
preSt.setInt(2, 12100);

// 第一次更改数据库
int row = preSt.executeUpdate();
System.out.println("更改行数:" + row);

PreparedStatement preSt2 = conn.prepareStatement("UPDATE OP_BackCategory SET Name = ? WHERE ID = ?");
preSt2.setString(1, "邱永臣第二次改名");
preSt2.setInt(2, 12100);

// 第二次更改数据库
row = preSt2.executeUpdate();
System.out.println("更改行数:" + row);

// 提交事务
conn.commit();

SQLUtils.release(conn, preSt, null);
}

如果在开启事务之后提交事务之前,发生了异常,那么事务无法提交,开启事务之后的所有sql更新都不会生效,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void testRollback() throws SQLException {
Connection conn = SQLUtils.getConnection();
// 关闭自动提交
conn.setAutoCommit(false);
PreparedStatement preSt = conn.prepareStatement("UPDATE OP_BackCategory SET Name = ? WHERE ID = ?");
preSt.setString(1, "邱永臣第一次改名");
preSt.setInt(2, 12100);
int row = preSt.executeUpdate();
System.out.println("更改行数:" + row);

// 人为抛出异常
int i = 0/0;

PreparedStatement preSt2 = conn.prepareStatement("UPDATE OP_BackCategory SET Name = ? WHERE ID = ?");
preSt2.setString(1, "邱永臣第二次改名");
preSt2.setInt(2, 12100);
row = preSt2.executeUpdate();
System.out.println("更改行数:" + row);

conn.commit();

SQLUtils.release(conn, preSt, null);
}

数据库的记录不会被改成:邱永臣第一次改名。

有时候我们做了一系列的sql更新操作,发生异常,我们不希望所有的更新都回滚,只回滚后来的一些sql更新,这时我们可以使用『回滚点』,指示事务仅回滚到某个地方,回滚点之前的sql更新保留,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public void testRollback() {
Connection conn = null;
Savepoint savepoint = null;
PreparedStatement preSt = null;

try {
conn = SQLUtils.getConnection();
// 关闭自动提交
conn.setAutoCommit(false);

PreparedStatement preX = conn.prepareStatement("UPDATE OP_BackCategory SET Name = ? WHERE ID = ?");
preX.setString(1, "邱永臣与创世纪");
preX.setInt(2, 12100);
preX.executeUpdate();

// 设置回滚点
savepoint = conn.setSavepoint();

preSt = conn.prepareStatement("UPDATE OP_BackCategory SET Name = ? WHERE ID = ?");
preSt.setString(1, "邱永臣第一次改名");
preSt.setInt(2, 12100);
int row = preSt.executeUpdate();
System.out.println("更改行数:" + row);

// 人为抛出异常,后面的"提交事务"无法执行
int i = 0 / 0;

PreparedStatement preSt2 = conn.prepareStatement("UPDATE OP_BackCategory SET Name = ? WHERE ID = ?");
preSt2.setString(1, "邱永臣第二次改名");
preSt2.setInt(2, 12100);
row = preSt2.executeUpdate();
System.out.println("更改行数:" + row);

// 提交事务
conn.commit();
SQLUtils.release(conn, preSt, null);
} catch (Exception e) {
System.out.println(e);
try {
// 手动回滚到sp,所以不会回滚全部的sql更新
conn.rollback(savepoint);
// 再次提交事务,此次提交的事务里,只包含事务开启后到回滚点之间的sql更新
// 如果不提交,回滚点在连接断开时才起作用,而不是实时起作用
conn.commit();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}

数据库连接池

池化技术

不管是进程管理、线程管理、内存管理,还是于数据库的连接管理,都被吃惯群众加入了池化技术。池化,简单的说,就是把所有的资源放入一起,放在池中,每次有人想要资源,不必重新申请,直接从池中获取即可。

熟悉操作系统的人都知道,在计算机里,创建一个新进程或新线程,是一件耗时耗力的事。想要一个新线程,若是从0到1创建一个,代价太大;一个线程用完,若是直接销毁而不是重新利用,则太浪费,利用率太低。综合以上两点,人们创建了线程池。在系统启动时就在池中创建一定数量的线程(最小线程数),有吃瓜群众申请线程时从池中拿取即可,用完把线程归还给线程池,要是池里的线程都被申请出去了,线程池会自动扩张,再创建一定数量的线程,直到总数量达到’最大线程数’。

数据库连接池的原理也是一样:一开始时创建最小连接数量的连接,放在连接池中,用户需要访问数据库,直接从池中抓取连接,不必耗费巨大的代价创建一个与数据库的连接(再说了,维持一个数据库连接对数据库来说,代价也很大,毕竟一个数据库能维持的连接数量是有上限的),访问完毕再将连接归还池中,若是池的大小不够,连接池会自动扩张,一直扩张到最大连接数量为止。

创建简单数据源

数据源,指的是实现了DataSource接口的类,通过该类,我们可以拿到数据库连接,一般数据源底层实现都包含连接池,所以我们创建数据源,其实就是创建连接池。

创建连接池,首先要解决Pool初始化。
初始化比较简单,在Pool初始化时,创建一定数量的Connection,并将Connection放入连接池的具体容器(比如LinkedList)。

其次,是比较复杂的一点,连接池里的Connection被申请出去,从LinkedList即可,但是Connection使用完毕,吃瓜群众调用了Connection.close()企图关闭连接,我们不能让吃瓜群众得逞,真的关掉连接,而是把Connection归还到Pool里面。也就是说,在吃瓜群众申请Connection时,我们必须用代理技术,给吃瓜群众返回一个Connection代理,调用close()方法绕过真实的close()方法,而是把Connection本身重新加入LinkedList。

创建数据源,我们需要实现DataSource接口,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package com.iloveqyc.service.SQL;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.LinkedList;
import java.util.Properties;
import java.util.logging.Logger;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/22
* Time: 下午11:47
* Usage: 简单的数据源
*/
@Slf4j
public class SimpleDataSource implements DataSource {

// 连接池的具体容器
private LinkedList<Connection> pool = new LinkedList<Connection>();
public String driver = null;
public String url = null;
public String user = null;
public String password = null;
public int initPoolSize = 0;

public SimpleDataSource(Properties prop) throws ClassNotFoundException {
driver = prop.getProperty("driver");
url = prop.getProperty("url");
user = prop.getProperty("user");
password = prop.getProperty("password");
initPoolSize = Integer.valueOf(prop.getProperty("initPoolSize"));

Class.forName(driver);

for (int i = 0; i < initPoolSize; i++) {
try {
Connection conn = getNewConnection();
pool.add(conn);
log.info("init pool: create connection success, conn: {}", conn);
} catch (SQLException e) {
log.error("init pool: create new connection fail", e);
}
}
log.info("the size of pool: {}", pool.size());
}

private Connection getNewConnection() throws SQLException {
return DriverManager.getConnection(url, user, password);
}

/**
* <p>Attempts to establish a connection with the data source that
* this <code>DataSource</code> object represents.
*
* @return a connection to the data source
* @throws SQLException if a database access error occurs
*/
public Connection getConnection() throws SQLException {
if (CollectionUtils.isNotEmpty(pool)) {
final Connection conn = pool.removeLast();
log.info("will return a connection from pool, conn: {}", conn);

// 创建代理对象需要:
// 1.类加载器
// 2.对象的接口
// 3.对象调用的handle方法
return (Connection) Proxy.newProxyInstance(SimpleDataSource.class.getClassLoader(),
new Class[]{Connection.class}, new InvocationHandler() {

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果吃瓜群众调用了Connection.close()方法,则把连接归还给池
if (method.getName().equals("close")) {
pool.addFirst(conn);
log.info("connection.close() is invoked, conn: {}, the size of pool: {}",
conn, pool.size());
return null;
}
log.info("invoke other method: ({}) of conn: {}, the size fo pool: {}",
method.getName(), conn, pool.size());
return method.invoke(conn, args);
}
});
} else {
log.error("pool is empty, get new connection for you");
return getNewConnection();
}
}

public Connection getConnection(String username, String password) throws SQLException {
return null;
}
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
public PrintWriter getLogWriter() throws SQLException {
return null;
}
public void setLogWriter(PrintWriter out) throws SQLException {
}
public int getLoginTimeout() throws SQLException {
return 0;
}
public void setLoginTimeout(int seconds) throws SQLException {
}
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}

创建数据源的工厂,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.iloveqyc.service.SQL;

import com.iloveqyc.service.Utils.SQLUtils;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/23
* Time: 上午1:21
* Usage: 简单数据源的工厂类
*/
public class SimpleDataSourceFactory {

public static SimpleDataSource buildDataSource() throws IOException, ClassNotFoundException {
InputStream inputStream = SQLUtils.class.getClassLoader().getResourceAsStream("db.properties");
Properties prop = new Properties();
prop.load(inputStream);
return new SimpleDataSource(prop);
}
}

SQLUtils负责关闭连接资源,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.iloveqyc.service.Utils;

import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/19
* Time: 下午11:26
* Usage: 关闭数据库连接资源
*/
public class SQLUtils {
public static void release(Connection conn, Statement st, ResultSet resultSet) {
try {
if (resultSet != null) {
resultSet.close();
resultSet = null;
}
if (st != null) {
st.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.iloveqyc.service.SQL;

import com.iloveqyc.service.Utils.SQLUtils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

/**
* User: qiuyongchen Nicolas.David
* Date: 2017/3/23
* Time: 上午12:28
* Usage: xxx
*/
public class TestPool {

public static void main(String[] args) throws SQLException {
SimpleConnectionPool connectionPool = new SimpleConnectionPool();

// 第一个连接
Connection conn = connectionPool.getConnection();
Statement st = conn.createStatement();
ResultSet res = st.executeQuery("SELECT * FROM OP_BackCategory");
System.out.println("数据库:");
while (res.next()) {
System.out.println(res.getObject("Name"));
}
System.out.println();

// 第二个连接
Connection connection = connectionPool.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM OP_BackCategory");
while (resultSet.next()) {
System.out.println(resultSet.getObject("Name"));
}
System.out.println();

// 第三个连接
Connection connection1 = connectionPool.getConnection();
Statement statement1 = connection1.createStatement();
ResultSet resultSet1 = statement1.executeQuery("SELECT * FROM OP_BackCategory");
while (resultSet1.next()) {
System.out.println(resultSet1.getObject("Name"));
}
System.out.println();

// 第四个连接
Connection connection2 = connectionPool.getConnection();
Statement statement2 = connection2.createStatement();
ResultSet resultSet2 = statement2.executeQuery("SELECT * FROM OP_BackCategory");
while (resultSet2.next()) {
System.out.println(resultSet2.getObject("Name"));
}
System.out.println();

SQLUtils.release(conn, st, res);
SQLUtils.release(connection, statement, resultSet);
SQLUtils.release(connection1, statement1, resultSet1);
SQLUtils.release(connection2, statement2, resultSet2);
}
}

在输出日志里可以看到,当连接池被用光时,会额外创建新连接,Connection的close()方法被调用,实际上该连接没有关闭,而是回到了连接池里。

我将上述的数据源代码稍微改造了一下,放在github上,地址:

SQL结果映射为JavaBean

我们使用select语句查询,其结果是一个ResultSet,

IBatis

MyBatis

Hibernate

Nginx

Nginx使用

Nginx根据cookie转发

假设我们有两台机器,其中一台是外界访问的入口,我们希望在请求里加入cookie,如果有cookie,就将请求转发到第二台机器,否则就由第一台机器处理请求,如何设置?

在nginx的配置目录conf中找到nginx_app.conf,在里面设置一个您想要的变量(比如isAlpha):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
upstream tomcat {
server 127.0.0.1:8080 weight=1 max_fails=2;
}

upstream tomcatAlpha {
server 10.66.65.91:8080 weight=1 max_fails=2;
}

server {
listen 80;
server_name www.dianping.com;
root /data/webapps/poseidon-merhcant-web/shared/webroot;
access_log logs/poseidon-merhcant-web.access.log main;
error_log logs/poseidon-merhcant-web.error.log notice;

set $hcv "-";
if ( $http_cookie ~* "_hc.v=(\S+)(;.*|$)"){
set $hcv $1;
}
set $dper "-";
if ( $http_cookie ~* "dper=(\S+)(;.*|$)"){
set $dper $1;
}
set $isAlpha "false";
if ( $http_cookie ~* "env=alpha"){
set $isAlpha "true";
}

include nginx_forward.conf;

location ~ /favicon.ico$ {
root /data/webapps;
expires 30d;
}

location / {
if ( -f $request_filename ) {
break;
}

#proxy_set_header Cookie $http_cookie;
if ( $isAlpha = "true") {
proxy_pass http://tomcatAlpha;
break;
}
proxy_pass http://tomcat;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}

上述配置中,让nginx扫描各个请求的cookie,如果有某个cookie,其名为env,其值为alpha,其域为.51ping.com,其path为/,那么isAlpha变量的值就被设置为true。

那么这个变量怎么用呢?答案在上面的配置中倒数第二段,如果isAlpha为true,那就将请求转发到tomcatAlpha,也就是10.66.65.91。

Maven

Git

ZooKeeper

Restful

RPC

Dubbo

SOA