再见!混乱代码,SpringBoot 后端接口规范
点击关注公众号,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>
-
业务层校验 -
Validator + BindResult校验 -
Validator + 自动抛出异常
public String addUser( User user, BindingResult bindingResult) {// 如果有参数校验失败,会将错误信息封装成对象组装在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方式调用接口,校验异常抛出 BindException// 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException// 单个参数校验异常抛出ConstraintViolationException// 处理 json 请求体调用接口校验失败抛出的异常(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方式调用接口,校验异常抛出 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";}
-
自定义校验注解 -
编写校验者类
// 标明由哪个类执行校验逻辑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();}/*** 系统异常 预期以外异常*/public ResultVO<?> handleUnexpectedServer(Exception ex) {log.error("系统异常:", ex);// GlobalMsgEnum.ERROR是我自己定义的枚举类return new ResultVO<>(GlobalMsgEnum.ERROR);}/*** 所以异常的拦截*/public ResultVO<?> exception(Throwable ex) {log.error("系统异常:", ex);return new ResultVO<>(GlobalMsgEnum.ERROR);}}
-
自定义异常可以携带更多的信息,不像这样只能携带一个字符串。 -
项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。 -
自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
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, "参数校验失败"),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> {/*** 状态码,比如1000代表响应成功*/private int code;/*** 响应信息,用来说明响应情况*/private String msg;/*** 响应的具体数据*/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("123@qq.com");return new ResultVO<>(user);}
public class Msg {//状态码private int code;//提示信息private String msg;//用户返回给浏览器的数据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 {// 将数据包装在ResultVO里后,再转换为json字符串响应给前端return objectMapper.writeValueAsString(new ResultVO<>(data));} catch (JsonProcessingException e) {throw new APIException("返回String类型错误");}}// 将原本的数据包装在ResultVO里return new ResultVO<>(data);}}
("/getUser")//@NotResponseBody //是否绕过数据统一响应开关public User getUser() {User user = new User();user.setId(1L);user.setAccount("12345678");user.setPassword("12345678");user.setEmail("123@qq.com");// 注意哦,这里是直接返回的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) {// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义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,如果从小开始那么排序反转即可// if(Float.parseFloat(pathVersion)>=Float.parseFloat(version)){// return this;// }}return null;}public int compareTo(ApiVersionCondition other, HttpServletRequest request) {return 0;// 优先匹配最新的版本号,和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) {// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义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授权认证,防止未授权用户获取数据; -
时间戳超时机制; -
URL签名,防止请求参数被篡改; -
防重放,防止接口被第二次请求,防采集; -
采用HTTPS通信协议,防止数据明文传输;
-
应用内一定要唯一,否则会出现授权混乱,A用户看到了B用户的数据; -
每次生成的Token一定要不一样,防止被记录,授权永久有效; -
一般Token对应的是Redis的key,value存放的是这个用户相关缓存信息,比如:用户的id; -
要设置Token的过期时间,过期后需要客户端重新登录,获取新的Token,如果Token有效期设置较短,会反复需要用户登录,体验比较差,我们一般采用Token过期后,客户端静默登录的方式,当客户端收到Token过期后,客户端用本地保存的用户名和密码在后台静默登录来获取新的Token,还有一种是单独出一个刷新Token的接口,但是一定要注意刷新机制和安全问题;
-
首先对通信的参数按key进行字母排序放入数组中(一般请求的接口地址也要参与排序和签名,那么需要额外添加url=http://url/getInfo这个参数) -
对排序完的数组键值对用&进行连接,形成用于加密的参数字符串 -
在加密的参数字符串前面或者后面加上私钥,然后用md5进行加密,得到sign,然后随着请求接口一起传给服务器。服务器端接收到请求后,用同样的算法获得服务器的sign,对比客户端的sign是否一致,如果一致请求有效
-
客户端通过用户名密码登录服务器并获取Token; -
客户端生成时间戳timestamp,并将timestamp作为其中一个参数; -
客户端将所有的参数,包括Token和timestamp按照自己的签名算法进行排序加密得到签名sign -
将token、timestamp和sign作为请求时必须携带的参数加在每个请求的URL后边,例:http://url/request?token=h40adc3949bafjhbbe56e027f20f583a&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e -
服务端对token、timestamp和sign进行验证,只有在token有效、timestamp未超时、缓存服务器中不存在sign三种情况同时满足,本次请求才有效;
-
通过Validator + 自动抛出异常来完成了方便的参数校验 -
通过全局异常处理 + 自定义异常完成了异常操作的规范 -
通过数据统一响应完成了响应数据的规范 -
多个方面组装非常优雅的完成了后端接口的协调,让开发人员有更多的经历注重业务逻辑代码,轻松构建后端接口
-
controller做好try-catch工作,及时捕获异常,可以再次抛出到全局,统一格式返回前端 -
做好日志系统,关键位置一定要有日志 -
做好全局统一返回类,整个项目规范好定义好 -
controller入参字段可以抽象出一个公共基类,在此基础上进行继承扩充 -
controller层做好入参参数校验 -
接口安全验证

往
期
推
荐
2、IntelliJ IDEA 2023.2新特性详解第二弹!
3、网友爆料 Win11/10“更新并关闭”失效:会自动重启

往 期 推 荐
2、IntelliJ IDEA 2023.2新特性详解第二弹!
3、网友爆料 Win11/10“更新并关闭”失效:会自动重启
评论
