美文网首页设计方案
关于微服务“统一标准”的一些看法

关于微服务“统一标准”的一些看法

作者: juconcurrent | 来源:发表于2019-12-30 23:27 被阅读0次

    前言

    在Java领域,主流的微服务框架是Spring Cloud + Spring Boot。这套框架集成了各类常用的组件,也提供了足够的扩展性,方便对其他组件进行集成。简化我们的集成难度之外,也让我们能快速地实现分布式架构。

    那么,问题来了。

    这么多组件协同地工作,如果没有在包和类的命名上没有进行统一规划,我们的代码将变得凌乱而脆弱。业界比较好的开发模式是DDD(领域驱动设计),这种模式虽然好,但是使用门槛较高,快速开发难度大。而传统开发模式是三层架构,即:Controller、Service和Persistense的方式,这种方式在大型项目的后期维护成本较高,但是中型项目的前期和中期,还是能带来足够多的效益。

    因此,在三层架构开发模式的基础之上,楼主站在自己的角度上对项目和包的结构进行了规划,统一了包和类的命名。并以此形成一套完整的、可落地的微服务命名解决方案。

    注:这儿的解决方案限于Spring Cloud+Spring Boot的架构,其他架构仅供参考。

    启动类

    启动类统一命名为Application,且必须加上以下注解。

    @SpringBootApplication
    @EnableEurekaClient
    @ComponentScan("com.xxxcompany")
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    项目结构

    项目统一使用maven进行管理,且包括以下模块。

    • {projectName} (folder: projectName) -- 根模块
      • ${projectName}-dataaccess (folder: dataaccess) -- 数据访问模块
      • ${projectName}-feign (folder: feign) -- 远程调用模块
      • ${projectName}-common (folder: common) -- 公共模块
      • ${projectName}-core (folder: core) -- 核心业务模块
      • ${projectName}-api (folder: api) -- 接口模块
      • ${projectName}-ipi (folder: ipi) -- 接口定义模块,可选

    模块间依赖关系如下所示。

    api
      -> ipi
      -> core
        -> dataaccess
        -> common
        -> feign
        -> ipi
    

    项目间依赖关系如下所示。

    Aapi -> Bipi
    

    需要特别说明的是,为了避免各个微服务项目之间的相互影响,ipi这个模块其实并不是必须的。这儿之所以会罗列ipi模块,是因为在某些场景下,ipi模块的使用可将问题尽早暴露出来。问题暴露的阶段大致包括:

    1. 编译期(最好)
    2. 启动期(其次)
    3. 运行期(再次)

    包和类的命名规则应如下所示。

    【注】:这儿的[D]表示是一个文件夹或者包,[F]表示是一个文件或者类。

    # dataaccess
    com.${companyName}.${projectName}.dataaccess
      -> dao [D]
        -> mysql [D]
          -> XxxMapper [F]
        -> cache [D]
          -> XxxCache [F]
      -> domain [D]
        -> mysql [D]
          -> XxxDO [F]
          -> ext [D]
            -> XxxExtDO [F]
    
    # feign
    com.${companyName}.${projectName}.feign
      -> ${thirdProjectName} [D]
        -> XxxFeign [F]
        -> param [D] [可选]
          -> request.xxx [D]
            -> XxxFeignReq [F]
          -> response.xxx [D]
            -> XxxFeignRes [F]
    
    # common
    com.${companyName}.${projectName}.common
      -> enums [D]
        -> XxxEnum [F]
      -> config [D]
        -> XxxConfig [F]
      -> configbean [D]
        -> XxxConfigBean [F]
      -> util [D]
        -> XxxUtil [F]
    
    # core
    com.${companyName}.${projectName}.core
      -> mq [D]
        -> rabbitmq [D]
          -> consumer [D]
            -> mo [D]
              -> XxxMO [F]
            -> XxxAmqpConsumer [F]
          -> producer [D]
            -> mo [D]
              -> XxxMO [F]
            -> XxxAmqpProducer [F]
        -> kafka [D]
          -> consumer [D]
            -> mo [D]
              -> XxxMO [F]
            -> XxxKafkaConsumer [F]
          -> producer [D]
            -> mo [D]
              -> XxxMO [F]
            -> XxxKafkaProducer [F]
      -> service [D]
        -> ${function} [D]
          -> XxxService [F]
          -> impl [D]
            -> XxxServiceImpl [F]
    
    # api
    com.${companyName}.${projectName}.api
      -> exception [D] [可选]
        -> GlobalExceptionHandler [F]
      -> controller [D]
        -> XxxApi [F]
      -> Application.java [F]
    
    # ipi
    # 为了使服务更容易被治理,我们对所有服务进行了分层,总共四层。
    # 同时,为了让我们的请求和返回类更容易编码和阅读,我们对请求类和响应类进行后缀约定。
    # 1. web层 (XxxReq / XxxRes)
    # 2. 业务服务层 (MicReq / MicRes)
    # 3. 业务中台层 (BcpReq / BcpRes)
    # 4. 数据中台层 (DcpReq / DcpRes)
    com.${companyName}.${projectName}.ipi
      -> icontroller [D]
        -> XxxIpi [F]
      -> param.request.${icontrollerName} [D]
        -> XxxReq [F]
      -> param.response.${icontrollerName} [D]
        -> XxxRes [F]
      -> statuscode [D]
        -> XxxStatusCode [F]
      -> validator [D]
        -> XxxAnnotationConstraintValidator [F]
        -> XxxAnnotation [F]
    

    接口返回类

    分布式架构中,通过对接口返回(也叫做接口响应)的状态码、描述信息、错误信息和挂载对象进行统一格式,我们将有可能对接口拦截进行更规范地处理。这儿,接口返回类,我们统一使用Response,该类包含以下属性。

    @Data
    public class Resposne<T> {
    
        private boolean success; // 成功标记
        private long code; // 状态码。1000表示成功,其他表示失败
        private String msg; // 描述信息
        private String errorMsg; // 错误信息
        private T data; // 挂载对象
    
        public Response<T> fallback() {
            // Some converted codes is here.
            // When failed, We can throws more BusinessException.
            // If you want to handle your codes more simple, 
            // just overload the fallback() methods.
        }
    }
    

    当挂载数据不需要包含任何数据时,必须使用java.lang.Void作为泛型参数。如果挂载对象有数据,需使用POJOs方式定义,且统一使用lombok注解,注解类型及顺序必须如下所示!
    如果挂载对象类需要继承其他类,必须加上注解@ToString(callSuper = true),从而避免toString()方法在调用后没有输出父类字段属性的异常情况出现。

    【注】:挂载类应尽量避免使用继承特性。可使用字段冗余定义+Orika转换的方式来代替,效果更好。

    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    @Accessors(chain = true)
    

    状态码

    状态码合理有效地定义,将降低我们排查线上问题的难度,加快我们定位问题的速度。
    因此,我们对状态码进行如下约定:

    1. 统一继承自IStatusCode接口,默认实现类为StatusCode
    2. StatusCode为公共状态码类,状态码范围为1000-9999。
      其中,成功状态码统一为1000,业务错误状态码统一为1006。
    3. 自定义状态码生成规则为:serviceCode * 1000000L + productCode * 10000L + customCode
      这种规则,支持的产品数量为100个(00 -> 99),而产品下的服务数量却不受限制。
    /**
     * 状态码抽象接口
     */
    public interface IStatusCode {
    
        /**
         * 状态码
         */
        long getCode();
    
        /**
         * 描述信息
         */
        String getMsg();
    }
    
    /**
     * 公共状态码
     */
    @Getter
    @AllArgsConstructor
    public enum StatusCode implements IStatusCode {
    
        SUCCESS(1000, "操作成功", "操作成功"),
        SYSTEM_ERROR(1001, "系统未知错误", "系统未知错误"),
        PERMISSION_DENIED(1002, "没有权限", "没有权限"),
        DATA_ERROR(1003, "数据错误", "数据错误"),
        REPEAT_SUBMIT(1004, "重复提交", "重复提交"),
        PARAM_ERROR(1005, "参数错误", "参数错误"),
        BUSINESS_ERROR(1006, "业务错误", "业务错误"),
        ALERT_ERROR(1007, "业务错误", "弹框提示类业务错误"),
        ;
    
        /**
         * 状态码
         */
        private long code;
    
        /**
         * 描述信息
         */
        private String msg;
    
        /**
         * 错误信息
         */
        private String errorMsg;
    }
    

    对象转换

    相对于Apache和Spring内置的Bean转换类库来说,Orika提供了更为完善的映射转换机制,它是一个优秀的、高性能的、低侵入的对象转换工具类库。对于对象之间的转换,我们在这儿统一使用Orika进行操作。
    所有的转换器统一继承自DefaultConverter,且统一命名为XxxConverter,格式大致如下所示:

    // 定义 start
    @Component
    public class SelfDefinedConverter extends BaseConverter {
        public SelfDefinedConverter() {
            // 特殊的规则定义放在这儿
        }
    
        public TargetType map(SourceType sourceObj) {
        }
    }
    // 定义 end
    
    // 使用 start
    @Autowired
    private SelfDefinedConverter selfDefinedConverter;
    
    TargetType targetObj = selfDefinedConverter.map(sourceObj);
    // 使用 end
    

    BaseConver如下所示:

    /**
     * 抽象的Converter,可用于类型注册、绑定mapperFacade,其目的为生成映射对象。
     */
    public abstract class BaseConverter {
    
        protected MapperFactory mapperFactory = OrikaMapperHelper.mapperFactory;
    
        protected void register(Class<?> typeA, Class<?> typeB) {
            mapperFactory.classMap(typeA, typeB)
                    .mapNulls(false)
                    .mapNullsInReverse(false)
                    .byDefault()
                    .register();
        }
    
        public MapperFacade getMapperFacade() {
            return mapperFactory.getMapperFacade();
        }
    }
    

    总结

    楼主基于自己过往的经验和思考,提出了对于RPC、返回类、Web校验器、状态码、关系型数据库、消息中间件、配置中心和转换器的统一命名,并对用法进行了简单的举例。其实,本文对微服务的囊括并不全面。后续,我们将根据集成的组件,进一步对命名和用法进行统一,使得我们的项目真正地达到企业级。增加我们代码的可阅读性、可维护性和健壮性。

    楼主能想到的后续可集成的组件大致包括:

    1. Elasticsearch -- 一个高性能的全文检索中间件
    2. HBase -- 可扩展的列存储数据库
    3. TIDB -- 关系型数据库的分布式架构版本
    4. Quartz -- 定时调度器(单机版和集群版都有)
    5. FastDFS -- 小文件分布式存储引擎
    6. Zookeeper -- 基于CP实现的分布式协调服务

    相关文章

      网友评论

        本文标题:关于微服务“统一标准”的一些看法

        本文链接:https://www.haomeiwen.com/subject/jjamoctx.html