0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SpringBoot拦截器与统一功能处理实战

jf_ro2CN3Fa 来源:CSDN 2023-08-27 10:44 次阅读

前言

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

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

统一用户登录权限验证

统一数据格式返回

统一异常处理

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

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

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

「代码复用」 :将通用功能抽取成独立的模块或组件,可以在多个地方共享使用,减少重复编写代码的工作量。

「可维护性」 :将通用功能集中处理,可以方便地对其进行修改、优化或扩展,而不需要在多个地方进行修改。

「代码整洁性」 :通过统一功能处理,可以使代码更加清晰、简洁,减少了冗余的代码。

「可扩展性」 :当需要添加新的功能时,只需要在统一功能处理的地方进行修改或扩展,而不需要在多个地方进行修改,降低了代码的耦合度。

1 统一用户登录权限验证

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

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

importorg.aspectj.lang.ProceedingJoinPoint;
importorg.aspectj.lang.annotation.*;
importorg.springframework.stereotype.Component;

/**
*@author兴趣使然黄小黄
*@version1.0
*@date2023/7/1816:37
*/
@Aspect//表明此类为一个切面
@Component//随着框架的启动而启动
publicclassUserAspect{
//定义切点,这里使用Aspect表达式语法
@Pointcut("execution(*com.hxh.demo.controller.UserController.*(..))")
publicvoidpointcut(){}


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

//环绕通知
@Around("pointcut()")
publicObjectaroundAdvice(ProceedingJoinPointjoinPoint){
System.out.println("进入环绕通知~");
Objectobj=null;
//执行目标方法
try{
obj=joinPoint.proceed();
}catch(Throwablee){
e.printStackTrace();
}
System.out.println("退出环绕通知~");
returnobj;
}

}

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

定义拦截规则非常困难。如注册⽅法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。

在切面类中拿到 HttpSession 比较难。

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

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

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

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

preHandle方法在请求到达控制器之前执行,可以用于进行身份验证、参数校验等;

postHandle方法在控制器处理完请求后执行,可以对模型和视图进行操作;

afterCompletion方法在视图渲染完成后执行,用于清理资源或记录日志。

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

创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法。

将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中,并且设置拦截规则。

具体实现如下:

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

importorg.springframework.web.servlet.HandlerInterceptor;

importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;
importjavax.servlet.http.HttpSession;

/**
*@author兴趣使然黄小黄
*@version1.0
*@date2023/7/1916:31
*统一用户登录权限验证——登录拦截器
*/
publicclassLoginInterceptorimplementsHandlerInterceptor{
@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{
//用户登录业务判断
HttpSessionsession=request.getSession(false);
if(session!=null&&session.getAttribute("userinfo")!=null){
returntrue;//验证成功,继续controller的流程
}
//可以跳转登录界面或者返回401/403没有权限码
response.sendRedirect("/login.html");//跳转到登录页面
returnfalse;//验证失败
}
}

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

importorg.springframework.context.annotation.Configuration;
importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;
importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
*@author兴趣使然黄小黄
*@version1.0
*@date2023/7/1916:51
*/
@Configuration
publicclassAppConfigimplementsWebMvcConfigurer{
@Override
publicvoidaddInterceptors(InterceptorRegistryregistry){
registry.addInterceptor(newLoginInterceptor())
.addPathPatterns("/**")//拦截所有请求
.excludePathPatterns("/user/login")//不拦截的url地址
.excludePathPatterns("/user/reg")
.excludePathPatterns("/**/*.html");//不拦截所有页面
}
}

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

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

8932c162-4482-11ee-a2ef-92fbcf53809c.png

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

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

8990fae8-4482-11ee-a2ef-92fbcf53809c.png

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

protectedvoiddoDispatch(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{
HttpServletRequestprocessedRequest=request;
HandlerExecutionChainmappedHandler=null;
booleanmultipartRequestParsed=false;
WebAsyncManagerasyncManager=WebAsyncUtils.getAsyncManager(request);
try{
try{
ModelAndViewmv=null;
ObjectdispatchException=null;
try{
processedRequest=this.checkMultipart(request);
multipartRequestParsed=processedRequest!=request;
mappedHandler=this.getHandler(processedRequest);
if(mappedHandler==null){
this.noHandlerFound(processedRequest,response);
return;
}
HandlerAdapterha=this.getHandlerAdapter(mappedHandler.getHandler());
Stringmethod=request.getMethod();
booleanisGet=HttpMethod.GET.matches(method);
if(isGet||HttpMethod.HEAD.matches(method)){
longlastModified=ha.getLastModified(request,mappedHandler.getHandler());
if((newServletWebRequest(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(Exceptionvar20){
dispatchException=var20;
}catch(Throwablevar21){
dispatchException=newNestedServletException("Handlerdispatchfailed",var21);
}
this.processDispatchResult(processedRequest,response,mappedHandler,mv,(Exception)dispatchException);
}catch(Exceptionvar22){
this.triggerAfterCompletion(processedRequest,response,mappedHandler,var22);
}catch(Throwablevar23){
this.triggerAfterCompletion(processedRequest,response,mappedHandler,newNestedServletException("Handlerprocessingfailed",var23));
}
}finally{
if(asyncManager.isConcurrentHandlingStarted()){
if(mappedHandler!=null){
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest,response);
}
}elseif(multipartRequestParsed){
this.cleanupMultipart(processedRequest);
}
}
}

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

booleanapplyPreHandle(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{
for(inti=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 方法,这与之前我们实现拦截器的步骤对应,如下图所示:

89dc818e-4482-11ee-a2ef-92fbcf53809c.png

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

1.4 统一访问前缀添加

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

@Configuration
publicclassAppConfigimplementsWebMvcConfigurer{
//给所有接口添加/hxh前缀
@Override
publicvoidconfigurePathMatch(PathMatchConfigurerconfigurer){
configurer.addPathPrefix("/hxh",c->true);
}
}

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

server.servlet.context-path=/hxh

2 统一异常处理

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

需要考虑以下几点:

异常处理的层次结构:定义异常处理的层次结构,确定哪些异常需要统一处理,哪些异常需要交给上层处理。

异常处理的方式:确定如何处理异常,比如打印日志、返回错误码等。

异常处理的细节:处理异常时需要注意的一些细节,比如是否需要事务回滚、是否需要释放资源等

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

@ControllerAdvice 表示控制器通知类。

@ExceptionHandler 异常处理器

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

importorg.springframework.web.bind.annotation.ControllerAdvice;
importorg.springframework.web.bind.annotation.ExceptionHandler;
importorg.springframework.web.bind.annotation.ResponseBody;

importjava.util.HashMap;

/**
*@author兴趣使然黄小黄
*@version1.0
*@date2023/7/1918:27
*统一异常处理
*/
@ControllerAdvice//声明是一个异常处理器
publicclassMyExHandler{

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

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

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

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

3 统一数据返回格式

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

状态码:用于标志请求成功失败的状态信息;

消息:用来描述请求状态的具体信息;

数据:包含请求的数据信息;

时间戳:可以记录请求的时间信息,便于调试和监控。

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

创建一个类,并添加 @ControllerAdvice 注解;

实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyWrite 方法。

示例代码如下:

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

importjava.util.HashMap;

/**
*@author兴趣使然黄小黄
*@version1.0
*@date2023/7/1918:59
*统一数据返回格式
*/
@ControllerAdvice
publicclassResponseAdviceimplementsResponseBodyAdvice{

/**
*此方法返回true则执行下面的beforeBodyWrite方法,反之则不执行
*/
@Override
publicbooleansupports(MethodParameterreturnType,ClassconverterType){
returntrue;
}

/**
*方法返回之前调用此方法
*/
@Override
publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){
HashMapresult=newHashMap<>();
result.put("code",200);
result.put("msg","");
result.put("data",body);
returnnull;
}
}

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

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

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

importjava.util.HashMap;

/**
*@author兴趣使然黄小黄
*@version1.0
*@date2023/7/1918:59
*统一数据返回格式
*/
@ControllerAdvice
publicclassResponseAdviceimplementsResponseBodyAdvice{

@Autowired
privateObjectMapperobjectMapper;

/**
*此方法返回true则执行下面的beforeBodyWrite方法,反之则不执行
*/
@Override
publicbooleansupports(MethodParameterreturnType,ClassconverterType){
returntrue;
}

/**
*方法返回之前调用此方法
*/
@Override
publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){
HashMapresult=newHashMap<>();
result.put("code",200);
result.put("msg","");
result.put("data",body);
if(bodyinstanceofString){
//需要对String特殊处理
try{
returnobjectMapper.writeValueAsString(result);
}catch(JsonProcessingExceptione){
e.printStackTrace();
}
}
returnresult;
}
}

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






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 处理器
    +关注

    关注

    68

    文章

    19155

    浏览量

    229059
  • 控制器
    +关注

    关注

    112

    文章

    16185

    浏览量

    177342
  • 状态机
    +关注

    关注

    2

    文章

    492

    浏览量

    27470
  • SpringBoot
    +关注

    关注

    0

    文章

    173

    浏览量

    167

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

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    HarmonyOS实战开发-如何在Navigation中完成路由拦截

    路由拦截器interceptor.ets,定义拦截容器、注册方法和公共拦截逻辑,interceptor.ets /** * 定义拦截实现接口 * * @param routerI
    发表于 05-08 14:21

    [推荐]奥运全球眼 网络视频摄像机 电话报警系统 -电话报警 GSM防盗

    ........................................................ 10012.4.1 个生命周期回调事件上有多个回调拦截器方法...... 10112.4.2 异常
    发表于 07-07 15:39

    我想做个号码拦截器。面对面5米内接收到对方的手机号码。我也咨询很多人,不是技

    我想做个号码拦截器。面对面5米内接收到对方的手机号码。我也咨询很多人,不是技术问题就是,怕这东西触犯法律。我只是正规用途,并不会触犯法律底线!望“能人”解决我的问题!样品只要符合以上条件,重金酬谢...谢谢!QQ896776242加我请注明电子*** 丁先生
    发表于 04-29 16:16

    Springboot是如何获取自定义异常并进行返回的

    HandlerExceptionResolver这里呢?看下代码:走完初始化,经过过滤器,拦截器终于到了我们的请求方法,我们的方法还报错了,所以会走到异常中,我们DispatcherServlet会
    发表于 03-22 14:15

    网络组件axios可以在OpenHarmony上使用了

    拦截器也是如此功能,只是在请求得到响应之后,对响应体的处理,通常是数据统一处理等,也常来判断登录失效等。axios的
    发表于 08-29 12:11

    动能拦截器六自由度仿真建模研究

    仿真建模技术是动能拦截器制导律研究中的重要技术,文中主要建立动能拦截器的轨道运动动力学以及姿态运动动力学模型,并建立完整的制导控制系统数学模型。文末,以某型
    发表于 08-07 08:50 14次下载

    springmvc 自定义拦截器实现未登录用户的拦截

    springmvc自定义拦截器实现未登录用户的拦截
    发表于 11-25 14:44 2511次阅读
    springmvc 自定义<b class='flag-5'>拦截器</b>实现未登录用户的<b class='flag-5'>拦截</b>

    Spring Boot 系列(八)@ControllerAdvice 拦截异常并统一处理

    Spring Boot 系列(八)@ControllerAdvice 拦截异常并统一处理 在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义
    发表于 01-16 18:39 293次阅读

    快速定位SpringBoot接口超时问题的神器

    渠道系统是个常见的spring-boot web工程,使用了集成的tomcat。分析了代码之后,发现并没有特殊的地方,没有特殊的过滤器或者拦截器,所以初步排除是业务代码问题
    的头像 发表于 10-19 10:22 779次阅读

    公司这套架构统一处理try...catch真香!

    有大量的冗余代码,而且还影响代码的可读性。这样就需要定义个全局统一异常处理器,以便业务层再也不必处理异常。
    的头像 发表于 02-27 10:47 459次阅读

    核心功能具体的执行过程-2

    这篇我们主要讲解下 axios 中的 配置、拦截器和执行链等些核心的功能到底是怎么运行的。
    的头像 发表于 03-01 09:59 539次阅读
    核心<b class='flag-5'>功能</b>具体的执行过程-2

    什么是 SpringBoot

    本文从为什么要有 `SpringBoot`,以及 `SpringBoot` 到底方便在哪里开始入手,逐步分析了 `SpringBoot` 自动装配的原理,最后手写了个简单的 `sta
    的头像 发表于 04-07 11:28 1262次阅读
    什么是 <b class='flag-5'>SpringBoot</b>?

    SpringBoot统一功能处理

    最初用户登录效验: 在每个方法中获取 Session 和 Session 中的用户信息,如果存在用户,那么就认为登录成功了,否则就登录失败了
    的头像 发表于 04-19 14:51 625次阅读

    springboot过滤器和拦截器哪个先执行

    Spring Boot是个用于构建Java应用程序的开发框架,它提供了许多功能和工具来简化开发和部署过程。其中两个重要的功能是过滤器和拦截器。本文将详细介绍Spring Boot过滤
    的头像 发表于 12-03 15:00 2417次阅读

    使用go语言实现个grpc拦截器

    在开发grpc服务时,我们经常会遇到些通用的需求,比如:日志、链路追踪、鉴权等。这些需求可以通过grpc拦截器来实现。本文使用go语言来实现个 grpc元模式(Unary)
    的头像 发表于 12-18 10:13 630次阅读
    使用go语言实现<b class='flag-5'>一</b>个grpc<b class='flag-5'>拦截器</b>