Tomcat 架构原理解析到架构设计借鉴(下)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
日志服务 SLS,月写入数据量 50GB 1个月
全局流量管理 GTM,标准版 1个月
简介: 接上文。

整体架构设计解析收获总结


通过前面对 Tomcat 整体架构的学习,知道了 Tomcat 有哪些核心组件,组件之间的关系。以及 Tomcat 是怎么处理一个 HTTP 请求的。下面我们通过一张简化的类图来回顾一下,从图上你可以看到各种组件的层次关系,图中的虚线表示一个请求在 Tomcat 中流转的过程。


image.png


连接器


Tomcat 的整体架构包含了两个核心组件连接器和容器。连接器负责对外交流,容器负责内部处理。连接器用 ProtocolHandler接口来封装通信协议和 I/O模型的差异,ProtocolHandler内部又分为 EndPointProcessor模块,EndPoint负责底层 Socket通信,Proccesor负责应用层协议解析。连接器通过适配器 Adapter调用容器。


对 Tomcat 整体架构的学习,我们可以得到一些设计复杂系统的基本思路。首先要分析需求,根据高内聚低耦合的原则确定子模块,然后找出子模块中的变化点和不变点,用接口和抽象基类去封装不变点,在抽象基类中定义模板方法,让子类自行实现抽象方法,也就是具体子类去实现变化点。


容器


运用了组合模式 管理容器、通过 观察者模式 发布启动事件达到解耦、开闭原则。骨架抽象类和模板方法抽象变与不变,变化的交给子类实现,从而实现代码复用,以及灵活的拓展。使用责任链的方式处理请求,比如记录日志等。


类加载器


Tomcat 的自定义类加载器 WebAppClassLoader为了隔离 Web 应用打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。防止 Web 应用自己的类覆盖 JRE 的核心类,使用 ExtClassLoader 去加载,这样即打破了双亲委派,又能安全加载。


如何阅读源码持续学习


学习是一个反人类的过程,是比较痛苦的。尤其学习我们常用的优秀技术框架本身比较庞大,设计比较复杂,在学习初期很容易遇到 “挫折感”,debug 跳来跳去陷入恐怖细节之中无法自拔,往往就会放弃。


找到适合自己的学习方法非常重要,同样关键的是要保持学习的兴趣和动力,并且得到学习反馈效果


学习优秀源码,我们收获的就是架构设计能力,遇到复杂需求我们学习到可以利用合理模式与组件抽象设计了可拓展性强的代码能力。


如何阅读


比如我最初在学习 Spring 框架的时候,一开始就钻进某个模块啃起来。然而由于 Spring 太庞大,模块之间也有联系,根本不明白为啥要这么写,只觉得为啥设计这么 “绕”。


错误方式


  • 陷入细节,不看全局:我还没弄清楚森林长啥样,就盯着叶子看 ,看不到全貌和整体设计思路。所以阅读源码学习的时候不要一开始就进入细节,而是宏观看待整体架构设计思想,模块之间的关系。


  • 还没学会用就研究如何设计:首先基本上框架都运用了设计模式,我们最起码也要了解常用的设计模式,即使是“背”,也得了然于胸。在学习一门技术,我推荐先看官方文档,看看有哪些模块、整体设计思想。然后下载示例跑一遍,最后才是看源码。


  • 看源码深究细节:到了看具体某个模块源码的时候也要下意识的不要去深入细节,重要的是学习设计思路,而不是具体一个方法实现逻辑。除非自己要基于源码做二次开发。


正确方式


  • 定焦原则:抓主线(抓住一个核心流程去分析,不要漫无目的的到处阅读)。


  • 宏观思维:从全局的视角去看待,上帝视角理出主要核心架构设计,先森林后树叶。切勿不要试图去搞明白每一行代码。


  • 断点:合理运用调用栈(观察调用过程上下文)。


带着目标去学


比如某些知识点是面试的热点,那学习目标就是彻底理解和掌握它,当被问到相关问题时,你的回答能够使得面试官对你刮目相看,有时候往往凭着某一个亮点就能影响最后的录用结果。


又或者接到一个稍微复杂的需求,学习从优秀源码中借鉴设计思路与优化技巧。


最后就是动手实践,将所学运用在工作项目中。只有动手实践才会让我们对技术有最直观的感受。有时候我们听别人讲经验和理论,感觉似乎懂了,但是过一段时间便又忘记了。


实际场景运用


简单的分析了 Tomcat 整体架构设计,从 【连接器】 到 【容器】,并且分别细说了一些组件的设计思想以及设计模式。接下来就是如何学以致用,借鉴优雅的设计运用到实际工作开发中。学习,从模仿开始。


责任链模式


在工作中,有这么一个需求,用户可以输入一些信息并可以选择查验该企业的 【工商信息】、【司法信息】、【中登情况】等如下如所示的一个或者多个模块,而且模块之间还有一些公共的东西是要各个模块复用。


这里就像一个请求,会被多个模块去处理。所以每个查询模块我们可以抽象为 处理阀门,使用一个 List 将这些 阀门保存起来,这样新增模块我们只需要新增一个阀门即可,实现了开闭原则同时将一堆查验的代码解耦到不同的具体阀门中,使用抽象类提取 “不变的”功能。


image.png


具体示例代码如下所示:


首先抽象我们的处理阀门, NetCheckDTO是请求信息


/**
 * 责任链模式:处理每个模块阀门
 */
public interface Valve {
    /**
     * 调用
     * @param netCheckDTO
     */
    void invoke(NetCheckDTO netCheckDTO);
}


定义抽象基类,复用代码。


public abstract class AbstractCheckValve implements Valve {
    public final AnalysisReportLogDO getLatestHistoryData(NetCheckDTO netCheckDTO, NetCheckDataTypeEnum checkDataTypeEnum){
        // 获取历史记录,省略代码逻辑
    }
    // 获取查验数据源配置
    public final String getModuleSource(String querySource, ModuleEnum moduleEnum){
       // 省略代码逻辑
    }
}

定义具体每个模块处理的业务逻辑,比如 【百度负面新闻】对应的处理

@Slf4j
@Service
public class BaiduNegativeValve extends AbstractCheckValve {
    @Override
    public void invoke(NetCheckDTO netCheckDTO) {
    }
}


最后就是管理用户选择要查验的模块,我们通过 List 保存。用于触发所需要的查验模块


@Slf4j
@Service
public class NetCheckService {
    // 注入所有的阀门
    @Autowired
    private Map<String, Valve> valveMap;
    /**
     * 发送查验请求
     *
     * @param netCheckDTO
     */
    @Async("asyncExecutor")
    public void sendCheckRequest(NetCheckDTO netCheckDTO) {
        // 用于保存客户选择处理的模块阀门
        List<Valve> valves = new ArrayList<>();
        CheckModuleConfigDTO checkModuleConfig = netCheckDTO.getCheckModuleConfig();
        // 将用户选择查验的模块添加到 阀门链条中
        if (checkModuleConfig.getBaiduNegative()) {
            valves.add(valveMap.get("baiduNegativeValve"));
        }
        // 省略部分代码.......
        if (CollectionUtils.isEmpty(valves)) {
            log.info("网查查验模块为空,没有需要查验的任务");
            return;
        }
        // 触发处理
        valves.forEach(valve -> valve.invoke(netCheckDTO));
    }
}


模板方法模式


需求是这样的,可根据客户录入的财报 excel 数据或者企业名称执行财报分析。


对于非上市的则解析 excel -> 校验数据是否合法->执行计算。


上市企业:判断名称是否存在 ,不存在则发送邮件并中止计算-> 从数据库拉取财报数据,初始化查验日志、生成一条报告记录,触发计算-> 根据失败与成功修改任务状态 。


image.png


重要的 ”变“ 与 ”不变“,


  • 不变的是整个流程是初始化查验日志、初始化一条报告前期校验数据(若是上市公司校验不通过还需要构建邮件数据并发送)、从不同来源拉取财报数据并且适配通用数据、然后触发计算,任务异常与成功都需要修改状态。


  • 变化的是上市与非上市校验规则不一样,获取财报数据方式不一样,两种方式的财报数据需要适配


整个算法流程是固定的模板,但是需要将算法内部变化的部分具体实现延迟到不同子类实现,这正是模板方法模式的最佳场景。


public abstract class AbstractAnalysisTemplate {
    /**
     * 提交财报分析模板方法,定义骨架流程
     * @param reportAnalysisRequest
     * @return
     */
    public final FinancialAnalysisResultDTO doProcess(FinancialReportAnalysisRequest reportAnalysisRequest) {
        FinancialAnalysisResultDTO analysisDTO = new FinancialAnalysisResultDTO();
    // 抽象方法:提交查验的合法校验
        boolean prepareValidate = prepareValidate(reportAnalysisRequest, analysisDTO);
        log.info("prepareValidate 校验结果 = {} ", prepareValidate);
        if (!prepareValidate) {
      // 抽象方法:构建通知邮件所需要的数据
            buildEmailData(analysisDTO);
            log.info("构建邮件信息,data = {}", JSON.toJSONString(analysisDTO));
            return analysisDTO;
        }
        String reportNo = FINANCIAL_REPORT_NO_PREFIX + reportAnalysisRequest.getUserId() + SerialNumGenerator.getFixLenthSerialNumber();
        // 生成分析日志
        initFinancialAnalysisLog(reportAnalysisRequest, reportNo);
    // 生成分析记录
        initAnalysisReport(reportAnalysisRequest, reportNo);
        try {
            // 抽象方法:拉取财报数据,不同子类实现
            FinancialDataDTO financialData = pullFinancialData(reportAnalysisRequest);
            log.info("拉取财报数据完成, 准备执行计算");
            // 测算指标
            financialCalcContext.calc(reportAnalysisRequest, financialData, reportNo);
      // 设置分析日志为成功
            successCalc(reportNo);
        } catch (Exception e) {
            log.error("财报计算子任务出现异常", e);
      // 设置分析日志失败
            failCalc(reportNo);
            throw e;
        }
        return analysisDTO;
    }
}


最后新建两个子类继承该模板,并实现抽象方法。这样就将上市与非上市两种类型的处理逻辑解耦,同时又复用了代码。


策略模式


需求是这样,要做一个万能识别银行流水的 excel 接口,假设标准流水包含【交易时间、收入、支出、交易余额、付款人账号、付款人名字、收款人名称、收款人账号】等字段。

现在我们解析出来每个必要字段所在 excel 表头的下标。但是流水有多种情况:


  1. 一种就是包含所有标准字段。


  1. 收入、支出下标是同一列,通过正负来区分收入与支出。


  1. 收入与支出是同一列,有一个交易类型的字段来区分。


  1. 特殊银行的特殊处理。


也就是我们要根据解析对应的下标找到对应的处理逻辑算法,我们可能在一个方法里面写超多 if else 的代码,整个流水处理都偶合在一起,假如未来再来一种新的流水类型,还要继续改老代码。最后可能出现 “又臭又长,难以维护” 的代码复杂度。


这个时候我们可以用到策略模式将不同模板的流水使用不同的处理器处理,根据模板找到对应的策略算法去处理。即使未来再加一种类型,我们只要新加一种处理器即可,高内聚低耦合,且可拓展。


image.png


定义处理器接口,不同处理器去实现处理逻辑。将所有的处理器注入到 BankFlowDataHandlerdata_processor_map中,根据不同的场景取出对已经的处理器处理流水。


public interface DataProcessor {
    /**
     * 处理流水数据
     * @param bankFlowTemplateDO 流水下标数据
     * @param row
     * @return
     */
    BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO, List<String> row);
    /**
     * 是否支持处理该模板,不同类型的流水策略根据模板数据判断是否支持解析
     * @return
     */
    boolean isSupport(BankFlowTemplateDO bankFlowTemplateDO);
}
// 处理器的上下文
@Service
@Slf4j
public class BankFlowDataContext {
    // 将所有处理器注入到 map 中
    @Autowired
    private List<DataProcessor> processors;
    // 找对对应的处理器处理流水
    public void process() {
         DataProcessor processor = getProcessor(bankFlowTemplateDO);
         for(DataProcessor processor : processors) {
           if (processor.isSupport(bankFlowTemplateDO)) {
             // row 就是一行流水数据
             processor.doProcess(bankFlowTemplateDO, row);
             break;
           }
         }
    }
}


定义默认处理器,处理正常模板,新增模板只要新增处理器实现 DataProcessor即可。


/**
 * 默认处理器:正对规范流水模板
 *
 */
@Component("defaultDataProcessor")
@Slf4j
public class DefaultDataProcessor implements DataProcessor {
    @Override
    public BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO) {
        // 省略处理逻辑细节
        return bankTransactionFlowDO;
    }
    @Override
    public String strategy(BankFlowTemplateDO bankFlowTemplateDO) {
      // 省略判断是否支持解析该流水
      boolean isDefault = true;
      return isDefault;
    }
}


通过策略模式,我们将不同处理逻辑分配到不同的处理类中,这样完全解耦,便于拓展。



相关文章
|
8天前
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
43 6
|
8天前
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
25 1
|
9天前
|
Kubernetes Cloud Native 云计算
云原生技术深度解析:重塑企业IT架构的未来####
本文深入探讨了云原生技术的核心理念、关键技术组件及其对企业IT架构转型的深远影响。通过剖析Kubernetes、微服务、容器化等核心技术,本文揭示了云原生如何提升应用的灵活性、可扩展性和可维护性,助力企业在数字化转型中保持领先地位。 ####
|
10天前
|
运维 Kubernetes Cloud Native
Kubernetes云原生架构深度解析与实践指南####
本文深入探讨了Kubernetes作为领先的云原生应用编排平台,其设计理念、核心组件及高级特性。通过剖析Kubernetes的工作原理,结合具体案例分析,为读者呈现如何在实际项目中高效部署、管理和扩展容器化应用的策略与技巧。文章还涵盖了服务发现、负载均衡、配置管理、自动化伸缩等关键议题,旨在帮助开发者和运维人员掌握利用Kubernetes构建健壮、可伸缩的云原生生态系统的能力。 ####
|
18天前
|
边缘计算 自动驾驶 5G
|
13天前
|
机器学习/深度学习 人工智能 自然语言处理
医疗行业的语音识别技术解析:AI多模态能力平台的应用与架构
AI多模态能力平台通过语音识别技术,实现实时转录医患对话,自动生成结构化数据,提高医疗效率。平台具备强大的环境降噪、语音分离及自然语言处理能力,支持与医院系统无缝集成,广泛应用于门诊记录、多学科会诊和急诊场景,显著提升工作效率和数据准确性。
|
20天前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
58 10
|
17天前
|
消息中间件 编解码 开发者
深入解析 Flutter兼容鸿蒙next全体生态的横竖屏适配与多屏协作兼容架构
本文深入探讨了 Flutter 在屏幕适配、横竖屏切换及多屏协作方面的兼容架构。介绍了 Flutter 的响应式布局、逻辑像素、方向感知、LayoutBuilder 等工具,以及如何通过 StreamBuilder 和 Provider 实现多屏数据同步。结合实际应用场景,如移动办公和教育应用,展示了 Flutter 的强大功能和灵活性。
86 6
|
17天前
|
存储 SQL 缓存
AnalyticDB 实时数仓架构解析
AnalyticDB 是阿里云自研的 OLAP 数据库,广泛应用于行为分析、数据报表、金融风控等应用场景,可支持 100 trillion 行记录、10PB 量级的数据规模,亚秒级完成交互式分析查询。本文是对 《 AnalyticDB: Real-time OLAP Database System at Alibaba Cloud 》的学习总结。
36 1
|
19天前
|
监控 安全 Serverless
"揭秘D2终端大会热点技术:Serverless架构最佳实践全解析,让你的开发效率翻倍,迈向技术新高峰!"
【10月更文挑战第23天】D2终端大会汇聚了众多前沿技术,其中Serverless架构备受瞩目。它让开发者无需关注服务器管理,专注于业务逻辑,提高开发效率。本文介绍了选择合适平台、设计合理函数架构、优化性能及安全监控的最佳实践,助力开发者充分挖掘Serverless潜力,推动技术发展。
44 1

推荐镜像

更多