kyyee
发布于 2026-06-05 / 9 阅读
0
0

从0到1:构建SpringBoot项目脚手架的设计思路与实战

从0到1:构建SpringBoot项目脚手架的设计思路与实战

以我的开源项目 springboot-project-seed 为例,聊聊如何将重复劳动转化为可复用的架构资产。

开篇:一个后端开发的日常困境

相信每一位后端开发都有过这样的经历:

接手一个新项目,从零开始搭建工程。选依赖、配框架、写拦截器、封装响应结构、处理异常……一套流程走下来,少则半天,多则一两天。更让人头疼的是,团队里每个人搭建的风格还不一样——有人用Lombok,有人不用;有人统一异常处理,有人直接在Controller里try-catch;有人规范了响应格式,有人随心所欲。

结果就是:代码风格千差万别,维护成本居高不下,新人上手一脸茫然。

我自己就深有体会。在一个大型J2EE项目里,后端清一色用Spring Boot,但每个人写出来的代码却像是出自不同的团队。这显然不是技术能力的问题,而是缺少一套统一的规范和可复用的基础框架

于是,我决定做一个项目种子(Seed Project)——把那些重复性的、共性的工作抽取出来,封装成一个可复用的脚手架。以后每开一个新项目,直接基于这个种子迭代,只关注业务逻辑就好。

这就是 springboot-project-seed 的由来。

今天这篇文章,我会带着你从0到1,完整复盘这个脚手架的设计思路和实现细节。它已经在GitHub上开源,收获了21颗星和14个fork,希望对你也有帮助。

GitHub仓库地址https://github.com/kyyee/springboot-project-seed

第一部分:先想清楚,我们要做什么

在动手写代码之前,先明确目标。

这个脚手架要解决什么问题?

  1. 统一技术栈:规定好Spring Boot版本、JDK版本、依赖管理工具,避免各搞一套。
  2. 统一代码风格:封装通用的响应结构、异常处理、日志切面,让所有API的行为保持一致。
  3. 提升开发效率:把那些"每个项目都要写一遍"的基础设施代码抽出来,开箱即用。
  4. 沉淀最佳实践:把日常开发中积累的实用功能(文件分片上传、WebSocket推送、Kafka消费者管理等)固化到项目中。

技术选型一览

组件 选型 理由
核心框架 Spring Boot 3.0.1 相比2.x系列,性能有显著提升,且支持JDK17
JDK OpenJDK 17 LTS版本,长期维护,Spring Boot 3.x强制要求
构建工具 Maven / Gradle 双选支持,满足不同习惯
数据库 PostgreSQL 15 功能强大的开源关系型数据库,同时支持MySQL切换
消息队列 Kafka 2.12-2.5.1 高吞吐、分布式,适合大规模消息场景
ORM MyBatis / JPA 灵活切换,项目中已配置好模板

项目结构一览

springboot-project-seed/
├── src/main/java/com/kyyee/
│   ├── Application.java              # 启动类
│   ├── common/                       # 通用模块
│   │   ├── config/                   # 配置类(WebSocket、Kafka、数据源等)
│   │   ├── enums/                    # 枚举(错误码等)
│   │   ├── exception/                # 自定义异常 & 全局异常处理器
│   │   ├── interceptor/              # MVC拦截器
│   │   ├── aspect/                   # AOP切面(日志等)
│   │   └── utils/                    # 工具类
│   ├── controller/                   # 控制器层
│   ├── service/                      # 业务逻辑层
│   ├── dao/                          # 数据访问层
│   └── model/                        # 实体类 & DTO
├── src/main/resources/
│   ├── application.yml               # 主配置文件
│   ├── application-dev.yml           # 开发环境配置
│   └── application-prod.yml          # 生产环境配置
├── docker/Dockerfile                 # Docker构建脚本
└── pom.xml / build.gradle            # 依赖管理

结构清晰,分层明确。下面我们用一张图来展示整个脚手架的模块关系:

flowchart TB subgraph 接入层["接入层"] Controller["Controller<br/>(RESTful API入口)"] Interceptor["HandlerInterceptor<br/>(用户上下文解析)"] WebSocket["WebSocket<br/>(实时消息推送)"] end subgraph 核心能力层["核心能力层"] Aspect["AOP切面<br/>(请求日志、耗时统计)"] ExceptionHandler["全局异常处理器<br/>(统一异常拦截)"] Res["统一响应封装<br/>(Res<T>、错误码)"] end subgraph 业务支撑层["业务支撑层"] Service["Service<br/>(业务逻辑)"] Async["@Async<br/>(异步任务)"] Scheduled["@Scheduled<br/>(定时任务)"] end subgraph 数据与集成层["数据与集成层"] DAO["DAO/MyBatis<br/>(数据访问)"] Kafka["Kafka<br/>(消息队列)"] PostgreSQL["PostgreSQL<br/>(关系型数据库)"] end Controller --> Interceptor Controller --> Aspect Controller --> ExceptionHandler Controller --> Res Controller --> Service Service --> Async Service --> Scheduled Service --> DAO Service --> Kafka DAO --> PostgreSQL WebSocket --> Async

接下来,我们深入到每一个核心模块,看看具体是怎么实现的。

第二部分:基石——统一响应与异常处理

这是每个Web后端项目最基础也最重要的两个部分。如果连API的返回格式都不统一,前后端联调就是一场灾难。

2.1 统一HTTP响应结构

我们先定义一个通用的响应类 Res<T>,所有的API接口都返回这个格式:

@Data
public final class Res<T> {
    private static final String SUCCESS_CODE = "0000000000";
    private String code = SUCCESS_CODE;
    private String msg = "请求成功";
    private T data;

    // 私有构造方法,强制使用静态工厂方法创建实例
    private Res() {}

    public static Res<Object> success() {
        return new Res<>();
    }

    public static <T> Res<T> success(T data) {
        return new Res<>(data);
    }

    public static Res<Object> error(String code, String msg) {
        return new Res<>(code, msg);
    }

    public static Res<Object> error(IErrorCode error) {
        return new Res<>(error.getCode(), error.getMsg());
    }

    @JsonIgnore
    public boolean isSuccess() {
        return SUCCESS_CODE.equals(code);
    }
}

设计要点

  • 使用泛型,让 data字段可以承载任意类型的数据。
  • 提供 success()error()静态工厂方法,语义清晰。
  • 通过 @JsonIgnoreisSuccess()不在JSON序列化时输出,仅用于程序判断。

对应的错误码枚举 BaseErrorCode

public enum BaseErrorCode implements IErrorCode {
    SUCCESS("0000000000", "操作成功"),
    SYS_INTERNAL_ERROR("9999999999", "系统内部错误"),
    INVALID_PARAM_ERROR("A000000001", "参数校验错误"),
    API_NOT_EXIST_ERROR("A000000002", "请求的API不存在"),
    HTTP_REQUEST_METHOD_NOT_SUPPORTED_ERROR("A000000003", "HTTP请求方法不支持"),
    FILE_SIZE_ERROR("A000000004", "文件大小超出限制"),
    CALL_FAILED("A000000005", "远程调用失败"),
    SQL_EXCEPTION("A000000006", "SQL执行异常");
    // ...
}

有了统一的响应结构,前端只需要按照 code 判断成功或失败,然后取 data 展示即可。

2.2 全局异常处理——让Controller层告别try-catch

如果每个Controller方法里都写一堆try-catch,代码会变得臃肿不堪。利用Spring Boot的 @ControllerAdvice,我们可以将异常处理统一收口。

下图展示了全局异常处理的工作流程:

flowchart TD Request["前端发起请求"] --> Controller["Controller层处理"] Controller -->|"业务正常"| Success["返回 Res.success(data)"] Controller -->|"抛出异常"| ExceptionHandler["GlobalExceptionHandler<br/>(@ControllerAdvice)"] ExceptionHandler --> CheckException{异常类型判断} CheckException -->|"参数校验异常"| Valid["MethodArgumentNotValidException<br/>→ 返回校验错误信息"] CheckException -->|"业务异常"| Business["BaseException<br/>→ 返回业务错误码+信息"] CheckException -->|"文件超限"| File["MaxUploadSizeExceededException<br/>→ 返回文件大小限制提示"] CheckException -->|"其他未知异常"| Unknown["Exception<br/>→ 返回系统内部错误(兜底)"] Valid --> Response["统一JSON响应<br/>{code, msg, data}"] Business --> Response File --> Response Unknown --> Response Response --> Client["返回给前端"]

核心实现:GlobalExceptionHandler.java

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 处理参数校验异常(@Valid校验失败)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ResponseEntity<Object> methodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("Params valid exception, URI={}", SessionHelper.getRequest().getRequestURI(), e);
        // 将多个字段的校验错误拼接成一条消息
        String message = buildValidationMessage(e.getBindingResult());
        final Res<Object> res = Res.error(BaseErrorCode.INVALID_PARAM_ERROR.getCode(), message);
        return new ResponseEntity<>(res, HttpStatus.BAD_REQUEST);
    }

    // 处理业务自定义异常
    @ExceptionHandler(BaseException.class)
    @ResponseBody
    public ResponseEntity<Object> baseException(BaseException e) {
        log.error("Business exception, message={}", e.getMessage(), e);
        final Res<Object> res = Res.error(e.getCode(), e.getMessage());
        return new ResponseEntity<>(res, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // 处理所有未捕获的异常——最后的兜底
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseEntity<Object> globalHandle(Exception e) {
        log.error("Unhandled exception", e);
        final Res<Object> res = Res.of(BaseErrorCode.SYS_INTERNAL_ERROR);
        return new ResponseEntity<>(res, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

这样做的好处

  1. Controller层极其干净——只需要专注业务逻辑,不需要关心异常处理。
  2. 用户体验一致——所有异常最终都以统一的JSON格式返回。
  3. 便于监控——所有异常日志都集中打印,方便排查问题。

举个例子,现在在Controller里写一个接口:

@PostMapping("/user")
public Res<UserVO> createUser(@Valid @RequestBody UserCreateDTO dto) {
    UserVO user = userService.create(dto);
    return Res.success(user);
}

如果 dto 校验失败(比如用户名不能为空),框架会自动抛出 MethodArgumentNotValidException,然后被 GlobalExceptionHandler 捕获,返回:

{
    "code": "A000000001",
    "msg": "用户名不能为空;邮箱格式不正确",
    "data": null
}

不需要在Controller里写任何异常处理的代码。

第三部分:提升——日志切面与用户上下文

有了基础响应和异常处理,项目已经"能跑"了。但要让它"好用",还需要一些提升开发体验的组件。

3.1 请求日志切面——让每个接口的调用链路一目了然

生产环境排查问题时,最怕的就是"这个接口什么时候被调用的?传参是什么?返回了什么?"——没有日志,一切都靠猜。

我们通过AOP统一记录所有Controller的请求和响应,整个流程如下:

sequenceDiagram participant Client as 前端 participant Aspect as RequestAspect<br/>(AOP切面) participant Controller as Controller participant Service as Service Client->>Aspect: 发起HTTP请求 Aspect->>Aspect: 记录请求参数<br/>log.info(REQUEST) Aspect->>Controller: 调用目标方法 Controller->>Service: 业务处理 Service-->>Controller: 返回结果 Controller-->>Aspect: 返回响应 Aspect->>Aspect: 记录响应结果与耗时<br/>log.info(RESPONSE) Aspect-->>Client: 返回JSON响应
@Aspect
@Component
@Slf4j
public class RequestAspect {

    // 匹配所有Controller中的RequestMapping方法
    @Pointcut("execution(* com.kyyee..controller..*.*(..))")
    private void request() {}

    @Around("request()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String requestURI = request.getRequestURI();

        // 打印请求入参
        log.info("REQUEST {} : {}", requestURI, JSON.toString(joinPoint.getArgs()));
  
        long startTime = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            // 打印响应结果和耗时
            log.info("RESPONSE {} : {}ms, data: {}", requestURI, System.currentTimeMillis() - startTime, result);
            return result;
        } catch (Throwable e) {
            log.error("REQUEST {} failed, args: {}", requestURI, JSON.toString(joinPoint.getArgs()), e);
            throw e;
        }
    }
}

效果:每次接口调用,控制台会清晰地打印出请求参数、响应数据、耗时,排查问题效率大大提升。

3.2 用户上下文传递——ThreadLocal的妙用

在微服务或前后端分离项目中,通常会将用户信息放在Header中传递(如 username:admin&usercode:admin)。我们需要在请求链路中随时获取当前用户信息,而不是在每个方法里层层传参。

解决方案:利用 HandlerInterceptor + ThreadLocal,其工作原理如下:

flowchart LR subgraph Request["请求进入"] Header["Header携带用户信息<br/>username:admin&usercode:admin"] end subgraph Interceptor["拦截器层"] Parse["BaseHeaderInterceptor<br/>解析Header中的用户信息"] Set["存入ThreadLocal<br/>UserHandler.setUser()"] end subgraph Business["业务处理层"] Controller["Controller"] Service["Service"] DAO["DAO"] Get["随时通过<br/>UserHandler.getUser()<br/>获取当前用户"] end subgraph End["请求结束"] Clear["afterCompletion<br/>清除ThreadLocal<br/>防止内存泄漏"] end Header --> Parse --> Set --> Controller Controller --> Service --> DAO Controller -.-> Get Service -.-> Get DAO -.-> Get Get --> Clear

第一步:拦截器解析Header

public class BaseHeaderInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String user = request.getHeader(GlobalConstant.USER);
        if (StringUtils.hasText(user)) {
            // 解析Header中的用户信息
            User userInfo = parseUserInfo(user);
            // 存入ThreadLocal
            UserHandler.setUser(userInfo);
        }
        return true; // 继续向下传递
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 请求结束后清除,防止内存泄漏
        UserHandler.remove();
    }
}

第二步:在业务代码中随时获取用户信息

@Service
public class OrderService {
    public OrderVO createOrder(OrderCreateDTO dto) {
        User currentUser = UserHandler.getUser();
        // 使用 currentUser.getUserCode() 进行业务处理
    }
}

核心原理ThreadLocal 为每个线程维护一份独立的变量副本,请求到达时通过拦截器注入用户信息,请求结束时清除。这样在整个请求链路中(Controller → Service → DAO)都可以随时获取用户信息,无需显式传参。

第四部分:进阶——异步处理与定时任务

现代后端系统离不开异步和定时任务。来看看脚手架里是怎么做的。

4.1 异步任务——@Async让方法"飞"起来

Spring Boot 使用 @Async 注解非常方便,只需要两步:

第一步:开启异步支持

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.initialize();
        return executor;
    }
}

第二步:在方法上添加 @Async 注解

@Service
public class NotificationService {
    @Async
    public void sendNotification(String message) {
        // 耗时操作,比如发送邮件、推送WebSocket消息
        // 这个方法被调用时会立即返回,在后台线程池中执行
    }
}

异步任务的执行流程:

sequenceDiagram participant Controller as Controller participant Async as @Async代理 participant Pool as 线程池 participant Biz as 异步业务逻辑 participant Main as 主线程 Controller->>Async: 调用异步方法 Async->>Pool: 提交任务到线程池 Pool->>Biz: 在池中线程执行 Async-->>Controller: 立即返回(不等待结果) Controller-->>Client: 快速响应前端 Note over Biz: 耗时操作在后台执行<br/>(发送邮件、推送消息等) Biz-->>Pool: 执行完成

实际应用场景:用户下单成功后,异步发送订单通知、推送WebSocket消息,不影响主流程的响应时间。

4.2 定时任务——一个实用的Kafka消费者重启检测

定时任务最常见的用法是定期执行数据统计、缓存刷新等。这里分享一个实战中遇到的场景:Kafka消费者偶尔会掉线,需要定时检测并自动重启。

flowchart TD Start["定时触发<br/>(每5分钟)"] --> CreateClient["创建Kafka AdminClient"] CreateClient --> GetAssigned["获取当前消费者组<br/>已订阅的Topic列表"] GetAssigned --> GetBusiness["获取所有业务Topic列表"] GetBusiness --> Compare["对比:找出未订阅的Topic"] Compare --> Check{是否存在<br/>未订阅的Topic?} Check -->|"否"| End["结束检测"] Check -->|"是"| Find["找到监听这些Topic的<br/>Kafka Listener容器"] Find --> Restart["依次停止并重启<br/>对应的消费者容器"] Restart --> Log["记录恢复日志"] Log --> End
@Component
@Slf4j
public class KafkaConsumerRestartTask {

    @Resource
    private KafkaAdmin kafkaAdmin;
    @Resource
    private KafkaListenerEndpointRegistry endpointRegistry;

    // 每隔5分钟执行一次检测
    @Scheduled(cron = "${kyyee.config.kafka.container.restart-corn:0 0/5 * * * ?}")
    public void consumerRestart() {
        try (AdminClient client = createAdminClient()) {
            // 1. 获取当前消费者组订阅的topic列表
            Set<String> assignedTopics = getAssignedTopics(client);
  
            // 2. 获取所有业务topic
            List<String> allBusinessTopics = getBusinessTopics();
  
            // 3. 找出那些没有被订阅的topic(说明消费者掉线了)
            List<String> unassignedTopics = allBusinessTopics.stream()
                .filter(t -> !assignedTopics.contains(t))
                .collect(Collectors.toList());
  
            if (unassignedTopics.isEmpty()) {
                return;
            }
  
            // 4. 找到监听这些topic的Kafka容器,重启它们
            for (MessageListenerContainer container : endpointRegistry.getAllListenerContainers()) {
                if (container.getContainerProperties().getTopics().containsAny(unassignedTopics)) {
                    container.stop();
                    container.start();
                    log.info("Restarted Kafka consumer for topics: {}", unassignedTopics);
                }
            }
        } catch (Exception e) {
            log.error("Kafka consumer restart failed", e);
        }
    }
}

价值:在生产环境中,Kafka消费者可能因为网络抖动、Rebalance超时等原因掉线。这个定时任务就像一个"守护进程",自动检测并恢复,大大减少了运维的人工干预。

第五部分:实战功能——文件分片上传与WebSocket

这两个功能是实际业务中非常常见的需求,脚手架里都给出了完整的示例实现。

5.1 文件分片上传

大文件上传如果一次性读取到内存,很容易导致OOM。分片上传的思路是:前端将文件切成多个小块,后端逐块接收并保存,最后再合并

flowchart TD subgraph 前端 File["大文件(如1GB)"] --> Slice["使用Blob.slice()<br/>切分为多个分片(如每片5MB)"] Slice --> Upload1["并发上传分片1"] Slice --> Upload2["并发上传分片2"] Slice --> UploadN["并发上传分片N"] end subgraph 后端 Upload1 --> Save1["保存分片1<br/>/data/uploads/fileId/1"] Upload2 --> Save2["保存分片2<br/>/data/uploads/fileId/2"] UploadN --> SaveN["保存分片N<br/>/data/uploads/fileId/N"] Save1 --> Check{所有分片<br/>都已上传?} Save2 --> Check SaveN --> Check Check -->|"是"| Merge["按顺序合并分片<br/>生成完整文件"] Merge --> Clean["清理临时分片目录"] Clean --> Result["返回文件访问路径"] end Result --> Client["前端获取文件"]

FileServiceImpl.java 中,核心逻辑如下:

@Service
public class FileServiceImpl implements FileService {
  
    private static final String UPLOAD_DIR = "/data/uploads/";
  
    @Override
    public void uploadChunk(String fileId, int chunkIndex, int totalChunks, MultipartFile chunk) {
        String chunkDir = UPLOAD_DIR + fileId + "/";
        File dir = new File(chunkDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        // 每个分片单独保存为一个临时文件
        chunk.transferTo(new File(chunkDir + chunkIndex));
  
        // 记录分片上传进度到Redis或数据库
    }
  
    @Override
    public String mergeChunks(String fileId, String fileName) {
        String chunkDir = UPLOAD_DIR + fileId + "/";
        String targetPath = UPLOAD_DIR + fileName;
  
        // 按顺序读取所有分片,合并成一个文件
        try (FileOutputStream fos = new FileOutputStream(targetPath)) {
            List<File> chunks = Arrays.stream(new File(chunkDir).listFiles())
                .sorted(Comparator.comparingInt(f -> Integer.parseInt(f.getName())))
                .collect(Collectors.toList());
  
            for (File chunk : chunks) {
                Files.copy(chunk.toPath(), fos);
            }
  
            // 清理临时分片文件
            FileUtils.deleteDirectory(new File(chunkDir));
        } catch (IOException e) {
            throw new BaseException("文件合并失败", e);
        }
  
        return targetPath;
    }
}

配套前端:项目中的 file.jsfile.html 提供了完整的前端示例,展示了如何使用 Blob.slice() 进行文件切片,并并发上传。

5.2 WebSocket消息推送

WebSocket在实时通知、聊天、数据推送等场景中非常实用。脚手架里配置了完整的WebSocket支持,其消息流转过程如下:

sequenceDiagram participant Client as 浏览器客户端 participant WS as WebSocket<br/>(/websocket) participant Broker as 消息代理<br/>(SimpleBroker) participant Sender as WebSocketSender participant Service as 业务Service Note over Client,WS: 1. 连接建立阶段 Client->>WS: 建立WebSocket/SockJS连接 WS-->>Client: 连接成功 Client->>Broker: 订阅 /topic/order/xxx Note over Service,Client: 2. 消息推送阶段 Service->>Service: 业务状态变更(如订单状态更新) Service->>Sender: 调用sendMessageToAll() Sender->>Broker: convertAndSend(destination, message) Broker->>Client: 推送消息到订阅的客户端

配置类

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket")      // WebSocket连接端点
                .setAllowedOriginPatterns("*")
                .withSockJS();                  // 支持SockJS降级方案
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");  // 消息推送前缀
        registry.setApplicationDestinationPrefixes("/app");
    }
}

消息发送器

@Component
public class WebSocketSender {

    @Resource
    private SimpMessagingTemplate messagingTemplate;

    public void sendMessageToAll(String destination, Object message) {
        // 向所有订阅了 /topic/xxx 的客户端推送消息
        messagingTemplate.convertAndSend(destination, message);
    }

    public void sendMessageToUser(String userId, String destination, Object message) {
        // 向特定用户推送消息
        messagingTemplate.convertAndSendToUser(userId, destination, message);
    }
}

使用示例:在订单状态变更时,主动向客户端推送通知。

@Service
public class OrderService {
    @Resource
    private WebSocketSender webSocketSender;

    public void updateOrderStatus(String orderId, String status) {
        // 更新订单状态...
  
        // 推送消息给前端
        webSocketSender.sendMessageToAll("/topic/order/" + orderId, 
            Map.of("orderId", orderId, "status", status));
    }
}

第六部分:部署与运维——Docker化支持

项目不光要能开发,还要能方便地部署。脚手架中包含了 Dockerfile,让你可以快速构建镜像并容器化运行。

flowchart LR subgraph Build["构建阶段"] Source["源代码"] --> Maven["Maven编译打包<br/>mvn clean package"] Maven --> JAR["生成JAR包"] end subgraph Image["镜像构建"] JAR --> Docker["Dockerfile<br/>基于openjdk:17"] Docker --> ImageOut["springboot-project-seed:latest"] end subgraph Run["运行阶段"] ImageOut --> Container["Docker容器<br/>端口映射: 8080"] Container --> PG["连接PostgreSQL"] Container --> Kafka["连接Kafka"] end
FROM openjdk:17-jdk-slim AS builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

构建和运行:

# 构建镜像
docker build -t springboot-project-seed:latest .

# 运行容器
docker run -p 8080:8080 springboot-project-seed:latest

配合 application-prod.yml 中的生产环境配置(如数据库连接、Kafka地址),可以实现环境隔离,一套代码在不同环境跑。

第七部分:如何使用这个脚手架

如果你觉得这个项目对你有帮助,想在自己的项目中使用,步骤非常简单:

flowchart TD Step1["① 克隆项目<br/>git clone ..."] --> Step2["② IDEA打开<br/>选择pom.xml或build.gradle"] Step2 --> Step3["③ 下载依赖<br/>Maven/Gradle自动下载"] Step3 --> Step4["④ 修改配置<br/>application-dev.yml<br/>配置PostgreSQL和Kafka"] Step4 --> Step5["⑤ 启动应用<br/>运行Application.main()"] Step5 --> Step6["⑥ 验证<br/>访问 http://localhost:8080"] Step6 --> Step7["⑦ 开始开发<br/>在controller/service/dao下添加业务代码"]
  1. 克隆项目

    git clone https://github.com/kyyee/springboot-project-seed.git
    
  2. 用IDEA打开,选择 pom.xmlbuild.gradle,Maven/Gradle会自动下载依赖。

  3. 修改配置:在 application-dev.yml 中配置你的PostgreSQL连接信息和Kafka地址。

  4. 启动应用:运行 Application.java 中的 main 方法。

  5. 验证:访问 http://localhost:8080,可以看到示例接口的返回。

  6. 开始你的业务开发:在 controllerservicedao 包下添加你自己的业务代码。

需要定制的话

  • 想换数据库?修改 application.yml 中的 spring.profiles.activemysql,再调整连接配置即可。
  • 不需要Kafka?在 pom.xml 中移除相关依赖,删除Kafka相关的配置类就好。

总结与感悟

回顾一下我们做了什么

从0到1,我们构建了一个基于Spring Boot 3.x的项目脚手架,它包含:

mindmap root((springboot<br/>-project-seed)) 统一响应 Res泛型封装 错误码枚举 静态工厂方法 全局异常处理 @ControllerAdvice 参数校验异常 业务自定义异常 兜底异常处理 日志切面 请求入参记录 响应结果记录 接口耗时统计 用户上下文 HandlerInterceptor ThreadLocal传递 请求结束清除 异步与定时 @EnableAsync @Async异步任务 @Scheduled定时任务 Kafka消费者自愈 实用功能 文件分片上传 WebSocket推送 多数据源支持 部署运维 Dockerfile 多环境配置
模块 功能
统一响应 标准的 {code, msg, data} JSON格式
全局异常处理 统一的异常捕获、日志记录、友好提示
请求日志AOP 自动记录入参、出参、耗时
用户上下文 通过ThreadLocal在请求链路中传递用户信息
异步任务 支持@Async,提升接口响应速度
定时任务 示例:Kafka消费者掉线自动恢复
文件分片上传 大文件分片接收、合并
WebSocket推送 实时消息推送示例
多数据源 支持主从或不同业务库
Docker化 一键构建镜像,方便部署

一些思考

做这个脚手架,最大的收获不是代码本身,而是「抽象」的能力。

当你第三次从零搭建一个Spring Boot项目时,你会开始思考:哪些东西是每个项目都需要的?哪些是通用的?哪些是可以配置的?把这些共性抽出来,封装成可复用的组件,其实就是架构设计的雏形。

正如我在README里写的:

"利用面向对象的思想,抽取这类Web后端api项目的共同之处封装成一个项目种子。以后再开发类似的项目,就能直接在这个项目种子上迭代,减少重复劳动。"

下一步的规划

  • 升级到Spring Boot 3.2+:随着Spring Boot版本的迭代,持续跟进新特性。
  • 集成Spring Cloud:让脚手架支持微服务架构。
  • 完善单元测试:提供测试基类和示例,提升代码质量。
  • 添加更多数据库示例:如MongoDB、Redis的集成。

写在最后

这个项目已经在GitHub上开源,如果你觉得对你有帮助,欢迎StarFork。如果在使用中发现问题,或者有好的建议,非常欢迎提 IssuePull Request,一起把它做得更好。

项目地址https://github.com/kyyee/springboot-project-seed

我的个人博客https://yanglei.ltd —— 我会在这里分享更多技术文章和思考,欢迎来逛逛。

生而为学,死亦无悔。愿我们在技术的道路上,不断精进,彼此成就。


评论