Spring统一功能处理
拦截器
场景: 我们要对一个网站实现强制登陆的功能,后端根据Session来判断用户是否登录,但是如果我们要这样实现,就需要对每一个接口都增加这样的逻辑处理 此时就比较麻烦
• 需要修改每个接⼝的处理逻辑
• 需要修改每个接⼝的返回结果
• 接⼝定义修改, 前端代码也需要跟着修改
有没有更简单的办法, 统⼀拦截所有的请求, 并进⾏Session校验呢, 这⾥我们学习⼀种新的解决办法: 拦截器
拦截器
什么是拦截器
拦截器是Spring框架提供的核⼼功能之⼀, 主要⽤来拦截⽤⼾的请求, 在指定⽅法前后, 根据业务需要执⾏预先设定的代码
也就是说, 允许开发⼈员提前预定义⼀些逻辑, 在⽤⼾的请求响应前后执⾏. 也可以在⽤⼾请求前阻⽌
其执⾏.
在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的
请求, 判断Session中是否有登录⽤⼾的信息. 如果有就可以放⾏, 如果没有就进⾏拦截.
拦截器的基本使用
定义拦截器
实现HandleInterceptor接口 重写方法
java
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor 目标方法执行前");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor 目标方法执行后");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("LoginInterceptor 视图执行后");
}
}
preHandle方法:目标方法执行前执行,返回true 继续执行后续操作,返回false 中断后续操作
postHandle方法:目标方法执行后执行
afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏
注册配置拦截器
实现WebMvcConfigurer接口 重写addInterceptors方法
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//⾃定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册⾃定义拦截器对象
registry.addInterceptor(loginInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径
}
}
拦截器详解
拦截器的拦截路径配置
拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效.我们在注册配置拦截器的时候, 通过 addPathPatterns()⽅法指定要拦截哪些请求. 也可以通过excludePathPatterns() 指定不拦截哪些请求
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//设置拦截器的请求路径
// /**表示拦截所有
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login")
.excludePathPatterns("/**/*.js")
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.png")
.excludePathPatterns("/**/*.html");
//addPath设置拦截那些请求
//excludePath设置不拦截哪些请求
}
}
在拦截器中除了可以设置 /** 拦截所有资源外,还有⼀些常⻅拦截路径设置:
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/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 |
添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏ preHandle() ⽅法,
这个⽅法需要返回⼀个布尔类型的值.
如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的⽅法.
如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据.
拦截器实现原理
当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.
所有请求都会先进到DispatcherServlet,执⾏doDispatch 调度⽅法. 如果有拦截器,会先执⾏拦截器preHandle() ⽅法的代码 , 如果 preHandle() 返回true, 继续访问controller中的⽅法.controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 和 afterCompletion(),返回给DispatcherServlet, 最终给浏览器响应数据.
初始化
DispatcherServlet的初始化⽅法 init() 在其⽗类 HttpServletBean 中实现的.
主要作⽤是加载 web.xml 中 DispatcherServlet 的 配置, 并调⽤⼦类的初始化.
在 HttpServletBean 的 init() 中调⽤了 initServletBean() , 它是在FrameworkServlet 类中实现的, 主要作⽤是建⽴ WebApplicationContext 容器(有时也称上下⽂), 并加载 SpringMVC 配置⽂件中定义的 Bean到该容器中, 最后将该容器添加到 ServletContext 中. 下⾯是initServletBean() 的具体代码:
初始化web容器的过程中, 会通过onRefresh 来初始化SpringMVC的容器
在initStrategies()中进⾏9⼤组件的初始化, 如果没有配置相应的组件,就使⽤默认定义的组件(在
DispatcherServlet.properties中有配置默认的策略, ⼤致了解即可
⽅法initMultipartResolver、initLocaleResolver、initThemeResolver、initRequestToViewNameTranslator、initFlashMapManager的处理⽅式⼏乎都⼀样(1.2.3.7.8,9),从应⽤⽂中取出指定的Bean, 如果没有, 就使⽤默认的.⽅法initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers的处理⽅式⼏乎都⼀样(4,5,6)
4. 初始化处理器映射器HandlerMappings:处理器映射器作⽤,1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx⽅法);如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进⾏排序;如果在ApplicationContext中没有发现有处理器映射器,则默认BeanNameUrlHandlerMapping作为处理器映射器
5. 初始化处理器适配器HandlerAdapter:作⽤是通过调⽤具体的⽅法来处理具体的请求;如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的 HandlerAdapter,并进⾏排序;如果在ApplicationContext中没有发现处理器适配器,则不设置异常处理器,则默认SimpleControllerHandlerAdapter作为处理器适配器
6. 初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有handlerExceptionResolver,则从ApplicationContext中获取到所有的HandlerExceptionResolver,并进⾏排序;如果在ApplicationContext中没有发现异常处理器解析器,则不设置异常处理器
处理请求
java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
//1.获取执行链
//遍历所有的HandlerMapper 找到与请求对应的Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
//2. 获取适配器
//遍历所有的HandlerAdapter 找到可以处理该Handler的HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//3. 执行拦截器的preHandler方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//4. 执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
//5. 执行拦截器的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//6.处理视图 处理之后执⾏拦截器afterCompletion⽅法
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//7.执⾏拦截器afterCompletion⽅法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
此处最关键的就是3 4 5 点 这里规定了执行目标方法前执行preHandle 执行目标方法之后执行postHandle方法
适配器模式
适配器模式是一种设计模式,用于将一个类的接口转换成客户端所期望的另一个接口。它允许原本不兼容的类能够合作无间。
适配器模式主要包括两个核心角色:目标接口(Target)和适配器(Adapter)。目标接口是客户端所期望的接口,适配器则是将原本不兼容的类转换成目标接口的中间层。
适配器模式可以通过两种方式实现:类适配器和对象适配器。在类适配器中,适配器继承了被适配类,并实现了目标接口。而在对象适配器中,适配器持有一个被适配对象的实例,并实现了目标接口。
使用适配器模式可以有以下几个好处:
- 可以让原本不兼容的类能够一起工作,提高代码的复用性。
- 可以封装已有的类,对外隐藏底层的实现细节。
- 可以在不修改现有代码的情况下引入新的功能。
总之,适配器模式是一种常用的设计模式,可用于解决不同接口之间不兼容的问题,使得原本无法合作的类能够协同工作。
HandlerAdapter 在 Spring MVC 中使⽤了适配器模式
HandlerAdapter 主要⽤于⽀持不同类型的处理器(如 Controller、HttpRequestHandler 或者Servlet 等),让它们能够适配统⼀的请求处理流程。这样,Spring MVC 可以通过⼀个统⼀的接⼝来处理来⾃各种处理器的请求
场景: 前⾯学习的slf4j 就使⽤了适配器模式, slf4j提供了⼀系列打印⽇志的api, 底层调⽤的是log4j 或者logback来打⽇志, 我们作为调⽤者, 只需要调⽤slf4j的api就⾏了
java
//Slf4j接口
interface Slf4jApi{
void log(String message);
}
//log4j接口
class Log4j{
void log4jLog(String message){
System.out.println("Log4j打印:" + message);
}
}
//slf4j和log4j适配器
class Slf4jLog4JAdapter implements Slf4jApi{
private Log4j log4j;
public Slf4jLog4JAdapter(Log4j log4j) {
this.log4j = log4j;
}
@Override
public void log(String message) {
log4j.log4jLog(message);
}
}
public class Slf4jDemo {
public static void main(String[] args) {
Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());
slf4jApi.log("使用slf4j打印日志");
}
}
适配器模式应⽤场景
⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷. 应⽤这种模式算是"⽆奈之举", 如果在设计初期,我们就能协调规避接⼝不兼容的问题, 就不需要使⽤适配器模式了
所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造, 并且希望可以复⽤原有代码实现新的功能. ⽐如版本升级等
统一数据返回格式
强制登录案例中, 我们共做了两部分⼯作
- 通过Session来判断⽤⼾是否登录
- 对后端返回数据进⾏封装, 告知前端处理的结果
后端统一返回结果
java
package com.bite.book.model;
import com.bite.book.enums.ResultCode;
import lombok.Data;
@Data
public class Result<T> {
/**
* 业务状态码
*/
private ResultCode code; //0-成功 -1 失败 -2 未登录
/**
* 错误信息
*/
private String errMsg;
/**
* 数据
*/
private T data;
public static <T> Result<T> success(T data){
Result result = new Result();
result.setCode(ResultCode.SUCCESS);
result.setErrMsg("");
result.setData(data);
return result;
}
public static <T> Result<T> fail(String errMsg){
Result result = new Result();
result.setCode(ResultCode.FAIL);
result.setErrMsg(errMsg);
result.setData(null);
return result;
}
public static <T> Result<T> fail(String errMsg,Object data){
Result result = new Result();
result.setCode(ResultCode.FAIL);
result.setErrMsg(errMsg);
result.setData(data);
return result;
}
public static <T> Result<T> unlogin(){
Result result = new Result();
result.setCode(ResultCode.UNLOGIN);
result.setErrMsg("用户未登录");
result.setData(null);
return result;
}
}
后端返回接口
java
@RequestMapping("/getBookListByPage")
public Result getBookListByPage(PageRequest pageRequest, HttpSession session){
log.info("查询翻页信息, pageRequest:{}",pageRequest);
//校验成功
if (pageRequest.getPageSize()<0 || pageRequest.getCurrentPage()<1){
return Result.fail("参数校验失败");
}
PageResult<BookInfo> bookInfoPageResult = null;
try {
bookInfoPageResult = bookService.selectBookInfoByPage(pageRequest);
//此处对返回的数据进行再次封装
return Result.success(bookInfoPageResult);
}catch (Exception e){
log.error("查询翻页信息错误,e:{}",e);
return Result.fail(e.getMessage());
}
}
拦截器帮我们实现了第⼀个功能, 接下来看SpringBoot对第⼆个功能如何⽀持
统一数据返回格式快速入门
统一的数据返回格式使用@ControllerAdvice
和 ResponseBodyAdvice
的方式实现
@ControllerAdvice表示控制器通知类
添加类 ResponseAdvice , 实现 ResponseBodyAdvice 接⼝, 并在类上添加 @ControllerAdvice 注解
java
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
//转json
private ObjectMapper objectMapper;
//判断是否要执行beforeBodyWrite方法
//ture为执行
//false不执行
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
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方法进行具体操作处理
统一异常处理
统⼀异常处理使⽤的是 @ControllerAdvice
+ @ExceptionHandler
来实现的,@ControllerAdvice
表⽰控制器通知类, @ExceptionHandler
是异常处理器,两个结合表⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件
java
package com.bite.book.config;
import com.bite.book.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
@ResponseBody
@Slf4j
public class ErrorHandler {
@ExceptionHandler
public Object handler(Exception e){
log.info("发生异常 e:{}",e.getMessage());
return Result.fail(e.getMessage());
}
@ExceptionHandler
public Object handler(NullPointerException e) {
return Result.fail("发⽣NullPointerException:"+e.getMessage());
}
@ExceptionHandler
public Object handler(ArithmeticException e) {
return Result.fail("发⽣ArithmeticException:"+e.getMessage());
}
}
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配