知识点思维导图
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 文件里真有),而不是运行时靠反射。
- 校验注解:参数进门先体检:但这里要先指出一个常见的坑:
学完自测
选择所有正确答案;提交后逐项核对判断依据。