NiceLeeのBlog 用爱发电 bilibili~

SpringBoot 踩坑记录(七)- http状态码

2018-12-18
nIceLee

阅读:


在Nginx端反向代理使用了缓存,由此引出了一个较为严重的问题,最新的文章不能及时更新,本文讨论的该问题的解决思路。

问题描述

在Nginx端,我缓存了读取指定具体文章的链接,以减免对Tomcat服务器的访问请求,链接形式如下:

location ~* /xxx/[0-9]+/[0-9]+

但是,缓存之后存在一个问题,最新的文章不能及时更新。
举个例子来说,假设文章一天一更,如果最新文章是/xxx/001/099
而我在文章的下部会有一个导航:上一页目录下一页
现在,
上一页指向/xxx/001/098
下一页指向/xxx/001/100
服务端在处理/xxx/001/100时,直接返回目录页(章节id的生成遵循一定原理,删除、插入文章等功能暂不考虑)

缓存后,游客在读取前一天的最新文章时,因为现在很多的浏览器都有预读取功能,哪怕不点下一页,实际也会访问使得Nginx缓存了“下一天的最新文章”,这使得文章内容实际上是个“旧的”目录页。
如果缓存时间设置较长,比如一周,那么在长达一周的时间内,游客访问的/xxx/001/100链接都是一个老的目录页。这是我们所不期望的。

解决思路

1、减少缓存过期时间

这是一种解决方案,但过期时间设置得过小,缓存也就没啥意义;有点大,对于有追更需求,希望第一时间获取最新文章的读者来说,也是不可忍受的。

2、更改下一页指向

上一页指向/xxx/001/098
下一页指向目录页
这个影响有所减小,但较长的一段时间内,最近几天的底部导航下一页将会失效,直接到目录页。从目录页获得的最新目录中可以访问最新章节。

3、置灰下一页

上一页指向/xxx/001/098
下一页无指向
和2 类似,较长的一段时间内,最近几天的底部导航将会下一页将会失效。

4、提供更新缓存接口

能较好解决缓存问题,接口最好包装一下,并加上其它逻辑。

5、分情况返回Http 状态码

Tomcat服务端在处理/xxx/001/100时, 返回正常文章时,正常处理;
返回到目录页时,考虑将状态码作修饰,比如404 OK,而不是直接200 OK

Nginx端location对404码做特殊配置,比如只缓存两分钟

proxy_cache_valid 404 120s;

本文采取的是第五种方法,并对其做详细讨论。

实现方案

SpringBoot 中修饰HTTP Status的方法具体有以下几种:

1、@ResponseStatus

@ResponseStatus是一个注解,可以作用在方法和类上面,如下使用(也可以配合Exception),

@RequestMapping(value = "/xxx/{bookId}/{chapterId}", method = RequestMethod.GET)
@ResponseStatus(code=HttpStatus.INTERNAL_SERVER_ERROR,reason="server error")
public String readChapter(@PathVariable int bookId, @PathVariable int chpterId){
    return "chapter";
}

其中,ResponseStatusreason如果设置的话,会返回报错页面,这不是我们需要的。而没有设置的话,返回的是500 OK,同时能正常渲染Thymeleaf。
但是,这里面存在一个问题,该方法就只能返回一个状态值,没法分情况讨论,配合异常使用又没法正常渲染Thymeleaf。
在纠结下去即使能成也不会“优雅”。

2、@ResponseEntity

该方法的一个样例如下,对于RestAPI来说没有问题,渲染Thymeleaf待尝试。

@RequestMapping(value = "/user", method = RequestMethod.GET)
public ResponseEntity<Map<String,Object>> getUser() throws IOException{
    Map<String,Object> map = new HashMap<String,Object>();
    map.put("name", "zhangsan");
    return new ResponseEntity<Map<String,Object>>(map,HttpStatus.OK);
}

3、HttpServletResponse

前文已经有所提及,增加状态修改即可。

@Controller
public class SampleController {
    
    @RequestMapping("/getUser")
	public String test(HttpServletResponse response){
		User user = new User();
		user.setId(123);
		user.setSex("male");
		user.setUsername("nicelee");
		String data = JSON.toJSONString(user);
		try {
            response.setStatus(500);
			response.setContentType("application/json;charset=UTF-8");
			PrintWriter out = response.getWriter();
			out.println(data);
			out.flush();
			out.close();
		} catch (Exception e) {
		  e.printStackTrace();
		}
		return null;
    }
}

本文采用第三种方法。

具体实现

如何使用HttpServletResponse,同时还返回html?
其实并不一定要使用response.getWriter()然后输入,只要修改状态码以后什么都不做,其它的表现其实依然如旧。

@RequestMapping(value = "/{bookId}/{chpterId}")
String readChapter(HttpServletResponse response, Model model, @PathVariable int bookId, @PathVariable int chpterId) {
    ...
    if ((lChpter == null) || (lChpter.size() == 0)) {
        error = "无此章节!!";
        response.setStatus(400);
        return "index";
    }else {...}
}

最后的返回结果如下:


内容
隐藏