以下内容纯属个人扯淡,仅供参考
目录
参考:
统一结果返回
题外话:前后端分离是一种设计理念,数据传输格式一般都是json,因此统一一个规范的数据格式有利于双方代码约定。因此即便使用了Thymeleaf、Freemarker等框架,也尽量这样设计:提供单独的控制器方法仅用于返回视图,额外提供方法用于数据交互(但这样设计的话是无法实现纯粹的RestFul风格的)
参考:SpringBoot2.2.2.RELEASE+Thymeleaf
(1)统一数据格式
public class CommonResult<T> {
private long code;
private String message;
private T data;
protected CommonResult() {
}
protected CommonResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public long getCode() {
return code;
}
public void setCode(long code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
这个CommonResult就是一个领域类,类似DTO。它定义了code、message、data三个属性,分别表示统一数据格式中的状态码、信息、数据,构造器和setter/getter是一个DTO必备的。
定义完上述DTO类后,我们如果要返回数据到前端,那么就像这样,示例:
@GeiMapping("user/{userId}")
public CommonResult<User> getUser(@ParamVariable String userId){
User user = userService.getOne(userId);
if(data == null){
return new CommonResult("500","操作失败",user);
}
return new CommonResult("200","操作成功",user);
}
扩展:泛型T。这里定义data是泛型,那么他能接收任意类型数据,这里用Object类型替换也是能接受任意类型的,但会有强制类型转换风险问题,这是运行时才会报出的问题,使用泛型可以在编译期就能发现类型转换问题。参考:java 泛型和object比较
(2)状态码、消息体枚举类
就功能而言是没问题的,我们在任何需要返回给前端数据的地方调用即可,但是就代码维护而言是有很多问题的:
1.硬编码
"200"、"操作成功"等字面量不建议直接写在业务代码逻辑里(IDEA会提示你这是魔法值)
2.表意不够
200本身是个无表意的量值,而若定义为private static final String SUCCESS = "200"才能表达这个200是成功的意思,代码
并非你自己看得懂就足够了
3.散乱、重构不友好
RFC规范规定:第1位数字表示了5种响应状态:1=消息,2=成功,3=重定向,3=请求错误,5=服务器错误
这样的设计下,最多支持10类大类,每个大类下最多100种具体情况,即共1000种具体响应码
当系统业务后期扩大到一定规模时,3位状态码已不足以支撑所有所有情况,3位数太少表意不够。
可以设计为5位,前2位表示大类情况,后3位表示具体情况,就共10000种具体响应码
那么,当你要用5位去重构替换3位时,你就不得不修改整个Web每个控制器方法返回处的代码
我们必须要知晓所有接口的返回值,才能知道整个系统返回给前端的code有哪些类型
如果能定义到一个类中,那么就显而易见的了
code字段对前端来说至关重要,它的值有限可知的,只能是500、400、401等等,前端的需要根据code的具体值去控制js逻辑,例如:200表示正常,那么就进行接下来的操作:从data中获取数据显示等等;500表示异常,此时需要取出msg值提示用户失败。因此code值间接控制着前端的代码执行方向,必须谨慎对待
msg字段是提示信息,本身也不是很重要。我们可以设计成半有限可知的:既有提供默认的值,如:操作成功、操作失败等等,也支持根据特定情况传入特定的提示进行选择,("用户名错误",这样的提示信息是特定业务下的返回信息)
因此,可以将code和msg设计到一个枚举类中,其中每个枚举实例中保存着那些有限可知的值。
public enum ResultCode {
/**
* 操作成功
*/
SUCCESS(200, "操作成功"),
/**
* 操作失败,服务器内部错误
*/
FAILED(500, "操作失败"),
/**
* 参数检验失败
*/
VALIDATE_FAILED(400, "请求参数有误"),
/**
* 暂未登录或token已经过期
*/
UNAUTHORIZED(401, "登录失败"),
/**
* 没有相关权限
*/
FORBIDDEN(403, "没有相关权限"),
/**
* 未找到资源
*/
NOT_FOUND(404,"未找到相关资源");
private long code;
private String message;
private ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
public long getCode() {
return this.code;
}
public String getMessage() {
return this.message;
}
}
这里是定义一个普通的枚举类ResultCode,其中6个实例分别对应返回给前端的6种状态码和对应的默认提示信息,如果还有更多的情况,就直接在枚举类中添加对应实例即可
现在我们可以这样使用
@GeiMapping("user/{userId}")
public CommonResult<User> getUser(@ParamVariable String userId){
User user = userService.getOne(userId);
if(data == null){
//默认提示消息
return new CommonResult(FAILED.getCode(),SUCCESS.getMessage(),user);
//如果是要自定义的提示消息
//return new CommonResult(FAILED.getCode(),"用户不存在",user);
}
return new CommonResult(SUCCESS.getCode(),SUCCESS.getMessage(),user);
}
扩展:如果我们希望枚举实例拥有某种能力,通过这个能力能完成某件事情,比如:doEat(),每个枚举实例都需要有doEat()这样的能力,但是每个枚举实例执行eat的内容都不一样,可以用枚举类实现接口完成
public interface Eat {
void doEat();
}
这个时候枚举类就需要有一个Eat类型的成员去完成这样的事情,并且每个枚举实例做的事情内容不一样
public enum ResultCode {
SUCCESS(200, "操作成功",new Eat(){
@Override
public void doEat(){
System.out.println("成功吃");
}
}),
FAILED(500, "操作失败",new Eat(){
@Override
public void doEat(){
System.out.println("失败吃");
}
});
private long code;
private String message;
private Eat eat;
private ResultCode(long code, String message,Eat eat) {
this.code = code;
this.message = message;
this.eat = eat;
}
public long getCode() {
return this.code;
}
public String getMessage() {
return this.message;
}
public void doEat() {
eat.doEat();
}
}
这样,调用不同枚举实例去完成doEat事情时,它们都具备doEat()方法能力,但是有各自的实现
ResultCode.SUCCESS.doEat();
ResultCode.FAILED.doEat();
而当每个枚举实例在这样的方法内执行的代码逻辑是一样时,就可以将方法都抽离到公共接口中
public interface IErrorCode {
/**
* 获取code属性
*
* @return -
*/
long getCode();
/**
* 获取Message属性
*
* @return -
*/
String getMessage();
}
因此,最终枚举类被设计成这样
public enum ResultCode implements IErrorCode {
/**
* 操作成功
*/
SUCCESS(200, "操作成功"),
/**
* 操作失败,服务器内部错误
*/
FAILED(500, "操作失败"),
/**
* 参数检验失败
*/
VALIDATE_FAILED(400, "请求参数有误"),
/**
* 暂未登录或token已经过期
*/
UNAUTHORIZED(401, "登录失败"),
/**
* 没有相关权限
*/
FORBIDDEN(403, "没有相关权限"),
/**
* 未找到资源
*/
NOT_FOUND(404,"未找到相关资源");
private long code;
private String message;
private ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
@Override
public long getCode() {
return this.code;
}
@Override
public String getMessage() {
return this.message;
}
}
这实际是"面向接口"思想用在枚举类上,那么若有方法需要接收ResultCode枚举类作为形参时,我们就可以传递一个IErrorCode接口,而不是ResultCode类型
(3)统一结果生成工具类
上面的代码还是有一点冗余的。我们每次返回CommonResult对象,Controller控制器方法需要知道太多细节了:它需要知道CommonResult构造器的3个参数具体每个参数细节,例如:当用户不存在时,它应该传递一个null,而不是user=null。
return new CommonResult(FAILED.getCode(),"用户不存在",user);
return new CommonResult(SUCCESS.getCode(),SUCCESS.getMessage(),user);
因此,我们应该把这种细节交由另外一个类负责,控制器方法只需要选择方法。它可以选择不传递任何数据给CommonResult对象返回给前端,也可以传递数据、特定的提示信息
public class CommonResultUtil {
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 默认失败返回结果
* 使用{@link ResultCode#FAILED}
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 通用接口
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未认证/登录的返回结果
*/
public static <T> CommonResult<T> unAuthenticated(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权的返回结果
*/
public static <T> CommonResult<T> unAuthorized(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
}
那么,现在调用方法就可以改成
return CommonResultUtil.success(user);
return CommonResultUtil.failed("用户不存在"); //这个字面量最好也定义一个static final来代替
疑问:分页对象怎么传递呢?答:这里使用的是MybatisPlus。定义一个分页对象,再将分页对象传递给CommonResult即可,分页对象定义如下:
/**
* 通用分页
*
* @date 17:23 2020/3/27
* @author
**/
@Data
public class CommonPage<T> {
/**
* 当前页
*/
private long pageNum;
/**
* 单页记录数
*/
private long pageSize;
/**
* 总页数
*/
private long totalPage;
/**
* 总记录数
*/
private long count;
/**
* 数据集
*/
private List<T> data;
/**
* 封装List数据到
*
* @date 17:33 2020/3/27
* @author 李文龙
* @param
* @return
**/
public static <T> CommonPage<T> restPage(IPage<T> page) {
CommonPage<T> result = new CommonPage<T>();
result.setTotalPage(page.getPages());
result.setPageNum(page.getCurrent());
result.setPageSize(page.getSize());
result.setCount(page.getTotal());
result.setData(page.getRecords());
return result;
}
}
调用示例
@GetMapping("listAll")
public CommonResult<CommonPage<PctMenuVO>> listAll(
@RequestParam Integer page,
@RequestParam Integer limit,
@RequestParam(value = "name", required = false) String name) {
CommonPage<PctMenuVO> commonPage = pctMenuService.queryPctMenuByNameWithPaging(page, limit, name);
return CommonResult.success(commonPage);
}
//PctMenuServiceImpl
public CommonPage<PctMenuVO> queryPctMenuByNameWithPaging(Integer current, Integer size, String name) {
Page<PctMenuVO> page = new Page<>(current, size);
IPage<PctMenuVO> data = baseMapper.queryPctMenuByNameWithPaging(page, name);
return CommonPage.restPage(data);
}
//PctMenuMapper
Page<PctMenuVO> queryPctMenuByNameWithPaging(Page<PctMenuVO> page, @Param("name")String name);
//PctMenuMapper.xml
//<select id="queryPctMenuByNameWithPaging" //resultType="com.yihuacomputer.yhcloud.vo.PctMenuVO">
// SELECT
// pm.ID,pm.CODE,pm.NAME,pm.URL,
// pm.TYPE,pm.PARENT_ID,pm.STATUS,pm.LAYOUT_EN,
// pm.REMARK,pm.OPERATOR,pm.OPERATE_TIME
// FROM PCT_MENU pm
// WHERE 1=1
// AND pm.STATUS != 3
// <if test="name != null">
// AND pm.NAME like '%${name}%'
// </if>
//</select>
全局异常处理
1、系统异常设计
1)参数校验异常
参考:SpringBoot2.2.2.RELEASE+参数校验
其中Hibernate Validator所提供的参数校验注解(或者是自定义的参数校验注解),在参数校验功能中若校验失败,则会抛出
MethodArgumentNotValidException
2)自定义参数校验异常
注解型参数校验只是在Controlller层,其在于当校验失败时,利用AOP原理拦截控制器方法调用并抛出异常。但这种方式只适用于简单的参数校验,当校验逻辑复杂时、或希望在Service业务层进行更自由化的参数校验时,可以自定义一个异常类,然后在业务逻辑进行编码实现参数校验,主动抛出异常以终止方法执行(注意:这种方式可能很难去复用校验逻辑,它适合特定的场景)
package com.yihuacomputer.yhcloud.common.exception;
import com.yihuacomputer.yhcloud.common.api.IErrorCode;
/**
* @ClassName ExcelParseException
* @Description Excel解析异常
* @Author 罗新宇
* @Date 2020/3/31 9:22
**/
public class ExcelParseException extends RuntimeException {
private IErrorCode errorCode;
public ExcelParseException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ExcelParseException(String message) {
super(message);
}
public ExcelParseException(Throwable cause) {
super(cause);
}
public ExcelParseException(IErrorCode errorCode,String message) {
super(message);
this.errorCode = errorCode;
}
public ExcelParseException(IErrorCode errorCode,String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public IErrorCode getErrorCode() {
return errorCode;
}
}
异常工具类
public class ExcelParseAsserts {
public static void fail(String message){
throw new ExcelParseException(message);
}
public static void fail(IErrorCode errorCode){
throw new ExcelParseException(errorCode);
}
public static void fail(IErrorCode errorCode,String message) {
throw new ExcelParseException(errorCode,message);
}
public static void fail(IErrorCode errorCode,String message,Throwable throwable){
throw new ExcelParseException(errorCode,message,throwable);
}
}
使用示例
if(cell.getCellTypeEnum() == CellType.NUMERIC){
//设置Y轴数值
pctRecord.setYValue(cell.getNumericCellValue());
}else{
//返回错误信息
ExcelParseAsserts.fail(ResultCode.FAILED,"第"+r+"行第"+c+"列数据格式不正确,应为数字类型!");
}
2、控制器通知
/**
* 全局异常处理器
*
* @date 16:49 2020/3/19
* @author
**/
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class GlobalExceptionHandler {
private static final String UNKNOWN_EXCEPTION_MSG = "未知异常";
/**
* 其他未捕获的异常
*
* @date 9:47 2020/4/8
* @author 李文龙
* @param e:
* @return
**/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public CommonResult<String> handleUnKnownException(Exception e) {
//TODO 目前是直接打印在控制台以便追踪问题,后期需要将异常跟踪信息备份到log文件中
e.printStackTrace();
String msg = e.getMessage();
if (StringUtils.isEmpty(msg)) {
return CommonResult.failed(msg);
}
return CommonResult.failed(UNKNOWN_EXCEPTION_MSG);
}
/**
* 注解型:参数校验异常
**/
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public CommonResult<String> handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
FieldError fieldError = result.getFieldError();
if (fieldError != null) {
return CommonResult.validateFailed(fieldError.getDefaultMessage());
} else {
return CommonResult.validateFailed();
}
}
/**
* 捕获表格解析异常
*/
@ResponseBody
@ExceptionHandler(value = ExcelParseException.class)
public CommonResult<String> handleParamException(ExcelParseException e) {
if (e.getMessage() != null) {
return CommonResult.failed(e.getMessage());
}
return CommonResult.failed();
}
}
疑问:多个异常处理器所处理的异常类型是有继承关系的,那如何判定使用哪个处理器呢?答:ExceptionHandler的执行顺序
通过查看源码:ExceptionHandlerMethodResolver#getMappedMethod方法可知,首先找到可以匹配的所有ExceptionHandler,然后对其进行排序,利用深度比较器算法(递归判断父类异常是否为目标异常)取深度最小的那个,也即匹配度最高的那个
结论:定义多个ExceptionHandler时,要注意其所处理的异常类的继承关系,这决定了处理优先级
全局日志收集
1、打印web层入口信息
2、日志文件收集