设计模式之美笔记12 深碍√TFBOYSˉ_ 2022-11-30 04:18 173阅读 0赞 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 ### 文章目录 ### * * 观察者模式 * * 原理及应用场景剖析 * 基于不同应用场景的不同实现方式 * 异步非阻塞观察者模式的简单实现 * EventBus框架功能需求 * guava EventBus的类和方法 * 自己实现EventBus框架 * * 1. Subscribe * 2. ObserverAction * 3. ObserverRegistry * 4. EventBus * 5. AsyncEventBus * 模板模式 * * 模板模式的原理和实现 * 模板模式作用1:复用 * * 1. java InputStream * 2. java AbstractList * 模板模式作用2:扩展 * * 1. java servlet * 2. JUnit TestCase * 回调的原理解析 * * 举例1:JdbcTemplate * 应用举例2:setClickListener() * 应用举例3:addShutdownHook() * 模板模式vs回调 之前学习创建型模式,主要解决“对象的创建”问题,和结构型模式,解决“类或对象的组合或组装”问题,接下来学习行为型模式,解决“类或对象的交互”问题。 ## 观察者模式 ## ### 原理及应用场景剖析 ### 观察者模式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()的实现原理。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70_pic_center] ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70_pic_center 1] 从图中可看出,最关键的一个数据结构是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方法,而模板模式更适合复杂的场景,且子类可复用父类提供的方法。 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70_pic_center]: /images/20221124/b72a411462fd4eb2832da12111192912.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70_pic_center 1]: /images/20221124/a553e71427b34f1cbe0fc724ab2098d8.png
相关 设计模式之美笔记16 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 解释器模式 解释器模式的原理和实现 深藏阁楼爱情的钟/ 2022年12月01日 11:53/ 0 赞/ 136 阅读
相关 设计模式之美笔记15 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 访问者模式 访问者模式的诞生 我就是我/ 2022年12月01日 05:16/ 0 赞/ 142 阅读
相关 设计模式之美笔记14 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 状态模式 背景 什么 水深无声/ 2022年11月30日 15:51/ 0 赞/ 146 阅读
相关 设计模式之美笔记13 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 策略模式 策略模式的原理和实现 忘是亡心i/ 2022年11月30日 12:27/ 0 赞/ 145 阅读
相关 设计模式之美笔记12 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 观察者模式 原理及应用场景剖析 深碍√TFBOYSˉ_/ 2022年11月30日 04:18/ 0 赞/ 174 阅读
相关 设计模式之美笔记11 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 门面模式 门面模式的原理和实现 ゞ 浴缸里的玫瑰/ 2022年11月28日 13:41/ 0 赞/ 155 阅读
相关 设计模式之美笔记10 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 序言 代理模式 桥接模式 柔情只为你懂/ 2022年11月28日 10:36/ 0 赞/ 149 阅读
相关 设计模式之美笔记9 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 工厂模式 1. 简单工厂 待我称王封你为后i/ 2022年11月28日 00:41/ 0 赞/ 148 阅读
相关 设计模式之美笔记8 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 单例模式 1. 为什么要使用单例 柔光的暖阳◎/ 2022年11月26日 07:52/ 0 赞/ 153 阅读
相关 设计模式之美笔记7 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 实战1:id生成器的重构 1. 需求背景 女爷i/ 2022年11月25日 13:19/ 0 赞/ 186 阅读
还没有评论,来说两句吧...