Controller中常见的一些注解
类级别注解
@RestController
@RestController
是 Spring MVC 中的复合注解,由 @Controller
和 @ResponseBody
组合而成,主要作用是:
- 标记当前类为 控制器(Controller),用于接收并处理 HTTP 请求。
- 自动为类中所有方法添加
@ResponseBody
效果,即方法的返回值会直接转换为 JSON(或其他指定格式)响应体,而非通过视图解析器跳转页面。
实际开发与应用:
- 场景适配:专为 RESTful API 开发设计。在前后端分离架构中,后端只需返回数据(如 JSON),无需处理页面渲染,
@RestController
省去了在每个方法上单独添加@ResponseBody
的冗余代码。例如代码中的CdmYJcdDhryController
是 “cdm 到会人员接口” 控制器,所有方法(如batchMerge
、listByJcdbh
)返回ResponseResult
对象,会被自动序列化为 JSON 响应给前端,符合 API 交互需求。 - 与
@Controller
的区别:若使用@Controller
,则需要在每个方法上添加@ResponseBody
才能返回 JSON,否则 Spring 会默认寻找视图(如 JSP),这在 API 开发中显然多余。因此,@RestController
是 API 开发的首选。 - 注意事项:若类中某几个方法需要返回视图(非 JSON),则不应使用
@RestController
,而应单独用@Controller
+ 方法级@ResponseBody
组合。
@RequiredArgsConstructor
使用@RequiredArgsConstructor
进行构造方法依赖注入
@RequiredArgsConstructor
是 Lombok 提供的注解,作用是:
- 自动为类中所有 被
final
修饰的成员变量 生成一个构造函数(非final
变量不会被包含)。 - 生成的构造函数参数顺序与类中成员变量的声明顺序一致。
实际开发与应用:
- 简化依赖注入代码:在 Spring 中,依赖注入(DI)的常用方式有
@Autowired
字段注入、构造函数注入等。其中,构造函数注入 是推荐的最佳实践(避免循环依赖、确保依赖不可变),但手动编写构造函数会产生大量模板代码。
private final ICdmYJcdDhryService cdmYJcdDhryService;
private final CdmYJcdDhryEsService cdmYJcdDhryEsService;
private final HttpServletRequest request;
- 这些
final
修饰的依赖,通过@RequiredArgsConstructor
会自动生成包含它们的构造函数,Spring 可通过该构造函数完成注入,无需再写@Autowired
。 - 强制依赖不可变:被
final
修饰的变量必须在初始化时赋值(通过构造函数),且后续不可修改,避免了依赖被意外篡改的风险,增强代码安全性。 - 便于测试:在单元测试中,可通过构造函数直接传入 mock 对象(如 Mockito 模拟的
ICdmYJcdDhryService
),无需依赖 Spring 容器,测试更灵活。 - 注意事项:
- 若类中有非
final
变量需要注入,@RequiredArgsConstructor
不会为其生成构造函数参数,此时需手动添加@Autowired
(但建议尽量使用final
修饰依赖)。 - 必须确保项目引入了 Lombok 依赖,否则会报 “找不到构造函数” 的错误。
- 若类中有非
不使用@RequiredArgsConstructor
进行构造方法依赖注入
如果不使用@RequiredArgsConstructor
,使用构造方法注入的时候就需要写构造方法了:
public class YourController {// 保留 final,明确依赖不可变private final ICdmYJcdDhryService cdmYJcdDhryService;private final CdmYJcdDhryEsService cdmYJcdDhryEsService;private final HttpServletRequest request;// 构造函数注入public YourController(ICdmYJcdDhryService cdmYJcdDhryService, CdmYJcdDhryEsService cdmYJcdDhryEsService, HttpServletRequest request) {this.cdmYJcdDhryService = cdmYJcdDhryService;this.cdmYJcdDhryEsService = cdmYJcdDhryEsService;this.request = request;}// 其他方法...
}
可以去掉 final
修饰符,Spring 依然能通过构造函数完成依赖注入。
为什么可以去掉 final
?
final
修饰符的核心作用是强制变量必须初始化且后续不可修改,它与 “构造函数注入能否生效” 本身没有直接关联。
- 即使去掉
final
,只要手动编写了包含这些变量的构造函数,Spring 依然会通过该构造函数注入依赖(因为构造函数注入的本质是 “通过参数传递依赖并赋值给成员变量”,与变量是否被final
修饰无关)。
建议保留 final
,原因如下:
- 保证依赖不可变:依赖注入的核心思想之一是 “依赖在对象创建时就确定,且生命周期内不应被修改”。
final
能强制这一点,避免后续代码中被意外赋值(比如误写this.cdmYJcdDhryService = null;
之类的错误)。 - 明确依赖的必要性:
final
变量必须在构造函数中初始化,这能直观告诉其他开发者:“这些依赖是对象创建的必要条件,缺一不可”。 - 符合最佳实践:Spring 官方推荐构造函数注入时使用
final
修饰依赖,这是行业普遍遵循的规范,能减少潜在 bug。
理论上也可以使用lombok
的@AllArgsConstructor
理论上可以使用 @AllArgsConstructor
替代 @RequiredArgsConstructor
实现依赖注入,但两者存在关键区别,需要根据场景选择,通常不推荐用 @AllArgsConstructor
专门处理依赖注入 ,原因如下:
1. 两个注解的核心区别
@RequiredArgsConstructor
:只为被final
修饰的成员变量生成构造函数(非final
变量不包含)。@AllArgsConstructor
:为所有成员变量(无论是否被final
修饰)生成构造函数,参数顺序与变量声明顺序一致。
2. 用 @AllArgsConstructor
进行依赖注入的问题
假设类中除了需要注入的依赖,还有其他非依赖的成员变量(如本地配置、临时变量等):
@AllArgsConstructor // 为所有变量生成构造函数
public class YourController {// 需要注入的依赖(通常建议用 final)private final ICdmYJcdDhryService cdmService;// 非依赖的本地变量(不需要注入)private String appName; private int maxRetryCount;
}
此时 @AllArgsConstructor
会生成包含 **cdmService
、appName
、maxRetryCount**
三个参数的构造函数。这会导致:
- 注入冗余:Spring 会强制要求在注入时传入
appName
、maxRetryCount
等非依赖变量(否则报错),但这些变量可能是本地配置,不需要从容器中注入。 - 语义混淆:构造函数参数包含了“需要注入的依赖”和“本地变量”,其他开发者无法直观区分哪些是容器管理的依赖,降低代码可读性。
3. 什么时候 @AllArgsConstructor
可行?
只有当类中所有成员变量都是需要注入的依赖(且无需区分是否为 final
)时,@AllArgsConstructor
才能安全替代 @RequiredArgsConstructor
,例如:
@AllArgsConstructor // 此时所有变量都是依赖,无冗余参数
public class YourController {private ICdmYJcdDhryService cdmService; // 即使没加 finalprivate CdmEsService esService;private HttpServletRequest request;
}
这种情况下,@AllArgsConstructor
生成的构造函数会包含所有依赖,Spring 可以正常注入,效果上等同于手动编写包含所有变量的构造函数。
4. 为什么更推荐 @RequiredArgsConstructor
?
- 精准性:只针对
final
变量生成构造函数,而final
通常用于标记“必须注入的核心依赖”,符合依赖注入的语义(依赖不可变、创建时必须初始化)。 - 避免冗余:过滤掉非
final
的本地变量,不会强制要求注入无关参数。 - 符合****最佳实践:配合
final
使用,明确区分“依赖”和“本地变量”,代码意图更清晰。
总结
- 可以用
@AllArgsConstructor
进行依赖注入,但仅适用于类中所有成员变量都是需要注入的依赖的场景。 - 绝大多数情况下,
@RequiredArgsConstructor
更合适:它通过final
修饰符精准定位需要注入的依赖,避免冗余参数,且符合“依赖不可变”的最佳实践。 - 若使用
@AllArgsConstructor
,建议确保类中没有非依赖的成员变量,否则会引入不必要的麻烦。
@RequestMapping
@RequestMapping(value = "/cdm/dhry", produces = HttpHeaderConstants.APPLICATION_JSON)
@RequestMapping
是 Spring MVC 用于 映射 HTTP 请求路径 的核心注解,类级别标注时作用是:
value
:指定当前控制器处理的 基础请求路径(所有方法的路径会以此为前缀)。produces
:核心作用就是指定 Controller 接口响应给客户端的内容格式类型(即 HTTP 响应头中的Content-Type
)。
实际开发与应用:
- 统一接口路径管理:类级别
@RequestMapping
定义基础路径(如/cdm/dhry
),方法级别再定义子路径(如/batchMerge
),最终完整路径为基础路径 + 子路径
(如/cdm/dhry/batchMerge
)。这种设计的优势是:- 避免路径重复编写,减少冗余(如所有 “到会人员” 相关接口都以
/cdm/dhry
开头)。 - 便于接口分类与维护(通过基础路径可快速定位接口所属模块)。
- 避免路径重复编写,减少冗余(如所有 “到会人员” 相关接口都以
- 规范响应格式:
produces = HttpHeaderConstants.APPLICATION_JSON
明确指定响应为 JSON 格式,有两个实际作用:- 前端可根据响应头的
Content-Type: application/json
直接解析数据,避免格式识别错误。 - 若客户端请求的
Accept
头不包含application/json
(如Accept: text/html
),Spring 会返回 406 Not Acceptable 错误,提前阻断不兼容的请求。
- 前端可根据响应头的
@RequestMapping
还可以用在方法上,可以用于所有请求方式的Http方法,还可通过method
指定允许的 HTTP 方法(如method = RequestMethod.POST
),但实际开发中更常用@PostMapping
、@GetMapping
等派生注解(方法级别)来简化写法。
@RequestMapping(value = "/cdm/dhry")`相当于 `@RequestMapping("/cdm/dhry")
不写
produces
时,Spring 会根据请求的Accept
头、返回值类型和消息转换器动态协商响应的 Content-Type,没有固定默认值。最常见的场景是:当返回 Java 对象且配置了 JSON 处理器(如 Jackson)时,默认会返回
application/json
。标准的 Spring Boot Web 项目中(引入
spring-boot-starter-web
),Jackson 转换器是默认配置的
请求映射相关
常用的请求映射相关的注解主要有:
@PostMapping`、`@GetMapping`、`@DeleteMapping`、`@PutMapping`、`@RequestMapping
这些注解的核心目的就一个:告诉Spring,当一个HTTP请求进来时,该由哪个类的哪个方法来处理。
@RequestMapping
@RequestMapping
如果没有指定,可以处理所有HTTP方法(GET, POST, PUT, DELETE等)的请求。但推荐指定方法,否则会有安全风险,而且指定方法后语义更强、也便于维护。
用法1:指定指定**method
**属性。
@RequestMapping(value = "/user", method = RequestMethod.GET) // 等效于 @GetMapping("/user")
public String getUser() {return "user";
}
如果在方法上只写@RequestMapping("/path")
而不指定method
,那么该方法会响应所有类型的HTTP请求。这通常不是想要的,容易引发混乱和安全问题。所以,现在在方法级别,们基本都用它的“子孙”注解。
用法2:在类级别使用。这是它至今仍然非常有用的主要原因。用于定义模块路径。
@RestController
@RequestMapping("/api/v1/users") // 【关键】类级别的路径,下面所有方法的路径都要以此为基础
public class UserController {@GetMapping("/{id}") // 实际访问路径是:/api/v1/users/123public User getUser(@PathVariable Long id) {// ...}@PostMapping // 实际访问路径是:/api/v1/userspublic User createUser(@RequestBody User user) {// ...}
}
@RequestMapping
不指定请求方法的风险
@RequestMapping` 在技术上完全可以不指定 `method
@RequestMapping("/somePath")
public String handleEverything() {// 这个方法会响应 GET, POST, PUT, DELETE... 所有请求!return "view";
}
那么,为什么说“必须指定”,或者更准确地说,“强烈建议指定”呢?
- 场景一:安全隐患 想象一下,写了一个删除用户的方法,本意是通过
POST /users/delete
来调用,但忘了写method = RequestMethod.POST
。
// 危险的写法!
@RequestMapping("/users/delete")
public String deleteUser(Long userId) {userService.delete(userId);return "success";
}
这时,用户只需要在浏览器地址栏输入 https://yoursite.com/users/delete?userId=123
(一个简单的GET请求),就能轻松删除用户!这被称为 CSRF(跨站请求伪造) 的温床,是严重的安全漏洞。
- 场景二:混乱与难以维护 一个方法处理所有类型的请求,如果要避免以上的安全风险,意味着需要在方法内部用
if-else
来判断请求方法,代码会变得非常臃肿和难以理解。
// 危险的写法!
@RequestMapping("/users/delete")
public String deleteUser(Long userId) {userService.delete(userId);return "success";
}
结论: 在方法级别使用 @RequestMapping
时,99.9%的情况下都应该指定HTTP方法。不指定方法是一种“懒政”,会带来潜在的风险和混乱。这就是为什么Spring后续提供了 @GetMapping
等更具体、更安全的注解,它们本质上就是 @RequestMapping(method = ...)
的快捷方式,逼着把方法类型定死,从语法层面杜绝了上述问题。
@GetMapping
[读取、导航]
-
核心用途:获取资源、跳转页面、查询数据。GET请求应该是幂等的(多次执行效果相同)和安全的(不改变服务器状态)。
-
@RequestMapping(method = RequestMethod.GET)
的快捷方式。 -
场景:
- 获取用户信息:
GET /users/1
- 跳转到列表页:
GET /products
- 带条件搜索:
GET /users?name=John&age=25
(参数用@RequestParam
接收)
- 获取用户信息:
@GetMapping
通过 params
属性来限制更精确的匹配
能根据请求参数(URL中 ?
后面的部分) 来进一步路由请求
核心思想: 同一个URL路径,因为参数不同,可以由不同的方法来处理。
实战场景:
假设有一个用户查询接口 /api/users
,但查询条件不同,想让不同的Service方法处理,以获得更清晰的代码结构。
@RestController
@RequestMapping("/api/users")
public class UserController {// 场景1:精确匹配到 "type=admin"// 只有当请求是 GET /api/users?type=admin 时,才会进入这个方法@GetMapping(params = "type=admin")public List<User> getAdminUsers() {return userService.findAdminUsers();}// 场景2:要求必须有 "active" 参数,值无所谓// 匹配 GET /api/users?active=true 或 GET /api/users?active=false@GetMapping(params = "active")public List<User> getUsersByActiveStatus(@RequestParam boolean active) {return userService.findByActiveStatus(active);}// 场景3:要求必须 **没有** "active" 参数// 只匹配 GET /api/users@GetMapping(params = "!active")public List<User> getAllUsers() {return userService.findAllUsers();}// 场景4:组合条件// 匹配 GET /api/users?department=hr&level=senior@GetMapping(params = {"department=hr", "level=senior"})public List<User> getHrSeniorUsers() {return userService.findHrSeniorUsers();}
}
- 职责分离:每个方法只负责一个非常具体的查询场景,代码更纯粹。
- 避免大泥球方法:不需要在一个庞大的
getUsers
方法里写一堆if (type != null) ... if (active != null)...
的判断。 - API意图清晰:通过注解就能清晰地知道每个接口的用途。
@PostMapping
[新建]
@RequestMapping(method = RequestMethod.POST)
的快捷方式。- 核心用途:提交表单、创建新资源、执行非幂等操作。
- 场景:
- 用户注册:
POST /users
(Body中包含用户JSON数据) - 上传文件:
POST /files/upload
- 登录:
POST /login
(虽然登录是验证,但它改变了服务器的会话状态) - 经验:
- 数据通常放在请求体(Body)中,用
@RequestBody
来接收。 - 非常重要:POST请求不是幂等的。重复提交可能会导致创建两个相同的订单、两个相同的用户。所以在前端和后台都要做防重复提交处理(例如:提交后按钮禁用、Token机制等)。
- 数据通常放在请求体(Body)中,用
- 用户注册:
@PutMapping
[整体更新]
@RequestMapping(method = RequestMethod.PUT)
的快捷方式。- 实战应用与争议:
- 核心用途:替换一个已存在的资源。需要提供资源的完整信息。
- RESTful风格场景:
PUT /users/1
(Body中包含id为1的用户的全部字段,即使没改的也要传) - 争议与现状:
- 理论很美好,现实很骨感。在实际业务中,很多更新都是“部分更新”(比如只修改用户的手机号)。如果严格按照PUT的语义,需要前端传回整个对象,这在大对象时非常浪费带宽。
- 因此,很多人会用
PUT
来表示更新,但实际实现是“部分更新”。或者,更常见的,直接使用PATCH
注解(代表部分更新)来规避这个语义问题。 - AI的建议:在团队内部统一规范。如果要用PUT做整体更新,就明确要求传全部字段;如果要做部分更新,要么用
PATCH
,要么用POST /users/1/update-phone
这种更直观的方式。
@DeleteMapping
[删除]
@RequestMapping(method = RequestMethod.DELETE)
的快捷方式。- 实战应用与技巧:
- 核心用途:删除一个资源。
- RESTful风格场景:
DELETE /users/1
(删除id为1的用户) - 技巧与坑:
- 删除操作通常是软删除(Soft Delete),即只是修改数据库中的一个
deleted
状态字段,而不是真的从物理上删除数据。所以在方法内部,很可能是在执行一个UPDATE
语句,而不是DELETE
语句。这没关系,对外保持DELETE
的语义即可。 - 安全警告:一定要做好权限校验!防止用户通过修改ID恶意删除他人的数据。例如,在删除前要校验当前登录用户是否有权删除目标资源。
- 删除操作通常是软删除(Soft Delete),即只是修改数据库中的一个
@PostMapping
和@GetMapping
GET
和 POST
是绝对的主力,甚至很多项目只用它们俩。
@GetMapping
- 本质:从服务器拿 数据。
- 参数传递:参数只能 拼接在URL后面(即查询字符串
?key=value
)。有长度限制 (不同浏览器限制不同,一般是几KB),并且全部明文暴露 。 - 特性:幂等 且安全 。刷一百次页面,结果都一样,不会创建100个订单。
- 实战使用场景:
- 点击链接、输入网址访问页面。
- 搜索、筛选、分页查询。
- 获取JSON数据,用于前端渲染。
1.
// 典型Get请求:参数在URL里,用于获取数据
@GetMapping("/products")
public Page<Product> searchProducts(@RequestParam String keyword,@RequestParam(defaultValue = "0") int page,@RequestParam(defaultValue = "10") int size) {return productService.search(keyword, page, size);
}
@PostMapping
- 本质:让服务器做一件事(通常是创建或修改数据)。
- 参数传递:参数可以放在URL里(不推荐),但主要放在请求体(Body) 中。可以是表单格式,也可以是JSON格式。没有长度限制 ,且Body内容在HTTP层面不直接可见(更安全,但需用HTTPS加密)。
- 特性:非幂等 。重复提交可能产生重复数据(如创建两个订单)。
- 实战使用场景:
- 提交表单(用户注册、登录、发表评论)。
- 创建新资源(下单、新建文章)。
- 执行动作(虽然RESTful不推荐,但很常用),如
/user/1/disable
。 - 复杂查询:当查询条件非常复杂,用URL传参太长太乱时,也可以用POST 。这就是所谓的 “POST用于查询” 的例外情况。
1.
// 典型Post请求:数据在Body里,用于创建
@PostMapping("/orders")
public Order createOrder(@RequestBody CreateOrderRequest request) {// 防重复提交逻辑往往写在这里或通过拦截器实现return orderService.create(request);
}// 例外情况:用POST做复杂查询
@PostMapping("/products/advanced-search")
public List<Product> advancedSearch(@RequestBody ComplexSearchCriteria criteria) {// 因为criteria对象可能包含几十个字段,用GET传参无法实现return productService.advancedSearch(criteria);
}
为什么可以只用 GET
和 POST
?
因为在HTTP/1.1的早期,很多浏览器只很好地支持了这两个方法。所以形成了“读取用GET,提交用POST”的广泛实践。直到今天,很多传统项目、前后端不分离的项目依然如此。
但是,为什么现代开发更推荐区分使用 PUT
、DELETE
?
- 语义化和自描述性:看到
@DeleteMapping("/users/1")
,任何人都不用看代码就知道这是删除操作。如果写成@PostMapping("/users/1/delete")
,虽然也能懂,但不够直接和统一。 - API****设计规范化 :对于面向外部或移动端的RESTful API,使用标准方法能让API使用者更容易理解和使用。
- 工具支持:很多API测试工具、文档生成工具(如Swagger)对标准HTTP方法的支持更好。
最终建议:
- 对内、简单的系统:如果觉得只用
GET
和POST
更顺手,完全可以,这是经过无数项目验证的可靠模式。 - 对外的RESTful API**、新项目**:建议遵循
GET
(查)、POST
(增)、PUT
/PATCH
(改)、DELETE
(删)的规范,让代码更现代、更专业。