Springboot开发规范
分层、Bean、配置、MyBatis/JPA
- [Requirement] 采用标准四层架构:
controller(接口层)→service(业务层)→mapper/repository(数据访问层)→entity(实体层),禁止跨层调用。 - [Requirement] Controller 只做参数校验、调用 Service、组装响应,禁止包含业务逻辑与数据库操作。
- [Requirement] Service 层承载业务逻辑,事务边界在 Service 层;禁止在 Controller 或 Mapper 上加事务。
- [Standard] DTO 与 DO 分离:Controller 入参用
XxxRequest、出参用XxxResponse;Service 与 Mapper 之间用Entity/DO;禁止 Entity 直接出参。 - [Standard] 包结构按业务模块垂直拆分,而非按技术层水平拆分:
com.company.user、com.company.order,每个模块内含 controller/service/mapper/entity。 - [Default] 公共工具、常量、异常放
common包,跨模块复用的抽象放core包。 - [Caution] 禁止循环依赖:A 依赖 B,B 又依赖 A;通过抽取公共接口或中间层解决。
com.company.project
├── common/ # 通用工具、常量、异常
│ ├── Result.java
│ ├── BizException.java
│ └── Constants.java
├── user/ # 用户模块(垂直拆分)
│ ├── controller/
│ │ └── UserController.java
│ ├── service/
│ │ ├── UserService.java
│ │ └── impl/UserServiceImpl.java
│ ├── mapper/
│ │ └── UserMapper.java
│ ├── entity/
│ │ └── UserDO.java
│ └── dto/
│ ├── UserRequest.java
│ └── UserResponse.java
└── order/ # 订单模块
└── ...// Controller:只做参数校验与响应组装
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
public Result<UserResponse> create(@Valid @RequestBody UserRequest request) {
UserResponse resp = userService.create(request);
return Result.success(resp);
}
}// 反模式:Controller 写业务逻辑 + 直接操作数据库
@RestController
public class UserController {
@Autowired
private UserMapper userMapper; // 直接注入 Mapper
@PostMapping("/users")
public Result create(@RequestBody UserRequest req) {
if (req.getName() == null) { // 业务校验写在 Controller
return Result.fail("name is required");
}
UserDO user = new UserDO();
user.setName(req.getName());
userMapper.insert(user); // 直接操作 DB,跳过 Service
return Result.success();
}
}- [Requirement] 依赖注入使用构造器注入(推荐
@RequiredArgsConstructor+final字段),禁止字段注入(@Autowired直接标注字段)。 - [Requirement] Bean 作用域默认单例,禁止使用
@Scope("prototype")解决线程安全问题,应通过局部变量或 ThreadLocal。 - [Standard]
@Configuration类中用@Bean声明第三方组件,方法名即 Bean 名,返回类型用接口。 - [Standard] 条件装配用
@ConditionalOnProperty、@ConditionalOnMissingBean,避免硬编码if/else。 - [Standard] 启动类
@SpringBootApplication的scanBasePackages显式指定根包,避免扫描范围不确定。 - [Default]
@Component、@Service、@Repository、@Controller注解职责清晰,不要混用;Service 用@Service,数据访问用@Repository。 - [Caution] 禁止在 Bean 的构造器或
@PostConstruct中执行耗时操作(远程调用、大量初始化),会拖慢启动。
// 构造器注入(推荐)
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final OrderService orderService;
@Override
public UserResponse create(UserRequest request) {
// ...
}
}
// 条件装配
@Configuration
public class CacheConfig {
@Bean
@ConditionalOnProperty(prefix = "cache", name = "type", havingValue = "redis")
public CacheService redisCacheService(RedisTemplate<String, Object> redisTemplate) {
return new RedisCacheService(redisTemplate);
}
@Bean
@ConditionalOnMissingBean(CacheService.class)
public CacheService defaultCacheService() {
return new LocalCacheService();
}
}// 反模式:字段注入,不利于测试与不可变性
@Service
public class UserServiceImpl {
@Autowired
private UserMapper userMapper; // 字段注入
@Autowired
private OrderService orderService;
}
// 反模式:用 prototype 解决线程安全
@Service
@Scope("prototype") // 错误做法,应使用局部变量
public class OrderService {
private int count; // 共享可变状态
}- [Requirement] 敏感信息(数据库密码、密钥、token)禁止明文写在配置文件中,必须通过环境变量、配置中心或加密(如 jasypt)注入。
- [Requirement] 配置按环境拆分:
application.yml(公共)、application-dev.yml、application-test.yml、application-prod.yml,通过spring.profiles.active激活。 - [Standard] 自定义配置用
@ConfigurationProperties(prefix = "xxx")绑定到 POJO,禁止到处@Value("${xxx}")散落读取。 - [Standard] 配置项命名用小写短横线(kebab-case):
user-service.timeout,禁止驼峰。 - [Standard] 启动时校验必填配置:
@ConfigurationProperties类加@Validated,字段用@NotNull、@Min等约束。 - [Default] 数据库连接池、线程池等资源参数必须显式配置,禁止使用框架默认值(如 HikariCP 默认最大连接数 10)。
- [Caution] 禁止在
application.yml中硬编码环境相关的 IP/域名,应使用配置中心或环境变量占位。
# application.yml
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
application:
name: user-service
user-service:
timeout: 3000
max-retry: 3
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:3306/user_db
username: ${DB_USER:root}
password: ${DB_PASSWORD:} # 从环境变量注入,禁止明文
hikari:
maximum-pool-size: 20
minimum-idle: 5// 配置绑定 POJO
@Data
@Validated
@ConfigurationProperties(prefix = "user-service")
public class UserServiceProperties {
@Min(100)
private int timeout = 3000;
@Min(1)
private int maxRetry = 3;
}
// 启用
@SpringBootApplication
@EnableConfigurationProperties(UserServiceProperties.class)
public class Application { }# 反模式:明文密码
spring:
datasource:
password: root123 # 敏感信息明文// 反模式:到处 @Value 散落
@Service
public class UserService {
@Value("${user-service.timeout}")
private int timeout;
@Value("${user-service.max-retry}")
private int maxRetry;
// 配置分散在多个类,难以统一管理
}- [Requirement] 禁止在 Service 中写 SQL,SQL 统一放 Mapper(XML 或注解);MyBatis 优先用 XML 写复杂 SQL,简单 CRUD 可用注解。
- [Requirement] 禁止
select *,必须明确字段列表;禁止在 XML 中拼接 SQL 字符串,必须用#{}传参,禁止${}(除表名/排序字段等无法参数化场景,且必须白名单校验)。 - [Requirement] 分页必须用分页插件(PageHelper 或 MyBatis-Plus),禁止手动
limit;大表分页禁止深翻页(offset过大),用游标分页。 - [Standard] Mapper 接口方法名与 SQL id 一一对应,命名清晰:
selectById、selectByCondition、insert、updateById、deleteById。 - [Standard] 实体类(DO)字段与数据库列名映射用驼峰自动转换(
mybatis.configuration.map-underscore-to-camel-case: true),不要在 XML 中写冗余 ResultMap。 - [Standard] 事务用
@Transactional,必须指定rollbackFor = Exception.class(默认只回滚 RuntimeException);事务方法必须 public,且不能被同类内部调用(代理失效)。 - [Default] 读写分离场景,读方法标注
@Transactional(readOnly = true)。 - [Caution]
@Transactional方法内禁止调用远程服务或消息发送,避免长事务;消息发送用事务消息或本地消息表保证最终一致性。
// Mapper 接口
@Mapper
public interface UserMapper {
UserDO selectById(@Param("id") Long id);
List<UserDO> selectByCondition(@Param("status") Integer status);
int insert(UserDO user);
int updateById(UserDO user);
}<!-- UserMapper.xml -->
<select id="selectById" resultType="com.company.user.entity.UserDO">
SELECT id, name, email, status, created_at
FROM t_user
WHERE id = #{id}
AND deleted = 0
</select>
<select id="selectByCondition" resultType="com.company.user.entity.UserDO">
SELECT id, name, email, status
FROM t_user
WHERE deleted = 0
<if test="status != null">
AND status = #{status}
</if>
ORDER BY id DESC
</select>// Service 事务
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderRequest request) {
OrderDO order = new OrderDO();
// ... 组装
orderMapper.insert(order);
// 扣库存等其他操作,同一事务
}
}<!-- 反模式:select * + ${} 拼接 -->
<select id="find" resultType="User">
SELECT * FROM t_user WHERE name = '${name}' <!-- SQL 注入风险 -->
</select>// 反模式:同类内部调用导致事务失效
@Service
public class OrderServiceImpl {
public void create() {
this.doCreate(); // 内部调用,@Transactional 不生效
}
@Transactional(rollbackFor = Exception.class)
public void doCreate() { ... }
}
// 反模式:事务内远程调用
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
orderMapper.insert(order);
inventoryClient.deduct(); // 远程调用,长事务风险
}- [Requirement] 单元测试使用 JUnit 5 + Mockito,Service 层测试必须 Mock 掉 Mapper 与外部依赖,禁止连真实数据库。
- [Requirement] 测试类命名
XxxTest,测试方法命名方法名_场景_预期结果,如getUserById_notFound_throwException。 - [Standard] 使用
@SpringBootTest做集成测试时必须指定classes与webEnvironment,避免加载全部上下文拖慢速度。 - [Standard] Controller 层测试用
@WebMvcTest+MockMvc,只加载 Web 层,Service 用@MockBean替换。 - [Standard] 断言用 AssertJ 的
assertThat,可读性优于 JUnit 原生assertEquals。 - [Default] 数据库集成测试用 Testcontainers 启动真实数据库,禁止用 H2 替代(H2 与生产 DB 行为不一致)。
- [Caution] 测试禁止依赖外部网络、文件系统绝对路径、系统时间;时间相关用
Clock注入或 Mock。
// Service 单元测试
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@Mock
private UserMapper userMapper;
@InjectMocks
private UserServiceImpl userService;
@Test
void getUserById_notFound_throwException() {
// given
Long userId = 999L;
when(userMapper.selectById(userId)).thenReturn(null);
// when & then
assertThatThrownBy(() -> userService.getUserById(userId))
.isInstanceOf(BizException.class)
.hasMessage("用户不存在");
}
@Test
void getUserById_success_returnUser() {
// given
UserDO user = new UserDO();
user.setId(1L);
user.setName("Alice");
when(userMapper.selectById(1L)).thenReturn(user);
// when
UserResponse resp = userService.getUserById(1L);
// then
assertThat(resp.getName()).isEqualTo("Alice");
verify(userMapper, times(1)).selectById(1L);
}
}// Controller 测试
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void createUser_validRequest_return200() throws Exception {
UserRequest request = new UserRequest();
request.setName("Alice");
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(request)))
.andExpect(status().isOk());
}
}// 反模式:单元测试连真实数据库
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService; // 加载真实 Mapper,连真实 DB
@Test
void test() {
userService.getUserById(1L); // 依赖数据库数据,不稳定
}
}
// 反模式:测试方法无意义命名
@Test
void test1() { ... } // 无法从名字看出测试意图