目录
一、拦截器快速入门
(1)什么是拦截器
(2)拦截器的使用步骤
1、定义拦截器
🍀preHandle() 方法
🍀postHandle() 方法
🍀afterCompletion() 方法
2、注册配置拦截器
二、拦截器详解
1、拦截路径
2、拦截器执行流程
三、登录校验(图书管理系统)
1、定义拦截器
2、注册配置拦截器
3、更改图书馆管理系统代码(用户登录接口)
(1)后端代码
(2)前端代码
四、适配器模式
1、适配器模式定义
2、适配器模式角色
3、适配器模式的实现
4、适配器模式应用场景
上篇博客完成了强制登录功能,后端程序需要根据 Session 来判断用户是否登录,但是实现方法是比较麻烦的,需要完成下面几个步骤:
1、需要修改每个接口的处理逻辑
2、需要修改每个接口的返回结果
3、接口定义修改,前段代码也需要跟着修改
那有没有更简单的方式,统一拦截所有的请求,并进行 Session 的校验的?有,那就是Spring 框架提供的核心功能之一:拦截器。
一、拦截器快速入门
(1)什么是拦截器
拦截器是 Spring 框架提供的核心功能之一,主要用来拦截用户的请求,在指定的方法前后,根据业务需要执行预先设定的代码。
也就是说,允许开发人员提前预定义一些逻辑,在用户的请求、响应前后执行。也可以在用户请求前阻止其执行。
在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息,如果有 -> 就放行,如果没有 -> 就进行拦截。
如图:
这种情况就类似我们去银行办理业务,去银行办业务需要有带身份证,如果没有身份证,就不能取号,更不要提后续的业务,没有就要回家拿再过来,有就去取号,排队等待,办理你需要的业务。
拦截器在这里就类似你有没有身份证,有身份证才能办理后续的业务,没有就要执行其他逻辑(回家拿身份证)。
接下来,我们来学习拦截器的基本使用。
(2)拦截器的使用步骤
练习拦截器的使用以及后面的使用都是使用图书管理系统项目。
1、定义拦截器
自定义拦截器,实现 HandlerInterceptor 接口,并且重写所有方法,代码如下:
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("LoginInterceptor preHandle(目标方法执行前执行).....");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("LoginInterceptor postHandle(目标方案执行后执行).....");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("LoginInterceptor afterCompletion(视图渲染完毕后执行,最后执行).....");}
}
🍀preHandle() 方法
目标方法执行前执行。返回true:继续执行后续操作;返回false:中断后续操作。
🍀postHandle() 方法
目标方法执行后执行。
🍀afterCompletion() 方法
视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图了,暂不了解)。
2、注册配置拦截器
创建一个 WebConfig 类,实现WebMvcConfigurer接口,并重写addInterceptors方法(),代码如下:
@Configuration
public class WebConfig implements WebMvcConfigurer {@AutowiredLoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表⽰拦截所有请求;}
}
现在启动服务器,访问任意接口,观察打印的日志
查询bookId=1的图书信息,如图:
观察日志:
可以看到,在执行目标方法前,就执行了 preHandle() 方法了;因为preHandle返回了true,才能执行执行目标方法,目标方法执行完后,执行 postHandle() 方法;最后才执行 afterCompletion() 方法。
现在我们把 preHandle() 方法的返回值修改成 false试试,观察打印结果
再次访问 /book/queryBookById?bookId=1 ,观察返回的信息,这次没有返回信息了,如图:
再看看控制台打印的日志:
因为 preHandle() 方法返回的是 false,所有没有打印 postHandle() 和afterCompletion() 方法,也就意味着中断了后续的操作。
二、拦截器详解
拦截器的入门程序完成后,接下来我们学习拦截器的使用细节。拦截器的使用细节主要介绍两个部分:1、拦截器的拦截路径配置 2、拦截器实现原理
1、拦截路径
拦截路径是指我们定义的这个拦截器,对哪些请求生效。
我们在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截哪些请求。也可以通过 excludePathPatterns() 指定不拦截哪些请求。
上述代码中,我们配置的是 /** ,表示拦截所有的请求。
比如用户登录校验,我们希望可以对除了登录之外所有的路径生效,代码如下:
@Configuration
public class WebConfig implements WebMvcConfigurer {@AutowiredLoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表⽰拦截所有请求.excludePathPatterns("/user/login")//设置拦截器不拦截哪些请求路径;}
}
现在请求 /user/login 接口,就不会打印上面说的那三个接口了
因为没有拦截它,也就不会执行那三个方法,打印其日志了。
在拦截器中除了可以设置 /** 拦截所有资源外,还有一些常见拦截路径设置:
拦截路径 | 含义 | 举例 |
/* | ⼀级路径 | 能匹配/user,/book,/login,不能匹配 /user/login |
/** | 任意级路径 | 能匹配/user,/user/login,/user/reg |
/book/* | /book下的⼀级路径 | 能匹配/book/addBook,不能匹配/book/addBook/1,/book |
/book/** | /book下的任意级路径 | 能匹配/book,/book/addBook,/book/addBook/2,不能匹 配/user/login |
2、拦截器执行流程
正常的调⽤顺序:
有了拦截器后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图:
1、添加拦截器后,执行 Controller 的方法之前,请求会先被拦截器拦截住,执行 preHadnle() 方法,这个方法需要返回一个布尔类型的值。如果返回true,就表示放行本次操作,继续访问 controller中的方法;如果返回false,则拦截(controller中的方法也不会执行)。
2、controller当中的方法执行完毕后,在回过来执行 postHandle() 这个方法 以及 afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据。
三、登录校验(图书管理系统)
学习了拦截器的基本操作之后,接下来我们需要完成最后一步操作:通过拦截器来完成图书管理系统中的登录校验功能。
1、定义拦截器
从 session 中获取用户信息,如果 session 中不存在,就返回false,并设置http状态码为401,否则返回true。代码如下:
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("LoginInterceptor preHandle(目标方法执行前执行).....");//获取Session,判断Session中存储的userinfo信息是否为空HttpSession session = request.getSession(true);UserInfo userInfo = (UserInfo) session.getAttribute(Constant.USER_SESSION_KEY);if(userInfo == null || userInfo.getId() <= 0) {//用户未登录response.setStatus(401);return false;}return true;}
}
http状态码401:Unauthorized
Indicates that authentication is required and was either not provided or has failed. If the request already included authorization credentials, then the 401 status code indicates that those credentials were not accepted.中文解释:未经过认证。指示身份验证是必需的,没有提供身份验证或身份验证失败。如果请求已经包含授权凭据,那么401状态码表示不接受这些凭据。
其中 request.getSession() 方法有下面两种形式,有参和无参,如图:
其中,无参的 getSession() 方法 默认 是 getSession(true);
getSession(true) 表示如果有Session,则返回Session,如果没有,则创建一个Session。
getSession(false) 表示 获取Session可能会是null。
2、注册配置拦截器
配置拦截器拦截所有的请求,除了 /user/login 和前端的页面,代码如下:
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表⽰拦截所有请求.excludePathPatterns("/user/login")//设置拦截器不拦截哪些请求路径.excludePathPatterns("/css/**")//排除前端静态资源.excludePathPatterns("/js/**").excludePathPatterns("/pic/**").excludePathPatterns("/**/*.html");}
}
也可以改成下面这样子,定义一个链表,把要排除的路径都放进链表中,代码如下:
//也可以改成下面这样子
//定义一个链表,把要排除的路径都放进链表中
@Configuration
public class WebConfig implements WebMvcConfigurer {@AutowiredLoginInterceptor loginInterceptor;private List<String> excludePaths = Arrays.asList("/user/login","/css/**","/js/**","/pic/**","/**/*.html");@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表⽰拦截所有请求.excludePathPatterns(excludePaths)//设置拦截器排除拦截的路径;}
}
运行程序,进行测试
访问:http://127.0.0.1:8080/book_list.html?pageNum=1
可以看到有401的状态码。
通过fiddler抓包观察:
返回的响应状态码就是401。
现在进行登录,如图:
再次访问:http://127.0.0.1:8080/book_list.html?pageNum=1,如图:有内容了
3、更改图书馆管理系统代码(用户登录接口)
(1)后端代码
因为我们已经定义和注册配置了拦截器,所以这里统一帮我们做了校验用户是否登录,没有登录就返回false(拦截),并且设置响应的状态码为401;用户已经登录了,能获取到的Session能通过它拿到userInfo信息,userinfo也有对应的用户信息,就是用户登录了,则返回 true;就是上面定义拦截器的内容,代码如下:
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("LoginInterceptor preHandle(目标方法执行前执行).....");//获取Session,判断Session中存储的userinfo信息是否为空HttpSession session = request.getSession(true);UserInfo userInfo = (UserInfo) session.getAttribute(Constant.USER_SESSION_KEY);if(userInfo == null || userInfo.getId() <= 0) {//用户未登录response.setStatus(401);return false;}return true;}
}
修改 controller的"/book/getBookListByPage" 接口,也就是删除一些逻辑判断,因为拦截器已经帮我们做了这些逻辑处理,代码如下:
@RequestMapping("/getBookListByPage")public Result<PageResult<BookInfo>> getBookListByPage(PageRequest pageRequest, HttpSession session) {log.info("查询图书的列表, 请求参数pageRequest: {}", pageRequest);//从session中获取用户信息//如果用户信息为空, 说明用户未登录
// UserInfo loginUserInfo = (UserInfo) session.getAttribute(Constant.USER_SESSION_KEY);
// if(loginUserInfo == null || loginUserInfo.getId() < 0) {
// return Result.nologin();
// }
// //参数校验
// if(pageRequest.getPageNum() == null) {
// //返回默认第一页,如果pageSize也没设置,则会使用默认的10
// pageRequest.setPageNum(1);
// }PageResult<BookInfo> bookList = bookService.getBookListByPage(pageRequest);return Result.success(bookList);}
现在进行测试,未登录,访问:http://127.0.0.1:8080/book_list.html?pageNum=1,返回401状态码
登录后,再访问,结果如下:能返回信息
但上面没有实现强制登录功能,不应该出现401的状态码,应该跳转到登录界面,这是因为还没有改前端代码,下面修改前端代码。
(2)前端代码
getBookList();function getBookList() {$.ajax({url: "/book/getBookListByPage" + location.search,type: "get",success: function (result) {// if (result.code == "NOLOGIN") {//用户登录// location.href = "login.html";// }if (result.data != null && result.data.records != null) {console.log("拿到参数")var finalHtml = "";for (book of result.data.records) {finalHtml += '<tr>';finalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="' + book.id + '" class="book-select"></td>'finalHtml += '<td>' + book.id + '</td>';finalHtml += '<td>' + book.bookName + '</td>';finalHtml += '<td>' + book.author + '</td>';finalHtml += '<td>' + book.count + '</td>';finalHtml += '<td>' + book.price + '</td>';finalHtml += '<td>' + book.publish + '</td>';finalHtml += '<td>' + book.statusCN + '</td>';finalHtml += '<td>';finalHtml += '<div class="op">';finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';finalHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';finalHtml += '</div>';finalHtml += '</td>';finalHtml += '</tr>';}$("tbody").html(finalHtml);var data = result.data;//翻页信息$("#pageContainer").jqPaginator({totalCounts: data.count, //总记录数pageSize: 10, //每页的个数visiblePages: 5, //可视页数currentPage: data.pageRequest.pageNum, //当前页码first: '<li class="page-item"><a class="page-link">首页</a></li>',prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',//页面初始化和页码点击时都会执行onPageChange: function (page, type) {console.log("第" + page + "页, 类型:" + type);if (type == "change") {location.href = "book_list.html?pageNum=" + page;}}});}},error: function (error) {console.log(error);if(error != null && error.status == 401) {location.href = "login.html";}}});}
也就是多增加 error 时,应该怎么做,上面是进行判断了返回响应的信息,响应是不是空,已经响应的状态码是不是401,是的话就给他跳转到登录界面。
现在进行测试一下,URL:http://127.0.0.1:8080/book_list.html?pageNum=1,会跳转到登录界面
观察日主,如图:
打印了preHandle()方法执行后就没有后续的内容了。
要是登录后,再访问 http://127.0.0.1:8080/book_list.html?pageNum=1,就会展示第一页的图书列表。
还有其他接口的代码没改,后面会学习统一功能,到时候使用统一功能再一起改。
四、适配器模式
1、适配器模式定义
适配器模式,也叫包装模式。将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类也可以合作无间。
简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方使用。把两个不兼容的接口通过一定的方式使之兼容。
比如下图的两个接口,本身是不兼容的(参数类型不一样,参数个数不一样等等)
可以通过适配器的方式,使之兼容
而在日常生活中,适配器模式也非常常见,例如转换插头、网络转接头、耳机转接头等。
出国旅游必备物品之一就是转换插头。不同国家的插头标准是不一样的,出国后我们手机 / 电脑充电器可能就没办法使用了。比如美国电器 110V,中国 220V,就要有一个适配器将 110V 转化为 220V。国内经常使用转换插头把两头转为三头,或者三头转两头。
2、适配器模式角色
Target:目标接口(可以是抽象类或接口),客户希望直接用的接口。
Adaptee:适配者,但是与 Target 不兼容。
Adapter:适配器类,此模式的核心。通过继承或者引用适配者的对象,把适配者转为目标接口。
client:需要使用适配器的对象。
3、适配器模式的实现
场景:之前学习的 Slf4j 就使用了适配器模式,Slf4j 提供了一系列打印日志的 API, 底层调用的是 log4j 或者 logback 来打日志,我们作为调用者,只需要调用 Slf4j 的API 即可。
代码如下:
Target:
public interface Slf4jLog {void log(String message);
}
Adaptee:
public class Log4j {public void log4jPrint(String message) {System.out.println("我是Log4j, 打印内容是: " + message);}
}
Adapter:
Log4j 想使用 Slf4jLog log接口,但是不行,因为自身 打印的方法 和Slf4jLog 的不同。所以这时候就就需要使用适配器,定义一个类,通过继承 Slf4jLog 接口,来实现它的方法,再引入 Log4j,打印Log4j的内容。
public class Log4jAdapter implements Slf4jLog{private Log4j log4j;public Log4jAdapter(Log4j log4j) {this.log4j = log4j;}@Overridepublic void log(String message) {log4j.log4jPrint("我是适配器, 打印日志为: " + message);}
}
client:
public class Main {public static void main(String[] args) {Slf4jLog slf4jLog = new Log4jAdapter(new Log4j());slf4jLog.log("我是客户端");}
}
打印内容如下:
可以看到,适配器就是起到一层包装的效果,因为接口和 Slf4jLog 不同,所以就使用适配器类 实现 Slf4jLog 接口,同时引用 Log4j,使用 Log4j 的打印方法(包装一层再给 Log4j 打印)。
最终还是给 Log4j 打印。适配器的作用就是给两个不同的接口,给其中一个接口进行包装一下,再丢给另一个接口进行使用。
可以看出,有了适配器模型,对已经定义好的接口,可以不进行修改,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行。
4、适配器模式应用场景
一般来说,适配器模式可以看做一种 “补偿模式”,用来 补救 设计上的缺陷。应用这种模式算是 “无奈之举”,如果在设计初期,我们就能协调规避接口不兼容的问题,就不需要使用适配器模式了。
所以,适配器模式更多的应用场景主要是对正在运行的代码进行改造,并且希望可以复用原有代码实现新的功能,比如版本升级等。