在成熟的项目开发中,都会由基础包提供一些项目通用性的功能组件,避免每个项目重复造轮子。本项目将springboot项目开发中常用的基础功能进行封装,目前本包具备统一依赖管理、异常处理、响应报文包装、统一日志管理、敏感数据加解密等功能。支持可插拔方式,只要引入依赖便具备上述功能。
github地址 github.com/chenxuancod… 下面讲讲如何实现~~~ (star star star)
统一依赖管理
将一些常用的依赖,统一梳理到基础包中,可以方便后续对组件进行管理(升级或漏洞修复之类),基础包中的组件依赖原则是稳定以及最少依赖。目前base包括以下组件,基本满足springboot项目开发的基本功能。各项目基础包的依赖由base统一管理,只需要引入base模块即可,特性包由各项目自己引入。 目前的基础包已经集成了mybatis-plus swagger等常用的基础组件
组件名称 | 版本 |
---|---|
spring-boot-starter-validation | 2.3.12.RELEASE |
spring-boot-starter-web | 2.3.12.RELEASE |
spring-boot-starter-test | 2.3.12.RELEASE |
spring-boot-starter-aop | 2.3.12.RELEASE |
mysql-connector-java | 8.0.16 |
mybatis-plus | 3.4.0 |
springfox-swagger2 | 2.8.0 |
springfox-swagger-ui | 2.8.0 |
swagger-bootstrap-ui | 1.8.5 |
lombok | 1.18.20 |
hutool-all | 5.7.14 |
异常处理
定义了统一全局异常处理器,鼓励不在业务代码中进行异常捕获, 将 dao、service、controller 层的所有异常全部抛出到上层. 减少try-catch对业务代码的侵入性
如果需要返回接口的指定错误提示信息,可以直接抛出自定义异常AiException
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日志请查看:
链路追踪
目前链路追踪通过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
在方法上标记即可
PS
由于没上传maven中央仓库,所以需要将代码down下来后install或者deploy到自己的***然后引入
<dependency>
<groupId>com.sleeper</groupId>
<artifactId>base</artifactId>
<version>1.0.0</version>
</dependency>
链接:https://juejin.cn/post/7035870090734567437