架构师技能2:组件化思想之框架脚手架基础应用框架。

Posted hguisu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了架构师技能2:组件化思想之框架脚手架基础应用框架。相关的知识,希望对你有一定的参考价值。

        在《架构设计》https://blog.csdn.net/hguisu/category_5905793.html系列里面主要谈的是架构相关方法论,没有具体到代码层面。最近抽点时间来总结架构师如何站在巨人的肩膀上眺望远方。

        管理的精髓就是“制度管人,流程管事”。而所谓流程,就是对一些日常工作环节、方式方法、次序等进行标准化、规范化。且不论精不精髓,在技术团队中,对一些通用场景,统一规范是必要的,只有步调一致,才能高效向前。

一、框架和组件


这个在《架构设计(1)-谈谈架构》 已经明确:

框架是组件实现的规范,提供一定的约束、规范、配置,具备通用业务的基础开发能力,提供一定程度的组件、工具等公共封装。例如:MVC、MVP、MVVM等,是提供基础功能的半成品,例如开源框架:Spring、spring boot、Django等,这是可以拿来直接使用或者在此基础上二次开发。框架是规范,架构是结构。

1、框架主要有两特性:

1)、框架是提供基础功能的半成品:已经对基础的代码(例如文件上传,数据库查询)进行了封装并提供相应的API,开发者在使用框架是直接调用封装好的api可以省去很多代码编写,从而提高工作效率和开发速度。

2)、框架是规范:框架规范应用的体系结构。它定义了整体结构,模块的划分、模块的主要职责,模块之间的协作。

2、使用框架的目的:

1)、提高开发效率:

        使用成熟的框架,基础工作已经完成,应用开发人员只需要集中精力完成系统的业务逻辑设计。框架一般已经处理好基础功能和细节问题,比如事务处理,安全性,数据流控制等问题。还有框架一般都经过很多人使用,所以结构很好,所以扩展性也很好,而且它是不断升级的,你可以直接享受别人升级代码带来的好处。

2)、提高团队人员协同效率。

通过框架规范,规定大层次的约束与规范,较小层次的设计在这些约束与规范下进行的话,能最大限度地满足某些方面的特性,如可读性、可靠性、可扩展性、安全性。同时有使得代码更为清晰及更易维护。最终目的是提高团队协作效率,降低工程维护成本。

3、组件化

组件化就是基于可重用的目的,将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,已较少耦合。把重复的代码提取出来合并成为一个个组件,组件最重要的就是重用(复用),位于框架最底层,其他功能都依赖于组件,可供不同功能使用,独立性强。
大部分来说,组件主要分三层:业务组件,基础业务组件以及基础组件,组件之间只能通过接口耦合,也就是依赖倒置原则,每个组件都提供对外的接口文档以描述该组件提供的功能。

组件化的好处:解耦,平台化,职责单一,复用性,编译集成。

  1. 解耦:每个组件都是一个单一的工程(项目),对外只提供接口。组件之间的依赖只能通过接口,通过工程或者项目的方式,可以很大程度避免代码之间的耦合。
  2. 职责单一:每个组件只提供单一的功能,每个组件都可以单独去维护扩展,只要接口不变。
  3. 复用性强:基于职责单一,那么新项目中就可以依赖需要的组件。如果有其他项目需要该组件可以直接引入使用,而不是拷贝代码。
  4. 平台化:这个其实是最有价值的,如果你作为一个平台产品,其他业务或者兄弟部门的开发同学想集成到你的产品中,那么他在开发测试的时候就很方便的依赖必须的组件,方便调试。这样,在多部门,多team去联合调试的时候,会节省很多的时间,但是这个要求文档必须要够完善,以便于其他人能够很方便的去接入。类似于:支付宝,美团等等平台级的产品。
  5. 编译集成:单个组件化组合成一个产品,可单独开发,测试,发布一个组件。对于编译来说可以很快速的定位问题以及快速编译,打包。
  6. 组件单独测试方便:测试完成后进行集中测试。

二、组件化之基础应用框架/脚手架


1、脚手架和基础应用框架

组件化到组件的粒度到底多大,如何区分业务组件以及基础业务组件?这个需要根据具体项目具体分析。

脚手架:基于开源框架或者组件的整合,只是一个空架子,把项目的基础环境配置和maven相关依赖都搭建好,封装程度较低,偏向于一套技术最佳实践。做前端node的都知道,经常去npm install 一个脚手架或者vue-cli脚手架搭建工具,然后用脚手架命令去创建一个空架子的项目。

基础应用框架:除了整合应用使用的开源框架,同时还封装项目应用到的基本工具和基础功能:日志,接口协议、异常处理、安全处理、单元测试、参数校验等。即在脚手架的基础。

注明:以上仅个人理解脚手架和项目基础技术框架。

在微服务里面或者生命周期比较长的项目,一般是需要对使用不同原始框架拼装项目的基础框架,目的是封装提供项目使用的基础功能和规范项目技术体系,不要重复造轮子,提高团队的协同效率,降低维护成本。

2、基础应用框架的需求背景

1)、持续积累的通用功能、公共功能

      团队甚至开发人员自己,都会在工作中持续积累沉淀一些框架性的东西,比如一些通用功能和公共功能,让其他人不用重复造轮子,比如:后台功能的审批单流程,

2)、技术架构体系实现中蕴含的通用功能、公共功能

       比如我们要使用Springboot+SpringCloud来实现微服务,那么这套技术架构体系里面,就会有一些通用的、公共的功能。如根据当前业务需求针对服务调用:幂等、调用重试、熔断策略等等的等进行封装。比如我们对feign进行二次封装:可以实现特定的熔断,重试、接口统一签名,返回结果异常处理等。

      不同的技术架构体系,里面蕴含的东西是不一样的,需要具体问题具体分析。

3:设计过程中提炼出的通用功能、公共功能

这类功能,有些可能做到基础框架里面,也有可能是体现成为一些公共的工具包,或者通用父类,一般来说,这些功能跟业务还是有一定的结合的,实现上也有一定的特殊性,适用面没有那么广。

3、rest服务基础框架中常见的基本功能

例如:

1、封装基础组件:如spring boot的版本统一规范和依赖规范。

2、restFull接口规范:比如rest接口返回格式。

3、日志组件统一规范处理。

4、异常处理统一规范处理。

5、db相关CRUD等基础功能实现

6、参数校验

7、安全处理机制。

8、事务处理机制。

9、api文档

10、基础应用配置。

11、模型统一规范格式。

12、一些基础工具类:比如项目使用获取ip,邮箱、手机号等校验。

13、单元测试规范。

 下面是一一具体描述。

三、基础应用框架说明


1、基础组件和依赖统一规范管理:

主要spring boot的版本统一规范和依赖规范。主要的措施:

1)、通过maven的parent工程统一管理项目所有的jar的版本。

2)管理jar包依赖,在parent工程中配置依赖通用性jar包,然后引用该parent的项目就会自动继承。

3)parent工程通过<dependencyManagement>管理了可选的jar依赖,具体项目是按需继承。

如:<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</dependencyManagement>  

具体项目只要按需去依赖自己所需的jar包,并且version 和 scope 都继承pom。

<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
</dependency>

2、日志组件统一规范:

Java应用项目经常遇到的一个棘手的问题就是:依赖的包使用了不同的日志组件,常用的有log4j, log4j2, logback, common-logging, JUL等,导致日志输出异常混乱。因此日志的输出有必要进行统一配置,而不是针对不同的日志组件分别配置。

 因此需要做统一规范:

  • 使用SLF4J作为日志API框架:通过适配各种日志实现的桥接包,接收所有的日志请求。
  • 使用logback作为最终的唯一日志实现,处理SLF4J收集的所有日志

因此为了统一规范日志的记录,及方便日后扩展,通过抽象接口对日志记录进行了封装。
 

 

 然后在具体项目统一使用:

import com.xxx.framework.logs.Logger;
import com.xxx.framework.logs.LoggerFactory;

private static final Logger logger = LoggerFactory.getLogger(xxController.class);

3、restFull接口规范:

采用的微服务框架,前后端分离,前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。

为了提高团队间接口对接的效率,需要做前后端交互的格式规范。一般统一的规范如下:

1、请求格式规范:建议使用json body格式,传入参数包含公共请求头的要求(如:app_version,api_version,device等)。

2、返回格式规范:

JSON体方式,定义如下:


   code:integer,#返回状态码	
   message:string,#返回信息描述,直接展示友好用户提示信息
   error:string,#错误描述信息,方便开发直接定位
   data:object #返回值

code状态码:我们可以参考http状态设计:

HTTP状态码:
200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误

分类的好处就把错误类型归类到某个区间内,如果区间不够,可以设计成4位数。

#900~999   区间表示系统异常错误
#1000~1999 区间表示用户模块1错误
#2000~2999 区间表示用户模块2错误

这样前端开发人员在得到返回值后,根据状态码就可以知道,大概什么错误,再根据message相关的信息描述,可以快速定位。

简单实现,这里我是为兼容旧项目,不得使用status代替code,这个无关紧要,只要规范统一旧可以。

@Data
public class RestVO<T> 

    private int status = 0;
    private String msg = "success";
    //前端不展示,方便开发检查。
    private String error;
    private T data;

    public RestVO() 
    

    public RestVO(T data) 
        this.data = data;
    

    public RestVO(int code, String msg) 
        this.status = code;
        this.msg = msg;
    

    public RestVO(int code, String msg, T data) 
        this.status = code;
        this.msg = msg;
        this.data = data;
    

    public RestVO(RestCode restCode) 
        this.status = restCode.getCode();
        this.msg = restCode.getMsg();
    

    public RestVO(RestCode restCode, String error) 
        this.status = restCode.getCode();
        this.msg = restCode.getMsg();
        this.error = error;
    

4、异常处理统一规范:

      基础框架组件需要对异常进行统一处理,规范异常处理的行为。这样各个具体项目基于这个框架组建开,减少重复造轮子。

应用程序的错误和异常,可以分成两大类:

一是预期的例异常,由我们开发人员在编码的时候主动抛出;

二是不预期的异常,就是程序运行出现了异常情况,如npe异常、io异常等。

这些异常的统一处理,需要依赖统一的rest接口格式,因为要直接展示给用户看,在抛出异常的时候,要包含异常code和message,而message可能需要经过友好处理后展示给用户看,比如npe异常,不可能就直接展示当前服务器npe消息错误。

处理异常做两方面的工作:一是通过rest接口返回给前端,二是记录异常日志。

具体实现:

Spring学习笔记(9)一springMVC全局异常处理_黄规速博客:学如逆水行舟,不进则退-CSDN博客_springmvc全局异常处理 

5、db相关CRUD等基础功能实现

如果使用mybatis-plus,已经基本实现一套通用的CRUD的基础功能,但是可以基于mybatis-plus的代码生成器,结合项目规范提供代码生成器。

6、参数校验

参数校验主要是定义一些注解实现请求参数校验和格式化

1、示例1:@RegionFormat(地区格式化):

被@RegionFormat注解的属性,会接受一个地区id转换为地区对象。

@RegionFormat
private Region region;

上述注解假设如下请求:

xxx?region=3

则会将id为1的地区及其父的信息读取并形成Region对象

例如region=3的地区为长安街,父辈为为东城区-->北京,刚形成的Region对象数据如下:


    "cityId": 2,
    "townId": 0,
    "countyId": 3,
    "provinceId": 1,
    "province": "长安街",
    "county": "东城区",
    "city": "北京市",
    "town": ""

2、@Mobile (手机格式校验)

使用@Mobile注解可以校验手机号码的正确性。

使用:在声明的对象手机号码属性上面加@Mobile

 @Mobile
 private String mobile;

如果校验失败会抛出如下异常:


  "code": "004",
  "message": "手机号码格式不正确"

7、安全处理机制

主要是防xss攻击的一些措施。例如防xss攻击过滤。

8、统一事务规划和处理

对于分布式事务的处理,基本步骤是:

(1)尽量不要出现分布式事务,都是本地的经典事务,这是最好的

(2)实在规避不了,需要出现分布式事务,也尽量是弱事务,最终一致即可。所谓弱事务,指的是主事务成功的时候,不强制要求子事务同时完成,可以在随后一段时间内,子事务自行完成,成功即可。另外,一般来说,弱事务失败不会导致主事务的回滚

(3)如果必须要使用分布式事务,且是强事务,那要选择并实现合理的事务处理方案,比如:使用Seata、本地消息表 等方案。

我们需要对于经典事务进行统一的事务规划和配置实现;对于分布式的事务,提供统一的、公共的事务实现的支持。

9、微服务调用机制

      各个微服务是要相互远程调用的,那它怎么调用过去的呢?该由谁来接这些调用的请求?一些公共的处理由谁来做?微服务的调用层和业务之间怎么结合起来?等等的一系列问题

      对于这些问题,通常也需要结合具体的技术架构体系,抽取出公共的功能,去统一实现成为微服务的基础功能。

     比如:使用SpringCloud的2020版(目前最新的)来实现微服务,那么微服务之间调用的基本方式,常用的组合是openfeign+loadbalancer+sentinel,被调用的服务会单独做一个由SpringMVC实现的Controller层,来负责接收并转发处理这些微服务之间调用的请求。

主要功能:

1、幂等调用处理:微服务调用过程中,由于网络并不稳定,通常客户端调用的时候,都是开启了重试机制的,难免会出现一次请求多次重复调用某一个方法的情况,也就是说,微服务向外提供的接口,应该实现幂等性。

2、重试机制:调用失败后,是否需要重试,重试的策略等,这个可以通过注解的方式给调用方。

3、熔断策略:熔断降级的策略有哪些?

  •  慢调用比例熔断:超过指定时长的调用为慢调用,统计单位时长内慢调用的比例,超过阈值则熔断
  • 异常比例熔断:统计单位时长内异常调用的比例,超过阈值则熔断
  • 异常数熔断:统计单位时长内异常调用的次数,超过阈值则熔断

10、api文档

使用Swagger2做api文档,同时封装Swagger配置基类:
public abstract class AbstractSwagger2 

    /**
     * 构建认证token参数
     *
     * @return token参数
     */
    protected List<Parameter> buildParameter() 

        ParameterBuilder tokenPar = new ParameterBuilder();
        List<Parameter> pars = new ArrayList<Parameter>();
//       tokenPar.name("sign").description("sign").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
//       pars.add(tokenPar.build());
        return pars;
    

待续... ...

还有很多,比如

11、基础应用配置。

12、模型统一规范格式。

13、一些基础工具类:比如项目使用获取ip,邮箱、手机号等校验。

14、单元测试规范。

15、分布式ID的统一生成。

16、各种工具辅助类。

17、权限管理。

18、任务管理。

19、消息管理。

20、工作流。

四、基础应用框架封装的方式


框架的本质是基础功能代码,最终是要被业务使用的,那就首先要想好怎么被业务使用。

封装框架的方式:

1、提供基础抽象类或者接口,

接口一般代表的是规范,使用者通过实现接口来使用基础框架提供的功能。

抽象类一般代表的是模板,使用者通过继承的方式来使用基础框架提供的功能。

需要根据具体情况具体选择,由于java的单继承,所以尽量使用接口,业务最终会实现框架提供的接口,在框架内部做一些通用的对业务来说是透明的处理,最终运行时框架会回调这个接口的实现来处理业务。

针对数据库的CRUD基础操作,建议提供基础抽象类,封装CRUD基础方法实现。

2、通过注解或注解 + AOP的方式

       使用自定义注解来应用基础框架里面的功能。或者是使用AOP的方式,把基础框架里面的功能和业务功能结合起来。使用自定义注解这种方式会更加优雅和灵活,对于项目基本无侵入。

       比如我们有些rest接口需要签名认证,如果每个调用方都自己实现,就会重复实现一套签名。我们在feign接口,自定义注解:

     @FeignAppSign(appId = "$app.appId", appKey = "$app.appKey")

  然后feign组件通过使用契约Contract生成签名。
public void feignContract(Class<?> clz) 
 if (clz.isAnnotationPresent(FeignAppSign.class)) 
    String appId = clz.getAnnotation(FeignAppSign.class).appId();
    String appKey = clz.getAnnotation(FeignAppSign.class).appKey();
    appId = resolve(appId);
    appKey = resolve(appKey);
    params = SignUtil.buildSignParams(appId, appKey);
  

 3、通过配置方式

如果一个系统在扩展时只涉及到修改配置文件,而原有的代码没有做任何修改,该系统即可认为是一个符合开闭原则的系统。

4、 提供缺省功能实现,可通过SPI 进行扩展

通过spi方式,框架提供了很好扩展机制:使用SPI设计,框架可以很容易引入扩展点,同时应用要扩展框架逻辑也很容易实现。框架可扩展设计可以基于这个原则进行设计扩展点。 

使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

SPI设计的关键是程序定义通用的接口:

比如JDBC,然后不同服务提供商根据接口做自己的实现如Driver,然后程序在运行时根据加载到的接口实现不同,实现不同具体的功能,如操作不同的数据库。

spi详解可以看我早期总结的文章:https://blog.csdn.net/hguisu/article/details/124370243

五、基础应用框架设计


框架设计一定要符合面向对象设计原则:设计原则:面向对象设计原则详解_hguisu的博客-CSDN博客_设计模式原则

第一、组件化/模块化

如果把框架看作一个整体,那这个整体可以拆分成一个个组件,每个组件都是可以独立存在的部分,每个组件可以独立由不同人开发,组件与组件之间面向抽象,尽可能少的耦合,这其实也是面向对象设计的基本思想。

组件主要分三层,业务组件,基础业务组件以及基础组件,组件之间只能通过接口耦合,也就是依赖倒置原则,每个组件都提供对外的接口文档以描述该组件提供的功能。

组件化的好处不仅仅可以帮助框架设计者保持思维清晰、提高框架的可维护性,同时通过组件化的方式,能统一业务代码规范,减少冗余代码,提高开发效率。具体:

  1. 解耦:每个组件都是一个单一的工程(项目),对外只提供接口。组件之间的依赖只能通过接口,通过工程或者项目的方式,可以很大程度避免代码之间的耦合。
  2. 职责单一:每个组件只提供单一的功能,每个组件都可以单独去维护扩展,只要接口不变。
  3. 复用性强:基于职责单一,那么新项目中就可以依赖需要的组件,减少冗余代码,提高开发效率。

第二、分层设计

架构设计(3)--架构模式具体详细说明分层设计。

比较有代表性的如dubbo这种rpc框架,一次rpc调用对应着一系列步骤

接力,那么dubbo在设计时就把这些步骤划分出了层次。http://dubbo.apache.org/zh-cn/docs/dev/design.html

这种分层的思想十分常见,再比如基本都熟知的tcp/ip网络协议,就是一个分层设计的典型例子。

分层的结果是层与层解耦,层与层之间面向抽象设计,每层都是可替换的,每层的修改都不会影响到其它层,虽然会增加一定的复杂度,但是带来的好处是完全值得的。

第三、善用设计模式

设计模式是前人在实践中总结的,用来解决各种一般场景的最佳实践,用好了设计模式对提高代码的可读性、可维护性、可扩展性都非常有帮助,但是如果只是一味地生搬硬套,可能只会给编程增加一些负担,并不会带来什么明显的好处。

使用设计模式的目的就是寻找变化进行封装。

设计模式概论:封装变化_hguisu的博客-CSDN博客

https://blog.csdn.net/hguisu/category_1133340.html?spm=1001.2014.3001.5482

以上是关于架构师技能2:组件化思想之框架脚手架基础应用框架。的主要内容,如果未能解决你的问题,请参考以下文章

架构师技能2:框架脚手架应用基础框架。

架构师技能2:基础框架和组件规范

前端架构师-week2-脚手架架构设计和框架搭建

MySQL系列:GitHub标星1w的Java架构师必备技能

Java架构师必备框架技能核心笔记,太香了

脚手架架构设计和框架搭建