Spring 拦截器与统一功能的处理

拦截器

在设计网站的时候,一般都会有一个登录的页面需要用户去填写,但是我们后端的接口出来登录接口之外,还有其他的接口,如果用户没有按常规的套路先登录在浏览网站,而是直接访问网站内容的话,我们就需要先判断用户是否处于登录状态,如果不是就需要强制用户去登录(跳转到登录页面)

强制登录功能的代码如果每个接口都去写的话,就会很麻烦,有没有更加简单的操作就可以实现用户强制登录的功能呢?有的兄弟,有的。

就是Spring 给我们提供了一个拦截器的功能,拦截器用于拦截 url 的请求,如果符合通行情况就放行,如果不符合通行情况就进行拦截,即用户无法访问后面的接口。


拦截器的使用:

  1. 定义拦截器
  2. 注册拦截器

定义拦截器,首先需要 绑定接口 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 可以使用

如何解决:

  1. 注入依赖 ObjectMapper , Spring 已经帮我们实现好了,直接调用即可
  2. 标上注解 @SneakyThrows
  3. 使用 instanceof 关键字判断数据类型
  4. 对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 的父类,但是我们捕获到的异常会优先交给子类处理,然后才是父类,也就是说父类只是用来保底的,避免其他异常没有被捕获到。

相关推荐
橘猫云计算机设计几秒前
基于django优秀少儿图书推荐网(源码+lw+部署文档+讲解),源码可白嫖!
java·spring boot·后端·python·小程序·django·毕业设计
黑猫Teng4 分钟前
Spring Boot拦截器(Interceptor)与过滤器(Filter)深度解析:区别、实现与实战指南
java·spring boot·后端
小智疯狂敲代码6 分钟前
Java架构师成长之路-框架源码系列-整体认识Spring体系结构(1)
后端
星河浪人10 分钟前
Spring Boot启动流程及源码实现深度解析
java·spring boot·后端
佩奇的技术笔记11 分钟前
中级:Maven面试题精讲
java·面试·maven
Lizhihao_22 分钟前
JAVA-堆 和 堆排序
java·开发语言
极客先躯27 分钟前
高级java每日一道面试题-2025年3月21日-微服务篇[Nacos篇]-什么是Nacos?
java·开发语言·微服务
工业互联网专业36 分钟前
基于springboot+vue的动漫交流与推荐平台
java·vue.js·spring boot·毕业设计·源码·课程设计·动漫交流与推荐平台
雷渊39 分钟前
深入分析Spring的事务隔离级别及实现原理
java·后端·面试
Smilejudy1 小时前
不可或缺的相邻引用
后端