前言
本章节,我们要完善公共微服务,主要涉及统一参数校验,统一结果响应,统一异常处理,统一日志记录等方案的设计与实现。
这一章,会在在smartcar-common项目和smartcar-member项目中来回完善,注意观察代码位置。
话不多说,开始上菜~
一、公共工具包
在springcloud微服务架构中,common项目是不用部署的,其他子项目依赖common服务,它只是用来提供其他子项目的共同部分,目的是减少代码重复。
所以,我们会把一些通用的功能,放到common项目中,比如mysql、mybatis、lombok、commons、fastjson、knife4j、validation、hutool…等通用工具类的依赖。
1.1.引入公共依赖
在smartcat-common项目的pom文件中,引入一些常用依赖。
如下:
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<mybatis.version>2.1.4</mybatis.version>
<mysql.version>8.0.20</mysql.version>
<lombok.version>1.18.8</lombok.version>
<commons.version>4.4</commons.version>
<fastjson.version>1.2.62</fastjson.version>
<knife4j.version>2.0.8</knife4j.version>
<validation.version>2.3.5.RELEASE</validation.version>
<hutool.version>5.6.3</hutool.version>
</properties>
<dependencies>
<!--hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--校验类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${validation.version}</version>
</dependency>
<!--Knife4j文档类-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!--lombok工具类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>compile</scope>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>${mysql.version}</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!--json处理类-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!--单元测试类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
项目中引入这些公用依赖之后,其他子项目只要引入了common服务,就获得了这些公共的能力。
二、统一参数校验
后端接口一般对请求的参数要求进行安全校验,参数校验的重要性自然不必多说。所以我们引用了spring-boot-starter-validation 依赖(底层是Hibernate Validator),它可以非常方便的制定规则,并自动完成参数校验。
常用的校验注解:
@Max 所注解的元素必须是数字,且值小于等于给定的值
@Min 所注解的元素必须是数字,且值小于等于给定的值
@Range 所注解的元素需在指定范围区间内
@NotNull 所注解的元素值不能为null
@NotBlank 所注解的元素值有内容
@Null 所注解的元素值为null
@Past 所注解的元素必须是某个过去的日期,要求date类型
@PastOrPresent 所注解的元素必须是过去某个或现在日期,要求date类型
@Future 所注解的元素必须是将来某个日期,要求date类型
@Pattern 所注解的元素必须满足给定的正则表达式,可用匹配。
@Size 所注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内
@Email 所注解的元素需满足Email邮箱格式
@AssertFalse 所注解的元素必须是Boolean类型,且值为false
@AssertTrue 所注解的元素必须是Boolean类型,且值为true
@DecimalMax 所注解的元素必须是数字,且值小于等于给定的值
@DecimalMin 所注解的元素必须是数字,且值大于等于给定的值
@Digits 所注解的元素必须是数字,且值必须是指定的位数
因为smartcar-member在之前的章节中,就已经引入了common依赖,也就是说可以在member项目中进行参数校验了。
2.1.实体类上加上注解
在smartcar-member 项目的实体类上,给需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息。
举例如下:
@Data
@Accessors(chain = true)
@ApiModel(value = "用户/会员信息表")
public class MemberUser {
@ApiModelProperty("手机号")
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[1]+\\d{10}$",message = "手机号格式错误")
private String phone;
@ApiModelProperty("生日")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "GMT+8")
@Pattern(message = "无效的出生日期!",regexp = "(([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|((0[48]|[2468][048]|[3579][26])00))-02-29)")
private String birth;
@ApiModelProperty("真实姓名")
@NotBlank(message = "姓名不能为空")
@Size(min=2, max=8,message = "您的名字也太长了吧~")
private String realName;
@NotNull(message = "密码不能为空")
@Size(min = 6, max = 12, message = "长度必须是6-12个字符")
private String password;
}
什么,正则表达式不会写?正则生成工具,拿去拿去。
2.2.Controller中加上注解
校验规则和错误提示配置完毕后,接下来只需要在接口需要校验的参数上加上
@Valid 或@Validated 注解,并添加BindResult参数来返回信息,即可:
@Api(tags = "用户模块")
@RestController
@RequestMapping("member")
public class MemberUserController {
@PostMapping("/checkAddUser")
public String addUser_Validator(@RequestBody @Validated MemberUser user,BindingResult bindingResult) {
if(bindingResult.hasErrors()){
for (ObjectError error : bindingResult.getAllErrors()) {
return error.getDefaultMessage();
}
}
List<MemberUser> users = memberUserService.queryUser(user.getPhone());
return users.toString();
}
}
2.3.测试参数校验
启动项目,打开swagger界面,发起请求,出现错误提示:
这样就达到了参数校验的要求,更多参数校验,直接查看源码照样配置即可。
在上诉方式中,每写一个接口都要添加一个BindingResult参数,然后再提取错误信息返回给前端,属实不妥,有没有解决办法呢?有,进行全局异常处理。
三、统一业务异常处理
在Spring Cloud的微服务架构中,一个微服务通常不可避免地要同时 面向内部
和 面向外部
提供相应的功能接口。
-
面向外部提供的服务接口时,会通过服务网关(如Zuul、Gateway)来给客户提供服务,如用户登陆、注册等接口。
-
面向内部提供的服务接口时,会通过远程调用(如Feign、Ribbon)微服务间彼此的内部方法,从而实现一个完整的业务功能。
所以:
- 在编写面向外部的服务接口时,服务端所有的异常处理我们都要进行相应地捕获,并在controller层映射成相应的错误码和错误信息。因为这种接口是直接暴露给用户的,需要友好的提示,否则用户看到会一头雾水。
如出现异常或错误,则会返回错误码和错误信息,如:
{
"code": "500",
"msg": "请求参数错误",
"data": null
}
- 面向内部的接口发生异常时,我们则会更直截了当一些,就像调用本地接口一样清楚错误位置。通常把这种错误信息以异常的方式被集成的FeignClient捕获,进行Fallback处理,这个我们放到后面章节详说。
无论是面向内部或外部的微服务,在服务端我们都应该设计一个全局异常处理类,来统一异常处理,这也是优化代码结构的一种方式。
3.1.自定义业务异常介绍
在一个大型项目中,通常要求自定义异常类型,但是,保持一个合理的异常继承体系是非常重要的。
一般常见的做法是自定义一个
BaseException
类作为“ 根异常 ”,然后,派生出各种业务类型的异常。
如下:
BaseException 需要从一个适合的Exception派生,通常建议从RuntimeException派生:
public class BaseException extends RuntimeException {...}
其他业务类型的异常就可以从BaseException派生:
public class UserNotFoundException extends BaseException {...}
这样,抛出异常的时候,就可以选择合适的构造方法。
3.2.建立业务异常基础类
在smartcar-common项目中,新增exception文件和BaseException类。
代码如下:
/**
* @Description: 1.异常处理-基础类
* @Author zoutao
* @Date 2021/4/24
*/
@Data
public abstract class BaseException extends RuntimeException {
/**
* 所属模块
*/
private String module;
/**
* 错误码
*/
private String code;
/**
* 错误码对应的参数
*/
private Object[] args;
/**
* 错误消息
*/
private String defaultMessage;
public BaseException(String module, String code, Object[] args, String defaultMessage) {
this.module = module;
this.code = code;
this.args = args;
this.defaultMessage = defaultMessage;
}
public BaseException(String module, String code, Object[] args) {
this(module, code, args, null);
}
public BaseException(String module, String defaultMessage) {
this(module, null, null, defaultMessage);
}
public BaseException(String code, Object[] args) {
this(null, code, args, null);
}
public BaseException(String defaultMessage) {
this(null, null, null, defaultMessage);
}
}
3.3.建立业务异常类
再新建一个BusinessException 业务异常类,来继承该根异常类,这样就可以捕捉对应的业务异常。
/**
* @description: 2.业务异常处理类
* @author: zoutao
* @date: 2021/4/24
*/
public class BusinessException extends BaseException {
private static final long serialVersionUID = 1L;
public BusinessException(String module, String code, Object[] args, String defaultMessage) {
super(module, code, args, defaultMessage);
}
public BusinessException(String module, String code, Object[] args) {
super(module, code, args);
}
public BusinessException(String module, String defaultMessage) {
super(module, defaultMessage);
}
public BusinessException(String code, Object[] args) {
super(code, args);
}
public BusinessException(String defaultMessage) {
super(defaultMessage);
}
}
这种类可以建立多个,来对应不同的业务异常,这些异常需要我们手动抛出,比如在业务层中有些条件并不符合业务逻辑时,就可以抛出它。
3.4.测试业务异常抛出
在smartcar-common项目中新建一个测试类,测试业务异常。
使用示例:
import com.smart.car.common.res.exception.BaseException;
/**
* TODO 业务异常测试
* @author zoutao.blog.csdn.net
* @date 2021/5/12
*/
public class ExceptionTest {
public static void main(String[] args) {
String token = login("admin","95271");
System.out.println(token);
}
static String login(String username, String password) {
if (username.equals("admin")) {
if (password.equals("9527")) {
return "login successful";
} else {
throw new BusinessException("Error username or password.");
}
} else {
throw new UserNotFoundException("User not found.");
}
}
}
//自定义业务异常类02
class UserNotFoundException extends BaseException {
public UserNotFoundException() {
super();
}
public UserNotFoundException(String message) {
super(message);
}
}
运行main方法,结果图示:
这样就证明统一的业务异常处理方法被调用了。
注意:
除了自定义的业务异常类以外,我们还可以在各个服务中,建立一个全局异常处理类,来自动抛出异常,比如对参数校验引发的不明确型的异常,需要使用到SpringBoot中的
@RestControllerAdvice
注解。具体实现我们放到对应子项目中去说。
四、统一结果响应
面向外部的接口请求时,我们通常会将接口的返回数据以JSON格式来进行响应,如接口处理成功后,返回信息如下:
{
"code": "200",
"msg": "请求成功",
"data": {
"realName": "zhangsan",
"phone": 18984714120
}
}
成功就返回的数据列表,失败就会返回异常捕捉后的提示信息,这样的统一响应数据格式,是前后端规范开发中必须要做的!
4.1.自定义响应结果体
返回的JSON数据,让其带上一些固有字段,用途如下:
- code为返回结果的状态码;
- msg为返回结果的提示消息;
- data为返回的业务数据;
这3个为固有属性,每次响应结果都会携带它们。
在smartcar-common项目中,新建一个ResponseResult类,作为统一的响应体,返回数据。
代码如下:
@Data
public class ResponseResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 返回的状态码
*/
private int code;
/**
* 返回的信息
*/
private String msg;
/**
* 返回的数据
*/
private T data;
/** 成功 */
public static final int SUCCESS = Constants.SUCCESS;
/** 失败 */
public static final int FAIL = Constants.FAIL;
public static <T> ResponseResult<T> ok()
{
return restResult(null, SUCCESS, null);
}
public static <T> ResponseResult<T> ok(T data) {
return restResult(data, SUCCESS, Constants.SUMSG);
}
public static <T> ResponseResult<T> ok(T data, String msg)
{
return restResult(data, SUCCESS, msg);
}
public static <T> ResponseResult<T> fail()
{
return restResult(null, FAIL, null);
}
public static <T> ResponseResult<T> fail(String msg)
{
return restResult(null, FAIL, msg);
}
public static <T> ResponseResult<T> fail(T data)
{
return restResult(data, FAIL, null);
}
public static <T> ResponseResult<T> fail(T data, String msg)
{
return restResult(data, FAIL, msg);
}
public static <T> ResponseResult<T> fail(int code, String msg)
{
return restResult(null, code, msg);
}
private static <T> ResponseResult<T> restResult(T data, int code, String msg) {
ResponseResult<T> apiResult = new ResponseResult<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
}
4.2.自定义状态码
code字段,通常,我们是定义为自己能看得懂的。
创建一个Constants 状态码类:
public class Constants {
/**
* 成功标记
*/
public static final Integer SUCCESS = 200;
public static final String SUMSG = "请求成功";
/**
* 对象创建成功
*/
public static final Integer CREATED = 201;
/**
* 请求已经被接受
*/
public static final Integer ACCEPTED = 202;
/**
* 操作已经执行成功,但是没有返回数据
*/
public static final Integer NO_CONTENT = 204;
/**
* 资源已被移除
*/
public static final Integer MOVED_PERM = 301;
/**
* 重定向
*/
public static final Integer SEE_OTHER = 303;
/**
* 资源没有被修改
*/
public static final Integer NOT_MODIFIED = 304;
/**
* 参数列表错误(缺少,格式不匹配)
*/
public static final Integer BAD_REQUEST = 400;
/**
* 未授权
*/
public static final Integer UNAUTHORIZED = 401;
/**
* 访问受限,授权过期
*/
public static final Integer FORBIDDEN = 403;
/**
* 资源,服务未找到
*/
public static final Integer NOT_FOUND = 404;
/**
* 不允许的http方法
*/
public static final Integer BAD_METHOD = 405;
/**
* 资源冲突,或者资源被锁
*/
public static final Integer CONFLICT = 409;
/**
* 不支持的数据,媒体类型
*/
public static final Integer UNSUPPORTED_TYPE = 415;
/**
* 失败标记
* 系统内部错误
*/
public static final Integer FAIL = 500;
/**
* 接口未实现
*/
public static final int NOT_IMPLEMENTED = 501;
/**
* 加入会员
* 状态-会员1否0是
*/
public static final Integer IS_MEMBERVIP = 0;
public static final Integer NOT_MEMBERVIP = 1;
}
还有更多的,自己定义就是了。
4.3.测试结果响应
使用示例,就在接口需要return返回信息的地方,采用如下写法即可:
return ResponseResult.fail("查询失败!");
return ResponseResult.ok();
或者:
ResponseResult<T> result = new ResponseResult<>();
result.setCode(200);
result.setData(userData);
result.setMsg("查询成功");
return result;
common项目的总体结构:
五、统一子项目的全局异常
要和第三点的业务异常进行区分,业务异常是放置在common项目中,用于捕捉各个微服务中,可能会出现的业务逻辑异常,需要我们手动抛出异常才会触发。
而这里说指的全局异常,是针对当前微服务下的全局异常,比如参数校验出现错误,但是错误是哪种,不便于手动捕获,所以,采用自动的全局异常来统一捕获这种不明确的异常。
自动拦截异常,我们可以用到springboot的
@RestControllerAdvice
注解,标识为统一异常处理类。
自动统一处理,我们可以用到springboot的@ExceptionHandler
注解,标识为统一异常处理的方法。
5.1.添加全局异常类
以smartcar-member项目为例,在其中新建一个类,在这个类上加上@RestControllerAdvice注解和@ExceptionHandler注解。
如下:
/**
* TODO 控制层的全局异常处理
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.smart.car.member.controller")
public class ExceptionControllerAdvice {
//参数校验异常处理
@ResponseBody
@ExceptionHandler(value= MethodArgumentNotValidException.class)
public ResponseResult handleValidException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{},异常类型:{}", e.getMessage(), e.getClass());
// 从异常对象中拿到ObjectError对象
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError)->{
// 提取错误提示信息
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
});
ResponseResult result = new ResponseResult();
result.setCode(Constants.VALID_EXCEPTION_CODE);
result.setMsg(Constants.VALID_EXCEPTION_MSG);
result.setData(errorMap);
return result;
}
//全局方法异常处理
@ExceptionHandler(value=Throwable.class)
public ResponseResult handleException(Throwable throwable) {
log.error("未知异常{},异常类型:{}", throwable.getMessage(), throwable.getClass());
ResponseResult result = new ResponseResult();
result.setCode(Constants.UNKNOWN_EXCEPTION_CODE);
result.setMsg(Constants.UNKNOWN_EXCEPTION_MSG);
return result;
}
}
启动项目,发起请求。触发参数校验异常捕获,图示:
证明统一参数异常处理方法被调用了。
再调用其他接口,触发全局异常捕获,如图:
证明统一处理方法也被调用了。
@ExceptionHandler(value=Throwable.class)
public ResponseResult handleException(Throwable throwable) {
log.error("未知异常{},异常类型:{}", throwable.getMessage(), throwable.getClass());
ResponseResult result = new ResponseResult();
result.setCode(Constants.UNKNOWN_EXCEPTION_CODE);
result.setMsg(Constants.UNKNOWN_EXCEPTION_MSG);
return result;
}
由此,可以在7个微服务中都添加这样的全局异常,防止前端出现不友好异常信息。
六、统一日志记录
web日志框架比较丰富,早期,很多项目采用Commons Logging加Log4j,由于spring boot对 logback已默认集成,因此推荐在项目中使用logback组件。
现在的微服务项目,通常采用logback+@slf4j,slf4j 类似于 Logging,是一个日志接口,Logback 类似于Log4j,是一个日志的实现。
6.1.日志格式配置
我们是在其他子项目中配置,因为common下是没有配置文件的。比如在smartcar-member项目的application.yml文件操作。
在yml配置日志相关的输出路径和等级信息:
#logback配置
logging:
level:
root: info
file:
name: ./tmp/member.log
级别排序为: TRACE < DEBUG < INFO < WARN < ERROR;
表示info级别的日志信息,都会记录到log文件中。(由于日志在我们本案例中没有太多用处,就简单配置即可)
6.2.测试生成日志文件
在需要的类上中添加lombok的 @Slf4j
注解,然后配合 log对象来输出日志信息即可。
log.trace("日志输出 trace");
log.debug("日志输出 debug");
log.info("日志输出 info");
log.warn("日志输出 warn");
log.error("日志输出 error");
启动项目,随便调用一下接口,日志文件就会生成,位置如图:
小课堂
1.BaseException类继承Exception或RuntimeException的区别?
Java compiler 要求所有的Exception要么被catch,要么被throw,除非这是一个RuntimeExeption(e instanceof RuntimeException)。
也就是说,通常的Exception一定要被处理,而RuntimeException不强制要求处理。
从JAVA的设计思想来看,这是从类/方法设计者角度来看待的两种异常。
- 一种是设计者认为这个方法在使用过程中开发者能够自行处理的异常,也就是说,这是一个可恢复的操作。
- 另一种是设计者认为开发者不能自行处理的异常,Jvm就会抛出,要求调用者去捕获该异常, 根本区别在于设计者认为开发者是否能够处理这个异常。
至于继承谁?得看你的类具体功能。如果你继承了Exception,要么抛出给上级调用者,要么调用异常代码时进行捕捉,设置相对应的处理方式。
如果继承的是RuntimeException,可以不用抛出,也可以不用捕捉,但问题是,在运行的过程中异常才会展现出来,一但出错,后面程序将无法继续运行。各有利弊。
总结:
如果extends Exception,则必须捕捉或者抛出,
如果 extends RuntimeException ,则可以不处理。
相关导航:
08优雅的生成百万测试数据
打造优秀的后端接口体系