再見!混亂代碼,SpringBoot 后端接口規(guī)范
點擊關(guān)注公眾號,Java干貨及時送達
作者:魅Lemon
<dependency><!--新版框架沒有自動引入需要手動引入--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><!--在引用時請在maven中央倉庫搜索最新版本號--><version>2.0.2</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
-
業(yè)務層校驗 -
Validator + BindResult校驗 -
Validator + 自動拋出異常
public String addUser( User user, BindingResult bindingResult) {// 如果有參數(shù)校驗失敗,會將錯誤信息封裝成對象組裝在BindingResult里List<ObjectError> allErrors = bindingResult.getAllErrors();if(!allErrors.isEmpty()){return allErrors.stream().map(o->o.getDefaultMessage()).collect(Collectors.toList()).toString();}// 返回默認的錯誤信息// return allErrors.get(0).getDefaultMessage();return validationService.addUser(user);}
public class User {private Long id;private String account;private String password;private String email;}
public class ValidationController {private ValidationService validationService;public String addUser( User user) {return validationService.addUser(user);}}
// 使用form data方式調(diào)用接口,校驗異常拋出 BindException// 使用 json 請求體調(diào)用接口,校驗異常拋出 MethodArgumentNotValidException// 單個參數(shù)校驗異常拋出ConstraintViolationException// 處理 json 請求體調(diào)用接口校驗失敗拋出的異常(MethodArgumentNotValidException.class)public ResultVO<String> MethodArgumentNotValidException(MethodArgumentNotValidException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();List<String> collect = fieldErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());return new ResultVO(ResultCode.VALIDATE_FAILED, collect);}// 使用form data方式調(diào)用接口,校驗異常拋出 BindException(BindException.class)public ResultVO<String> BindException(BindException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();List<String> collect = fieldErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());return new ResultVO(ResultCode.VALIDATE_FAILED, collect);}
-
定義一個分組類(或接口) -
在校驗注解上添加groups屬性指定分組 -
Controller方法的@Validated注解添加分組類
public interface Update extends Default{}
public class User {private Long id;......}
public String update( User user) {return "success";}
-
自定義校驗注解 -
編寫校驗者類
// 標明由哪個類執(zhí)行校驗邏輯public HaveNoBlank {// 校驗出錯時默認返回的消息String message() default "字符串中不能含有空格";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };/*** 同一個元素上指定多個該注解時使用*/public List {NotBlank[] value();}}
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {public boolean isValid(String value, ConstraintValidatorContext context) {// null 不做檢驗if (value == null) {return true;}// 校驗失敗return !value.contains(" ");// 校驗成功}}
package com.csdn.demo1.global;import org.springframework.validation.ObjectError;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;public class ExceptionControllerAdvice {public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {// 從異常對象中拿到ObjectError對象ObjectError objectError = e.getBindingResult().getAllErrors().get(0);// 然后提取錯誤提示信息進行返回return objectError.getDefaultMessage();}/*** 系統(tǒng)異常 預期以外異常*/public ResultVO<?> handleUnexpectedServer(Exception ex) {log.error("系統(tǒng)異常:", ex);// GlobalMsgEnum.ERROR是我自己定義的枚舉類return new ResultVO<>(GlobalMsgEnum.ERROR);}/*** 所以異常的攔截*/public ResultVO<?> exception(Throwable ex) {log.error("系統(tǒng)異常:", ex);return new ResultVO<>(GlobalMsgEnum.ERROR);}}
-
自定義異??梢詳y帶更多的信息,不像這樣只能攜帶一個字符串。 -
項目開發(fā)中經(jīng)常是很多人負責不同的模塊,使用自定義異??梢越y(tǒng)一了對外異常展示的方式。 -
自定義異常語義更加清晰明了,一看就知道是項目中手動拋出的異常。
package com.csdn.demo1.global;import lombok.Getter;//只要getter方法,無需setterpublic class APIException extends RuntimeException {private int code;private String msg;public APIException() {this(1001, "接口錯誤");}public APIException(String msg) {this(1001, msg);}public APIException(int code, String msg) {super(msg);this.code = code;this.msg = msg;}}
//自定義的全局異常public String APIExceptionHandler(APIException e) {return e.getMsg();}
public enum ResultCode {SUCCESS(1000, "操作成功"),FAILED(1001, "響應失敗"),VALIDATE_FAILED(1002, "參數(shù)校驗失敗"),ERROR(5000, "未知錯誤");private int code;private String msg;ResultCode(int code, String msg) {this.code = code;this.msg = msg;}}
package com.csdn.demo1.global;import lombok.Getter;public class ResultVO<T> {/*** 狀態(tài)碼,比如1000代表響應成功*/private int code;/*** 響應信息,用來說明響應情況*/private String msg;/*** 響應的具體數(shù)據(jù)*/private T data;public ResultVO(T data) {this(ResultCode.SUCCESS, data);}public ResultVO(ResultCode resultCode, T data) {this.code = resultCode.getCode();this.msg = resultCode.getMsg();this.data = data;}}
public class ExceptionControllerAdvice {public ResultVO<String> APIExceptionHandler(APIException e) {// 注意哦,這里傳遞的響應碼枚舉return new ResultVO<>(ResultCode.FAILED, e.getMsg());}public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {ObjectError objectError = e.getBindingResult().getAllErrors().get(0);// 注意哦,這里傳遞的響應碼枚舉return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());}}
("/getUser")public ResultVO<User> getUser() {User user = new User();user.setId(1L);user.setAccount("12345678");user.setPassword("12345678");user.setEmail("[email protected]");return new ResultVO<>(user);}
public class Msg {//狀態(tài)碼private int code;//提示信息private String msg;//用戶返回給瀏覽器的數(shù)據(jù)private Map<String,Object> data = new HashMap<>();public static Msg success() {Msg result = new Msg();result.setCode(200);result.setMsg("請求成功!");return result;}public static Msg fail() {Msg result = new Msg();result.setCode(400);result.setMsg("請求失??!");return result;}public static Msg fail(String msg) {Msg result = new Msg();result.setCode(400);result.setMsg(msg);return result;}public Msg(ReturnResult returnResult){code = returnResult.getCode();msg = returnResult.getMsg();}public Msg add(String key,Object value) {this.getData().put(key, value);return this;}}
// 表明該注解只能放在方法上public NotResponseBody {}
package com.csdn.demo1.global;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.core.MethodParameter;import org.springframework.http.MediaType;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.web.bind.annotation.RestControllerAdvice;import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;// 注意哦,這里要加上需要掃描的包public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {// 如果接口返回的類型本身就是ResultVO那就沒有必要進行額外的操作,返回false// 如果方法上加了我們的自定義注解也沒有必要進行額外的操作return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class));}public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {// String類型不能直接包裝,所以要進行些特別的處理if (returnType.getGenericParameterType().equals(String.class)) {ObjectMapper objectMapper = new ObjectMapper();try {// 將數(shù)據(jù)包裝在ResultVO里后,再轉(zhuǎn)換為json字符串響應給前端return objectMapper.writeValueAsString(new ResultVO<>(data));} catch (JsonProcessingException e) {throw new APIException("返回String類型錯誤");}}// 將原本的數(shù)據(jù)包裝在ResultVO里return new ResultVO<>(data);}}
("/getUser")//@NotResponseBody //是否繞過數(shù)據(jù)統(tǒng)一響應開關(guān)public User getUser() {User user = new User();user.setId(1L);user.setAccount("12345678");user.setPassword("12345678");user.setEmail("[email protected]");// 注意哦,這里是直接返回的User類型,并沒有用ResultVO進行包裝return user;}
-
基于path的版本控制 -
基于header的版本控制
public ApiVersion {// 默認接口版本號1.0開始,這里我只做了兩級,多級可在正則進行控制String value() default "1.0";}
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+\\.\\d+)");private final String version;public ApiVersionCondition(String version) {this.version = version;}public ApiVersionCondition combine(ApiVersionCondition other) {// 采用最后定義優(yōu)先原則,則方法上的定義覆蓋類上面的定義return new ApiVersionCondition(other.getApiVersion());}public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());if (m.find()) {String pathVersion = m.group(1);// 這個方法是精確匹配if (Objects.equals(pathVersion, version)) {return this;}// 該方法是只要大于等于最低接口version即匹配成功,需要和compareTo()配合// 舉例:定義有1.0/1.1接口,訪問1.2,則實際訪問的是1.1,如果從小開始那么排序反轉(zhuǎn)即可// if(Float.parseFloat(pathVersion)>=Float.parseFloat(version)){// return this;// }}return null;}public int compareTo(ApiVersionCondition other, HttpServletRequest request) {return 0;// 優(yōu)先匹配最新的版本號,和getMatchingCondition注釋掉的代碼同步使用// return other.getApiVersion().compareTo(this.version);}public String getApiVersion() {return version;}}
public class PathVersionHandlerMapping extends RequestMappingHandlerMapping {protected boolean isHandler(Class<?> beanType) {return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);}protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType,ApiVersion.class);return createCondition(apiVersion);}protected RequestCondition<?> getCustomMethodCondition(Method method) {ApiVersion apiVersion = AnnotationUtils.findAnnotation(method,ApiVersion.class);return createCondition(apiVersion);}private RequestCondition<ApiVersionCondition>createCondition(ApiVersion apiVersion) {return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());}}
public class WebMvcConfiguration implements WebMvcRegistrations {public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {return new PathVersionHandlerMapping();}}
public class TestController {public String query(){return "test api default";}public String query2(){return "test api v1.1";}public String query3(){return "test api v3.1";}}
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {private static final String X_VERSION = "X-VERSION";private final String version ;public ApiVersionCondition(String version) {this.version = version;}public ApiVersionCondition combine(ApiVersionCondition other) {// 采用最后定義優(yōu)先原則,則方法上的定義覆蓋類上面的定義return new ApiVersionCondition(other.getApiVersion());}public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {String headerVersion = httpServletRequest.getHeader(X_VERSION);if(Objects.equals(version,headerVersion)){return this;}return null;}public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {return 0;}public String getApiVersion() {return version;}}
-
Token授權(quán)認證,防止未授權(quán)用戶獲取數(shù)據(jù); -
時間戳超時機制; -
URL簽名,防止請求參數(shù)被篡改; -
防重放,防止接口被第二次請求,防采集; -
采用HTTPS通信協(xié)議,防止數(shù)據(jù)明文傳輸;
-
應用內(nèi)一定要唯一,否則會出現(xiàn)授權(quán)混亂,A用戶看到了B用戶的數(shù)據(jù); -
每次生成的Token一定要不一樣,防止被記錄,授權(quán)永久有效; -
一般Token對應的是Redis的key,value存放的是這個用戶相關(guān)緩存信息,比如:用戶的id; -
要設置Token的過期時間,過期后需要客戶端重新登錄,獲取新的Token,如果Token有效期設置較短,會反復需要用戶登錄,體驗比較差,我們一般采用Token過期后,客戶端靜默登錄的方式,當客戶端收到Token過期后,客戶端用本地保存的用戶名和密碼在后臺靜默登錄來獲取新的Token,還有一種是單獨出一個刷新Token的接口,但是一定要注意刷新機制和安全問題;
-
首先對通信的參數(shù)按key進行字母排序放入數(shù)組中(一般請求的接口地址也要參與排序和簽名,那么需要額外添加url=http://url/getInfo這個參數(shù)) -
對排序完的數(shù)組鍵值對用&進行連接,形成用于加密的參數(shù)字符串 -
在加密的參數(shù)字符串前面或者后面加上私鑰,然后用md5進行加密,得到sign,然后隨著請求接口一起傳給服務器。服務器端接收到請求后,用同樣的算法獲得服務器的sign,對比客戶端的sign是否一致,如果一致請求有效
-
客戶端通過用戶名密碼登錄服務器并獲取Token; -
客戶端生成時間戳timestamp,并將timestamp作為其中一個參數(shù); -
客戶端將所有的參數(shù),包括Token和timestamp按照自己的簽名算法進行排序加密得到簽名sign -
將token、timestamp和sign作為請求時必須攜帶的參數(shù)加在每個請求的URL后邊,例:http://url/request?token=h40adc3949bafjhbbe56e027f20f583a&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e -
服務端對token、timestamp和sign進行驗證,只有在token有效、timestamp未超時、緩存服務器中不存在sign三種情況同時滿足,本次請求才有效;
-
通過Validator + 自動拋出異常來完成了方便的參數(shù)校驗 -
通過全局異常處理 + 自定義異常完成了異常操作的規(guī)范 -
通過數(shù)據(jù)統(tǒng)一響應完成了響應數(shù)據(jù)的規(guī)范 -
多個方面組裝非常優(yōu)雅的完成了后端接口的協(xié)調(diào),讓開發(fā)人員有更多的經(jīng)歷注重業(yè)務邏輯代碼,輕松構(gòu)建后端接口
-
controller做好try-catch工作,及時捕獲異常,可以再次拋出到全局,統(tǒng)一格式返回前端 -
做好日志系統(tǒng),關(guān)鍵位置一定要有日志 -
做好全局統(tǒng)一返回類,整個項目規(guī)范好定義好 -
controller入?yún)⒆侄慰梢猿橄蟪鲆粋€公共基類,在此基礎上進行繼承擴充 -
controller層做好入?yún)?shù)校驗 -
接口安全驗證

往
期
推
薦
2、IntelliJ IDEA 2023.2新特性詳解第二彈!
3、網(wǎng)友爆料 Win11/10“更新并關(guān)閉”失效:會自動重啟

往 期 推 薦
2、IntelliJ IDEA 2023.2新特性詳解第二彈!
3、網(wǎng)友爆料 Win11/10“更新并關(guān)閉”失效:會自動重啟
評論
圖片
表情
