Web应用程序多模块单体应用代码目录结构设计
Web应用程序多模块单体应用代码目录结构设计
前言
每个有经验的开发者都有自己程序的开发规范,这篇文章是根据个人经验,以SpringBoot框架,java开发语言为示例,归纳总结出的一种代码目录结构。
在开发实践中不一定完全适合所有人,但是对于大部分人来说,应该可以满足大部分的场景。同时在实践中,需要基于需求,进行相应的调整,但大体上建议遵循该目录结构,提高代码的可读性以及可维护性.
后端
模块划分
即使是单体应用程序,也建议按照模块进行划分,每个模块对应一个目录,目录下存放该模块的代码。
📁 推荐模块结构(点击展开)
framework:框架模块,为整个应用程序提供基础功能,例如:数据库连接、缓存连接、日志记录、异常处理等。框架模块下应当有以下目录:- configration:框架级配置文件存放目录,例如:数据库连接配置、swagger配置文件等;
- exception:异常处理类存放目录;
- aspects:框架级切面类存放目录;
- interceptors:框架级拦截器存放目录;
- filters:框架级过滤器存放目录;
common:通用模块,为整个应用程序提供通用功能,例如:文件上传、图片处理、数据验证等,同时实体类、枚举类、工具类也可以放在该模块下。common模块下应当有以下目录:- models :模型存放目录,例如:实体类、请求参数模型、响应参数模型等;
- constants :常量存放目录,例如:枚举类、静态常量类等;
- utils :工具类存放目录;
业务模块:为业务功能提供支持,例如:后台管理、用户管理、权限管理、订单管理等。每个模块下应当有以下目录:
- service:业务逻辑处理类存放目录,包含接口定义类和实现类;
- controller:控制器存放目录;
- mapper:数据库操作类存放目录;
注意:如果业务模块中需要自定义特有的拦截器或者过滤器等需求,可以在对应的模块下创建相应的目录,例如:
interceptors、filters、aspects等。async:异步任务模块(可选),为应用程序提供异步任务功能,例如:定时任务、邮件发送、短信发送等。异步任务模块下应当有以下目录:- service:异步任务处理类存放目录,包含接口定义类和实现类;
- listener:监听器存放目录;
- mapper:数据库操作类存放目录;
- configration:异步任务配置文件存放目录,例如:定时任务配置文件等;
常见业务模块示例:
admin:后台模块,为后台提供功能,例如:用户管理、角色管理、权限管理等;user:用户模块,为用户提供功能,例如:用户注册、用户登录、用户注销、用户信息管理等;auth:认证模块,为用户提供认证功能,例如:用户登录、用户注册、用户注销等;命名要求
数据库
表名
要求使用下划线
_分隔前缀说明(
视需求而定)sys_:管理端数据表前缀,例如:系统配置表sys_configtb_:功能业务相关表,记录业务数据,也可以使用其他前缀,例如:业务中台bme_
注意
在 单体应用 或 系统体量较小 的情况下,不建议使用前缀。极其不推荐使用拼音缩写作为表名,建议使用英文单词!
字段名
- 要求使用下划线
_分隔 - 建议使用英文单词
✅ 正确示范:
user_name
❌ 错误示范:yhm
实体类
项目中应只出现三种后缀实体类:DTO、VO、PO。
提示
有些项目会使用一个 entity 类作为全局数据传输的数据封装类,这样的好处是避免了数据传输对象和实体对象之间的转换。但当页面展示内容与实体类存在差异时,需修改实体类,导致其臃肿复杂。因此,除非必要,尽量避免仅使用 entity 类作为全局数据传输对象。
领域驱动设计(DDD)提示
这里可能有些读者阅读过领域驱动设计(DDD)的设计文章,因此可以看出这是一个 贫血模型。如果想要引入 充血模型,则需要考虑当前系统是否存在领域模型。如果存在,则应该创建领域模型,当前的实体模型只用来和数据库进行交互。
存放位置
- 实体类应都存在于
model包中 - 每种类型的实体类应新建独立子包(如
dto、vo、po) - 在各类型包下再按 业务内容 创建子包
📌 示例路径:model/dto/user/CreateUserDTO
其中 user 是根据对象内容新增的一层归纳。
命名规范
| 类型 | 用途 | 命名示例 |
|---|---|---|
DTO | 接口请求参数 / 多入参封装 | CreateUserDTO,PaginatedQueryUserInfoListDTO |
VO | 接口响应参数 / 多返回值封装 | QueryUserDetailInfoVO |
PO | 数据库实体 / DB 映射字段 | UserPO |
方法名命名
方法命名采用 蛇形命名法(camelCase),首字母小写,格式为:{action}{object}{data range}{condition}
🔍 命名组件说明
action:动作(create,update,delete,query,paginatedQuery)object:操作对象(如User)data range(可选):detailInfo,infoListcondition(可选):如ByUnCode
💡 Service 接口方法命名示例(UserService)
- Boolean createUser(CreateUserDTO createUserDTO)
- Boolean updateUser(UpdateUserDTO updateUserDTO)
- Boolean deleteUser(String unCode) 或 Boolean deleteUser(DeleteUserDTO deleteUserDTO)
- QueryUserDetailInfoVO queryUserDetailInfo(String unCode)
- PaginatedQueryUserInfoListVO paginatedQueryUserInfoList(Page page)
变量名命名
变量名需“名副其实”,见名知意。若需注释补充,应考虑优化命名。
| 类型 | 建议 | 示例 |
|---|---|---|
| 布尔值 | 使用正逻辑(is, exist) | ✅ isSuccess❌ flag, notSuccess |
| 字符串 | 可加 Str 后缀 | errorMsgStr |
| 数值 | 明确对象 | timeOfRequestRetry |
| 数组 | 可加 Arry 后缀 | idArry |
| 列表 | 可加 List 后缀 | userList |
警告
极特殊情况下,变量命名 isXxxx 可能在序列化时导致错误。
代码功能划分
在继续对后端进行目录结构设计时,我们需要有以下共识作为前提:
- 每个业务实体都要有几个通用属性,例如:表id、业务主键id、创建人、创建时间、更新人、更新时间、逻辑删除标识、版本号等(详见 通用功能 章节);
- 除了自身模块的Service可以引入当前模块的Mapper之外,其他模块都不允许直接引入当前模块的Mapper。如果需要查询数据或者操作指定模块的数据,则需要通过引入指定模块的Service来实现,例如:当需要在订单模块查询用户信息时,则需要引入用户模块的Service,而不是直接引入用户模块的Mapper;
通用功能
工具类
💾 TimeUtil - 日期工具类
用途:日期格式化,例如创建日志表名需要
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeUtil {
/**
* @param date
* @return String
* @description: 将Date对象转化为yyyyMMdd格式的数据
*/
public static String getDateFormatStrYYYYMMDD(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
return sdf.format(date);
}
}🔑 UUIDUtil - UUID工具类
用途:返回32位小写去除"-"的uuid
import java.util.UUID;
public class UUIDUtil {
/**
* @description: 返回32位小写去除"-"的uuid
*/
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-","");
}
}审计日志
📋 审计日志规范
审计日志,记录用户操作日志,包含 操作人、操作IP、操作时间、操作内容、操作结果 等信息。该功能应当在framework层完成,并借助消息队列异步记录到数据库中。
异常处理
在设计系统时,考虑到多语言支持以及统一集中管理需求,则定义了异常响应枚举接口:
/**
* @description: 基础响应状态码接口,所有的响应状态码枚举都应实现该接口
*/
public interface BaseResCodeEnum {
String code();
String msg();
}每个异常响应状态码枚举,均继承该接口。例如:用户管理模块的异常响应状态码枚举:
/**
* @description: 用户相关响应状态码枚举类
*/
@Slf4j
public enum UserResCodeEnum implements BaseResCodeEnum {
RC_USER_NOT_EXIST("10000","用户不存在!"),
RC_ERROR_PASSWORD("10001","密码错误!"),
RC_ERROR_CAPTCHA("10002","验证码错误!"),
RC_ERROR_CREATE("10003","创建用户失败!"),
RC_ERROR_UPDATE("10004","更新用户失败!"),
RC_ERROR_BATCH_DELETE("10005","批量删除用户失败!"),
RC_ERROR_NOT_LOGIN("10006","用户未登录!"),
;
private String code;
private String msg;
UserResCodeEnum(String code,String msg) {
this.code = code;
this.msg = msg;
}
@Override
public String code() {
return code;
}
@Override
public String msg() {
return msg;
}
/**
* 根据code获取msg静态方法,每个响应状态码枚举类都应有这个方法
*/
public static UserResCodeEnum getMsgByCode(String code) {
for (UserResCodeEnum userResCodeEnum : UserResCodeEnum.values()) {
if (userResCodeEnum.code().equals(code)) {
return userResCodeEnum;
}
}
log.error("getMsgByCode error:无法获取枚举值");
return null;
}
}为了能够统一处理异常,需要定义自定义异常处理类,并且定义自定义的异常接口,并实现两种异常。
自定义异常接口:
/**
* @description: 基本异常接口
*/
public interface BaseException {
String getCode();
Object[] getExtendMessage();
String getMsg();
BaseResCodeEnum getResCodeEnum();
}自定义的异常实现类:
/**
* @description: 自定义业务异常
*/
public class BusinessException extends RuntimeException implements BaseException{
private BaseResCodeEnum resCodeEnum;
/**
* @description: 扩展消息数组,用于构造异常响应消息
*/
private Object[] extendMessage;
public BusinessException(BaseResCodeEnum resCodeEnum) {
this.resCodeEnum = resCodeEnum;
}
public BusinessException(BaseResCodeEnum resCodeEnum,Object ... args) {
this.resCodeEnum = resCodeEnum;
this.extendMessage = args;
}
@Override
public String getCode() {
return resCodeEnum.code();
}
@Override
public Object[] getExtendMessage() {
return extendMessage;
}
@Override
public String getMsg() {
return resCodeEnum.msg();
}
@Override
public BaseResCodeEnum getResCodeEnum() {
return resCodeEnum;
}
}/**
* @description: 自定义系统异常
*/
public class SystemException extends RuntimeException implements BaseException{
private BaseResCodeEnum resCodeEnum;
/**
* @description: 扩展消息数组,用于构造异常响应消息
*/
private Object[] extendMessage;
public SystemException(BaseResCodeEnum resCodeEnum) {
this.resCodeEnum = resCodeEnum;
}
@Override
public String getCode() {
return resCodeEnum.code();
}
@Override
public Object[] getExtendMessage() {
return extendMessage;
}
@Override
public String getMsg() {
return resCodeEnum.msg();
}
@Override
public BaseResCodeEnum getResCodeEnum() {
return resCodeEnum;
}
}自定义的异常处理类:
📋异常处理类示例代码(点击展开)
/**
* @description: 异常处理切面类
*/
@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
if (ArrayUtils.isNotEmpty(e.getExtendMessage())) {
String msg = e.getResCodeEnum().msg();
msg = MessageFormat.format(msg, e.getExtendMessage());
return Result.failure(e.getResCodeEnum(), msg, e);
}
//返回错误信息
return Result.failure(e.getResCodeEnum(), e);
}
/**
* 处理系统异常
*/
@ExceptionHandler(SystemException.class)
public Result handleSystemException(SystemException e) {
//返回错误信息
return Result.failure(e.getCode(), e.getMessage());
}
@ExceptionHandler(MissingServletRequestParameterException.class)
public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException e){
log.error(e.getMessage(), e);
String msg = MessageFormat.format(CommonResCodeEnum.RC_PARAM_MISS.msg(),e.getParameterName());
return Result.failure(CommonResCodeEnum.RC_PARAM_MISS.code(),msg);
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error(e.getMessage(), e);
//处理未知异常信息
return Result.failure(CommonResCodeEnum.RC_UNKNOWN_ERROR.code(),e.getMessage());
}
}有了上面的异常处理逻辑逻辑作为支撑之后,在实现业务逻辑时需要抛出异常时,只需要参考以下代码逻辑抛出异常即可
//业务逻辑...
throw new BusinessException(UserResCodeEnum.RC_USER_NOT_EXIST);提示
在抛出异常时,默认传入定义好的异常信息响应枚举,会自动返回对应的响应信息,如果需要扩展消息,则可以查看自定义的异常处理类中关于扩展消息的实现逻辑,并添加扩展消息。示例中演示的是业务异常信息,如果需要抛出系统异常信息,则需要更换成系统异常SystemException。
数据封装
统一响应对象数据封装
📦统一响应模板接口定义:
/**
*
* @description: 统一响应模板接口
*/
public interface BaseResult {
/**
* 响应码
*/
String getCode();
/**
* 消息体
*/
String getMsg();
}接口定义之后,定义统一响应对象,并实现该接口。
🎯 统一响应对象:
📋 统一响应对象示例代码(点击展开)
/**
* @description: 统一响应模板对象
*/
public class Result<T> implements BaseResult, Serializable {
private static final long serialVersionUID = 1L;
/**
* 响应状态码,默认成功
*/
@Schema(description = "响应状态码")
private String code = CommonResCodeEnum.RC_SUCCESS.code();
/**
* 执行状态
*/
@Schema(description = "执行状态")
private boolean success = true;
/**
* 响应的数据体
*/
@Schema(description = "响应的数据体")
private T data;
/**
* 异常信息
*/
private BaseException exception;
/**
* 消息
*/
@Schema(description = "消息")
private String msg = CommonResCodeEnum.RC_SUCCESS.msg();
/**
* 构造方法
*/
public Result() {
}
public Result(String code) {
this.code = code;
if (CommonResCodeEnum.RC_SUCCESS.code().equals(code)) {
this.success = true;
} else {
this.success = false;
}
this.msg = CommonResCodeEnum.getMsgByCode(code).msg();
}
public Result(T data) {
this.data = data;
}
public Result(String code, String msg) {
this.code = code;
if (CommonResCodeEnum.RC_SUCCESS.code().equals(code)) {
this.success = true;
} else {
this.success = false;
}
this.msg = msg;
}
public Result(String code, String msg, BaseException be) {
this.code = code;
if (CommonResCodeEnum.RC_SUCCESS.code().equals(code)) {
this.success = true;
} else {
this.success = false;
}
this.msg = msg;
this.exception = be;
}
public Result(String code, T data) {
this.code = code;
this.data = data;
}
public Result(String code, T data, BaseException exception) {
this.code = code;
this.data = data;
this.exception = exception;
}
public Result(String code, T data, String msg, BaseException exception) {
this.code = code;
this.data = data;
this.msg = msg;
this.exception = exception;
}
public static <T> Result<T> success() {
return new Result<>();
}
public static <T> Result<T> success(T data) {
return new Result<>(data);
}
public static <T> Result<T> failure() {
return new Result<>(CommonResCodeEnum.RC_FAILURE.code());
}
public static <T> Result<T> failure(BaseResCodeEnum resCode) {
return new Result<>(resCode.code(), resCode.msg());
}
public static <T> Result<T> failure(BaseResCodeEnum resCode, BaseException be) {
return new Result<>(resCode.code(), resCode.msg(), be);
}
public static <T> Result<T> failure(BaseResCodeEnum resCode,String msg, BaseException be) {
return new Result<>(resCode.code(), msg, be);
}
public static <T> Result<T> failure(String code) {
return new Result<>(code);
}
public static <T> Result<T> failure(String code, String msg) {
if (StringUtils.isNotEmpty(msg)){
return new Result<>(code, msg);
}
return new Result<>(code);
}
public void setCode(String code) {
this.code = code;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public void setSuccess(boolean success) {
this.success = success;
}
public boolean getSuccess() {
return this.success;
}
public BaseException getException() {
return exception;
}
public void setException(BaseException exception) {
this.exception = exception;
}
/**
* 非必要不要调用,需要严格按照枚举的code进行翻译
*/
@Deprecated
public void setMsg(String msg) {
this.msg = msg;
}
@Override
public String getCode() {
return code;
}
@Override
public String getMsg() {
return msg;
}
}💡 统一响应对象说明
统一响应对象封装了 数据、状态码、消息 等信息,并实现了 BaseResult 接口,因此,该对象可以作为统一响应对象使用。
统一响应对象的重要性:在和前端进行交互时,前端可在前置路由卫士中对响应结果进行判断,并返回对应的错误信息,以此实现统一的错误处理。
🎯 统一响应对象使用场景:
public class NoticeController {
@Autowired
private INoticeService noticeService;
@GetMapping("/query")
public Result<NoticeVO> query(@RequestParam Long noticeId){
NoticeVO noticeVO = noticeService.query(noticeId);
return Result.success(noticeVO);
}
@PostMapping("/add")
public Result add(@RequestBody NoticeDTO noticeDTO){
noticeService.add(noticeDTO);
return Result.success();
}
}当在拦截器中出现异常时,需要返回错误信息,此时可以使用统一响应对象,并将自定义的错误响应信息枚举传给统一响应对象,序列化至返回流中,并返回给前端。
public class JWTAuthenticationFilter extends OncePerRequestFilter implements Ordered {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 拦截器逻辑...
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(response.getOutputStream(), Result.failure(AuthResCodeEnum.RC_NOT_FOUND_TOKEN));
//后续逻辑...
}
}模型基类封装
为了能够统一处理请求和响应参数模型,定义了 DTO 模型基类和 VO 模型基类,所以请求参数模型和返回结果模型都应该继承基类模型(特殊业务实现除外)。实际使用场景有:统一对响应模型中的创建人和修改人进行翻译。各个模型基类模型如下:
/**
* @description: 分页获取信息列表接口请求参数基础模型,每一个分页查询接口的请求参数模型若无特殊需求都应该继承该模型
*/
@Data
@Schema(description = "分页获取信息列表接口请求参数基础模型")
public class PaginationQueryBaseDTO {
/**
* 分页参数:当前页码
*/
@Schema(description = "分页参数:当前页码",required = true)
private Long current;
/**
* 分页参数:页面大小
*/
@Schema(description = "分页参数:页面大小",required = true)
private Long pageSize;
/**
* 模糊搜索关键词
*/
@Schema(description = "模糊搜索关键词")
private String context;
}/**
* @description: 基础响应信息模型,若无特殊需求,则每一个响应模型都应该继承该模型
*/
@Data
@Schema(description = "基础响应信息模型")
public class BaseVO {
/**
* 业务主键
*/
@Schema(description = "业务主键")
private String unCode;
/**
* 记录创建人
*/
@Schema(description = "记录创建人")
private String createBy;
/**
* 记录创建时间
*/
@Schema(description = "记录创建时间")
private Date createTime;
/**
* 记录更新人
*/
@Schema(description = "记录更新人")
private String updateBy;
/**
* 记录最近更新时间
*/
@Schema(description = "记录最近更新时间")
private Date updateTime;
}/**
* @description: 基础实体类
*/
@Data
public class BaseModel implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 表主键
*/
private Long id;
/**
* 业务主键
*/
@TableId(value = "un_code", type = IdType.INPUT)
private String unCode;
/**
* 记录创建人
*/
private String createBy;
/**
* 记录创建时间
*/
private Date createTime;
/**
* 记录更新人
*/
private String updateBy;
/**
* 记录最近更新时间
*/
private Date updateTime;
/**
* 逻辑删除标识,0:未删除;1:已删除
*/
@TableLogic
private Integer delFlag;
}前端
下面以vue3为例,介绍前端项目目录结构。React项目同理。
命名要求
📝 命名规范
- 文件名称:使用首字母小写的驼峰命名方式,例如:
dictType.js - 目录名称:使用首字母小写的驼峰命名方式,例如:
dictType
目录结构
📁 项目目录结构概览
src 目录下包含以下核心目录,各自承担不同的职责:
🧩 公共组件目录 (src/components/)
存放公共自定义组件,例如:
- 分页组件
- 表格组件
- 表单组件
- 弹窗组件
目录结构:
components/
├── componentName/
│ └── index.vue
└── anotherComponent/
└── index.vue每个组件都有独立的目录,目录名称即为组件名称,内部包含 index.vue 文件。
📋 常量定义目录 (src/constants/)
存放功能模块的常量定义。
文件命名:xxx.js(xxx 为功能模块名称)
示例:
- 用户管理功能 →
user.js - 字典管理功能 →
dict.js
// user.js
export const USER_STATUS = {
ACTIVE: 'active',
INACTIVE: 'inactive'
};📄 页面组件目录 (src/pages/)
存放各业务模块的页面组件。
目录结构:
pages/
└── user/ # 用户管理模块
├── index.vue # 主页面组件
└── components/ # 模块子组件
├── UserList.vue
└── UserForm.vue命名规则:
- 主页面组件:
index.vue - 子组件:
xxx.vue(根据业务命名)
⚙️ 工具类目录 (src/utils/)
存放功能模块的工具函数。
文件命名:xxx.js(xxx 为功能模块名称)
示例:
- 用户管理功能 →
user.js - 通用工具函数 →
common.js
// user.js
export const formatUserInfo = (user) => {
// 格式化用户信息逻辑
};📄 服务类目录 (src/services/)
存放各业务模块的服务类(API 请求封装)。
目录结构:
services/
└── user/ # 用户管理模块
└── index.js # 服务实现文件文件内容:包含具体的业务实现逻辑,如接口请求的封装。
// services/user/index.js
export const getUserList = (params) => {
return request.get('/api/users', { params });
};📦 状态管理目录 (src/stores/)
存放状态管理相关的类和逻辑。
用于管理应用的全局状态,如用户信息、权限数据等。
中间件
(内容待补充)