邱永臣叫西门追雪

喜剧演员,兼开发工程师(github.com/qiuyongchen)


  • Home

  • Archives

  • Search

对Quartz的学习和研究

Posted on 2017-05-10 | Edited on 2019-02-18
Symbols count in article: 4.2k | Reading time ≈ 7 mins.

简要

Quartz是一个用java编写的job调度框架。
支持数据库,也就是说它可以连接数据库…
支持集群部署,您可以同时在多台机器上部署一个Quartz实例,job数量少时集群部署可以提高可用性…
支持cron表达式(此条是真的)…

尝试使用Quartz

程序员言道:”show me the code”,说得就是在学习任何工具框架之前,最好先跑一跑代码,对框架有第一印象之后再学习。Quartz是个job调度器,没有使用过它的童鞋一时半会儿无法理解调度器,因为job调度是个领域,对该领域没个了解是不行的。

简单地理解,Quartz运行的时候有3部分:Job、触发器和调度器。Job是我们自己写的逻辑代码,比如说发送一封秘密邮件。触发器,配置了Job什么时候执行。调度器,是调度框架的核心,没有它,Job和触发器没有意义。总而言之,言而总之,调度器根据触发器的配置,在特定的条件下『触发』特定的动作,比如我们设定触发器在每天凌晨1点触发我们的发邮件Job,调度器会在每天1点定时启动Job。

Read more »

外观(设计模式)

Posted on 2017-05-07 | Edited on 2017-06-07
Symbols count in article: 274 | Reading time ≈ 1 mins.

Facade模式,也就是外观模式,将大量的负责代码融在一起形成子系统,为子系统提供一个外观,外部见到的只有外观,而没有子系统内部的具体实现。

举个例子,运用外观模式之前,要调用两个不同类的功能,就必须先实例化两个类,然后逐一调用它们的功能;在运行了外观模式之后,就能将两个类放在一个子系统中,加上一个外观,只需要通过外观就能一次性调用两个类的功能。

再举个例子,一般的后台系统会被划分为 web 和 server 两个项目,这就是外观模式,web 通过 server 的外观来调用 server 的所有功能,而不必知道 server 的内部实现。

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

Posted on 2017-03-02 | Edited on 2019-02-20
Symbols count in article: 73k | Reading time ≈ 2:01

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

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 & Session

Cookie

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

从0到1理解linux/shell_环境变量

Posted on 2017-02-21 | Edited on 2019-04-14
Symbols count in article: 772 | Reading time ≈ 1 mins.

我这一瞻仰shell,得两星期。

shell脚本,实质是一种脚本语言。既然是语言,学过计算机语言(如C/Java/JavaScript)的童鞋有足够的能力和义务瞬间反应过来:shell脚本必然包含变量、常量、表达式、函数、语句等。

定义变量

不同于程序员们追求的google code style,shell变量在定义时,变量名和变量值之间有且仅有一个=号,多一个空格都不行,比如下面的『谷歌式』:

1
fileName=url.log.2017-02-17

嘛意思?一个fileName,其名,不,其值为url.log.2017-02-17。

(值得注意的是,变量名和变量值之间不能有空格。)

近日有个小需求,将文件内容读取进一个变量,咋办呢?

1
longestParam=$(cat longestParam.text) + ""

使用变量

变量定义好了,怎么用呢?
用大括号囚禁变量,再在最前边添加一个美元符号即可,

1
echo "环境变量longestParam:" $longestParam

将普通变量声明为环境变量

一般的变量被定义出来,只能在该shell进程中使用,其它进程(包括子进程)不知晓变量之存在。要想一个变量拥有『全局变量』的超能力,需要使用export,

1
export longestParam

环境变量到底在哪里发挥作用呢?
有歌唱:”简单点,说话的方式简单点”。在一长段shell脚本里,开头定义了普通变量,但在其它命令里无法获取该变量的值,必须声明为环境级别的变量,为什么呢?因为定义变量时,默认只能在该脚本里访问(比如用于shell脚本的for循环),使用其它命令本质上是创建了其它进程,自然是无法获取普通变量,只能获取环境变量。

删除变量

可以使用unset指令删除普通变量和环境变量,

1
unset longestParam

Emacs Guide

Posted on 2017-02-17 | Edited on 2017-06-07
Symbols count in article: 209 | Reading time ≈ 1 mins.

2014年,一个老外将我引入Emacs的世界。随后的一年,我使用Emacs作为主要编辑器。
又过了一年,我的精力不在Linux上。当我回过头,发现自己脑海里,除了上下左右的快捷键,其余的均已遗忘。

导航

向上一行/向下一行/向前一个字符/向后一个字符

ctrl + p/n/f/b
(Linux/Win/Mac均相同)

回到行首/回到行尾

ctrl + a/e

删除光标后一个字符/删除光标后的本行所有内容

ctrl + d/k

逗号分割值(CSV)

Posted on 2017-02-17 | Edited on 2017-06-07
Symbols count in article: 430 | Reading time ≈ 1 mins.

CSV全称为comma separated value(逗号分割值),是一种数据格式,用来规定数据的存储形式,形式如:

1
2
3
id,name,phone
1,"邱永臣",1234567
2,"邱永臣",1234568

CSV可以被看做是一个简易数据库,它能存储大量的记录(一行代表一个记录),最重要的是,CSV是字符文本而不是二进制(这代表着你可以借助文本编辑器打开csv文件并查看其内容)。

CSV有且仅有以下规则:

  • DOS风格的换行,即CR/LF(最后一行可选)。
  • 每条记录的字段数量应当相等。
  • 任何字段都可以使用双引号包裹。
  • 换行符、单引号、双引号、斜杠和逗号必须用双引号包裹。

CSV几乎被所有的数据库/电子表格软件兼容。当然,除了csv之外,你也可以使用XML保存数据(如果你不嫌弃xml的冗余繁杂),你也可以使用JSON(个人认为,json更擅长保存对象,csv擅长保存记录),甚至,你还可以使用HTML…

注解这玩意

Posted on 2017-02-07 | Edited on 2017-06-07
Symbols count in article: 4.8k | Reading time ≈ 8 mins.

注解&东西

注解,是什么滴干活?注解,是一种神奇的东西,您可以在类、接口、方法、变量身前添加注解(形式是一个’@’符号后面带上注解名,有时候还可以带上类似参数一样的玩意),之后呢,注解就会发生化学/物理反应,在您的代码里乱搅和,正所谓出其不意,搞你一把,神出鬼没,用行内的黑话说,这叫『打注解』。

人说,活久见,活得久了,就什么都见过了。什么人,或者说,什么情况下会拎出注解?使用注解的人就像使用MacBook的人,要么是小白,要么是绝顶高手。

小白的人呢,最喜用@Override,您问他:这个注解是干什么的呀?他会回答:哦,Override声明了该方法是接口方法,重写方法。
(毫无疑问,邱永臣属于小白行列)

那些高手们呢,根本不屑于使用什么@Override/@Deprecated,心想:哼,吾等圣贤之才也,岂与子相同并论?然后撇开官方已有的Override/Deprecated/SuppressWarnings,自定义注解,所以,您会在各大开源框架里头见到大量注解,不要惊讶。

举Spring为例,大量的 @Service / @Autowired / @Component / @Source(与java自带的重复) / @Qualified,还有更著名的Restful @Get(“User/info”)…
一个例子不够看,再举lombok为例。不开化的同学,喜欢写很多Getter/Setter,或是自己实例化logger,已经开化的同学呢,中意安装lombok插件,填上几个 @Data / @Getter / @Setter / @Slf4j,开开心心地在一旁偷懒。
还有,切面!最常见的切面,非日志切面莫属。想一想,打一个 @GiveMeSomeLog,日志文件里凭空变魔术一样,多出运行上下文、方法参数、函数执行状态等等信息,用两个字回答我,厉不厉害?
除此之外,注解被某些童鞋用来当做校验数据的利器。比如,在某个DTO的某个字段前边,明码标价,以强硬的姿态,声明字段的合法取值范围 @INeedYouToBe(min=0, max=9),这也是极厉害的。

到这里,我们可以得到结论:
注解是一种用来声明『本注解所在之处,虽不会片甲不留,却也有神奇之事发生』的手段。

可是,注解的形式是什么?是接口,是类,是方法,还是变量?
为什么在运行环境里,注解会起作用?编译器认识注解么?JVM认识注解么?
注解在编译后,以什么形式存在?单独的class文件?
为什么注解一定要以 @ 符号开头,用英文单词qiuyongchen开头可不可以?(强迫症惧怕@)
打上注解后,为什么会发生神奇之事?(打上@Getter,谁赋予Bean一个Getter?)
注解能用在哪些地方?什么人有资格拥有注解?类?方法?对象?
注解能活到什么时候?编译后就不见了?运行时能捕捉到注解么?
…
正所谓,『People live to deal with problem, if you don’t have problems to deal with, you’re probably 』人活着,就是为了解决问题,为了搞注解,我们需要提出很多问题。

为了回答这些问题,我们需要海量的知识背景,不用急,请听我慢慢道来:那是很久很久以前….

注解&本质

注解本质上是什么呢?
无头绪时,可以从自定义注解追查线索,一般,自定义注解会用以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MethodInfo {

String author() default "xxx@gmail.com";

String date();

int version() default 1;
}

自定义MethodInfo注解,注意,MethodInfo类型不是interface,不是class,而是,奇怪的@interface。莫名其妙哦,搞个@interface出来干嘛?不像接口不像类,二不像咩。
(这里我们就要开始悲天悯人了,您瞧,Java的大师们,已经没方法了,如果注解是interface,开发者需要搞出implements,如果注解是class,开发者需要实现方法,也不能指定默认值 default “trinea@gmail.com”,没方法,新开一套规则吧,定义为@interface,毕竟看起来比较像接口)

实际上,自定义注解,@interface确实创建了一个接口,只不过这个接口有点特殊,规则特殊,用途特殊,是接口中的一朵奇葩。
更神奇的是,自定义注解时,该注解默认隐式扩展java.lang.annotation.Annotation接口。默认隐式扩展?什么意思呢?意思就是Java官方霸气地站在你面前,嚣张的说:”小子,你是开发者,扩展接口时必须使用extend,而注解是我们亲儿子,它不用使用extend也能扩展其它接口!注解自打生下来那一刻起,就拥有它老爹Annotation的一切了,不用嫉妒了,死心吧”

到这里,我们可以得到结论:
注解是特殊的接口

知道注解是特殊的接口,对应的实现在何方?(有歌唱:我的爱人哪,你在何方?又有诗云:天涯何处无芳草。)
为了追查到注解的实现,可从Method类入手。
注解分3种:源码级(lombok)、字节码级、运行时级(Spring及各大框架)。运行时级别的注解运用了Java的反射(最劲爆的功能是’猜测’出一个对象的方法,并执行它!),借助Method类,获取到对象里边绑定的乱七八糟的注解,通常会是下边的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void parseMethodAnnotations() {
for (Annotation methodAnnotation : method.getAnnotations()) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
RestMethod methodInfo = null;

for (Annotation innerAnnotation : annotationType.getAnnotations()) {
if (RestMethod.class == innerAnnotation.annotationType()) {
methodInfo = (RestMethod) innerAnnotation;
break;
}
}
……
}
}

method从哪儿来?呵呵,自然是用反射从对象身上搜刮得到的。我们找到method的源码java.lang.reflect.Method类,看看getAnnotation方法的实现:

1
2
3
4
5
6
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
if (annotationClass == null)
throw new NullPointerException();

return (T) declaredAnnotations().get(annotationClass);
}

再追一下declaredAnnotations()和对应的对象:

1
2
3
4
5
6
7
8
9
10
11
private transient Map<Class<? extends Annotation>, Annotation> declaredAnnotations;

private synchronized Map<Class<? extends Annotation>, Annotation> declaredAnnotations() {
if (declaredAnnotations == null) {
declaredAnnotations = AnnotationParser.parseAnnotations(
annotations, sun.misc.SharedSecrets.getJavaLangAccess().
getConstantPool(getDeclaringClass()),
getDeclaringClass());
}
return declaredAnnotations;
}

追查到这里,案件的线索已很明显(报告警察叔叔,就是他!就是他把枪塞在我的手上的)。
前后结合,城乡结合,古今结合,那开眼的同学就会发现,打在方法身上的子弹,不,打在方法身上的注解,编译完事后,以扩展并实现Annotation接口的某种’对象’的形式存在,依附存活在方法里面,成为了方法的’属性’。

到这里,我们可以得到结论:
注解是特殊的类

注解&化学物理反应

要自定义注解,需要三部分内容,也就是邱永臣所说的『注解三重奏』。

声明注解

注解姓甚名谁,家里有几口人,几头牛,几亩地等等信息,均在此昭告天下,比如:

1
2
3
4
5
6
7
8
9
10
11
12
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MethodInfo {

String author() default "xxx@gmail.com";

String date();

int version() default 1;
}

声明使用

注解是把骇人的钝剑(既然是钝的,如何骇人?),无所不往,杀人不见血,落地不开花。但剑是不会主动杀人的,你得告诉它一个目标,它才能远程制导,瞄准,点火发射,华丽升空,绽放绝美的烟火。

1
2
3
4
@MethodInfo(date = "2017-02-07")
public String getAppName() {
return "nothing";
}

这样呢,注解才能getAppName身上”搞搞震”,演绎出它自身的艺术。

注解艺术

您给注解指定了目标,需要注解执行什么动作呢?这就因人而异了,可以仅仅是统计目标的存活数据,也可以建立超巨型烟雾罩,让目标永无见光之日,可以采取的动作太多了,三万年也数不完。
大体的思路,就是利用反射,找到注解所在的目标,为所欲为?此地无银三百两,邻人阿二不曾偷。

参考资料

  1. Java核心技术卷二
  2. Java注解(Annotation)
  3. Java深度历险(六)——Java注解
  4. 公共技术点之 Java 注解 Annotation

从0到1使用Guava(Collection系列)

Posted on 2017-01-24
Symbols count in article: 625 | Reading time ≈ 1 mins.

一、Lists.transform转换ListA为ListB(非guava)

某个List中放有Info,其定义如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.dianping.overseas.poseidon.audit.remote;

/**
*
* Created by qiuyongchen on 2017/1/24.
*/
public class Info {

public String Id;

public int type;

}

存放该Info的List定义如下:

1
List<Info> infoList = Lists.newArrayList();

要从infoList提取出type,组成另一个List,不需要用for循环,只需使用apache的CollectionUtils工具包,比如:

1
2
3
4
5
6
7
List<Integer> typeList = Lists.transform(infoList, new Function<Info, Integer>() {
@Nullable
@Override
public Integer apply(@Nullable Info info) {
return info.type;
}
});

部署Rest架构的Web应用(美团云)

Posted on 2016-11-08
Symbols count in article: 1k | Reading time ≈ 2 mins.

先前,创建且在本地成功运行rest架构的项目,该怎么样部署到远程机器上呢?

导出war包

在导出包前,先设置artifacts的打包过程(file->project structure->Artifacts->选中rcs-web:war->勾选build on make)。接着,在本地运行一遍项目,找到打包好的war包。

搭建远程环境

(手头有台美团云,所以就用它做示例。)

jdk

现在(2016-11-07 21:32:03)美团云默认装载JKD1.7,如果没有的话,可以手动安装:

1
yum install java-1.8.0-openjdk.x86_64

而后验证:

1
java -version

tomcat7

(注:tomcat6带不起上一篇博文里的Rest架构项目)
从tomcat官网下载tomcat7(推荐使用wget),然后使用以下命令启动tomcat:

1
2
3
4
tar -zxv -f apache-tomcat-7.0.29.tar.gz // 解压压缩包
rm -rf apache-tomcat-7.0.29.tar.gz // 删除压缩包
mv apache-tomcat-7.0.29 tomcat
/usr/local/tomcat/bin/startup.sh //启动tomcat

(可以考虑让tomcat7开机自启)

编辑tomcat的权限文件:

1
emacs /usr/local/tomcat/conf/tomcat-users.xml

加入几行:

1
2
3
4
<role rolename="admin-gui"/>
<role rolename="manager-gui"/>
<role rolename="manager-status"/>
<user username="root" password="root" roles="admin-gui,tomcat,role1,manage-status,manager-gui"/>

然后重启tomcat7。

部署应用

访问http://ip:8080/manager/html,在”war file to deploy”一栏里发布之前导出的war包。

再次访问http://ip:8080/rcs-web/user/,即可看到浏览器渲染出的JSON...

Rest风格的后端web(SpringMVC)

Posted on 2016-11-06 | Edited on 2019-02-20
Symbols count in article: 16k | Reading time ≈ 26 mins.

前后端分离

天啦噜,我们希望『前后端分离』,前端的组件是css/js/html/图片,负责显示数据。数据打哪儿来?答案是后端。

后端服务器专门服务于前端,兢兢业业,勤勤恳恳,前端要什么,后端就给什么。

比如,前端向后端发出请求:www.ilovcecl.com/users/,目的是获取用户列表;逻辑复杂的后端服务器掐指一算,洞悉前端要的数据,便以JSON格式返回用户列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"id": 3,
"name": "Jerome",
"age": 45,
"salary": 30000
},
{
"id": 4,
"name": "Silvia",
"age": 50,
"salary": 40000
}
]

可见,前后端分离的过程中,后端会以JSON格式返回数据,数据URI符合Rest风格(当然前后端通信除了Rest外还有其它架构)。

本文给出的正是以JSON风格返回数据、符合Rest风格的后端web项目,不依赖于xml配置,结构简单。

操作说明

你可以从GitHub下载项目源码:https://github.com/qiuyongchen/rcs-web/archive/v1.0.0.zip
使用IntelliJ IDEA导入项目,自动刷新pom依赖(部分IDEA版本有Bug无法刷新,请在shell中输入mvn clean package,手动刷新);导入完成后配置tomcat,启动项目,访问http:localhost:8080/user/,耐心等待几秒钟,JSON即展现你眼前。

你也可以一步步地创建项目,添加文件,最后启动。

项目结构

本项目里,除了一个pom文件外,其余的均是java文件,不依赖传统的spring配置xml。
结构图:

src

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
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
<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>com.ilovecl</groupId>
<artifactId>rcs-web</artifactId>
<packaging>war</packaging>
<version>1.0.0</version>
<name>rcs-web</name>

<properties>
<springframework.version>4.2.0.RELEASE</springframework.version>
<jackson.version>2.5.3</jackson.version>
</properties>

<dependencies>
<!--SpringMVC Rest begin-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${springframework.version}</version>
</dependency>
<!--SpringMVC Rest end-->

<!--jackson begin-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!--jackson end-->

<!--servlet begin-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<!--servlet end-->
</dependencies>


<!--编译管理-->
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.4</version>
<configuration>
<warSourceDirectory>src/main/java</warSourceDirectory>
<warName>rcs-web</warName>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<finalName>rcs-web</finalName>
</build>
</project>

项目初始化 WebInitilizer.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
package config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
* spring会自动在源码包中(pom->warSourceDirectory)寻找继承
* AbstractAnnotationConfigDispatcherServletInitializer的类,
* 将其作为web项目的初始化类
* Created by qiuyongchen on 2016/11/5.
*/
public class WebInitilizer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{WebConfiguration.class};
}

@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}

@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}

}

项目配置类 WebConfiguration.java

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

/**
* 如果源码直接放在pom->warSourceDirectory下,
* 则basePackages为'.'即可;
* 如果源码放在更深层级,
* 则需指定目录层级,如'com.xxx'
* Created by qiuyongchen on 2016/11/5.
*/

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = ".")
public class WebConfiguration {

}

控制类 RcsController.java

一个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
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
122
123
124
125
package controller;

/**
* 凡是带有注解@RestController的类,
* 都被当做controller处理
* Created by qiuyongchen on 2016/11/5.
*/

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

import model.User;
import service.UserService;

@RestController
public class RcsController {

@Autowired
UserService userService; //Service which will do all data retrieval/manipulation work


//-------------------Retrieve All Users--------------------------------------------------------

@RequestMapping(value = "/user/", method = RequestMethod.GET)
public ResponseEntity<List<User>> listAllUsers() {
List<User> users = userService.findAllUsers();
if (users.isEmpty()) {
return new ResponseEntity<List<User>>(HttpStatus.NO_CONTENT);//You many decide to return HttpStatus.NOT_FOUND
}
return new ResponseEntity<List<User>>(users, HttpStatus.OK);
}


//-------------------Retrieve Single User--------------------------------------------------------

@RequestMapping(value = "/user/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> getUser(@PathVariable("id") long id) {
System.out.println("Fetching User with id " + id);
User user = userService.findById(id);
if (user == null) {
System.out.println("User with id " + id + " not found");
return new ResponseEntity<User>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<User>(user, HttpStatus.OK);
}


//-------------------Create a User--------------------------------------------------------

@RequestMapping(value = "/user/", method = RequestMethod.POST)
public ResponseEntity<Void> createUser(@RequestBody User user, UriComponentsBuilder ucBuilder) {
System.out.println("Creating User " + user.getName());

if (userService.isUserExist(user)) {
System.out.println("A User with name " + user.getName() + " already exist");
return new ResponseEntity<Void>(HttpStatus.CONFLICT);
}

userService.saveUser(user);

HttpHeaders headers = new HttpHeaders();
headers.setLocation(ucBuilder.path("/user/{id}").buildAndExpand(user.getId()).toUri());
return new ResponseEntity<Void>(headers, HttpStatus.CREATED);
}


//------------------- Update a User --------------------------------------------------------

@RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)
public ResponseEntity<User> updateUser(@PathVariable("id") long id, @RequestBody User user) {
System.out.println("Updating User " + id);

User currentUser = userService.findById(id);

if (currentUser == null) {
System.out.println("User with id " + id + " not found");
return new ResponseEntity<User>(HttpStatus.NOT_FOUND);
}

currentUser.setName(user.getName());
currentUser.setAge(user.getAge());
currentUser.setSalary(user.getSalary());

userService.updateUser(currentUser);
return new ResponseEntity<User>(currentUser, HttpStatus.OK);
}

//------------------- Delete a User --------------------------------------------------------

@RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
public ResponseEntity<User> deleteUser(@PathVariable("id") long id) {
System.out.println("Fetching & Deleting User with id " + id);

User user = userService.findById(id);
if (user == null) {
System.out.println("Unable to delete. User with id " + id + " not found");
return new ResponseEntity<User>(HttpStatus.NOT_FOUND);
}

userService.deleteUserById(id);
return new ResponseEntity<User>(HttpStatus.NO_CONTENT);
}

//------------------- Delete All Users --------------------------------------------------------

@RequestMapping(value = "/user/", method = RequestMethod.DELETE)
public ResponseEntity<User> deleteAllUsers() {
System.out.println("Deleting All Users");

userService.deleteAllUsers();
return new ResponseEntity<User>(HttpStatus.NO_CONTENT);
}

}

Model类 User.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
package model;

/**
* Created by qiuyongchen on 2016/11/5.
*/
public class User {

private long id;

private String name;

private int age;

private double salary;

public User() {
id = 0;
}

public User(long id, String name, int age, double salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public double getSalary() {
return salary;
}

public void setSalary(double salary) {
this.salary = salary;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (id ^ (id >>> 32));
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (id != other.id)
return false;
return true;
}

@Override
public String toString() {
return "User [id=" + id + ", name=" + name + ", age=" + age
+ ", salary=" + salary + "]";
}

}

Service接口 UserService.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
package service;

import model.User;

import java.util.List;

/**
* Created by qiuyongchen on 2016/11/5.
*/
public interface UserService {

User findById(long id);

User findByName(String name);

void saveUser(User user);

void updateUser(User user);

void deleteUserById(long id);

List<User> findAllUsers();

void deleteAllUsers();

public boolean isUserExist(User user);

}

Service接口实现类 UserServiceImpl.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
package service.impl;

/**
* Created by qiuyongchen on 2016/11/5.
*/

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import model.User;
import service.UserService;

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService {

private static final AtomicLong counter = new AtomicLong();

private static List<User> users;

static {
users = populateDummyUsers();
}

public List<User> findAllUsers() {
return users;
}

public User findById(long id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return null;
}

public User findByName(String name) {
for (User user : users) {
if (user.getName().equalsIgnoreCase(name)) {
return user;
}
}
return null;
}

public void saveUser(User user) {
user.setId(counter.incrementAndGet());
users.add(user);
}

public void updateUser(User user) {
int index = users.indexOf(user);
users.set(index, user);
}

public void deleteUserById(long id) {

for (Iterator<User> iterator = users.iterator(); iterator.hasNext(); ) {
User user = iterator.next();
if (user.getId() == id) {
iterator.remove();
}
}
}

public boolean isUserExist(User user) {
return findByName(user.getName()) != null;
}

private static List<User> populateDummyUsers() {
List<User> users = new ArrayList<User>();
users.add(new User(counter.incrementAndGet(), "Sam", 30, 70000));
users.add(new User(counter.incrementAndGet(), "Tom", 40, 50000));
users.add(new User(counter.incrementAndGet(), "Jerome", 45, 30000));
users.add(new User(counter.incrementAndGet(), "Silvia", 50, 40000));
return users;
}

public void deleteAllUsers() {
users.clear();
}

}

技术栈.分布式监控CAT

Posted on 2016-11-03 | Edited on 2019-02-20
Symbols count in article: 655 | Reading time ≈ 1 mins.

Cat是什么?

一种实时、全量性的开源监控系统,主要是监控JAVA后端(开源地址:http://github.com/dianping/cat)

Cat监控的东西?

代码执行次数;
代码执行耗时(极强!一个URL深入到DB的全过程);
业务指标(诸如支付率、转化率、订单数);
定时上报JVM、GC

Cat的特性有哪些?

a.全量
有足够的能力,记下以往每天的数据(异步地压缩前一天的监测数值,保存到HDFS)。
b.实时
遇到埋点,客户端在MQ通道里发送代码监测情况,服务端异步读取消息队列,分析并保存为当前小时报表(用的正是实时计算!)
c.高可靠
追求高可靠不简单哪,Cat为了尽可能存活得久,站得稳,使用了异步序列化、异步传输(自定义序列化协议,放弃通用序列化协议,不仅如此,还使用Netty的NIO来保证通信)
d.高扩展
消息异步发送到消息队列,历史报表异步发送到HDFS,各个步骤均可以分布式部署,近乎横向地增加机器,扩展性能极强哦~
由于很多步骤都异步化,所以对系统的实时性处理要求不高,就算是实时分析当前小时的报表时遇到瓶颈,Cat也可以多开几个线程,并发消费消息,并进行相应的增量计算。
e.搞吞吐
每天Cat处理的数据高达100T,要将这么多数据都存下来,真不容易!

Cat的消息存储

每个消息的ID都是定制的,格式都是『应用名称-机器IP-时间除以小时得到的整点数-索引INDEX』,存储结构如下图:

根据特定机器,并根据索引index去找到48bits的索引,根据索引找到压缩数据,解压,据偏移地址拿到64K的消息。

关于优化的思考(初期)

Posted on 2016-10-31
Symbols count in article: 2k | Reading time ≈ 3 mins.

交代

总而言之,言而总之,80%问题由20%错误引起(2/8原理适用于金钱分配、权力分配、人口位置、缓存命中、时间效率、问题根源等等等等,非常神奇有木有)。

此次做优化,我渐渐地觉得,写代码追求的不是『越多越好』,也不是『越少越好』,而是『在保证质量的情况下代码尽可能少』。『越多越好』或『越少越好』只有数量一个参数,需要再加上质量参数,质量参数的权重是数量参数的十倍(在特殊情况下甚至能膨胀至百倍千倍)。

业务这玩意不像烧饼,上线后就不管,后续的接口维护优化不可少。业务代码,考验的不是能不能写出来(是个人都能写业务),是写出来的代码质量。

正如28原则,引起我们优化的原因太少,优化到最后,我已觉得自己在做重复工作。

依据现有(2016-10-31)的标准,接口的95线应小于100ms,给前端提供的URL应小于200ms。200ms,这是什么概念?一般的接口实现,代码在CPU里执行时间往往不到5ms,剩余92ms浪费在多线程等待、锁等待、网络IO、SQL执行。

大头SQL

多线程等待或者锁等待不常见,最常见的是网络IO和SQL,其中SQL在接口耗时中占据了大头。

一条有索引的单条件select查询(select * from TABLE where Condition=A limit 1),仅仅花费1ms罢了。这次优化中部分SQL的Condition要么没有索引,要么Condition的种类太少索引相当于不存在;部分SQL并没有limit的概念,动不动就查询几千几万条数据,数据库连接不多写操作不多还好些,如果数据库已经到了瓶颈,一次查询海量数据也是灾难;部分SQL使用了in语句,in语句中有上百个可选性,好可怕。

上述SQL多少可以被原谅,有些超低级错误SQL,好可怕。

循环SQL:『在一段代码里,没有使用批量化SQL(一次查询有限数量),反而在for循环中调用同一个DAO操作。』

仔细想一想,我发觉,SQL是绝对性的瓶颈(在接口耗时中,SQL占比超过50%),业务逻辑越加复杂,SQL没有大突破,怕是要被淘汰。

大头网络

网络连接非常极其以及特别的不稳定,尽管RPC大部分是同机房传输,遇到海量连接并发时,RPC的延迟就不可控了。

偏偏有那么一些人,出于复杂度或业务时间考虑,总是复用现有RPC,而不是开发批量化RPC,于是出现了循环RPC。

循环RPC:『在for循环中调用某RPC,该RPC本可以拥有批量版本。』

最坏的情况莫过于,一个接口祭出循环RPC,祭出的RPC里是循环SQL。

有一种RPC的错误使用方式可能被忽略,那就是使用RPC传输的数据大小。RPC在传输之前必须序列化,序列化几十K数据比较简单,但序列化1M数据就让CPU心寒了。

尺寸对QPS影响也不可小觑:对于1K数据,pigeon服务端qps能到10几w,而1M,大概就只能到几千。

缓存

有些非常关键的接口,比如获取一个用户的详细订单信息,需要查询多个表,调用多个RPC服务,接口的耗时接近100ms。

面对这种棘手情况,可以使用『反第三范式』,将用户的订单冗余到一个表之中,只需调用一个RPC,查询一个表即可。可惜订单的状态多,变动频繁,『反第三范式』的代价太高(保持多个表的实时一致让人伤脑筋)。

除了反范式,也可以使用缓存,将接口即将返回的数据保存的redis里头,下次查询让缓存打头阵。

缓存优点挺多,不仅可以分担数据库的压力,还能减少响应时间,瞬间返回。

可惜缓存的最佳使用情景不多(最佳情景:99.9%的请求均可以在缓存中找到),绝大部分数据仅仅是普通的热点数据,80%的请求对应20%缓存数据,剩余20%请求由于缓存过期,不得不重新查SQL,耗时又一次接近100ms。

也就是说,大部分情况下,缓存只能优化80%的请求,剩余20%请求还是会超时。

搜索

SQL不擅长批量查询(不管是in,还是海量结果的查询),正所谓人各有长短,SQL不行,但搜索非常适合批量查询。

搜索相当于半离线的数据库,每一条数据都有极多索引,只要条件合理,查询非常的快。另外,搜索还能分担数据库的压力。

和数据库相比,搜索的缺点是实时性没保证(增量间隔几秒钟时间),在对实时性要求比较高的场景没法用。(用户支付完,在好几秒时间内显示未支付,真伤脑筋)

但在商家后台等一些项目里,搜索大有发挥之地,理论上,整个商家后台都不必直接查询SQL,只需和搜索打交道即可。

分页

若是没有分页,哪怕使用批量搜索,一次性查询海量数据,怕是神仙也受不了。所以在查询批量数据时,决不能忽略分页。

在前端列表显示数据考虑分页,在搜索时考虑分页,在SQL查询时考虑分页。扩展开来,SQL中若有in关键字,也要限制可选项的数量。

完美结局

综合来看,分页、搜索、缓存和网络IO,无一不是因为数据库吊车尾。

SQL负于Cache

Posted on 2016-10-15
Symbols count in article: 538 | Reading time ≈ 1 mins.

梦萦国庆

2016年的国庆节,部门业务出现离奇的SQL:
每天,当人们从睡梦中醒来又再一次进入梦乡时,短短的12小时,数据库查询次数高达千万。
当时,部门订单量不破1000,何来千万级别的select?

经过一个时辰的调查走访,邱永臣发现了幕后黑手,它不是代码,不是BUG,而是公司内部的订单中心。

原来,在APP改版后,为了适应PM的需求,在用户切换到『我的』tab的瞬间,订单中心便是耗费巨大的时间和资源,轮询全公司上下20个业务部门的服务,调查该用户在各部门的订单,进而计算出总订单数量。

此案怎破?

手段一

向订单中心反馈,要求改『订单中心轮询』为『业务主动推送』。对方果断拒绝,原因:涉众过多,一时半会无法更改。细想之下,也是,似乎有点道理。

手段二

部门内全部放开RPC,只拦截SQL。压力从SQL身上挪开,转到何方?我们想到了KV缓存。部门内的用户挪移至KV缓存,订单中心的轮询到达时,首先让缓存承压,若该用户真属于我部门,放行SQL,从数据库中拿订单信息,不然,别怪我们心狠,拦截该SQL。

做了优化后,SQL从每天的2500W将至50W,而缓存则由0暴增至6000W。

完美结局

从此,凭借巅峰时期QPS承受30W的KV,无论那订单中心再请求,数据库也不会受丝毫影响。

使用 AOP 记录日志

Posted on 2016-09-05 | Edited on 2016-09-06
Symbols count in article: 4k | Reading time ≈ 7 mins.

几天前,本教主接到mentor布置的任务:使用 AOP 来记日志,替代掉重复日志的代码。

本教主自号聪明机智寡言侠义小郎君,此事自然是小Case。

躬身亲做AOP

为了使用 AOP,首先我们得认识到 AOP 是什么对吧,术语原理什么的请翻以前的博文,此番写一下 AOP 的种类。

  1. 总所周知,spring 是个强大的容器,本身实现spring AOP。
  2. 除了spring AOP,业内还有个叫 AspectJ 的玩意,它比 spring AOP 强大了很多很多,它有多厉害?它强大到spring AOP只实现了它部分的功能,spring AOP 只能拦截普通方法,AspectJ 却能拦截构造方法和字段。

所以总结地说,下面将会有2种 AOP,一种是 spring AOP,一种是 AspectJ。

一、spring AOP

实现 spring AOP 有两种做法,一种是使用 xml 来定义切面、切点和通知,另一种是使用@Aspect注解(这种做法实际上是前一种的简化版)

使用 xml 来定义切面

此种方法稍微累赘(spring曾因为大量使用xml配置而被诟病,后来慢慢转向使用注解)。

1.首先要写一个切面(aspect):

1
2
3
4
5
6
7
8
9
10
/**
* Created by qiuyongchen on 2016-09-01 17:54:32.
*/
public class OtaOrderLogAspect {

public String OtaTaskLog(){
doSomething()...
}

}

这个切面里的 OtaTaskLog就是一个 advice 通知,还记得我之前说过的话吗?

一个切面里,有切入点,有通知,如果切入点匹配到了被代理的函数,就启动通知。

上边代码里的OtaOrderLogAspect类就是个切面,里边的OtaTaskLog方法相当于通知,若是好运,匹配到目标方法,就将其拦截,调用通知后再执行目标方法。

2.其次需要将OtaOrderLogAspect托管给 spring。

1
2
<bean id = "logAspect"
class="com.xxx. OtaOrderLogAspect"/>

到这里为止,spring 就知道该切面的存在了,那么,该怎么做,才能让 spring 在恰当的代码处启动通知,执行额外的动作呢?答案是使用 spring AOP。

3.使用 spring AOP

使用 spring AOP 特有的 xml 配置,逐个定义 aspect/pointcut/advice,如下:

1
2
3
4
5
<aop:config>
<aop:aspect ref="logAspect">
<aop:before pointcut="execution(* *(..))" method="OtaTaskLog" />
</aop:aspect>
</aop:config>

上面的配置中,定义了切面(就是OtaOrderLogAspect类),定义了一个 before通知,该通知对应的切入点是任意类里的任意方法(execution( (..))是一种 AspectJ 切入点表达式语言写出来的,该语言可以用来筛选我们想要拦截的方法,具体语法可浏览:http://www.cnblogs.com/javaee6/p/3779826.html),也定义了一个advice 的核心方法(advice分有 before/after/around 等几种),也就是OtaTaskLog,拦截到目标方法后,先执行OtaTaskLog方法,再执行目标方法。

使用@Aspect注解来创建切面

看了上面的那种写法,您是不是觉得有点繁琐呢?每次定义一个切点或切面都要额外写一个 xml 配置,神仙都觉得麻烦。
于是呢,在众多程序员的吐槽中,springAOP 增加了注解的用法,使用注解,完全不需要配置 xml。

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

@Service("otaOrderLogAspect")
@Aspect
@Slf4j
public class OtaOrderLogAspect {

private static final int LOG_MIN = 0;

private static final int LOG_MAX = 3820;

@Resource
private IOtaOrderLogService otaOrderLogService;

/**
* 供应商主动推送的接口处的切点
*/
@Pointcut("execution(* *(int, int)) && @annotation(OtaAopLog))")
public void OtaPushLogPointCut(){}

@Around("OtaPushLogPointCut() && @annotation(otaAopLog)")
public Response OtaPushLog(ProceedingJoinPoint proceedingJoinPoint, OtaAopLog otaAopLog) {
OtaOrderLog orderLog = new OtaOrderLog();

// 获取注解中的参数
int actionType = 0;
actionType = otaAopLog.actionType();
orderLog.setActionType((byte)actionType);

// 记录入参
Object[] args = proceedingJoinPoint.getArgs();
if (args.length == 2) {
orderLog.setOrderId((Integer) args[0]);
orderLog.setParam(String.valueOf(args[1]));
}

// 记录出参
Response response = new Response();
try {
response = (Response) proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}

orderLog.setResponse(JSONObject.toJSONString(response));

// 本地测试时需睡眠主线程,否则异步存数据会失败
// Thread.sleep(10000);

otaOrderLogService.saveLogAsyn(orderLog);
log.info("本次AOP记录的内容主要为:{}", orderLog);
return response;
}
}

使用@Aspect表明该类是切面,使用@Pointcut来过滤目标方法,使用@Around来创建通知。
除了代码,我们需开启 spring 的自动代理:

1
<aop:aspectj-autoproxy/>

做了以上两点,在任何一个拥有两个参数的方法前,加上一个@OtaAopLog注解,AOP 就能自动帮您记日志了。

二、AspectJ

AspectJ 的功能比 springAOP 强大多了,比如,AspectJ 可以在类执行构造方法时进行拦截。
为了使用AspectJ,需要声明独特的类型 aspect:

1
2
3
4
5
6
7
public aspect LogAspect {
pointcut logPointCut() : execute(* *(int, int));

after() returning() : log() {
doSomething()...
}
}

如果要将 bean 注入到LogAspect中,需要特殊的 xml 声明:

1
2
3
<bean class="com.xxx.LogAspect" factory-method="aspectOf">
<property name="xxxName" ref="xxx"/>
</bean>

之所以要这么声明,是因为 AspectJ的切面在 AspectJ 启动时就创建了,spring 无法干预,自然也就没法注入。
而使用切面提供的 aspectOf() 方法,就能获取由 AspectJ 创建的切面的实例,进而 spring 可以注入。

JDK源码之concurrentHashMap

Posted on 2016-08-17 | Edited on 2016-08-28
Symbols count in article: 5.9k | Reading time ≈ 10 mins.

当风再起时,你将找到心中的诗。

HashTable,空有线程同步的特性,因全程使用synchronized关键字,速度过慢,故被抛弃。

HashMap,新时代贵族,在HashTable的基础上做了大量优化而来,被广泛应用于各种场景中,却不再支持线程同步。

ConcurrentHashMap,在 HashTable 的基础上做了另一个方向的优化,在尽量保证速度的同时,支持全并发的读操作,支持部分并发的写操作(默认情况可达到16并发写)。

总数据结构

首先来看看 ConcurrentHashMap 的结构,既然它基于 HashTable 改造而来,它就和 HashTable 有一定的关联性。ConcurrentHashMap 中默认使用了16个Segment存储数据,如:

1
final Segment<K,V>[] segments;

每个 Segment 元素都可以看做一个传统的 Hashtable,拥有自己的写锁,看到这您明白了吗?ConcurrentHashMap 默认使用了16个 HashTable一样的东西,所以它才支持了16并发读。

Segment

再来看看Segment 又是怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* The per-segment table. Elements are accessed via
* entryAt/setEntryAt providing volatile semantics.
*/
transient volatile HashEntry<K,V>[] table;

/**
* The number of elements. Accessed only either within locks
* or among other volatile reads that maintain visibility.
*/
transient int count;

/**
* The total number of mutative operations in this segment.
* Even though this may overflows 32 bits, it provides
* sufficient accuracy for stability checks in CHM isEmpty()
* and size() methods. Accessed only either within locks or
* among other volatile reads that maintain visibility.
*/
transient int modCount;

每个 Segment 段里头又是一个 HashEntry 数组,看到这,有 HashMap/HashTable 基础的同学瞬间就反应过来了:

HashTable 和 HashMap 里边也是用一个数组来存数据,要插入数据时预习算好数组Index,然后再插入到相应的位置,如果该位置已经有数据,就将新数据和已有数据组成一条新链,链头插入 HashEntry 数组中。

没错!就是这样,Concurrent 的结构也是一样的!

值得一提的是,此处的HashEntry 和 HashMap 的 HashEntry 有些不一样,ConcurrentHashMap 的如下:

1
2
3
4
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;

HashMasp 的如下:

1
2
3
4
final K key;
V value;
Entry<K,V> next;
int hash;

可见,hash 与 key 用 final 修饰,表明一生不变放纵不羁的态度,不会发生改变;而 value 和 next 则使用 volatile 修饰,保证了线程之间的可见性。

那么,有童鞋就问了:同样的结构,为什么 ConcurrentHashMap 就支持完全并发的读,而 HashMap 就不支持呢?

具体的原因,请看下面的解释。

ConcurrentHashMap的完全并发读

get 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}

在使用 get 操作时,会首先定位到某个 Segment,再定位该Segment上某个Index的元素,如果是条冲突链,就一直遍历完该链,而不管该链“已被遍历过的地方”发生了什么变化。注意这里的“已被遍历过的地方”,因为读的过程中可能某些元素会被其他线程改变,比如删除等,但由于其删除操作的特殊性,使得其支持并发性。

remove 操作

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
final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}

在删除之前,首先申请重入锁,得到锁后,去遍历冲突链,以便删除数据。

put

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
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}

也是首先申请重入锁,申请到后再遍历冲突链。

注

值得注意的是,之前的 ConcurrentHashMap 使用了 final 来修饰 HashEntry 中的 next指针,那样每次删除时重新构建一条冲突链而不是申请锁,就能做到绝大部分的并发读。

1234…7

邱永臣

一个吃瓜群众,有时跑龙套,有时码字.

100 posts
13 categories
33 tags
GitHub E-Mail
© 2015 – 2021 邱永臣 | 480k | 13:20
备案号 粤ICP备19015297号
|