Controller 开发实践
This content is not available in your language yet.
本章介绍 Feat Cloud 中基于注解的 Controller 编程模型,包括路由映射、参数绑定、拦截器等核心功能。
API 接口说明
Section titled “API 接口说明”| 注解 | 作用 | 适用场景 |
|---|---|---|
@Controller | 标识 HTTP 请求处理器 | 定义控制器类 |
@RequestMapping | 映射 HTTP 请求到处理方法 | 定义 API 接口路径和方法 |
@Param | 绑定 URL 查询参数 | 获取 URL 中的查询参数 |
@PathParam | 绑定 URL 路径变量 | 获取 URL 路径中的变量 |
@InterceptorMapping | 配置请求拦截器 | 实现权限验证、日志记录等 |
@PostConstruct | 控制器初始化方法 | 执行初始化逻辑 |
@PreDestroy | 控制器销毁方法 | 执行清理逻辑 |
@Controller
Section titled “@Controller”@Controller 注解用于标识一个类作为 HTTP 请求处理器。被标记的类中的方法可以通过 @RequestMapping 注解映射到特定的 URL 路径。
最简单的用法是不带任何参数:
@Controllerpublic class HelloController {
@RequestMapping("/hello") public String hello() { return "Hello, Feat Cloud!"; }}上述代码定义了一个 HelloController,其 hello() 方法处理所有发送到 /hello 的 HTTP 请求。
指定基础路径
Section titled “指定基础路径”通过 value 参数为控制器指定基础路径:
@Controller("/api")public class ApiController {
@RequestMapping("/users") public List<User> getUsers() { return userService.getAllUsers(); }}此时 getUsers() 方法处理的是 /api/users 请求。
启用响应压缩
Section titled “启用响应压缩”通过 gzip 参数启用 Gzip 压缩,并可通过 gzipThreshold 设置压缩阈值:
@Controller(value = "/api", gzip = true, gzipThreshold = 512)public class CompressedController {
@RequestMapping("/large-data") public String getLargeData() { // 当响应内容超过 512 字节时自动启用 Gzip 压缩 return generateLargeContent(); }}参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | String | "" | 控制器基础路径前缀 |
gzip | boolean | false | 是否启用 Gzip 压缩 |
gzipThreshold | int | 256 | 压缩阈值(字节),仅当响应超过此值时启用压缩 |
@RequestMapping
Section titled “@RequestMapping”@RequestMapping 注解用于将 HTTP 请求映射到处理方法。它可以配置请求路径和 HTTP 方法。
value 属性指定请求路径:
@Controller("/api")public class UserController {
// 处理 GET /api/users @RequestMapping("/users") public List<User> getAllUsers() { return userService.getAllUsers(); }
// 处理 GET /api/users/{id} @RequestMapping("/users/{id}") public User getUser(@PathParam("id") String id) { return userService.getUserById(id); }}限定 HTTP 方法
Section titled “限定 HTTP 方法”method 属性指定处理的 HTTP 方法:
@Controller("/api/users")public class UserController {
// GET /api/users @RequestMapping(method = RequestMethod.GET) public List<User> getAllUsers() { return userService.getAllUsers(); }
// POST /api/users @RequestMapping(method = RequestMethod.POST) public User createUser(User user) { return userService.createUser(user); }
// PUT /api/users/{id} @RequestMapping(value = "/{id}", method = RequestMethod.PUT) public User updateUser(@PathParam("id") String id, User user) { return userService.updateUser(id, user); }
// DELETE /api/users/{id} @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public void deleteUser(@PathParam("id") String id) { userService.deleteUser(id); }}支持的 HTTP 方法: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
路径匹配规则
Section titled “路径匹配规则”| 模式 | 说明 | 示例 |
|---|---|---|
/path | 精确匹配 | 只匹配 /path |
/path/{id} | 路径变量 | 匹配 /path/123, /path/abc |
/path/* | 通配符 | 匹配 /path/foo, /path/bar |
@Param
Section titled “@Param”@Param 注解用于将 URL 查询参数绑定到方法参数。
@Controller("/api")public class UserController {
// GET /api/search?keyword=feat @RequestMapping(value = "/search", method = RequestMethod.GET) public List<User> searchUsers(@Param("keyword") String keyword) { return userService.searchByKeyword(keyword); }}@Controller("/api")public class UserController {
// GET /api/filter?minAge=20&maxAge=30 @RequestMapping(value = "/filter", method = RequestMethod.GET) public List<User> filterByAge(@Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge) { return userService.filterByAgeRange(minAge, maxAge); }}可选参数处理
Section titled “可选参数处理”查询参数可能为空,建议进行空值检查:
@RequestMapping(value = "/search", method = RequestMethod.GET)public List<User> searchUsers(@Param("keyword") String keyword) { if (keyword == null || keyword.trim().isEmpty()) { return Collections.emptyList(); } return userService.searchByKeyword(keyword);}对于可选参数,可以提供默认值:
@RequestMapping(value = "/paginate", method = RequestMethod.GET)public List<User> paginateUsers(@Param("page") Integer page, @Param("size") Integer size) { int pageNum = page != null ? page : 1; int pageSize = size != null ? size : 10; return userService.paginate(pageNum, pageSize);}@PathParam
Section titled “@PathParam”@PathParam 注解用于将 URL 路径变量绑定到方法参数。
单个路径变量
Section titled “单个路径变量”@Controller("/api")public class UserController {
// GET /api/users/123 @RequestMapping(value = "/users/{id}", method = RequestMethod.GET) public User getUserById(@PathParam("id") Integer id) { return userService.getUserById(id); }}多个路径变量
Section titled “多个路径变量”@Controller("/api")public class OrderController {
// GET /api/users/123/orders/456 @RequestMapping(value = "/users/{userId}/orders/{orderId}", method = RequestMethod.GET) public Order getOrder(@PathParam("userId") Integer userId, @PathParam("orderId") String orderId) { return orderService.getOrder(userId, orderId); }}Feat Cloud 支持将 HTTP 请求体自动绑定到方法参数对象,无需额外注解。
首先定义一个 POJO:
public class User { private Integer id; private String name; private Integer age; private String email;
// 必须有公共的无参构造方法 public User() {}
// Getters and Setters public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; }}然后在控制器方法中使用该类型作为参数:
@Controller("/api/users")public class UserController {
// POST /api/users // Content-Type: application/json // Body: {"name": "张三", "age": 25, "email": "zhangsan@example.com"} @RequestMapping(method = RequestMethod.POST) public User createUser(User user) { return userService.createUser(user); }
// PUT /api/users/{id} @RequestMapping(value = "/{id}", method = RequestMethod.PUT) public User updateUser(@PathParam("id") Integer id, User user) { return userService.updateUser(id, user); }}复杂对象绑定
Section titled “复杂对象绑定”支持嵌套对象的自动绑定:
public class Order { private Integer id; private String orderNo; private User user; private List<OrderItem> items;
// 无参构造方法和 setter/getter}
public class OrderItem { private Integer id; private String productName; private Integer quantity;
// 无参构造方法和 setter/getter}
@Controller("/api/orders")public class OrderController { @RequestMapping(method = RequestMethod.POST) public Order createOrder(Order order) { return orderService.createOrder(order); }}@InterceptorMapping
Section titled “@InterceptorMapping”@InterceptorMapping 注解用于配置请求拦截器,可以在请求处理前后执行自定义逻辑,如权限验证、日志记录、跨域处理等。
拦截器方法必须返回 Interceptor 类型:
@Controllerpublic class AuthInterceptor {
@InterceptorMapping("/api/admin/*") public Interceptor authInterceptor() { return new Interceptor() { @Override public void intercept(Context context, CompletableFuture<Void> completableFuture, Chain chain) throws Throwable { // 前置处理:验证权限 String token = context.Request.getHeader("Authorization"); if (token == null || !isValidToken(token)) { context.Response.setHttpStatus(HttpStatus.UNAUTHORIZED); context.Response.write("Unauthorized".getBytes()); completableFuture.complete(null); return; }
// 继续执行后续拦截器和目标处理器 chain.proceed(context, completableFuture);
// 后置处理:记录日志 System.out.println("访问完成: " + context.Request.getRequestURI()); } }; }
private boolean isValidToken(String token) { return token.startsWith("Bearer ") && token.length() > 10; }}多个拦截路径
Section titled “多个拦截路径”@InterceptorMapping({"/api/admin/*", "/api/secure/*"})public Interceptor authInterceptor() { // ...}跨域处理示例
Section titled “跨域处理示例”@Controllerpublic class CorsInterceptor {
@InterceptorMapping("/*") public Interceptor corsInterceptor() { return new Interceptor() { @Override public void intercept(Context context, CompletableFuture<Void> completableFuture, Chain chain) throws Throwable { // 添加跨域响应头 context.Response.setHeader("Access-Control-Allow-Origin", "*"); context.Response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); context.Response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
// 处理预检请求 if ("OPTIONS".equals(context.Request.getMethod())) { context.Response.setHttpStatus(HttpStatus.OK); completableFuture.complete(null); return; }
chain.proceed(context, completableFuture); } }; }}日志记录示例
Section titled “日志记录示例”@Controllerpublic class LoggingInterceptor {
@InterceptorMapping("/api/*") public Interceptor loggingInterceptor() { return new Interceptor() { @Override public void intercept(Context context, CompletableFuture<Void> completableFuture, Chain chain) throws Throwable { long startTime = System.currentTimeMillis();
// 继续执行 chain.proceed(context, completableFuture);
// 记录请求处理时间 long endTime = System.currentTimeMillis(); System.out.println(context.Request.getMethod() + " " + context.Request.getRequestURI() + " - " + (endTime - startTime) + "ms"); } }; }}路径匹配规则:
| 模式 | 说明 | 示例 |
|---|---|---|
/path | 精确匹配 | 只匹配 /path |
/path/* | 匹配子路径 | 匹配 /path/foo, /path/bar |
/* | 匹配所有 | 匹配所有请求 |
生命周期管理
Section titled “生命周期管理”@PostConstruct 和 @PreDestroy 注解用于管理控制器的生命周期,分别在实例创建后和销毁前执行。
@Controller("/api/users")public class UserController {
private Map<Integer, User> userStore;
@PostConstruct public void init() { System.out.println("Controller 初始化完成"); userStore = new ConcurrentHashMap<>(); // 加载初始数据、建立数据库连接等 }
@RequestMapping(method = RequestMethod.GET) public List<User> getAllUsers() { return new ArrayList<>(userStore.values()); }
@PreDestroy public void destroy() { System.out.println("Controller 正在销毁"); // 关闭连接、释放资源、保存数据等 userStore.clear(); }}执行时机:
| 注解 | 执行时机 | 常见用途 |
|---|---|---|
@PostConstruct | Controller 实例创建后,首次处理请求前 | 加载配置、初始化缓存、建立连接 |
@PreDestroy | 应用关闭,Controller 实例销毁前 | 关闭连接、释放资源、保存数据 |
访问请求上下文
Section titled “访问请求上下文”通过方法参数注入 Context 对象,可以获取完整的请求信息:
@Controller("/api")public class ApiController {
@RequestMapping("/request-info") public Map<String, Object> getRequestInfo(Context context) { Map<String, Object> info = new HashMap<>();
// 获取请求头 info.put("contentType", context.Request.getHeader("Content-Type")); info.put("userAgent", context.Request.getHeader("User-Agent"));
// 获取请求信息 info.put("method", context.Request.getMethod()); info.put("uri", context.Request.getRequestURI()); info.put("remoteAddr", context.Request.getRemoteAddr());
// 获取请求体(字节数组) byte[] body = context.Request.getBody();
return info; }}通过 Context 对象可以直接控制响应:
@Controller("/api")public class ResponseController {
@RequestMapping("/custom-response") public void customResponse(Context context) { // 设置响应状态码 context.Response.setHttpStatus(HttpStatus.CREATED);
// 设置响应头 context.Response.setHeader("X-Custom-Header", "value");
// 写入响应体 context.Response.write("Custom response".getBytes()); }}- 单一职责:每个控制器只负责一个业务领域
- 合理命名:控制器类名使用
Controller后缀,方法名使用动词开头 - 路径设计:使用 RESTful 风格的路径设计
- 参数验证:对所有输入参数进行验证
- 异常处理:统一处理业务异常,返回标准错误格式
- 缓存使用:对于频繁访问的数据,使用
@PostConstruct初始化缓存 - 响应压缩:对大响应启用 Gzip 压缩
- 异步处理:对于耗时操作,考虑使用异步处理
- 避免重复代码:抽取公共逻辑到拦截器或服务层
- 权限控制:使用拦截器实现统一的权限验证
- 输入验证:对所有用户输入进行严格验证
- 防止注入:使用参数绑定而非字符串拼接
- 敏感信息保护:避免在响应中包含敏感信息
常见问题与解决方案
Section titled “常见问题与解决方案”1. 请求体绑定失败
Section titled “1. 请求体绑定失败”问题:JSON 请求体无法绑定到对象
可能原因:
- 缺少无参构造方法
- 缺少 setter 方法
- JSON 字段名与 Java 属性名不匹配
- Content-Type 不是 application/json
解决方案:
- 添加公共的无参构造方法
- 为所有属性添加 setter 方法
- 确保 JSON 字段名与 Java 属性名一致
- 确保请求头 Content-Type: application/json
2. 路径变量绑定失败
Section titled “2. 路径变量绑定失败”问题:@PathParam 无法绑定路径变量
可能原因:
- 路径变量名与注解参数不匹配
- 类型转换失败
解决方案:
- 确保路径变量名与
@PathParam参数一致 - 使用合适的类型,或在方法内进行类型转换
3. 拦截器不生效
Section titled “3. 拦截器不生效”问题:定义的拦截器没有执行
可能原因:
- 路径匹配规则不正确
- 拦截器方法返回 null
- 拦截器未调用
chain.proceed()
解决方案:
- 检查路径匹配规则
- 确保拦截器方法返回有效的 Interceptor 对象
- 确保调用
chain.proceed()继续执行
4. 响应压缩不生效
Section titled “4. 响应压缩不生效”问题:启用了 gzip 但响应未压缩
可能原因:
- 响应内容小于 gzipThreshold
- 客户端不支持压缩
- 配置顺序错误
解决方案:
- 调整 gzipThreshold 值
- 检查客户端 Accept-Encoding 头
- 确保 gzip 配置正确
以下是一个完整的 RESTful API 控制器,综合展示了本章的所有功能:
@Controller("/api/users")public class UserController {
private static final Map<Integer, User> userStore = new ConcurrentHashMap<>(); private static final AtomicInteger idGenerator = new AtomicInteger(1);
@PostConstruct public void init() { // 初始化测试数据 User user = new User(); user.setId(idGenerator.getAndIncrement()); user.setName("张三"); user.setAge(25); userStore.put(user.getId(), user); }
// 查询所有用户 @RequestMapping(method = RequestMethod.GET) public List<User> getAllUsers() { return new ArrayList<>(userStore.values()); }
// 根据 ID 查询用户 @RequestMapping(value = "/{id}", method = RequestMethod.GET) public User getUserById(@PathParam("id") Integer id) { return userStore.get(id); }
// 创建用户(请求体自动绑定) @RequestMapping(method = RequestMethod.POST) public User createUser(User user) { user.setId(idGenerator.getAndIncrement()); userStore.put(user.getId(), user); return user; }
// 更新用户 @RequestMapping(value = "/{id}", method = RequestMethod.PUT) public User updateUser(@PathParam("id") Integer id, User user) { User existing = userStore.get(id); if (existing != null) { existing.setName(user.getName()); existing.setAge(user.getAge()); } return existing; }
// 删除用户 @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public Map<String, Object> deleteUser(@PathParam("id") Integer id) { Map<String, Object> result = new HashMap<>(); User removed = userStore.remove(id); result.put("success", removed != null); return result; }
// 按关键字搜索(查询参数) @RequestMapping(value = "/search", method = RequestMethod.GET) public List<User> searchUsers(@Param("keyword") String keyword) { if (keyword == null || keyword.trim().isEmpty()) { return Collections.emptyList(); } List<User> result = new ArrayList<>(); for (User user : userStore.values()) { if (user.getName().contains(keyword)) { result.add(user); } } return result; }
// 分页查询 @RequestMapping(value = "/paginate", method = RequestMethod.GET) public Map<String, Object> paginateUsers(@Param("page") Integer page, @Param("size") Integer size) { int pageNum = page != null ? page : 1; int pageSize = size != null ? size : 10;
List<User> users = new ArrayList<>(userStore.values()); int total = users.size(); int start = (pageNum - 1) * pageSize; int end = Math.min(start + pageSize, total);
List<User> pageUsers = users.subList(start, end);
Map<String, Object> result = new HashMap<>(); result.put("total", total); result.put("page", pageNum); result.put("size", pageSize); result.put("data", pageUsers);
return result; }
@PreDestroy public void destroy() { userStore.clear(); }}- Feat Cloud 入门:快速搭建第一个 Feat Cloud 应用
- CloudOptions 配置参考:了解服务配置选项
- MyBatis 集成:学习数据库操作
- 部署指南:了解如何部署 Feat Cloud 应用