🏀🏀🏀来都来了,不妨点个关注!
🎧🎧🎧博客主页:欢迎各位大佬!
文章目录
1. 为什么要进行统一功能的处理
Spring统一功能处理的原因主要有以下几点:
1. 提高代码的可维护性、可重用性和可扩展性。在应用程序中,存在一些通用的功能需求,如身份验证、日志记录、异常处理等。这些功能需要在多个地方进行调用和处理。如果每个地方都单独实现这些功能,会导致代码冗余、难以维护和重复劳动。通过统一功能处理的方式,可以将这些通用功能抽取出来,以统一的方式进行处理,从而简化代码结构,提高代码的可读性和可维护性。
2. 降低系统的代码耦合度。 在项目中,无论是controller层、service层还是dao层都可能会有异常发生。如果每个过程都单独处理异常,会导致系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。因此,将异常处理从各处理过程解耦出来,可以使相关处理过程的功能更加单一,也便于进行异常信息的统一处理和维护。
2. 统一用户登录验证
对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步
骤:
- 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的 preHandle(执⾏具体⽅法之前的预处理)⽅
法。 - 将⾃定义拦截器加⼊ WebMvcConfigurer 的 addInterceptors ⽅法中。
2.1 自定义拦截器
package com.example.demo.config;
import com.example.demo.common.AppVar;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 自定义拦截器
*/
@Component
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("do UserInterceptor");
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute(AppVar.SESSION_KEY) != "") {
//用户已经登录
return true;//执行后面的流程
}
response.sendRedirect("https://www.baidu.com");
return false;
}
}
2.2 将自定义拦截器添加到系统配置
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(new UserInterceptor()); //添加我们自定义的拦截器
registry.addInterceptor(userInterceptor)
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/user/reg")
.excludePathPatterns("/user/login")
.excludePathPatterns("img/**");//排除所有图片
}
}
其中:
addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意⽅法(也就是所有⽅法)。
excludePathPatterns:表示需要排除的 URL。
说明:以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)。
2.4 拦截器实现原理
正常调用顺序:
加入拦截器后,会在Controeller层之前加一个统一的登录处理:
2.5 拦截器实现源码分析
在我们执行Spring项目的时候,它首先会执行一个 DispatcherServlet调度器,如下图,下面我们就进行它的源码来分析。
所有⽅法都会执⾏ DispatcherServlet 中的 doDispatch 调度⽅法,doDispatch 源码如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.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;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
我们只用关注一小部分即可:
我们再进入applyPreHandle方法的源码:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
从上述源码可以看出,在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor 并执⾏拦截器中
的 preHandle ⽅法,这样就会咱们前⾯定义的拦截器对应上了,如下图所示
此时用户的登录验证就完毕了,这就是拦截器的原理。
3. 统一异常处理
统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表
示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执⾏某个通知,
也就是执⾏某个⽅法事件,具体实现代码如下:
package com.example.demo.config;
import com.example.demo.common.ResultAjax;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
//@ControllerAdvice
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(NullPointerException.class)
public ResultAjax doNullPointerException(NullPointerException e) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMessage("空指针异常:" + e.getMessage());
resultAjax.setData(null);
return resultAjax;
}
}
其中,方法名和返回值可以自定义,其中最重要的是 @ExceptionHandler(Exception.class) 注解。
这里我们是自定义了一个类ResultAjax来接受返回值。
由于在上述统一异常处理中,我们只处理了空指针的异常处理,在实际中,我们一般会加一个保底的异常处理,即当我们定义的异常无法捕获该异常时,我们有所有异常的父类Exception来捕获,代码如下:
@ExceptionHandler(Exception.class)
public ResultAjax doException(Exception e) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMessage("异常:" + e.getMessage());
resultAjax.setData(null);
return resultAjax;
}
4. 统一数据格式返回
ResultAjax定义如下:
package com.example.demo.common;
import lombok.Data;
@Data
public class ResultAjax {
private int code; //状态码
private String msg; //状态码的描述信息
private Object data; //返回数据
/**
* 返回成功对象
* @param data
* @return
*/
public static ResultAjax success(Object data) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMsg("");
resultAjax.setData(data);
return resultAjax;
}
public static ResultAjax success(String msg, Object data) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMsg(msg);
resultAjax.setData(data);
return resultAjax;
}
public static ResultAjax fail(int code,String msg){
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(code);
resultAjax.setMsg(msg);
resultAjax.setData(null);
return resultAjax;
}
public static ResultAjax fail(int code,String msg,Object data){
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(code);
resultAjax.setMsg(msg);
resultAjax.setData(data);
return resultAjax;
}
}
4.1 为什么需要统一的数据返回?
- 方便前端程序员更好的接收和解析后端返回的数据。
- 降低前后端程序员沟通的成本,按照某个特定的格式返回就可以了。
一般我们统一返回的数据格式如下: - 状态码:用来标识执行成功或失败的状态信息
- 消息:用来描述请求的具体消息
- 数据:包括请求的数据消息
4.2 统一数据格式返回的实现
一般我们通过@ControllerAdvice+ResponseBodyAdvice的方式实现,具体如下:
定义一个类加上@ControllerAdvice注解,并继承ResponseBodyAdvice接口:
package com.example.demo.config;
import com.example.demo.common.ResultAjax;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 对返回的数据类型做强制的数据转换
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* true -> 才会调用 beforeBodyWrite 方法,
* 反之则永远不会调用 beforeBodyWrite 方法
*
* @param returnType
* @param converterType
* @return
*/
@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) {
if (body instanceof ResultAjax) {
return body;
}
return ResultAjax.success(body);
}
}
当我们继承了这个接口后,我们就需要重写它的上面这两个方法,其中supports返回true才会调用下面的beforeBodyWrite方法。值得注意的是我们需要对String类型的body做单独的处理,因为当我们走下面的步骤时会走两个解析引擎:
报错信息:
它是在转换成JSON字符串的时候报错的,所以我们可以在转换前做一个判断,如果它是String类型的,我们先提前将它转换成JSON字符串即可。
以上就是全部内容了,感谢支持!