跳转到内容

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() { ... }   // 无法从名字看出测试意图