记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步
之前学习创建型模式,主要解决“对象的创建”问题,和结构型模式,解决“类或对象的组合或组装”问题,接下来学习行为型模式,解决“类或对象的交互”问题。
观察者模式
原理及应用场景剖析
观察者模式Observer design pattern,也叫发布订阅模式publish-subscribe design pattern,设计模式一书定义:define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
一般说,被依赖的对象叫被观察者observable,依赖的对象叫观察者observer,不过,实际开发中,有各种叫法,如subject-observer、publisher-subscriber、producer-consumer、eventEmitter-eventListener、dispatcher-listener。
实际上,观察者模式是个比较抽象的模式,根据不同的应用场景和需求,有完全不同的实现方式。最经典的一种实现方式。
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(Message message);
}
public interface Observer {
void update(Message message);
}
public class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(Message message) {
for (Observer observer:observers){
observer.update(message);
}
}
}
public class ConcreteObserverOne implements Observer {
@Override
public void update(Message message) {
//todo 获取消息通知,执行自己的逻辑
System.out.println("ConcreteObserverOne is notified");
}
}
public class ConcreteObserverTwo implements Observer {
@Override
public void update(Message message) {
//todo 获取消息通知,执行自己的逻辑
System.out.println("ConcreteObserverTwo is notified");
}
}
public class Demo {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
subject.registerObserver(new ConcreteObserverOne());
subject.registerObserver(new ConcreteObserverTwo());
subject.notifyObservers(new Message());
}
}
实际上,上面的代码只能算是“模板代码”,只能反映大体的设计思路。在真实的软件开发中,并不需要照搬上面的模板代码。观察者模式的实现方法各式各样,函数、类的命名根据业务场景的不同有很大区别,如register函数可叫attach,remove函数可叫detach等。不过,设计思路差不多。
通过一个例子了解下什么情况下需要用到这种设计模式?或者说,这种设计模式能解决什么问题?
假设在开发一个p2p投资理财系统,用户注册成功后,会给用户发放投资体验金。代码实现大致如下:
public class UserController {
private UserService userService;//依赖注入
private PromotionService promotionService;//依赖注入
public Long register(String telephone, String password){
//省略输入参数的校验代码
//省略userService.register()异常的try-catch代码
long userId = userService.register(telephone,password);
promotionService.issueNewUserExperienceCash(userId);
return userId;
}
}
}
虽然注册接口做了两件事,注册和发放体验金,违反单一职责原则,但是,如果没有扩展和修改的需求,现在的代码实现可以接收,如果非得用观察者模式,需要引入更多的类和复杂的代码结构,反而是过度设计。
相反,如果需求频繁变动,如用户注册成功后,不再发放体验金,而是改为发放优惠券,并且要给用户发送一封“欢迎注册成功”的站内信。这种情况,就需要频繁修改register()方法的代码,违反开闭原则。而且,注册成功后需要执行的后续操作越来越多,register()方法的逻辑越来越复杂,影响到代码的可读性和可维护性。
这种情况下,观察者模式派上用场。重构后:
public interface RegObserver {
void handleRegSuccess(long userId);
}
public class RegPromotionObserver implements RegObserver {
private PromotionService promotionService;
@Override
public void handleRegSuccess(long userId) {
promotionService.issueNewUserExperienceCash(userId);
}
}
public class RegNotificationObserver implements RegObserver {
private NotificationService notificationService;
@Override
public void handleRegSuccess(long userId) {
notificationService.sendInboxMessage(userId,"Welcome...");
}
}
public class UserController {
private UserService userService;
private List<RegObserver> regObservers = new ArrayList<>();
//一次性设置好,之后也不可能动态的修改
public void setRegObservers(List<RegObserver> observers){
regObservers.addAll(observers);
}
public Long register(String telephone,String password){
//省略输入参数的校验代码
//省略userService.register()异常的try-catch代码
long userId = userService.register(telephone,password);
for (RegObserver observer:regObservers){
observer.handleRegSuccess(userId);
}
return userId;
}
}
当需要添加新的观察者的时候,如用户注册成功后,推送用户注册信息到大数据征信系统,基于观察者模式的代码实现,UserController类的register()方法不需要修改,只需要添加一个实现了RegObserver接口的类,并通过setRegObservers()方法将其注册到UserController类中即可。
可能说,当把发送体验金替换为发送优惠券,需要修改RegPromotionObserver类中的handleRegSuccess()方法的代码,违反了开闭原则。不过,相对于register()方法,handleRegSuccess()方法的逻辑简单的多,修改更不容易出错,引入bug的风险更低。
总结:设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码结构,行为型模式是将不同行为代码解耦,具体到观察者模式,是将观察者和被观察者代码解耦。
基于不同应用场景的不同实现方式
观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都是这种模式的影子,比如,邮件订购、rss feeds,本质都是观察者模式。
不同的应用场景和需求下,这个模式也有截然不同的实现方式,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。
之前讲到的实现方式,从刚刚的分类方式看,是同步阻塞的实现方式。观察者和被观察者代码在一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成后,才执行后续代码。对照上面的用户注册的例子,register()方法依次调用执行每个观察者的handleRegSuccess()方法,等到都执行完成后,才会返回结果给客户端。
如果注册接口是个调用比较频繁的接口,对性能非常敏感,希望接口的响应时间尽可能短,那么可以将同步阻塞的实现方式改为异步非阻塞的实现方式,以此减少响应时间。具体说,当userService.register()方法执行完成后,启动一个新的线程执行观察者的handleRegSuccess()方法,这样userController.register()方法就不需要等到所有的handleRegSuccess()都执行完成后才返回结果给客户端。userController.register()方法从执行3个SQL语句才返回,减少到执行1个SQL语句就返回,响应时间粗略减少为原来1/3。
那如何实现一个异步非阻塞的观察者模式呢?创建一个新的线程执行代码。更优雅的实现方式是基于EventBus实现。
刚才的两个场景,不管同步阻塞实现方式还是异步非阻塞实现方式,都是进程内的实现方式,如果用户注册成功,还要发送用户信息给大数据征信系统,而大数据征信系统是个独立的系统,跟它的交互是跨不同进程的,如何实现跨进程的观察者模式呢?
如果大数据征信系统提供了发送用户注册信息的RPC接口,仍可以沿用之前的实现思路,在handleRegSuccess()方法中调用rpc接口发送数据。但还有更优雅、更常用的实现方式,就是基于消息队列(message queue,如activeMQ)所实现。
当然,这种实现也有弊端,就是需要引入一个新的系统(消息队列),增加维护成本,不过好处也很明显。在原来的实现方式中,观察者需要注册到被观察者中,被观察者需要依次遍历观察者来发送消息。基于消息队列,被观察者和观察者更加彻底的解耦,两者相互不感知。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。
和生产-消费者模型的区别是什么?发布-订阅是一对多,生产-消费也可能是一对多啊,细究的话,一条消息被生产出来,在生产者-消费者模型中只能被一个消费者消费,而在发布-订阅中可被多个订阅者消费。
异步非阻塞观察者模式的简单实现
有两种实现方式,一种是在每个handleRegSuccess()方法中创建一个新的线程执行代码逻辑;另一种是在UserController的register()中使用线程池来执行每个观察者的handleRegSuccess()方法。具体代码
public class RegPromotionObserver implements RegObserver {
// 第一种实现方式,其他类代码不变,就没有再重复罗列
private PromotionService promotionService;//依赖注入
@Override
public void handleRegSuccess(final long userId) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
promotionService.issueNewUserExperienceCash(userId);
}
});
thread.start();
}
}
//第二种实现方式,其他类代码不变,没有再重复罗列
public class UserController {
private UserService userService;//依赖注入
private List<RegObserver> regObservers = new ArrayList<>();
private Executor executor;
public UserController(Executor executor){
this.executor = executor;
}
public void setRegObservers(List<RegObserver> observers){
regObservers.addAll(observers);
}
public Long register(String telephone,String password){
//省略输入的校验代码
//省略userService.register()异常的try-catch代码
final long userId = userService.register(telephone,password);
for (final RegObserver observer: regObservers){
executor.execute(new Runnable() {
@Override
public void run() {
observer.handleRegSuccess(userId);
}
});
}
return userId;
}
}
对第一种实现方式,频繁的创建和销毁线程比较耗时,且并发线程数无法控制,创建过多的线程会导致堆栈溢出。第二种,尽管利用了线程池解决了第一种实现方式的问题,但线程池、异步执行逻辑都耦合在register()方法中,增加这部分业务代码的维护成本。
如果更极端点,需要再同步阻塞和异步非阻塞之间灵活切换,需要不停修改UserController的代码。此外,如果项目中,不止一个业务模块需要用到异步非阻塞观察者模式,这样的代码无法复用。
框架的作用:隐藏实现细节,降低开发难度,做到代码复用,解耦业务和非业务代码,让程序员聚焦业务开发。针对异步非阻塞观察者模式,可抽象为框架来达到该效果。这个框架就是EventBus。
EventBus框架功能需求
EventBus,事件总线,提供实现观察者模式的骨架代码,可以基于此框架,非常容易的在自己的业务场景中实现观察者模式,其中Google guava eventbus就是比较著名的EventBus框架,不仅支持异步非阻塞模式,也支持同步阻塞模式。
举例说明:
public class UserController {
private UserService userService;
private EventBus eventBus;
private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;
public UserController(){
eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_THREAD_POOL_SIZE));
}
public void setRegObservers(List<Object> observers){
for (Object observer:observers){
eventBus.register(observer);
}
}
public Long register(String telephone, String password){
//省略输入参数的校验代码
//省略userService.register()异常的try-catch代码
long userId = userService.register(telephone,password);
eventBus.post(userId);
return userId;
}
}
public class RegPromotionObserver {
private PromotionService promotionService;
@Subscribe
public void handleRegSuccess(long userId){
promotionService.issueNewUserExperienceCash(userId);
}
}
public class RegNotificationObserver {
private NotificationService notificationService;
@Subscribe
public void handleRegSuccess(long userId){
notificationService.sendInboxMessage(userId,"...");
}
}
利用EventBus框架实现的观察者模式,和从零编写的观察者模式相比,实现思路大致一样,都要定义Observer,并通过register()注册Observer,也都要通过调用某个方法(如EventBus的post()方法)给Observer发送消息(在EventBus中消息被称作事件event)。
但实现细节有些区别,基于EventBus,不需要定义Observer接口,任意类型的对象都可以注册到EventBus中,通过@Subscribe
注解标明类中哪个方法可接收被观察者发送的消息。
guava EventBus的类和方法
- EventBus AsyncEventBus
对外暴露的所有可调用接口,都封装在EventBus类中,其中,EventBus实现同步阻塞的观察者模式,而AsyncEventBus继承自EventBus,提供异步非阻塞的观察者模式,用法:
EventBus eventBus = new EventBus();//同步阻塞模式
EventBus eventBus = new AysncEventBus(Executors.newFixedThreadPool(8));//异步非阻塞
- register()方法
EventBus提供register()方法用来注册观察者。具体定义如下,可接受任何类型(Object)的观察者;经典的观察者模式的实现中,register()方法必须接受实现了同一Observer接口的类对象。
public void register(Object object);
- unregister()方法
该方法用于从EventBus中删除某个观察者。
public void unregister(Object object);
- post()方法
EventBus类提供post方法,用来给观察者发送消息,具体定义
public void post(Object object);
和经典的观察者模式不同之处是,当调用post()方法发送消息时,并非把消息发送给所有的观察者,而是发送给可匹配的观察者。也就是能接收的消息类型是发送消息(post方法定义的event)类型的父类。举例说明:
如AObserver能接收的消息类型是XMsg,BObserver能接收的消息类型是YMsg,CObserver能接收的消息类型是ZMsg。其中,XMsg是YMsg的父类,当发送如下消息时,相应能接收到消息的可匹配观察者如下:
XMsg xMsg = new XMsg();
YMsg yMsg = new YMsg();
ZMsg zMsg = new ZMsg();
post(xMsg);==>AObserver接收到消息
post(yMsg);==>AObserver、BObserver接收到消息
post(zMsg);==>CObserver接收到消息
可能会问,每个Observer能接收到的消息类型是哪里定义的?看下Guava EventBus最特别的地方,就是@Subscribe
注解。
- @Subscribe注解
通过@Subscribe
注解标明某个方法能接收哪种类型的消息,具体的使用代码如下。在DObserver类,通过@Subscribe
注解两个方法f1() f2()
public class DObserver {
//...省略其他属性和方法...
@Subscribe
public void f1(PMsg event){//...}
@Subscribe
public void f1(QMsg event){//...}
}
当通过register()方法将DObserver类对象注册到EventBus时,EventBus会根据@Subscribe
注解找到f1()和f2(),并将两个方法能接收的消息类型记录下来(PMsg->f1, QMsg->f2)。通过post()方法发送消息(如QMsg消息)的时候,EventBus会通过之前的记录(QMsg->f2),调用相应的方法f2.
自己实现EventBus框架
重点是两个核心方法register()和post()的实现原理。
从图中可看出,最关键的一个数据结构是Observer注册表,记录消息类型和可接收消息函数的对应关系,当调用register()方法注册观察者时,EventBus通过解析@Subscribe
注解,生成Observer注册表。当调用post()方法发送消息时,EventBus通过注册表找到相应的可接收消息的方法,然后通过java的反射语法来动态的创建对象、执行方法。对于同步阻塞模式,EventBus在一个线程内依次执行相应的方法。对于异步非阻塞模式,EventBus通过一个线程执行相应的方法。
整个小框架的代码实现包括5个类:EventBus、AsyncEventBus、Subscribe、ObserverAction、ObserverRegistry。依次看这5个类。
1. Subscribe
是个注解,用于标明观察者的哪个函数可接收消息。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface Subscribe {
}
2. ObserverAction
用于表示@Subscribe
注解的方法,其中,target表示观察者类,method表示方法,主要用在ObserverRegistry观察者注册表中。
public class ObserverAction {
private Object target;
private Method method;
public ObserverAction(Object target, Method method) {
this.target = target;
this.method = method;
this.method.setAccessible(true);
}
public void execute(Object event){
// event是method方法的参数
try{
method.invoke(target,event);
}catch (InvocationTargetException | IllegalAccessException e){
e.printStackTrace();
}
}
}
3. ObserverRegistry
就是Observer注册表,是最复杂的一个类,框架几乎所有的核心逻辑都在这个类中。这个类大量使用java的反射语法,不过代码整体不难理解,其中一个比较有技巧的是CopyOnWriteArraySet的使用。
CopyOnWriteArraySet,在写入数据的时候,会创建一个新的set,并将原始数据clone到新的set中,在新的set中写入数据完成后,再用新的set替换老的set。保证了在写入数据时,不影响数据的读取操作,,以此解决读写并发问题。此外,通过加锁的方式,避免并发写冲突。
public class ObserverRegistry {
private ConcurrentHashMap<Class<?>, CopyOnWriteArraySet<ObserverAction>> registry;
public void register(Object observer){
Map<Class<?>, Collection<ObserverAction>> observerActions = findAllObserverActions(observer);
for (Map.Entry<Class<?>,Collection<ObserverAction>> entry:observerActions.entrySet()){
Class<?> eventType = entry.getKey();
Collection<ObserverAction> eventActions = entry.getValue();
CopyOnWriteArraySet<ObserverAction> registeredEventActions = registry.get(eventType);
if (registeredEventActions == null){
registry.putIfAbsent(eventType,new CopyOnWriteArraySet<>());
registeredEventActions = registry.get(eventType);
}
registeredEventActions.addAll(eventActions);
}
}
public List<ObserverAction> getMatchedObserverActions(Object event){
List<ObserverAction> matchedObservers = new ArrayList<>();
Class<?> postedEventType = event.getClass();
for (Map.Entry<Class<?>,CopyOnWriteArraySet<ObserverAction>> entry:registry.entrySet()){
Class<?> eventType = entry.getKey();
Collection<ObserverAction> eventActions = entry.getValue();
if (postedEventType.isAssignableFrom(eventType)){
matchedObservers.addAll(eventActions);
}
}
return matchedObservers;
}
private Map<Class<?>,Collection<ObserverAction>> findAllObserverActions(Object observer){
Map<Class<?>,Collection<ObserverAction>> observerActions = new HashMap<>();
Class<?> clazz = observer.getClass();
for (Method method: getAnnotatedMethod(clazz)){
Class<?>[] parameterTypes = method.getParameterTypes();
Class<?> eventType = parameterTypes[0];
if (!observerActions.containsKey(eventType)){
observerActions.put(eventType,new ArrayList<>());
}
observerActions.get(eventType).add(new ObserverAction(observer,method));
}
return observerActions;
}
private List<Method> getAnnotatedMethod(Class<?> clazz){
List<Method> annotatedMethods = new ArrayList<>();
for (Method method: clazz.getDeclaredMethods()){
if (method.isAnnotationPresent(Subscribe.class)){
Class<?>[] parameterTypes = method.getParameterTypes();
Preconditions.checkArgument(parameterTypes.length == 1,
"Method %s has @Subscribe annotation but has %s parameters."
+"Subscriber methods must have exactly 1 parameter.",method,parameterTypes.length);
annotatedMethods.add(method);
}
}
return annotatedMethods;
}
}
4. EventBus
EventBus实现的是阻塞同步的观察者模式,里面MoreExecutors.directExecutor()
是Google Guava提供的工具类,看似多线程,实则单线程。之所以这样实现,是为了和AsyncEventBus统一代码逻辑,做到代码复用。
public class EventBus {
private Executor executor;
private ObserverRegistry registry = new ObserverRegistry();
public EventBus(){
this(MoreExecutors.directExecutor());
}
protected EventBus(Executor executor){
this.executor = executor;
}
public void register(Object object){
registry.register(object);
}
public void post(Object event){
List<ObserverAction> observerActions = registry.getMatchedObserverActions(event);
for (ObserverAction observerAction:observerActions){
executor.execute(new Runnable() {
@Override
public void run() {
observerAction.execute(event);
}
});
}
}
}
5. AsyncEventBus
有了EventBus,AsyncEventBus实现很简单,为实现异步非阻塞的观察者模式,不能再继续用MoreExecutors.directExecutor()
而是需要在构造函数中,由调用者注入线程池。
public class AsyncEventBus extends EventBus {
public AsyncEventBus(Executor executor){
super(executor);
}
}
至此,就实现了可用的EventBus,不过在细节上,Google Guava EventBus做了很多优化,如优化在注册表查找消息可匹配方法的算法。
模板模式
模板模式主要用来解决复用和扩展两个问题。
模板模式的原理和实现
模板模式,全称模板方法设计模式,Template Method Design Pattern。定义:Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure. 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类实现。模板方法模式可让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
这里的“算法”,可理解为广义的“业务逻辑”,并不特指某个算法。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
代码更简单,如下,templateMethod()方法定义为final,是为了避免子类重写它。method1()和method2()定义为abstract,是为了强迫子类去实现。实际项目开发中,实现较为灵活。
public abstract class AbstractClass {
public final void templateMethod(){
//...
method1();
//...
method2();
//...
}
protected abstract void method1();
protected abstract void method2();
}
public class ConcreteClass1 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
public class ConcreteClass2 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
AbstractClass demo = new ConcreteClass1();
demo.templateMethod();
模板模式作用1:复用
把一个算法中不变的流程抽象到父类的模板方法templateMethod()中,将可变的部分method1()和method2()
留给子类实现。所有的子类都可复用父类模板方法定义的流程代码。通过两个例子看下。
1. java InputStream
java IO类库中,有很多类的设计都用到模板模式,如InputStream、OutputStream、Reader、Writer。以InputStream举例。InputStream的read()方法是个模板方法,定义了读取数据的整个流程,并暴露一个可由子类定制的抽象方法。只是这个方法也被命名为read(),只是参数和模板方法不同。
public abstract class InputStream implements Closeable {
//...省略其他代码...
public int read(byte b[], int off, int len) throws IOException{
if (b == null){
throw new NullPointerException();
}else if (off < 0 || len < 0 || len > b.length - off){
throw new IndexOutOfBoundsException();
}else if (len ==0){
return 0;
}
int c = read();
if (c == -1){
return -1;
}
b[off] = (byte)c;
int i = 1;
try{
for (; i < len; i++){
c = read();
if (c == -1){
break;
}
b[off + i] = (byte)c;
}
}catch (IOException e){
}
return i;
}
public abstract int read() throws IOException;
@Override
public void close() throws IOException {
}
}
public class ByteArrayInputStream extends InputStream {
//...省略其他代码...
private int pos;
private int count;
private byte[] buf;
@Override
public synchronized int read() throws IOException {
return (pos < count)?(buf[pos++] & 0xff):-1;
}
}
2. java AbstractList
在AbstractList类中,addAll()方法可看做模板方法,add()是子类需要重写的方法,尽管没有声明为abstract,但函数实现直接抛出UnsupportedOperationException异常。前提是如果子类不重写是不能使用的。
public abstract class AbstractList<E> {
public boolean addAll(int index, Collection<? extends E> c){
rangeCheckForAdd(index);
boolean modified = false;
for (E e:c){
add(index++,e);
modified = true;
}
return modified;
}
public void add(int index, E element){
throw new UnsupportedOperationException();
}
//...
}
模板模式作用2:扩展
模板模式第二个作用是扩展,这里说的扩展,不是代码的扩展性,而是框架的扩展性,有点类似控制反转。基于该作用,模板模式常用在框架的开发中,让框架用户可在不修改框架源码的情况下,定制化框架的功能。通过junit TestCase、java Servlet两个例子来解释。
1. java servlet
对java web项目开发说,常用的开发框架是springMVC。不过,如果抛开这些高级框架来开发web项目,必然会用到servlet。实际上,使用比较底层的servlet开发web项目并不难,只需定义一个继承HttpServlet的类,并重写doGet()或doPost()方法分别处理get和post请求。
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello world");
}
}
此外,还需要在配置文件web.xml中写配置,tomcat、jetty等servlet容器启动时,会自动加载这个配置文件中的url和servlet之间的映射关系。
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.ai.doc.template.servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
在浏览器输入网址(http://127.0.0.1:8080/hello)后,servlet容器会接收到相应的请求,根据url和servlet的映射关系,找到相应的servlet(HelloServelt),然后执行它的service()方法,service()方法定义在父类HttpServlet中,会调用doGet()或doPost()方法,然后输出数据(“Hello world”)到网页。
再看HttpServlet的service()方法
public class HttpServlet {
public void service(ServletRequest req, ServerHttpResponse res) throws Exception{
HttpServletRequest request;
HttpServletResponse response;
if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)){
throw new ServletException("non-HTTP request or response");
}
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
service(request,response);
}
protected void service(HttpServletRequest req,HttpServletResponse resp)throws Exception{
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 login
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 = lString.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;
}
}
//...
}
可看出,HttpServlet的service()方法就是一个模板方法,实现了整个HTTP请求的执行流程,doGet() doPost()是模板中可由子类来定制的部分。
实际上相当于servlet框架提供了一个扩展点(doGet() doPost()方法),让框架用户在不用修改servlet框架源码的情况下,将业务代码通过扩展点嵌入框架中执行。
2. JUnit TestCase
和servlet类似,junit框架也通过模板模式提供一些功能扩展点(setUp() tearDown()等),让框架用户可在这些扩展点扩展功能。
在使用junit测试框架编写单元测试时,编写的测试类都要继承框架提供的TestCase类,在TestCase类中,runBare()方法是模板方法,定义执行测试用例的整体流程:先执行setUp做些准备工作,再执行runTest方法运行真正的测试代码,最后执行tearDown做扫尾工作。
TestCase类的具体代码如下,尽管setUp和tearDown并非抽象方法,还提供默认实现,不强制子类重新是吸纳,但也可在子类中定制,所以符合模板模式的定义。
public abstract class TestCase extends Assert implements Test {
public void runBare() throws Throwable{
Throwable exception = null;
setUp();
try{
runTest();
}catch (Throwable running){
exception = running;
}finally {
try{
tearDown();
}catch (Throwable tearingDown){
if (exception == null) exception = tearingDown;
}
}
if (exception != null) throw exception;
}
protected void setUp() throws Exception{}
protected void tearDown() throws Exception{}
}
回调的原理解析
相较于普通的方法调用,回调是一种双向调用关系,A类事先注册某个函数F到B类,A类在调用B类的P函数时,B类反过来调用A类注册给他的F函数,这里的F函数就是“回调函数”。A调用B,B反过来调用A,这种调用机制就叫做“回调”。
A如何将回调函数传递给B类呢?不同的编程语言,有不同的实现方法,C语言使用函数指针,java使用报过了回调函数的类对象,简称为回调对象。举例如下:
public interface ICallback {
void methodToCallback();
}
public class BClass {
public void process(ICallback callback){
//...
callback.methodToCallback();
//...
}
}
public class AClass {
public static void main(String[] args) {
BClass b = new BClass();
b.process(new ICallback() {//回调对象
@Override
public void methodToCallback() {
System.out.println("call back me.");
}
});
}
}
上述代码是java回调的典型代码实现,从代码中可看出,回调跟模板模式一样,也有复用和扩展的功能。除了回调函数,BClass类的process()方法中的逻辑都可复用。如果ICallback、BClass是框架代码,AClass是使用框架的客户端代码,通过ICallback定制process()方法,也就是说,框架因此具有扩展的能力。
实际上,回调不仅可用在代码设计上,在更高层的架构设计上也较为常用。如通过三方支付系统来实现支付功能,用户在发起支付请求后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的URL)给第三方,等三方支付系统执行完成后,将结果通过回调接口返回给用户。
回调可分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指在函数返回后执行回调函数。上面的代码实际上是同步回调的实现方式,在process()函数返回之前,执行完回调函数methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付后不需等待回调接口被调用就直接返回。从应用场景看,同步回调看上去更像模板模式,异步回调更像观察者模式。
举例1:JdbcTemplate
spring提供很多template类,如JdbcTemplate RedisTemplate RestTemplate,尽管都叫xxxTemplate,但是并非基于模板模式实现,而是基于回调实现,确切的说是同步回调。而同步回调从应用场景上很像模板模式,所以,命名上用template作为后缀。
以JdbcTemplate 为例分析,java提供jdbc类库封装不同类型的数据库操作,不过直接用jdbc编写操作数据库的代码稍微复杂,如下面使用jdbc查询用户信息的代码。
public class JdbcDemo {
public User queryUser(long id){
Connection conn = null;
Statement stmt = null;
try{
//1.加载驱动
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo","");
//2. 创建statement类对象,用来执行SQL语句
stmt = conn.createStatement();
//3. resultSet类,用来存放获取的结果集
String sql = "select * from user where id="+id;
ResultSet resultSet = stmt.executeQuery(sql);
String eid = null,ename = null, price = null;
while(resultSet.next()){
User user = new User();
user.setId(resultSet.getLong("id"));
user.setName(resultSet.getString("name"));
user.setTelephone(resultSet.getString("telephone"));
return user;
}
}catch (ClassNotFoundException e){
//TODO log
}catch (SQLException e){
//todo log
}finally {
if (conn != null){
try {
conn.close();
}catch (SQLException e){
//todo log
}
}
if (stmt != null){
try {
stmt.close();
}catch (SQLException e){
//todo log
}
}
}
return null;
}
}
queryUser()方法包含很多流程性质的代码,和业务无关,如加载驱动、创建数据库连接、创建statement、关闭连接、关闭statement、处理异常。针对不同的SQL执行请求,这些流程性质的代码相同,可复用。spring提供JdbcTemplate进一步封装,简化数据库编程,使用JdbcTemplate查询用户信息,只需要编写跟这个业务有关的代码。包括查询用户的SQL语句、查询结果与User对象之间的映射关系。其他流程性质的代码都封装在JdbcTemplate类中,不需要每次都重新编写。重写后:
public class JdbcTemplateDemo {
private JdbcTemplate jdbcTemplate;
public User queryUser(long id){
String sql = "select * from user where id="+id;
return jdbcTemplate.query(sql,new UserRowMapper().get(0));
}
private class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setTelephone(rs.getString("telephone"));
return user;
}
}
}
而JdbcTemplate的底层如何实现的呢?JdbcTemplate通过回调机制,将不变的执行流程抽离出来,放到模板方法execute()中,将可变的部分设计为回调StatementCallback,由用户定制。query函数是对execute函数的二次封装,让接口用起来更方便。
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return (List)this.query((String)sql, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper)));
}
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (this.logger.isDebugEnabled()) {
this.logger.debug("Executing SQL query [" + sql + "]");
}
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
QueryStatementCallback() {
}
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
Object var4;
try {
rs = stmt.executeQuery(sql);
ResultSet rsToUse = rs;
if (JdbcTemplate.this.nativeJdbcExtractor != null) {
rsToUse = JdbcTemplate.this.nativeJdbcExtractor.getNativeResultSet(rs);
}
var4 = rse.extractData(rsToUse);
} finally {
JdbcUtils.closeResultSet(rs);
}
return var4;
}
public String getSql() {
return sql;
}
}
return this.execute((StatementCallback)(new QueryStatementCallback()));
}
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(this.getDataSource());
Statement stmt = null;
Object var7;
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null && this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
stmt = conToUse.createStatement();
this.applyStatementSettings(stmt);
Statement stmtToUse = stmt;
if (this.nativeJdbcExtractor != null) {
stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
}
T result = action.doInStatement(stmtToUse);
this.handleWarnings(stmt);
var7 = result;
} catch (SQLException var11) {
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, this.getDataSource());
con = null;
throw this.getExceptionTranslator().translate("StatementCallback", getSql(action), var11);
} finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, this.getDataSource());
}
return var7;
}
应用举例2:setClickListener()
在客户端开发中,经常给控件注册事件监听器,如下,在Android应用开发中,给button控件的点击事件注册监听器。
Button button = (Button)findViewById(R.id.button);
button.setOnclickListener(new OnclickListener(){
@Override
public void onclick(View v){
System.out.println("I am clicked.");
}
});
从代码结构上看,事件监听器很像回调,即传递一个包含回调函数onclick()的对象给另一个函数。从应用场景看,又像观察者模式,即先注册观察者onClickListener,当用户点击按钮,发送点击事件给观察者,并执行相应的onClick()函数。
回调分为同步回调和异步回调,这里的回调算是异步回调,往setOnClickListener()函数中注册好回调函数后,并不需要等待回调函数执行,也印证了异步回调比较像观察者模式。
应用举例3:addShutdownHook()
hook,钩子,有人觉得hook是callback的一种应用,callback更侧重语法机制的描述,hook更侧重应用场景的描述。hook比较经典的应用场景是tomcat和jvm的shutdown hook。以jvm举例,提供Runtime.addShutdownHook(Thread hook)方法,可注册一个jvm关闭的hook。当应用程序关闭时,jvm自动调用hook代码,示例:
public class ShutdownHookDemo {
private static class ShutdownHook extends Thread{
@Override
public void run() {
System.out.println("I am called during shutting down.");
}
}
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new ShutdownHook());
}
}
再看addShutdownHook()的代码实现,如下:
public class Runtime {
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
}
class ApplicationShutdownHooks {
static synchronized void add(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");
if (hook.isAlive())
throw new IllegalArgumentException("Hook already running");
if (hooks.containsKey(hook))
throw new IllegalArgumentException("Hook previously registered");
hooks.put(hook, hook);
}
/* Iterates over all application hooks creating a new thread for each
* to run in. Hooks are run concurrently and this method waits for
* them to finish.
*/
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
try {
hook.join();
} catch (InterruptedException x) { }
}
}
}
有关hook的逻辑被封装到ApplicationShutDownHooks类,当应用程序关闭时,jvm会调用该类的runHooks()方法,创建多个线程,并发执行多个hook。注册完hook后,并不需要等待hook的执行完成,也算是异步回调。
模板模式vs回调
从应用场景和代码实现的角度,对比模板模式和回调
应用场景看,同步回调和模板模式几乎一致,都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调和模板模式有很大区别,更像是观察者模式。
从代码实现上看,回调和模板模式完全不同,回调基于组合关系实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系实现,子类重写父类的抽象方法,是一种类之间的关系。
组合优于继承,在代码实现上,回调相对于模板模式更灵活。
- 像java这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
- 回调可使用匿名类创建回调对象,不用事先定义类;模板模式针对不同的实现都要定义不同的子类
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,即便只用到其中一个模板方法,子类必须实现所有的抽象方法;而回调更灵活,只需往用到的模板方法中注入回调对象即可。
callback更加灵活,适合算法逻辑较少的场景,如guava的Futures.addCallback回调onSuccess onFailure方法,而模板模式更适合复杂的场景,且子类可复用父类提供的方法。