前言
项目框架构建每个公司要求都不同,没有什么“一定是最好的”标准,但一个优秀的后端框架和一个糟糕的后端框架对比起来差异还是蛮大的,其中最重要的关键点就是看是否规范!
本文就一步一步演示如何构建起一个优秀的后端框架体系。
构建项目
导入web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
统一参数校验
后端接口一般都会做接口参数校验,比如非空校验、长度校验、格式校验等
“妈见打”校验方式:
public String addUser(User user) {
if (user != null) {
return "请输入用户信息!";
}
if (StringUtils.isEmpty(user.getUserName())) {
return "用户名不能为空!";
}
if (StringUtils.isEmpty(user.getPassword())) {
return "密码不能为空!";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return "邮箱格式不正确";
}
return "success";
}
这样写当然是没有什么错,但是这样写不利于维护、繁琐、代码冗余…
所以不要这么写
使用Validator进行校验:
Validator可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:
@Data
public class User {
@NotNull(message = "用户名不能为空")
private String userName;
@NotNull(message = "密码不能为空")
private String password;
@NotNull(message = "性别不能为空")
private String sex;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
记得在接口需要校验的参数上加上@Valid注解
ps:springboot2.3版本前spring-boot-starter-web自带validate依赖,
springboot2.3及以后需要另外加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
以为这样就完事了?
Validator + 自动抛出异常
用户名称不传,调用接口,控制台抛出MethodArgumentNotValidException异常
10:30:28.998 WARN 10252 --- [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.zhs.springboot.controller.UserController.addUser(com.zhs.springboot.model.entity.User): [Field error in object 'user' on field 'userName': rejected value [null]; codes [NotNull.user.userName,NotNull.userName,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.userName,userName]; arguments []; default message [userName]]; default message [用户名不能为空]] ]
参数校验不通过就会抛出异常,但是没有返回给前端,接下来捕获这个异常
新建全局异常处理类:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
StringBuilder errorMsg = new StringBuilder();
List<ObjectError> objectErrorList = e.getBindingResult().getAllErrors();
for (ObjectError objectError : objectErrorList) {
errorMsg.append(objectError.getDefaultMessage()).append(",");
}
errorMsg.delete(errorMsg.length() - 1, errorMsg.length());
// 然后提取错误提示信息进行返回
return errorMsg.toString();
}
}
请求接口,并响应
如果希望拦截到一个参数就返回,需要加上一个配置:
@Configuration
public class ValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
true为快速返回即拦截到一个参数时就返回
ok,终于拿到想要的结果!以后我们再想写接口参数校验,就只需要在入参的成员变量上加上Validator校验规则注解,然后在参数上加上@Valid注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!
统一异常处理
全局处理当然不会只能处理一种异常,用途也不仅仅是对一个参数校验方式进行优化。在实际开发中,如何对异常处理其实是一个很麻烦的事情。
自定义异常
先写一个异常抽象类(目的是为了方便扩展)
@Getter
public abstract class AbstractException extends RuntimeException {
/**
* 错误码
*/
private String errorCode;
/**
* 错误信息
*/
private String errorMessage;
/**
* 异常
*/
private Exception exception;
/**
* @param errorCode 错误码
* @param message 错误信息
*/
protected AbstractException(String errorCode, String message) {
this.errorCode = errorCode;
this.errorMessage = message;
}
/**
* @param errorCode 错误码
* @param exception 异常
* @param message 错误信息
*/
protected AbstractException(String errorCode, String message, Exception exception) {
this.errorCode = errorCode;
this.exception = exception;
this.errorMessage = message;
}
}
然后写一个异常类:
public class BizException extends AbstractException {
public BizException(String errorCode, String message) {
super(errorCode, message);
}
public BizException(String errorCode, String message, Exception exception) {
super(errorCode, message, exception);
}
}
全局异常处理类中记得捕获这个异常:
//业务异常
@ExceptionHandler(BizException.class)
public BaseResponse bizException(BizException ex) {
ex.printStackTrace();//打印异常信息,为了好排查问题
return new BaseResponse(ex.getErrorCode(), ex.getErrorMessage());
}
结果:
例子中写的是业务异常,主要处理业务相关的异常,也可以按模块来分,比如:用户相关异常等;
项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
统一数据响应体
无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!
@Getter
//@JsonInclude的作用 保证序列化json的时候,如果是null的对象,key也会消失
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseResponse<T> implements Serializable {
/**响应码*/
private String code;
/**响应信息*/
private String message;
/**数据长度*/
private Long count;
/**数据*/
private T data;
public BaseResponse(String code, String message) {
this.code = code;
this.message = message;
}
public BaseResponse(ResultCode resultCode) {
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
public BaseResponse(ResultCode resultCode, T data) {
this(resultCode);
this.data = data;
}
public BaseResponse(ResultCode resultCode, T data, Long count) {
this(resultCode);
this.data = data;
this.count = count;
}
public static BaseResponse createSuccess() {
return new BaseResponse(ResultCode.SUCCESS);
}
public static <T> BaseResponse<T> createSuccess(T data) {
return new BaseResponse<T>(ResultCode.SUCCESS, data);
}
public static <T> BaseResponse<T> createSuccess(T data, Long count) {
return new BaseResponse<T>(ResultCode.SUCCESS, data, count);
}
}
此响应体包适用返回数据和分页,满足大部分场景。
响应码枚举
@Getter
public enum ResultCode {
/**系统处理成功*/
SUCCESS("0000", "成功"),
/**系统未知异常*/
ERROR("9999", "未知异常"),
/***************************** 1000 运行时异常 *****************************/
/**空指针异常*/
NULL_ERROR("1001", "空指针异常"),
/**类型转换异常*/
CLASS_ERROR("1002", "类型转换异常"),
/**IO异常*/
IO_ERROR("1003", "IO异常"),
/**未知方法异常*/
UNKNOWN_ERROR("1004", "未知方法异常"),
/**数组越界异常*/
INDEX_OUT_ERROR("1005", "数组越界异常"),
/**栈溢出异常*/
STACK_ERROR("1006", "栈溢出异常"),
/**除数不能为0异常*/
ARITHMETIC_ERROR("1007", "除数不能为0异常"),
/***************************** 2000 公共组件异常 *****************************/
/**日期格式转换异常*/
DATE_ERROR("2001", "除数不能为0异常"),
/**线程池使用异常*/
THREAD_POOL_ERROR("2002","线程池使用异常"),
/**无法找到文件*/
FILE_NOT_FOUND("2003","无法找到文件"),
/**文件删除错误*/
FILE_DELETE_ERROR("2004","文件删除错误"),
/**Excel导出错误*/
EXCEL_EXPORT_ERROR("2005","Excel导出错误"),
/**MD5转换异常*/
MD5_ERROR("2006","MD5转换异常"),
/**密码加密错误*/
PASSWORD_ERROR("2007","密码加密错误"),
/***************************** 3000 参数校验异常 *****************************/
/**参数校验异常*/
PARAM_ERROR("3001", "参数校验异常");
private String code;
private String message;
ResultCode(String code, String desc) {
this.code = code;
this.message = desc;
}
}
可根据实际情况拓展
异常处理返回体:
//业务异常
@ExceptionHandler(BizException.class)
public BaseResponse bizException(BizException e) {
e.printStackTrace();
return new BaseResponse(e.getErrorCode(), e.getErrorMessage());
}
//方法参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public BaseResponse MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
StringBuilder errorMsg = new StringBuilder();
List<ObjectError> objectErrorList = e.getBindingResult().getAllErrors();
for (ObjectError objectError : objectErrorList) {
errorMsg.append(objectError.getDefaultMessage()).append(",");
}
errorMsg.delete(errorMsg.length() - 1, errorMsg.length());
// 然后提取错误提示信息进行返回
return new BaseResponse(ResultCode.PARAM_ERROR.getCode(), errorMsg.toString());
}
这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!
业务返回体:
数据响应:
已经拿到统一返回的格式
总结
项目体系该怎么构建都没有一个绝对统一的标准,不是说一定要按照本文的来才是最好的,这里还有待优化。
码云地址:https://gitee.com/Zhsai/spring-boot-frame