代码语言

知识点思维导图

29 个知识节点

Java(13) - 常用注解详解

读完后,你应能完成以下任务:

  • 绘制“Java(13) - 常用注解详解 / 先建立心智模型:注解 ≈ 前端的装饰器 / 框架约定”的关键对象与数据流,解释“Java 注解就是这套思路。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Java(13) - 常用注解详解 / Web 层注解:把 HTTP 请求接进来”设计正常与异常输入,验证“这一类决定"哪个 URL 由哪个方法处理、参数怎么取"。”,输出首个偏差位置与回归测试结果。
  • 实现“Java(13) - 常用注解详解 / @RestController + @RequestMapping”的最小代码或配置,检验“【前端类比】等价于 NestJS 里 @Controller('organization') 给整个类挂一个路由前缀,并声明"我返回的是数据(JSON),不是页面"。”,输出命令、结果与 Diff,并说明不适用边界。

第 8 课我们认识了"注解是什么"(贴在代码上的标签 + 反射读取),这一课把日常开发中天天打交道的注解按"职责分类"系统过一遍——它们就是 Java 后端的"装饰器全家桶"。


一、先建立心智模型:注解 ≈ 前端的装饰器 / 框架约定

在 React/Vue 里,你早就习惯了"贴个标记,框架就帮你做事":

// Vue:贴个装饰器,框架就把它当组件/属性处理
@Component
export default class UserCard extends Vue {
  @Prop({ required: true }) userId!: number   // 标记:这是必填的 prop
}

// Angular / NestJS 更直接
@Controller('organization')
export class OrganizationController {
  @Get('getById')
  getById(@Query('id') id: number) { /* ... */ }
}

Java 注解就是这套思路。注解本身不干活,干活的是读取注解的框架(Spring、MyBatis、Lombok)。你贴标签,框架在启动或编译时用反射(见第 8 课)把标签翻译成真实行为。

前端概念 Java 注解 共同点
NestJS @Controller @RestController 标记一个类是 HTTP 入口
NestJS @Get('/x') @GetMapping("/x") 把方法绑到某个路由
Vue @Prop @RequestParam 声明一个外部传入的参数
class-validator @IsNotEmpty @NotBlank 声明校验规则
Vue @Component 注册 @Service / @Component 交给容器统一管理

下面按 5 大类 展开。

┌─────────────────────────────────────────────────────────┐
│  请求进来后注解的作用位置(对应第 4 课五站流程)          │
│                                                           │
│  HTTP ─▶ @RestController ─▶ @Service ─▶ @Mapper ─▶ MySQL │
│           @GetMapping        @Transactional               │
│           @RequestParam      @Autowired                   │
│           @RequestBody                                    │
│           @Valid 校验                                     │
│                                                           │
│  贯穿全程的 Lombok:@Data / @Slf4j / @Builder(编译期生效)│
└─────────────────────────────────────────────────────────┘

二、Web 层注解:把 HTTP 请求接进来

这一类决定"哪个 URL 由哪个方法处理、参数怎么取"。

2.1 @RestController + @RequestMapping

【前端类比】等价于 NestJS 里 @Controller('organization') 给整个类挂一个路由前缀,并声明"我返回的是数据(JSON),不是页面"。

demo 匿名化示例代码(demo-basic/.../biz/controller/OrganizationController.java):

@RestController                       // 标记:HTTP 入口类,方法返回值直接序列化成 JSON
@RequestMapping("/organization")           // 类级路由前缀,下面所有方法都带 /organization
public class OrganizationController extends BaseController {

    @Autowired                        // 由容器注入 Service,不用自己 new
    private OrganizationService organizationService;
}
  • @RestController = @Controller + @ResponseBody 的组合。@ResponseBody 的意思是"方法返回的对象自动转成 JSON 写回响应体"。没有它,Spring 会把返回值当成"视图名"去找页面模板(老式 MVC 套路,前后端分离项目用不到)。
  • @RequestMapping("/organization") 贴在上做前缀;也能贴在方法上,但方法级通常用更具体的 @GetMapping/@PostMapping

2.2 @GetMapping / @PostMapping

【前端类比】就是 router.get('/getById')router.post('/...')。它们是 @RequestMapping(method=GET) 的简写。

@GetMapping("/getById")               // 等价 @RequestMapping(value="/getById", method=GET)
public R<OrganizationOut> getOrganization(@RequestParam("id") int id) {
    return R.ok(organizationService.getOrganization(id));
}

完整 URL = 类前缀 + 方法路径 = /organization/getById,正是第 4 课用过的那个示例接口。

HTTP 方法 注解 典型用途
GET @GetMapping 查询,参数在 URL(?id=1
POST @PostMapping 新增/复杂查询,参数在请求体
PUT @PutMapping 更新
DELETE @DeleteMapping 删除

2.3 @RequestParam / @PathVariable / @RequestBody:三种取参方式

这是最容易混淆的一组,关键看"参数藏在请求的哪个位置"。

GET /organization/getById?id=123
                     └────┘ → @RequestParam("id")

GET /organization/123
            └─┘   → @PathVariable(路径里的一段)

POST /organization/listByGroup
Body: { "ids": [1,2,3] }   → @RequestBody(整个 JSON 反序列化成对象)

demo 匿名化示例代码对照(同一个 OrganizationController):

// ① @RequestParam:从 URL 查询串 ?id=xxx 取单个值
@GetMapping("/getById")
public R<OrganizationOut> getOrganization(@RequestParam("id") int id) {
    return R.ok(organizationService.getOrganization(id));
}

// ② 多个 @RequestParam:?groupId=1&name=xxx
@GetMapping("/getByName")
public R<OrganizationOut> getByName(@RequestParam("groupId") int groupId,
                               @RequestParam("name") String name) {
    return R.ok(organizationService.getByName(groupId, name));
}

// ③ @RequestBody:POST 请求体里的整个 JSON 映射成 OrganizationIn 对象
@PostMapping("/listByGroup")
public R<List<OrganizationOut>> listByGroup(@RequestBody OrganizationIn organizationIn) {
    return R.ok(organizationService.listByGroup(organizationIn));
}
注解 参数位置 前端发请求时怎么传 前端类比
@RequestParam URL 查询串 ?k=v axios.get('/x', { params: { id } }) Express req.query.id
@PathVariable URL 路径段 /x/{id} axios.get('/x/' + id) Express req.params.id
@RequestBody 请求体 JSON axios.post('/x', { ids }) Express req.body

@PathVariable 在 demo 这套接口里用得少(团队偏好 query/body),它长这样:

// 假想写法:路径里的 {id} 绑定到方法参数
@GetMapping("/organization/{id}")
public R<OrganizationOut> getById(@PathVariable("id") Integer id) { ... }

易错点:@RequestParam 默认是必填的,缺参数会直接 400。允许不传要写 @RequestParam(value="id", required=false),或给默认值 defaultValue="0"


三、容器层注解:把对象交给 Spring 管理

第 5 课讲过 @Autowired 注入。这里把"谁能被注入"的注解补齐。

【前端类比】想象一个全局的 DI 容器(类似 Pinia/Vuex 的 store 注册,或 NestJS 的 Provider 体系)。打上标记的类,Spring 启动时会扫描到、创建好实例(叫 Bean)、放进容器;需要用的地方再"注入"进去,你永远不用手动 new

启动扫描 ──▶ 发现 @Service/@Component 的类 ──▶ new 出单例 ──▶ 放进容器
                                                              │
@Autowired 字段 ◀── 容器按类型找到对应 Bean 塞进去 ◀──────────┘

3.1 @Service / @Component

demo 匿名化示例代码(OrganizationService.java):

@Slf4j
@Service("organizationService")            // 标记为业务层 Bean,括号里是自定义 Bean 名字
public class OrganizationService extends ServiceImpl<OrganizationMapper, Organization> {

    @Autowired
    private DriverOrganizationService driverOrganizationService;   // 注入另一个 Service

    @Autowired
    private RemotePermissionService remotePermissionService;  // 注入远程调用客户端
}

@Service@Component 功能上几乎一样,都是"把这个类注册进容器"。区别只是语义

注解 语义(贴在哪类对象上)
@Component 通用组件,没归到具体层时用
@Service 业务逻辑层(Service)
@Repository 数据访问层(DAO,会额外做异常转换)
@Controller / @RestController Web 层

它们底层都是 @Component 的"特化版本",分开命名纯粹是为了让代码可读性更强——一眼看出这个类属于哪一层。

3.2 @Autowired vs @Resource:两种注入

两者都能完成注入,核心区别是按什么找 Bean

@Autowired @Resource
出身 Spring 自带 Java 标准(JSR-250)
默认匹配方式 类型(byType) 名字(byName)
找不到唯一类型时 配合 @Qualifier("名字") 指定 直接用字段名当 Bean 名
// 按类型注入:容器里 OrganizationService 类型只有一个,直接塞
@Autowired
private OrganizationService organizationService;

// 当同一类型有多个实现、需要按名字精确指定时:
@Resource(name = "organizationService")
private OrganizationService organizationService;

实践建议:demo 项目里以 @Autowired 为主,够用且统一就好。只有"同一接口有多个实现 Bean,要指定其中一个"时,@Resource(name=...)@Autowired + @Qualifier 才派上用场。


四、数据层注解:连接数据库

4.1 @Mapper

【前端类比】Mapper 接口就像你只写了一份 API 的"类型声明(interface)",却没写实现——框架(MyBatis)在运行时用动态代理(反射,见第 8 课)自动帮你生成实现,去执行对应的 SQL。

demo 匿名化示例代码(OrganizationMapper.java):

// 继承 MyBatis-Plus 的 BaseMapper,自动获得增删改查;自定义方法配 XML 里的 SQL
public interface OrganizationMapper extends BaseMapper<Organization> {

    // @Param 给 SQL 里的占位符命名,XML 中用 #{organization} 引用
    IPage<Organization> getOrganizationPage(Page page, @Param("organization") Organization organization);

    Organization selectOrganizationForUpdate(@Param("id") Integer id);
}

注意这里没有写 @Mapper——因为 demo 在一个配置类(MybatisPlusConfigurer,贴了 @Configuration)上统一配了 @MapperScan,把整个包下的接口自动当成 Mapper 扫描,不必每个都贴。两种写法等价:

// 写法一:每个接口单独贴
@Mapper
public interface OrganizationMapper extends BaseMapper<Organization> { }

// 写法二(demo 用的):在配置类上一次性扫描整个包(可同时扫多个包)
@MapperScan(value = {"com.example.platform.basic.service.mapper",
                     "com.example.platform.common.web.mapper"})

@Param 则是给方法参数起一个"SQL 里能引用的名字"。多参数时必须加,否则 XML 里不知道 #{id} 对应哪个入参。

4.2 @Transactional:事务

【前端类比】前端没有直接对应物,但你可以类比"一组操作要么全成功、要么全回滚"——像 Promise 里"任何一步 reject 就整体失败",只不过这里失败时数据库会把已做的改动撤销

demo 匿名化示例代码(OrganizationService.java,更新网点余额):

/**
 * 增量更新网点与收支方式余额
 * @param updateReq 余额变更请求
 */
@Transactional(rollbackFor = Exception.class, timeout = 6)  // 抛任何异常都回滚,最长 6 秒
public void updateBalanceWithLock(IncrementUpdateSettleRemainderIn updateReq) {
    // select for update:悲观锁锁住这一行,防止并发改余额时互相覆盖
    Organization organization = baseMapper.selectOrganizationForUpdate(updateReq.getOrganizationId());
    // ... 后续多次更新,要么全部成功提交,要么任一步出错全部回滚
}

两个关键参数解释 WHY:

  • rollbackFor = Exception.class这是必须写的。Spring 默认只在遇到 RuntimeException 才回滚,遇到受检异常(Checked Exception)不回滚。显式写上 Exception.class 才能保证"任何异常都回滚",避免钱算错了却没回滚的事故。
  • timeout = 6:事务最长 6 秒,超时自动回滚,防止长事务一直占着数据库行锁拖垮系统。

坑提醒:@Transactional 基于动态代理,同类内部方法自己调自己(this.xxx)不会生效;而且方法必须是 public。这两点是新手最常踩的坑。


五、Lombok 注解:消灭模板代码(编译期生效)

第 5、8 课提过 @Data。Lombok 和前面几类不同——它在编译期就把代码"生成"出来(你看不到,但 class 文件里真有),而不是运行时靠反射。

【前端类比】很像 TypeScript 的语法糖或编译期宏:你写得少,编译后展开成完整代码。

5.1 @Data

demo 匿名化示例代码(FlowFinishVo.java):

@Data                    // 一行顶一堆:自动生成 getter/setter/toString/equals/hashCode
public class FlowFinishVo {
    private Integer applyId;
    private Integer applyState;
    private Map<String, Object> extInfo;
}

@Data 一个注解 = 下面这一大坨的总和:

Lombok 注解 自动生成的内容 对应 JS
@Getter / @Setter 所有字段的 get/set 方法 TS 的属性访问
@ToString toString() JSON.stringify 的感觉
@EqualsAndHashCode equals() / hashCode() 对象内容比较
@Data 以上全部打包 ——

如果你手动实现,一个有 3 个字段的类要写几十行 getter/setter,Lombok 让你只写字段声明。

5.2 @Slf4j:日志

@Slf4j                   // 自动生成一个名为 log 的日志对象,直接用 log.info(...)
@Service("organizationService")
public class OrganizationService extends ServiceImpl<OrganizationMapper, Organization> {

    public void doSomething() {
        log.info("网点查询入参 id={}", id);   // {} 占位符,类似 console.log 但更规范
        log.error("更新余额失败", e);          // 第二个参数是异常,会打完整堆栈
    }
}

【前端类比】log 就是个加强版 console,但 {} 占位、按级别(info/warn/error)输出、能落盘到日志文件。没有 @Slf4j 你得手写 private static final Logger log = LoggerFactory.getLogger(...) 那一长串。

5.3 @Builder:链式构造

demo 匿名化示例代码(OilSegmentFactor.java,四段油耗计算因子):

@Data
@Builder                 // 生成建造者,支持链式 .xxx().build() 创建对象
@NoArgsConstructor       // 生成无参构造(很多框架反序列化需要)
@AllArgsConstructor      // 生成全参构造(@Builder 依赖它)
public class OilSegmentFactor {
    private TransRoadEnum transRoadEnum;       // 道路类型
    private TransKnapsackEnum transKnapsackEnum;  // 空重驶类型
}

有了 @Builder,创建对象时就能像写对象字面量一样清晰:

// 链式:哪个字段是什么值一目了然,参数多时远比构造函数可读
OilSegmentFactor factor = OilSegmentFactor.builder()
        .transRoadEnum(TransRoadEnum.HIGHWAY)
        .transKnapsackEnum(TransKnapsackEnum.HEAVY)
        .build();

【前端类比】几乎就是 JS 里直接写 const factor = { transRoadEnum: 'HIGHWAY', transKnapsackEnum: 'HEAVY' }——@Builder 把 Java 啰嗦的构造过程变得像写对象字面量一样直观。

小知识:@Builder 依赖全参构造,所以通常和 @NoArgsConstructor + @AllArgsConstructor 一起出现(这也是上面那段匿名化示例代码同时贴了 4 个注解的原因)。


六、校验注解:参数进门先体检

【前端类比】完全对应 class-validator(NestJS 常用)或表单库的规则声明——在数据进入业务逻辑前,先按规则校验,不合格直接打回。

6.1 在 DTO 字段上声明规则

demo 匿名化示例代码(FlowFinishVo.java)里就贴了校验注解。但这里要先指出一个常见的坑

@Data
public class FlowFinishVo {
    @NotBlank(message = "applyId不能为空!")   // ⚠️ 用错了:@NotBlank 只能校验字符串
    private Integer applyId;                  // 字段是 Integer,不是 String
}

@NotBlank 只支持 String/CharSequence。贴在 Integer 上,一旦真正触发校验会抛 UnexpectedTypeException(找不到对应的校验器)。这段是线上代码里的一处历史遗留写法,正确的写法应该用 @NotNull

@Data
public class FlowFinishVo {
    @NotNull(message = "applyId不能为空!")    // 数字/对象判空用 @NotNull
    private Integer applyId;
}

记住选注解的口诀:字符串用 @NotBlank,集合用 @NotEmpty,数字和其它对象用 @NotNull

常用校验注解:

注解 作用 适用类型 class-validator 对应
@NotNull 不能为 null 任意 @IsDefined
@NotBlank 不能为 null/空串/纯空格 String @IsNotEmpty
@NotEmpty 不能为 null/空集合 String/集合 @ArrayNotEmpty
@Min / @Max 数值范围 数字 @Min / @Max
@Size 长度/元素个数范围 String/集合 @Length
@Pattern 正则匹配 String @Matches

6.2 在 Controller 入参上用 @Valid 触发校验

只声明规则还不够,必须在接收参数处加 @Valid(或 Spring 的 @Validated)来"扳动开关",框架才会真正执行校验:

// @Valid 告诉框架:进方法前先按 FlowFinishVo 里的注解逐条校验
@PostMapping("/flow/finish")
public R<Boolean> finish(@Valid @RequestBody FlowFinishVo vo) {
    // 能走到这里,说明 applyId 已经通过 @NotNull 校验,不必再手动判空
    return R.ok(service.finish(vo));
}

校验流程:

请求 JSON ──▶ @RequestBody 反序列化成 FlowFinishVo
                        │
                   @Valid 触发校验
                        │
          ┌─────────────┴─────────────┐
       通过                          不通过
        │                              │
   进入方法体                抛 MethodArgumentNotValidException
                            (配合全局异常处理,见第 7 课,
                             统一返回 message 给前端)

@Valid(Java 标准)和 @Validated(Spring 扩展)功能高度重叠,OrganizationController 里两个都 import 了。日常用 @Valid 即可;@Validated 多用于需要"分组校验"的进阶场景。


七、本课小结

  • Web 层@RestController+@RequestMapping 标记入口类,@GetMapping/@PostMapping 绑路由;取参三件套——@RequestParam(URL 查询串)、@PathVariable(路径段)、@RequestBody(请求体 JSON)。
  • 容器层@Service/@Component/@Repository/@Controller 把对象注册进 Spring 容器(语义不同,本质都是 @Component);@Autowired(按类型)和 @Resource(按名字)负责注入。
  • 数据层@Mapper(或启动类 @MapperScan 批量扫描)让接口变成可执行 SQL 的代理;@Transactional 管事务,记牢 rollbackFor=Exception.class 和"自调用不生效、必须 public"两个坑。
  • Lombok@Data(getter/setter 全家桶)、@Slf4j(自动 log 对象)、@Builder(链式构造),编译期生成代码,消灭模板。
  • 校验:在 DTO 字段贴 @NotNull/@NotBlank 等规则,再在 Controller 入参加 @Valid 才真正触发,配合第 7 课的全局异常处理统一返回错误信息。
  • 核心心智:注解只是标签,真正干活的是读它的框架(Spring 运行时反射 / Lombok 编译期生成)。

下一课预告:第 14 课进入 Spring Boot 的"骨架"——从 @SpringBootApplication 启动类讲起,看一个 Spring Boot 应用是怎么自动装配、把上面这些注解串成一个能跑起来的服务的。

八、总结

  • 先建立心智模型:注解 ≈ 前端的装饰器 / 框架约定:Java 注解就是这套思路。
  • Web 层注解:把 HTTP 请求接进来:这一类决定"哪个 URL 由哪个方法处理、参数怎么取"。
  • 容器层注解:把对象交给 Spring 管理:@Service 和 @Component 功能上几乎一样,都是"把这个类注册进容器"。
  • 数据层注解:连接数据库:注意这里没有写 @Mapper——因为 demo 在一个配置类(MybatisPlusConfigurer,贴了 @Configuration)上统一配了 @MapperScan,把整个包下的接口自动当成 Mapper 扫描,不必每个都贴。
  • Lombok 注解:消灭模板代码(编译期生效):Lombok 和前面几类不同——它在编译期就把代码"生成"出来(你看不到,但 class 文件里真有),而不是运行时靠反射。
  • 校验注解:参数进门先体检:但这里要先指出一个常见的坑:

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“常用注解详解”中,需要同时满足“先建立心智模型:注解 ≈ 前端的装饰器 / 框架约定”与“Web 层注解:把 HTTP 请求接进来”。给定正文约束“在 React/Vue 里,你早就习惯了"贴个标记,框架就帮你做事"。”,哪些判断保持了原有处理机制?多选
2“常用注解详解”出现偏差:“在“常用注解详解 / @RestController + @RequestMappin”中,即使不满足“【前端类比】等价于 NestJS 里 @Controller('organization') 给整个类挂一个路由前缀,并声明"我返回的是数据(JSON),不是页面"”,结果与副”已成为实际行为。围绕“@RestController + @RequestMapping”与“@GetMapping / @PostMapping”,哪些判断能定位被改变的职责或边界?多选
3评审“常用注解详解”方案时,验收条件包含“这是最容易混淆的一组,关键看"参数藏在请求的哪个位置"。”。关于“@RequestParam / @PathVariable / @RequestBody:三种取参方式”与“容器层注解:把对象交给 Spring 管理”的哪些决策符合正文机制?多选