从零到一搭建基础架构(4)-base模块搭建下篇

3,509 阅读7分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!


Hello,这里是爱 Coding,爱 Hiphop,爱喝点小酒的 AKA 柏炎。

本篇是手把手搭建基础架构专栏的第四篇。

第一篇:从零到一搭建基础架构(1)-玩转maven依赖版本管理

第二篇:从零到一搭建基础架构(2)-如何构建基础架构模块划分

第三篇:从零到一搭建基础架构(3)-base模块搭建上篇

上文为大家介绍了base包中Maven依赖定义、统一请求响应与POJO划分准则

本文接着上篇开头所抛出的问题

  1. 工具类泛滥,同一工程中StringUtil的引用有外部引入,有内部jar包引入还有自己定义的
  2. 异常定义混乱,导致在Spring统一response拦截的地方区分业务异常与code错误困难
  3. 通用性高的枚举重复定义,比如是否枚举男女枚举
  4. 通用的常量散落在业务系统中,导致各个业务系统中重复的逻辑定义
  5. ...

建议先阅读第三篇哦~

image.png

一、统一工具类

现在,立刻,马上,打开你正在开发的一个项目,搜索一下StringUtil,是不是出来了一大堆定义?有在三方jar的,有在内部jar的,有在当前工程定义的。

image.png

任意打开两个StringUtIl看一下里面的方法,基本上都是一样的逻辑:isEmpty、isBlank等等。这对于一个项目而言,完全是没有必要的重复代码。

各个团队有自己的独立的工具引用,有自己的工具类定义。重复的方法满天飞,在某个Aservice中你引用了AStringUtil,在Bservice引用了BStringUtil。BStringUtil中没有isBlank的方法,你还要去BStringUtIl里面去加个方法,但明明这些功能逻辑已经在AStringUtil已经实现了。

image.png

为了简化业务工程中对于类似工具类的重复开发与重复引用,我们在base包中可以专门划分个utils包

code演示

上面的code演示中的线程池我也写过一篇解析:我用这个线程池捕获了后端妹子的芳心

尽量将第三方引用的工具包定义在base包中,然后定义CommonStringUtil继承工具包中所定义的StringUtil,所有业务方都来使用CommonStringUtil。

image-20221017161127644.png

这样就能将通用的StringUtil逻辑都维护在统一的地方,减少了重复逻辑。

这里有的同学会有疑问,工具类是统一了,但是不是缺少了灵活性?

因为有的业务工程会将部分业务相关的通用逻辑做在StringUtil中。

其实思路上也很简单,常规的业务code还是来使用CommonStringUtil。有特殊需求的地方我们可以定义独立的BizStringUtil来继承CommonStringUtil,这样就互相不影响了。

image-20221017160917201.png

二、统一异常定义

业务逻辑中抛出特定的异常然后被捕获处理,这个是非常常见的操作。

但是如果到Controller这个异常还没有被处理,那么在Spring中我们可以使用全局异常拦截来处理异常。

对于在Spring中如何处理全局异常不是很清楚的可以参考此篇:Spring中优雅的处理全局异常

在Spring的全局异常处理中,它是以异常类的类型来映射说当前的异常该被如何解析处理。

如果找不到异常类型,异常将继续往上抛,在Filter层被框架处理。

异常在Filter层被框架处理后我们将无法定义其标准的业务响应格式,因此我们一般会定义一个顶层异常的Handler

@ExceptionHandler(Throwable.class)
public Object handle(Throwable ex) {
    log.error("全局异常", ex);
    return Result.error(BaseResult.SYSTEM_ERROR);
}

到这里一切都很正常,但是你发现一个问题没有?

每个异常需要定义一个处理类,一旦你漏了一个特定的业务异常,你的提示信息将无法告知给用户。

比如你对于用户登陆失败定义了一个异常为:LoginException,但没有定义特定的handler。当用户输入密码错误时,LoginController抛出了LoginException,它将被顶级的异常处理类捕获,告诉用户系统异常了。用户都不知道是因为系统挂了、密码数错了还是账号输入错了,非常的不友好。

因此我们应该对于这种特定的运行时业务异常做一个父级定义。

那这个运行时的父级异常怎么定义呢?可以使用RuntimeException作为父级异常吗?

image.png

肯定不行!为什么?

如果我们将RuntimeException作为父级的异常,那么在全局异常处理的地方,我们只能定义RuntimeException的Handler。RuntimeException仅支持getMessage()方法,我们只能在抛出异常的时候将异常信息给写入,然后将异常信息原封不动的抛出去。

如果系统用户仅为国内用户还行,如果是国际化系统,你抛出一个密码错误,老外肯定看不懂。

在上篇定义统一响应实体时还记不记得有一个errorCode字段,我们使用这个字段作为异常抛出时的error的key,根据这个key来进行国际化的映射。

因此我们应该定义一个顶层的SeviceException来支持errorCode字段,这样我们在抛出特定的异常时就可以使用errorCode去映射国际化信息。

OK,到这里我们在来看一下登陆失败时抛出登陆异常时它的解析流程:

  1. 如果存在LoginException的handler,返回特定异常的handler的响应
  2. 如果存在ServiceException的handler,统一的业务异常处理,根据errorCode映射国际化信息返回统一响应
  3. 如果存在RuntimeException的handler,直接返回errorCode的统一响应
  4. 如果存在Throwable的handler,返回系统异常message的统一响应

code演示

三、通用的枚举与常量定义

用户是男是女、数据库标识位是与否、密码强度校验的正则表达式等等这种通用性较高的枚举与常量,我基本上在每个项目中都能看到这样的枚举与常量定义。

且不说代码重复吧,至少对于男女这种枚举的定义,有的系统把男性定义为Man,有系统定位man,系统间交互比较性别时就很容易出问题。

再看密码强度校验的正则常量,同公司或者说同部门的产品校验逻辑基本都一样。在A系统要定义一份,B系统也要定义一份,逻辑重复不说,正则的实现方式很多样,还很容易出现bug。

因此这种通用性,业务无关性的类都应该被整合在base中。

public enum YesNoEnum {
​
    YES(1, "是",Boolean.TRUE),
    NO(0, "否",Boolean.FALSE),
    ;
​
    @Getter
    private Integer key;
    @Getter
    private String name;
    @Getter
    private Boolean value;
​
    YesNoEnum(Integer key, String name,Boolean value) {
        this.key = key;
        this.name = name;
        this.value = value;
    }
​
    /**
     * 根据枚举KEY获取枚举
     * @param key
     * @return
     */
    public static YesNoEnum getByKey(Integer key) {
        return Arrays.stream(values()).filter(e->Objects.equals(key, e.key))
                .findFirst()
                .orElse(null);
    }
​
}

通用枚举code演示

通用常量code演示

四、总结

base包的职责分为上下两篇为大家介绍它在基础架构包中的定位与核心的code演示。

花里胡哨的讲这么多,其实在我看来,只要你的code具备通用性、业务无关性和无需配置开箱即用性你就可以放在base中。

image.png

base包实际上就是一个大型的工具包。

五、联系我

如果你觉得文章写得不错,点赞评论+关注,么么哒~

微信:baiyan_lou

我的第一本掘金小册《深入浅出DDD》已经在掘金上线,欢迎大家试读~

DDD的微信群我也已经建好了,由于文章内不能放二维码,大家可以加我微信,备注DDD交流,我拉你进群,欢迎交流共同进步。