前情提要:从微信公众号跳转微信小程序时,如果传入的参数过长会导致填写“小程序路径”选项无法填写,这时需要后端来实现类似“短链接”的功能,让前端获取保存的信息、解析、重定向到目的页。

既然功能与短链接基本一致,这个需求也就被扩展成为短链接中心,除微信小程序之外的 Web 应用也可以使用,只要在生成短链接时传入是否需要重定向的参数即可。不过重定向使用的 HTTP 状态码倒也需要酌情选择。

使用搜索引擎可以发现很多文章都提到了 301 和 302 重定向,301 和 302 由 HTTP/1.0 引入,是最早使用的重定向。301 属于永久重定向,浏览器会将重定向 URL 缓存下来,缓存存在的期间浏览器的后续的请求会直接跳转到最终地址,不向服务器发出请求。而 302 属于临时重定向,浏览器后续访问到 URL 后仍然会向服务器请求,得到新的响应后决定是否执行跳转。

题外话:浏览器的重定向实现是由 HTTP Header 中的 Location 参数决定的,如果服务器返回的 HTTP Header 中带有 Location 参数,浏览器就会根据这个参数跳转到新的 URL 上,同时根据 HTTP 状态码决定是否将本次的重定向缓存。

另外还有 303 这个状态码,表示当前请求的 URL 存在着另一个请求结果,重定向后请求方法会变为 GET,且浏览器不会缓存下这次重定向的结果。具体应用如早期常用的 form 表单提交,提交成功后重定向回来用于刷新页面,由于前端工程化的原因,form 表单提交配合 303 状态码的使用在近些年也渐渐减少了。

301、 302 状态码的重定向在不同的浏览器中的效果却有所区别,不同的浏览器对重定向后的请求方法实现不一致,可能会出现如 POST 请求在重定向跳转后变成了 GET 请求的情况,具体取决于浏览器的代码实现,按设计之初的意图重定向跳转后的请求方式是不应该被改变的,让 303 状态码失去了意义。同时在一些特殊情况下这可能导致用户的请求被服务器返回错误信息。

1999 年 6 月 标准化的 HTTP/1.1 ( RFC 2616 )包含 307 状态码,用于标准化 302 状态码下的临时重定向。2014 年 6 月在 RFC7238、RFC7538 为 HTTP/1.1 补充了 308 状态码,用于标准化 301 状态码下的永久重定向,规定了重定向后的请求方法与原始方法一致,保持不变。由于 308 出现的太晚,较老的浏览器可能不支持,存在兼容性问题。(在文章发布的 2022 年应该不会再出现这么老的浏览器了吧?)

总结:301、308 是永久重定向,跳转一次后浏览器会缓存重定向结果,但 301 会将请求方式转为 GET。而302、303、307是临时重定向,不缓存重定向结果,302、303 会将请求方式转为 GET。


Java 中关于重定向的实现:

可以直接调用 JDK 自带的 HttpServletResponse 接口,其中有已经封装完成的 response.sendRedirect("xxx"); 方法用于重定向跳转。

    // 业务代码实现
    @RequestMapping(value = "/s/{shortLink}")
    public void jump(HttpServletResponse response, @PathVariable(value = "shortLink") String shortLink){
        try {
            //根据传入shortLink参数取出短链接信息跳转...
            response.sendRedirect("xxx");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Oracle JDK 8 HttpServletResponse 接口实现
    public void sendRedirect(String url) throws IOException {
        this.setHeader("Location", url);
        this.setStatus(302);
        this.setCommitted(true);
    }

这里可以看到 JDK HttpServletResponse 接口实现将 HTTP 状态码固定为了 302, 这可能逼到一位强迫症程序员。如果项目使用了 SpringBoot 框架可以使用 @ResponseStatus 注解来自定义响应的 HTTP 状态码,设置响应码的参数为框架定义好的枚举类 HttpStatus ,有趣的是框架的枚举类中(发文时用的是 SpringBoot 2.6.3)将 HTTP 302 标记为了弃用。

    @RequestMapping(value = "/s/{shortLink}")
    @ResponseStatus(value = HttpStatus.TEMPORARY_REDIRECT)
    public void jump(HttpServletResponse response, @PathVariable(value = "shortLink") String shortLink){
        try {
            //根据传入shortLink参数取出短链接信息跳转...
            response.setHeader("location","xxx");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }