再见!混乱代码,SpringBoot 后端接口规范
Java技术迷
共 28690字,需浏览 58分钟
·
2023-08-10 03:01
点击关注公众号,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方法,无需setter
public 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层做好入参参数校验 -
接口安全验证
评论