Hello~我是 sleeper,本篇跟大家介绍一个自己开发的 SpringBoot 开发基础包。
在成熟的项目开发中,都会由基础包提供一些项目通用性的功能组件,避免每个项目重复造轮子。本项目(base)将 SpringBoot 项目开发中常用的基础功能进行封装,目前本包具备统一依赖管理、异常处理、响应报文包装、统一日志管理、敏感数据加解密、优雅停机等功能。支持可插拔方式,只要引入依赖便具备上述功能。
本项目代码均已上传 github github.com/chenxuancod… (球球 star 哦~~~)
下面讲讲如何实现~~~
统一依赖管理
将一些常用的依赖,统一梳理到基础包中,可以方便后续对组件进行管理(升级或漏洞修复之类),基础包中的组件依赖原则是稳定以及最少依赖。目前 base 包括以下组件,基本满足 springboot 项目开发的基本功能。各项目基础包的依赖由 base 统一管理,只需要引入 base 模块即可,特性包由各项目自己引入。目前的基础包已经集成了 mybatis-plus、 swagger 等常用的基础组件,各组件版本如下图所示:
异常处理
定义了统一全局异常处理器,鼓励不在业务代码中进行异常捕获, 将 dao、service、controller 层的所有异常全部抛出到上层. 减少try-catch对业务代码的侵入性
如果需要返回接口的指定错误提示信息,可以直接抛出自定义异常ApiException
:
throw new ApiException("两次密码输入不一致");
复制代码
实现原理
使用@RestControllerAdvice
开启全局异常的捕获,自定义一个方法使用ExceptionHandler
注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。
@Slf4j
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(ApiException.class)
public ResultVO<String> apiExceptionHandler(ApiException e) {
log.error("接口请求异常:{}{}",e.getResultCode(),e.getMsg());
return new ResultVO<>(e.getResultCode(), e.getMsg());
}
@ExceptionHandler
public ResultVO unknownException(Exception e) {
log.error("发生了未知异常", e);
return new ResultVO<>(ResultCode.ERROR, "系统出现错误, 请联系网站管理员!");
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}
}
复制代码
其它未显式抛出的异常会自动被外层异常处理器识别为未知错误并返回前端。
日志处理
在 springboot 项目中,通常使用 logback 组件进行日志管理。那么如果每一个服务自己写一份 logback 配置文件,势必会导致日志格式、日志路径五花八门,不好管理,所以日志处理交由基础包统一处理。通常日志处理需要思考的几个点包括:日志如何打印、日志如何拆分管理、日志如何收集。
出入参日志打印
在 Controller 方法上使用 @WebLog 便可实现请求响应报文的打印
@PostMapping("/register")
@ApiOperation(value = "注册")
@WebLog
public String register(@RequestBody @Validated RegisterParam param) {
userService.register(param);
return "操作成功";
}
复制代码
实现原理:
定义日志切面LogAspect
public class LogAspect {
@Pointcut("@annotation(com.sleeper.common.base.annotate.WebLog)")
public void webLog() {}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("IP:{} Class Method:{}.{} Request Args: {}",request.getRemoteAddr(),joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), new Gson().toJson(joinPoint.getArgs()));
}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
log.info("Response Args : {} Time-Consuming : {} ms", new Gson().toJson(result),System.currentTimeMillis() - startTime);
return result;
}
}
复制代码
WebLog
注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface WebLog {
}
复制代码
日志拆分保存
日志拆分保存通过 logback 进行配置,当前日志进行错误日志与普通日志的拆分,每种文件类型再按天进行拆分,当天超过 200M 的日志文件再以文件名中编号递增的形式进行拆分,具体规则如下${LOG_ERROR_HOME}/${springAppName}-%d{yyyy-MM-dd}.%i.log ${LOG_INFO_HOME}/${springAppName}-%d{yyyy-MM-dd}.%i.log
|
使用AsyncAppender
异步输出的方式输出日志,完整的 logback 日志请查看:logback配置
链路追踪
目前链路追踪通过 MDC 实现,MDC 是 Slf4J 类日志系统中实现分布式多线程日志数据传递的重要工具可利用 MDC 将一些运行时的上下文数据打印出来。关于 MDC 的介绍可以看看这篇juejin.cn/post/690122…
实现原理
通过拦截器对请求进行拦截,生成 traceId 并通过 MDC put 接口设置到 THreadLocalMap 中
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader("traceId");
if (traceId == null) {
traceId = IdUtil.getSnowflake().nextIdStr();
}
MDC.put("traceId", traceId);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
MDC.remove("TRACE_ID");
}
复制代码
在 logback-spring.xml 中增加 %X{traceId}
<property name="PATTERN" value="%red(%d{yyyy-MM-dd HH:mm:ss.SSS}) %X{traceId} %yellow(%-5level) %highlight([%t]) %boldMagenta([%C]).%green(%method[%L]): %m%n"/>
复制代码
响应报文自动封装
通常接口都需要按照一定的结构返回,包括服务处理结果编码、编码对应的文本信息、返回值等,可以通过 @RestControllerAdvice
对 Controller 进行增强实现响应报文的自动封装
@RestControllerAdvice("com.sleeper")
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
// 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
return !returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseWrap.class);
}
@Override
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);
}
}
复制代码
对于不想自动封装结果的接口,使用注解 @NotResponseWrap
在方法上标记即可
敏感数据加解密
有时候,在开发过程中需要对某一些数据如手机号、身份证号等数据在保存到数据库的时候进行加密处理,防止数据泄露。本基础包提供了@SensitiveData
(作用于 CLASS) @SensitiveField
(作用于 FEILD,以实现加解密操作,只需在数据实体对象上加上 @SensitiveData
注解,在敏感字段上加上@SensitiveField
便可实现敏感数据加解密操作。
@Data
@SensitiveData
public class SysUser implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private String id;
@SensitiveField
private String mobile;
}
复制代码
实现原理
重写 mybatis 拦截器 ResultSetHandler.handleResultSets
ParameterHandler.setParameters
方法,在设置请求参数时识别注解并将字段进行 AES 加密,在获取结果集时识别注解并将字段进行 AES 解密.详情请看:敏感数据加解密
优雅停机
没有优雅停机,服务器此时直接直接关闭(kill -9),那么就会导致当前正在容器内运行的业务直接失败,在某些特殊的场景下产生脏数据。开启优雅停机后,在 web 容器关闭时,web 服务器将不再接收任何请求,并将等待活动请求完成的缓冲器。
实现原理
基础包使用的 Spring Boot 2.3 版本内置此功能,不需要再自行扩展容器线程池来处理,Jetty, Reactor Netty, Tomcat 和 Undertow 以及反应式和基于 Servlet 的 web 应用程序都支持优雅停机功能,只需要配置server.shutdown=graceful
.本基础包默认开启优雅停机功能,通过SPI
机制在服务中进行配置注入,并结合后续部署脚本以服务优雅停机的统一管理。
@Configuration
public class ShutDownConfig {
@Autowired
ServerProperties serverProperties;
@Autowired
LifecycleProperties lifecycleProperties;
@Bean
public void setShutDownConfig() {
serverProperties.setShutdown(Shutdown.GRACEFUL);
lifecycleProperties.setTimeoutPerShutdownPhase(Duration.ofSeconds(20));
}
复制代码
PS
由于没上传 maven 中央仓库,所以需要将代码 down 下来后 install 或者 deploy 到自己的私服然后引入
<dependency>
<groupId>com.sleeper</groupId>
<artifactId>base</artifactId>
<version>1.0.0</version>
</dependency>
复制代码
评论