你好,我是猿java。
作为一名 Java程序员,对 Controller肯定不陌生,它是与外部客户端通信的入口,比如常见的 REST
操作(GET、PUT、POST、DELETE等),那么,Controller里面应该如何编写才算优雅呢?
其实,一个优雅的 Controller,里面的代码主要包含下面 6个部分:
- 接收 HTTP(s)请求
- 解析请求参数
- 验证请求参数
- 调用业务方法
- 组织返回数据
- 统一异常处理
下面一一讲解这 6个部分:
接收 HTTP(s)请求
接收 HTTP(s)请求是 Controller的入口,这里以查询用户信息为例进行说明,如下代码:
1 2 3 4 5 6 7 8
| @RestController public class UserController { @GetMapping("/user/{userId}") public void getUserById(@PathVariable String userId) { } }
|
在上面的示例中,我们使用 URL/user/{id}
接收用户发出的 GET请求,然后通过getUserById
方法进行真实的业务处理。通过上面的代码,一个请求就被
Controller层成功接收了。
说明:@RestController=@Controller+ResponseBody
解析请求参数
接收到请求后,一般需要对请求参数进行解析,如下示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @RestController public class UserController { @PostMapping("/user/register") public void getGradeById(@RequestBody User user) { } }
public class User { private String nickname; private Integer age; }
|
上述示例代码将请求的 body映射到 User对象上,因此,请求的 body体应该是:
1 2 3 4
| { "nickname": "huahua", "age": "18" }
|
在 SpringMVC 中,常见的参数类型及其用途如下:
原始 HTTP请求和响应对象
直接接收原始的 HTTP请求和响应对象,HttpServletRequest 和 HttpServletResponse
1 2 3 4 5
| @RequestMapping("/test") public void example(HttpServletRequest request, HttpServletResponse response) { }
|
路径变量 (@PathVariable)
用于获取 URL 路径中的动态部分。
1 2 3 4 5 6
| @RequestMapping("/user/{id}") public String getUser(@PathVariable("id") String userId) { return "userDetail"; }
|
请求参数 (@RequestParam)
用于获取 URL 查询参数或表单数据。
1 2 3 4 5 6
| @RequestMapping("/search") public String search(@RequestParam("query") String query) { return "searchResults"; }
|
请求体 (@RequestBody)
用于接收请求体中的数据,常用于处理 JSON 或 XML 格式的数据。
1 2 3 4 5 6
| @RequestMapping(value = "/create", method = RequestMethod.POST) public String create(@RequestBody User user) { return "user"; }
|
模型属性 (@ModelAttribute)
用于绑定表单数据到模型对象。
1 2 3 4 5 6
| @RequestMapping("/register") public String register(@ModelAttribute User user) { return "user"; }
|
会话属性 (@SessionAttribute)
用于访问会话中的属性。
1 2 3 4 5 6
| @RequestMapping("/profile") public String profile(@SessionAttribute("user") User user) { return "profile"; }
|
用于访问 HTTP 请求头信息。
1 2 3 4 5 6
| @RequestMapping("/headers") public String headers(@RequestHeader("User-Agent") String userAgent) { return "headerInfo"; }
|
Cookie 值 (@CookieValue)
用于访问 Cookie 的值。
1 2 3 4 5 6
| @RequestMapping("/cookies") public String cookies(@CookieValue("sessionId") String sessionId) { return sessionId; }
|
自定义参数解析器
可以通过实现 HandlerMethodArgumentResolver接口来自定义参数解析逻辑。
1 2 3 4 5
| @RequestMapping("/custom") public String custom(CustomObject customObject) {
return ""; }
|
验证请求参数
请求参数的验证需要在 Controller层完成,如下代码,对 nickname进行判空处理,参数验证一般有 2种方式:
- 原始方式,这种方式比较灵活,如果需要对参数进行一些逻辑计算后再校验;
- 借助三方工具,比如 Spring validation,javax validation等,这种方式灵活度会低一些,但是更优雅;
1 2 3 4 5 6 7 8 9 10 11
| @RestController public class UserController { @PostMapping("/user/register") public void getGradeById(@RequestBody User user) { if (StringUtils.isBlank(user.getNickname)) { throw new Exception("Nickname is required."); } } }
|
或者使用 Spring validation验证机制,Controller需要增加@Validated
注解,User对象中增加@NotBlank
注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestController public class UserController { @PostMapping("/user/register") public void getGradeById(@Validated @RequestBody User user) { } }
public class User { @NotBlank(message = "Nickname is required.") private String nickname; private Integer age; }
|
调用业务方法
如下代码,调用 UserService.register()进行注册业务处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; }
@PostMapping("/user/register") public void getGradeById(@Validated @RequestBody User user) { userService.register(user); } }
public class User { @NotBlank(message = "Nickname is required.") private String nickname; private Integer age; }
|
关于调用业务方法,这里的业务方法是写一个大而全的方法?还是需要按业务归类?
遵守一个原则:有强关联性的逻辑放在一个service方法内,没有强关联性的单令拎出来。
这里以用户注册之后需要新人发券为例进行说明:
大而全的方法
1 2 3 4 5 6 7 8 9 10 11 12
| @PostMapping("/user/register") public void getGradeById(@Validated @RequestBody User user) { userService.doRegister(user); }
public String doRegister(Uswr user){ String userId = userService.register(user); coupon.sendCoupon(userId); return userId; }
|
业务归类
1 2 3 4 5 6
| @PostMapping("/user/register") public void getGradeById(@Validated @RequestBody User user) { userService.register(user); coupon.sendCoupon(userId); }
|
组织返回数据
如下代码,调用 UserService.register()进行注册业务处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @RestController public class UserController { private final UserService userService;
public UserController(UserService userService) { this.userService = userService;
}
@PostMapping("/user/register") public UserResponse getGradeById(@Validated @RequestBody User user) { String userId = userService.regist(user);
return new UserResponse(userId, user.getNickname); } }
public class UserResponse { private String userId; private String nickname; }
|
统一异常处理
比如上述过程在 userService.regist(user);出现异常时,可以做一个try-catch,然后在 Controller层封装有业务意思的异常信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @RestController public class UserController { private final UserService userService;
@PostMapping("/user/register") public UserResponse getGradeById(@Validated @RequestBody User user) { try { String userId = userService.regist(user); } catch (Exception e) { throw new CustomException(); } return new UserResponse(userId, user.getNickname); } }
|
建议和总结
看过很多代码,业务逻辑全部写在 Controller层,并不能说这样的做法是错的,但是看起来很别扭,不优雅!因此,建议在编写代码时,最好能遵守一个比较好的规范,比如常见的SOLID
规范。
SOLID 实际上是五个设计原则首字母的缩写,它们分别是:
- 单一职责原则(Single responsibility principle, SRP)
- 开放封闭原则(Open–closed principle, OCP)
- Liskov 替换原则(Liskov substitution principle, LSP)
- 接口隔离原则(Interface segregation principle, ISP)
- 依赖倒置原则(Dependency inversion principle, DIP)
建议多去阅读一些优秀开源框架的代码规范,相信我:养成一个良好的编码规范,绝对受益颇多!
学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。