詳解SpringBoot統(tǒng)一響應(yīng)體解決方案
前言
最近在優(yōu)化自己之前基于Spring AOP的統(tǒng)一響應(yīng)體的實(shí)現(xiàn)方案。
什么是統(tǒng)一響應(yīng)體呢?在目前的前后端分離架構(gòu)下,后端主要是一個(gè)RESTful API的數(shù)據(jù)接口。
但是HTTP的狀態(tài)碼數(shù)量有限,而隨著業(yè)務(wù)的增長,HTTP狀態(tài)碼無法很好地表示業(yè)務(wù)中遇到的異常情況。
那么可以通過修改響應(yīng)返回的JSON數(shù)據(jù),讓其帶上一些固有的字段,例如以下這樣的
{
"code": 10000,
"msg": "success",
"data": {
"id": 2,
"name": "test"
}
}
其中關(guān)鍵屬性的用途如下:
- code為返回結(jié)果的狀態(tài)碼
- msg為返回結(jié)果的消息
- data為返回的業(yè)務(wù)數(shù)據(jù)
這3個(gè)屬性為固有屬性,每次響應(yīng)結(jié)果都會(huì)有帶有它們。
需求
希望實(shí)現(xiàn)一個(gè)能夠代替基于AOP的實(shí)現(xiàn)方案,需要滿足以下幾點(diǎn):
- 原有的基于AOP的實(shí)現(xiàn)方案需要Controller的返回類型為Object,需要新方案不限制返回類型
- 原有的基于AOP的實(shí)現(xiàn)方案需要通過切面表達(dá)式+注解控制切點(diǎn)的Controller(注解的包名修改會(huì)導(dǎo)致切面表達(dá)式的修改,即需要修改兩處地方),需要新方案能夠基于注解,而不需要修改切面表達(dá)式
方案思路
基于上述的需求,選擇使用Spring的Controller增強(qiáng)機(jī)制,其中關(guān)鍵的類為以下3個(gè):
- @ControllerAdvice:類注解,用于指定Controller增強(qiáng)處理器類。
- ResponseBodyAdvice:接口,實(shí)現(xiàn)后beforeBodyWrite()方法后可以對響應(yīng)的body進(jìn)行修改,需要結(jié)合@ControllerAdvice使用。
- @ExceptionHandler:方法注解,用于指定異常處理方法,需要結(jié)合@ControllerAdvice和@ResponseBody使用。
示例關(guān)鍵代碼
本示例使用的Spring Boot版本為2.1.6.RELEASE,同時(shí)需要開發(fā)工具安裝lombok插件
引入依賴
<dependencies> <!--web-starter--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--test-starter--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
統(tǒng)一響應(yīng)體
Controller增強(qiáng)后統(tǒng)一響應(yīng)體對應(yīng)的對象
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
/**
* 統(tǒng)一的公共響應(yīng)體
* @author NULL
* @date 2019-07-16
*/
@Data
@AllArgsConstructor
public class ResponseResult implements Serializable {
/**
* 返回狀態(tài)碼
*/
private Integer code;
/**
* 返回信息
*/
private String msg;
/**
* 數(shù)據(jù)
*/
private Object data;
}
統(tǒng)一響應(yīng)注解
統(tǒng)一響應(yīng)注解是一個(gè)標(biāo)記是否開啟統(tǒng)一響應(yīng)增強(qiáng)的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 統(tǒng)一響應(yīng)注解<br/>
* 添加注解后,統(tǒng)一響應(yīng)體才能生效
* @author NULL
* @date 2019-07-16
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface BaseResponse {
}
狀態(tài)碼枚舉
統(tǒng)一響應(yīng)體中返回的狀態(tài)碼code和狀態(tài)信息msg對應(yīng)的枚舉類
/**
* 返回狀態(tài)碼
*
* @author NULL
* @date 2019-07-16
*/
public enum ResponseCode {
/**
* 成功返回的狀態(tài)碼
*/
SUCCESS(10000, "success"),
/**
* 資源不存在的狀態(tài)碼
*/
RESOURCES_NOT_EXIST(10001, "資源不存在"),
/**
* 所有無法識別的異常默認(rèn)的返回狀態(tài)碼
*/
SERVICE_ERROR(50000, "服務(wù)器異常");
/**
* 狀態(tài)碼
*/
private int code;
/**
* 返回信息
*/
private String msg;
ResponseCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
業(yè)務(wù)異常類
業(yè)務(wù)異常類是用于識別業(yè)務(wù)相關(guān)的異常,需要注意這個(gè)異常類強(qiáng)制需要以ResponseCode作為構(gòu)造方法入?yún)?,這樣可以通過捕獲異常獲得返回的狀態(tài)碼信息
import com.rjh.web.response.ResponseCode;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 業(yè)務(wù)異常類,繼承運(yùn)行時(shí)異常,確保事務(wù)正?;貪L
*
* @author NULL
* @since 2019-07-16
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class BaseException extends RuntimeException{
private ResponseCode code;
public BaseException(ResponseCode code) {
this.code = code;
}
public BaseException(Throwable cause, ResponseCode code) {
super(cause);
this.code = code;
}
}
異常處理類
用于處理Controller運(yùn)行時(shí)未捕獲的異常的處理類。
import com.rjh.web.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 異常處理器
*
* @author NULL
* @since 2019-07-16
*/
@ControllerAdvice(annotations = BaseResponse.class)
@ResponseBody
@Slf4j
public class ExceptionHandlerAdvice {
/**
* 處理未捕獲的Exception
* @param e 異常
* @return 統(tǒng)一響應(yīng)體
*/
@ExceptionHandler(Exception.class)
public ResponseResult handleException(Exception e){
log.error(e.getMessage(),e);
return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null);
}
/**
* 處理未捕獲的RuntimeException
* @param e 運(yùn)行時(shí)異常
* @return 統(tǒng)一響應(yīng)體
*/
@ExceptionHandler(RuntimeException.class)
public ResponseResult handleRuntimeException(RuntimeException e){
log.error(e.getMessage(),e);
return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null);
}
/**
* 處理業(yè)務(wù)異常BaseException
* @param e 業(yè)務(wù)異常
* @return 統(tǒng)一響應(yīng)體
*/
@ExceptionHandler(BaseException.class)
public ResponseResult handleBaseException(BaseException e){
log.error(e.getMessage(),e);
ResponseCode code=e.getCode();
return new ResponseResult(code.getCode(),code.getMsg(),null);
}
}
響應(yīng)增強(qiáng)類
Conrtoller增強(qiáng)的統(tǒng)一響應(yīng)體處理類,需要注意異常處理類已經(jīng)進(jìn)行了增強(qiáng),所以需要判斷一下返回的對象是否為統(tǒng)一響應(yīng)體對象。
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 統(tǒng)一響應(yīng)體處理器
* @author NULL
* @date 2019-07-16
*/
@ControllerAdvice(annotations = BaseResponse.class)
@Slf4j
public class ResponseResultHandlerAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
log.info("returnType:"+returnType);
log.info("converterType:"+converterType);
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(MediaType.APPLICATION_JSON.equals(selectedContentType) || MediaType.APPLICATION_JSON_UTF8.equals(selectedContentType)){ // 判斷響應(yīng)的Content-Type為JSON格式的body
if(body instanceof ResponseResult){ // 如果響應(yīng)返回的對象為統(tǒng)一響應(yīng)體,則直接返回body
return body;
}else{
// 只有正常返回的結(jié)果才會(huì)進(jìn)入這個(gè)判斷流程,所以返回正常成功的狀態(tài)碼
ResponseResult responseResult =new ResponseResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),body);
return responseResult;
}
}
// 非JSON格式body直接返回即可
return body;
}
}
使用示例
首先準(zhǔn)備一個(gè)User對象
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 用戶類
* @author NULL
* @date 2019-07-16
*/
@Data
@EqualsAndHashCode
public class User implements Serializable {
private Integer id;
private String name;
}
然后是準(zhǔn)備一個(gè)簡單的UserController即可
import com.rjh.web.entity.User;
import com.rjh.web.exception.BaseException;
import com.rjh.web.response.BaseResponse;
import com.rjh.web.response.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 測試用的Controller
*
* @author NULL
* @date 2019-07-16
*/
@BaseResponse
@RestController
@RequestMapping("users")
public class UserController {
@GetMapping("/{userId}")
public User getUserById(@PathVariable Integer userId){
if(userId.equals(0)){
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
}
if(userId.equals(1)){
throw new RuntimeException();
}
User user=new User();
user.setId(userId);
user.setName("test");
return user;
}
}
運(yùn)行結(jié)果
在瀏覽器直接訪問http://127.0.0.1:8080/users/0,則返回結(jié)果如下(結(jié)果經(jīng)過格式化處理):
{
"code": 10001,
"msg": "資源不存在",
"data": null
}
在瀏覽器直接訪問http://127.0.0.1:8080/users/1,則返回結(jié)果如下(結(jié)果經(jīng)過格式化處理):
{
"code": 50000,
"msg": "服務(wù)器異常",
"data": null
}
在瀏覽器直接訪問http://127.0.0.1:8080/users/2,則返回結(jié)果如下(結(jié)果經(jīng)過格式化處理):
{
"code": 10000,
"msg": "success",
"data": {
"id": 2,
"name": "test"
}
}
由運(yùn)行結(jié)果可以得知統(tǒng)一響應(yīng)增強(qiáng)其實(shí)已經(jīng)生效了,而且能夠很好的處理異常。
示例代碼地址
下面是這個(gè)示例的代碼地址,如果覺得不錯(cuò)或者幫助到你,希望大家給個(gè)Star:
https://github.com/spring-based-solutions/spring-web-unified-response-demo
參考資料
https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web.html#mvc-ann-controller-advice
https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web.html#mvc-ann-exceptionhandler
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于PreparedStatement的setObject作用及說明
這篇文章主要介紹了關(guān)于PreparedStatement的setObject作用及說明,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
java中@JsonValue和@JsonCreator使用
本文主要介紹了java中@JsonValue和@JsonCreator使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-06-06
java聯(lián)調(diào)生成測試數(shù)據(jù)工具類方式
這篇文章主要介紹了java聯(lián)調(diào)生成測試數(shù)據(jù)工具類方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
Spring MVC+MyBatis+MySQL實(shí)現(xiàn)分頁功能實(shí)例
分頁功能是我們?nèi)粘i_發(fā)中經(jīng)常會(huì)遇到的,下面這篇文章主要給大家介紹了Spring MVC+MyBatis+MySQL實(shí)現(xiàn)分頁功能的相關(guān)資料,文中介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-06-06
解決在IDEA中創(chuàng)建多級package的問題
這篇文章主要介紹了解決在IDEA中創(chuàng)建多級package的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02
Spring Cloud項(xiàng)目前后端分離跨域的操作
這篇文章主要介紹了Spring Cloud項(xiàng)目前后端分離跨域的操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
java進(jìn)行遠(yuǎn)程部署與調(diào)試及原理詳解
這篇文章主要介紹了java進(jìn)行遠(yuǎn)程部署與調(diào)試及原理詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
關(guān)于ResultSet(結(jié)果集)用法詳解
本文將深入探討 ResultSet 的基本概念、常見操作及使用注意事項(xiàng),具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04

