一分包策略:一个业务一个包

按业务垂直分包,不做形式化分层冗余——没有多个实现类的 Service 不建 impl 这类形式主义的冗余实现;每个固定分层包下可按业务建子包(controller.redbag、service.redbag…):

// 固定分包树(一个包中可包含多个相同业务类)
{root}                 根路径:SpringBoot 中放 Application 启动类
{root}.aspect          切面:业务统一拦截处理
{root}.config          配置:服务配置定义
{root}.constant        常量:业务常量定义
{root}.controller      MVC 控制层:接口映射与参数处理
{root}.service         MVC 服务层:业务逻辑处理
{root}.mapper          MVC 数据持久层:数据库操作
{root}.pojo            POJO 领域对象分组包(entity/dto/ipo/vo 的父包)
{root}.pojo.entity     Entity 数据库实体对象:数据库一对一实体映射封装
{root}.pojo.dto        DTO 数据传输对象:多表联查实体封装与业务对象封装
{root}.pojo.ipo        IPO 接口入参对象:接口请求参数封装与校验
{root}.pojo.vo         VO 业务视图对象:接口返回参数封装
{root}.util            工具类
HTTP 客户端(Forest)同样按安全域分包:forest.open 调用互联网公开接口、forest.auth 调用认证接口、forest.lan 调用局域网内部接口、forest.paas 调用平台方接口、forest.conf 拦截器配置——调用哪个安全域,包路径即声明。

二方法命名:前缀即操作语义

Service / Mapper 层方法名前缀与 URL 接口方法名前缀保持一致,读名知操作:

操作前缀示例
获取单个对象getgetById、getByPhone
获取多个对象listlistByStatus
分页查询pagepageByCondition
统计countcountByType
插入insertinsertBatch
删除deletedeleteById
修改updateupdateStatus
动作类(登录 / 注册 / 上传等)无前缀login、register、upload
正例(真实代码——方法名与端点动作对齐,一个端点一件事)
// 反馈与认领工单服务:认领 / 释放 / 完成 / 删除各一个方法,名字即业务动作
public TaskSummaryVO claim(TaskClaimIPO ipo) { ... }
public TaskSummaryVO release(TaskReleaseIPO ipo) { ... }
public TaskSummaryVO resolve(TaskResolveIPO ipo) { ... }
public List<TaskMineVO> listMine(String reporterId) { ... }

三POJO 四型:类型即边界

类型全称用途
Entity数据对象与数据库表结构一一对应,通过 Mapper 层向上传输
DTOData Transfer ObjectService 向外传输的对象:第三方接口固定返回对象、多表联查对象、特殊业务传输对象
IPOInterface Param Object接口入参对象,可实现对参数的多层次嵌套封装与 @Valid 校验
VOView Object业务视图对象,Web 向前端传输的响应参数封装
【强制】POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO
【推荐】出参按视图拆分,字段收敛靠类型结构保证

同一个任务的列表 / 详情 / 回执是三个不同的出参类型——列表不带材料全文、回执不含联系方式,「返回什么」在 VO 类型层收敛,不靠调用方自觉。

正例(真实代码)
public TaskSummaryVO insert(TaskInsertIPO ipo) { ... }      // 列表条目:摘要字段
public TaskDetailVO getDetail(String taskId) { ... }        // 详情:材料全文 + 签名图片地址
public List<TaskMineVO> mine(String reporterId) { ... }    // 回执:状态摘要 + 本人自述

四框架编码铁律

4.1【强制】Servlet Filter 直接使用方法参数,禁止 ThreadLocal 取请求

doFilterInternal(request, response, chain) 已提供参数必须直接使用——通过 RequestContextHolder ThreadLocal 获取 request/response 时,RequestContextFilter 的执行顺序不保证早于自定义 Filter,ThreadLocal 未就绪会导致 NPE。写回响应用 Result.response(nativeResponse) 带参版本,禁止无参版本(内部走 ThreadLocal,可能返回 null)。

4.2【强制】@ConditionalOnMissingBean 必须显式指定类型参数

裸用无参版本时 Spring Boot 检测方法返回类型的 raw class(泛型擦除后),对 FilterRegistrationBean<X> 等泛型 Bean 会退化为检测 FilterRegistrationBean——容器中存在任意同类 Bean 就跳过目标创建(静默失效)。意图是「配置开关控制 Bean 创建」时优先 @ConditionalOnProperty。排查「Bean 静默未创建」第一步看 ConditionEvaluationReport(启动参数 --debug)。

反例
@Bean
@ConditionalOnMissingBean                        // 泛型擦除后检测 raw class,同类 Bean 存在即静默跳过
public FilterRegistrationBean<XxxFilter> xxxFilter() { ... }
正例
@Bean
@ConditionalOnMissingBean(XxxFilter.class)      // 显式类型,精确判定
public FilterRegistrationBean<XxxFilter> xxxFilter() { ... }
4.3【强制】可选依赖使用双重可选机制

自动配置引用非强制依赖的 Bean 时,必须 @ConditionalOnBean(X.class) + @Autowired(required = false) 双保险。@Resource 是强制注入(JSR-250 不支持 required=false),对可选依赖必须改用 @Autowired(required = false),否则依赖缺失时容器启动失败。

4.4【强制】请求体可重复读 Wrapper 必须对 multipart 感知

构造 HttpServletRequestWrapper 缓存 body 字节实现「请求体可重复读」时,body 缓存与 getParts() 参数解析互斥(二者都独占容器底层流的唯一一次可读性;Tomcat 的 multipart 解析不走 Wrapper 覆盖的 getInputStream())。必须按 content-type 感知处理、默认跳过 multipart 包装,无差别包装会导致参数读到空 body 或文件上传损坏两类静默错误。

五规约扫描与把关记录

规约不是「自认为遵守」——以《阿里巴巴 Java 开发规约》官方规则集(P3C,PMD 引擎)对本服务全部 31 个源文件做静态扫描,扫描纳入交付流程:

把关环节实际动作样例
扫描P3C 官方 10 册规则全量扫描31 个源文件 → 65 条发现(P1×8 / P2×7 / P3×50)
分诊逐条定性:真实改进 / 团队口径 / 框架约定豁免27 条 @author 缺失 → 结对开发无个人作者概念,按团队口径处理;6 条 xxxIPO 命名 → 基础框架既定后缀,登记豁免
修复真实改进项进待办闭环:提常量 / 补分支 / 改顺序魔法值 → 提取具名常量(容量上限、列表上限均已落码);switch 缺 default → 显式抛错
分诊本身见判断力:不盲从规约——框架约定的 IPO 后缀登记豁免而非改名,「规约本意是可追责、Git 已承载」的 @author 不为合规而合规。最终符合的是工程理性,不是表格里的分数。