项目开发基本配置
项目开发基本配置
接口文档引入knife4j
引入依赖:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
修改与Spring不兼容的配置:
spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
Swagger配置(开箱即用,只需修改自己的信息,默认是扫描RestController):
@Configuration
@EnableSwagger2WebMvc
public class SwaggerConfig {
@Bean(value = "defaultApi2")
Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
//配置网站的基本信息
.apiInfo(new ApiInfoBuilder()
//网站标题
.title("云聊天室接口文档")
//标题后面的版本号
.version("v1.0")
.description("云聊天室接口文档")
//联系人信息
.contact(new Contact("全民制作人iKun", "http://yunfei.plus", "1844025705@qq.com"))
.build())
.select()
//指定接口的位置
.apis(RequestHandlerSelectors
.withClassAnnotation(RestController.class)
)
.paths(PathSelectors.any())
.build();
}
/**
* 增加如下配置可解决Spring Boot 6.x 与Swagger 3.0.0 不兼容问题
**/
@Bean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties, Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || Objects.equals(ManagementPortType.get(environment), ManagementPortType.DIFFERENT));
}
}
使用:
实体类上返回信息:
@ApiModel("用户信息返回")
public class UserInfoResponse {
@ApiModelProperty(value = "用户id")
private Long id;
@ApiModelProperty(value = "用户昵称")
private String name;
@ApiModelProperty(value = "用户头像")
private String avatar;
@ApiModelProperty(value = "性别 1为男性,2为女性")
private Integer sex;
@ApiModelProperty(value = "剩余改名次数")
private Integer modifyNameChance;
}
接口上注解:
@RequestMapping("/user")
@RestController
@Api(tags = "用户相关接口")
public class UserController {
@GetMapping("/userInfo")
@ApiOperation("获取用户信息")
public UserInfoResponse getUserInfo(@RequestParam Long userId) {
return new UserInfoResponse();
}
}
前后端交互体协议
所有返回给前端的信息都用ApiResult进行包装:
@Data
@ApiModel("基础返回体")
public class ApiResult<T> {
@ApiModelProperty("成功标识true or false")
private Boolean success;
@ApiModelProperty("错误码")
private Integer errCode;
@ApiModelProperty("错误消息")
private String errMsg;
@ApiModelProperty("返回对象")
private T data;
}
一些常见的请求和返回格式:
CursorPageBaseReq.java
@Data
@ApiModel("游标翻页请求")
@AllArgsConstructor
@NoArgsConstructor
public class CursorPageBaseReq {
@ApiModelProperty("页面大小")
@Min(0)
@Max(100)
private Integer pageSize = 10;
@ApiModelProperty("游标(初始为null,后续请求附带上次翻页的游标)")
private String cursor;
public Page plusPage() {
return new Page(1, this.pageSize, false);
}
@JsonIgnore
public Boolean isFirstPage() {
return StringUtils.isEmpty(cursor);
}
}
IdReqVO.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IdReqVO {
@ApiModelProperty("id")
@NotNull
private long id;
}
PageBaseReq.java
@Data
@ApiModel("基础翻页请求")
public class PageBaseReq {
@ApiModelProperty("页面大小")
@Min(0)
@Max(50)
private Integer pageSize = 10;
@ApiModelProperty("页面索引(从1开始)")
private Integer pageNo = 1;
/**
* 获取mybatisPlus的page
*
* @return
*/
public Page plusPage() {
return new Page(pageNo, pageSize);
}
}
CursorPageBaseResp.java
@Data
@ApiModel("游标翻页返回")
@AllArgsConstructor
@NoArgsConstructor
public class CursorPageBaseResp<T> {
@ApiModelProperty("游标(下次翻页带上这参数)")
private String cursor;
@ApiModelProperty("是否最后一页")
private Boolean isLast = Boolean.FALSE;
@ApiModelProperty("数据列表")
private List<T> list;
public static <T> CursorPageBaseResp<T> init(CursorPageBaseResp cursorPage, List<T> list) {
CursorPageBaseResp<T> cursorPageBaseResp = new CursorPageBaseResp<T>();
cursorPageBaseResp.setIsLast(cursorPage.getIsLast());
cursorPageBaseResp.setList(list);
cursorPageBaseResp.setCursor(cursorPage.getCursor());
return cursorPageBaseResp;
}
@JsonIgnore
public Boolean isEmpty() {
return CollectionUtil.isEmpty(list);
}
public static <T> CursorPageBaseResp<T> empty() {
CursorPageBaseResp<T> cursorPageBaseResp = new CursorPageBaseResp<T>();
cursorPageBaseResp.setIsLast(true);
cursorPageBaseResp.setList(new ArrayList<T>());
return cursorPageBaseResp;
}
}
IdRespVO.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IdRespVO {
@ApiModelProperty("id")
private long id;
public static IdRespVO id(Long id) {
IdRespVO idRespVO = new IdRespVO();
idRespVO.setId(id);
return idRespVO;
}
}
PageBaseResp.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("基础翻页返回")
public class PageBaseResp<T> {
@ApiModelProperty("当前页数")
private Integer pageNo;
@ApiModelProperty("每页查询数量")
private Integer pageSize;
@ApiModelProperty("总记录数")
private Long totalRecords;
@ApiModelProperty("是否最后一页")
private Boolean isLast = Boolean.FALSE;
@ApiModelProperty("数据列表")
private List<T> list;
public static <T> PageBaseResp<T> empty() {
PageBaseResp<T> r = new PageBaseResp<>();
r.setPageNo(1);
r.setPageSize(0);
r.setIsLast(true);
r.setTotalRecords(0L);
r.setList(new ArrayList<>());
return r;
}
public static <T> PageBaseResp<T> init(Integer pageNo, Integer pageSize, Long totalRecords, Boolean isLast, List<T> list) {
return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLast, list);
}
public static <T> PageBaseResp<T> init(Integer pageNo, Integer pageSize, Long totalRecords, List<T> list) {
return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLastPage(totalRecords, pageNo, pageSize), list);
}
public static <T> PageBaseResp<T> init(IPage<T> page) {
return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), page.getRecords());
}
public static <T> PageBaseResp<T> init(IPage page, List<T> list) {
return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), list);
}
public static <T> PageBaseResp<T> init(PageBaseResp resp, List<T> list) {
return init(resp.getPageNo(), resp.getPageSize(), resp.getTotalRecords(), resp.getIsLast(), list);
}
/**
* 是否是最后一页
*/
public static Boolean isLastPage(long totalRecords, int pageNo, int pageSize) {
if (pageSize == 0) {
return false;
}
long pageTotal = totalRecords / pageSize + (totalRecords % pageSize == 0 ? 0 : 1);
return pageNo >= pageTotal ? true : false;
}
}
登录拦截器
token拦截器
我们可以使用拦截器,对所有请求进行拦截,可以做一个统一的身份认证,需要注意的是,我们请求的时候,参数是放在Authorization
里面,这里面是两部分组成,格式为:Bearer token
,所以我们还需要解析,去掉前面的Bearer
,注意后面有个空格
@Order(-2)
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_SCHEMA = "Bearer ";
public static final String ATTRIBUTE_UID = "uid";
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取用户登录token
String token = getToken(request);
Long validUid = loginService.getValidUid(token);
if (Objects.nonNull(validUid)) {// 有登录态
request.setAttribute(ATTRIBUTE_UID, validUid);
} else {
boolean isPublicURI = isPublicURI(request.getRequestURI());
if (!isPublicURI) {// 又没有登录态,又不是公开路径,直接401
HttpErrorEnum.ACCESS_DENIED.sendHttpError(response);
return false;
}
}
return true;
}
/**
* 判断是不是公共方法,可以未登录访问的
*
* @param requestURI
*/
private boolean isPublicURI(String requestURI) {
String[] split = requestURI.split("/");
return split.length > 2 && "public".equals(split[3]);
}
private String getToken(HttpServletRequest request) {
String header = request.getHeader(AUTHORIZATION_HEADER);
return Optional.ofNullable(header)
.filter(h -> h.startsWith(AUTHORIZATION_SCHEMA))
.map(h -> h.substring(AUTHORIZATION_SCHEMA.length()))
.orElse(null);
}
}
Collector拦截器
将上面拦截到的token保存到RequestHolder里面,下次可以直接从这里取数据
/**
* 信息收集的拦截器
*/
@Order(1)
@Slf4j
@Component
public class CollectorInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
RequestInfo info = new RequestInfo();
info.setUid(Optional.ofNullable(request.getAttribute(TokenInterceptor.ATTRIBUTE_UID))
.map(Object::toString).map(Long::parseLong).orElse(null));
info.setIp(ServletUtil.getClientIP(request));
RequestHolder.set(info);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestHolder.remove();
}
}
RequestHolder
/**
* 请求上下文-用于存储请求信息
*/
public class RequestHolder {
private static final ThreadLocal<RequestInfo> threadLocal = new ThreadLocal<>();
public static void set(RequestInfo requestInfo) {
threadLocal.set(requestInfo);
}
public static RequestInfo get() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
}
RequestInfo
@Data
public class RequestInfo {
private Long uid;
private String ip;
}
拦截器配置
上面一共有两个拦截器,我们要注意拦截器的顺序,先拦截token,再保存到ThreadLocal里面
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;
@Autowired
private CollectorInterceptor collectorInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/capi/**");
registry.addInterceptor(collectorInterceptor)
.addPathPatterns("/capi/**");
}
}
使用Holder
这时候我们可以在任意地方通过 ThreadLocal获取用户信息了。
@GetMapping("/userInfo")
@ApiOperation("获取用户信息")
public ApiResult<RequestInfo> getUserInfo() {
RequestInfo requestInfo = RequestHolder.get();
return ApiResult.success(requestInfo);
}
测试
ApiFox添加全局参数:格式为:Bearer token
请求结果:
全局异常处理器
在参数校验的时候,如果不进行异常的捕获,那么返回给前端的格式数据都不统一,而且有可能暴露我们的后台信息,因此我们要对这些异常进行捕获,做统一的处理
异常处理器
使用RestControllerAdvice捕获
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* validation参数校验异常
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ApiResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException e) {
StringBuilder errorMsg = new StringBuilder();
e.getBindingResult().getFieldErrors().forEach(x -> errorMsg.append(x.getField()).append(x.getDefaultMessage()).append(","));
String message = errorMsg.toString();
log.info("validation parameters error!The reason is:{}", message);
return ApiResult.fail(CommonErrorEnum.PARAM_VALID.getErrorCode(), message.substring(0, message.length() - 1));
}
@ExceptionHandler(value = Throwable.class)
public ApiResult<?> throwable(Throwable e) {
log.error("system exception! The reason is :{}", e.getMessage());
return ApiResult.fail(CommonErrorEnum.SYSTEM_ERROR);
}
}
异常错误枚举
为了规范,我们定义接口,使每个异常错误枚举都要有这两个方法
public interface ErrorEnum {
Integer getErrorCode();
String getErrorMsg();
}
异常错误枚举类:
@AllArgsConstructor
@Getter
public enum CommonErrorEnum implements ErrorEnum {
SYSTEM_ERROR(-1, "系统出小差了,请稍后再试哦~~"),
PARAM_VALID(-2, "参数校验失败{0}"),
FREQUENCY_LIMIT(-3, "请求太频繁了,请稍后再试哦~~"),
LOCK_LIMIT(-4, "请求太频繁了,请稍后再试哦~~"),
;
private final Integer code;
private final String msg;
@Override
public Integer getErrorCode() {
return this.code;
}
@Override
public String getErrorMsg() {
return this.msg;
}
}
使用
在我们的controller中,对要校验的字段加@Valid
注解
@PostMapping("/modifyName")
@ApiOperation("修改用户名")
public ApiResult<Void> modifyName(@Valid @RequestBody ModifyNameReq modifyNameReq) {
userService.modifyName(RequestHolder.get().getUid(), modifyNameReq.getName());
return ApiResult.success();
}
在实体类中,设置校验的规则:
@NotNull
@Length(max = 6, message = "用户名可别取太长,不然我记不住噢")
@ApiModelProperty("用户名")
private String name;
测试
自定义异常
如果业务逻辑中出现问题,我们希望可以快速抛出异常,结束方法:
@Override
public void modifyName(Long uid, String name) {
User user = baseMapper.getUserByName(name);
if (user.getName().equals(name)) {
throw new BusinessException("名称不能重复");
}
}
于是就需要定义自己的异常,然后加入到全局异常处理器中:
/**
* 自定义校验异常(如参数校验等)
*/
@ExceptionHandler(value = BusinessException.class)
public ApiResult businessExceptionHandler(BusinessException e) {
log.info("business exception!The reason is:{}", e.getMessage(), e);
return ApiResult.fail(e.getErrorCode(), e.getMessage());
}
自定义的业务异常:
@Data
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
protected Integer errorCode;
/**
* 错误信息
*/
protected String errorMsg;
public BusinessException() {
super();
}
public BusinessException(String errorMsg) {
super(errorMsg);
this.errorMsg = errorMsg;
}
public BusinessException(Integer errorCode, String errorMsg) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BusinessException(Integer errorCode, String errorMsg, Throwable cause) {
super(errorMsg, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BusinessException(ErrorEnum error) {
super(error.getErrorMsg());
this.errorCode = error.getErrorCode();
this.errorMsg = error.getErrorMsg();
}
@Override
public String getMessage() {
return errorMsg;
}
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
业务校验工具类
为了能够更加快速的排除异常,我们使用工具类,就像Junit单元测试理的Assert一样
public class AssertUtil {
/**
* 校验到失败就结束
*/
private static Validator failFastValidator = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory().getValidator();
/**
* 全部校验
*/
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
/**
* 注解验证参数(校验到失败就结束)
*
* @param obj
*/
public static <T> void fastFailValidate(T obj) {
Set<ConstraintViolation<T>> constraintViolations = failFastValidator.validate(obj);
if (constraintViolations.size() > 0) {
throwException(CommonErrorEnum.PARAM_VALID, constraintViolations.iterator().next().getMessage());
}
}
/**
* 注解验证参数(全部校验,抛出异常)
*
* @param obj
*/
public static <T> void allCheckValidateThrow(T obj) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj);
if (constraintViolations.size() > 0) {
StringBuilder errorMsg = new StringBuilder();
Iterator<ConstraintViolation<T>> iterator = constraintViolations.iterator();
while (iterator.hasNext()) {
ConstraintViolation<T> violation = iterator.next();
//拼接异常信息
errorMsg.append(violation.getPropertyPath().toString()).append(":").append(violation.getMessage()).append(",");
}
//去掉最后一个逗号
throwException(CommonErrorEnum.PARAM_VALID, errorMsg.toString().substring(0, errorMsg.length() - 1));
}
}
/**
* 注解验证参数(全部校验,返回异常信息集合)
*
* @param obj
*/
public static <T> Map<String, String> allCheckValidate(T obj) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj);
if (constraintViolations.size() > 0) {
Map<String, String> errorMessages = new HashMap<>();
Iterator<ConstraintViolation<T>> iterator = constraintViolations.iterator();
while (iterator.hasNext()) {
ConstraintViolation<T> violation = iterator.next();
errorMessages.put(violation.getPropertyPath().toString(), violation.getMessage());
}
return errorMessages;
}
return new HashMap<>();
}
//如果不是true,则抛异常
public static void isTrue(boolean expression, String msg) {
if (!expression) {
throwException(msg);
}
}
public static void isTrue(boolean expression, ErrorEnum errorEnum, Object... args) {
if (!expression) {
throwException(errorEnum, args);
}
}
//如果是true,则抛异常
public static void isFalse(boolean expression, String msg) {
if (expression) {
throwException(msg);
}
}
//如果是true,则抛异常
public static void isFalse(boolean expression, ErrorEnum errorEnum, Object... args) {
if (expression) {
throwException(errorEnum, args);
}
}
//如果不是非空对象,则抛异常
public static void isNotEmpty(Object obj, String msg) {
if (isEmpty(obj)) {
throwException(msg);
}
}
//如果不是非空对象,则抛异常
public static void isNotEmpty(Object obj, ErrorEnum errorEnum, Object... args) {
if (isEmpty(obj)) {
throwException(errorEnum, args);
}
}
//如果不是非空对象,则抛异常
public static void isEmpty(Object obj, String msg) {
if (!isEmpty(obj)) {
throwException(msg);
}
}
public static void equal(Object o1, Object o2, String msg) {
if (!ObjectUtil.equal(o1, o2)) {
throwException(msg);
}
}
public static void notEqual(Object o1, Object o2, String msg) {
if (ObjectUtil.equal(o1, o2)) {
throwException(msg);
}
}
private static boolean isEmpty(Object obj) {
return ObjectUtil.isEmpty(obj);
}
private static void throwException(String msg) {
throwException(null, msg);
}
private static void throwException(ErrorEnum errorEnum, Object... arg) {
if (Objects.isNull(errorEnum)) {
errorEnum = BusinessErrorEnum.BUSINESS_ERROR;
}
throw new BusinessException(errorEnum.getErrorCode(), MessageFormat.format(errorEnum.getErrorMsg(), arg));
}
}
此时上面名称重复的校验就由3行变成了1行
AssertUtil.isEmpty(user, "用户名已存在");