跳转到内容

Controller 开发实践

本章介绍 Feat Cloud 中基于注解的 Controller 编程模型,包括路由映射、参数绑定、拦截器等核心功能。


注解作用适用场景
@Controller标识 HTTP 请求处理器定义控制器类
@RequestMapping映射 HTTP 请求到处理方法定义 API 接口路径和方法
@Param绑定 URL 查询参数获取 URL 中的查询参数
@PathParam绑定 URL 路径变量获取 URL 路径中的变量
@InterceptorMapping配置请求拦截器实现权限验证、日志记录等
@PostConstruct控制器初始化方法执行初始化逻辑
@PreDestroy控制器销毁方法执行清理逻辑

@Controller 注解用于标识一个类作为 HTTP 请求处理器。被标记的类中的方法可以通过 @RequestMapping 注解映射到特定的 URL 路径。

最简单的用法是不带任何参数:

@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "Hello, Feat Cloud!";
}
}

上述代码定义了一个 HelloController,其 hello() 方法处理所有发送到 /hello 的 HTTP 请求。

通过 value 参数为控制器指定基础路径:

@Controller("/api")
public class ApiController {
@RequestMapping("/users")
public List<User> getUsers() {
return userService.getAllUsers();
}
}

此时 getUsers() 方法处理的是 /api/users 请求。

通过 gzip 参数启用 Gzip 压缩,并可通过 gzipThreshold 设置压缩阈值:

@Controller(value = "/api", gzip = true, gzipThreshold = 512)
public class CompressedController {
@RequestMapping("/large-data")
public String getLargeData() {
// 当响应内容超过 512 字节时自动启用 Gzip 压缩
return generateLargeContent();
}
}

参数说明:

参数类型默认值说明
valueString""控制器基础路径前缀
gzipbooleanfalse是否启用 Gzip 压缩
gzipThresholdint256压缩阈值(字节),仅当响应超过此值时启用压缩

@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);
}
}

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

模式说明示例
/path精确匹配只匹配 /path
/path/{id}路径变量匹配 /path/123, /path/abc
/path/*通配符匹配 /path/foo, /path/bar

@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);
}
}

查询参数可能为空,建议进行空值检查:

@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 注解用于将 URL 路径变量绑定到方法参数。

@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);
}
}
@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);
}
}

支持嵌套对象的自动绑定:

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 注解用于配置请求拦截器,可以在请求处理前后执行自定义逻辑,如权限验证、日志记录、跨域处理等。

拦截器方法必须返回 Interceptor 类型:

@Controller
public 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;
}
}
@InterceptorMapping({"/api/admin/*", "/api/secure/*"})
public Interceptor authInterceptor() {
// ...
}
@Controller
public 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);
}
};
}
}
@Controller
public 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
/*匹配所有匹配所有请求

@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();
}
}

执行时机:

注解执行时机常见用途
@PostConstructController 实例创建后,首次处理请求前加载配置、初始化缓存、建立连接
@PreDestroy应用关闭,Controller 实例销毁前关闭连接、释放资源、保存数据

通过方法参数注入 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());
}
}

  1. 单一职责:每个控制器只负责一个业务领域
  2. 合理命名:控制器类名使用 Controller 后缀,方法名使用动词开头
  3. 路径设计:使用 RESTful 风格的路径设计
  4. 参数验证:对所有输入参数进行验证
  5. 异常处理:统一处理业务异常,返回标准错误格式
  1. 缓存使用:对于频繁访问的数据,使用 @PostConstruct 初始化缓存
  2. 响应压缩:对大响应启用 Gzip 压缩
  3. 异步处理:对于耗时操作,考虑使用异步处理
  4. 避免重复代码:抽取公共逻辑到拦截器或服务层
  1. 权限控制:使用拦截器实现统一的权限验证
  2. 输入验证:对所有用户输入进行严格验证
  3. 防止注入:使用参数绑定而非字符串拼接
  4. 敏感信息保护:避免在响应中包含敏感信息

问题:JSON 请求体无法绑定到对象

可能原因

  • 缺少无参构造方法
  • 缺少 setter 方法
  • JSON 字段名与 Java 属性名不匹配
  • Content-Type 不是 application/json

解决方案

  • 添加公共的无参构造方法
  • 为所有属性添加 setter 方法
  • 确保 JSON 字段名与 Java 属性名一致
  • 确保请求头 Content-Type: application/json

问题@PathParam 无法绑定路径变量

可能原因

  • 路径变量名与注解参数不匹配
  • 类型转换失败

解决方案

  • 确保路径变量名与 @PathParam 参数一致
  • 使用合适的类型,或在方法内进行类型转换

问题:定义的拦截器没有执行

可能原因

  • 路径匹配规则不正确
  • 拦截器方法返回 null
  • 拦截器未调用 chain.proceed()

解决方案

  • 检查路径匹配规则
  • 确保拦截器方法返回有效的 Interceptor 对象
  • 确保调用 chain.proceed() 继续执行

问题:启用了 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();
}
}