- 博主簡介:想進(jìn)大廠的打工人
- 博主主頁:@xyk:
- 所屬專欄:?JavaEE進(jìn)階?
上一篇文章我們講解了Spring AOP是一個基于面向切面編程的框架,用于將某方面具體問題集中處理,通過代理對象來進(jìn)行傳遞,但使用原生Spring AOP實現(xiàn)統(tǒng)一的攔截是非常繁瑣的。而在本節(jié),我們將使用一種簡單的方式進(jìn)行統(tǒng)一功能處理,具體如下:
- 統(tǒng)一用戶登錄權(quán)限驗證
- 統(tǒng)一數(shù)據(jù)格式返回
- 統(tǒng)一異常處理
目錄
文章目錄
0、我們先來回顧?下最初?戶登錄驗證的實現(xiàn)方法:
一、統(tǒng)一用戶登錄權(quán)限驗證
1.1 使用原生Spring AOP 實現(xiàn)統(tǒng)一攔截
1.2 使用Spring 攔截器實現(xiàn)統(tǒng)一用戶登錄驗證
1.3 攔截器實現(xiàn)原理
1.4 實現(xiàn)原理源碼分析
二、統(tǒng)一異常處理
三、統(tǒng)一數(shù)據(jù)返回格式
3.1 為什么要統(tǒng)一數(shù)據(jù)返回格式?
3.2 統(tǒng)一數(shù)據(jù)返回格式的實現(xiàn)
結(jié)語
0、我們先來回顧?下最初用戶登錄驗證的實現(xiàn)方法:
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 某?法 1
*/
@RequestMapping("/m1")
public Object method(HttpServletRequest request) {
// 有 session 就獲取,沒有不會創(chuàng)建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 說明已經(jīng)登錄,業(yè)務(wù)處理
return true;
} else {
// 未登錄
return false;
}
}
/**
* 某?法 2
*/
@RequestMapping("/m2")
public Object method2(HttpServletRequest request) {
// 有 session 就獲取,沒有不會創(chuàng)建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 說明已經(jīng)登錄,業(yè)務(wù)處理
return true;
} else {
// 未登錄
return false;
}
}
// 其他?法...
}
從上述代碼可以看出,每個?法中都有相同的?戶登錄驗證權(quán)限,它的缺點是:
1. 每個?法中都要單獨寫?戶登錄驗證的?法,即使封裝成公共?法,也?樣要傳參調(diào)?和在?法中進(jìn)?判斷。
2. 添加控制器越多,調(diào)??戶登錄驗證的?法也越多,這樣就增加了后期的修改成本和維護(hù)成本。
3. 這些?戶登錄驗證的?法和接下來要實現(xiàn)的業(yè)務(wù)?何沒有任何關(guān)聯(lián),但每個?法中都要寫?遍。
一、統(tǒng)一用戶登錄權(quán)限驗證
1.1 使用原生Spring AOP 實現(xiàn)統(tǒng)一攔截
我們上一篇文章講解了原生AOP:【Spring】Spring AOP 初識及實現(xiàn)原理解析
以使用原生 Spring AOP 來實現(xiàn)?戶統(tǒng)?登錄驗證為例, 主要是使用前置通知和環(huán)繞通知實現(xiàn)的,具體實現(xiàn)如下:
package com.example.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component // 隨著框架的啟動而啟動
@Aspect // 告訴框架我是一個切面類
public class UserAspect {
// 定義切點(配置攔截規(guī)則)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){
}
@Before("pointcut()")
public void beforeAdvice(){
System.out.println("執(zhí)行了前置通知~");
}
@After("pointcut()")
public void AfterAdvice(){
System.out.println("執(zhí)行了后置通知~");
}
/**
* 環(huán)繞通知
* @param joinPoint
* @return
*/
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint){
System.out.println("進(jìn)入了環(huán)繞通知~");
Object obj = null;
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("退出了環(huán)繞通知~");
return obj;
}
}
從上面代碼可以看出,使用原生的 Spring AOP 實現(xiàn)統(tǒng)一攔截的難點比較復(fù)雜,不易實現(xiàn)。定義攔截規(guī)則非常困難。如注冊?法和登錄?法是不攔截的,這樣的話排除?法的規(guī)則很難定義,甚?沒辦法定義。而且在切面類中拿到 HttpSession 比較難。
為了解決Spring AOP的問題,Spring 提供了攔截器~
1.2 使用Spring 攔截器實現(xiàn)統(tǒng)一用戶登錄驗證
要使用Spring 攔截器,需要先創(chuàng)建一個實現(xiàn)了 HandlerInterceptor 接口的攔截器類,該接口定義了三個方法:preHandle、postHandle和afterCompletion。
- preHandle方法在請求到達(dá)控制器之前執(zhí)行,可以用于進(jìn)行身份驗證、參數(shù)校驗等;
- postHandle方法在控制器處理完請求后執(zhí)行,可以對模型和視圖進(jìn)行操作;
- afterCompletion方法在視圖渲染完成后執(zhí)行,用于清理資源或記錄日志。
攔截器的實現(xiàn)分為以下兩個步驟:
1. 創(chuàng)建?定義攔截器,實現(xiàn) HandlerInterceptor 接?的 preHandle(執(zhí)?具體?法之前的預(yù)處理)方法。
2. 將?定義攔截器加入?WebMvcConfigurer 的 addInterceptors 方法中
具體實現(xiàn)如下:
1.先創(chuàng)建自定義攔截器
package com.example.demo.config;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
/**
* 此方法返回一個 boolean,如果為 true 表示驗證成功,可以繼續(xù)執(zhí)行后續(xù)流程
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用戶登錄業(yè)務(wù)判斷
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null){
// 說明用戶已經(jīng)登錄
return true;
}
// 可以跳轉(zhuǎn)到登錄頁面 or 返回一個 401 / 403 沒有權(quán)限碼
response.sendRedirect("/login.html");
// response.setStatus(403);
return false;
}
}
2.配置攔截器并設(shè)置攔截規(guī)則:
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 攔截所有請求
.excludePathPatterns("/user/login") // 排除的url地址
.excludePathPatterns("/user/reg")
.excludePathPatterns("/**/*.html");
}
}
1.3 攔截器實現(xiàn)原理
當(dāng)有了攔截器后,會在調(diào)用 Controller 之前進(jìn)行相應(yīng)的業(yè)務(wù)處理,執(zhí)行的流程如下圖所示:
1.4 實現(xiàn)原理源碼分析
所有的 Controller 執(zhí)?都會通過?個調(diào)度器 DispatcherServlet 來實現(xiàn),這?點可以從 Spring Boot 控制臺的打印信息看出,如下圖所示:
?而所有的方法都會執(zhí)行 DispatcherServlet 中的 doDispatch 調(diào)度方法,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;
}
}
// 調(diào)?預(yù)處理【重點】
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 執(zhí)? Controller 中的業(yè)務(wù)
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);
}
}
}
從上述源碼可以看出在開始執(zhí)? Controller 之前,會先調(diào)? 預(yù)處理?法 applyPreHandle,?
applyPreHandle ?法的實現(xiàn)源碼如下:
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 并執(zhí)?攔截器中的 preHandle ?法,這樣就會我們前?定義的攔截器對應(yīng)上了,如下圖所示:
?此時,相應(yīng)的preHandle中的業(yè)務(wù)邏輯就會執(zhí)行。
二、統(tǒng)一異常處理
統(tǒng)一異常處理是指在應(yīng)用程序中定義一個公共的異常處理機(jī)制,用來處理所有的異常情況。
本文講述的統(tǒng)一異常處理使用的是 @ControllerAdvice + @ExceptionHandler 來實現(xiàn)的:
- @ControllerAdvice 表示控制器通知類。
- @ExceptionHandler 異常處理器。
以上兩個注解組合使用,表示當(dāng)出現(xiàn)異常的時候執(zhí)行某個通知,即執(zhí)行某個方法事件,具體實現(xiàn)代碼如下:
package com.example.demo.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
/**
* @author xyk的電腦
* @version 1.0
* @description: TODO
* @date 2023/7/15 18:49
*/
@ControllerAdvice
@ResponseBody
public class MyExHandler {
/**
* 攔截所有的空指針異常,進(jìn)行統(tǒng)一的數(shù)據(jù)返回了
* @param e
* @return
*/
@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> nullException(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code",-1);
result.put("msg","空指針異常:" + e.getMessage()); // 錯誤碼的描述信息
result.put("data",null);
return result;
}
@ExceptionHandler(Exception.class)
public HashMap<String,Object> exception(Exception e){
HashMap<String,Object> result = new HashMap<>();
result.put("code",-1);
result.put("msg","異常:" + e.getMessage()); // 錯誤碼的描述信息
result.put("data",null);
return result;
}
}
上述代碼中,實現(xiàn)了對所有空指針異常和所有異常的保底措施的攔截并進(jìn)行統(tǒng)一的數(shù)據(jù)返回。
三、統(tǒng)一數(shù)據(jù)返回格式
3.1 為什么要統(tǒng)一數(shù)據(jù)返回格式?
統(tǒng)?數(shù)據(jù)返回格式的優(yōu)點有很多,?如以下幾個:
1. ?便前端程序員更好的接收和解析后端數(shù)據(jù)接口返回的數(shù)據(jù)。
2. 降低前端程序員和后端程序員的溝通成本,按照某個格式實現(xiàn)就?了,因為所有接?都是這樣返回的。
3. 有利于項?統(tǒng)?數(shù)據(jù)的維護(hù)和修改。
4. 有利于后端技術(shù)部?的統(tǒng)?規(guī)范的標(biāo)準(zhǔn)制定,不會出現(xiàn)稀奇古怪的返回內(nèi)容
3.2 統(tǒng)一數(shù)據(jù)返回格式的實現(xiàn)
統(tǒng)?的數(shù)據(jù)返回格式可以使? @ControllerAdvice + ResponseBodyAdvice 的?式實現(xiàn),具體實現(xiàn)代碼如下:
1.創(chuàng)建一個類,并添加@ControllerAdvice注解
2.實現(xiàn)ResponseBodyAdvice接口,并重寫 supports 和 beforeBodyWrite方法
package com.example.demo.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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;
import java.util.HashMap;
/**
* @author xyk的電腦
* @version 1.0
* @description: TODO
* @date 2023/7/15 19:18
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* 此方法返回 true 則執(zhí)行下面 beforeBodyWrite 方法
* 反之則不執(zhí)行
*/
@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("code",200);
result.put("msg","");
result.put("data",body);
if (body instanceof String){
// 需要特殊處理,因為 String 在轉(zhuǎn)換的時候會報錯
try {
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return result;
}
}
注意,如果返回的 body 原始數(shù)據(jù)類型是 String ,則會出現(xiàn)類型轉(zhuǎn)化異常,因為當(dāng)我們返回給前端Spring引用類型時,Spring的渲染引擎還沒有加載好就已經(jīng)返回給前端了,會出現(xiàn)ClassCastException
。因此,如果原始返回數(shù)據(jù)類型為 String ,則需要使用 jackson 進(jìn)行單獨處理。
結(jié)語
創(chuàng)作不易,如果你有任何問題,歡迎評論區(qū)討論哦~~?文章來源:http://www.zghlxwxcb.cn/news/detail-634149.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-634149.html
到了這里,關(guān)于【Spring Boot】攔截器與統(tǒng)一功能處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!