🐸 青蛙编程网
首页 / Java / Spring Boot / 3. 控制器与路由

控制器与路由

这是什么:控制器(Controller) = 决定「访问哪个网址就做什么」的那段 Java 代码,相当于网站门口的接待员。

为什么用:上一章网站跑起来了,可 8080 端口除了报错什么也不干——网址和代码之间还没有路。不写控制器,你连一个能返回「你好」的页面都做不出来。

如何用:本章先写一个最简单的控制器,再用 @PathVariable 接路径参数、@RequestParam 接查询参数,最后试一条注解返回 JSON。

控制器:网址背后的接待员

回顾 Flask 的思路:@app.route("/") 声明"访问 / 就调用这个函数"。Spring Boot 里干这事的类叫控制器,类头上标 @RestController;每个方法再用 @GetMapping("网址") 挂一条路由。

一个请求从浏览器到你的方法,中间是这么一条流水线:

浏览器访问 /hello
   ↓
内嵌 Tomcat 接住请求(网络层的事,框架包了)
   ↓
Spring MVC 查"路由表":/hello 归 HelloController.hello 管
   ↓
调用你的方法,拿到返回值
   ↓
把返回值写给浏览器

看明白了吗?你只写中间那个方法,前后四步全是框架的活——上一章说的"你只管造零件,框架负责组装调用",落地就是这么回事。那张"网址 → 方法"的对照表,术语叫路由表,Spring MVC 启动时就把所有控制器的路由登记完毕,请求来了按表调度,一查一个准。下面把最常用的几类路由写法一次写全。

在 src/main/java/com/example/demo/ 下(和启动类同目录)新建 HelloController.java:

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "你好,Spring Boot!";
    }
}

重启应用(Ctrl + C 停掉后重新 mvnw spring-boot:run),访问 http://localhost:8080/hello:

你好,Spring Boot!

方法返回什么,浏览器就显示什么——和 Flask 的 return 一个道理。

顺带认识一下方法签名里藏的约定:public String hello() 里,public 让框架够得着这个方法,String 说明返回的是文字。返回类型写 void 行不行?能编译,但浏览器会收到一片空白——没有返回值就没有内容可给,所以控制器方法几乎都带着返回类型。

动态参数:@PathVariable

固定文字的网址只能"背诵课文",真实网站的网址要能带变量——比如 /hello/小青蛙、/hello/大黄,总不能一个访客写一个方法。路径写 {name} 占位,方法参数用 @PathVariable 接:

@GetMapping("/hello/{name}")
public String helloName(@PathVariable String name) {
    return "你好," + name + "!";
}

访问 http://localhost:8080/hello/小青蛙,浏览器显示 你好,小青蛙!。换成任何名字都行——一个方法服务无数访客。

查询参数:@RequestParam

网址 ? 后面的部分叫查询参数(如 ?name=Tom),适合传"可给可不给"的附加信息,用 @RequestParam 接,还能设默认值:

@GetMapping("/greet")
public String greet(@RequestParam(defaultValue = "游客") String name) {
    return "欢迎你," + name + "!";
}

访问 /greet 显示 欢迎你,游客!;访问 /greet?name=张三 显示 欢迎你,张三!。

路径变量和查询参数还能混搭,一个方法接多路数据:

@GetMapping("/hello/{name}/age/{age}")
public String helloAge(@PathVariable String name, @PathVariable int age) {
    return name + ",你 " + age + " 岁了,正好学 " + (age + 10) + " 门语言!";
}

访问 /hello/小青蛙/3,你会看到:

小青蛙,你 3 岁了,正好学 13 门语言!

几个占位符就写几段路径、配几个 @PathVariable,参数名要和花括号里的名字一模一样——名字对不上,参数接到的就是 null 或直接报错。

返回 JSON:一条注解的魔法

给"人"看网页之前,先看现代后端最常见的活:给"程序"喂 JSON。手机 App、小程序、Vue 页面,后端吐的都是 JSON——这条路由学会,就等于摸到了前后端分离的门把手。

把返回值换成 Map(面向对象的集合章节学过),再访问试试:

@GetMapping("/user")
public Map<String, Object> user() {
    return Map.of("name", "小青蛙", "level", 3);
}

浏览器显示的不是 Java 打印格式,而是标准 JSON:

{"name":"小青蛙","level":3}

这就是 @RestController 的含义:这个类里所有返回值直接写给浏览器——字符串原样给,对象自动转 JSON(背后是 Jackson 库在干活)。前后端分离项目里,后端接口基本都在返回 JSON。别忘了 import java.util.Map;。

Map 只是开胃菜,把面向对象学的类直接交给框架,效果更惊艳:

@GetMapping("/frog")
public Frog frog() {
    return new Frog("小青蛙", 3, true);
}
public class Frog {
    private String name;
    private int age;
    private boolean vip;

    public Frog(String name, int age, boolean vip) {
        this.name = name;
        this.age = age;
        this.vip = vip;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public boolean isVip() { return vip; }
}

访问 /frog,你会看到:

{"name":"小青蛙","age":3,"vip":true}

Jackson 转对象靠的就是 getter:它拿着对象挨个调 getName、getAge、isVip,方法名去掉 get/is,剩下的就是 JSON 的键名——getScore 变 score,isVip 变 vip。上一章说"布尔属性的 getter 用 is 开头、命名要守惯例",这里立刻兑现了:命名乱起,JSON 字段就跟着乱。返回 List 集合同理,会自动变成 JSON 数组。

解读表

注解 / 概念 作用
@RestController 标记控制器类,返回值直接写给浏览器
@GetMapping("/hello") 把 GET 请求 /hello 路由到这个方法
@PathVariable 接住网址路径里的 {name} 占位符
@RequestParam 接住 ?name=xx 查询参数,可设 defaultValue
返回 Map / 对象 自动转成 JSON 交给浏览器

@PathVariable 和 @RequestParam 都在"接参数",什么时候用哪个,一张表分清:

对比项 @PathVariable @RequestParam
参数在哪 网址路径里(/hello/小青蛙) 问号后面(/greet?name=Tom)
适合传什么 资源的"身份证",如 /user/42 筛选、微调,如 ?page=2&size=10
能否省略 不能,路径少一段就 404 能,可配 defaultValue
类比 快递地址里的门牌号 备注栏里的附加要求

另外,把本章四个方法合起来,一份完整可跑的控制器长这样——直接对照检查你写的文件:

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "你好,Spring Boot!";
    }

    @GetMapping("/hello/{name}")
    public String helloName(@PathVariable String name) {
        return "你好," + name + "!";
    }

    @GetMapping("/greet")
    public String greet(@RequestParam(defaultValue = "游客") String name) {
        return "欢迎你," + name + "!";
    }

    @GetMapping("/user")
    public Map<String, Object> user() {
        return Map.of("name", "小青蛙", "level", 3);
    }
}

❓ 常见问题

Q1:明明加了新路由,访问却 404?

按概率排查三件事:① 改完代码重启应用了吗——Spring Boot 不会自动加载新代码;② 新类是不是放在 com.example.demo 包(或子包)里——放外面 @SpringBootApplication 扫描不到,等于没注册;③ 网址拼对了吗——/hello 和 /Hello 是两条路由,大小写敏感。

Q2:404 到底是什么意思?

"你要的网页不存在"——Spring MVC 翻遍路由表,没找到和这个网址对应的方法。它是 Web 开发出勤率最高的错误码,九成九是路由没对上,对照 Q1 三步排查基本能解决。

Q3:@RestController 和 @Controller 什么区别?

@RestController = @Controller + "返回值直接写给浏览器"。返回文字、JSON 用前者;返回页面名(下一章 Thymeleaf)要用不带 Rest 的 @Controller。用错了的后果很滑稽:想返回页面,浏览器却收到页面名的纯文字——下一章会专门演示。

Q4:一个类里能写多少个方法?能拆多个类吗?

方法数量不限,路由不能重复——两条路由挂同一个网址,启动时直接报错。实际项目习惯按业务拆类:用户相关的放 UserController,商品相关的放 ProductController,一个类管一摊,找代码不迷路。

Q5:能直接返回一个 List 吗?

能,而且常用。返回 List<String> 会变成 JSON 数组 ["苹果","香蕉"],返回 List<Frog> 就是对象数组——框架连循环转 JSON 的活都包了。在方法返回类型上老老实实写 List<...>,Jackson 见了自动处理,不需要任何额外注解。

⚠️ 新手翻车现场

翻车一:忘了 @RestController 注解

public class HelloController {   // ❌ 类头上没挂注解

    @GetMapping("/hello")
    public String hello() { return "你好"; }
}

问题:类不被 Spring 识别,路由压根没注册,访问 /hello 永远 404——而且代码看起来"完全正确",相当迷惑。

修正:类定义上一行补上 @RestController。

翻车二:两个方法抢同一条路由

@GetMapping("/hello")
public String hello() { return "你好"; }

@GetMapping("/hello")        // ❌ 和上面重复了
public String hello2() { return "又你好"; }

问题:启动直接失败,日志里一句大写的 Ambiguous mapping(暧昧不清的映射)——同一个网址挂了两个方法,框架不知道该叫谁。

修正:给第二个方法换一个网址,比如 /hello2。

翻车三:@RequestParam 参数接不到,启动或访问时报错

@GetMapping("/greet")
public String greet(@RequestParam String name) { ... }
// 访问 /greet(不带 ?name=xxx)→ 400 Bad Request

问题:不设 defaultValue 的 @RequestParam 是必填的,缺了参数 Spring 直接拒绝请求。

修正:要么访问时带上参数 ?name=张三,要么声明 defaultValue 给个兜底值。

🧰 调试小技巧:看懂状态码

排障最快的入口是浏览器按 F12 → 切到 Network(网络) 标签 → 刷新页面,点开那条请求,Status(状态码) 一栏直接告诉你问题性质:

状态码 意思 多半是这些原因
200 成功 万事大吉
404 网址不存在 路由拼错 / 忘了重启 / 类不在扫描包里
400 请求不合法 必填的 @RequestParam 没传
405 方式不允许 路由在,但 GET/POST 用反了(下一章表单常见)
500 服务器炸了 你的方法里抛了异常,去命令行看红色堆栈

记住"400 找参数,404 找路由,500 看日志",排障方向立刻清晰一半。养成先看状态码再改代码的习惯,能少走大量弯路。

🎯 这些写法什么时候用

  • 返回字符串:调试接口、给前端占位、极简演示——先让路通了再说
  • @PathVariable:定位"哪一个",比如 /user/42 看 42 号用户,REST 风格接口的主力写法
  • @RequestParam:控制"怎么查",比如分页 ?page=1、搜索 ?keyword=蛙,配 defaultValue 更友好
  • 返回对象/Map:前后端分离项目的标准姿势,后端专心吐 JSON,页面交给前端渲染
  • 按业务拆多个控制器类,路由前缀和方法名起得见名知意,三个月后的你会感谢现在的你

📝 本章小结

  • 控制器 = 网址的接待员;请求流水线五步里你只写"处理方法"一步
  • @RestController 让返回值直写给浏览器:字符串原样返回,对象自动转 JSON(Jackson 亲力亲为,靠 getter 定键名)
  • @GetMapping 挂路由,@PathVariable 接路径段,@RequestParam 接问号参数并可设默认值
  • 404 先查三件事:重启了吗、类在扫描包里吗、网址拼对吗
  • 路由不能重复,Ambiguous mapping = 两个方法抢一个网址
  • 记忆口诀:RestController 当接待,GetMapping 挂门牌;PathVariable 接路径,问号参数 RequestParam 来;返回对象变 JSON,前后端分离全靠它。

记忆口诀

RestController 当接待,GetMapping 挂门牌;PathVariable 接路径,问号参数 RequestParam 来;返回对象变 JSON,前后端分离全靠它。

动手改

三个小活都围绕"多挂一条路由"展开,每做完记得重启验证:

  1. 加一个 /bye 路由,返回"再见,欢迎再来"
  2. 给 /greet 再加一个 @RequestParam String mood(不设默认值),然后只访问 /greet?name=张三,看看报什么错——体会"必填参数"
  3. 让 /user 返回三对以上的键值(提示:Map.of 一次最多放 10 对)

下一章:模板渲染,告别纯文字,输出真正的网页 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。