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


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

本篇是手把手建立根底架构专栏的第七篇,👇🏻是专栏历史文章,依次读取效果更佳。

第一篇:从零到一建立根底架构(1)-玩转maven依靠版本管理

第二篇:从零到一建立根底架构(2)-怎样构建根底架构模块划分

第三篇:从零到一建立根底架构(3)-base模块建立上篇

第四篇:从零到一建立根底架构(4)-base模块建立下篇

第五篇:从零到一建立根底架构(5)-让你的RPC原地起飞

第六篇:从零到一建立根底架构(6)-让你的服务组件化

根底架构Demo:common-frame

你需求先clone common-dependency

然后执行mvn clean install 将 common-dependency包打到你本地仓库

否则你拉下来common-frame工程后会报找不到

<parent>
<groupId>com.baiyan</groupId>
<artifactId>common-dependency</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>

你是否在遭受以下的困扰:

  • 明明是写过的代码为什么得不到复用?

  • Controller怎样要处理这么多的事务逻辑?

  • 大局性装备与模块级装备咱们该怎样处理?

本文将为我们介绍怎样运用根底架构建立起的你的体系门面,让别人一眼望去就知道你的体系正在供给什么的事务功用与装备。

image.png

一、什么是门面?

现在市面上除了比较少量的大厂运用DDD架构进行事务开发,大多数的公司仍是运用MVC进行事务开发。

DDD与MVC比照文章,能够参阅我的小册试读内容:DDD是什么?为什么咱们用DDD?

为什么MVC是大多数公司的选择?简单,易上手,新手友好。

M(模型),V(视图),C(操控器)三者在完成增修正查上有一套十分固定的模板。

三者的串联逻辑:操控器从模型层获取到的数据映射成视图展示给用户。

Spring中常见的操作,咱们把M定为DAO,V定为Controller,C定为Service。

可是经历过历史项目的同学都会有这种感觉,Controller跟Service的鸿沟总是含糊不清的,在Controller里边会写好多事务逻辑,夸张的一点的Controller直接调用DAO来处理事务逻辑。

M与V之间的映射联络跳过了C的流程,导致M与V之间的处理变成了一次性买卖,碰到相似的逻辑的时分咱们无法进行复用。

image.png

咱们以封闭订单这个case为例,假定咱们在事务上封闭订单能够用户主动封闭被迫封闭(超时未付出)

主动封闭这个case比较好了解,Controller接收到恳求,调用Service处理逻辑,Service调用DAO修正模型。假如咱们含糊了M与V之间的鸿沟,就会导致大量的逻辑存在于Controller中。这时咱们被迫封闭订单时,总不能直接去调用Contoller的逻辑吧(假如你这么做了,那我只能说牛逼👍🏻)。相同的逻辑在被迫封闭订单中要再写一遍(比方完成方法是守时使命、时刻轮等)。

想想就觉得十分的麻瓜。这仍是仅仅一种被迫封闭的场景,后边假如增加一个MQ监听封闭订单,是不是还要再加一段如出一辙的代码?

image.png

所以为了逻辑具有一定的通用性、可复用性,咱们应该把逻辑收缩到操控层(Service)来处理。Controller层作为体系功用的门面,只需承受恳求、校验参数、参数转化、映射Service的成果的代码(比方Service回来男性为1,Controller将1映射成男,其实便是DTO与VO的转化)。

咱们能够以为门面仅仅一层壳,壳内填充物是Service。

能够作为门面的有哪些界说呢?

界说 描绘
controller 用户web恳求处理。
apiImpl(第六篇的RPC接口完成类) 它的定位其实与Controller相似,只不过它的效果域是内部服务。
MqConsumer MQ能够看做一种特殊的RPC,异步的处理内部服务的消息。
守时使命 体系自身作为逻辑触发入口,核心逻辑仍是由Service触发。
体系发动后的Runner 相似于@PostConstruct是发动过程中的逻辑,而Runner是发动后的处理,相似于守时使命,只不过它仅在发动完成后触发一次。

上述的门面界说组成了Maven模块interaction(用户交互层), 你能够基于上述五种类型快速知道体系正在供给什么样的功用。

image-20210531143802455.png

对照common-frame中interaction的maven层级,在运用服务的interaction模块下应该存在上述的几个门面的package。

image.png

二、门面的体系装备

装备这个东西吧,是一个十分奇特的东西。因为它只供给一些组件级或许体系级的功用装备与特点,一般不参加逻辑处理。所以在多模块工程中界说装备的类的时分通常会把他们放到Service的模块中。

那么在common-frame中咱们也这么处理,把所有装备都放在common-frame-service中能够吗?

image.png

肯定不可,为什么?

common-frame为什么要拆成多模块?除了后续做事务运用脚手架与分层主张指导之外,更多的是想给事务服务有足够多的选择能够自由决定来引证所需求的模块。

比方我现在只需求common-frame-service的依靠即可,可是在common-frame-service的依靠里边却包含了许多interaction层面或许发动类层面的装备类与maven引证。

比方用户服务现在想要引证common-frame-service,可是common-frame-service中增加swagger-ui的maven引证与装备。用户服务仅仅想要运用common-frame-service中所包含的那些组件级的装备,可是你却给我引入了web层接口的组件装备,这明显十分的不合理。

image.png

因而从maven引证与装备视点来说是十分有必需求存在interaction这样一个模块来独立放置这些门面等级的装备。

那么能够放在门面的装备界说是什么样的呢?

2.1.国际化装备

国际化装备存在的含义是让你的数据响应契合用户所在的region,它是属于用户交互等级的。

2.2.收支参序列化装备

咱们在进行日期格局序列化的时分,常常会有把日期、时刻映射成yyyy-MM-dd HH:mm:ssyyyy-MM-ddHH:mm:ss格局的字符串回来给前端。相同的,前端也会将yyyy-MM-dd HH:mm:ssyyyy-MM-ddHH:mm:ss格局的字符串恳求给事务服务,事务又需求映射成Date相关的java类。

Spring在日期格局的收支参序列化供给了 @DateTimeFormat、@JsonFormat注解。

关于有日期序列化需求的特点只需标上这两个注解就能完成2022-10-24 10:10:10与LocalDateTime互相转化的需求。

可是弊端是,每个特点都需求标。有没有什么方法统一完成这个序列化需求?

Spring默认是Jackson来进行序列化,所以咱们只需求修正Jackson的序列化装备即可。

@Configuration
@ConditionalOnProperty(value = "baiyan.config.jackson.enable", havingValue = "true")
public class CommonJacksonConfig {
​
    public static final String timeFormat = "HH:mm:ss";
    public static final String dateFormat = "yyyy-MM-dd";
    public static final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
​
    /**
     * 大局时刻格局化
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> {
            builder.simpleDateFormat(dateTimeFormat);
            //日期序列化
            builder.serializers(new LocalTimeSerializer(DateTimeFormatter.ofPattern(timeFormat)));
            builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(dateFormat)));
            builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
            //日期反序列化
            builder.deserializers(new LocalTimeDeserializer(DateTimeFormatter.ofPattern(timeFormat)));
            builder.deserializers(new LocalDateDeserializer(DateTimeFormatter.ofPattern(dateFormat)));
            builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
        };
    }
}

只需你的出参的字段是LocalDateTime的字段,会主动序列化成yyyy-MM-dd HH:mm:ss,入参格局为yyyy-MM-dd HH:mm:ss,假如承受参数类型为LocalDateTime,也会主动映射,不需求再增加 @DateTimeFormat、@JsonFormat注解。

相同的,假如你有用户交互层的序列化战略你也应该将序列化装备增加在interaction层。

2.3.大局反常阻拦

大局反常阻拦属于事务处理等级的兜底反常处理方案,产生反常时它将作为兜底的反常响应报文回来给用户。

在common-frame中现已界说了一个GlobalExceptionHandler,它的代码比较简单,它的根底思路我在Spring中优雅的处理大局反常也介绍过。

这里我着重说一下在common-frame中界说GlobalExceptionHandler与在事务运用中界说GlobalExceptionHandler有什么区别。

common-frame中GlobalExceptionHandler仅对common-frame框架级的反常做处理,事务运用的GlobalExceptionHandler对本服务内的特定事务反常做处理。

这两者应该是相得益彰的,事务运用的GlobalExceptionHandler应该承继common-frame的GlobalExceptionHandler。

结合第五篇:从零到一建立根底架构(5)-让你的RPC原地起飞,你觉得事务运用承继common-frame的GlobalExceptionHandler后会有什么问题?

Controller接口与ApiImpl(RPC接口完成)本质上都是契合Spring Web接口界说的,都是标注了@Controller或许@RestController的。关于apiImpl来说,接口假如产生了反常,我期望经过具体的反常来告知给调用方。可是因为GlobalExceptionHandler的存在,rpc的反常将会被处理包装成标准结构回来,而导致Jackson序列化失利。

比方调用如下的rpc接口产生反常

@RequestMapping(VersionConfig.COMMON_RPC_VERSION_URL+"user")
public interface UserApi {
​
    @PostMapping("/by_id")
    UserDTO getUserDetail(@RequestParam("String") String id);
​
}

它将被GlobalExceptionHandler阻拦,响应给调用方报文是

{
  "code": 500,
  "errorCode": null,
  "message": "恳求失利",
  "traceId": null,
  "data": null
}

而UserDTO的结构为

@Data
public class UserDTO {
​
    /**
     * 用户id
     */
    private Long id;
​
    /**
     * 用户名
     */
    private String userName;
​
    /**
     * 用户展示名称
     */
    private String realName;
}

Spring运用Jackson将rpc报文映射成UserDTO会报错,而调用rpc接口真实的反常将被掩盖。

因而在common-frame中界说了几个注解

image-20221024173310852.png

假定事务运用的GlobalExceptionHandler仅对web controller恳求收效,不效果与rpc 恳求。

那么咱们能够在Controller上标识@Web的注解,在RPC接口上标识@Rpc注解。界说UserGlobalExceptionHandler为

@ControllerAdvice(annotations = Web.class)
public class UserGlobalExceptionHandler extends GlobalExceptionHandler{
​
}

这样controller的恳求产生反常将会包装为统一响应的报文,rpc的恳求产生反常将会直接传递反常。

2.4.链路信息赋值

这个操作比较好了解,在分布式运用下,咱们运用大局的链路id来跟踪恳求的调用链。因而咱们在恳求报文中需求把当时恳求上下文的链路id回来出去,咱们才能根据链路id来进行定位

@ControllerAdvice
public class AddTraceIdResponseBodyAdvice implements ResponseBodyAdvice<BaseResult> {
​
    @Autowired
    private Tracer tracer;
​
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return BaseResult.class.isAssignableFrom(returnType.getParameterType());
    }
​
    @Override
    public BaseResult beforeBodyWrite(BaseResult body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        body.setTraceId(tracer.currentSpan().context().traceIdString());
        return body;
    }
}

三、总结

本文从MVC分层责任动身,知道了工程门面的界说:承受恳求、校验参数、参数转化、映射Service的成果。

并介绍了能够作为门面的五种类型:rpc接口完成、MQ顾客、守时使命、发动后使命、controller

最后为我们介绍了门面层的装备与service/component的装备独立开的必要性,并讲解了common-frame中所供给的的几个公共装备。

image.png

四、联络我

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

微信:baiyan_lou

我的第一本小册《深入浅出DDD》现已在上线,欢迎我们试读~

DDD的微信群我也现已建好了,因为文章内不能放二维码,我们能够加我微信,备注DDD沟通,我拉你进群,欢迎沟通共同进步。