SpringBoot 统一处理:登录校验-拦截器、异常处理、数据格式返回-焦点播报
本篇将要学习 Spring Boot 统一功能处理模块,这也是 AOP 的实战环节
【资料图】
用户登录权限的校验实现接口 HandlerInterceptor+ WebMvcConfigurer异常处理使用注解 @RestControllerAdvice+ @ExceptionHandler数据格式返回使用注解 @ControllerAdvice并且实现接口 @ResponseBodyAdvice1. 统一用户登录权限效验
用户登录权限的发展完善过程
最初用户登录效验:在每个方法中获取 Session 和 Session 中的用户信息,如果存在用户,那么就认为登录成功了,否则就登录失败了第二版用户登录校验:提供统一的方法,在每个需要验证的方法中调用统一的用户登录身份效验方法来判断第三版用户登录效验:使用 Spring AOP 来统一进行用户登录效验第四版用户登录效验:使用 Spring 拦截器来实现用户的统一登录验证1.1 最初用户登录权限效验
@RestController@RequestMapping("/user")public class UserController { @RequestMapping("/a1") public Boolean login (HttpServletRequest request) { // 有 Session 就获取,没有就不创建 HttpSession session = request.getSession(false); if (session != null && session.getAttribute("userinfo") != null) { // 说明已经登录,进行业务处理 return true; } else { // 未登录 return false; } } @RequestMapping("/a2") public Boolean login2 (HttpServletRequest request) { // 有 Session 就获取,没有就不创建 HttpSession session = request.getSession(false); if (session != null && session.getAttribute("userinfo") != null) { // 说明已经登录,进行业务处理 return true; } else { // 未登录 return false; } }}
这种方式写的代码,每个方法中都有相同的用户登录验证权限,缺点是:
每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也一样要传参调用和在方法中进行判断添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成功和维护成功这些用户登录验证的方法和现在要实现的业务几乎没有任何关联,但还是要在每个方法中都要写一遍,所以提供一个公共的 AOP 方法来进行统一的用户登录权限验证是非常好的解决办法。1.2 Spring AOP 统一用户登录验证
统一用户登录验证,首先想到的实现方法是使用 Spring AOP 前置通知或环绕通知来实现
@Aspect // 当前类是一个切面@Componentpublic class UserAspect { // 定义切点方法 Controller 包下、子孙包下所有类的所有方法 @Pointcut("execution(* com.example.springaop.controller..*.*(..))") public void pointcut(){} // 前置通知 @Before("pointcut()") public void doBefore() {} // 环绕通知 @Around("pointcut()") public Object doAround(ProceedingJoinPoint joinPoint) { Object obj = null; System.out.println("Around 方法开始执行"); try { obj = joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("Around 方法结束执行"); return obj; }}
但如果只在以上代码 Spring AOP 的切面中实现用户登录权限效验的功能,有这样两个问题:
没有办法得到 HttpSession和 Request 对象我们要对一部分方法进行拦截,而另一部分方法不拦截,比如注册方法和登录方法是不拦截的,也就是实际的拦截规则很复杂,使用简单的 aspectJ 表达式无法满足拦截的需求1.3 Spring 拦截器
针对上面代码 Spring AOP 的问题,Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现有两步:
1.创建自定义拦截器,实现 Spring 中的 HandlerInterceptor接口中的 preHandle方法
2.将自定义拦截器加入到框架的配置中,并且设置拦截规则
给当前的类添加 @Configuration注解实现 WebMvcConfigurer接口重写 addInterceptors方法注意:一个项目中可以同时配置多个拦截器
(1)创建自定义拦截器
/** * @Description: 自定义用户登录的拦截器 * @Date 2023/2/13 13:06 */@Componentpublic class LoginIntercept implements HandlerInterceptor { // 返回 true 表示拦截判断通过,可以访问后面的接口 // 返回 false 表示拦截未通过,直接返回结果给前端 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.得到 HttpSession 对象 HttpSession session = request.getSession(false); if (session != null && session.getAttribute("userinfo") != null) { // 表示已经登录 return true; } // 执行到此代码表示未登录,未登录就跳转到登录页面 response.sendRedirect("/login.html"); return false; }}
(2)将自定义拦截器添加到系统配置中,并设置拦截的规则
addPathPatterns:表示需要拦截的 URL,**表示拦截所有⽅法excludePathPatterns:表示需要排除的 URL说明:拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)。
/** * @Description: 将自定义拦截器添加到系统配置中,并设置拦截的规则 * @Date 2023/2/13 13:13 */@Configurationpublic class AppConfig implements WebMvcConfigurer { @Resource private LoginIntercept loginIntercept; @Override public void addInterceptors(InterceptorRegistry registry) {// registry.addInterceptor(new LoginIntercept());//可以直接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
(2)创建 controller包,在包中创建 UserController,写登录页面和首页的业务代码
@RestController@RequestMapping("/user")public class UserController { @RequestMapping("/login") public boolean login(HttpServletRequest request,String username, String password) { boolean result = false; if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) { if(username.equals("admin") && password.equals("admin")) { HttpSession session = request.getSession(); session.setAttribute("userinfo","userinfo"); return true; } } return result; } @RequestMapping("/index") public String index() { return "Hello Index"; }}
(3)运行程序,访问页面,对比登录前和登录后的效果
1.5 拦截器实现原理
有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图所示
实现原理源码分析
所有的 Controller执行都会通过一个调度器 DispatcherServlet来实现
而所有方法都会执行 DispatcherServlet中的 doDispatch调度⽅法,doDispatch源码分析如下:
通过源码分析,可以看出,Sping 中的拦截器也是通过动态代理和环绕通知的思想实现的
1.6 统一访问前缀添加
所有请求地址添加 api 前缀,c 表示所有
@Configurationpublic class AppConfig implements WebMvcConfigurer { // 所有的接口添加 api 前缀 @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.addPathPrefix("api", c -> true); }}
2. 统一异常处理
给当前的类上加 @ControllerAdvice表示控制器通知类
给方法上添加 @ExceptionHandler(xxx.class),表示异常处理器,添加异常返回的业务代码
@RestController@RequestMapping("/user")public class UserController { @RequestMapping("/index") public String index() { int num = 10/0; return "Hello Index"; }}
在 config 包中,创建 MyExceptionAdvice类
@RestControllerAdvice // 当前是针对 Controller 的通知类(增强类)public class MyExceptionAdvice { @ExceptionHandler(ArithmeticException.class) public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) { HashMap<String, Object> result = new HashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg" , "算出异常:"+ e.getMessage()); return result; }}
也可以这样写,效果是一样的
@ControllerAdvicepublic class MyExceptionAdvice { @ExceptionHandler(ArithmeticException.class) @ResponseBody public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) { HashMap<String, Object> result = new HashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg" , "算数异常:"+ e.getMessage()); return result; }}
如果再有一个空指针异常,那么上面的代码是不行的,还要写一个针对空指针异常处理器
@ExceptionHandler(NullPointerException.class)public HashMap<String,Object> nullPointerExceptionAdvice(NullPointerException e) { HashMap<String, Object> result = new HashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg" , "空指针异常异常:"+ e.getMessage()); return result;}@RequestMapping("/index")public String index(HttpServletRequest request,String username, String password) { Object obj = null; System.out.println(obj.hashCode()); return "Hello Index";}
但是需要考虑的一点是,如果每个异常都这样写,那么工作量是非常大的,并且还有自定义异常,所以上面这样写肯定是不好的,既然是异常直接写 Exception 就好了,它是所有异常的父类,如果遇到不是前面写的两种异常,那么就会直接匹配到 Exception
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配
@ExceptionHandler(Exception.class)public HashMap<String,Object> exceptionAdvice(Exception e) { HashMap<String, Object> result = new HashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg" , "异常:"+ e.getMessage()); return result;}
可以看到优先匹配的还是前面写的 空指针异常
3. 统一数据格式返回
3.1 统一数据格式返回的实现
1.给当前类添加 @ControllerAdvice
2.实现 ResponseBodyAdvice重写其方法
supports方法,此方法表示内容是否需要重写(通过此⽅法可以选择性部分控制器和方法进行重写),如果要重写返回 truebeforeBodyWrite方法,方法返回之前调用此方法@ControllerAdvicepublic class MyResponseAdvice implements ResponseBodyAdvice { // 返回一个 boolean 值,true 表示返回数据之前对数据进行重写,也就是会进入 beforeBodyWrite 方法 // 返回 false 表示对结果不进行任何处理,直接返回 @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("state",1); result.put("data",body); result.put("msg",""); return result; }}@RestController@RequestMapping("/user")public class UserController { @RequestMapping("/login") public boolean login(HttpServletRequest request,String username, String password) { boolean result = false; if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) { if(username.equals("admin") && password.equals("admin")) { HttpSession session = request.getSession(); session.setAttribute("userinfo","userinfo"); return true; } } return result; } @RequestMapping("/reg") public int reg() { return 1; }}
3.2 @ControllerAdvice 源码分析
通过对 @ControllerAdvice源码的分析我们可以知道上面统一异常和统一数据返回的执行流程
(1)先看 @ControllerAdvice 源码
可以看到 @ControllerAdvice派生于 @Component组件而所有组件初始化都会调用 InitializingBean接口
(2)下面查看 initializingBean 有哪些实现类
在查询过程中发现,其中 Spring MVC 中的实现子类是 RequestMappingHandlerAdapter,它里面有一个方法 afterPropertiesSet()方法,表示所有的参数设置完成之后执行的方法
(3)而这个方法中有一个 initControllerAdviceCache 方法,查询此方法
发现这个方法在执行时会查找使用所有的 @ControllerAdvice类,发送某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装,比如发生异常是调用异常的 Advice 方法实现的
关键词:
推荐阅读
游轮是什么 全球最大邮轮有多大?
游轮是什么最初的游轮是用来运输货物的,现在的邮轮建的格外的大,已经成为了身份和地位的象征。全球最大邮轮有多大?1、皇家加勒比海洋魅力 【详细】
阿尔卑斯山简介 阿尔卑斯山地质特点是什么
阿尔卑斯山简介阿尔卑斯山呈弧形,长1200公里,宽130-260公里,平均海拔约3000米,总面积约22万平方公里。海拔4000米以上的山峰有128座。最 【详细】
沙漠蝗简介 沙漠蝗怎么会有侵入中国的风险呢?
沙漠蝗简介沙漠蝗是非洲和亚洲热带沙漠地区山谷和绿洲的主要农业害虫,飞行能力强,食量大,能聚集形成巨大的蝗群。一平方公里的蝗虫可容纳 【详细】
首都新机场叫什么名字 机场是24小时开放的吗?
首都新机场叫什么名字?一般指北京大兴国际机场。北京大兴国际机场定位为大型国际航空枢纽,国家发展新动力源,支撑雄安新区建设的京津冀区 【详细】
什么牌子的插排好 优质的插排应该具备哪些特质呢?
什么牌子的插排好品牌插座有公牛、西门子, TCL, 西蒙, 奇胜, 松下, 施耐德, ABB、朗能,等。为了满足大众对插座的各种需求,各 【详细】
相关新闻
- SpringBoot 统一处理:登录校验-拦截器、异常处理、数据格式返回-焦点播报
- 【全球聚看点】为了让更多人用上卫星互联网,SpaceX星链降价居然这么狠!
- 每日快报!带你探索AI面试的奇妙经历,结果令人惊喜!
- 618前夕 - 聊干货,小米13 Ultra体验:这或许是今年最佳影像产品
- 新资讯:深圳燃气不超30亿可转债获上交所通过 国信证券建功
- 游轮是什么 全球最大邮轮有多大?
- 天天观点:减肥吃什么鱼(减肥可以吃鲫鱼吗 鲫鱼怎么吃减肥)
- 快讯:中粮期货点评:短期白糖仍有较强上行动力
- 扎哈罗娃谈美债务上限危机:美国一切全靠印钞机和军事基地|每日动态
- 我国深海装备技术水平持续提升 为南海沉船遗址考古研究提供科技支撑
- 四川景点现透明科技厕所 透明厕所在世界各地的应用有哪些?
- 慢性支气管炎用哪些药物搭配好_慢性支气管炎用哪些药 全球报资讯
- 微软宣布邀请更多 Xbox 玩家加入 Alpha 和 Alpha Skip-Ahead 通道 快播
- 传音 Infinix Note 30/5G / Pro 手机发布:5000mAh 电池-世界关注
- 【环球时快讯】鸿蒙系统再次突破,份额提升至8%,这才是华为回归的关键
- Mainchain:新的BTC硬分叉,展示真正的侧链力量 热门
- 360户外球机6Pro简测
- 每日速递:美国制裁也挡不住:华为全年出货量上调至4000万台,鸿蒙占比达8%
- 世界新动态:想在印度赚钱?别做梦了!苹果代工厂宣布退出印度业务,利润太低
- 地球关灯一小时是什么 这一小时我们能做什么呢?