一个 HTTP 请求的完整旅程,从浏览器到数据库
把浏览器发请求到数据库查数据这条链路串起来,涉及 Filter、Interceptor、Controller、Service、Mapper 各层职责。
一个 HTTP 请求的完整旅程,从浏览器到数据库
之前写项目的时候,经常会有这种感觉:
浏览器发了个请求,后台收到了,然后呢,然后就不知道了。
只知道 Controller 里写个注解,Service 里写点逻辑,Mapper 里写个 SQL,数据就查出来了。
但中间到底发生了什么,一直没想明白。
后来学了 SpringMVC、Filter、Interceptor、ThreadLocal 这些东西之后,终于能把这条链路串起来了。
今天就把一个 HTTP 请求从发出到返回的全过程梳理一遍。
起点:浏览器发出请求
假设用户在浏览器里点了「登录」按钮,填了用户名密码,点击提交。
这时候浏览器会发出一个 HTTP 请求:
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "123456"
}
这个请求会经过网络,最终到达我们的 Spring Boot 应用。
第一站:Filter(过滤器)
请求到达服务器之后,首先会经过 Filter。
Filter 是 JavaWeb 的三大组件之一(Servlet、Filter、Listener),工作在请求到达 Servlet 之前和响应返回客户端之前。
在我们项目里,Filter 主要干两件事:
第一件事:解析 Token
@WebFilter("/api/*")
public class TokenParseFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String token = request.getHeader("token");
if (token != null && !token.isEmpty()) {
try {
Claims claims = JwtUtils.parseToken(token);
// 把用户信息存入 ThreadLocal
UserContext.set(claims.get("userId", Integer.class),
claims.get("username", String.class));
} catch (Exception e) {
// Token 无效,不存
}
}
chain.doFilter(req, res);
}
}
第二件事:清理 ThreadLocal
请求结束后,一定要清理 ThreadLocal,不然可能会影响下一个请求。
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(req, res);
} finally {
UserContext.remove(); // 清理 ThreadLocal
}
}
第二站:Interceptor(拦截器)
Filter 处理完之后,请求会进入 SpringMVC 的拦截器。
Interceptor 比 Filter 更细粒度,可以针对特定路径进行拦截。
在我们项目里,Interceptor 主要做登录校验:
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
// 白名单:登录、登出接口不校验
String url = req.getRequestURI();
if (url.contains("/auth/login") || url.contains("/auth/logout")) {
return true;
}
// 检查 ThreadLocal 中是否有用户信息
if (UserContext.get() == null) {
res.sendRedirect("/api/401");
return false;
}
return true;
}
}
登录接口不走拦截器,其他接口都要校验有没有登录。
第三站:Controller
拦截器放行之后,请求到达 Controller。
Controller 是 SpringMVC 的入口,负责接收请求、解析参数、调用 Service、返回响应。
@RestController
@RequestMapping("/emps")
public class EmpController {
@Autowired
private EmpService empService;
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
Emp emp) {
return Result.success(empService.page(page, pageSize, emp));
}
@PostMapping
public Result save(@RequestBody Emp emp) {
empService.save(emp);
return Result.success();
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
empService.delete(id);
return Result.success();
}
}
@RestController 表示这是一个返回 JSON 的控制器。
@RequestMapping 映射请求路径。
@GetMapping、@PostMapping、@DeleteMapping 指定请求方式。
@RequestParam、@RequestBody、@PathVariable 分别获取请求参数、请求体、路径参数。
第四站:Service
Controller 调用 Service,Service 是业务逻辑的核心。
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
@Override
public PageResult page(Integer page, Integer pageSize, Emp emp) {
PageHelper.startPage(page, pageSize);
List<Emp> list = empMapper.selectByCondition(emp);
Page<Emp> p = (Page<Emp>) list;
return new PageResult(p.getTotal(), p.getResult());
}
@Override
@Transactional
public void save(Emp emp) {
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
}
@Override
@Transactional
public void delete(Integer id) {
empMapper.deleteById(id);
}
}
@Service 标记这是一个 Spring 管理的 Bean。
@Transactional 标记这个方法需要事务支持。
PageHelper 是分页插件,自动帮我们处理分页逻辑。
第五站:Mapper(数据访问层)
Service 调用 Mapper,Mapper 负责和数据库打交道。
@Mapper
public interface EmpMapper {
List<Emp> selectByCondition(Emp emp);
Emp selectById(Integer id);
void insert(Emp emp);
void update(Emp emp);
void deleteById(Integer id);
}
对应 XML 映射文件:
<mapper namespace="com.myapp.mapper.EmpMapper">
<select id="selectByCondition" resultType="Emp">
SELECT * FROM emp
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="gender != null">
AND gender = #{gender}
</if>
</where>
ORDER BY create_time DESC
</select>
<insert id="insert" parameterType="Emp" useGeneratedKeys="true" keyProperty="id">
INSERT INTO emp (name, gender, phone, job, entrydate, createTime, updateTime)
VALUES (#{name}, #{gender}, #{phone}, #{job}, #{entrydate}, #{createTime}, #{updateTime})
</insert>
</mapper>
MyBatis 会自动把 SQL 结果映射到 Emp 对象。
第六站:数据库
Mapper 执行 SQL,最终到达数据库。
MySQL 收到 SQL 之后,执行查询或更新,然后把结果返回给 MyBatis。
SELECT * FROM emp WHERE name LIKE '%张%' ORDER BY create_time DESC LIMIT 0, 10
这个 SQL 会遍历 emp 表,找出所有名字包含「张」的员工,按创建时间倒序排列,取前 10 条。
返回:从数据库到浏览器
数据从数据库返回之后,会沿着原路返回:
MySQL → MyBatis → Service → Controller → Interceptor → Filter → 浏览器
Controller 把结果包装成统一的响应格式:
{
"code": 1,
"msg": "success",
"data": {
"rows": [...],
"total": 100
}
}
然后经过 Interceptor 和 Filter,最终返回给浏览器。
浏览器收到响应,更新页面显示。
ThreadLocal:请求内用户传递
在整个请求链路中,有一个概念很重要:ThreadLocal。
在 Filter 里,我们把解析出来的用户信息存入 ThreadLocal:
UserContext.set(claims.get("userId", Integer.class),
claims.get("username", String.class));
然后在 Service 里可以直接获取当前用户:
Integer userId = UserContext.getUserId();
ThreadLocal 的作用是:在当前线程内共享数据,跨线程隔离。
好处是不需要层层传参,任何地方都能拿到当前登录用户。
但要注意两点:
- 请求结束后一定要清理:
UserContext.remove() - 不适合存大对象,只适合存用户 ID、用户名这种小数据
全局异常处理
请求过程中难免会出异常,怎么统一处理?
用 @RestControllerAdvice:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
log.error("业务异常:", e);
return Result.error(e.getMessage());
}
@ExceptionHandler(SystemException.class)
public Result handleSystemException(SystemException e) {
log.error("系统异常:", e);
return Result.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("未知异常:", e);
return Result.error("系统繁忙,请稍后再试");
}
}
这样不管哪个 Controller 抛出异常,都会被这个处理器捕获,返回统一的错误格式。
完整请求流程图
把整个过程串起来:
浏览器
↓ HTTP 请求
Filter(解析 Token,存入 ThreadLocal)
↓
Interceptor(校验登录状态)
↓
Controller(接收请求,解析参数)
↓
Service(业务逻辑,事务管理)
↓
Mapper(SQL 执行)
↓
MySQL(数据持久化)
↓
返回结果
↓
Controller(包装响应)
↓
Interceptor
↓
Filter(清理 ThreadLocal)
↓
浏览器
一句话总结
一个 HTTP 请求的完整旅程就是:
浏览器 → Filter → Interceptor → Controller → Service → Mapper → MySQL → 原路返回 → 浏览器
其中:
- Filter 负责通用预处理(编码、Token 解析)
- Interceptor 负责细粒度拦截(登录校验)
- Controller 负责接收请求和返回响应
- Service 负责业务逻辑
- Mapper 负责数据访问
- MySQL 负责数据存储
ThreadLocal 贯穿整个过程,在请求开始时存入用户信息,请求结束时清理。
全局异常处理器保证不管哪里出错,都能返回统一的错误格式。
理解了这条链路,再看 Spring Boot 项目,就不会觉得那么神秘了。