作者:zuoxiaolong8810(左潇龙),转载请注明出处。
上一章,我们学习了设计模式的概念,以及为什么要学习设计模式,还有在进行系统设计时应当遵守的六大原则,本章我们就来开始一一的学习GOF当中的二十三钟设计模式。
我一会在思考如何去诠释这么多设计模式,因为网上有很多现成的,可供学习的资料,我在想有什么地方可以让各位跟着我的节奏去学习,而不是网上的那些资料,优势在哪里,思考很久,我觉得唯一的优势,或者说我能有的优势,就是简单通俗易懂。
遵循着中心思想通俗易懂,我们首先来回顾一下单例模式为何要出现,又或者说什么样的类可以做成单例的。
在我的工作过程中,我发现所有可以使用单例模式的类都有一个共性,那就是这个类没有自己的状态,换句话说,这些类无论你实例化多少个,其实都是一样的。
我稍微总结一下,一般最容易区别的地方就在于,这些类,都没有非静态的,可设置值的属性。
我来举个反面教材,即一个不能被做成单例模式的类。
public class NoSingleton {
private int changedField;
private static int staticChangedField;
private void setChangedField(int value){
this.changedField = value;
}
private static void setStaticChangedField(int value){
staticChangedField = value;
}
} 上面这个类,有两个属性,一个是静态的,一个是非静态的,而且非静态的属性我们提供了set方法(暂且不考虑突破属性访问权限的反射机制),那么这个类就是具有自己的状态的,因为它有一个实例级别的变量changedField,这个变量在每个实例当中都有可能是不一样的。而静态的则不会有这种担忧,因为它是被这个类的所有实例共享的。
所以上述这个类不能做成单例的类,一般可以做成单例的类的样子都有以下特点。
1.所有的对外可变属性都是静态的。
2.所有的非静态属性都是私有的,并且没有对外公开的可以设置值的方法。
下面,我们就来看一下做成单例的几种方式。
首先,我们来看一下最标准也是最原始的单例模式的构造方式。
public class Singleton {
//一个静态的实例
private static Singleton singleton;
//私有化构造函数
private Singleton(){}
//给出一个公共的静态方法返回一个单一实例
public static Singleton getInstance(){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
} 这是在不考虑并发访问的情况下标准的单例模式的构造方式,这种方式通过几个地方来限制了我们取到的实例是唯一的。
1.静态实例,带有static关键字的属性在每一个类中都是唯一的,此为保证单例的最重要的一步。
2.限制客户端随意创造实例,即私有化构造方法。
3.给一个公共的获取实例的静态方法,注意,是静态的方法,因为这个方法是在我们未获取到实例的时候就要提供给客户端调用的,所以如果是非静态的话,那就变成一个矛盾体了,因为非静态的方法必须要拥有实例才可以调用。
4.判断只有持有的静态实例为null时才调用构造方法创造一个实例,否则就直接返回。
假如你去面试一家公司,给了你一道题,让你写出一个单例模式的例子,那么如果你是刚出大学校门的学生,你能写出上面这种示例,假设我是面试官的话,满分100的话,我会给90分,剩下的那10分算是给更优秀的人一个更高的台阶。但如果你是一个有过两三年工作经验的人,如果你写出上面的示例,我估计我最多给你30分,甚至心情要是万一不好的话可能会一分不给。
为什么同样的示例放到不同的人身上差别会这么大,就是因为前面我提到的那个情况,在不考虑并发访问的情况下,上述示例是没有问题的。
至于为什么在并发情况下上述的例子是不安全的呢,我在这里给各位制造了一个并发的例子,用来说明,上述情况的单例模式,是有可能造出来多个实例的,我自己测试了约莫100次左右,最多的一次,竟然造出了3个实例。下面给出代码,大约运行10次(并发是具有概率性的,10次只是保守估计,也可能一次,也可能100次)就会发现我们创造了不只一个实例。
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestSingleton {
boolean lock ;
public boolean isLock() {
return lock;
}
public void setLock(boolean lock) {
this.lock = lock;
}
public static void main(String[] args) throws InterruptedException {
final Set<String> instanceSet = Collections.synchronizedSet(new HashSet<String>());
final TestSingleton lock = new TestSingleton();
lock.setLock(true);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executorService.execute(new Runnable() {
public void run() {
while (true) {
if (!lock.isLock()) {
Singleton singleton = Singleton.getInstance();
instanceSet.add(singleton.toString());
break;
}
}
}
});
}
Thread.sleep(5000);
lock.setLock(false);
Thread.sleep(5000);
System.out.println("------并发情况下我们取到的实例------");
for (String instance : instanceSet) {
System.out.println(instance);
}
executorService.shutdown();
}
}
我在程序中同时开启了100个线程,去访问getInstance方法,并且把获得实例的toString方法获得的实例字符串装入一个同步的set集合,set集合会自动去重,所以看结果如果输出了两个或者两个以上的实例字符串,就说明我们在并发访问的过程中产生了多个实例。
程序当中让main线程睡眠了两次,第一次是为了给足够的时间让100个线程全部开启,第二个是将锁打开以后,保证所有的线程都已经调用了getInstance方法。
好了,这下我们用事实说明了,上述的单例写法,我们是可以创造出多个实例的,至于为什么在这里要稍微解释一下,虽说我一直都喜欢用事实说话,包括看书的时候,我也不喜欢作者跟我解释为什么,而是希望给我一个例子,让我自己去印证。
造成这种情况的原因是因为,当并发访问的时候,第一个调用getInstance方法的线程,在判断完singleton是null的时候,就进入了if块准备创造实例,但是同时另外一个线程在第一个调用的线程还未创造出来之前,就又进行了singleton是否为null的判断,这时singleton依然为null,所以第二个线程也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就造成单例模式并非单例。
为了避免这种情况,我们就要考虑并发的情况了,我们最容易想到的方式应该是下面这样的方式。
public class BadSynchronizedSingleton {
//一个静态的实例
private static BadSynchronizedSingleton synchronizedSingleton;
//私有化构造函数
private BadSynchronizedSingleton(){}
//给出一个公共的静态方法返回一个单一实例
public synchronized static BadSynchronizedSingleton getInstance(){
if (synchronizedSingleton == null) {
synchronizedSingleton = new BadSynchronizedSingleton();
}
return synchronizedSingleton;
}
} 上面的做法很简单,就是将整个获取实例的方法同步,这样在一个线程访问这个方法时,其它所有的线程都要处于挂起等待状态,倒是避免了刚才同步访问创造出多个实例的危险,但是我只想说,这样的设计实在是糟糕透了,这样会造成很多无谓的等待,所以为了表示我的愤怒,我在类名上加入Bad。
其实我们同步的地方只是需要发生在单例的实例还未创建的时候,在实例创
原文地址:http://www.javaarch.net/jiagoushi/698.htm
Spring rest对etag支持
etag(entity tag)是http响应头中用来判断对应响应结果是否修改。可以使用hash算法来计算etag的值。
比如:第一次访问
curl -H "Accept: application/json" -i http://localhost:8080/rest-sec/api/resources/1
响应为:
HTTP/1.1 200 OK
ETag: "f88dd058fe004909615a64f01be66a7"
Content-Type: application/json;charset=UTF-8
Content-Length: 52
那么下次客户端访问的时候If-None-Match或者If-Match头来让服务端判断是否修改
curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"'
-i http://localhost:8080/rest-sec/api/resources/1
如果没有修改,则服务端会返回
HTTP/1.1 304 Not Modified
ETag: "f88dd058fe004909615a64f01be66a7"
我们重新修改请求参数:
curl --user admin@fake.com:adminpass -H "Content-Type: application/json" -i
-X PUT --data '{ "id":1, "name":"newRoleName2", "description":"theNewDescription" }' http://localhost:8080/rest-sec/api/resources/1
响应为:
HTTP/1.1 200 OK
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Content-Length: 0
如果我们再访问
curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i
http://localhost:8080/rest-sec/api/resources/1
则返回:
HTTP/1.1 200 OK
ETag: "03cb37ca667706c68c0aad4cb04c3a211"
Content-Type: application/json;charset=UTF-8
Content-Length: 56
spring对etag的支持,在web.xml中加上这个filter
<filter>
<filter-name>etagFilter</filter-name>
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>etagFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
测试etags
第一次判断etag不为空
@Test
public void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() {
// Given
Resource existingResource = getApi().create(new Resource());
String uriOfResource = baseUri + "/" + existingResource.getId();
// When
Response findOneResponse = RestAssured.given().
header("Accept", "application/json").get(uriOfResource);
// Then
assertNotNull(findOneResponse.getHeader(HttpHeaders.ETAG));
}
第二次,我们从上一次取出etag之后把etag发送给服务端验证
@Test
public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() {
// Given
T existingResource = getApi().create(createNewEntity());
String uriOfResource = baseUri + "/" + existingResource.getId();
Response findOneResponse = RestAssured.given().
header("Accept", "application/json").get(uriOfResource);
String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);
// When
Response secondFindOneResponse= RestAssured.given().
header("Accept", "application/json").headers("If-None-Match", etagValue)
.get(uriOfResource);
// Then
assertTrue(secondFindOneResponse.getStatusCode() == 304);
}
测试服务端响应结果变化了,则
@Test
public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() {
// Given
T existingResource = getApi().create(createNewEntity());
String uriOfResource = baseUri + "/" + existingResource.getId();
Response findOneResponse = RestAssured.given().
header("Accept", "application/json").get(uriOfResource);
String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);
existingResource.setName(randomAlphabetic(6))
getApi().update(existingResource.setName(randomString));
// When
Response secondFindOneResponse= RestAssured.given().
header("Accept", "application/json").headers("If-None-Match", etagValue)
.get(uriOfResource);
// Then
assertTrue(secondFindOneResponse.getStatusCode() == 200);
}
如果请求带上不正确的etag值并且加上if-match判断,则会返回412Precondition Failed
@Test
public void givenResourceExists_whenRetrievedWithIfMatchIncorrectEtag_then412IsReceived() {
// Given
T existingResource = getApi().create(createNewEntity());
// When
String uriOfResource = baseUri + "/" + existingResource.getId();
Response findOneResponse = RestAssured.given().header("Accept", "application/json").
headers("If-Match", randomAlphabetic(8)).get(uriOfResource);
// Then
assertTrue(findOneResponse.getStatusCode() == 412);
}
github示例地址:https://github.com/eugenp/REST