如果本文对您有所帮助,动动小手,点赞不迷路~
每次新建一个项目时,大量工作需要重复,所以做了一个基于springboot的脚手架项目。
一个成熟的springboot项目应该包含哪些基本内容?
- token校验:对接口的安全性进行一定的保障;
- swagger文档:方便前后端联调;
- 代码生成器:减轻开发中POJO类、CRUD工作量,加快开发速度;
- 统一返回格式封装:包含基础返回数据格式和分页返回数据格式;
- 常用工具类:看业务需求和工作中遇见比较好的工具类;
- 全局异常配置:将错误信息返回给用户时便于理解。具体应包含:错误枚举和自定义异常;
- 多环境配置文件
- 日志配置:出现问题时便于查找。具体应包含:日志文件名按日期创建、统一打印接口出入参等。
JWT token
token主要是对访问资源做一定的保护,防止受到攻击,以下示例中只进行了登录校验,并未对资源访问角色做验证,部分配置和业务代码未贴出。
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthenticationInterceptor())
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/sendSms")
// 开放swagger
.excludePathPatterns("/swagger-ui.html")
.excludePathPatterns("/webjars/**")
.excludePathPatterns("/v2/**")
.excludePathPatterns("/swagger-resources/**")
.addPathPatterns("/**");
}
}
@Slf4j
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
public static AuthenticationInterceptor interceptor;
@Resource
private UserService userService;
@Resource
private SysConfig sysConfig;
@Resource
private JwtUtil jwtUtil;
@PostConstruct
public void init() {
interceptor = this;
interceptor.sysConfig = this.sysConfig;
interceptor.userService = this.userService;
interceptor.jwtUtil = this.jwtUtil;
}
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws BusinessException {
String token = httpServletRequest.getHeader(interceptor.sysConfig.getHeaderToken());
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
if (method.isAnnotationPresent(ValidUser.class)) {
ValidUser validUser = method.getAnnotation(ValidUser.class);
if (!validUser.value()) {
return true;
}
}
// 执行认证
if (token == null) {
throw new BusinessException(ResponseCode.UN_AUTHENTICATION);
}
// 获取token中的userId
Long userId;
try {
userId = interceptor.jwtUtil.getUserIdByToken(token);
} catch (JWTDecodeException j) {
throw new BusinessException(ResponseCode.UN_AUTHENTICATION);
}
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(User::getStatus, 1);
wrapper.eq(User::getId, userId);
User user = interceptor.userService.getBaseMapper().selectOne(wrapper);
if (user == null) {
throw new BusinessException(ResponseCode.USER_NOT_EXIST);
}
if (user.getStatus() == 0) {
throw new BusinessException(ResponseCode.USER_IS_DISABLED);
}
// 验证 token
interceptor.jwtUtil.validateToken(user, token);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
}
}
@Slf4j
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
public static AuthenticationInterceptor interceptor;
@Resource
private UserService userService;
@Resource
private SysConfig sysConfig;
@Resource
private JwtUtil jwtUtil;
@PostConstruct
public void init() {
interceptor = this;
interceptor.sysConfig = this.sysConfig;
interceptor.userService = this.userService;
interceptor.jwtUtil = this.jwtUtil;
}
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws BusinessException {
String token = httpServletRequest.getHeader(interceptor.sysConfig.getHeaderToken());
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
if (method.isAnnotationPresent(ValidUser.class)) {
ValidUser validUser = method.getAnnotation(ValidUser.class);
if (!validUser.value()) {
return true;
}
}
// 执行认证
if (token == null) {
throw new BusinessException(ResponseCode.UN_AUTHENTICATION);
}
// 获取token中的userId
Long userId;
try {
userId = interceptor.jwtUtil.getUserIdByToken(token);
} catch (JWTDecodeException j) {
throw new BusinessException(ResponseCode.UN_AUTHENTICATION);
}
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(User::getStatus, 1);
wrapper.eq(User::getId, userId);
User user = interceptor.userService.getBaseMapper().selectOne(wrapper);
if (user == null) {
throw new BusinessException(ResponseCode.USER_NOT_EXIST);
}
if (user.getStatus() == 0) {
throw new BusinessException(ResponseCode.USER_IS_DISABLED);
}
// 验证 token
interceptor.jwtUtil.validateToken(user, token);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
}
}
swagger
swagger配置较为简单:引入依赖、初始化配置即可。
//模块创建示例
@Bean
public Docket createUserRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("用户")
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.abab.common.controller"))
.paths(PathSelectors.regex("/user/.*"))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("springboot项目基础框架")
.description("springboot项目脚手架")
.version("1.0")
.contact(new Contact("alex","url","123@idwarf.cn"))
.build();
}
我的项目中使用了两套swaggerUI,一套是springfox-swagger-ui
,另一套是swagger-bootstrap-ui
。界面如下:
代码生成器
项目中常使用的代码生成器有:lombok注解(构造方法,setter,getter,hashcode,toString等),mybatis-plus(可直接使用CRUD的SQL,无需自己开发)。
以上详情请自行百度。
统一返回格式封装
可根据业务需求自行定义,示例仅供参考。示例中ResponseCode.java
为错误码枚举类。
@Data
@ApiModel("统一封装返回数据格式")
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
@ApiModelProperty("错误码")
private Integer code;
@ApiModelProperty("错误信息")
private String message;
@ApiModelProperty("返回数据")
private Object data;
public static Result<Void> error(String message) {
return new Result(ResponseCode.SERVER_EXCEPTION.getCode(), message, null);
}
public static Result<Void> error(Integer code, String message) {
return new Result(code, message, null);
}
public static Result<Void> error(ResponseCode responseCode) {
return new Result(responseCode.getCode(), responseCode.getMessage(), null);
}
public static <T> Result<T> success(T data) {
return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMessage(), data);
}
public static <T> Result<T> success() {
return new Result<T>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMessage(), null);
}
@JsonIgnore
public boolean isFailed() {
return !ResponseCode.SUCCESS.getCode().equals(this.code);
}
@JsonIgnore
public boolean isSucceed() {
return ResponseCode.SUCCESS.getCode().equals(this.code);
}
}
分页返回格式:
@Data
public class Page<T> {
/**
* 分页数据
*/
@ApiModelProperty(value = "返回数据")
private List<T> records;
/**
* 总条数
*/
@ApiModelProperty(value = "返回数据总数")
private Integer total;
/**
* 总页数
*/
@ApiModelProperty(value = "总页数")
private Integer pages;
/**
* 当前页
*/
@ApiModelProperty(value = "当前页码")
@Min(value = 1, message = "请输入正确的页码")
private Integer current;
/**
* 每页显示数量
*/
@ApiModelProperty(value = "每页显示数量")
@Min(value = 1, message = "请输入正确的数量")
private Integer size;
/**
* 设置MySQL查询中 limit offset
*/
@ApiModelProperty(hidden = true)
@JsonIgnore
public Integer getStart() {
return (this.current - 1) * size;
}
/**
* 设置总记录数和页面总数
*
* @param total 总记录数
*/
@ApiModelProperty(hidden = true)
public void setTotal(Integer total) {
this.total = total;
this.setPages(this.total % this.size > 0 ? this.total / this.size + 1 : this.total / this.size);
}
}
常用工具类
靠开发中的积累。
全局异常配置
自定义异常拦截处理
此节需配合统一返回格式封装
一起看,此处只例举部分异常,根据自定义异常和业务需求可自行删减,需注意:子异常需放在父异常上面。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@Resource
private HttpServletRequest request;
@ExceptionHandler(TokenExpiredException.class)
@ResponseBody
public Result<Void> tokenExpiredHandler(TokenExpiredException e) {
log.info("accessToken已失效:", e.getMessage());
return Result.error(ResponseCode.INVALID_TOKEN);
}
@ExceptionHandler(JWTVerificationException.class)
@ResponseBody
public Result<Void> jwtVerificationException(JWTVerificationException e) {
log.info("accessToken校验失败:", e.getMessage());
return Result.error(ResponseCode.UN_AUTHENTICATION);
}
@ExceptionHandler(Exception.class)
@ResponseBody
public Result<Void> unchangedExceptionHandler(Exception e) {
log.error("请求url[{}],发生[{}]异常: ", request.getRequestURL(), e.getClass().getName(), e);
return Result.error(ResponseCode.SERVER_EXCEPTION.getCode(), e.getMessage());
}
}
多环境配置文件
针对不同的环境需要有不同的配置,最为显著的就是数据库配置(上面的swagger
也只在dev
和uat
中配置),springboot中可以创建多个配置文件,根据环境去读取指定配置。
在application.properties中指定环境(启动脚本中也可指定):
spring.profiles.active=dev
日志配置
对日志输出配置可自行百度,本次配置主要是使用AOP对用户的出参和入参进行打印,以便查找并修复线上问题。
@Slf4j
@Aspect
@Component
public class AspectLog {
/**
* 换行符
*/
private static final String LINE_SEPARATOR = System.lineSeparator();
@Around("execution( * com.abab.common.controller..*.*(..))")
public Object accountController(ProceedingJoinPoint joinPoint) throws Throwable {
return doAround(joinPoint);
}
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取 @WebLog 注解的描述信息
/*String methodDescription = getAspectLogDescription(joinPoint);*/
// 打印请求相关参数
log.info("========================================== Start ==========================================");
// 打印请求 url
log.info("URL : {}", request.getRequestURL().toString());
// 打印描述信息
/*log.info("Description : {}", methodDescription);*/
// 打印 Http method
log.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
log.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
log.info("IP : {}", request.getRemoteAddr());
// 打印请求入参
log.info("Request Args : {}", getParams(joinPoint));
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
// 打印出参
log.info("Response Data : {}", JSONObject.toJSONString(result));
} catch (Throwable throwable) {
//throwable.printStackTrace();
log.info("Response ERROR : {}", throwable);
throw throwable;
} finally {
// 执行耗时
log.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
log.info("=========================================== End ===========================================" + LINE_SEPARATOR);
}
return result;
}
private String getParams(JoinPoint joinPoint) {
JSONObject params = new JSONObject();
// 参数名
String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
// 参数值
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if ((arg instanceof HttpServletResponse) || (arg instanceof HttpServletRequest)
|| (arg instanceof MultipartFile) || (arg instanceof MultipartFile[])) {
continue;
}
try {
params.put(argNames[i], args[i]);
} catch (Exception e1) {
log.error(e1.getMessage());
}
}
}
return params.toJSONString();
}
}