企业级微服务开发实战(三):公共模块设计与统一规范封装

目录

[一 封装基础通用工具类](#一 封装基础通用工具类)

1.背景

2.公共模块和基础通用包创建成功

3.封装JSON基础工具类

(1)为什么需要Json工具类

(2)核心设计

(3)常见的JSON技术

(4)封装步骤

(5)常量问题

(6)泛型擦除问题

(7)Jackson核心配置详解

4.封装Bean拷贝工具类

(1)什么是Bean拷贝

(2)为什么需要Bean拷贝

(3)常见数据模型介绍

(4)核心设计

(5)拷贝方案选择

(6)总结

(7)Spring提供的Bean拷贝工具类问题

5.封装时间戳工具类

(1)核心设计

6.封装String工具类

(1)核心设计

(2)提供字符串的核心操作

(3)封装url匹配方法

7.封装工具类的原则

8.基础通用工具类验证通过

[二 封装统一模块](#二 封装统一模块)

1.接口文档

(1)内容

2.封装统一状态码

(1)意义

(2)核心设计

3.封装统一数据结构

(1)核心设计

[三 统一异常处理](#三 统一异常处理)

前提:一次请求里可能出现异常的地方

1.核心设计

2.微服务侧统一异常处理

3.网关侧统一异常处理

(1)具体方式

(2)handle方法具体实现

[四 统一自定义异常](#四 统一自定义异常)

1.为什么需要自定义异常

2.核心设计

3.代码实现

4.异常捕获

5.测试

(1)微服务侧测试

(2)网关侧测试

[五 封装统一规范化参数校验](#五 封装统一规范化参数校验)

1.为什么要封装统一规范化参数校验

2.如何统一规划化参数校验

3.核心设计

4.spring-boot-validation集成步骤

(1)引入依赖

(2)添加注解

注解类型

请求参数根据的请求方法分为两类

根据参数的传递方式

(1)使用RequestBody接收参数(POST/PUT)

(2)使用RequestParam(GET/DELETE)

(3)使用PathVariable(GET/DELETE)

5.异常捕获

(1)问题

(2)具体处理

[六 统一常量](#六 统一常量)

1.为什么要封装常量

2.核心设计

3.代码实现

4.测试

[七 统一线程池](#七 统一线程池)

1.为什么需要封装统一线程池

为什么需要多线程

为什么需要线程池

2.ThreadPoolTaskExecutor介绍

3.核心设计

4.代码实现

(1)对于配置类的都需要加上@Configuration,并且启动异步处理需要加上@EnableAsync

(2)配置ThreadPoolTaskExecutor

5.测试

(1)引入封装了线程池的对应依赖

(2)测试线程池的基本功能

(3)验证个性化配置


一 封装基础通用工具类

1.背景

很多工具类在项目中使用频繁,并且本质都是一样的,与业务无关,因此可以抽取出来,方便后续进行复用

2.公共模块和基础通用包创建成功

通用工具类封装:

3.封装JSON基础工具类

(1)为什么需要Json工具类

①网络传输:Json格式的的数据量相对较小,在带宽有限的网络环境下,能够高效地传输数据

②序列化存储:比如要存储在redis等存储机制中,json会比xml占据更少的空间

③日志输出:用json来输出对象,节省空间

(2)核心设计

①提供json转对象

②提供对象转json

③提供对象转json格式化

(3)常见的JSON技术

①Gson:

(Ⅰ)性能:中规中矩,但是大多数的应用场景已足够

(Ⅱ)对象序列化:在处理大对象和深层次嵌套对象时性能会下降

(Ⅲ)日期处理:默认的日期处理较为简单,通常需要定制序列化反序列化格式

②FastJson:高速著称,性能优秀

(Ⅰ)安全性:FastJson曾爆出一些安全漏洞,因此需要关注库的更新,并在使用时启用安全模式

(Ⅱ)兼容性:处理复杂对象或者特定情况下,可能需要调整配置或自定义序列化器

③Jackson:

(Ⅰ)性能:Jackson性能非常好,适合于大规模数据处理

(Ⅱ)配置复杂度:功能强大,但配置和定制化选项较多,学习曲线较陡

(Ⅲ)版本更新:更新较为频繁,开发者需要时刻关注版本的变动和更新

选型建议性能:FastJson > Jackson > Gson

功能:Jackson > FastJson > Gson

易用性:Gson > Jackson > FastJson

社区和⽂档:Jackson > Gson > FastJson

使用场景和建议

简单应用:如果只是简单的对象-JSON转换且性能要求不⾼,Gson是⼀个不错的选择。

性能优先:在对性能要求较⾼的应⽤中,FastJson可能是更好的选择,但要注意安全问题。

功能丰富:如果需要丰富的功能和强⼤的⾃定义⽀持,Jackson是最佳选择,特别是在企业级应⽤中。

(4)封装步骤

①引入对应依赖:

XML 复制代码
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>

②ObjectMapper进行初始化:设为静态变量,在静态代码块进行初始化,并设置好对应的配置参数

③objToString:对于为null的也直接处理为null,对于String类型的则无需再安装

④objToStringPretty:将字符串该换行的换行,该缩进的缩进

⑤stringToobj:对于字符串的判空操作,也需要提取出来,以及class都需要进行null值判断

注:三者都需要对于obj的类型为String进行处理

(5)常量问题

常量在代码中是写死的,以及没有注释未经解释的,被成为魔法值

带来的问题:

①代码可读性差:对于后面阅读代码的人是个谜团

②代码维护困难:这个常量可能在代码的不止一个地方出现,如果要进行修改的话,就得在多个地方进行查找和修改

(6)泛型擦除问题

①概念:泛型擦除是指Java编译器在编译泛型代码时,会移除泛型类型参数的相关信息,使得生成的字节码文件不包含泛型类型信息,这使得Java泛型在运行时表现为原始类型

因此原本为List<T>类型对象转为字符串再转回原对象的时候,无法成功进行转回

②为什么出现:

(Ⅰ)向后兼容:Java5后才开始支持泛型,为了新版本代码和老版本代码兼容,泛型擦除是一种简单的实现方法,因为泛型的信息已经在运行的时候擦除了,因此旧版本的JVM可以运行新版本的代码

(Ⅱ)类型安全:虽然泛型擦除,但是在编译阶段会进行检查,对于这种类型错误的情况是可以发现解决的

(Ⅲ)避免性能开销:运行时无需保存泛型的信息,可以减少性能开销

③解决方法:

对于常见的集合类型先反序列化Collection为泛型Collection

如果是ArrayList<YourBean>那么使⽤ObjectMapper 的

getTypeFactory().constructParametricType(collectionClass, YourBean.class); 如果是

HashMap<String,YourBean>那么 ObjectMapper 的

getTypeFactory().constructMapType(HashMap.class,String.class, YourBean.class);]

得到JavaType,然后再将其传入readValue作为Class进行解析

注:此处的HashMap的key我们限定为String类型的,使用频率最多,其他类型的可以自己再去包装

④对于嵌套泛型类型的还是无法解决,此时可以写一个传入TypeReference类的方法,用于处理这种情况,后续使用的时候只需要构造一下new TypeReference<T>,把要反序列化的类进行传入即可

(7)Jackson核心配置详解

Jackson在序列化和反序列化发挥作用,我们学习的时候就需要搞清楚它是在序列化的时候起作用还是在反序列化的时候起作用

①.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false):在反序列化时,遇到字符串里有的属性但是java类的里没有,此时默认会抛异常,设置为false时不会抛异常

②.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false):在序列化时,如果Java对象中包含日期类型,默认情况下Jackson可能会将日期转换为时间戳,设置为false,其则不会转换为时间戳,而是根据后续的配置去

③.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false):在序列化时,Java对象中不存在任何属性值(为空),默认是抛出异常的,设置为false则可以将其变为不抛出异常

④.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false):在反序列化时,json数据中指定的类型信息与期望的java层次结构不符

具体解释:适用于有继承关系的类,对于其子类的序列字符串,反序列化时传的是其父类的类,此时要能正确将子类的属性也正常进行反序列化可以在父类上加上这个注解,然后序列化字符串上加上type属性的值即可

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")

@JsonSubTypes({

@JsonSubTypes.Type(value = TestDog.class, name = "dog"),

@JsonSubTypes.Type(value = TestCat.class, name = "cat")

})

注:此时只需要在json字符串里加上type="cat",然后转换的Class传为TestAnimal,其也能正确转换为TestCat

⑤.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false):在序列化时,Java对象里有以Date类型作为键的,设置为true则转换为时间戳,设置为false,则按照其他日期的相关配置来进行处理,默认值为false

⑥.configure(MapperFeature.USE_ANNOTATIONS, false):Jackson支持在Java类的属性和方法上添加各种注解来定制序列化和反序列化,将此项设为false,表示不依赖此项进行操作

例如:上面的@JsonSubTypes,@JsonProperty(可以让其序列化的时候不按照类的属性名输出,而是按指定的属性名进行输出

⑦.addModule(new JavaTimeModule()):这是序列化LocalDateTIme和LocalDate的必要配置,由Jackson-data-JSR310实现,默认Jackson是不支持序列化这两种类型的

⑧.defaultDateFormat(new SimpleDateFormat(CommonConstants.STANDARD_FORMAT)):所有的日期格式都统⼀为以下的样式,即yyyy-MM-dd HH:mm:ss(只针对于Date类型)

⑨.addModule(new SimpleModule()

.addSerializer(LocalDateTime.class, new

LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) //序列时

起作用

.addDeserializer(LocalDateTime.class, new

LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) //反序

列时起作用

)

对于LocalDateTIme和LocalDate时间类型序列列化时转换为json字符串时可以按照指定格式转

换,

反序列化时可以也可以将指定的时间字符串类型转换为LocalDateTIme和LocalDate。

⑩.serializationInclusion(JsonInclude.Include.NON_NULL) :只针对非空的值进行序列化(传大量null的值也会占用带宽)

4.封装Bean拷贝工具类

(1)什么是Bean拷贝

将一个Java对象(源Bean)属性值复制到另一个Java对象(目标Bean)中的操作

(2)为什么需要Bean拷贝

因为web项目中经常会分为多层,不同层之间使用的数据模型不同,此时就需要将数据从一种数据模型转换为另一种数据模型,因此需要Bean拷贝

那么为什么需要这么多种数据模型呢?

(3)常见数据模型介绍

DO,BO,DTO,VO,POJO

DO:在DAO层与数据库进行交换的对象

BO:存在于业务逻辑层,它封装了业务逻辑的相关的数据和操作,BO不仅仅是简单的数据容器,还包含了业务规则和业务流程的相关方法

DTO:是一个用来传输数据的对象,DTO用于不同层之间数据的传递,比如service层传controller层,controller传service层,避免层与层之间传递一些不必要的数据,同时可以隐藏内部数据结构和实现细节

VO:用于展示数据的对象(返回给前端)

POJO:表示Java的普通对象,没有特殊的要求和限制,可以用来表示任何类型的数据

好处:

①提高代码的可读性和可维护性:开发人员能更清楚数据的流动和使用方式

②解耦:不同层之间的修改不会互相影响

③优化性能:比如DTO和VO可以根据自身功能和需要对进行裁剪和拼装,合理利用网络传输资源,优化性能

划分过细的缺点:

①增加复杂度:划分的越多,开发者需要理解的就越多,每个人的理解本身又是不一样的

②过度设计:有时本身是比较简单的一个模块,引入了过多的抽象和复杂性,提高了开发成本,过多的对象转换和数据处理可能反而增加数据开销

③维护成本上升:划分的对象变多,需要维护的对象也变多

④耦合度增加:有的时候分的太细致了反而增加了耦合度`

注:一个东西再好也要考虑其缺点,也要注意适度

重要:使用数据模型之前,要在团队达成共识,概念一致

此处的模型约定:

(Ⅰ)DO默认使用实体类原名,如UserDO简写为DO,

(Ⅱ)对于服务层,入参和出参都是DTO,入参为xxxReqDTO,出参为xxxDTO

(Ⅲ)控制层,使用VO

(4)核心设计

①提供基础对象的拷贝功能

②提供列表对象的拷贝功能

(5)拷贝方案选择

①手动拷贝:一个属性一个属性进行set

②拷贝工具类:BeanUtils.copyProperties()

无法解决的情况:

(Ⅰ)当源对象和目标对象的属性名称不一样的时候,无法正常转换

(Ⅱ)源bean转目标bean时,需要对源bean内的属性进行处理后再存于目标bean中

③手动拷贝+拷贝工具类:

单个对象的普通拷贝:直接使用spring的拷贝工具类

单个对象的特殊拷贝:需要手动拷贝,针对具体对象提供专门的bean拷贝方法

对其他大部分一致的直接用BeanUtils.copyProperties(),对于部分特殊手动进行处理

如果需要特殊处理就直接在源bean类添加对应的转换方法

④内置拷贝工具类+拷贝工具类+手动拷贝:

列表对象的普通拷贝:spring提供的拷贝类无法支持列表直接拷贝,所以我们需要内部自己封装一个bean拷贝类

(6)总结

单个对象的普通拷贝:使用spring的工具类

单个对象特殊拷贝:使用spring工具类+手动拷贝,写在源Bean的类内作为成员方法

列表对象的普通拷贝:使用自己封装的内置工具类

列表对象的特殊拷贝:使用自己封装的内置工具类+手动拷贝,作为源Bean的静态方法

(7)Spring提供的Bean拷贝工具类问题

①属性类型不一致导致拷贝失败

(a)不同类的属性的类型不同会导致拷贝失败,比较A类id属性的类型的String,B类id属性的类型是Integer,此时会拷贝失败

(b)当A类的类型是long而B类的类型是Long时,此时也会导致拷贝失败,会抛出FatalException

②底层实现为反射拷贝效率低:

底层实现是先通过反射先获取get和set方法然后再进行拷贝

③BeanUtils.copyProperties()是浅拷贝,先拷贝其它属性,对于类型是自定义类的,单独为目标bean创建一个对象再进行拷贝

④内部类数据是无法拷贝过来的:源bean类内定义有内部类,目标bean也有,但是此时没办法直接拷贝过去,处理方式与深拷贝类似

注:对于所有使用到spring封装Bean拷贝工具类的,都需要考虑到上面几个问题,特别是深浅拷贝的问题

5.封装时间戳工具类

(1)核心设计

①获取当前时间戳②返回未来x秒/x天/x月/x年的时间戳③计算两个时间戳之间的差异

所有的方法均提供返回秒级和毫秒级的实现

6.封装String工具类

(1)核心设计

①提供字符串的核心操作(使用现成提供成熟的工具类)

②判断url是否与规则匹配+判断url是否与指定匹配规则链表中的任意一个匹配规则匹配(自己进行实现)

注:@Deprecated:被弃用的类

(2)提供字符串的核心操作

引入apache依赖:

XML 复制代码
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>

(3)封装url匹配方法

使用AntPathMatcher

匹配原则:

?:匹配单个任意字符

*:匹配任意字符(不跨层级)

**:匹配任意字符串(可跨层级)

对于列表url的匹配的封装方法的类似,参数校验那块需要校验列表是否为null或是否为空,并且这个判断很常用,应当进行封装,我们的封装原则还是先选择成熟厂商提供的,没有我们再自己封装,对于这个方法apache为我们提供了CollectionsUtils.isEmpty方法,我们直接使用即可

注意选择厂商提供的工具类尽量选择统一一个厂商的,减少引入

7.封装工具类的原则

①首先从成熟的厂商里去找,推荐spring,apache,具体不做限制,但要安全可靠,明确其特点,在使用的时候要注意其特点

②如果成熟厂商没有提供我们需要的方法,在内部对其进行封装

③公司如果对此有明确要求,以公司要求为准(常见要求:对于一种工具类的使用尽量统一

8.基础通用工具类验证通过

二 封装统一模块

1.接口文档

(1)内容

①接口概述:接口名称,接口功能,接口类别

②接口地址

③请求方法

④请求参数

⑤响应数据

⑥请求和响应示例

2.封装统一状态码

(1)意义

①简化接口使用方开发

②明确性

③易检索:状态码通常是数字,方便在日志,监控系统错误时查看

④可维护性:后面需要修改只需要修改定义,不需要到处修改

⑤错误分类:可将错误码分为客户端错误,服务器错误,业务逻辑等,方便一查看就知道是哪部分出现错误了

⑥统一性:不同项目的状态码大多都是统一的

(2)核心设计

(1)对于状态码,统一异常处理,统一结果返回等都属于协议相关的内容,我们将其放于bite-common-domain

(2)定义状态枚举,添加状态编码和状态消息来作为状态的基本信息

因为我们希望这个状态定义完了之后不轻易改变,而枚举正好能满足这个要求

(3)状态码的定义:状态码的定义要有可读性,此处采用前三位为HTTP状态码,后三位为项目中给出更细致的区分,便于定位错误的大方向以及定位项目的具体错误

前三位状态码对前后端来说都能先快速判断出大致是什么出了问题

3.封装统一数据结构

(1)核心设计

提供成功和失败的封装接口

注:使用@Getter和@Setter按需导入比@Data全部一起导入性能更好,按需导入,减少不必要的引入

三 统一异常处理

前提:一次请求里可能出现异常的地方

(1)请求到达网关,网关处理请求异常

(2)网关进行请求转发到微服务,转发的过程中服务发生异常(网络异常)

(3)微服务处理请求,处理请求的过程中出现异常

(4)微服务调用操作数据库,数据库操作异常

(5)数据库本身发生网络或其他异常

对于3,4,5异常,可以通过ExceptionHandler和@RestControllerAdvice进行处理,并返回全局统一数据结构

对于1,2,需要在网关服务配置全局异常处理

1.核心设计

(1)对于通过ExceptionHandler和@RestControllerAdvice进行处理的异常,我们统一封装在bite-common-security包下,为各个模块

(2)网关层面的全局异常,需要封装在网关微服务中

(3)异常处理好后返回统一的响应数据结构

(4)对于每种异常返回与之对应的状态码

(5)修改http状态码为状态码的前三位

注:不论是什么代码,重复写了好几次肯定是有问题的,要把他提取出来,后期才好维护,免得后面哪个地方有问题还得改多次,而且不一定能保证都改对了

注:每次启动服务前都坚持一下当前的中间件是否在部署状态

说明:做核心设计一方面不仅仅是为了让我们开发的时候有一个核心依据,保证我们写得代码不会出大问题,该写的东西都有写到,都有写完整,后面进行测试的时候也可以看一下哪些地方没测全,逐一去进行验证

2.微服务侧统一异常处理

封装一个GlobalExceptionHandler

注:此处的GlobalExceptionHandler需要进行注册,但是当前是在公共模块下,此时需要进行自动装配

自动装配的方式:

先在 resource 目录下创建META-INF⽬录,然后再在 resource ⽬录下创建spring⽬录,然后再

spring⽬录下创建

org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。

注:当前写的还无法处理网关层出现的异常

证明:当url的前缀正确,其能正确转发到对应的微服务中,此时后面的url找不到对应的,此时抛出的url未找到的异常,因为有对应的全局异常捕获器,因此可以被异常捕获器捕获,返回正确的数据结构

对于前缀不正常,网关无法正确进行转发的,此时抛出的异常我们在微服务侧定义的异常无法在网关层进行正确的捕获,此时无法返回正确的数据结构

3.网关侧统一异常处理

(1)具体方式

实现ErrorWebExceptionHandler接口

当异常发生时,实现ErrorWebExceptionHandler接口的类的handle 方法会自动调用对异常进行统⼀处理。

我们在handle方法里去写对异常的具体处理以及统一数据结构返回

(2)handle方法具体实现

注:对于网关侧的NoResourceFoundException,具体是找不到对应的服务,无法进行正确的转发

对于微服务侧的NoResourceFoundException,是转发到了具体的服务,但是在服务上找不到对应的路径

复制代码
/**
 * 处理器
 *
 * @param exchange ServerWebExchange
 * @param ex 异常信息
 * @return ⽆
 */
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
//响应已经提交到客⼾端,⽆法再对这个响应进⾏常规的异常处理修改了,直接返回⼀个含原始异常ex的Mono.error(ex)
        if (response.isCommitted()) {
            return Mono.error(ex);
        }
        String retMsg;
        int retCode=ResultCode.ERROR.getCode();
        if (ex instanceof NoResourceFoundException) {
            retCode = ResultCode.SERVICE_NOT_FOUND.getCode();
            retMsg = ResultCode.SERVICE_NOT_FOUND.getMsg();
        } else {
            retMsg = ResultCode.ERROR.getMsg();
        }
//按照统⼀状态码的特点,前三位是http状态码。从中截取http状态码
        int httpCode =
                Integer.parseInt(String.valueOf(retCode).substring(0,3));
        log.error("[⽹关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().
                getPath(), ex.getMessage());
        return webFluxResponseWriter(response,
                HttpStatus.valueOf(httpCode),retMsg, retCode);
    }
    private static Mono<Void> webFluxResponseWriter(ServerHttpResponse
                                                            response, HttpStatus status, Object value, int code) {
        return webFluxResponseWriter(response,
                MediaType.APPLICATION_JSON_VALUE, status, value, code);
    }

此处提供一个NoResourceFoundException的异常处理作为参考,后续其他异常的处理可以参照这个来进行

四 统一自定义异常

1.为什么需要自定义异常

(1)增加代码的可读性同时也符合业务规则和语义,业务逻辑往往自身的规则和限制,使用自定义异常可以更好地体现这些规则,代码也更易于理解和维护

(2)满足业务需要,内置的异常不能满足我们的需求,因此我们需要自定义异常类

(3)统一性:不同的业务需要定义不同的异常,但是这些异常有一些相似的地方,我们此处定义的是一个异常的模板,后续的异常都可以根据这个模板来进行定义

2.核心设计

(1)提供一种自定义异常的模板,实际项目从自身情况出发定义其他类型异常

(2)异常继承运行时异常,自定义异常往往需要运行时才能抛具体异常

(3)自定义异常核心组成包含状态码和状态码提示信息,后面R.fail要构造数据的时候可以很轻松地从异常里面拿信息,除此的其他内容具体视情况而定

注:脚手架提供最通用最基础的属性,后续根据具体的业务可以根据需要去新增别的属性

(4)提供三种使用方式:

①使用标准状态码,状态码和状态信息都从标准状态码进行获取

②仅传入状态信息,状态码使用默认的错误状态码

③传入状态信息和状态码

注:推荐归推荐,作为脚手架还是要把不同的使用方式都提供给用户,由用户根据自身的情况来进行选择

(5)提供异常捕获:微服务和网关服务侧都应该提供自定义异常的捕获和处理

注:核心设计对于整个模块很重要,建议所有的模块在开始写代码之前都先进行核心设计,然后再开始写代码,这样能保证写出来的代码逻辑是没问题和完整的,后续测试也可以1通过核心设计这块来逐一进行测试验证

3.代码实现

java 复制代码
package com.zhku.zhkucommondomain.exception;

import com.zhku.zhkucommondomain.domain.ResultCode;
import lombok.Getter;
import lombok.Setter;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 12290
 * Date: 2026-06-04
 * Time: 16:43
 */
/**
 * ⾃定义异常模板
 */
@Getter
@Setter
public class ServiceException extends RuntimeException {
    /**
     * 错误码
     */
    private int code;
    /**
     * 错误信息
     */
    private String message;
    //推荐的方式
    /**
     * 响应构造异常
     * @param resultCode 响应信息
     */
    public ServiceException(ResultCode resultCode){
        this.code=resultCode.getCode();
        this.message=resultCode.getMsg();
    }
    /**
    * 消息构造异常
    * @param message 异常消息
    */
    public ServiceException(String message){
        this.code=ResultCode.ERROR_CODE.getCode();
        this.message=message;
    }
    /**
     * 消息和响应码定制异常
     * @param message 消息
     * @param code 响应码
     */
    public ServiceException(int code,String message){
        this.code=code;
        this.message=message;
    }
}

4.异常捕获

微服务侧

java 复制代码
/**
     * 业务异常
     *
     * @param e 异常信息
     * @param request 请求
     * @param response 响应
     * @return 业务异常结果
     */
    @ExceptionHandler(ServiceException.class)
    public R<?> handleServiceException(ServiceException e, HttpServletRequest request,
                                       HttpServletResponse response) {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生业务异常", requestURI,  e);
        setResponseCode(response,e.getCode());
        return R.fail(e.getCode(), e.getMessage());
    }

网关侧

补充一个if-else的语句即可

java 复制代码
 if (ex instanceof NoResourceFoundException) {
            retCode = ResultCode.SERVICE_NOT_FOUND.getCode();
            retMsg = ResultCode.SERVICE_NOT_FOUND.getMsg();
        } else if(ex instanceof ServiceException ){
            retCode =  ((ServiceException) ex).getCode();
            retMsg = ex.getMessage();
        }

5.测试

(1)微服务侧测试

在test目录下对应的TestController下设置条件语句测试三种构造语句是否都能正常返回

(2)网关侧测试

定义一个全局过滤器,通过网关层的时候肯定会经过这个全局过滤器,在其方法内直接抛出ServiceException,因为是全局过滤器,因此不论测试哪个接口都会抛出异常

注:测试的时候可以对照着核心设计,看一开始的设计是否都进行了实现

五 封装统一规范化参数校验

1.为什么要封装统一规范化参数校验

(1)保证数据质量:避免因数据质量差导致业务逻辑错误,如空值(可能导致空指针异常),数据范围错误(比如传的是数组下标,可能出现数组下标越界异常),进行参数校验可以在一开始就避免这种情况继续往下执行

(2)保障系统安全:防止参数成为安全漏洞入口,防SQL注入,跨站脚本攻击

(3)增强系统稳定:减少异常和问题的出现,提供系统的稳定性

(4)统一性:这模块具有统一性和通用性

常见的参数校验类型:

数字:判空,大小范围

字符串:判空,字符串长度,

时间:判空,时间范围(比如活动的开始时间和结束时间)

引用-判空,除此之外还需要判断其内部的属性

其本质的判断逻辑的是相似的,只是具体的范围可能不同

注:有的时候觉得一个东西搞起来很麻烦,就去找一下有没有现成的框架

2.如何统一规划化参数校验

使用spring-boot-validation

3.核心设计

(1)提供通用的规范化参数校验方案

(2)通过注解可快速完成接口参数校验

(3)参数校验产生的异常,应由统一异常处理,返回前端统一数据结构

4.spring-boot-validation集成步骤

(1)引入依赖

XML 复制代码
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

(2)添加注解

注解类型

@NotNull:当前对象不为null

@NotEmpty:集合类,字符串,数组不为空,即长度大于0

@NotBlank:字符串的值不能为空白,即不能只包含空格

@Email:检查是否为邮箱格式

@Future:是否为未来日期

@Past:是否为过去日期

@Pattern:是否满足对应的正则表达式

①在controller层进行参数校验

②根据不同的请求分析清楚参数是怎么传递过来的,从而确定用于参数校验的注解所加的位置

请求参数根据的请求方法分为两类

Post/Put:使用RequestBody传递参数

Get/Delete:使用RequestParam/PathVariable传递参数

根据参数的传递方式
(1)使用RequestBody接收参数(POST/PUT)

①在DTO上声明约束注解

②在方法参数上声明校验注解

(2)使用RequestParam(GET/DELETE)

①使用DTO的形式,仍然是在对应的属性上加约束注解,在方法参数上声明校验注解(抛出MethodArgumentNotValidException)

②使用参数平铺的方式(GET/DELETE):

步骤(抛出ConstraintViolationException):

(Ⅰ)约束注解直接加在参数前面

(Ⅱ)在Controller类的前面统一加上@Validated

(3)使用PathVariable(GET/DELETE)

步骤(抛出ConstraintViolationException):

(Ⅰ)约束注解直接加在参数前面

(Ⅱ)在Controller类的前面统一加上@Validated

说明:根据需求确定请求方法--->根据请求方法确定参数传递方式--->确定参数接收方式--->如何进行参数校验

5.异常捕获

(1)问题

多个异常信息要怎么进行处理--任何形式都可以,只要与前端协商好即可

此处使用逗号分隔多个错误提示信息

(2)具体处理

MethodArgumentNotValidException异常:

java 复制代码
    /**
     * 参数校验异常
     * @param e 异常信息
     * @param request 请求
     * @param response 响应
     * @return 异常报文
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request,
                                                      HttpServletResponse response) {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生参数校验异常", requestURI, e);
        setResponseCode(response, ResultCode.INVALID_PARA.getCode());
        String message = joinMessage(e);
        return R.fail(ResultCode.INVALID_PARA.getCode(), message);
    }

    private String joinMessage(MethodArgumentNotValidException e) {
        //先获取错误列表
        List<ObjectError> errors=e.getAllErrors();
        //判空
        if(CollectionUtils.isEmpty(errors)){
            return "";
        }
        //把错误信息整合成字符串返回
        return errors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));
    }
    

ConstraintViolationException:

这个异常在spring-boot-starter-validation里,我们要在zhku-common-security里捕获需要引入对应的依赖

此处我们将spring-boot-starter-validation封装进zhku-common-security的原因:

①参数校验属于安全维度

②参数校验校验工具和微服务全局异常处理绑定在一起的,因为参数校验过程中可能会抛出异常,那么在项目里对这些异常的处理是必要的

③参数校验属于公共使用的模块,可以避免每个模块都得单独再引入一次

java 复制代码
 @ExceptionHandler({ConstraintViolationException.class})
    public R<Void>
    handleConstraintViolationException(ConstraintViolationException e,
                                       HttpServletRequest request,
                                       HttpServletResponse
                                               response) {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}', 发⽣参数校验异常",requestURI, e);
        setResponseCode(response,ResultCode.INVALID_PARA.getCode());
        String message = e.getMessage();
        return R.fail(ResultCode.INVALID_PARA.getCode(),message);
    }

注:参数校验的异常发生在网关层之后,网关层不需要对异常进行统一处理

六 统一常量

1.为什么要封装常量

(1)便于代码复用

(2)降低代码维护

(3)增强代码的可读性和可理解性:常量起的名字就能推断出这个常量的用途

(4)提高代码结构的清晰度

(5)统一性:不同项目的常量虽有特殊性,但是还是有一部分是统一的,我们提取公共统一的那部分

2.核心设计

(1)放在哪里:zhku-common-domain,因为常量也相当于是一种协议的值,常量的值很多时候是要给前端展示的,需要跟前端商量后展示

(2)封装通用的常量

(3)不同的类型的常量封装在不同的类,方便进行管理,统一命名XXConstants

(4)每个常量要有明确的语义名称

(5)但是由于每个人对英文的理解不同,因此常量及常量类需要写清楚的明确的注释

(6)常量类和常量不能冲突,比如CacheConstants,RedisConstants,比如默认值是使用INIT还是DEFAULT

(7)提供常量封装模板(CommonConstants)

3.代码实现

(1)常量封装方式--后续自己要在此基础上进行封装要怎么封装

原则:无则添加,有则跳过

①先对常量分类,如果已经有常量类,查看里面是否有重复的常量(看得时候不要只看英文,也要看中文注释,因为英文理解每个有偏差),若没有则进行添加

②如果没有对应的常量类,那就先创建先对应的常量类,再创建对应的常量

③如果类别不好分,那就先放到CommonConstants

(2)代码实现

4.测试

注:实际开发也是按照这个流程,先想清楚为什么要这样,然后就进行核心的具体设计,然后进行代码开发,然后进行测试

七 统一线程池

1.为什么需要封装统一线程池

需要使用和管理多线程

为什么需要多线程

(1)提高资源的利用率:提高cpu等资源利用率,单线程遇阻塞时CPU闲置,多线程可让其他线程利用空闲CPU,提升整体性能

举例:比如现在是单线程,借助cpu进行IO操作(文件读写,网络通信),若出现阻塞,此时就会释放cpu资源,那这段时间cpu资源就闲置了,整个就阻塞了,此时资源利用率就会比较低

若是多线程,即使其中一个线程阻塞了,它把cpu资源释放了,这块的资源也能被用来给其他线程继续处理业务,利用起来,资源利用率提高了

(2)增强程序性能和用户体验:

在交互应用中,一个任务可分成多个子任务并行执行使应用响应更迅速,对于比价耗时的应用可以利用多线程将其在后台执行不影响用户其他操作提升用户体验

比如用户访问商品详情页:

打开来有获取商品基本信息,获取库存状态,获取用户评价,获取类似商品

这些内容不可能会存在一起,要查询的内容就会比较多,如果单线程去查,那么耗时就会比较长

如果是多线程的话,耗时只看查询最长耗时的时间,而不是多个查询累加的,提高效率,提高性能,用户体验也提升

再比如,听歌软件,希望能够一边听歌一边下载,两头都不耽误,使用多线程能实现这一点,用户体验更好

为什么需要线程池

(1)资源管理:线程池可以有效地管理线程资源,避免因为线程的频繁创建和销毁导致资源浪费

(2)性能提升:线程的创建和销毁本身是一个相对耗时的过程,使用线程池可以复用自己创建的线程,减少系统开销,提高响应速度

(3)灵活性和可扩展性:线程池可以根据实际情况去调整线程池相关配置参数以适应不同的工作负载,提高系统的可扩展性,同时也可以限制并发线程的数量,防止系统因为线程过多而过度消耗资源,导致系统崩溃

(4)提供更强大的功能:延时定时线程池

(5)统一性:线程池配置代码逻辑,封装通用性的,保留特性(线程池的配置)

2.ThreadPoolTaskExecutor介绍

是Spring提供的一个用于异步任务的线程池实现类,它基于Java的ThreadPoolExecutor进行了封装,使得在Spring应用中更方便地配置和使用线程池来处理异步任务

使用时,使用配置类对ThreadPoolTaskExecutor进行配置,在业务代码中使用@Async完成多线程业务开发

3.核心设计

(1)封装在哪里:线程池属于通用基础包,封装在zhku-common-core

(2)具体封装什么:封装线程池核心配置代码

(3)线程池配置参数可根据实际情况动态配置(使用Nacos配置)

注:对于统一通用的我们就直接进封装,对于特性的配置我们通过Nacos实现动态配置

(4)通过注解可以快速完成多线程业务开发

(5)保持线程池具备个性化配置的特性(不同微服务可根据情况进行个性化配置

4.代码实现

(1)对于配置类的都需要加上@Configuration,并且启动异步处理需要加上@EnableAsync

(2)配置ThreadPoolTaskExecutor

java 复制代码
package com.zhku.zhkucommoncore.config;
import com.zhku.zhkucommoncore.domain.enums.RejectType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
/**
 * 线程池配置
 */
@EnableAsync
@Configuration
public class ThreadPoolConfig {
    /**
     * 核⼼线程数
     */
    @Value("${thread.pool-executor.corePoolSize:5}")
    private Integer corePoolSize;
    /**
     * 最⼤线程数
     */
    @Value("${thread.pool-executor.maxPoolSize:100}")
    private Integer maxPoolSize;
    /**
     * 阻塞队列⼤⼩
     */
    @Value("${thread.pool-executor.queueCapacity:100}")
    private Integer queueCapacity;
    /**
     * 空闲存活时间
     */
    @Value("${thread.pool-executor.keepAliveSeconds:60}")
    private Integer keepAliveSeconds;
    /**
     * 线程名称前缀
     */
    @Value("${thread.pool-executor.prefixName:thread-service-}")
    private String prefixName;
    /**
     * 拒绝策略 枚举取值参考:RejectType
     */
    @Value("${thread.pool-executor.rejectHandler:2}")
    private Integer rejectHandler;
    /**
     * 注册和配置线程池执⾏器
     *
     * @return 线程池执⾏器
     */
    @Bean("threadPoolTaskExecutor")
    public Executor getThreadExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setThreadNamePrefix(prefixName);
//策略
        executor.setRejectedExecutionHandler(getRejectHandler());
        return executor;
    }
    /**
     * 拒绝策略
     *
     * @return 拒绝策略处理器
     */
    public RejectedExecutionHandler getRejectHandler() {
        if (RejectType.AbortPolicy.getValue().equals(rejectHandler)) {
            return new ThreadPoolExecutor.AbortPolicy();
        } else if
        (RejectType.CallerRunsPolicy.getValue().equals(rejectHandler)) {
            return new ThreadPoolExecutor.CallerRunsPolicy();
        } else if
        (RejectType.DiscardOldestPolicy.getValue().equals(rejectHandler)) {
            return new ThreadPoolExecutor.DiscardOldestPolicy();
        } else {
            return new ThreadPoolExecutor.DiscardPolicy();
        }
    }
}

对于拒绝策略的配置说明:由于拒绝策略要求传的是一个对象,但是在配置中不好配置,因此我们使用传数字的形式,并且使用枚举将这些数字与具体的拒绝策略一一对应,通过一个方法去映射,这样就可以解决拒绝策略配置的问题

对于保持线程池具备个性化配置的特性(不同的微服务可根据情况进行个性化配置):

①统一线程池需要封装在公共包中,便于不同的微服务快速引入统一线程池

②不同的微服务可利用自身在Nacos的配置,对线程池进行个性化配置

5.测试

(1)引入封装了线程池的对应依赖

XML 复制代码
<dependency>
<groupId>com.zhku</groupId>
<artifactId>zhku-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

(2)测试线程池的基本功能

通过统一线程池能创建线程: 接口-->创建一个新线程

使用线程的用@Async(),将线程池传进去

(3)验证个性化配置

再创建一个模板微服务配置配置不同的参数进行验证

对应的配置项:

bash 复制代码
thread:
  pool-executor:
    corePoolSize: 20
    maxPoolSize: 50
    queueCapacity: 80
    keepAliveSeconds: 60
    prefixName: thread-service
    rejectHandler: 2
相关推荐
我登哥MVP1 小时前
Spring Boot 从“会用”到“精通”:参数绑定体系全景
java·spring boot·spring·servlet·maven·intellij-idea·mybatis
C137的本贾尼1 小时前
JDBC 编程:用 Java 连接 MySQL
java·开发语言·mysql
caimouse1 小时前
Reactos 第 3 章 内存管理 — 【上篇】用户态/内核态两侧的内存对象与地址映射
windows·架构
caimouse1 小时前
ReactOS 架构
架构
代码的小搬运工1 小时前
【iOS】MVC架构
ios·架构·mvc
华大哥1 小时前
spring boot 和php 调用 LibreOffice 转换 Excel 到 PDF 完整实现
java·pdf·excel
程序员佳佳1 小时前
向量引擎:AI 时代的“记忆中枢“,从原理到落地的完整认知框架
人工智能·gpt·架构·aigc·ai编程
微风欲寻竹影1 小时前
Java数据结构——二叉树相关OJ题目详解
java·数据结构
微风欲寻竹影1 小时前
Java数据结构——二叉树(Binary Tree)详解
java·数据结构·算法