百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

告别繁琐:SpringBoot 拦截器与统一功能处理

ztj100 2024-12-15 18:00 26 浏览 0 评论

前言

Spring AOP是一个基于面向切面编程的框架,用于将横切性关注点(如日志记录、事务管理)与业务逻辑分离,通过代理对象将这些关注点织入到目标对象的方法执行前后、抛出异常或返回结果时等特定位置执行,从而提高程序的可复用性、可维护性和灵活性。

但使用原生Spring AOP实现统一的拦截是非常繁琐、困难的。而在本节,我们将使用一种简单的方式进行统一功能处理,这也是AOP的一次实战,具体如下:

  • 统一用户登录权限验证
  • 统一数据格式返回
  • 统一异常处理

基于 Spring Boot + MyBatis Plus + Vue 3.2 + Vite + Element Plus 实现的前后端分离博客,包含后台管理系统,支持文章、分类、标签管理、仪表盘等功能。

GitHub 地址:https://github.com/weiwosuoai/WeBlog

Gitee 地址:https://gitee.com/AllenJiang/WeBlog

0 为什么需要统一功能处理?

统一功能处理是为了提高代码的可维护性、可重用性和可扩展性而进行的一种设计思想。在应用程序中,可能存在一些通用的功能需求,例如身份验证、日志记录、异常处理等。

这些功能需要在多个地方进行调用和处理,如果每个地方都单独实现这些功能,会导致代码冗余、难以维护和重复劳动。通过统一功能处理的方式,可以将这些通用功能抽取出来,以统一的方式进行处理。这样做有以下几个好处:

  • 「代码复用」:将通用功能抽取成独立的模块或组件,可以在多个地方共享使用,减少重复编写代码的工作量。
  • 「可维护性」:将通用功能集中处理,可以方便地对其进行修改、优化或扩展,而不需要在多个地方进行修改。
  • 「代码整洁性」:通过统一功能处理,可以使代码更加清晰、简洁,减少了冗余的代码。
  • 「可扩展性」:当需要添加新的功能时,只需要在统一功能处理的地方进行修改或扩展,而不需要在多个地方进行修改,降低了代码的耦合度。

1 统一用户登录权限验证

1.1 使用原生 Spring AOP 实现统一拦截的难点

以使用原生 Spring AOP 来实现?户统?登录验证为例,主要是使用前置通知和环绕通知实现的,具体实现如下

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/18 16:37
 */
@Aspect // 表明此类为一个切面
@Component // 随着框架的启动而启动
public class UserAspect {
    // 定义切点, 这里使用 Aspect 表达式语法
    @Pointcut("execution(* com.hxh.demo.controller.UserController.*(..))")
    public void pointcut(){ }


    // 前置通知
    @Before("pointcut()")
    public void beforeAdvice() {
        System.out.println("执行了前置通知~");
    }

    // 环绕通知
    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
        System.out.println("进入环绕通知~");
        Object obj = null;
        // 执行目标方法
        try {
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("退出环绕通知~");
        return obj;
    }

}

从上述的代码示例可以看出,使用原生的 Spring AOP 实现统一拦截的难点主要有以下几个方面:

  • 定义拦截规则非常困难。如注册?法和登录?法是不拦截的,这样的话排除?法的规则很难定义,甚?没办法定义。
  • 在切面类中拿到 HttpSession 比较难。

为了解决 Spring AOP 的这些问题,Spring 提供了拦截器~

1.2 使用 Spring 拦截器实现统一用户登录验证

Spring拦截器是Spring框架提供的一个功能强大的组件,用于在请求到达控制器之前或之后进行拦截和处理。拦截器可以用于实现各种功能,如身份验证、日志记录、性能监测等。

要使用Spring拦截器,需要创建一个实现了HandlerInterceptor接口的拦截器类。该接口定义了三个方法:preHandle、postHandle和afterCompletion。

  • preHandle方法在请求到达控制器之前执行,可以用于进行身份验证、参数校验等;
  • postHandle方法在控制器处理完请求后执行,可以对模型和视图进行操作;
  • afterCompletion方法在视图渲染完成后执行,用于清理资源或记录日志。

拦截器的实现可以分为以下两个步骤:

  • 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法。
  • 将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中,并且设置拦截规则。

具体实现如下:

step1. 创建自定义拦截器,自定义拦截器是一个普通类,代码如下:

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 16:31
 * 统一用户登录权限验证 —— 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 用户登录业务判断
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            return true; // 验证成功, 继续controller的流程
        }
        // 可以跳转登录界面或者返回 401/403 没有权限码
        response.sendRedirect("/login.html"); // 跳转到登录页面
        return false; // 验证失败
    }
}

step2. 配置拦截器并设置拦截规则,代码如下:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 16:51
 */
@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/user/login") // 不拦截的 url 地址
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/**/*.html"); // 不拦截所有页面
    }
}

1.3 拦截器的实现原理及源码分析

当有了拦截器后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图所示:

「拦截器实现原理的源码分析」

从上述案例实现结果的控制台的日志信息可以看出,所有的 Controller 执?都会通过?个调度器 DispatcherServlet 来实现。

而所有的方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,doDispatch 源码如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;
            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    this.noHandlerFound(processedRequest, response);
                    return;
                }
                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                String method = request.getMethod();
                boolean isGet = HttpMethod.GET.matches(method);
                if (isGet || HttpMethod.HEAD.matches(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
                
                // 调用预处理
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
                // 执行 Controller 中的业务
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                this.applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }
    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            this.cleanupMultipart(processedRequest);
        }
    }
}

从上述源码可以看出,在执行 Controller 之前,先会调用 预处理方法 applyPreHandle,该方法源码如下:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
    // 获取项目中使用的拦截器 HandlerInterceptor
        HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
        if (!interceptor.preHandle(request, response, this.handler)) {
            this.triggerAfterCompletion(request, response, (Exception)null);
            return false;
        }
    }
    return true;
}

在上述源码中,可以看出,在 applyPreHandle 中会获取所有拦截器 HandlerInterceptor 并执行拦截器中的 preHandle 方法,这与之前我们实现拦截器的步骤对应,如下图所示:

此时,相应的preHandle中的业务逻辑就会执行。

1.4 统一访问前缀添加

统一访问前缀的添加与登录拦截器实现类似,即给所有请求地址添加 /hxh 前缀,示例代码如下:

@Configuration
public class AppConfig implements WebMvcConfigurer {
    // 给所有接口添加 /hxh 前缀
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("/hxh", c -> true);
    }
}

另一种方式是在application配置文件中配置:

server.servlet.context-path=/hxh

2 统一异常处理

统一异常处理是指 在应用程序中定义一个公共的异常处理机制,用来处理所有的异常情况。这样可以避免在应用程序中分散地处理异常,降低代码的复杂度和重复度,提高代码的可维护性和可扩展性。

基于 Spring Boot + MyBatis Plus + Vue 3.2 + Vite + Element Plus 实现的前后端分离博客,包含后台管理系统,支持文章、分类、标签管理、仪表盘等功能。

GitHub 地址:https://github.com/weiwosuoai/WeBlog

Gitee 地址:https://gitee.com/AllenJiang/WeBlog

需要考虑以下几点:

  • 异常处理的层次结构:定义异常处理的层次结构,确定哪些异常需要统一处理,哪些异常需要交给上层处理。
  • 异常处理的方式:确定如何处理异常,比如打印日志、返回错误码等。
  • 异常处理的细节:处理异常时需要注意的一些细节,比如是否需要事务回滚、是否需要释放资源等

本文讲述的统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的:

  • @ControllerAdvice 表示控制器通知类。
  • @ExceptionHandler 异常处理器。

以上两个注解组合使用,表示当出现异常的时候执行某个通知,即执行某个方法事件,具体实现代码如下:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:27
 * 统一异常处理
 */
@ControllerAdvice // 声明是一个异常处理器
public class MyExHandler {

    // 拦截所有的空指针异常, 进行统一的数据返回
    @ExceptionHandler(NullPointerException.class) // 统一处理空指针异常
    @ResponseBody // 返回数据
    public HashMap<String, Object> nullException(NullPointerException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", "-1"); // 与前端定义好的异常状态码
        result.put("msg", "空指针异常: " + e.getMessage()); // 错误码的描述信息
        result.put("data", null); // 返回的数据
        return result;
    }
}

上述代码中,实现了对所有空指针异常的拦截并进行统一的数据返回。

在实际中,常常设置一个保底,比如发生的非空指针异常,也会有保底措施进行处理,类似于 try-catch 块中使用 Exception 进行捕获,代码示例如下:

@ExceptionHandler(Exception.class) 
@ResponseBody 
public HashMap<String, Object> exception(Exception e) {
    HashMap<String, Object> result = new HashMap<>();
    result.put("code", "-1"); // 与前端定义好的异常状态码
    result.put("msg", "异常: " + e.getMessage()); // 错误码的描述信息
    result.put("data", null); // 返回的数据
    return result;
}

3 统一数据返回格式

为了保持 API 的一致性和易用性,通常需要使用统一的数据返回格式。一般而言,一个标准的数据返回格式应该包括以下几个元素:

  • 状态码:用于标志请求成功失败的状态信息;
  • 消息:用来描述请求状态的具体信息;
  • 数据:包含请求的数据信息;
  • 时间戳:可以记录请求的时间信息,便于调试和监控。

实现统一的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式实现,具体步骤如下:

  • 创建一个类,并添加 @ControllerAdvice 注解;
  • 实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyWrite 方法。

示例代码如下:

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.HashMap;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:59
 * 统一数据返回格式
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    /**
     * 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    /**
     * 方法返回之前调用此方法
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "");
        result.put("data", body);
        return null;
    }
}

但是,如果返回的 body 原始数据类型是 String ,则会出现类型转化异常,即 ClassCastException。

因此,如果原始返回数据类型为 String ,则需要使用 jackson 进行单独处理,实现代码如下:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.HashMap;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:59
 * 统一数据返回格式
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    /**
     * 方法返回之前调用此方法
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "");
        result.put("data", body);
        if (body instanceof String) {
            // 需要对 String 特殊处理
            try {
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}

但是,在实际业务中,上述代码只是作为保底使用,因为状态码始终返回的是200,过于死板,还需要具体问题具体分析。

来源:blog.csdn.net/m0_60353039/

article/details/131810657

相关推荐

sharding-jdbc实现`分库分表`与`读写分离`

一、前言本文将基于以下环境整合...

三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么

在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...

MySQL8行级锁_mysql如何加行级锁

MySQL8行级锁版本:8.0.34基本概念...

mysql使用小技巧_mysql使用入门

1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...

MySQL/MariaDB中如何支持全部的Unicode?

永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...

聊聊 MySQL Server 可执行注释,你懂了吗?

前言MySQLServer当前支持如下3种注释风格:...

MySQL系列-源码编译安装(v5.7.34)

一、系统环境要求...

MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了

对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...

MySQL字符问题_mysql中字符串的位置

中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...

深圳尚学堂:mysql基本sql语句大全(三)

数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...

MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?

大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...

一文讲清怎么利用Python Django实现Excel数据表的导入导出功能

摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...

用DataX实现两个MySQL实例间的数据同步

DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...

MySQL数据库知识_mysql数据库基础知识

MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...

如何为MySQL中的JSON字段设置索引

背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...

取消回复欢迎 发表评论: