目录
🚩拦截器
🎈什么是拦截器?
🎈如何使用拦截器
🎓自定义拦截器
🎓注册拦截器
🎈拦截器详解
🎓拦截路径
🎓拦截器执⾏流程
🔴DispatcherServlet 源码分析(了解)
🚩适配器模式
🎈适配器模式的实现
🚩统⼀数据返回格式
🎈使用
🎈测试
🎓返回的类型是Result类型
🎓返回的类型是String类型
🎓返回的类型是Integer/Boolean类型
🚩统⼀异常处理
🚩总结
我们再实现了图书管理系统之后,如果不登录,就不能进行进入图书列表页和不能参与一些功能操作。所以我们需要强制登录操作。在之前的登录操作都是从HttpSession中获取session对象,看是否存在,如果不存在就表示未登录,如果存在就表示登陆状态。这种操作是比较繁琐的,每个接口都需要进行校验。
//1.判断用户是否登录 //如果用户信息为空, 说明用户未登录 UserInfo loginUserInfo = (UserInfo) session.getAttribute(constants.USER_SESSION_KEY); if (loginUserInfo==null || loginUserInfo.getId()<=0){ return Result.nologin("用户未登录"); }
有一种简单的办法,统一拦截所有的请求,并进行session校验——拦截器.
如果用户未登录,那么就直接拦截,不用每个接口都要进行session校验。
拦截器是Spring框架提供的核⼼功能之⼀, 主要⽤来拦截⽤⼾的请求, 在指定⽅法前后, 根据业务需要执⾏预先设定的代码。也就是说,允许开发人员 提前预定义一些逻辑,在用户的请求响应前后执行,也就是在用户请求前阻止其执行。 在拦截器中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断session中是否有登录用户的信息,如果有就可以放行,如果没有就进行拦截。
举个例子:
比如我们去银行办理业务中,在办理业务的前后,就可以进行一些拦截的操作。
办理业务之前,先取号,如果带身份证了那么就取号成功,如果没有带那么取号失败。
业务办理结束之后,要给业务办理人员的服务进行评价。
这些都是拦截器做的工作。
一、实现HandlerInterceptor接⼝,并重写其所有⽅法
Interceptor是拦截器的意思。
/** * 自定义拦截器 */ @Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { //请求处理前,执行的逻辑,类似于安保查证件(true放行 ,false拦截) @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("LoginInterceptor preHandle"); return true; } //请求处理之后,处理的逻辑 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("LoginInterceptor postHandle"); } //视图渲染后,执行的逻辑 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("LoginInterceptor afterCompletion"); } }
- preHandle()⽅法:⽬标⽅法执⾏前执⾏. 返回true: 继续执⾏后续操作; 返回false: 中断后续操作.
- postHandle()⽅法:⽬标⽅法执⾏后执⾏
- afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏(后端开发现在⼏乎不涉及视图, 暂不了解)
二、注册配置拦截器:实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法package com.example.cl.config; import com.example.cl.interceptor.LoginInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 注册配置拦截器 */ @Configuration @Slf4j public class WebConfig implements WebMvcConfigurer { @Autowired public LoginInterceptor loginInterceptor;//自定义拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器 registry.addInterceptor(loginInterceptor).addPathPatterns("/**") .excludePathPatterns("/user/login") .excludePathPatterns("/**/*.js") .excludePathPatterns("/**/*.css") .excludePathPatterns("/**/*.pic") .excludePathPatterns("/**/*.html"); } }
1.和用户是否登录无关,都能打印再控制台上。 (还没有进行具体拦截操作,首先打印日志观察效果)
2.用户未登录状态
请求处理前,如果用户登录那么就返回true,就执行下面操作,如果用户未登录就返回false,拦截后面的操作。再观察运⾏结果
//请求处理前,执行的逻辑,类似于安保查证件(true放行 ,false拦截) @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session=request.getSession(false); if(session!=null&&session.getAttribute(constants.USER_SESSION_KEY)!=null){ log.info("LoginInterceptor preHandle"); return true; } response.setStatus(401); log.info("————————————登录失败,用户未登录————————————"); return false; }
返回false,直接拦截后续的操作。
3.用户登录状态
可以看到preHandle ⽅法执⾏之后就放⾏了, 开始执⾏⽬标⽅法, ⽬标⽅法执⾏完成之后执⾏ postHandle和afterCompletion⽅法.
拦截器的⼊⻔程序完成之后,接下来我们来介绍拦截器的使⽤细节。拦截器的使⽤细节我们主要介绍 两个部分:
- 1. 拦截器的拦截路径配置
- 2. 拦截器实现原理
拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效. 我们在注册配置拦截器的时候, 通过 addPathPatterns() ⽅法指定要拦截哪些请求. 也可以通过 excludePathPatterns() 指定不拦截哪些请求.上述代码中, 我们配置的是 /** , 表⽰拦截所有的请求. ⽐如⽤⼾登录校验, 我们希望可以对除了登录之外所有的路径⽣效
//注册自定义拦截器 registry.addInterceptor(loginInterceptor).addPathPatterns("/**") .excludePathPatterns("/user/login") .excludePathPatterns("/**/*.js") .excludePathPatterns("/**/*.css") .excludePathPatterns("/**/*.pic") .excludePathPatterns("/**/*.html");
有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如
- 1. 添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏ preHandle() ⽅法, 这个⽅法需要返回⼀个布尔类型的值. 如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的 ⽅法. 如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).
- 2. controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据
观察我们的服务启动日志
当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.所有请求都会先进到DispatcherServlet,执⾏doDispatch 调度⽅法.进行初始化操作如果有拦截器, 会先执⾏拦截器 preHandle() ⽅法的代码, 如果 preHandle() 返回true继续访问controller中的⽅法. controller 当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 和 afterCompletion() ,返回给 DispatcherServlet, 最终给浏览器响应数据.
适配器模式定义⽐如下⾯两个接⼝, 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等) 可以使用适配器方式,进行兼容
- 适配器模式, 也叫包装器模式. 将⼀个类的接⼝,转换成客⼾期望的另⼀个接⼝, 适配器让原本接⼝不兼容的类可以合作⽆间.
- 简单来说就是⽬标类不能直接使⽤, 通过⼀个新类进⾏包装⼀下, 适配调⽤⽅使⽤. 把两个不兼容的接⼝通过⼀定的⽅式使之兼容.
日常生活中,适配器模式是非常常见的,比如转换插头,网络转换头等等。
出国旅⾏必备物品之⼀就是转换插头. 不同国家的插头标准是不⼀样的, 出国后我们⼿机/电脑充电器 可能就没办法使⽤了. ⽐如美国电器 110V,中国 220V,就要有⼀个适配器将 110V 转化为 220V. 国内也经常使⽤转换插头把两头转为三头, 或者三头转两头适配器模式⻆⾊ • Target: ⽬标接⼝ (可以是抽象类或接⼝), 客⼾希望直接⽤的接⼝ • Adaptee: 适配者, 但是与Target不兼容 • Adapter: 适配器类, 此模式的核⼼. 通过继承或者引⽤适配者的对象, 把适配者转为⽬标接⼝ • client: 需要使⽤适配器的对象
/** * slf4j接口 */ public interface Slf4jLog { void log(String message); }
/** * log4j接口 */ public class Log4j { public void log4jPrint(String message){ System.out.println("我是Log4j,打印日志内容为:"+message); } }
/** * slf4j和log4j适配器 */ public class Log4jAdapter implements Slf4jLog{ private Log4j log4j; public Log4jAdapter(Log4j log4j) { this.log4j = log4j; } @Override public void log(String message) { log4j.log4jPrint("我是适配器,打印日志为:"+message); } }
/** * 客户端调用 */ public class Main { public static void main(String[] args) { Slf4jLog slf4jLog=new Log4jAdapter(new Log4j()); slf4jLog.log("我是客户端"); } }
适配器模式应⽤场景 ⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷. 应⽤这种模式算是"⽆奈之举", 如果在设计初期,我们就能协调规避接⼝不兼容的问题, 就不需要使⽤适配器模式了 ,所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造, 并且希望可以复⽤原有代码实现新的功能. ⽐如版本升级等.
强制登录案例中, 我们共做了两部分⼯作 1. 通过Session来判断⽤⼾是否登录 2. 对后端返回数据进⾏封装, 告知前端处理的结果后端统一返回结果
@ControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice { @Autowired private ObjectMapper objectMapper; @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } @Override @SneakyThrows public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { return Result.success(body); } }
- supports⽅法: 判断是否要执⾏beforeBodyWrite⽅法. true为执⾏, false不执⾏. 通过该⽅法可以选择哪些类或哪些⽅法的response要进⾏处理, 其他的不进⾏处理.
- beforeBodyWrite⽅法: 对response⽅法进⾏具体操作处理
postman测试登录功能,我们发现嵌套了一层。
我们可以改进一下。
@Override @SneakyThrows public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if(body instanceof Result>){ return body; } return Result.success(body); }
如果body的类型就是Result,那么直接返回body即可。
此时返回数据正常。
@RequestMapping("/user") @RestController public class UserController { @Autowired public UserService userService; @RequestMapping(value = "/login",produces = "application/json") public String login(String userName, String password, HttpSession session){ //1.校验参数 if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){ return "用户名或者密码为空"; } //2.验证密码是否正确 UserInfo userInfo=userService.getUserInfoByName(userName);//从前端获取到用户名和密码 if(userInfo==null){ return "用户不存在"; } if(!password.equals(userInfo.getPassword())){ return "密码错误"; } //3.正确的情况 //setAttributes中的参数是键值对方式,当后面获取session的时候,用constants.USER_SESSION_KEY即可获得userInfo session.setAttribute(constants.USER_SESSION_KEY,userInfo); return ""; } }
解决方案:
if(body instanceof String){ return objectMapper.writeValueAsString(Result.success(body)); }
@RestController @RequestMapping("/test") public class TestController { @RequestMapping("/t2") public Integer t2(){ return 1; } @RequestMapping("/t3") public Boolean t3(){ return true; } }
- 1. ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据
- 2. 降低前端程序员和后端程序员的沟通成本, 按照某个格式实现就可以了, 因为所有接⼝都是这样返回的.
- 3. 有利于项⽬统⼀数据的维护和修改.
- 4. 有利于后端技术部⻔的统⼀规范的标准制定, 不会出现稀奇古怪的返回内容
@Data public class Result
{ private ResultStatus code; //业务码 不是Http状态码 200-成功 -2 失败 -1 未登录 private String errMsg; //错误信息, 如果业务成功, errMsg为空 private T data; }
/** * 全局异常处理 * @RestControllerAdvice=@ControllerAdvice+@ResponseBody */ @ControllerAdvice @Slf4j @ResponseBody public class ExceptionAdvice { @ExceptionHandler public Result handException(Exception e){ log.error("发生异常, e:"+e); return Result.fail("内部错误"); } }
类名, ⽅法名和返回值可以⾃定义, 重要的是注解 接⼝返回为数据时, 需要加 @ResponseBody 注解以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类), 就返回⼀个 Result的对象, Result 对象的设置参考 Result.fail("内部错误")//一般抛出异常是不会具体告诉你什么错误.
@ControllerAdvice @Slf4j @ResponseBody public class ExceptionAdvice { @ExceptionHandler public Result handException(Exception e){ log.error("发生异常, e:"+e); return Result.fail("内部错误"); } @ExceptionHandler public Result handException(NullPointerException e){ log.error("发生异常,e:"+e); return Result.fail("发生空指针异常"); } @ExceptionHandler public Result handException(ArithmeticException e){ log.error("发生异常,e:"+e); return Result.fail("发生算术异常"); } }
模拟制造异常
@RestController @RequestMapping("/test") public class TestController { @RequestMapping("/t1") public String t1(){ int i=1/0; System.out.println(i); return "String"; } @RequestMapping("/t2") public Integer t2(){ String ret=null; System.out.println(ret.length()); return 1; } @RequestMapping("/t3") public Boolean t3(){ Integer[] integers=new Integer[]{1,2,3,4}; System.out.println(integers[5]); return true; } }
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配 .
由于i=1/0 算术异常,首先匹配的是自己当前异常类,再次是父类。 String ret=null;
System.out.println(ret.length()); Integer[] integers=new Integer[]{1,2,3,4};
System.out.println(integers[5]); 越界异常,再定义的异常中,没有,那么就找父类。
本章节主要介绍了SpringBoot 对⼀些统⼀功能的处理⽀持.
- 1. 拦截器的实现主要分两部分: 1. 定义拦截器(实现HandlerInterceptor 接⼝) 2. 配置拦截器
- 2. 统⼀数据返回格式通过@ControllerAdvice + ResponseBodyAdvice 来实现
- 3. 统⼀异常处理使⽤@ControllerAdvice + @ExceptionHandler 来实现, 并且可以分异常来处理
山高路远。