springboot开发,有这个包就够了!

3,322 阅读4分钟

2021-12-25更新

  1. 本项目已上传中央仓库,欢迎使用
<dependency>
    <groupId>io.github.chenxuancode</groupId>
    <artifactId>base</artifactId>
    <version>1.0.0-Release</version>
</dependency>
  1. 快速生成项目:执行如下命令,可快速生成包含本项目功能的springboot项目
mvn archetype:generate -DarchetypeGroupId=io.github.chenxuancode -DarchetypeArtifactId=archetype -DarchetypeVersion=1.0.0-Release -DgroupId=io.github.chenxuancode -DartifactId=demo -Dport=8888

Tip:

  • DgroupId修改为你的groupId,DartifactId修改为你项目的artifactId
  • 命令控制台会确认你项目的groupId、artifactId等信息,如无需修改回车确认即可
  1. 本项目代码已更新至github.com/chenxuancod…

last: 欢迎大家反馈建议!!! 码字不易,各位小可爱给个点赞关注star~~~

-------------------------------------------------------------------------分割

正文

在成熟的项目开发中,都会由基础包提供一些项目通用性的功能组件,避免每个项目重复造轮子。本项目将springboot项目开发中常用的基础功能进行封装,目前本包具备统一依赖管理异常处理响应报文包装统一日志管理敏感数据加解密等功能。支持可插拔方式,只要引入依赖便具备上述功能。

下面讲讲如何实现~~~

统一依赖管理

将一些常用的依赖,统一梳理到基础包中,可以方便后续对组件进行管理(升级或漏洞修复之类),基础包中的组件依赖原则是稳定以及最少依赖。目前base包括以下组件,基本满足springboot项目开发的基本功能。各项目基础包的依赖由base统一管理,只需要引入base模块即可,特性包由各项目自己引入。 目前的基础包已经集成了mybatis-plus swagger等常用的基础组件

组件名称版本
spring-boot-starter-validation2.3.12.RELEASE
spring-boot-starter-web2.3.12.RELEASE
spring-boot-starter-test2.3.12.RELEASE
spring-boot-starter-aop2.3.12.RELEASE
mysql-connector-java8.0.16
mybatis-plus3.4.0
springfox-swagger22.8.0
springfox-swagger-ui2.8.0
swagger-bootstrap-ui1.8.5
lombok1.18.20
hutool-all5.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在方法上标记即可

敏感数据加解密

有时候,在开发过程中需要对某一些数据如手机号、身份证号等数据在保存到数据库的时候进行加密处理,防止数据泄露。本基础包提供了@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解密.详情请看: github.com/chenxuancod…

优雅停机

没有优雅停机,服务器此时直接直接关闭(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));
    }