在 Spring Boot 项目中,以下是几个包结构设计。
- 基于领域驱动设计和分层架构的包结构
- 基于功能分层而非按业务模块的包结构(下一篇介绍)
基于领域驱动设计和分层架构包结构
这个设计遵循了领域驱动设计(DDD)和分层架构的思想,同时保持了Spring Boot的简洁性。
txt
src/main/java
└── com
└── example
└── app
├── Application.java # 启动类
│
├── common # 公共模块
│ ├── annotation # 自定义注解
│ ├── config # 全局配置
│ ├── constant # 常量定义
│ ├── enums # 枚举类
│ ├── exception # 异常处理
│ │ ├── GlobalExceptionHandler.java
│ │ └── BusinessException.java
│ ├── model # 公共模型
│ │ ├── page # 分页相关
│ │ │ ├── PageRequest.java # 基础分页请求类
│ │ │ ├── PageResult.java # 分页结果封装类
│ │ │ └── PaginationUtils.java # 分页工具类
│ │ ├── response # 响应封装
│ │ │ ├── Result.java
│ │ │ └── ErrorResponse.java
│ │ └── validation # 验证工具
│ │ └── ValidatorUtils.java
│ └── util # 工具类
│ └── DateUtils.java
│
├── modules # 业务模块
│ ├── user # 用户模块
│ │ ├── controller
│ │ │ └── UserController.java
│ │ ├── service
│ │ │ ├── UserService.java
│ │ │ └── impl
│ │ │ └── UserServiceImpl.java
│ │ ├── repository # 数据访问层
│ │ │ └── UserMapper.java
│ │ ├── model # 模块内模型
│ │ │ ├── dto # 数据传输对象
│ │ │ │ ├── request
│ │ │ │ │ ├── UserCreateRequest.java
│ │ │ │ │ ├── UserUpdateRequest.java
│ │ │ │ │ └── UserQueryRequest.java # 继承PageRequest
│ │ │ │ └── response
│ │ │ │ └── UserResponse.java
│ │ │ ├── entity # 实体类
│ │ │ │ └── UserEntity.java
│ │ │ ├── vo # 视图对象
│ │ │ │ └── UserVO.java
│ │ │ └── query # 查询条件
│ │ │ └── UserQueryCondition.java
│ │ └── converter # 对象转换器
│ │ └── UserConverter.java
│ │
│ └── product # 产品模块(类似结构)
│ ├── controller
│ ├── service
│ ├── repository
│ └── model
│ ├── dto
│ │ ├── request
│ │ │ └── ProductQueryRequest.java
│ │ └── response
│ ├── entity
│ └── query
│
├── infrastructure # 基础设施层
│ ├── persistence # 持久化相关
│ │ └── mybatis # MyBatis配置
│ │ ├── mapper # Mapper XML文件
│ │ └── config
│ │ └── MyBatisConfig.java
│ └── web # Web相关配置
│ └── WebMvcConfig.java
│
└── security # 安全模块(可选)
├── config
├── filter
└── service
关键包说明
- common (公共模块)
-
存放整个项目通用的代码,避免重复。
-
分页相关的类(
PageRequest
,PageResult
)放在common.model.dto
中。
- moduleX (业务模块)
-
按功能模块划分,每个模块内聚自己的功能。
-
模块内部再按分层(controller/service/mapper)组织。
- model 子包
-
entity : 数据库实体类(如
User
) -
dto : 数据传输对象,用于前后端交互(如
UserDTO
用于创建/更新用户) -
query : 查询条件封装(如
UserQuery
,包含各种查询条件) -
vo: 视图对象,专门为前端展示定制的模型(可能组合多个实体)
- infrastructure 包(基础设施)
-
persistence/mybatis:MyBatis 相关配置
mapper
:XML 映射文件config
:MyBatis 配置类
包结构设计原则
-
模块化:按业务领域划分模块,而非技术层次
-
分层明确:controller -> service -> repository 清晰调用链
-
领域驱动:每个模块包含自己的模型、服务、仓储
-
高内聚低耦合:
- 模块间通过接口交互
- 公共组件放在 common 包
-
可扩展性:
- 新增业务只需添加新模块
- 不影响现有结构
-
关注点分离:
- 请求/响应 DTO 与实体分离
- 业务逻辑与基础设施分离
这种包结构设计结合了领域驱动设计(DDD)和分层架构的优点,特别适合中大型 Spring Boot 项目。它提供了清晰的代码组织方式,良好的可维护性和可扩展性。
适配 MyBatis Dynamic SQL 的包结构最佳实践
如果要使用MyBatis Generator生成MyBatis Dynamic SQL代码,我们需要对包结构做一些调整,因为MyBatis Dynamic SQL采用了一种不同的编程模型。
MyBatis Dynamic SQL的特点:
-
使用Java DSL构建动态SQL
-
不需要XML映射文件
-
生成的代码包括表对象、支持类以及Mapper接口
为了适配前面的包结构,我们需要:
-
将生成的表对象(Table)放在合适的包下
-
将生成的Mapper接口放在repository包下
-
将生成的支持类(如主键、字段等)放在专门的包下
MyBatis Dynamic SQL 是一个强大的动态 SQL 构建框架,当它与 MyBatis Generator 结合使用时,可以生成高度优化的动态 SQL 代码。以下是针对我们之前设计的包结构,适配 MyBatis Dynamic SQL 的最佳实践方案:
调整后的包结构
csharp
src/main/java
└── com
└── example
└── app
├── modules
│ ├── user
│ │ ├── controller
│ │ ├── service
│ │ ├── repository
│ │ │ ├── dynamic # MyBatis Dynamic SQL 生成代码
│ │ │ │ ├── UserMapper.java
│ │ │ │ ├── UserTable.java
│ │ │ │ ├── UserDynamicSqlSupport.java
│ │ │ │ └── ...
│ │ │ └── UserRepository.java # 自定义仓库接口
│ │ ├── model
│ │ │ ├── entity
│ │ │ │ └── UserEntity.java # MBG 生成的实体类
│ │ │ ├── dto
│ │ │ │ ├── request
│ │ │ │ │ └── UserQueryRequest.java
│ │ │ │ └── response
│ │ │ └── query
│ │ │ └── UserQueryCondition.java
│ │ └── converter
│ └── product
│ └── ... # 类似结构
└── infrastructure
└── persistence
└── mybatis
├── dynamic # MyBatis Dynamic SQL 基础配置
│ ├── config
│ │ └── MyBatisDynamicConfig.java
│ └── support
│ └── BaseRepository.java
└── mapper # XML 映射文件(如果需要)
MyBatis Generator 配置 (generatorConfig.xml
)
xml
<context id="MyBatis3DynamicSql" targetRuntime="MyBatis3DynamicSql">
<!-- 数据库驱动 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/your_db"
userId="root"
password="password">
</jdbcConnection>
<!-- 实体类生成位置 -->
<javaModelGenerator
targetPackage="com.example.app.modules.{module}.model.entity"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- Dynamic SQL 支持类生成位置 -->
<javaClientGenerator
type="ANNOTATEDMAPPER"
targetPackage="com.example.app.modules.{module}.repository.dynamic"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="rootInterface" value="com.example.app.infrastructure.persistence.mybatis.dynamic.support.BaseMapper"/>
</javaClientGenerator>
<!-- 表配置 -->
<table tableName="user"
domainObjectName="UserEntity"
mapperName="UserMapper">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<!-- 模块识别插件 -->
<plugin type="com.example.app.infrastructure.persistence.mybatis.generator.ModuleAwareGeneratorPlugin"/>
</context>
基础接口和配置
1. 基础 Mapper 接口 (BaseMapper.java
)
java
// src/main/java/com/example/app/infrastructure/persistence/mybatis/dynamic/support/BaseMapper.java
public interface BaseMapper<T> {
long count(CountDSLCompleter completer);
int delete(DeleteDSLCompleter completer);
int insert(T record);
int insertMultiple(Collection<T> records);
T selectOne(SelectDSLCompleter completer);
List<T> select(SelectDSLCompleter completer);
List<T> selectDistinct(SelectDSLCompleter completer);
int update(UpdateDSLCompleter completer);
}
2. MyBatis Dynamic SQL 配置类
java
// src/main/java/com/example/app/infrastructure/persistence/mybatis/dynamic/config/MyBatisDynamicConfig.java
@Configuration
public class MyBatisDynamicConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setTypeAliasesPackage("com.example.app.modules.*.model.entity");
// 配置 MyBatis Dynamic SQL
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*.xml")
);
return sessionFactory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public MapperFactoryBean<BaseMapper> baseMapper(SqlSessionTemplate sqlSessionTemplate) {
MapperFactoryBean<BaseMapper> factory = new MapperFactoryBean<>(BaseMapper.class);
factory.setSqlSessionTemplate(sqlSessionTemplate);
return factory;
}
}
自定义 Repository 实现
1. 自定义 Repository 接口
java
// src/main/java/com/example/app/modules/user/repository/UserRepository.java
public interface UserRepository {
PageResult<UserEntity> findByCondition(
UserQueryCondition condition,
PageRequest pageRequest
);
Optional<UserEntity> findByUsername(String username);
// 其他自定义方法
}
2. Repository 实现类 (使用 Dynamic SQL)
java
// src/main/java/com/example/app/modules/user/repository/impl/UserRepositoryImpl.java
@Repository
public class UserRepositoryImpl implements UserRepository {
private final UserMapper userMapper;
private final UserTable userTable = UserTable.user;
@Autowired
public UserRepositoryImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public PageResult<UserEntity> findByCondition(
UserQueryCondition condition,
PageRequest pageRequest
) {
// 构建动态查询
SelectDSLCompleter completer = dsl -> dsl.where()
.and(userTable.username, isLikeWhenPresent(condition.getUsername()))
.and(userTable.status, isEqualToWhenPresent(condition.getStatus()))
.orderBy(buildOrderSpecifier(pageRequest));
// 查询总数
long total = userMapper.count(completer);
// 分页查询
List<UserEntity> data = userMapper.select(completer,
c -> c.limit(pageRequest.getSize()).offset(pageRequest.getOffset()));
return new PageResult<>(pageRequest, total, data);
}
private OrderByDSL buildOrderSpecifier(PageRequest pageRequest) {
OrderByDSL orderBy = new OrderByDSL();
if (pageRequest.getSort() != null) {
BasicColumn column = getColumnForSort(pageRequest.getSort());
if (column != null) {
if ("desc".equalsIgnoreCase(pageRequest.getOrder())) {
orderBy.orderBy(column.descending());
} else {
orderBy.orderBy(column);
}
}
}
return orderBy;
}
private BasicColumn getColumnForSort(String sortField) {
// 映射排序字段到表列
switch (sortField) {
case "username": return userTable.username;
case "createdAt": return userTable.createdAt;
default: return null;
}
}
@Override
public Optional<UserEntity> findByUsername(String username) {
return userMapper.selectOne(c -> c.where(userTable.username, isEqualTo(username)));
}
}
Service 层适配
java
// src/main/java/com/example/app/modules/user/service/impl/UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserConverter userConverter;
@Autowired
public UserServiceImpl(UserRepository userRepository, UserConverter userConverter) {
this.userRepository = userRepository;
this.userConverter = userConverter;
}
@Override
public PageResult<UserResponse> getUsers(UserQueryRequest request) {
// 参数校验
request.validate();
// 转换查询条件
UserQueryCondition condition = userConverter.toCondition(request);
// 执行查询
PageResult<UserEntity> entityResult = userRepository.findByCondition(
condition,
request
);
// 转换为响应DTO
return convertToResponse(entityResult);
}
private PageResult<UserResponse> convertToResponse(PageResult<UserEntity> entityResult) {
List<UserResponse> responses = entityResult.getData().stream()
.map(userConverter::toResponse)
.collect(Collectors.toList());
return new PageResult<>(
entityResult.getPage(),
entityResult.getSize(),
entityResult.getTotal(),
responses
);
}
}
分页查询优化
1. 分页参数构建器
java
// src/main/java/com/example/app/common/model/page/PageDSLBuilder.java
public class PageDSLBuilder {
public static <T> SelectDSLCompleter applyPagination(
PageRequest pageRequest,
SelectDSLCompleter baseCompleter
) {
return dsl -> {
baseCompleter.accept(dsl);
// 应用分页
dsl.limit(pageRequest.getSize())
.offset(pageRequest.getOffset());
// 应用排序
if (pageRequest.getSort() != null) {
applySorting(dsl, pageRequest);
}
};
}
private static void applySorting(SelectDSL<?> dsl, PageRequest pageRequest) {
// 根据请求构建排序逻辑
// 实际实现中需要映射排序字段到表列
if ("username".equals(pageRequest.getSort())) {
if ("desc".equalsIgnoreCase(pageRequest.getOrder())) {
dsl.orderBy(UserTable.user.username.descending());
} else {
dsl.orderBy(UserTable.user.username);
}
}
// 添加更多字段...
}
}
2. 在 Repository 中使用
java
@Override
public PageResult<UserEntity> findByCondition(
UserQueryCondition condition,
PageRequest pageRequest
) {
// 基础查询条件
SelectDSLCompleter baseCompleter = dsl -> dsl.where()
.and(userTable.username, isLikeWhenPresent(condition.getUsername()))
.and(userTable.status, isEqualToWhenPresent(condition.getStatus()));
// 查询总数
long total = userMapper.count(baseCompleter);
// 应用分页和排序
SelectDSLCompleter paginatedCompleter =
PageDSLBuilder.applyPagination(pageRequest, baseCompleter);
// 执行分页查询
List<UserEntity> data = userMapper.select(paginatedCompleter);
return new PageResult<>(pageRequest, total, data);
}
高级特性:复杂查询构建
java
@Override
public PageResult<UserEntity> searchUsers(UserQueryCondition condition, PageRequest pageRequest) {
// 构建动态查询
SelectDSLCompleter completer = dsl -> {
dsl.where();
// 基本条件
if (condition.getUsername() != null) {
dsl.and(userTable.username, isLike(condition.getUsername() + "%"));
}
// 状态条件
if (condition.getStatus() != null) {
dsl.and(userTable.status, isEqualTo(condition.getStatus()));
}
// 时间范围
if (condition.getCreatedAtStart() != null || condition.getCreatedAtEnd() != null) {
dsl.and(userTable.createdAt,
isBetween(condition.getCreatedAtStart())
.and(condition.getCreatedAtEnd()));
}
// 角色关联查询
if (condition.getRoleIds() != null && !condition.getRoleIds().isEmpty()) {
dsl.and(userTable.id, in(
select(UserRoleTable.userRole.userId)
.from(UserRoleTable.userRole)
.where(UserRoleTable.userRole.roleId, isIn(condition.getRoleIds()))
);
}
};
// 查询总数
long total = userMapper.count(completer);
// 应用分页
SelectDSLCompleter paginatedCompleter =
PageDSLBuilder.applyPagination(pageRequest, completer);
// 执行查询
List<UserEntity> data = userMapper.select(paginatedCompleter);
return new PageResult<>(pageRequest, total, data);
}
MyBatis Generator 插件增强
模块感知插件
java
// src/main/java/com/example/app/infrastructure/persistence/mybatis/generator/ModuleAwareGeneratorPlugin.java
public class ModuleAwareGeneratorPlugin extends PluginAdapter {
@Override
public void initialized(IntrospectedTable table) {
String tableName = table.getFullyQualifiedTable().getIntrospectedTableName();
String module = determineModule(tableName);
// 设置实体类包路径
String entityPackage = table.getContext().getJavaModelGeneratorConfiguration().getTargetPackage();
entityPackage = entityPackage.replace("{module}", module);
table.setBaseRecordType(entityPackage + "." + table.getBaseRecordType());
// 设置Mapper包路径
String mapperPackage = table.getContext().getJavaClientGeneratorConfiguration().getTargetPackage();
mapperPackage = mapperPackage.replace("{module}", module);
table.setMyBatis3JavaMapperType(mapperPackage + "." + table.getMyBatis3JavaMapperType());
// 设置动态SQL支持类名称
String supportClass = table.getFullyQualifiedTable().getDomainObjectName() + "DynamicSqlSupport";
table.setDynamicSqlSupportType(mapperPackage + "." + supportClass);
}
private String determineModule(String tableName) {
if (tableName.startsWith("user_")) return "user";
if (tableName.startsWith("product_")) return "product";
return "common";
}
@Override
public boolean validate(List<String> warnings) {
return true;
}
}
生成 Lombok 注解的实体类
java
// src/main/java/com/example/app/infrastructure/persistence/mybatis/generator/LombokEntityGenerator.java
public class LombokEntityGenerator extends DefaultJavaModelGenerator {
@Override
public List<CompilationUnit> getCompilationUnits() {
List<CompilationUnit> units = super.getCompilationUnits();
units.stream()
.filter(unit -> unit instanceof TopLevelClass)
.map(unit -> (TopLevelClass) unit)
.forEach(this::addLombokAnnotations);
return units;
}
private void addLombokAnnotations(TopLevelClass topLevelClass) {
topLevelClass.addImportedType("lombok.Getter");
topLevelClass.addImportedType("lombok.Setter");
topLevelClass.addImportedType("lombok.NoArgsConstructor");
topLevelClass.addImportedType("lombok.AllArgsConstructor");
topLevelClass.addImportedType("lombok.Builder");
topLevelClass.addAnnotation("@Getter");
topLevelClass.addAnnotation("@Setter");
topLevelClass.addAnnotation("@NoArgsConstructor");
topLevelClass.addAnnotation("@AllArgsConstructor");
topLevelClass.addAnnotation("@Builder");
// 移除生成的getter/setter
topLevelClass.getMethods().clear();
}
}
最佳实践总结
-
分层清晰:
- 生成的 Dynamic SQL 代码放在
repository.dynamic
包 - 自定义 Repository 实现放在
repository
包 - 实体类放在
model.entity
包
- 生成的 Dynamic SQL 代码放在
-
接口隔离:
- 使用自定义 Repository 接口隔离业务代码与生成代码
- 业务层只依赖自定义 Repository 接口
-
动态查询:
- 利用 MyBatis Dynamic SQL 构建类型安全的动态查询
- 封装分页逻辑到工具类中
-
模块化支持:
- 通过自定义插件实现按模块生成代码
- 保持各模块代码的独立性
-
优化分页:
- 使用 DSL 构建器统一处理分页和排序
- 避免在业务层直接处理分页细节
-
对象转换:
- 使用 Converter 将实体转换为 DTO
- 保持业务层返回的是响应对象而非实体
-
类型安全:
- 利用 Dynamic SQL 的类型安全特性
- 减少 SQL 注入风险
-
性能优化:
- 按需查询字段,避免 SELECT *
- 使用 JOIN 替代 N+1 查询
生成代码示例
生成的实体类
java
// src/main/java/com/example/app/modules/user/model/entity/UserEntity.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserEntity {
private Long id;
private String username;
private String email;
private Integer status;
private Date createdAt;
private Date updatedAt;
}
生成的 Dynamic SQL 支持类
java
// src/main/java/com/example/app/modules/user/repository/dynamic/UserDynamicSqlSupport.java
public final class UserDynamicSqlSupport {
public static final UserTable user = new UserTable();
public static final SqlColumn<Long> id = user.id;
public static final SqlColumn<String> username = user.username;
public static final SqlColumn<String> email = user.email;
public static final SqlColumn<Integer> status = user.status;
public static final SqlColumn<Date> createdAt = user.createdAt;
public static final SqlColumn<Date> updatedAt = user.updatedAt;
public static final class UserTable extends SqlTable {
public final SqlColumn<Long> id = column("id", JDBCType.BIGINT);
public final SqlColumn<String> username = column("username", JDBCType.VARCHAR);
public final SqlColumn<String> email = column("email", JDBCType.VARCHAR);
public final SqlColumn<Integer> status = column("status", JDBCType.INTEGER);
public final SqlColumn<Date> createdAt = column("created_at", JDBCType.TIMESTAMP);
public final SqlColumn<Date> updatedAt = column("updated_at", JDBCType.TIMESTAMP);
public UserTable() {
super("user");
}
}
}
生成的 Mapper 接口
java
// src/main/java/com/example/app/modules/user/repository/dynamic/UserMapper.java
@Mapper
public interface UserMapper extends BaseMapper<UserEntity> {
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
long count(CountDSLCompleter completer);
@DeleteProvider(type = SqlProviderAdapter.class, method = "delete")
int delete(DeleteDSLCompleter completer);
@InsertProvider(type = SqlProviderAdapter.class, method = "insert")
int insert(UserEntity record);
// 其他基础方法...
}
执行生成命令
bash
mvn mybatis-generator:generate
项目启动配置
java
// src/main/java/com/example/app/Application.java
@SpringBootApplication
@MapperScan({
"com.example.app.modules.*.repository.dynamic",
"com.example.app.infrastructure.persistence.mybatis.dynamic.support"
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
这种设计完美适配了 MyBatis Dynamic SQL,同时保持了项目结构的清晰和一致性。它结合了自动生成代码的高效性和自定义代码的灵活性,特别适合需要复杂动态查询的中大型项目。
分页查询流程
最佳实践建议
- 严格分层:
-
Controller 调用 Service,Service 调用 Mapper。
-
避免跨层调用(如 Controller 直接调用 Mapper)。
- 使用 DTO 和 VO 分离模型:
-
数据库实体(Entity)只在数据访问层使用。
-
服务层接收 DTO,返回 VO 或 DTO。
-
避免直接暴露实体给前端。
- 模块独立性:
-
模块之间通过接口交互,避免直接依赖内部实现。
-
每个模块有自己的模型,避免跨模块使用模型(需要时通过转换)。
- 统一响应格式:
- 在
common.web
包中定义统一响应体(如Result<T>
)。
java
public class Result<T> {
private int code;
private String message;
private T data;
}
- 全局异常处理:
- 在
common.exception
包中实现@ControllerAdvice
全局异常处理器。
- 配置集中管理:
-
所有配置类放在
common.config
包中。 -
使用
@ConfigurationProperties
绑定配置参数。