拦截器
在设计网站的时候,一般都会有一个登录的页面需要用户去填写,但是我们后端的接口出来登录接口之外,还有其他的接口,如果用户没有按常规的套路先登录在浏览网站,而是直接访问网站内容的话,我们就需要先判断用户是否处于登录状态,如果不是就需要强制用户去登录(跳转到登录页面)
强制登录功能的代码如果每个接口都去写的话,就会很麻烦,有没有更加简单的操作就可以实现用户强制登录的功能呢?有的兄弟,有的。
就是Spring 给我们提供了一个拦截器的功能,拦截器用于拦截 url 的请求,如果符合通行情况就放行,如果不符合通行情况就进行拦截,即用户无法访问后面的接口。

拦截器的使用:
- 定义拦截器
- 注册拦截器
定义拦截器,首先需要 绑定接口 HandlerInterceptor
HandlerInterceptor 的源码如下:
java
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
preHandle 表示在进入接口前进行拦截,如果方法返回结果为 true 则放行,否则就进行拦截
postHandle 这个方法是在目标方法执行完之后执行的,目标方法也就是我们的网页接口afterCompletion 这个方法是在视图渲染完毕后执行的,最后执行,由于我们现在后端开发不涉及视图,所以这个方法大家不用深入了解
示例:
java
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("目标方法执行前执行...");
if(!checkLoginStatus(request)) {
//用户未登录
//通过 response 返回用户未登录的状态
response.setStatus(401);
response.setContentType("text/html;charset=utf-8");
String s = "用户未登录";
response.getOutputStream().write(s.getBytes("UTF-8"));
return false;
}
//用户已登录,放行
return true;
}
//判断用户是否登录
private boolean checkLoginStatus(HttpServletRequest request) {
HttpSession session = request.getSession(false);//如果没有 session 就不需要创建 session
if(session == null) {
return false;
}
//获取 session 里面的对象
UserInfo userInfo = (UserInfo) session.getAttribute(Constans.USER_INFO_KEY);
if(userInfo == null || userInfo.getId() < 0) {
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("目标方法执行完之后....");
}
}
拦截器定义完之后就要考虑哪些 url 请求需要拦截,首先登录页面不应该拦截,否则用户就无法登录,其次是前端的请求也不应该拦截,用户一般是通过前端的请求来对网页进行访问的,我们只需要在后端处理好用户是否未登录的判断即可,如果没有登录直接拦截住,这样前端也获取不了后端的数据,并让前端告知用户需要先进行登录并跳转到登录页面即可。
综上所述:登录接口和前端的请求不拦截,其他全部拦截:
代码示例:
java
//注册配置拦截器
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
//将拦截器注入
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/book/**");
}
}
由于我这里的后端访问接口出来 登录之外,剩下的都是 /book 开头的 url ,所以我直接拦截 /book 就可以了。
首先创建一个类用于注册拦截器实现 WebMvcConfigurer
注入拦截器
重写 addInterceptors 方法,在方法内部添加拦截器,使用 addPathPatterns() 添加要拦截的路径,excludePathPatterns() 添加不拦截(放行)的路径

以上拦截规则可以拦截此项目中的使用URL,包括静态文件(图片文件, JS 和 CSS 等文件).
统一返回结果
我们每一个接口的返回内容可能不是同一个数据类型的,如果这些直接返回给前端,就会增大前端的工作量,为了提升我们工程的效率,我们会将这些结果进行统一格式,这样前后端的交互也会变得更加容易。
如果我们把每一个接口的结果都进行封装,这显然不显示,我们可以通过类似拦截器这种,在外界直接对返回的结果进行封装即可。
统一返回数据格式使用 @ControllerAdvice 和 ResponseBodyAdvice 的 方式实现
@ControllerAdvice 表示控制层通知类
ResponseBodyAdvice 是一个接口,通过实现这个接口的方式实现对返回结果的封装
java
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return false;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return null;
}
}
supports 方法表示:判断是否要执行 beforeBodyWrite 方法,返回值 true 表示执行,false 表示不执行
beforeBodyWrite 方法则是对数据统一格式的处理
示例:
java
@Data
public class Result<T> {
// -1 表示用户未登录
// -2 表示后端出现严重错误
// 200 表示正常返回结果
public int code;
//对状态码进行解释
public String desc;
//封装结果
public T data;
//正常返回结果
public static <T> Result<T> success(T data) {
Result result = new Result();
result.setCode(200);
result.setDesc("正常返回结果");
result.setData(data);
return result;
}
//用户未登录
public static <T> Result<T> unlogin() {
Result result = new Result();
result.setCode(-1);
result.setDesc("用户未登录,请先进行登录操作");
return result;
}
//后端出现严重错误
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.setCode(-2);
result.setDesc(msg);
return result;
}
}
java
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof String) {
return objectMapper.writeValueAsString(Result.success(body));
}
if(body instanceof Result) {
return body;
}
return Result.success(body);
}
}
A instanceof B 表示 A 是否由 B 向上转型过来的。
这里要单独处理 String 的数据类型,是因为 Spring 不会直接对 String 进行处理的:
SpringMVC默认会注册一些自带的 HttpMessageConverter (从先后顺序排列分别为
ByteArrayHttpMessageConverter, StringHttpMessageConverter, SourceHttpMessageConverter,
SourceHttpMessageConverter, AllEncompassingFormHttpMessageConverter )
其中AllEncompassingFormHttpMessageConverter 会根据项目依赖情况添加对应的 HttpMessageConverter
在依赖中引入jackson包后,容器会把 MappingJackson2HttpMessageConverter 自动注册到
messageConverters 链的末尾.
Spring会根据返回的数据类型,从 messageConverters 链选择合适的HttpMessageConverter .
当返回的数据是非字符串时, 使用的 MappingJackson2HttpMessageConverter 写入返回对象.
当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为
StringHttpMessageConverter 可以使用
如何解决:
- 注入依赖 ObjectMapper , Spring 已经帮我们实现好了,直接调用即可
- 标上注解 @SneakyThrows
- 使用 instanceof 关键字判断数据类型
- 对String 类型调用 writeValueAsString() 方法
统一异常
如果接口有些异常没有处理到,我们可以在外面封装一个类用来统一地处理异常的情况:
使用 @Controller 注解表示控制层的通知类
使用 @ResponseBody 表示 返回的是数据,而不是页面,如果不加的话,出现异常需要返回的就是页面,程序就可能会死循环找页面,导致栈溢出【当接口返回数据的时候,一定要加上 @ResponseBody 】
使用 @ExceptionHandle 注解表示异常处理器,加在方法上面就可以捕获和处理异常了。
代码示例:
第一种实现方式:在注解旁边加上你要捕获的异常的类型
java
@Slf4j
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public Result handle0(Exception e) {
log.error("发生异常, e", e);
e.printStackTrace();
return Result.error("内部错误,请联系管理员");
}
@ExceptionHandler(NullPointerException.class)
public Result handle(Exception e) {
log.error("发生空指针异常", e);
return Result.error("内部错误,请联系管理员");
}
@ExceptionHandler(IndexOutOfBoundsException.class)
public Result handle2(Exception e) {
log.error("发生数组越界异常", e);
return Result.error("内部错误,请联系管理员");
}
}
第二种实现方式是在方法的参数写上你要捕获的异常类型
java
@Slf4j
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler
public Result handle0(Exception e) {
log.error("发生异常, e", e);
e.printStackTrace();
return Result.error("内部错误,请联系管理员");
}
@ExceptionHandler
public Result handle(NullPointerException e) {
log.error("发生空指针异常", e);
return Result.error("内部错误,请联系管理员");
}
@ExceptionHandler
public Result handle2(IndexOutOfBoundsException e) {
log.error("发生数组越界异常", e);
return Result.error("内部错误,请联系管理员");
}
}
上面你会发现 Exception 是 NullPointerException 和 IndexOutOfBoundsException 的父类,但是我们捕获到的异常会优先交给子类处理,然后才是父类,也就是说父类只是用来保底的,避免其他异常没有被捕获到。