资讯动态

SpringBoot单体项目目录结构最佳实践:分层设计与模块划分

发布时间:2026/9/11 1:21:19 来源:尧图企业网站定制
SpringBoot 项目的目录结构说起来是个老生常谈的话题但很多人在实际落地时还是会踩坑。我见过不少项目包名乱起、类随便放甚至有人把所有代码都堆在 controller 里一个类几千行。用 SpringBoot 做单体项目目录结构直接影响后续的维护成本、团队协作效率甚至决定了项目能不能在半年后还能改得动。这篇东西我就结合自己做过的几个真实项目把单体项目里 SpringBoot 目录结构的最佳实践一次说清楚。这套东西适合谁看刚入行的后端开发、带团队的 leader、还有那些接手了个乱七八糟的老项目正准备重构的朋友。不管你是第一次建项目还是已经在踩坑的路上照着这套思路去梳理都能让项目清爽很多。1. SpringBoot 单体项目目录结构的设计逻辑1.1 为什么目录结构不是小事很多初学者觉得目录结构无所谓能跑就行反正 JVM 又不管你类放在哪个包。这个想法大错特错。目录结构本质上是对代码职责边界的物理划分它决定了团队协作时每个人改代码的影响范围决定了新成员接手项目的学习成本也决定了后续拆微服务时能不能顺利剥离。我举个实际例子。之前接手过一个项目它的用户相关代码分布在三个包里面controller 里有 UserControllerservice 里有 UserServiceutils 里还有 UserUtilsentity 和 model 里竟然各有一份 User 类。当时我们要加一个用户状态的字段改了 entity 里的 User结果 model 里的 User 没改序列化到前端的时候状态字段全是 null排查了一整天才找到问题。这种问题就是目录结构混乱带来的连锁反应。SpringBoot 的目录结构不像一些框架那样强制约定它能给你自由也容易让你放纵。官方文档其实推荐了基础的分层方式但官方推荐只是底线实际项目中还需要根据业务规模、团队习惯做细化。1.2 单体项目 vs 微服务的结构权衡在做目录结构设计之前先想清楚你的项目是单体还是微服务。这里讨论的主题是单体项目那就不能拿微服务的思路硬套。微服务强调按业务域拆分每个服务有自己独立的代码库。单体项目则所有代码在一个仓库里面这种情况下最常见的组织方式有两种一种叫按技术分层就是 com.company.project 下面直接分 controller、service、mapper、entity 这些包。这种方式的优点是结构简单一眼就能看懂每层有哪些类适合业务规模不大、团队人数少的项目。另一种叫按业务模块分包就是在技术分层之上先按业务域拆一层比如 com.company.project.order 下面再分 controller、service、mapper。这种方式的优点是业务边界清晰后续如果要拆微服务可以按包直接提取适合业务复杂度较高的单体应用。两种方式没有绝对的对错要看项目阶段。我的经验是如果是单团队维护的中小型项目技术分层够用了如果是多团队协作、业务模块繁多强烈建议加一层业务模块划分。大多数卡在中间的团队是名义上按业务模块拆了实际上类还是乱放这种比纯技术分层更糟糕。2. 标准目录结构与核心包解析2.1 完整的包结构长什么样我这里给出一个我实际项目中沉淀下来的标准结构先看整体长什么样com.company.project ├── ProjectApplication.java ├── common │ ├── constant │ ├── enums │ ├── exception │ ├── result │ └── utils ├── config │ ├── WebMvcConfig.java │ ├── MybatisPlusConfig.java │ ├── JacksonConfig.java │ └── Knife4jConfig.java ├── controller │ ├── UserController.java │ └── OrderController.java ├── service │ ├── UserService.java │ └── impl │ ├── UserServiceImpl.java │ └── OrderServiceImpl.java ├── mapper │ ├── UserMapper.java │ └── OrderMapper.java ├── entity │ ├── User.java │ └── Order.java ├── dto │ ├── UserLoginDTO.java │ └── OrderCreateDTO.java ├── vo │ ├── UserInfoVO.java │ └── OrderDetailVO.java ├── bo │ └── UserQueryBO.java └── task ├── OrderTimeoutTask.java └── DataSyncTask.java这个结构是目前市面上 SpringBoot 单体项目的主流布局它把不同的职责放到了不同的包下面类与类之间的引用关系也变得有迹可循而不是谁想依赖谁就依赖谁。2.2 启动类为什么要放在最外层很多人没意识到启动类的位置其实是有讲究的。ProjectApplication.java 放在 com.company.project 的最外层不是为了好看而是因为 SpringBoot 的组件扫描机制默认扫描启动类所在包及其子包。如果启动类放在 com.company.project 下面那么 SpringBootApplication 会默认扫描 com.company.project.controller、com.company.project.service、com.company.project.mapper 等所有子包里的组件。一旦你把启动类放到某个子包里比如 com.company.project.controller那 Spring 只会扫描 controller 和它下面的子包service 和 mapper 全都扫描不到启动直接报错。我在实际项目中见过有人把启动类挪到了 config 包里结果 SpringBoot 启动时找不到任何 Controller所有接口 404排查了大半天。后来发现就是扫描范围的问题。所以记住一条铁律启动类永远放在根包下面不改位置。还有一个细节是启动类本身它除了 SpringBootApplication 注解之外最好别写其他业务代码。如果需要配置一些启动时的逻辑可以单独写一个 ApplicationRunner 或者 CommandLineRunner 的实现类放在 config 或 task 包里面而不是堆在启动类里面。2.3 controller 层只做路由和参数绑定controller 层是 Web 层它唯一的职责是接收 HTTP 请求、校验参数格式、调用 service、把结果封装成响应返回给前端。它不应该包含任何业务逻辑。很多初学者写代码的时候喜欢在 controller 里直接写业务查询数据库、字符串拼接、状态判断全塞进去。这样写第一版很爽因为少写了好多类。但是到了第二版需求变更的时候你会发现自己陷入了一个大泥潭一个 controller 里全是杂七杂八的逻辑根本没有办法复用而且一旦有多个接口都要用同一段逻辑就只能复制粘贴。标准的 controller 应该是这样RestController RequestMapping(/api/user) RequiredArgsConstructor public class UserController { private final UserService userService; PostMapping(/login) public ResultString login(RequestBody Valid UserLoginDTO loginDTO) { String token userService.login(loginDTO); return Result.success(token); } GetMapping(/info) public ResultUserInfoVO info(RequestParam Long userId) { return Result.success(userService.getUserInfo(userId)); } }注意几个要点controller 不直接操作 mapper不自己 new Service而是通过构造器注入配合 Lombok 的 RequiredArgsConstructor 非常简洁方法签名只做参数绑定和结果封装所有业务逻辑都交给 service。2.4 service 层业务逻辑的核心载体service 层是业务逻辑的核心承载者。在大多数单体项目中service 层是最容易写乱的层因为它的边界宽泛既可以处理业务流程又可以调第三方接口又可以做数据组装。我在项目中习惯给 service 层设计接口 实现类的模式。接口定义业务能力实现类放具体逻辑。虽然有些观点认为不需要接口直接类就行但对于稍复杂的项目接口能带来几个实际好处一是方便写单元测试时 mock二是将调用方与实现解耦三是看接口就能快速了解这个模块提供哪些能力。service 层的实现类要加 Service 注解事务注解 Transactional 放在实现类的方法上而不是接口方法上。为什么因为 Spring 的事务是基于 AOP 的代理对象拦截的是实现类的方法接口上的注解在某些代理方式下不生效这是一个很隐蔽的坑。public interface UserService { String login(UserLoginDTO loginDTO); UserInfoVO getUserInfo(Long userId); } Service RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserMapper userMapper; private final RedisTemplateString, String redisTemplate; Override Transactional(rollbackFor Exception.class) public String login(UserLoginDTO loginDTO) { // 具体业务逻辑 return token; } Override public UserInfoVO getUserInfo(Long userId) { // 查询和组装逻辑 return userInfoVO; } }这里常见的问题是在 service 内部调用本类的其他方法。比如 getUserInfo 里调用了 this.getUserDetail()这会导致 Transactional 失效。原因很简单Spring 的代理是基于对象的this 调用走的是当前对象而不是代理对象事务切面根本没机会介入。要解决这个问题要么把需要事务的方法抽到另一个类要么将本类注入自己要么直接用 TransactionTemplate。2.5 mapper 层与 entity数据访问层该怎么组织mapper 层是 MyBatis 或 MyBatis-Plus 的数据访问层。在最新的 MyBatis 规范中mapper 是接口SQL 写在 XML 里或者通过注解写。接口上要加 Mapper 注解或者在启动类上加 MapperScan(com.company.project.mapper) 批量扫描。entity 实体类对应数据库表结构这一层比较简单但有几个坑要提醒。第一entity 里不要写业务方法它就是纯数据载体。第二字段命名使用驼峰命名法并在 mybatis 配置中开启 map-underscore-to-camel-case这样数据库的下划线字段能自动映射到驼峰属性mybatis: configuration: map-underscore-to-camel-case: true第三如果使用了逻辑删除、乐观锁这些功能最好在 entity 中显式加上对应字段和注解不然 MyBatis-Plus 的插件不会自动生效。2.6 config、common、dto、vo 等辅助包的定位config 包存放所有配置类。比如 WebMvcConfig拦截器、跨域配置、MybatisPlusConfig分页插件、JacksonConfigJSON 序列化规则、Knife4jConfigAPI 文档配置等。这一包里的类一般只做配置不做业务处理。common 包放项目内通用能力。其中 constant 放常量类enums 放枚举类exception 放自定义异常与全局异常处理器result 放统一返回结果封装utils 放工具类。特别说一下 exception强烈建议写一个全局异常处理器用 RestControllerAdvice 注解捕获业务异常和系统异常统一返回 Result 格式。这样做的好处是 controller 里不用到处 try-catch业务异常直接 throw 就行代码会干净非常多。dto、vo、bo 这三个概念很多人分不清。DTOData Transfer Object用于接收前端传参比如登录参数、创建订单参数VOView Object用于向前端返回数据比如用户信息视图BOBusiness Object用于业务层内部传递的数据对象比如带复杂查询条件的对象。它们的本质目的都是避免将 entity 直接暴露给前端或接收前端参数从而隔离数据结构和数据库结构的变化。3. 目录结构设计背后的为什么三个核心原则3.1 依赖方向要单向流动在分层的体系中依赖方向应该是 controller 依赖 serviceservice 依赖 mapper所有层都依赖 entity、dto、vo 和 common 这些基础包。反过来的依赖要坚决禁止mapper 不能反向依赖 servicecontroller 不能直接操作 mapper。为什么这么设计因为依赖方向一旦混乱就会出现循环依赖的问题。比如 A service 调了 B serviceB service 又调了 A serviceSpring 启动时就会报错。依赖方向清晰的项目即使出现多个 service 互相调用也可以通过构造器注入直接发现循环依赖提前暴露问题而不是等到运行时才爆。我在项目里会给团队定一个规则controller 里的方法逻辑控制在三行以内超过三行就必须抽到 service。这只是一个粗粒度的判断标准但实操下来很有效能逼着大家把业务下放到正确的层。3.2 包的粒度拆太粗是灾难拆太细是负担包的粒度也是很有讲究的。拆太粗比如整个项目就一个 controller 包放 50 个类找个业务接口要翻半天。拆太细比如一个只有 5 个字段的类就单独建一个包整个项目几十个包同样让人头疼。我的经验规则是一个业务模块相关的 controller/service/mapper/entity 保持同名包不额外拆子包。当某个包里的类超过 15 个时考虑按业务域子包拆分。只有通用的、与业务无关的类才放进 common 下面业务相关的类一律放到业务目录下面。比如订单模块拆成 com.company.project.order.controller、com.company.project.order.service、com.company.project.order.mapper、com.company.project.order.entity 这样比把所有 controller 堆到顶层更能体现业务边界。3.3 通用返回结果不要用 Map 当返回值很多项目图省事controller 里直接返回 Map 甚至返回 Object。这种写法在写 Demo 的时候没有问题但在正式项目中会带来灾难性的后果前端无法约定数据结构、接口文档难以自动生成、调用方不知道返回字段类型、后期字段变更影响不可控。我建议自己写一个统一的返回结果类放在 common/result 包下面Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }有了这个类所有的 controller 都返回 Result 前端解析逻辑统一处理。配合全局异常处理器不管代码里面抛出了什么异常返回给前端的始终是标准结构联调的时候省心太多了。4. 实操从零搭建一套合理的 SpringBoot 目录结构4.1 使用 IDEA 创建项目时直接规划包结构很多人用 IDEA 的 Spring Initializr 创建完项目后就直接往里面写代码包结构长什么样全凭心情。这里分享一下我的做法。创建项目时 Group 填 com.companyArtifact 填 project-name包名就自动生成为 com.company.project。项目创建完成后先不要急着写业务代码先把空的包结构建好。我一般是按这个顺序建包com.company.project ├── common (右键 New - Package) │ ├── constant │ ├── enums │ ├── exception │ ├── result │ └── utils ├── config ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── bo └── task建好这些空包之后再去写第一个业务功能的时候就知道每个类该放哪了而不是写一个类想半天。这套初始结构适用于绝大多数单体项目如果项目确实很小可以砍掉 bo 和 task其他推荐保留。4.2 每个包中最小必要类的参考空包只是骨架每个包里应该放哪些类才真正考验设计能力。这里我按包给一个最小必要类清单common/result 包里放 Result 类和 PageResult 类分页结果PageResult 在列表查询接口中几乎是必需的Data public class PageResultT { private Long total; private ListT records; public static T PageResultT of(Long total, ListT records) { PageResultT result new PageResult(); result.setTotal(total); result.setRecords(records); return result; } }common/exception 包里放一个自定义业务异常 BusinessException 和一个全局异常处理器 GlobalExceptionHandler。GlobalExceptionHandler 至少要处理三类异常业务异常、参数校验异常MethodArgumentNotValidException、兜底的 Exception。这个类写好了controller 层的 try-catch 可以全部删掉。config 包里按需添加配置类。做 Web 项目第一步建议加 WebMvcConfig 处理跨域加 Knife4jConfig 配置接口文档。这两个配置不需要很多代码但是越早加上越舒服不用等接口写完再补。controller/service/mapper/entity 这四层按业务模块逐步添加每新增一个模块就按四层套一套UserController、UserService、UserServiceImpl、UserMapper、User 实体。这样形成肌肉记忆之后团队成员之间看代码毫无障碍。4.3 配置文件与资源目录的安排SpringBoot 的配置文件默认在 src/main/resources 下面这个不用多说。但有几个实践建议值得参考。首先是多环境配置。我习惯拆成 application.yml、application-dev.yml、application-prod.yml 三份。application.yml 里只放公共配置比如应用名、端口不同环境差异化的配置数据库连接、Redis 地址放到各自的 profile 文件中。这样切换环境只需要指定 spring.profiles.activedev 或 prod不用在部署时手工改数据库地址。其次是静态资源和 mapper XML 文件的位置。如果使用 MyBatis 的 XML 方式写 SQLXML 文件我建议放到 resources/mapper/ 目录下面并在 application.yml 中指定mybatis: mapper-locations: classpath:mapper/*.xml不要为了约定优于配置就把 mapper XML 和 Java 接口放在同一个包下虽然 MyBatis 也支持但那样会让构建配置复杂化而且打包的时候容易漏文件。把 XML 统一放到 resources 下是很多团队验证过的稳定方案。还有一个细节resources 目录下的 static 和 templates 在前后端分离的项目中基本用不到。如果是自己写个小工具页面可以留着如果是纯后端接口项目建议删掉或者忽略避免误导新人以为这里要放页面模板。4.4 完整业务模块代码骨架示范前面说了那么多我来写一个完整的示例模块演示一个典型的用户模块按这套目录结构怎么落地。第一步创建 User 实体类Data TableName(user) public class User { TableId(type IdType.AUTO) private Long id; private String username; private String password; private String nickname; private String phone; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }第二步创建 UserMapper 接口Mapper public interface UserMapper extends BaseMapperUser { }第三步创建 DTO 接收前端参数Data public class UserLoginDTO { NotBlank(message 用户名不能为空) private String username; NotBlank(message 密码不能为空) private String password; }第四步创建 VO 返回前端数据Data public class UserInfoVO { private Long id; private String username; private String nickname; private String phone; private LocalDateTime createTime; }第五步定义 UserService 接口public interface UserService { String login(UserLoginDTO loginDTO); UserInfoVO getUserInfo(Long userId); void updateUserInfo(UserUpdateDTO updateDTO); }第六步写 UserServiceImpl 实现类Service RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserMapper userMapper; Override public String login(UserLoginDTO loginDTO) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUsername, loginDTO.getUsername()); User user userMapper.selectOne(wrapper); if (user null) { throw new BusinessException(1001, 用户不存在); } // 密码校验省略... return token; } Override public UserInfoVO getUserInfo(Long userId) { User user userMapper.selectById(userId); if (user null) { throw new BusinessException(1001, 用户不存在); } UserInfoVO vo new UserInfoVO(); BeanUtils.copyProperties(user, vo); return vo; } }最后写 UserControllerRestController RequestMapping(/api/user) RequiredArgsConstructor public class UserController { private final UserService userService; PostMapping(/login) public ResultString login(RequestBody Valid UserLoginDTO loginDTO) { return Result.success(userService.login(loginDTO)); } GetMapping(/info) public ResultUserInfoVO info(RequestParam Long userId) { return Result.success(userService.getUserInfo(userId)); } }这套代码看下来可能觉得平平无奇但正是这种平平无奇才是最佳实践。凡是让人眼前一亮的代码往往复杂过度而真正好的工程代码是每个人看第二遍都能马上改动的。5. 常见问题排查与避坑指南5.1 SpringBoot 启动类包扫描失效这个前面提过是最常见的问题。现象是项目能启动但访问接口 404或者某些 Bean 没有被注入。90% 的原因是启动类位置不对或者组件放在扫描范围之外。排查步骤很简单先确认启动类在根包下然后看需要注入的类上是否有 Service、Component、Repository 等注解再用 Autowired 注入的变量是否在启动类所在包的子包中。如果都没问题还可以在启动类上加 debug 参数--debug看看 Spring 到底扫描了哪些类。有一个容易被忽略的情况是某些第三方包的 Bean 需要手动开启配置类。比如使用 MapperScan 时如果 mapper 接口不在默认扫描路径下即使加了 MapperScan 也扫不到。这时候要检查 MapperScan 的包路径是不是准确的而不是只写在启动类上就觉得万事大吉。5.2 循环依赖问题如果说 SpringBoot 单体项目有什么问题让人最头疼循环依赖可以排前三。A service 依赖 B serviceB service 依赖 A service项目启动时报错The dependencies of some of the beans in the application context form a cycleSpringBoot 2.6 之后默认禁止循环依赖启动直接报错。很多人遇到这个问题的第一反应是加 Lazy 注解绕过这种做法虽然能让项目跑起来但是掩盖了设计问题。正确的处理方式是重新审视业务逻辑想办法把 A 和 B 之间互相调用的逻辑抽取到 C service 中或者用事件机制解耦。如果实在移除不了再考虑 Lazy。我见过一个项目因为不想改设计在一个 service 里面加了五六个 Lazy后面每次启动都要等半天而且各种代理异常不断。最好的方式是回到目录结构的原则上让依赖关系单向流动从源头避免循环。5.3 实体类字段与数据库字段映射不上这种情况通常表现为查出来的数据某些字段是 null。原因一般有两个一是数据库字段是下划线命名user_name实体字段是驼峰命名userName但没有开启驼峰映射二是使用了 resultMap 但 resultMap 中没写全字段。第一个原因的解决办法是在 application.yml 中加mybatis: configuration: map-underscore-to-camel-case: true第二个原因就要去检查 mapper XML把 resultMap 的字段写全。还有一个隐蔽的场景是 MyBatis-Plus 的 LambdaQueryWrapper 在实体类字段名和数据库字段名不一致时的选择比如实体字段是 userName数据库字段是 username没有下划线这种情况下驼峰映射没问题但如果实体字段名和数据库字段完全对不上需要在字段上加 TableField 注解指定列名。其实这一类问题最有用的工具就是开启 MyBatis 的 SQL 日志logging: level: com.company.project.mapper: debug在日志里看到实际生成的 SQL 之后再对照实体类字段问题基本就清楚了。5.4 过度设计的诱惑讲完了避坑我想说一个相反的极端。有些开发者看完各种最佳实践之后开始疯狂设计每个接口都要有专属的 DTO 和 VO还要再加一层防腐层每个 service 都是接口与实现类光空类就建了几十个。这个方向也不对。最佳实践是给实际业务服务的不是用来表演的。一个只有 20 个接口的小项目搞出几百个类别人接手的时候光理解结构就要花一周这不是工程素养这是自嗨。我的判断标准是项目小于 5 个业务模块直接用技术分层不用刻意加业务包。DTO 和 VO 只有在字段与 entity 差异较大时才值得建完全相同的场景直接返回 entity 问题不大但需要注意安全性。service 接口如果只有一个实现类而且没有测试替代需求可以直接用类。加接口的好处有限但坏的代码结构伤害很大。判断设计是否合理有一个很实用的标准一个新人接手你的项目看了一个星期的代码之后能不能准确说出我想加一个功能应该改哪些文件。如果答案是模糊的说明目录结构的设计还不到位。6. 几个补充的实战技巧目录结构不是死的随着项目演进要灵活调整。这里分享几个我在实际项目中用得上的补充技巧。第一个是关于 task 包。单体项目里经常会用到定时任务比如订单超时关闭、数据同步。我把定时任务类统一放在 task 包下并且每个任务类名以 Task 结尾。这样在做任务治理、排查任务执行问题时只需要扫这一个包非常高效。配合 Scheduled 注解一个简单的定时任务几行代码就能写完不需要引入额外的分布式任务调度中心。第二个是关于 event 包。随着业务复杂度增加service 之间的耦合会越来越重。比如用户注册成功后要发短信、发优惠券、记录日志如果都在 register 方法里同步调用代码会越来越臃肿。这时候可以在项目中引入 Spring 的 ApplicationEvent事件发布者和监听者解耦监听者放在独立的 event/listener 包下面。目录结构上就多出 com.company.project.event 和 com.company.project.event.listener 两个包。这个模式对于降低 service 层复杂度非常有帮助。第三个是关于 constant 和 enums 的选择。很多人纠结常量定义在哪。我的规则是如果是模块内部的业务常量放在实体类或业务类内部作为 static final 字段或者是枚举类如果是全局通用的常量比如状态码、请求头名称放在 common/constant 下。不要所有常量都堆到一个大 Constant 类里那样最后一定会变成一个几千行的垃圾场。第四个是关于 utils 的使用。新手最容易犯的错是把什么都写进 util 类结果 util 类又长又耦合。我的经验是 util 里面只放无状态的静态方法比如日期格式化、加密、字符串处理。任何涉及配置和业务上下文的逻辑都不应该放在 util 里面。如果发现一个 util 方法需要传一堆配置参数说明这个地方应该是一个 Service 而不是 util。7. 从单体到微服务目录结构转型的预留虽然这篇是讲单体项目但只要你用的是按业务模块分包的结构后续的转型会比想象中顺利。这一点我深有体会。之前在重构一个单体电商后端的时候当时按 com.company.project.order、com.company.project.product、com.company.project.user 这样的模块分包。后面公司要把订单模块拆出来独立部署做法就是把这个包整体复制到新项目去掉其他模块的依赖补齐缺失的接口调用两周时间就完成了拆分。如果在初期就采用了纯技术分层所有业务类混在一起拆微服务的时候就要梳理每个类的业务归属一个订单相关的类可能分布在五六个包里涉及十几个其他模块的调用这种拆分工作量大到几乎让人放弃。所以我的建议是即使你现在做的单体项目也尽量预留一个业务模块维度的包边界。不需要一开始就分得很细但至少做到实体类、服务类能按业务域归位。这不是过度设计而是为未来留一条活路。最后再分享一下我踩过的坑。早期写的项目所有东西都是直接堆在默认包下面的连包结构都没有。当时觉得项目小无所谓后面加功能的时候真的是改一处崩三处后来痛定思痛花了整整两周时间做代码结构化重构把每个类都放到了该放的位置。自那以后我再也没有在目录结构上含糊过每次建项目第一件事就先把包结构搭好。这件事真的值得你多花点时间去做等到项目大了再想回头治理成本就高了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价