【设计模式】结合Tomcat源码,分析外观模式/门面模式的特性和应用场景
导航:
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
目录
一、经典的组建家庭影院流程
二、传统方式解决影院管理
2.1 实现方案:客户端直接调用各流程
2.2 优缺点和改进思路
三、外观模式
3.1 外观模式/门面模式
3.2 外观类、子系统和客户端
3.3 优缺点和适用场景
四、外观模式解决影院管理
4.1 实现方案:将各流程组装到外观类
4.2 核心代码
4.2.1 子系统:投影仪、DVD等类
4.2.2 外观类:家庭影院外观类
4.2.3 客户端:家庭影院的准备、观看、结束
五、外观模式在三层架构中的应用
5.1 三层架构和MVC设计模式
5.1.1 三层架构
5.1.2 MVC设计模式
5.2 外观模式在三层架构中的应用
六、外观模式在Tomcat源码中的应用
6.1 外观类:ApplicationContextFacade
6.2 子系统:Map
6.3 客户端:ApplicationContext
6.4 Tomcat和Spring的ApplicationContext区别
一、经典的组建家庭影院流程
问题描述:
- 组建家庭影院过程:
- 直接用遥控器:统筹各设备开关
- 开爆米花机
- 放下屏幕
- 开投影仪
- 开音响
- 开DVD,选dvd
- 去拿爆米花
- 调暗灯光
- 播放
- 观影结束后,关闭各种设备
- 需求:组建一个电影院,要求完成上述流程
二、传统方式解决影院管理
2.1 实现方案:客户端直接调用各流程
各个电器类(子系统):包括打开、关闭等功能
/**
* @Author: vince
* @CreateTime: 2024/10/23
* @Description: DVD类
* @Version: 1.0
*/
public class DVDPlayer{
/**
* 播放
*/
public void play(){
System.out.println("播放DVD");
}
/**
* 打开
*/
public void on(){
System.out.println("打开DVD");
}
/**
* 关闭
*/
public void off(){
System.out.println("关闭DVD");
}
}
/**
* @Author: vince
* @CreateTime: 2024/10/23
* @Description: 投影仪
* @Version: 1.0
*/
public class Projector{
public void on() {
System.out.println("打开投影仪");
}
}
// ...
客户端:
/**
* @Author: vince
* @CreateTime: 2024/10/23
* @Description: 客户端测试类
* @Version: 1.0
*/
public class ClientTest{
public static void main(String[] args){
// 1、创建对象:创建各电器类对象
// 2、打开电器:调用各对象的on()方法
// 3、放映:调用DVDPlayer对象的play方法
// 4.关闭:关闭所有电器
}
}
2.2 优缺点和改进思路
优点:
- 简单:比较好理解,简单易操作
缺点:
- 调用过程混乱:客户端创建各个子系统的对象,并直接去调用子系统(对象)相关方法,会造成调用过程混乱,没有清晰的过程
- 无法直接维护子系统:不利于在 ClientTest 中去维护对子系统的操作
改进思路分析:
- 抽取界面类:定义一个高层接口(界面类),给子系统接口提供一致的界面(例如ready,play,pause,end等方法),屏蔽内部子系统的细节。
三、外观模式
3.1 外观模式/门面模式
外观模式(Facade):也叫过程模式/门面模式,是一种结构型设计模式。外观模式通常创建一个外观类,将子系统类的多个流程方法组装起来。
外观模式通过封装复杂的子系统接口,提供一个简化的统一接口,减少了客户端与子系统的直接依赖,降低了耦合性。
UML 类图
结构型模式(Structural Patterns):
主要用于描述对象之间的组合关系。包括“代理模式”、“适配器模式”、“桥接模式”、“装饰者模式”、“外观模式”、“享元模式”和“组合模式”等。这些模式可以帮助我们更好地设计程序结构,提高代码的灵活性和可维护性。
外观模式将多个子系统通过界面接口统一访问,改变了各个子系统与客户端之间的组合关系,所以是结构型设计模式。
3.2 外观类、子系统和客户端
外观模式的三个角色:
- 外观类(Facade):提供统一的界面类,组装子系统的各个流程。例如软件安装外观类有个“一键安装”方法,组装了选择安装目录、选择组件、开启开机自启动等子流程。
- 子系统:各功能的实际执行者。例如系统服务类设置开机自启动。
- 客户端(Client):调用者。客户端通过调用外观类提供的简化接口,与各子系统交互,从而实现功能。例如点击外观对象的“一键安装”方法,一键安装软件。
举例:
- 一键安装:在 PC 上安装软件的时候经常有一键安装选项,省去选择安装目录、安装的组件、选择开机自启动、选择是否生成快捷方式等步骤
- 重启手机:手机的关机选项,就是把关机和启动组合为一个操作
3.3 优缺点和适用场景
优点:
- 简化用户操作:将具体的各个流程组装到外观类中,这样客户端调用时,直接调用外观类的方法即可。
- 高内聚低耦合:将子系统的实现细节隐藏在外观类之后,客户端只与外观类交互,降低了系统各部分之间的耦合性。
- 分层结构:外观模式可以作为上层系统与下层子系统交互的中间层,简化每一层之间的依赖。
- 性能高:将各个子系统流程的分别调用,改为外观类一次性调用,减少网络通信成本,提高 了客户端的响应速度。
- 易于维护:当需要修改流程时,只需要维护外观类的方法,调换各个流程,不需要关注各个子系统的具体实现。
缺点:
- 掩盖子系统复杂性:将子系统的实现细节隐藏在外观类之后,开发者可能对各个子系统的具体实现细节了解不够深刻。
- 过度设计:一些场景子系统并不多,而且流程简单,未来也不需要扩展新的流程,用外观模式就会有些过度设计。这也是众多设计模式的缺点,需要充分考虑适用场景
使用场景:
- 子系统各流程复杂:当子系统很多时,为了防止遗留流程,并且方便管理各个流程的前后顺序,可以使用外观模式
- 兼容旧系统功能:在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个 Facade 类,来提供遗留系统的比较清晰简单的接口,让新系统与 Facade 类交互,提高复用性
- 三层架构:Java项目中,我们常常将Dao注入到Service,Service将多个Dao组合在一起,这其实也用到了外观模式。
四、外观模式解决影院管理
4.1 实现方案:将各流程组装到外观类
4.2 核心代码
4.2.1 子系统:投影仪、DVD等类
子系统类主要包括投影仪、DVD等类,拥有打开、关闭等功能(普通方法) :
投影仪类:
/**
* @Author: vince
* @CreateTime: 2024/10/23
* @Description: 投影仪
* @Version: 1.0
*/
public class Projector {
private static Projector projector = new Projector();
public static Projector getInstance() {
return projector;
}
public void on() {
System.out.println("打开投影仪...");
}
public void off() {
System.out.println("关闭投影仪...");
}
public void focus() {
System.out.println("投影仪聚焦...");
}
public void zoom() {
System.out.println("投影仪放大...");
}
}
DVD 播放器类:
/**
* @Author: vince
* @CreateTime: 2024/10/23
* @Description: DVD类
* @Version: 1.0
*/
public class DVDPlayer {
private static DVDPlayer player = new DVDPlayer();
public static DVDPlayer getInstance() {
return player;
}
public void on() {
System.out.println("打开DVD播放器...");
}
public void off() {
System.out.println("关闭DVD播放器...");
}
public void play() {
System.out.println("播放DVD播放器...");
}
public void pause() {
System.out.println("暂停DVD播放器...");
}
public void setDvd(String dvd) {
System.out.println("选dvd:" + dvd + "...");
}
}
荧幕
/**
* @Author: vince
* @CreateTime: 2024/10/24
* @Description: 荧幕类
* @Version: 1.0
*/
public class Screen {
private static Screen screen = new Screen();
public static Screen getInstance() {
return screen;
}
public void up() {
System.out.println("升起荧幕...");
}
public void down() {
System.out.println("拉下荧幕...");
}
}
音响
/**
* @Author: vince
* @CreateTime: 2024/10/24
* @Description: 音响类
* @Version: 1.0
*/
public class Stereo {
private static Stereo stereo = new Stereo();
public static Stereo getInstance() {
return stereo;
}
public void on() {
System.out.println("打开立体声...");
}
public void off() {
System.out.println("关闭立体声...");
}
public void setVolume(Integer volume) {
System.out.println("立体声音量+" + volume + "...");
}
}
灯光
/**
* @Author: vince
* @CreateTime: 2024/10/24
* @Description: 灯光类
* @Version: 1.0
*/
public class TheaterLights {
private static TheaterLights lights = new TheaterLights();
public static TheaterLights getInstance() {
return lights;
}
public void on() {
System.out.println("打开灯光...");
}
public void off() {
System.out.println("关闭灯光...");
}
public void dim() {
System.out.println("调暗灯光...");
}
public void bright() {
System.out.println("调亮灯光...");
}
}
爆米花机器
/**
* @Author: vince
* @CreateTime: 2024/10/24
* @Description: 爆米花机
* @Version: 1.0
*/
public class Popcorn {
private static Popcorn popcorn = new Popcorn();
public static Popcorn getInstance() {
return popcorn;
}
public void on() {
System.out.println("打开爆米花机器...");
}
public void off() {
System.out.println("关闭爆米花机器...");
}
public void pop() {
System.out.println("取出爆米花...");
}
}
4.2.2 外观类:家庭影院外观类
外观类包括一个或多个组装方法,将各个流程(子系统类的方法)组装起来。
家庭影院 Facade
/**
* @Author: vince
* @CreateTime: 2024/10/24
* @Description: 外观类:家庭影院外观类,包括准备、观看、暂停、结束等大流程
* @Version: 1.0
*/
public class HomeTheaterFacade {
private Popcorn popcorn;
private Screen screen;
private Stereo stereo;
private TheaterLights lights;
private Projector projector;
private DVDPlayer player;
public HomeTheaterFacade() {
this.popcorn = Popcorn.getInstance();
this.screen = Screen.getInstance();
this.stereo = Stereo.getInstance();
this.lights = TheaterLights.getInstance();
this.projector = Projector.getInstance();
this.player = DVDPlayer.getInstance();
}
/**
* 准备看电影:调用开灯、放屏幕、开投影仪、调暗灯等流程
*/
public void ready() {
// 打开灯光
lights.on();
// 开爆米花机
popcorn.on();
// 放下屏幕
screen.down();
// 开投影仪,聚焦、放大
projector.on();
projector.focus();
projector.zoom();
// 开音响,设置音量
stereo.on();
stereo.setVolume(8);
// 开DVD,选dvd
player.on();
player.setDvd("坦塔尼克号");
// 取爆米花,关闭爆米花机器
popcorn.pop();
popcorn.off();
// 调暗灯光
lights.dim();
}
/**
* 看电影
*/
public void play() {
player.play();
}
/**
* 暂停电影
*/
public void pause() {
player.pause();
}
/**
* 关闭电影:调用关投影仪、关音响、开灯、收屏幕等流程
*/
public void end() {
player.off();
projector.off();
stereo.off();
lights.bright();
screen.up();
}
}
4.2.3 客户端:家庭影院的准备、观看、结束
客户端类创建外观类对象,调用各个组装方法。
/**
* @Author: vince
* @CreateTime: 2024/10/24
* @Description: 客户端:调用影院外观类的准备、观看、结束等流程
* @Version: 1.0
*/
public class ClientTest {
public static void main(String[] args) throws InterruptedException {
HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade();
System.out.println("===========家庭影院初始化============");
homeTheaterFacade.ready();
System.out.println("===========家庭影院沉浸式播放============");
homeTheaterFacade.play();
Thread.sleep(1000);
System.out.println("===========家庭影院暂停============");
homeTheaterFacade.pause();
Thread.sleep(1000);
System.out.println("===========家庭影院沉浸式播放============");
homeTheaterFacade.play();
Thread.sleep(1000);
System.out.println("===========家庭影院结束============");
homeTheaterFacade.end();
}
}
五、外观模式在三层架构中的应用
5.1 三层架构和MVC设计模式
5.1.1 三层架构
开发过程中,我们把后端服务器Servlet拆分成三层,分别是web、service和dao,这也是程序员常提到的“Java味”:
- web层(表现层):直接与用户交互,负责接收用户输入和呈现数据
- service层(业务层):处理具体的业务逻辑。也称为服务层或应用层。
- dao层(数据访问层):负责与数据库交互,执行数据的增删改查
优点:
- 低耦合:各层的职责明确,页面交互、业务逻辑、数据库操作三层分离,降低了系统模块间的耦合。并且各个类功能一目了然,例如OrderController可以直接看出它是控制器。
- 易维护:每层的功能独立,业务逻辑更改后只需修改Service,数据库更改后只需修改dao。
- 可扩展:当增加新功能后,可以在各层扩展相关功能的类。
5.1.2 MVC设计模式
MVC设计模式:将后端Servlet设计为控制器controller、视图view、业务模型Model。
- 视图(View):显示UI页面数据,并与用户交互。前后端分离项目中视图就是前端代码,一体化项目中视图是JSP、Thymeleaf 等框架渲染成的HTML。
- 控制器(Controller):负责接收浏览器发送过来的请求,然后响应给浏览器
- 模型(Model):封装后端业务逻辑和应用的核心数据,与数据层交互。
流程:
- 控制器(例如serlvlet)用来接收浏览器发送过来的请求
- 控制器调用模型(例如JavaBean)来获取数据,比如从数据库查询数据;
- 控制器获取到数据后再交由视图(例如JSP)进行数据展示。
优点:
- 低耦合:将数据、UI 和控制逻辑分离,便于开发、维护和扩展。
- 可扩展性:将逻辑处理和视图渲染分开,各个小组件能直接复用。
5.2 外观模式在三层架构中的应用
外观模式像模板模式一样,都是很通用的设计模式,不只是JDK和常用框架的源码,我们开发过程中也会经常用到他们。
例如我们写Service时候,将Dao注入到Service,然后调用Dao的各个方法,这其实也用到外观模式的思想。
下面订单Service,注入了订单dao、商品dao和客户dao,然后对流程进行组装
/**
* @Author: vince
* @CreateTime: 2024/10/29
* @Description: 订单业务实现
* @Version: 1.0
*/
@Service
public class OrderServiceImpl implements OrderService{
/**
* 订单dao
*/
@Autowired
private OrderDao orderDao;
/**
* 商品dao
*/
@Autowired
private ProductDao productDao;
/**
* 客户dao
*/
@Autowired
private CustomerDao customerDao;
public Order placeOrder(Customer customer, Product product) {
// 外观模式思想:调用多个DAO方法,完成下单业务逻辑
// 1.保存客户信息
customerDao.save(customer);
// 2.更新商品库存
productDao.updateStock(product);
Order order = new Order(customer, product);
// 3.保存订单
orderDao.save(order);
return order;
}
}
六、外观模式在Tomcat源码中的应用
Tomcat源码使用了外观模式,以ApplicationContextFacade为例
6.1 外观类:ApplicationContextFacade
ApplicationContextFacade 是 Apache Tomcat 框架中的外观类,位于 org.apache.catalina.core 包下。是 ApplicationContext 的代理接口,主要用于屏蔽 ApplicationContext 的复杂内部实现。
其中,initClassCache()方法用于初始化classCache变量:
package org.apache.catalina.core;
/**
* 外观类:用于屏蔽 ApplicationContext 的复杂内部实现。
*
* @author Remy Maucherat
*/
public class ApplicationContextFacade implements ServletContext {
// ---------------------------------------------------------- Attributes
/**
* 缓存类对象:用于后续通过反射获取类的信息
*/
private final Map<String, Class<?>[]> classCache;
/**
* 缓存方法对象
*/
private final Map<String, Method> objectCache;
// ----------------------------------------------------------- Constructors
/**
* 构造方法:构造一个新对象实例,并初始化类对象缓存
* @param context The associated Context instance
*/
public ApplicationContextFacade(ApplicationContext context) {
super();
this.context = context;
classCache = new HashMap<>();
objectCache = new ConcurrentHashMap<>();
initClassCache();
}
/**
* 初始化类缓存:将ApplicationContext各方法加入类缓存中,value统一初始化成String类对象
*/
private void initClassCache() {
// 1.创建String的类对象
Class<?>[] clazz = new Class[] { String.class };
// 2.将该类对象作为value,key是ApplicationContext各方法,统一存到类缓存中
// 获取上下文
classCache.put("getContext", clazz);
// 获取 MIME 类型
classCache.put("getMimeType", clazz);
// 获取资源路径
classCache.put("getResourcePaths", clazz);
// 获取资源
classCache.put("getResource", clazz);
// 获取资源的输入流
classCache.put("getResourceAsStream", clazz);
// 获取请求分发器
classCache.put("getRequestDispatcher", clazz);
// 获取指定名称的分发器
classCache.put("getNamedDispatcher", clazz);
// 获取 Servlet
classCache.put("getServlet", clazz);
// 设置初始化参数
classCache.put("setInitParameter", new Class[] { String.class, String.class });
// 创建 Servlet 实例
classCache.put("createServlet", new Class[] { Class.class });
// 添加 Servlet
classCache.put("addServlet", new Class[] { String.class, String.class });
// 创建过滤器实例
classCache.put("createFilter", new Class[] { Class.class });
// 添加过滤器
classCache.put("addFilter", new Class[] { String.class, String.class });
// 创建监听器
classCache.put("createListener", new Class[] { Class.class });
// 添加监听器
classCache.put("addListener", clazz);
// 获取过滤器注册信息
classCache.put("getFilterRegistration", clazz);
// 获取 Servlet 注册信息
classCache.put("getServletRegistration", clazz);
// 获取初始化参数
classCache.put("getInitParameter", clazz);
// 设置属性
classCache.put("setAttribute", new Class[] { String.class, Object.class });
// 移除属性
classCache.put("removeAttribute", clazz);
// 获取资源的真实路径
classCache.put("getRealPath", clazz);
// 获取属性
classCache.put("getAttribute", clazz);
// 记录日志
classCache.put("log", clazz);
// 设置会话跟踪模式
classCache.put("setSessionTrackingModes", new Class[] { Set.class });
// 添加 JSP 文件
classCache.put("addJspFile", new Class[] { String.class, String.class });
// 声明角色
classCache.put("declareRoles", new Class[] { String[].class });
// 设置会话超时时间
classCache.put("setSessionTimeout", new Class[] { int.class });
// 设置请求字符编码
classCache.put("setRequestCharacterEncoding", new Class[] { String.class });
// 设置响应字符编码
classCache.put("setResponseCharacterEncoding", new Class[] { String.class });
}
// ...
}
6.2 子系统:Map
子系统是Map类,外观类创建了Map对象classCache作为变量,存储各个类的类对象,之后通过反射获取这些类的信息
/**
* 外观类:用于屏蔽 ApplicationContext 的复杂内部实现。
*
* @author Remy Maucherat
*/
public class ApplicationContextFacade implements ServletContext {
// ---------------------------------------------------------- Attributes
/**
* 子系统:类缓存对象:用于后续通过反射获取类的信息
*/
private final Map<String, Class<?>[]> classCache;
// ...
}
6.3 客户端:ApplicationContext
ApplicationContext 是 Apache Tomcat 中的一个核心类,主要功能是表示一个 Web 应用程序的执行环境。它实现了 ServletContext 接口,负责获取响应字符编码、添加过滤器等Servlet 基础功能。
package org.apache.catalina.core;
/**
* 应用上下文类:ServletContext的标准实现,用于获取响应字符编码、添加过滤器等Servlet基础功能
*
* @author Craig R. McClanahan
* @author Remy Maucherat
*/
public class ApplicationContext implements ServletContext {
/**
* The facade around this object.
*/
private final ServletContext facade = new org.apache.catalina.core.ApplicationContextFacade(this);
/**
* 获取上下文
* @return {@link StandardContext }
*/
protected StandardContext getContext() {
return this.context;
}
/**
* 获取文件类型
*
* @param file 文件名
*/
@Override
public String getMimeType(String file) {
if (file == null) {
return null;
}
int period = file.lastIndexOf('.');
if (period < 0) {
return null;
}
String extension = file.substring(period + 1);
if (extension.length() < 1) {
return null;
}
return context.findMimeMapping(extension);
}
// ...其他方法
}
6.4 Tomcat和Spring的ApplicationContext区别
相同点: 两者都是应用程序上下文,用于管理和获取容器的环境信息。
不同点:
- Tomcat的ApplicationContext:Web容器相关的应用上下文,用于获取和管理Web容器的环境信息;
- Spring的ApplicationContext:Spring的IOC容器相关的应用上下文,用于获取和管理IOC容器的环境信息;
IOC容器:
- IOC控制反转思想:创建对象的控制权由内部(即new实例化)反转到外部(即IOC容器)。
- Bean:IOC容器中存放的一个个对象
- DI依赖注入:绑定IOC容器中bean与bean之间的依赖关系。例如将dao层对象注入到service层对象。
使用示例:
@SpringBootTest
class DemoApplicationTests {
@Autowired
private ApplicationContext applicationContext;
/**
* web容器:ApplicationContext是ServletContext接口的实现类
*/
@Autowired
private ServletContext servletContext;
/**
* 获取所有Bean
*/
@Test
void contextLoads() {
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println(beanDefinitionName);
}
}
/**
* 获取所有web容器的属性
*/
@Test
void servletTest() {
// 获取所有web容器的属性
Enumeration<String> attributeNames = servletContext.getAttributeNames();
while (attributeNames.hasMoreElements()) {
String name = attributeNames.nextElement();
System.out.println(name);
}
}
}