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

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

3天内不再提示

SpringBoot统一功能处理

jf_ro2CN3Fa 来源:CSDN 2023-04-19 14:51 次阅读

本篇将要学习 Spring Boot 统一功能处理模块,这也是 AOP 的实战环节

用户登录权限的校验实现接口 HandlerInterceptor + WebMvcConfigurer

异常处理使用注解 @RestControllerAdvice + @ExceptionHandler

数据格式返回使用注解 @ControllerAdvice 并且实现接口 @ResponseBodyAdvice

1. 统一用户登录权限效验

用户登录权限的发展完善过程

最初用户登录效验: 在每个方法中获取 Session 和 Session 中的用户信息,如果存在用户,那么就认为登录成功了,否则就登录失败了

第二版用户登录效验: 提供统一的方法,在每个需要验证的方法中调用统一的用户登录身份效验方法来判断

第三版用户登录效验: 使用 Spring AOP 来统一进行用户登录效验

第四版用户登录效验: 使用 Spring 拦截器来实现用户的统一登录验证

1.1 最初用户登录权限效验

@RestController
@RequestMapping("/user")
publicclassUserController{

@RequestMapping("/a1")
publicBooleanlogin(HttpServletRequestrequest){
//有Session就获取,没有就不创建
HttpSessionsession=request.getSession(false);
if(session!=null&&session.getAttribute("userinfo")!=null){
//说明已经登录,进行业务处理
returntrue;
}else{
//未登录
returnfalse;
}
}

@RequestMapping("/a2")
publicBooleanlogin2(HttpServletRequestrequest){
//有Session就获取,没有就不创建
HttpSessionsession=request.getSession(false);
if(session!=null&&session.getAttribute("userinfo")!=null){
//说明已经登录,进行业务处理
returntrue;
}else{
//未登录
returnfalse;
}
}
}

这种方式写的代码,每个方法中都有相同的用户登录验证权限,缺点是:

每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也一样要传参调用和在方法中进行判断

添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成功和维护成功

这些用户登录验证的方法和现在要实现的业务几乎没有任何关联,但还是要在每个方法中都要写一遍,所以提供一个公共的 AOP 方法来进行统一的用户登录权限验证是非常好的解决办法。

1.2 Spring AOP 统一用户登录验证

统一用户登录验证,首先想到的实现方法是使用 Spring AOP 前置通知或环绕通知来实现

@Aspect//当前类是一个切面
@Component
publicclassUserAspect{
//定义切点方法Controller包下、子孙包下所有类的所有方法
@Pointcut("execution(*com.example.springaop.controller..*.*(..))")
publicvoidpointcut(){}

//前置通知
@Before("pointcut()")
publicvoiddoBefore(){}

//环绕通知
@Around("pointcut()")
publicObjectdoAround(ProceedingJoinPointjoinPoint){
Objectobj=null;
System.out.println("Around方法开始执行");
try{
obj=joinPoint.proceed();
}catch(Throwablee){
e.printStackTrace();
}
System.out.println("Around方法结束执行");
returnobj;
}
}

但如果只在以上代码 Spring AOP 的切面中实现用户登录权限效验的功能,有这样两个问题:

没有办法得到 HttpSession 和 Request 对象

我们要对一部分方法进行拦截,而另一部分方法不拦截,比如注册方法和登录方法是不拦截的,也就是实际的拦截规则很复杂,使用简单的 aspectJ 表达式无法满足拦截的需求

1.3 Spring 拦截器

针对上面代码 Spring AOP 的问题,Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现有两步:

1.创建自定义拦截器,实现 Spring 中的 HandlerInterceptor 接口中的 preHandle方法

2.将自定义拦截器加入到框架的配置中,并且设置拦截规则

给当前的类添加 @Configuration 注解

实现 WebMvcConfigurer 接口

重写 addInterceptors 方法

注意:一个项目中可以同时配置多个拦截器

(1)创建自定义拦截器

/**
*@Description:自定义用户登录的拦截器
*@Date2023/2/1313:06
*/
@Component
publicclassLoginInterceptimplementsHandlerInterceptor{
//返回true表示拦截判断通过,可以访问后面的接口
//返回false表示拦截未通过,直接返回结果给前端
@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,
Objecthandler)throwsException{
//1.得到HttpSession对象
HttpSessionsession=request.getSession(false);
if(session!=null&&session.getAttribute("userinfo")!=null){
//表示已经登录
returntrue;
}
//执行到此代码表示未登录,未登录就跳转到登录页面
response.sendRedirect("/login.html");
returnfalse;
}
}

(2)将自定义拦截器添加到系统配置中,并设置拦截的规则

addPathPatterns:表示需要拦截的 URL,**表示拦截所有⽅法

excludePathPatterns:表示需要排除的 URL

说明:拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)。

/**
*@Description:将自定义拦截器添加到系统配置中,并设置拦截的规则
*@Date2023/2/1313:13
*/
@Configuration
publicclassAppConfigimplementsWebMvcConfigurer{

@Resource
privateLoginInterceptloginIntercept;

@Override
publicvoidaddInterceptors(InterceptorRegistryregistry){
//registry.addInterceptor(newLoginIntercept());//可以直接new也可以属性注入
registry.addInterceptor(loginIntercept).
addPathPatterns("/**").//拦截所有url
excludePathPatterns("/user/login").//不拦截登录注册接口
excludePathPatterns("/user/reg").
excludePathPatterns("/login.html").
excludePathPatterns("/reg.html").
excludePathPatterns("/**/*.js").
excludePathPatterns("/**/*.css").
excludePathPatterns("/**/*.png").
excludePathPatterns("/**/*.jpg");
}
}

1.4 练习:登录拦截器

要求

登录、注册页面不拦截,其他页面都拦截

当登录成功写入 session 之后,拦截的页面可正常访问

在 1.3 中已经创建了自定义拦截器 和 将自定义拦截器添加到系统配置中,并设置拦截的规则

(1)下面创建登录和首页的 html

38026caa-de7c-11ed-bfe3-dac502259ad0.png

(2)创建 controller 包,在包中创建 UserController,写登录页面和首页的业务代码

@RestController
@RequestMapping("/user")
publicclassUserController{

@RequestMapping("/login")
publicbooleanlogin(HttpServletRequestrequest,Stringusername,Stringpassword){
booleanresult=false;
if(StringUtils.hasLength(username)&&StringUtils.hasLength(password)){
if(username.equals("admin")&&password.equals("admin")){
HttpSessionsession=request.getSession();
session.setAttribute("userinfo","userinfo");
returntrue;
}
}
returnresult;
}

@RequestMapping("/index")
publicStringindex(){
return"HelloIndex";
}
}

(3)运行程序,访问页面,对比登录前和登录后的效果

3811586e-de7c-11ed-bfe3-dac502259ad0.png381bb926-de7c-11ed-bfe3-dac502259ad0.png

1.5 拦截器实现原理

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

382522e0-de7c-11ed-bfe3-dac502259ad0.png

实现原理源码分析

所有的 Controller 执行都会通过一个调度器 DispatcherServlet 来实现

382c5cd6-de7c-11ed-bfe3-dac502259ad0.png

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

3833f4a0-de7c-11ed-bfe3-dac502259ad0.png

通过源码分析,可以看出,Sping 中的拦截器也是通过动态代理和环绕通知的思想实现的

1.6 统一访问前缀添加

所有请求地址添加 api 前缀,c 表示所有

@Configuration
publicclassAppConfigimplementsWebMvcConfigurer{
//所有的接口添加api前缀
@Override
publicvoidconfigurePathMatch(PathMatchConfigurerconfigurer){
configurer.addPathPrefix("api",c->true);
}
}
383ead96-de7c-11ed-bfe3-dac502259ad0.png

2. 统一异常处理

给当前的类上加 @ControllerAdvice 表示控制器通知类

给方法上添加 @ExceptionHandler(xxx.class),表示异常处理器,添加异常返回的业务代码

@RestController
@RequestMapping("/user")
publicclassUserController{
@RequestMapping("/index")
publicStringindex(){
intnum=10/0;
return"HelloIndex";
}
}

在 config 包中,创建 MyExceptionAdvice 类

@RestControllerAdvice//当前是针对Controller的通知类(增强类)
publicclassMyExceptionAdvice{
@ExceptionHandler(ArithmeticException.class)
publicHashMaparithmeticExceptionAdvice(ArithmeticExceptione){
HashMapresult=newHashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg","算出异常:"+e.getMessage());
returnresult;
}
}

也可以这样写,效果是一样的

@ControllerAdvice
publicclassMyExceptionAdvice{
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
publicHashMaparithmeticExceptionAdvice(ArithmeticExceptione){
HashMapresult=newHashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg","算数异常:"+e.getMessage());
returnresult;
}
}
38442406-de7c-11ed-bfe3-dac502259ad0.png

如果再有一个空指针异常,那么上面的代码是不行的,还要写一个针对空指针异常处理器

@ExceptionHandler(NullPointerException.class)
publicHashMapnullPointerExceptionAdvice(NullPointerExceptione){
HashMapresult=newHashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg","空指针异常异常:"+e.getMessage());
returnresult;
}
@RequestMapping("/index")
publicStringindex(HttpServletRequestrequest,Stringusername,Stringpassword){
Objectobj=null;
System.out.println(obj.hashCode());
return"HelloIndex";
}
38495d86-de7c-11ed-bfe3-dac502259ad0.png

但是需要考虑的一点是,如果每个异常都这样写,那么工作量是非常大的,并且还有自定义异常,所以上面这样写肯定是不好的,既然是异常直接写 Exception 就好了,它是所有异常的父类,如果遇到不是前面写的两种异常,那么就会直接匹配到 Exception

当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配

@ExceptionHandler(Exception.class)
publicHashMapexceptionAdvice(Exceptione){
HashMapresult=newHashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg","异常:"+e.getMessage());
returnresult;
}

可以看到优先匹配的还是前面写的 空指针异常

3852b372-de7c-11ed-bfe3-dac502259ad0.png

3. 统一数据格式返回

3.1 统一数据格式返回的实现

1.给当前类添加 @ControllerAdvice

2.实现 ResponseBodyAdvice 重写其方法

supports 方法,此方法表示内容是否需要重写(通过此⽅法可以选择性部分控制器和方法进行重写),如果要重写返回 true

beforeBodyWrite 方法,方法返回之前调用此方法

@ControllerAdvice
publicclassMyResponseAdviceimplementsResponseBodyAdvice{

//返回一个boolean值,true表示返回数据之前对数据进行重写,也就是会进入beforeBodyWrite方法
//返回false表示对结果不进行任何处理,直接返回
@Override
publicbooleansupports(MethodParameterreturnType,ClassconverterType){
returntrue;
}

//方法返回之前调用此方法
@Override
publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){
HashMapresult=newHashMap<>();
result.put("state",1);
result.put("data",body);
result.put("msg","");
returnresult;
}
}
@RestController
@RequestMapping("/user")
publicclassUserController{

@RequestMapping("/login")
publicbooleanlogin(HttpServletRequestrequest,Stringusername,Stringpassword){
booleanresult=false;
if(StringUtils.hasLength(username)&&StringUtils.hasLength(password)){
if(username.equals("admin")&&password.equals("admin")){
HttpSessionsession=request.getSession();
session.setAttribute("userinfo","userinfo");
returntrue;
}
}
returnresult;
}

@RequestMapping("/reg")
publicintreg(){
return1;
}
}
385dbbd2-de7c-11ed-bfe3-dac502259ad0.png

3.2 @ControllerAdvice 源码分析

通过对 @ControllerAdvice 源码的分析我们可以知道上面统一异常和统一数据返回的执行流程

(1)先看 @ControllerAdvice 源码

386660ac-de7c-11ed-bfe3-dac502259ad0.png

可以看到 @ControllerAdvice 派生于 @Component 组件而所有组件初始化都会调用 InitializingBean 接口

(2)下面查看 initializingBean 有哪些实现类

在查询过程中发现,其中 Spring MVC 中的实现子类是 RequestMappingHandlerAdapter,它里面有一个方法 afterPropertiesSet()方法,表示所有的参数设置完成之后执行的方法

386cc3e8-de7c-11ed-bfe3-dac502259ad0.png

(3)而这个方法中有一个 initControllerAdviceCache 方法,查询此方法

3877f98e-de7c-11ed-bfe3-dac502259ad0.png

发现这个方法在执行时会查找使用所有的 @ControllerAdvice 类,发送某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装,比如发生异常是调用异常的 Advice 方法实现的。






审核编辑:刘清

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

    关注

    112

    文章

    15871

    浏览量

    175299
  • URL
    URL
    +关注

    关注

    0

    文章

    138

    浏览量

    15210
  • CSS
    CSS
    +关注

    关注

    0

    文章

    108

    浏览量

    14291
  • API接口
    +关注

    关注

    1

    文章

    81

    浏览量

    10398
  • SpringBoot
    +关注

    关注

    0

    文章

    172

    浏览量

    145

原文标题:SpringBoot 统一功能处理:用户登录权限校验-拦截器、异常处理、数据格式返回

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

收藏 人收藏

    评论

    相关推荐

    SpringBoot中的Druid介绍

    SpringBoot中Druid数据源配置
    发表于 05-07 09:21

    SpringBoot知识总结

    SpringBoot干货学习总结
    发表于 08-01 10:40

    怎么学习SpringBoot

    SpringBoot学习之路(X5)- 整合JPA
    发表于 06-10 14:52

    springboot集成mqtt

    springboot集成mqtt,大纲.数据入库1.数据入库解决方案二.开发实时订阅发布展示页面1.及时通讯技术2.技术整合
    发表于 07-16 07:53

    怎样去使用springboot

    怎样去使用springboot呢?学习springboot需要懂得哪些?
    发表于 10-25 07:13

    SpringBoot应用启动运行run方法

    )、refreshContext(context);SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器中的每个组件】;如果是web应用创建**AnnotationConfigEmbeddedWebApplicationContext**,否则
    发表于 12-20 06:16

    怎样去设计个基于springboot+freemark+jpa+MySQL的在线电影订票系统

    本系统是由springboot+freemark+jpa+MySQL实现的在线电影订票系统,主要的亮点功能有:支持短信发送接口、支付宝在线支付接口、座位锁定及并发处理、排片时间冲突检测等。本系统主要
    发表于 01-03 07:22

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

    源码剖析Springboot是如何获取自定义异常并进行返回的。来吧!第步:肯定是在Springboot启动的过程中进行的异常处理初始化,于是就找到了handlerExceptionR
    发表于 03-22 14:15

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

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

    什么是 SpringBoot

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

    SpringBoot的核心注解1

    今天跟大家来探讨下SpringBoot的核心注解@SpringBootApplication以及run方法,理解下springBoot为什么不需要XML,达到零配置
    的头像 发表于 04-07 14:34 606次阅读
    <b class='flag-5'>SpringBoot</b>的核心注解1

    SpringBoot的核心注解2

    今天跟大家来探讨下SpringBoot的核心注解@SpringBootApplication以及run方法,理解下springBoot为什么不需要XML,达到零配置
    的头像 发表于 04-07 14:34 1868次阅读
    <b class='flag-5'>SpringBoot</b>的核心注解2

    使用springboot完成流程的业务功能

    使用springboot开发流程使用的接口完成流程的业务功能 基于 Spring Boot + MyBatis Plus + Vue Element 实现的后台管理系统 + 用户小程序,支持 RBAC
    的头像 发表于 05-15 17:40 595次阅读
    使用<b class='flag-5'>springboot</b>完成流程的业务<b class='flag-5'>功能</b>

    springboot统一异常处理

    限流对于个微服务架构系统来说具有非常重要的意义,否则其中的某个微服务将成为整个系统隐藏的雪崩因素,为什么这么说?
    的头像 发表于 07-25 16:11 545次阅读
    <b class='flag-5'>springboot</b><b class='flag-5'>统一</b>异常<b class='flag-5'>处理</b>

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

    Spring AOP是个基于面向切面编程的框架,用于将横切性关注点(如日志记录、事务管理)与业务逻辑分离,通过代理对象将这些关注点织入到目标对象的方法执行前后、抛出异常或返回结果时等特定位置执行,从而提高程序的可复用性、可维护性和灵活性。
    的头像 发表于 08-27 10:44 773次阅读
    <b class='flag-5'>SpringBoot</b>拦截器与<b class='flag-5'>统一</b><b class='flag-5'>功能</b><b class='flag-5'>处理</b>实战