一,為什么要使用REST?
1,什么是REST?
REST是軟件架構的規范體系,它把資源的狀態用URL進行資源定位,
以HTTP動作(GET/POST/DELETE/PUT)描述操作
2,REST的優點?
各大機構提供的api都是RESTful風格,
這樣有統一的規范,可以減少學習開發的成本
3,實現統一的返回格式要考慮哪些問題?
首先是正常的數據返回
其次是出錯時的返回:
未知異常:在spring boot中由controlleradvice統一處理
我們給用戶返回的錯誤: 我們使用自定義的BusinessException
再次是一些原本由tomcat處理的報錯:404等,
我們也使用統一的格式返回
需要注意的地方:異常並不是僅僅拋出即可,需要寫到日志中供運維處理
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,演示項目的相關信息:
1,項目的地址:
https://github.com/liuhongdi/restresult
2,項目的相關說明:
數據的返回:
code:代碼,為0是表示成功執行,非0表示出錯
msg:提示信息,通常供出錯時報錯
data:頁面上返回的數據:
具體的格式由后端工程師和前端工程師之間約定
以上格式也是最常用的返回格式
我們用一個名為:ResultUtil的類對格式做了封裝
3,項目的結構:
如圖:

三,配置文件說明:
1,pom.xml
<!--validation begin-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--validation end-->
<!--aop begin-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--aop end-->
<!--log4j2 begin-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
<!--log4j2 end-->
<!-- JSON解析fastjson begin-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<!-- JSON解析fastjson end-->
<!--commons-lang3-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
說明:需要注意的是從2.3開始,validation不再默認被starter-web包含,使用時需要手動引入
2,application.properties
#log4j2
logging.config=classpath:log4j2.xml
指定了log4j2的配置文件路徑
3,log4j2.xml
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="INFO"> <Appenders> <Console name="STDOUT" target="SYSTEM_OUT"> <PatternLayout pattern=".%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{36} - %msg %n"/> </Console> <RollingFile immediateFlush="false" name="ErrorFile" fileName="/data/logs/tomcatlogs/error.log" filePattern="/data/logs/tomcatlogs/$${date:yyyy-MM}/error-%d{MM-dd-yyyy}-%i.log"> <Filters> <ThresholdFilter level="INFO" /> </Filters> <PatternLayout> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{35} - %msg %n</Pattern> </PatternLayout> <Policies> <TimeBasedTriggeringPolicy /> <SizeBasedTriggeringPolicy size="102400KB"/> </Policies> </RollingFile> <RollingFile immediateFlush="false" name="BusinessFile" fileName="/data/logs/tomcatlogs/bussiness.log" filePattern="/data/logs/tomcatlogs/$${date:yyyy-MM}/bussiness-%d{MM-dd-yyyy}-%i.log"> <PatternLayout> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{35} - %msg %n</Pattern> </PatternLayout> <Policies> <TimeBasedTriggeringPolicy /> <SizeBasedTriggeringPolicy size="102400KB"/> </Policies> </RollingFile> <RollingFile immediateFlush="false" name="BusinessErrorFile" fileName="/data/logs/tomcatlogs/bussinesserror.log" filePattern="/data/logs/tomcatlogs/$${date:yyyy-MM}/bussinesserror-%d{MM-dd-yyyy}-%i.log"> <PatternLayout> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{35} - %msg %n</Pattern> </PatternLayout> <Policies> <TimeBasedTriggeringPolicy /> <SizeBasedTriggeringPolicy size="102400KB"/> </Policies> </RollingFile> </Appenders> <Loggers> <AsyncLogger name="BusinessFile" level="info" additivity="false" includeLocation="true"> <appender-ref ref="BusinessFile"/> </AsyncLogger> <AsyncLogger name="BusinessErrorFile" level="info" additivity="false" includeLocation="true"> <appender-ref ref="BusinessErrorFile"/> </AsyncLogger> <AsyncRoot level="info" includeLocation="true"> <AppenderRef ref="STDOUT"/> <AppenderRef ref="ErrorFile" /> </AsyncRoot> </Loggers> </Configuration>
說明:我們配置了三個日志文件:
ErrorFile:系統默認的錯誤日志,
BusinessErrorFile:主動返回的業務錯誤信息,
BusinessFile:與錯誤無關的業務數據
四,java代碼說明
1,ResultUtil.java
public class ResultUtil implements Serializable { //uuid,用作唯一標識符,供序列化和反序列化時檢測是否一致 private static final long serialVersionUID = 7498483649536881777L; //標識代碼,0表示成功,非0表示出錯 private Integer code; //提示信息,通常供報錯時使用 private String msg; //正常返回時返回的數據 private Object data; public ResultUtil(Integer status, String msg, Object data) { this.code = status; this.msg = msg; this.data = data; } //返回成功數據 public static ResultUtil success(Object data) { return new ResultUtil(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), data); } //返回出錯數據 public static ResultUtil error(ResponseCode code) { return new ResultUtil(code.getCode(), code.getMsg(), null); } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
2,ResponseCode.java
public enum ResponseCode { // 系統模塊 SUCCESS(0, "操作成功"), ERROR(1, "操作失敗"), //web WEB_400(400,"錯誤請求"), WEB_401(401,"訪問未得到授權"), WEB_404(404,"資源未找到"), WEB_500(500,"服務器內部錯誤"), WEB_UNKOWN(999,"未知錯誤"), //parameter ARG_TYPE_MISMATCH(1000,"參數類型錯誤"), ARG_BIND_EXCEPTION(1001,"參數綁定錯誤"), ARG_VIOLATION(1002,"參數不符合要求"), ARG_MISSING(1003,"參數未找到"), //sign error SIGN_NO_APPID(10001, "appId不能為空"), SIGN_NO_TIMESTAMP(10002, "timestamp不能為空"), SIGN_NO_SIGN(10003, "sign不能為空"), SIGN_NO_NONCE(10004, "nonce不能為空"), SIGN_TIMESTAMP_INVALID(10005, "timestamp無效"), SIGN_DUPLICATION(10006, "重復的請求"), SIGN_VERIFY_FAIL(10007, "sign簽名校驗失敗"), ; ResponseCode(Integer code, String msg) { this.code = code; this.msg = msg; } private Integer code; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } private String msg; public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String toString(){ return "code:"+code+";msg:"+msg; } }
說明:這個枚舉對象用來保存返回的錯誤code的信息,
通常直接使用定義的常量來返回,在開發中既減少代碼,又方便集中維護
3,MyControllerAdvice.java
@ControllerAdvice public class MyControllerAdvice { private static Logger logger = LogManager.getLogger(MyControllerAdvice.class.getName()); private static Logger loggerBE = LogManager.getLogger("BusinessErrorFile"); //驗證參數時不符合要求 @ResponseBody @ExceptionHandler(value = ConstraintViolationException.class) public ResultUtil violationHandler(ConstraintViolationException e) { loggerBE.error("ConstraintViolationException: \n"+ ServletUtil.getUrl()+"\n"+e.getMessage(), e); ResponseCode.ARG_VIOLATION.setMsg(e.getMessage()); return ResultUtil.error(ResponseCode.ARG_VIOLATION); } //缺少應該傳遞的參數 @ResponseBody @ExceptionHandler(value = MissingServletRequestParameterException.class) public ResultUtil missingParameterHandler(MissingServletRequestParameterException e) { loggerBE.error("MissingServletRequestParameterException: "+e.getMessage(), e); ResponseCode.ARG_MISSING.setMsg(e.getMessage()); return ResultUtil.error(ResponseCode.ARG_MISSING); } //參數類型不匹配,用戶輸入的參數類型有錯誤時會報這個 @ResponseBody @ExceptionHandler(value = MethodArgumentTypeMismatchException.class) public ResultUtil misMatchErrorHandler(MethodArgumentTypeMismatchException e) { loggerBE.error("MethodArgumentTypeMismatchException: \n"+ ServletUtil.getUrl()+"\n"+e.getMessage(), e); ResponseCode.ARG_TYPE_MISMATCH.setMsg(e.getMessage()); return ResultUtil.error(ResponseCode.ARG_TYPE_MISMATCH); } //驗證時綁定錯誤 @ResponseBody @ExceptionHandler(value = BindException.class) public ResultUtil errorHandler(BindException ex) { BindingResult result = ex.getBindingResult(); StringBuilder errorMsg = new StringBuilder(); for (ObjectError error : result.getAllErrors()) { errorMsg.append(error.getDefaultMessage()).append(","); } errorMsg.delete(errorMsg.length() - 1, errorMsg.length()); ResponseCode.ARG_BIND_EXCEPTION.setMsg(errorMsg.toString()); loggerBE.error("BindException: \n"+ ServletUtil.getUrl()+"\n"+errorMsg.toString(), ex); return ResultUtil.error(ResponseCode.ARG_BIND_EXCEPTION); } /* *@author:liuhongdi *@date:2020/7/7 下午3:06 *@description:自定義的業務類異常的處理 * @param se *@return: */ @ResponseBody @ExceptionHandler(BusinessException.class) public ResultUtil serviceExceptionHandler(BusinessException se) { loggerBE.error("ServiceException: \n"+ ServletUtil.getUrl()+"\n"+se.getResponseCode(), se); ResponseCode rcode = se.getResponseCode(); return ResultUtil.error(rcode); } /* *@author:liuhongdi *@date:2020/7/7 下午3:05 *@description:通用的對異常的處理 * @param e *@return: */ @ResponseBody @ExceptionHandler(Exception.class) public ResultUtil exceptionHandler(Exception e) { logger.error("Exception: \n"+ ServletUtil.getUrl(), e); return ResultUtil.error(ResponseCode.ERROR); } //主動拋出的異常的處理 @ResponseBody @ExceptionHandler(ThrowException.class) public ResultUtil throwExceptionHandler(ThrowException e) { logger.error("ThrowException: \n"+ ServletUtil.getUrl()+"\n" +e.getMsg(), e); return ResultUtil.error(ResponseCode.ERROR); } }
說明:spring boot中用來處理異常的通用controller,
需要用@ControllerAdvice這個注解聲明
4,BusinessException
public class BusinessException extends RuntimeException{ //返回響應的代碼和提示信息 private ResponseCode rcode; public BusinessException(ResponseCode rcode) { this.rcode = rcode; } public ResponseCode getResponseCode() { return this.rcode; } public void setResponseCode(ResponseCode rcode) { this.rcode = rcode; } }
主動返回的報錯信息
5,ErrorConfig.java
@Component public class ErrorConfig implements ErrorPageRegistrar { @Override public void registerErrorPages(ErrorPageRegistry registry) { ErrorPage error400Page = new ErrorPage(HttpStatus.BAD_REQUEST, "/error/error/400"); ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/error/error/401"); ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/error/error/404"); ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/error/500"); registry.addErrorPages(error400Page,error401Page,error404Page,error500Page); } }
用來定義錯誤頁面的返回信息
說明:完整的代碼大家可以在github上閱讀
五,效果演示
1,測試參數驗證:
http://127.0.0.1:8080/home/home
返回:
{"code":1001,"msg":"homeid參數必須為正數","data":null}
測試錯誤的類型:
http://127.0.0.1:8080/home/home?homeid=abc
返回:
{"code":1001,"msg":"Failed to convert property value of type 'java.lang.String' to required type 'int' for property 'homeid';
nested exception is java.lang.NumberFormatException: For input string: \"abc\"","data":null}
正確的參數:
http://127.0.0.1:8080/home/home?homeid=10
返回:
{"code":0,"msg":"操作成功","data":"this is home"}
2,說明:
代碼中還針對aop,interceptor等做了演示,供參考
六,查看spring boot的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE)
