SpringBoot restful api接口设计以及统一响应JSON格式 包含所有错误的JSON,统一异常不使用切面
RESTful API 最佳实践作为一个 java后端开发人员,前后端分离的时代,我更专注于后端的开发;专业的人做专业的事
前后端分离,也需要前后端人员相互联调,为了更好的交互,统一一个设计规范;
1、Http的常用请求方法MethodGET 一搬用于获取数据
POST 用于提交数据
PUT 用于修改数据
DELETE 用于删除数据
2、Restful api 常用的几个注解@RestController 一般用于Controoler类上
@ResponseBody 用了这个 RestController 就没有必要 ResponseBody
@GetMapping 方法上
@PostMapping 方法上
@PutMapping 方法上
@DeleteMapping 方法上
比如一个Controller 的简单增删改查 四个接口
@RestController
@RequestMapping("/oftenReciver")
public class OftenReciverController extends BaseController {
@Autowired
IOftenReciverService thisService;
@Autowired
AppAuthTokenHandler tokenHandler;
@GetMapping
public R list()
{
return success(thisService.findList(page(new OrderBy(OrderBy.Direction.DESC,"rec.update_time")), tokenHandler.getAppUserId()));
}
@PostMapping
public R insertSave(OftenReciver entity){
return result(thisService.save(entity));
}
@PutMapping
public R editSave(OftenReciver entity){
return result(thisService.updateById(entity));
}
@GetMapping("/selectById/{id}")
public R selectById(@PathVariable("id") String id){
return success(thisService.getById(id));
}
@DeleteMapping("remove/{ids}")
public R delete(@PathVariable("id") List ids)
{
return result(thisService.removeByIds(ids));
}
}
这就是简单的 restful 风格的api了,但是restful风格的api,还不够,我们前后端分离开发的,都要统一一个规范,也就是数据格式,为了更方便的前后端的数据对接,减少前后端干架的可能性
3、设计固定数据JSON格式:
所以 我们得设计一个固定的数据格式:比如这个样的
JSON 格式:
{
code:0,
msg:'操作成功',
data:null
}
{
code:1,
msg:'系统异常',
data:null
}
{
code:2,
msg:'参数绑定失败',
data:null
}
{
code:404,
msg:'路径不存在',
data:null
}
{
code:401,
msg:'用户未登陆',
data:null
}
........
那么我们定义一个类做封装
R.java:
@Data
public class R
{
private static final int SUCCESS_CODE = 0;
private static final int ERROR_CODE = 1;
private static final String SUCCESS_R = "操作成功!";
private static final String ERROR_R = "操作失败!";
private int code;
private String msg;
private Object data;
public R() {}
public static R success()
{
R r = new R();
r.setCode(SUCCESS_CODE);
r.setMsg(SUCCESS_R);
return r;
}
public static R success(Object data)
{
R r = new R();
r.setCode(SUCCESS_CODE);
r.setMsg(SUCCESS_R);
r.setData(data);
return r;
}
public static R success(String msg)
{
R r = new R();
r.setCode(SUCCESS_CODE);
r.setMsg(msg);
return r;
}
// public static R error()
// {
// R r = new R();
// r.setCode(ERROR_CODE);
// r.setMsg(ERROR_R);
// return r;
// }
public static R error(Error error, String tip)
{
if (StringUtils.isNotBlank(tip) || StringUtils.isNotNull(tip))
{
tip = "," + tip;
}
else
{
tip = "";
}
return error(error.getCode(), error.getErrMsg() + tip);
}
public static R error(Error error)
{
return error(error, null);
}
public static R error(int code, String msg)
{
R r = new R();
r.setCode(code);
r.setMsg(msg);
return r;
}
}
4、定义错误枚举
为了更加规范接口,我又定义了一个错误的枚举,主要是为了统一错误类型,让前端更好做判断
长这样的 Error.java:
public enum Error
{
操作失败(-1, "操作失败"),
系统异常(1, "系统异常"),
参数为空异常(2, "参数为空异常"),
参数异常(14, "参数异常"),
参数绑定异常(3, "参数绑定异常"),
文件上传异常(4, "文件上传异常"),
请求方式错误(5, "请求方式不支持"),
请求路径异常(6, "请检查url是否正确"),
权限异常(7, "权限不足"),
未登陆异常(9, "尚未登陆"),
微信接口异常(10, "微信接口异常"),
数据解析异常(11, "数据解析异常"),
Http接口响应异常(12, "数据解析异常"),
请求凭证有误(13, "请求凭证有误");
private int code;
private String errMsg;
public int getCode()
{
return code;
}
public void setCode(int code)
{
this.code = code;
}
public String getErrMsg()
{
return errMsg;
}
public void setErrMsg(String errMsg)
{
this.errMsg = errMsg;
}
Error(int code, String errMsg)
{
this.code = code;
this.errMsg = errMsg;
}
}
这样子 在controller 就能 固定的响应 错误的类型以及我们设定好的 code与类型对应了
例如这样子的:
@PostMapping("/takePro")
public R takePro(@RequestParam("commodityId") Integer id)
{
Commodity commodity = thisService.getById(id);
isNull(Error.参数异常);
int code = 0;
if (StringUtils.isNull(commodity.getTakeCode()))
{
code = (int) ((Math.random() * 9 + 1) * 1000);
commodity.setTakeCode(code);
commodity.setTakeUserId(getCurrentUserId());
thisService.updateById(commodity);
}else {
return error(Error.操作失败,"已被领取");
}
return success(getMap(Constanst.TAKE_CODE, code));
}
5、自定义全局异常 响应统一格式数据
GlobalExceptionHandler.java:
/**
*
*
*
*
* @author 永健
* @since 2019-05-23 19:32
*/
@RestControllerAdvice
public class GlobalExceptionHandler extends BaseException
{
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 参数绑定错误
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public R handleRRException(MissingServletRequestParameterException e)
{
return R.error(Error.参数绑定异常,"参数绑定失败,请求参数异常");
}
/**
* GET,POST 等方法
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R handleRRException(HttpRequestMethodNotSupportedException e)
{
return R.error(Error.请求方式错误,"请求方式不对,换个姿势再来!@_@ ,试试 /GET/POST/PUT...");
}
/**
* 处理自定义异常
*/
@ExceptionHandler(MyException.class)
public R handleRRException(MyException e)
{
return R.error(e.getCode(), e.getMsg());
}
/**
* 处理自定义异常
*/
@ExceptionHandler(Exception.class)
public R handleException(Exception e)
{
e.printStackTrace();
return R.error(Error.系统异常,"服务器错误");
}
@ExceptionHandler(NoHandlerFoundException.class)
public R handlerNoFoundException(Exception e)
{
e.printStackTrace();
logger.error(e.getMessage(), e);
return R.error(Error.请求路径异常, "路径不存在,请检查路径是否正确");
}
@ExceptionHandler(DataIntegrityViolationException.class)
public R handleDuplicateKeyException(DataIntegrityViolationException e)
{
e.printStackTrace();
logger.error(e.getMessage(), e);
return R.error(Error.系统异常,"操作数据库出现异常:字段重复、有外键关联等");
}
/**
* 权限异常
*/
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
public R unauthorizedException(UnauthorizedException e)
{
e.printStackTrace();
logger.error(e.getMessage(), e);
return R.error(Error.权限异常);
}
}
这货我们也统一用封装的`R.java来处理` 这样子 自定义异常 以及所有的异常都能 统一拦截处理响应了
6、自定义异常类封装
`MyException.java`:
/**
*
*
*
*
* @author 永健
* @since 2019-05-23 19:32
*/
public class MyException extends RuntimeException
{
private static final long serialVersionUID = 1L;
private String msg;
private Error error;
private int code = 1;
public MyException(Error error)
{
super(error.getErrMsg());
this.code = error.getCode();
this.msg = error.getErrMsg();
}
public MyException(Error error, String msg)
{
super(msg);
this.code = error.getCode();
this.msg = error.getErrMsg() + "," + msg;
}
public String getMsg()
{
return msg;
}
public MyException setMsg(String msg)
{
this.msg = msg;
return this;
}
public int getCode()
{
return code;
}
public MyException setCode(int code)
{
this.code = code;
return this;
}
public Error getError()
{
return error;
}
public void setError(Error error)
{
this.code = error.getCode();
this.msg = error.getErrMsg();
this.error = error;
}
}
自定义异常类,的参数 也只限定 自定义的Error.java 这样子自定义的异常抛出 ,在GlobalExceptionHandler.java中处理,转换成 R.java的数据格式,这样子就达到了统一处理了,其它的异常都可以在这里拦截
7、404 异常配置
`1.application.yml 配置:`
spring:
mvc:
### 让mvc直接抛出异常,在全局异常类,捕获404 异常的类 NoHandlerFoundException.class
throw-exception-if-no-handler-found: true
### 关闭资源映射
resources:
add-mappings: false
在全局异常类,捕获404 异常的类 NoHandlerFoundException.class,然后写上我们异常的数据,这样子前端更简洁明了了。
8、写一个BaseControoler 来放置一些 常用的函数
public abstract class BaseController
{
/**
* 返回成功
*/
protected R success()
{
return R.success();
}
/**
* 返回失败消息
*/
protected R error(Error error)
{
return R.error(error);
}
protected R error()
{
return R.error(Error.操作失败);
}
/**
* 返回失败消息
*/
protected R error(Error error, String msg)
{
return R.error(error, msg);
}
/**
* 返回成功消息
*/
protected R successMsg(String message)
{
return R.success(message);
}
/**
* 返回成功消息
*/
protected R success(Object data)
{
return R.success(data);
}
/**
* 根据修改搜影响的行数返回结果
*/
protected R result(boolean flag)
{
return flag == true ? success() : error(Error.操作失败);
}
protected R result(boolean flag, String msg)
{
return flag == true ? successMsg(msg) : error(Error.操作失败);
}
protected R result(int row)
{
return row > 0 ? success() : error(Error.操作失败);
}
protected R result(int row, String msg)
{
return row > 0 ? successMsg(msg) : error(Error.操作失败);
}
/**
* 分页数据 默认使用更新时间降序
*/
protected Page page(OrderBy orderBy)
{
return getPage(orderBy);
}
protected Page page(OrderBy orderBy, int current, int size)
{
Page page = getPage(orderBy);
page.setSize(size);
page.setCurrent(current);
return page;
}
/**
* 自定义分页数据 默认使用更新时间降序
*/
protected Page page()
{
return getPage(new OrderBy(OrderBy.Direction.DESC, "update_time"));
}
/**
*
* 自定义分页条件
*/
private Page getPage(OrderBy orderBy)
{
Integer size = ServletUtils.getParamInteger("size");
if (size != null && size == -1)
{
size = Integer.MAX_VALUE;
}
Integer pageNum = ServletUtils.getParamInteger("current");
Page page &#61; new Page<>(pageNum &#61;&#61; null ? 0 : pageNum, size &#61;&#61; null ? 15 : size);
if (orderBy.getDirection().isAscending())
{
page.setAsc(orderBy.getColumns());
}
else
{
page.setDesc(orderBy.getColumns());
}
return page;
}
/**
*
* 设置查询参数
*/
protected Wrapper setParams(T t)
{
return new QueryWrapper().lambda().setEntity(t);
}
protected void out(String str, HttpServletResponse response)
{
ServletOutputStream outputStream &#61; null;
try
{
outputStream &#61; response.getOutputStream();
outputStream.write(str.getBytes());
outputStream.flush();
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
if (outputStream !&#61; null)
{
outputStream.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
protected Map getMap()
{
return new HashMap(8);
}
protected Map getMap(Object key, Object val)
{
Map map &#61; new HashMap(8);
map.put(key, val);
return map;
}
protected AppAuthTokenHandler tokenHandler(){
return BeanTool.getInstance(AppAuthTokenHandler.class);
}
protected User getCurrentUser(){
return tokenHandler().getUser();
}
protected Integer getCurrentUserId(){
return tokenHandler().getUserId();
}
}
比如 我们的 Mybastis-plus 的分页封装可以放在这里用&#xff0c;
map 的获取&#xff0c;当前登陆的用户信息 等等
**这样子 我们可以不使用切面也一样可以捕获所有的异常哦**